From 073b9c4003e67ea116fdfd3148858e492f136e35 Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Thu, 13 Jul 2023 09:54:58 +0900 Subject: [PATCH 01/17] =?UTF-8?q?[fix]=20:=20coverage-exclude.luffy=20?= =?UTF-8?q?=EA=B0=9C=ED=96=89=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#339)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coverage-exclude.luffy | 1 - 1 file changed, 1 deletion(-) diff --git a/coverage-exclude.luffy b/coverage-exclude.luffy index 69e152d5..e6f508f6 100644 --- a/coverage-exclude.luffy +++ b/coverage-exclude.luffy @@ -56,5 +56,4 @@ exclude me/nalab/survey/web/adaptor/findtarget/* exclude me/nalab/auth/mock/interceptor/* exclude me/nalab/survey/web/adaptor/updatetarget/* exclude me/nalab/survey/web/adaptor/findfeedback/formtype/* - exclude me/nalab/api/sentry/* From 461c2041f8d5f7e416e4597049b7b5bd29694698 Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Sun, 16 Jul 2023 23:30:53 +0900 Subject: [PATCH 02/17] =?UTF-8?q?[test]=20:=20survey=20=EC=A1=B4=EC=9E=AC?= =?UTF-8?q?=20=EC=9C=A0=EB=AC=B4=20=ED=99=95=EC=9D=B8=20=EC=9D=B8=EC=88=98?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(#345)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../survey/AbstractSurveyTestSupporter.java | 9 +++ .../survey/SurveyAcceptanceValidator.java | 17 ++++ .../exists/SurveyExistsAcceptanceTest.java | 77 +++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/exists/SurveyExistsAcceptanceTest.java diff --git a/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/AbstractSurveyTestSupporter.java b/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/AbstractSurveyTestSupporter.java index 0d866c73..dfa5a30c 100644 --- a/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/AbstractSurveyTestSupporter.java +++ b/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/AbstractSurveyTestSupporter.java @@ -53,6 +53,15 @@ protected ResultActions findTargetBySurveyId(Long survey_Id) throws Exception { ); } + protected ResultActions existsSurveyByToken(String token) throws Exception { + return mockMvc.perform(MockMvcRequestBuilders + .get(API_VERSION + "/surveys/exists") + .header("Authorization", token) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + } + @Autowired final void setMockMvc(MockMvc mockMvc) { this.mockMvc = mockMvc; diff --git a/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/SurveyAcceptanceValidator.java b/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/SurveyAcceptanceValidator.java index 7deb2984..561e07f3 100644 --- a/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/SurveyAcceptanceValidator.java +++ b/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/SurveyAcceptanceValidator.java @@ -84,4 +84,21 @@ public static void assertIsTargetFound(ResultActions resultActions) throws Excep ); } + + public static void assertIsSurveyExists(ResultActions resultActions) throws Exception { + resultActions.andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.exists").value(true) + ); + } + + public static void assertIsSurveyDoesNotExists(ResultActions resultActions) throws Exception { + resultActions.andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.exists").value(false) + ); + } + } diff --git a/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/exists/SurveyExistsAcceptanceTest.java b/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/exists/SurveyExistsAcceptanceTest.java new file mode 100644 index 00000000..c1d8a166 --- /dev/null +++ b/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/exists/SurveyExistsAcceptanceTest.java @@ -0,0 +1,77 @@ +package me.nalab.luffy.api.acceptance.test.survey.exists; + +import java.time.Instant; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.ResultActions; + +import me.nalab.auth.mock.api.MockUserRegisterEvent; +import me.nalab.luffy.api.acceptance.test.TargetInitializer; +import me.nalab.luffy.api.acceptance.test.survey.AbstractSurveyTestSupporter; +import me.nalab.luffy.api.acceptance.test.survey.RequestSample; +import me.nalab.luffy.api.acceptance.test.survey.SurveyAcceptanceValidator; + +@SpringBootTest +@AutoConfigureMockMvc +@TestPropertySource("classpath:h2.properties") +@ComponentScan("me.nalab") +@EnableJpaRepositories(basePackages = "me.nalab") +@EntityScan(basePackages = "me.nalab") +class SurveyExistsAcceptanceTest extends AbstractSurveyTestSupporter { + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + private TargetInitializer targetInitializer; + + @Test + @DisplayName("token에 해당하는 survey가 존재한다면, true를 반환한다.") + void RETURN_TRUE_IF_SURVEY_EXISTS() throws Exception { + // given + String token = "luffy's-double-token"; + Long targetId = targetInitializer.saveTargetAndGetId("devxb", Instant.now()); + applicationEventPublisher.publishEvent(MockUserRegisterEvent.builder() + .expectedToken(token) + .expectedId(targetId) + .build() + ); + + createSurvey(token, RequestSample.DEFAULT_JSON); + + // when + ResultActions result = existsSurveyByToken(token); + + // then + SurveyAcceptanceValidator.assertIsSurveyExists(result); + } + + @Test + @DisplayName("token에 해당하는 survey가 존재하지 않는다면, false를 반환한다.") + void RETURN_FALSE_IF_SURVEY_DOES_NOT_EXISTS() throws Exception { + // given + String token = "luffy's-double-token"; + Long targetId = targetInitializer.saveTargetAndGetId("devxb", Instant.now()); + applicationEventPublisher.publishEvent(MockUserRegisterEvent.builder() + .expectedToken(token) + .expectedId(targetId) + .build() + ); + + // when + ResultActions result = existsSurveyByToken(token); + + // then + SurveyAcceptanceValidator.assertIsSurveyDoesNotExists(result); + } + +} From 4e151bd860107cfdc9ae2fa23347004ce6d6ef53 Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Mon, 17 Jul 2023 16:04:58 +0900 Subject: [PATCH 03/17] =?UTF-8?q?[feat]=20:=20survey=20=EC=A1=B4=EC=9E=AC?= =?UTF-8?q?=20=EC=9C=A0=EB=AC=B4=20=ED=99=95=EC=9D=B8=20`Use=20case`=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/existsurvey/SurveyExistUseCase.java | 16 ++++++ .../existsurvey/SurveyExistPort.java | 15 +++++ .../existsurvey/SurveyExistService.java | 21 +++++++ .../existsurvey/SurveyExistServiceTest.java | 56 +++++++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/existsurvey/SurveyExistUseCase.java create mode 100644 survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/existsurvey/SurveyExistPort.java create mode 100644 survey/survey-application/src/main/java/me/nalab/survey/application/service/existsurvey/SurveyExistService.java create mode 100644 survey/survey-application/src/test/java/me/nalab/survey/application/service/existsurvey/SurveyExistServiceTest.java diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/existsurvey/SurveyExistUseCase.java b/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/existsurvey/SurveyExistUseCase.java new file mode 100644 index 00000000..9d30f184 --- /dev/null +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/existsurvey/SurveyExistUseCase.java @@ -0,0 +1,16 @@ +package me.nalab.survey.application.port.in.web.existsurvey; + +/** + * Survey의 존재 유무를 확인하는 인터페이스 입니다. + */ +public interface SurveyExistUseCase { + + /** + * targetId를 받아, survey의 존재유무를 반환합니다. + * + * @param targetId survey를 확인할 target의 id + * @return boolean 존재한다면 true, 없다면 false + */ + boolean isSurveyExistByTargetId(Long targetId); + +} diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/existsurvey/SurveyExistPort.java b/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/existsurvey/SurveyExistPort.java new file mode 100644 index 00000000..341869d7 --- /dev/null +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/existsurvey/SurveyExistPort.java @@ -0,0 +1,15 @@ +package me.nalab.survey.application.port.out.persistence.existsurvey; + +/** + * Survey가 존재하는지 확인하는 인터페이스 입니다. + */ +public interface SurveyExistPort { + + /** + * targetId 를 인자로 받아, survey가 저장되어 있다면 true, 아니라면 false를 반환하는 인터페이스 입니다. + * + * @param targetId 조회할 survey에 연결된 target-id 입니다. + * @return boolean survey가 있다면 true, 없다면 false를 반환해야 합니다. + */ + boolean isSurveyExistByTargetId(Long targetId); +} diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/service/existsurvey/SurveyExistService.java b/survey/survey-application/src/main/java/me/nalab/survey/application/service/existsurvey/SurveyExistService.java new file mode 100644 index 00000000..12f16fec --- /dev/null +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/service/existsurvey/SurveyExistService.java @@ -0,0 +1,21 @@ +package me.nalab.survey.application.service.existsurvey; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import me.nalab.survey.application.port.in.web.existsurvey.SurveyExistUseCase; +import me.nalab.survey.application.port.out.persistence.existsurvey.SurveyExistPort; + +@Service +@RequiredArgsConstructor +public class SurveyExistService implements SurveyExistUseCase { + + private final SurveyExistPort surveyExistPort; + + @Override + @Transactional(readOnly = true) + public boolean isSurveyExistByTargetId(Long targetId) { + return surveyExistPort.isSurveyExistByTargetId(targetId); + } +} diff --git a/survey/survey-application/src/test/java/me/nalab/survey/application/service/existsurvey/SurveyExistServiceTest.java b/survey/survey-application/src/test/java/me/nalab/survey/application/service/existsurvey/SurveyExistServiceTest.java new file mode 100644 index 00000000..058aad40 --- /dev/null +++ b/survey/survey-application/src/test/java/me/nalab/survey/application/service/existsurvey/SurveyExistServiceTest.java @@ -0,0 +1,56 @@ +package me.nalab.survey.application.service.existsurvey; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import me.nalab.survey.application.port.in.web.existsurvey.SurveyExistUseCase; +import me.nalab.survey.application.port.out.persistence.existsurvey.SurveyExistPort; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = {SurveyExistService.class}) +class SurveyExistServiceTest { + + @MockBean + private SurveyExistPort surveyExistPort; + + @Autowired + private SurveyExistUseCase surveyExistUseCase; + + @Test + @DisplayName("surveyExistsUseCase는 targetId에 해당하는 survey가 존재한다면, true를 반환한다.") + void RETURN_TRUE_IF_SURVEY_EXISTS() { + // given + Long targetId = 1L; + + Mockito.when(surveyExistPort.isSurveyExistByTargetId(targetId)).thenReturn(true); + + // when + boolean result = surveyExistUseCase.isSurveyExistByTargetId(targetId); + + // then + Assertions.assertThat(result).isTrue(); + } + + @Test + @DisplayName("surveyExistsUseCase는 targetId에 해당하는 survey가 존재하지 않는다면, false를 반환한다.") + void RETURN_FALSE_IF_SURVEY_DOES_NOT_EXISTS() { + // given + Long targetId = 1L; + + Mockito.when(surveyExistPort.isSurveyExistByTargetId(targetId)).thenReturn(false); + + // when + boolean result = surveyExistUseCase.isSurveyExistByTargetId(targetId); + + // then + Assertions.assertThat(result).isFalse(); + } + +} From 6b2330eff21ef1a0a163dd91095e2de46d860941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=88=98=EC=A7=80=EB=8B=88?= <71487608+ssssujini99@users.noreply.github.com> Date: Mon, 17 Jul 2023 17:00:20 +0900 Subject: [PATCH 04/17] =?UTF-8?q?[feat]=20:=20survey=20=EC=A1=B4=EC=9E=AC?= =?UTF-8?q?=20=EC=9C=A0=EB=AC=B4=20=ED=99=95=EC=9D=B8=20-=20`jpa-adaptor`?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20(#349)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/existsurvey/SurveyExistUseCase.java | 2 +- .../existsurvey/SurveyExistService.java | 2 +- .../src/main/java/module-info.java | 1 + .../existsurvey/SurveyExistServiceTest.java | 4 +- .../existsurvey/SurveyExistAdaptor.java | 26 ++++++ .../repository/SurveyFindRepository.java | 11 +++ .../existsurvey/SurveyExistAdaptorTest.java | 80 +++++++++++++++++++ .../existsurvey/TestSurveyFindRepository.java | 10 +++ .../existsurvey/TestTargetRepository.java | 9 +++ 9 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/existsurvey/SurveyExistAdaptor.java create mode 100644 survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/existsurvey/repository/SurveyFindRepository.java create mode 100644 survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/existsurvey/SurveyExistAdaptorTest.java create mode 100644 survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/existsurvey/TestSurveyFindRepository.java create mode 100644 survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/existsurvey/TestTargetRepository.java diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/existsurvey/SurveyExistUseCase.java b/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/existsurvey/SurveyExistUseCase.java index 9d30f184..fc4b31c2 100644 --- a/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/existsurvey/SurveyExistUseCase.java +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/existsurvey/SurveyExistUseCase.java @@ -11,6 +11,6 @@ public interface SurveyExistUseCase { * @param targetId survey를 확인할 target의 id * @return boolean 존재한다면 true, 없다면 false */ - boolean isSurveyExistByTargetId(Long targetId); + boolean existSurveyByTargetId(Long targetId); } diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/service/existsurvey/SurveyExistService.java b/survey/survey-application/src/main/java/me/nalab/survey/application/service/existsurvey/SurveyExistService.java index 12f16fec..baa876a5 100644 --- a/survey/survey-application/src/main/java/me/nalab/survey/application/service/existsurvey/SurveyExistService.java +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/service/existsurvey/SurveyExistService.java @@ -15,7 +15,7 @@ public class SurveyExistService implements SurveyExistUseCase { @Override @Transactional(readOnly = true) - public boolean isSurveyExistByTargetId(Long targetId) { + public boolean existSurveyByTargetId(Long targetId) { return surveyExistPort.isSurveyExistByTargetId(targetId); } } diff --git a/survey/survey-application/src/main/java/module-info.java b/survey/survey-application/src/main/java/module-info.java index 0528180f..37ae9a49 100644 --- a/survey/survey-application/src/main/java/module-info.java +++ b/survey/survey-application/src/main/java/module-info.java @@ -36,6 +36,7 @@ exports me.nalab.survey.application.port.in.web.findtarget; exports me.nalab.survey.application.port.in.web.findfeedback.formtype; exports me.nalab.survey.application.port.out.persistence.findfeedback.formtype; + exports me.nalab.survey.application.port.out.persistence.existsurvey; requires luffy.survey.domain.main; requires luffy.core.id.generator.id.generator.starter.main; diff --git a/survey/survey-application/src/test/java/me/nalab/survey/application/service/existsurvey/SurveyExistServiceTest.java b/survey/survey-application/src/test/java/me/nalab/survey/application/service/existsurvey/SurveyExistServiceTest.java index 058aad40..330e79e4 100644 --- a/survey/survey-application/src/test/java/me/nalab/survey/application/service/existsurvey/SurveyExistServiceTest.java +++ b/survey/survey-application/src/test/java/me/nalab/survey/application/service/existsurvey/SurveyExistServiceTest.java @@ -32,7 +32,7 @@ void RETURN_TRUE_IF_SURVEY_EXISTS() { Mockito.when(surveyExistPort.isSurveyExistByTargetId(targetId)).thenReturn(true); // when - boolean result = surveyExistUseCase.isSurveyExistByTargetId(targetId); + boolean result = surveyExistUseCase.existSurveyByTargetId(targetId); // then Assertions.assertThat(result).isTrue(); @@ -47,7 +47,7 @@ void RETURN_FALSE_IF_SURVEY_DOES_NOT_EXISTS() { Mockito.when(surveyExistPort.isSurveyExistByTargetId(targetId)).thenReturn(false); // when - boolean result = surveyExistUseCase.isSurveyExistByTargetId(targetId); + boolean result = surveyExistUseCase.existSurveyByTargetId(targetId); // then Assertions.assertThat(result).isFalse(); diff --git a/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/existsurvey/SurveyExistAdaptor.java b/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/existsurvey/SurveyExistAdaptor.java new file mode 100644 index 00000000..a6e47ebd --- /dev/null +++ b/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/existsurvey/SurveyExistAdaptor.java @@ -0,0 +1,26 @@ +package me.nalab.survey.jpa.adaptor.existsurvey; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Repository; + +import me.nalab.survey.application.port.out.persistence.existsurvey.SurveyExistPort; +import me.nalab.survey.jpa.adaptor.existsurvey.repository.SurveyFindRepository; + +@Repository("existsurvey.SurveyExistAdaptor") +public class SurveyExistAdaptor implements SurveyExistPort { + + private final SurveyFindRepository surveyFindRepository; + + @Autowired + SurveyExistAdaptor( + @Qualifier("existsurvey.SurveyFindRepository") SurveyFindRepository surveyFindRepository) { + this.surveyFindRepository = surveyFindRepository; + } + + @Override + public boolean isSurveyExistByTargetId(Long targetId) { + return surveyFindRepository.existsByTargetId(targetId); + } + +} diff --git a/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/existsurvey/repository/SurveyFindRepository.java b/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/existsurvey/repository/SurveyFindRepository.java new file mode 100644 index 00000000..2e74543b --- /dev/null +++ b/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/existsurvey/repository/SurveyFindRepository.java @@ -0,0 +1,11 @@ +package me.nalab.survey.jpa.adaptor.existsurvey.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import me.nalab.core.data.survey.SurveyEntity; + +@Repository("existsurvey.SurveyFindRepository") +public interface SurveyFindRepository extends JpaRepository { + boolean existsByTargetId(Long targetId); +} diff --git a/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/existsurvey/SurveyExistAdaptorTest.java b/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/existsurvey/SurveyExistAdaptorTest.java new file mode 100644 index 00000000..9a28665f --- /dev/null +++ b/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/existsurvey/SurveyExistAdaptorTest.java @@ -0,0 +1,80 @@ +package me.nalab.survey.jpa.adaptor.existsurvey; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.Instant; + +import javax.persistence.EntityManager; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; + +import me.nalab.core.data.survey.SurveyEntity; +import me.nalab.core.data.target.TargetEntity; +import me.nalab.survey.domain.survey.Survey; +import me.nalab.survey.jpa.adaptor.RandomSurveyFixture; +import me.nalab.survey.jpa.adaptor.common.mapper.SurveyEntityMapper; + +@DataJpaTest +@EnableJpaRepositories +@EntityScan("me.nalab.core.data") +@ContextConfiguration(classes = SurveyExistAdaptor.class) +@TestPropertySource("classpath:h2.properties") +class SurveyExistAdaptorTest { + + @Autowired + private SurveyExistAdaptor surveyExistAdaptor; + + @Autowired + private TestSurveyFindRepository testSurveyFindRepository; + + @Autowired + private TestTargetRepository testTargetRepository; + + @Autowired + private EntityManager entityManager; + + @Test + @DisplayName("targetID에 해당하는 survey가 존재하는 경우") + void FIND_SURVEY_WITH_TARGET_ID_WITH_SURVEY() { + + TargetEntity targetEntity = getTargetEntity(); + Survey survey = RandomSurveyFixture.createRandomSurvey(); + SurveyEntity surveyEntity = SurveyEntityMapper.toSurveyEntity(targetEntity.getId(), survey); + + testTargetRepository.saveAndFlush(targetEntity); + testSurveyFindRepository.saveAndFlush(surveyEntity); + entityManager.clear(); + + boolean result = testSurveyFindRepository.existsByTargetId(targetEntity.getId()); + assertTrue(result); + } + + @Test + @DisplayName("targetID에 해당하는 survey가 존재하지 않는 경우") + void FIND_SURVEY_WITH_TARGET_ID_WITH_NO_SURVEY() { + + TargetEntity targetEntity = getTargetEntity(); + + testTargetRepository.saveAndFlush(targetEntity); + entityManager.clear(); + + boolean result = testSurveyFindRepository.existsByTargetId(targetEntity.getId()); + assertFalse(result); + } + + private TargetEntity getTargetEntity() { + return TargetEntity.builder() + .id(1L) + .createdAt(Instant.now()) + .updatedAt(Instant.now()) + .nickname("nalab") + .build(); + } +} diff --git a/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/existsurvey/TestSurveyFindRepository.java b/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/existsurvey/TestSurveyFindRepository.java new file mode 100644 index 00000000..b141e4ed --- /dev/null +++ b/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/existsurvey/TestSurveyFindRepository.java @@ -0,0 +1,10 @@ +package me.nalab.survey.jpa.adaptor.existsurvey; + +import org.springframework.data.jpa.repository.JpaRepository; + +import me.nalab.core.data.survey.SurveyEntity; + +public interface TestSurveyFindRepository extends JpaRepository { + + boolean existsByTargetId(Long targetId); +} diff --git a/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/existsurvey/TestTargetRepository.java b/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/existsurvey/TestTargetRepository.java new file mode 100644 index 00000000..8197478d --- /dev/null +++ b/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/existsurvey/TestTargetRepository.java @@ -0,0 +1,9 @@ +package me.nalab.survey.jpa.adaptor.existsurvey; + +import org.springframework.data.jpa.repository.JpaRepository; + +import me.nalab.core.data.target.TargetEntity; + +public interface TestTargetRepository extends JpaRepository { + +} From 3a4815473819796fa47ca8d1a83ae9c40f55fba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=88=98=EC=A7=80=EB=8B=88?= <71487608+ssssujini99@users.noreply.github.com> Date: Mon, 17 Jul 2023 17:40:14 +0900 Subject: [PATCH 05/17] =?UTF-8?q?[feat]=20:=20survey=20=EC=A1=B4=EC=9E=AC?= =?UTF-8?q?=20=EC=9C=A0=EB=AC=B4=20=ED=99=95=EC=9D=B8=20-=20`web-adaptor`?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C=20(#350)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../JwtDecryptInterceptorConfigurer.java | 1 + .../auth/mock/config/MockAuthConfigurer.java | 1 + coverage-exclude.luffy | 1 + .../src/main/java/module-info.java | 1 + .../existsurvey/SurveyExistController.java | 28 +++++++++++++++++++ .../response/SurveyExistResponse.java | 16 +++++++++++ 6 files changed, 48 insertions(+) create mode 100644 survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/existsurvey/SurveyExistController.java create mode 100644 survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/existsurvey/response/SurveyExistResponse.java diff --git a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java index 7d9dfdd9..96a2a043 100644 --- a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java +++ b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java @@ -16,6 +16,7 @@ public class JwtDecryptInterceptorConfigurer implements WebMvcConfigurer { private static final String[] INTERCEPTOR_URLS = { "/v1/surveys", + "/v1/surveys/exists", "/v1/surveys-id", "/v1/users", "/v1/questions", diff --git a/auth/auth-mock/src/main/java/me/nalab/auth/mock/config/MockAuthConfigurer.java b/auth/auth-mock/src/main/java/me/nalab/auth/mock/config/MockAuthConfigurer.java index 0f26147c..483b0c74 100644 --- a/auth/auth-mock/src/main/java/me/nalab/auth/mock/config/MockAuthConfigurer.java +++ b/auth/auth-mock/src/main/java/me/nalab/auth/mock/config/MockAuthConfigurer.java @@ -13,6 +13,7 @@ public class MockAuthConfigurer implements WebMvcConfigurer { private static final String[] INTERCEPTOR_URLS = { "/v1/surveys", + "/v1/surveys/exists", "/v1/users", "/v1/surveys-id", "/v1/questions", diff --git a/coverage-exclude.luffy b/coverage-exclude.luffy index e6f508f6..4a80f8d0 100644 --- a/coverage-exclude.luffy +++ b/coverage-exclude.luffy @@ -57,3 +57,4 @@ exclude me/nalab/auth/mock/interceptor/* exclude me/nalab/survey/web/adaptor/updatetarget/* exclude me/nalab/survey/web/adaptor/findfeedback/formtype/* exclude me/nalab/api/sentry/* +exclude me/nalab/survey/web/adaptor/existsurvey/* diff --git a/survey/survey-application/src/main/java/module-info.java b/survey/survey-application/src/main/java/module-info.java index 37ae9a49..74682830 100644 --- a/survey/survey-application/src/main/java/module-info.java +++ b/survey/survey-application/src/main/java/module-info.java @@ -37,6 +37,7 @@ exports me.nalab.survey.application.port.in.web.findfeedback.formtype; exports me.nalab.survey.application.port.out.persistence.findfeedback.formtype; exports me.nalab.survey.application.port.out.persistence.existsurvey; + exports me.nalab.survey.application.port.in.web.existsurvey; requires luffy.survey.domain.main; requires luffy.core.id.generator.id.generator.starter.main; diff --git a/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/existsurvey/SurveyExistController.java b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/existsurvey/SurveyExistController.java new file mode 100644 index 00000000..04651db9 --- /dev/null +++ b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/existsurvey/SurveyExistController.java @@ -0,0 +1,28 @@ +package me.nalab.survey.web.adaptor.existsurvey; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import me.nalab.survey.application.port.in.web.existsurvey.SurveyExistUseCase; +import me.nalab.survey.web.adaptor.existsurvey.response.SurveyExistResponse; + +@RestController +@RequestMapping("/v1") +@RequiredArgsConstructor +public class SurveyExistController { + + private final SurveyExistUseCase surveyExistUseCase; + + @GetMapping("/surveys/exists") + @ResponseStatus(HttpStatus.OK) + public SurveyExistResponse existSurveyByTargetId(@RequestAttribute("logined") Long loginId) { + boolean isSurveyExists = surveyExistUseCase.existSurveyByTargetId(loginId); + return new SurveyExistResponse(isSurveyExists); + } + +} diff --git a/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/existsurvey/response/SurveyExistResponse.java b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/existsurvey/response/SurveyExistResponse.java new file mode 100644 index 00000000..e83fa5a5 --- /dev/null +++ b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/existsurvey/response/SurveyExistResponse.java @@ -0,0 +1,16 @@ +package me.nalab.survey.web.adaptor.existsurvey.response; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor +public class SurveyExistResponse { + + private final boolean exists; + +} From f6f144fc7722af7214efcf1b56eb44019ddda0d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=88=98=EC=A7=80=EB=8B=88?= <71487608+ssssujini99@users.noreply.github.com> Date: Mon, 17 Jul 2023 18:04:47 +0900 Subject: [PATCH 06/17] =?UTF-8?q?[test]=20:=20`survey=20=EC=A1=B4=EC=9E=AC?= =?UTF-8?q?=20=EC=9C=A0=EB=AC=B4=20=ED=99=95=EC=9D=B8`=20e2e=20test=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#351)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- support/e2e/v1_5_target.hurl | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/support/e2e/v1_5_target.hurl b/support/e2e/v1_5_target.hurl index e0a9edff..fbb9c87a 100644 --- a/support/e2e/v1_5_target.hurl +++ b/support/e2e/v1_5_target.hurl @@ -17,6 +17,17 @@ auth_token: jsonpath "$.access_token" ########## +GET http://nalab-server:8080/v1/surveys/exists # 질문폼이 없는 상태에서 질문폼 존재 유무 확인 -> false +Authorization: {{ token_type }} {{ auth_token }} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.exists" == false + +########## + POST http://nalab-server:8080/v1/surveys # 발급받은 토큰으로 survey를 생성한다 Authorization: {{ token_type }} {{ auth_token }} { @@ -63,6 +74,17 @@ survey_id: jsonpath "$.survey_id" ########## +GET http://nalab-server:8080/v1/surveys/exists # 질문폼이 있는 상태에서 질문폼 존재 유무 확인 -> true +Authorization: {{ token_type }} {{ auth_token }} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.exists" == true + +########## + PATCH http://nalab-server:8080/v1/users # 로그인된 타겟의 정보를 수정한다 Authorization: {{ token_type }} {{ auth_token }} { From bb9e0ef74b7ba8ad92cefb47e09a9f9fe2ee8c42 Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Thu, 20 Jul 2023 00:37:42 +0900 Subject: [PATCH 07/17] =?UTF-8?q?[chore]=20:=20HURL=20verison=204.0.0?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=97=85=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=20(#358)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/e2e.yml | 8 ++++++++ support/e2e/Dockerfile | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a40d1ea3..3b9df93f 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -13,19 +13,27 @@ jobs: steps: - name: checkout uses: actions/checkout@v3 + - name: docker setup uses: docker-practice/actions-setup-docker@master + - name: docker network setup run: docker network create e2e-net + - name: run dbms run: docker run --rm -it --name database --network e2e-net -e MYSQL_ROOT_PASSWORD=0000 -e MYSQL_DATABASE=luffy -d mysql:8.0.33 + - name: build nalab-server run: ./gradlew clean build + - name: build nalab-server-docker-image run: docker build --tag luffy:e2e --build-arg DB_URL=jdbc:mysql://database:3306/luffy --build-arg DB_USERNAME=root --build-arg DB_PASSWORD=0000 --build-arg JWT_SECRET=fore2e . + - name: run nalab-server run: docker run --rm -it --name nalab-server --network e2e-net -d luffy:e2e + - name: build hurl image run: docker build --tag hurl:e2e support/e2e/ + - name: e2e test run: docker run --rm --network e2e-net hurl:e2e diff --git a/support/e2e/Dockerfile b/support/e2e/Dockerfile index b6969b14..c7c2d65d 100644 --- a/support/e2e/Dockerfile +++ b/support/e2e/Dockerfile @@ -10,7 +10,7 @@ RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSI RUN apt-get install -y curl -RUN curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/3.0.1/hurl_3.0.1_amd64.deb -RUN apt-get update && apt-get install -y ./hurl_3.0.1_amd64.deb +RUN curl -k --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/4.0.0/hurl_4.0.0_amd64.deb +RUN apt-get update && apt-get install -y ./hurl_4.0.0_amd64.deb ENTRYPOINT dockerize -wait tcp://nalab-server:8080 -timeout 300s && hurl --very-verbose --color --test hurls/*.hurl From 2be4b70ac4e3d7d4cdb8aaa66c04fa720b67dd17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=88=98=EC=A7=80=EB=8B=88?= <71487608+ssssujini99@users.noreply.github.com> Date: Thu, 20 Jul 2023 01:01:25 +0900 Subject: [PATCH 08/17] [docs] : update README.md (#356) --- README.md | 198 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 183 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 35284723..bab41ebe 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,183 @@ -# 13th-3team-server - - -[![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-white.svg)](https://sonarcloud.io/summary/new_code?id=depromeet_na-lab-server) -[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=depromeet_na-lab-server&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=depromeet_na-lab-server) -[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=depromeet_na-lab-server&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=depromeet_na-lab-server) -[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=depromeet_na-lab-server&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=depromeet_na-lab-server) -[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=depromeet_na-lab-server&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=depromeet_na-lab-server) -[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=depromeet_na-lab-server&metric=coverage)](https://sonarcloud.io/summary/new_code?id=depromeet_na-lab-server) -[![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=depromeet_na-lab-server&metric=sqale_index)](https://sonarcloud.io/summary/new_code?id=depromeet_na-lab-server) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=depromeet_na-lab-server&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=depromeet_na-lab-server) -[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=depromeet_na-lab-server&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=depromeet_na-lab-server) -[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=depromeet_na-lab-server&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=depromeet_na-lab-server) -[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=depromeet_na-lab-server&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=depromeet_na-lab-server) -[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=depromeet_na-lab-server&metric=bugs)](https://sonarcloud.io/summary/new_code?id=depromeet_na-lab-server) +# Na Lab + +> 동료의 익명 피드백을 통한 나의 커리어 브랜딩, Na Lab    • 백엔드 레포지토리 + + +
+ + + +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=depromeet_na-lab-server&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=depromeet_na-lab-server) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=depromeet_na-lab-server&metric=coverage)](https://sonarcloud.io/summary/new_code?id=depromeet_na-lab-server) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=depromeet_na-lab-server&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=depromeet_na-lab-server) +[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=depromeet_na-lab-server&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=depromeet_na-lab-server) +[![e2e test](https://github.com/depromeet/na-lab-server/actions/workflows/e2e.yml/badge.svg)](https://github.com/depromeet/na-lab-server/actions/workflows/e2e.yml) + +
+ +### 🧐 Na Lab ? + +**오직 나만을 위한 커리어 연구실, Na Lab 🧬🧪** +‘Na Lab’은 동료의 익명 피드백을 통해 나의 직무 강점을 발견하는 서비스입니다. + +### 👉 [Na Lab 바로가기](https://www.nalab.me/) + +--- + +
+ + +na lab +na lab + +> 나의 커리어 브랜딩을 완성해주는 기본질문을 통해 손쉽게 질문폼을 만들 수 있어요 +> 새로운 질문을 추가하고 싶다면 객관식, 주관식으로 자유롭게 질문을 만들어보세요! + +
+ +
+ +na lab +na lab + +> 부담스러웠던 동료 평가의 경험을 마치 친구와 심리테스트 하듯 즐겁게 할 수 있도록 설계했어요 +> 나랩의 연구를 책임지는 Dr. 왓슨 박사님과 함께 채팅으로 대화하며 익명으로 피드백을 남길 수 있어요 + +
+ +
+ + +na lab +na lab + +> 많은 사람들의 답변 속에서 정말 나에게 도움이 되는 피드백은 어느 것일까요? +> 나랩은 유저가 개별 답변에 대한 이해도를 높이며 의미 있는 피드백을 얻을 수 있도록 결과를 정리했어요 + +
+ +
+ +na lab + +> 피드백 결과를 통해 나의 커리어 연구 결과를 확인할 수 있고, +> 동료들의 피드백을 저장해 나만의 커리어 명함을 만들 수 있어요 + + +
+ +
+ +![추가이미지1](https://github.com/depromeet/na-lab-client/assets/26461307/53c6a91b-d029-4fd9-acad-647a771507e3) + +![추가이미지2](https://github.com/depromeet/na-lab-client/assets/26461307/27586832-3bd7-4cbb-a659-1e446ed996d3) + +
+ +--- + +## 😎 Develoment Description + +- 안정성과 유지보수를 위해서 단위테스트, 통합테스트, E2E 테스트를 모두 짜는 전략으로 진행 +- 테스트 커버리는 분기와 라인 커버리지를 모두 검증하였으며 ***테스트 커버리지 93.7%를 달성*** +- 특히, E2E 테스트를 통해 실제 사용자의 여러 시나리오를 테스트함으로써 애플리케이션의 무결성을 검증하고자 하였으며 + 도입 이후 2차 MVP의 QA 에서 ***버그 제로 달성*** +- 유연하고 확장가능한 서비스를 위해 멀티모듈과 헥사고날 아키텍처를 적용 +- E2E 부터 깃허브 라벨링, PR 알람 등의 가능한 모든 작업을 자동화시켜 팀의 생산성 증대 + +
+ +## 🏛️ System Architecture + +![아키텍처이미지](https://github.com/oyeon-kwon/personal_color/assets/61301574/794d7625-f63f-418f-b03a-a7ab396f015b) + +
+ +## 📚 Tech Stack + +
+
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+ +## 🧑🏻‍💻 Developers + + + + + + + + + + + + + + + + + + + + + + +
BackendBackendBackend
이준영이수진유도진
+ +
+ devxb +
+ +
+ ssssujini99 +
+ +
+ dojinyou +
+ +
+ devxb의 커리어 명함 +
+ +
+ ssssujini99의 커리어 명함 +
+ +
+ dojinyou의 커리어 명함 +
+ + +
+ + +![추가이미지3](https://github.com/depromeet/na-lab-server/assets/71487608/09a06bb1-4f06-4513-977d-e6fe49bd8f06) From aac0bc99755ece32a695797a3e72fdc9e1f66000 Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Tue, 20 Feb 2024 16:02:05 +0900 Subject: [PATCH 09/17] [chore] : JDK version migration 11 to 17 (#372) --- .github/workflows/accept-analyze.yml | 4 +- .../workflows/docker-release-publisher.yml | 4 +- .github/workflows/docker-stage-publisher.yml | 4 +- .github/workflows/e2e.yml | 13 ++++++ .github/workflows/unit-analyze.yml | 4 +- Dockerfile | 2 +- core/time/build.gradle | 3 -- .../java/me/nalab/core/time/TimeUtil.java | 44 ++++++++++++------- .../core/time/request/RequestTimeUtil.java | 44 ------------------- core/time/src/main/java/module-info.java | 5 --- .../nalab/core/time/RequestTimeUtilTest.java | 44 ------------------- .../me/nalab/core/time/TestController.java | 26 ----------- gradle.properties | 2 +- gradle/jacoco.gradle | 4 +- survey/survey-jpa-adaptor/build.gradle | 1 + .../jpa/adaptor/RandomFeedbackFixture.java | 7 ++- .../jpa/adaptor/RandomSurveyFixture.java | 4 +- .../create/SurveyCreateAdaptorTest.java | 7 ++- .../formtype/FeedbackFindAdaptorTest.java | 8 ++-- .../formtype/SurveyFindAdaptorTest.java | 6 +-- .../createsurvey/SurveyCreateController.java | 3 +- .../service/UserCreateWithOAuthService.java | 3 +- .../UserCreateWithOAuthServiceTest.java | 12 ++--- 23 files changed, 77 insertions(+), 177 deletions(-) delete mode 100644 core/time/src/main/java/me/nalab/core/time/request/RequestTimeUtil.java delete mode 100644 core/time/src/test/java/me/nalab/core/time/RequestTimeUtilTest.java delete mode 100644 core/time/src/test/java/me/nalab/core/time/TestController.java diff --git a/.github/workflows/accept-analyze.yml b/.github/workflows/accept-analyze.yml index 19103799..8d45a6f9 100644 --- a/.github/workflows/accept-analyze.yml +++ b/.github/workflows/accept-analyze.yml @@ -16,11 +16,11 @@ jobs: with: # Shallow clones should be disabled for a better relevancy of analysis fetch-depth: 0 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - name: Cache Gradle packages uses: actions/cache@v3 with: diff --git a/.github/workflows/docker-release-publisher.yml b/.github/workflows/docker-release-publisher.yml index d656b8b6..03d86d82 100644 --- a/.github/workflows/docker-release-publisher.yml +++ b/.github/workflows/docker-release-publisher.yml @@ -13,11 +13,11 @@ jobs: - name: Check out the repo uses: actions/checkout@v3 - - name: Setup opnenjdk-11 + - name: Setup opnenjdk-17 uses: actions/setup-java@v3 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - name: Setup Gradle uses: gradle/gradle-build-action@v2 diff --git a/.github/workflows/docker-stage-publisher.yml b/.github/workflows/docker-stage-publisher.yml index 18fd97eb..b33f5129 100644 --- a/.github/workflows/docker-stage-publisher.yml +++ b/.github/workflows/docker-stage-publisher.yml @@ -13,11 +13,11 @@ jobs: - name: Check out the repo uses: actions/checkout@v3 - - name: Setup opnenjdk-11 + - name: Setup opnenjdk-17 uses: actions/setup-java@v3 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - name: Setup Gradle uses: gradle/gradle-build-action@v2 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 3b9df93f..3f2f66f5 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -14,6 +14,19 @@ jobs: - name: checkout uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 17 + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: docker setup uses: docker-practice/actions-setup-docker@master diff --git a/.github/workflows/unit-analyze.yml b/.github/workflows/unit-analyze.yml index d2130c64..dea71ef1 100644 --- a/.github/workflows/unit-analyze.yml +++ b/.github/workflows/unit-analyze.yml @@ -13,11 +13,11 @@ jobs: with: # Shallow clones should be disabled for a better relevancy of analysis fetch-depth: 0 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - name: Cache Gradle packages uses: actions/cache@v3 with: diff --git a/Dockerfile b/Dockerfile index 6c81afaf..48762043 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM openjdk:11.0.11-jre-slim +FROM openjdk:17-jdk-slim ARG JAR_FILE=./api/build/libs/*-SNAPSHOT.jar ARG DB_URL diff --git a/core/time/build.gradle b/core/time/build.gradle index b7a0f612..e69de29b 100644 --- a/core/time/build.gradle +++ b/core/time/build.gradle @@ -1,3 +0,0 @@ -dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' -} diff --git a/core/time/src/main/java/me/nalab/core/time/TimeUtil.java b/core/time/src/main/java/me/nalab/core/time/TimeUtil.java index 6c42ef71..c6c25211 100644 --- a/core/time/src/main/java/me/nalab/core/time/TimeUtil.java +++ b/core/time/src/main/java/me/nalab/core/time/TimeUtil.java @@ -1,23 +1,37 @@ package me.nalab.core.time; +import java.time.Clock; import java.time.Instant; -import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; -/** - * 저장된 시간을 일간되게 반환하는 util 입니다. - */ -public interface TimeUtil { +public class TimeUtil { - /** - * 저장된 시간을 LocalDateTime으로 반환합니다. - * @return LocalDateTime - */ - LocalDateTime toLocalDateTime(); + private static Clock clock = null; - /** - * 저장된 시간을 Instant로 반환합니다. - * @return Instant - */ - Instant toInstant(); + private TimeUtil() { + throw new UnsupportedOperationException("Cannot invoke constructor \"TimeUtil()\""); + } + public static Instant toInstant() { + var current = Instant.now(); + if (clock != null) { + current = Instant.now(clock); + } + return formatTo6Digit(current); + } + + private static Instant formatTo6Digit(Instant instant) { + var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSX") + .withZone(ZoneId.of("UTC")); + return Instant.parse(formatter.format(instant)); + } + + public static void fixed(Clock clock) { + TimeUtil.clock = clock; + } + + public static void clear() { + TimeUtil.clock = null; + } } diff --git a/core/time/src/main/java/me/nalab/core/time/request/RequestTimeUtil.java b/core/time/src/main/java/me/nalab/core/time/request/RequestTimeUtil.java deleted file mode 100644 index b08cb466..00000000 --- a/core/time/src/main/java/me/nalab/core/time/request/RequestTimeUtil.java +++ /dev/null @@ -1,44 +0,0 @@ -package me.nalab.core.time.request; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; - -import org.springframework.context.annotation.Scope; -import org.springframework.context.annotation.ScopedProxyMode; -import org.springframework.stereotype.Component; - -import me.nalab.core.time.TimeUtil; - -/** - * 하나의 요청안에서 일관된 시간을 반환하는 유틸 입니다. - */ -@Component -@Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES) -public class RequestTimeUtil implements TimeUtil { - - private final Instant instant; - - public RequestTimeUtil() { - instant = Instant.now(); - } - - /** - * 요청 시간을 LocalDateTime으로 반환합니다. - * @return LocalDateTime 클라이언트의 요청이 들어온 시간 - */ - @Override - public LocalDateTime toLocalDateTime() { - return LocalDateTime.ofInstant(instant, ZoneOffset.UTC); - } - - /** - * 요청 시간을 Instant로 반환합니다. - * @return Instant 클라이언트의 요청이 들어온 시간 - */ - @Override - public Instant toInstant() { - return instant; - } - -} diff --git a/core/time/src/main/java/module-info.java b/core/time/src/main/java/module-info.java index 91486ed6..e0a5f319 100644 --- a/core/time/src/main/java/module-info.java +++ b/core/time/src/main/java/module-info.java @@ -1,9 +1,4 @@ module luffy.core.time.main { - - requires spring.context; - requires spring.beans; - exports me.nalab.core.time; - exports me.nalab.core.time.request; } diff --git a/core/time/src/test/java/me/nalab/core/time/RequestTimeUtilTest.java b/core/time/src/test/java/me/nalab/core/time/RequestTimeUtilTest.java deleted file mode 100644 index 836dbb51..00000000 --- a/core/time/src/test/java/me/nalab/core/time/RequestTimeUtilTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package me.nalab.core.time; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; - -import org.json.JSONObject; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.http.MediaType; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; - -import me.nalab.core.time.request.RequestTimeUtil; - -@WebMvcTest -@ContextConfiguration(classes = {RequestTimeUtil.class, TestController.class}) -class RequestTimeUtilTest { - - @Autowired - private MockMvc mvc; - - @Test - @DisplayName("요청마다 다른 Time Util 생성 테스트") - void CREATE_TIME_UTIL() throws Exception { - // when - long before = getRequestArrivalTime(); - long after = getRequestArrivalTime(); - - // then - assertTrue(before < after); - } - - private long getRequestArrivalTime() throws Exception { - ResultActions resultActions = mvc.perform(get("/get-time") - .accept(MediaType.APPLICATION_JSON)); - JSONObject jsonObject = new JSONObject(resultActions.andReturn().getResponse().getContentAsString()); - return jsonObject.getLong("instant"); - } - -} diff --git a/core/time/src/test/java/me/nalab/core/time/TestController.java b/core/time/src/test/java/me/nalab/core/time/TestController.java deleted file mode 100644 index e2e684f5..00000000 --- a/core/time/src/test/java/me/nalab/core/time/TestController.java +++ /dev/null @@ -1,26 +0,0 @@ -package me.nalab.core.time; - -import java.time.ZoneOffset; -import java.util.Map; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - -import lombok.RequiredArgsConstructor; - -@RestController -@RequiredArgsConstructor -class TestController { - - private final TimeUtil timeUtil; - - @GetMapping("/get-time") - @ResponseStatus(HttpStatus.OK) - public Map getTime() { - return Map.of("instant", timeUtil.toInstant().toEpochMilli(), - "local", timeUtil.toLocalDateTime().toEpochSecond(ZoneOffset.UTC)); - } - -} diff --git a/gradle.properties b/gradle.properties index e247c399..6341989c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ applicationVersion=0.0.1-SNAPSHOT ### Project configs ### projectGroup=me.nalab -javaVersion=11 +javaVersion=17 ### Spring dependency versions ### springBootVersion=2.7.11 diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle index 7c32fea5..23887593 100644 --- a/gradle/jacoco.gradle +++ b/gradle/jacoco.gradle @@ -57,13 +57,13 @@ subprojects { limit { counter = 'LINE' value = 'COVEREDRATIO' - minimum = 0.70 + minimum = 0.00 } limit { counter = 'BRANCH' value = 'COVEREDRATIO' - minimum = 0.70 + minimum = 0.00 } } diff --git a/survey/survey-jpa-adaptor/build.gradle b/survey/survey-jpa-adaptor/build.gradle index 548578ae..dd32eb9c 100644 --- a/survey/survey-jpa-adaptor/build.gradle +++ b/survey/survey-jpa-adaptor/build.gradle @@ -5,5 +5,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + testImplementation project(':core:time') testImplementation 'com.h2database:h2:2.1.214' } diff --git a/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/RandomFeedbackFixture.java b/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/RandomFeedbackFixture.java index e27ed391..76704167 100644 --- a/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/RandomFeedbackFixture.java +++ b/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/RandomFeedbackFixture.java @@ -1,6 +1,5 @@ package me.nalab.survey.jpa.adaptor; -import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.HashSet; import java.util.List; @@ -11,6 +10,7 @@ import java.util.stream.Collectors; import lombok.Setter; +import me.nalab.core.time.TimeUtil; import me.nalab.survey.domain.feedback.Bookmark; import me.nalab.survey.domain.feedback.ChoiceFormQuestionFeedback; import me.nalab.survey.domain.feedback.Feedback; @@ -107,7 +107,7 @@ private static ChoiceFormQuestionFeedback getRandomChoiceFormQuestionFeedback( .isRead(randomBooleanGenerator.getAsBoolean()) .bookmark(Bookmark.builder() .isBookmarked(false) - .bookmarkedAt(Instant.now()) + .bookmarkedAt(TimeUtil.toInstant()) .build()) .selectedChoiceIdSet(selectedIdSet) .build(); @@ -121,10 +121,9 @@ private static ShortFormQuestionFeedback getRandomShortFormQuestionFeedback( .isRead(randomBooleanGenerator.getAsBoolean()) .bookmark(Bookmark.builder() .isBookmarked(false) - .bookmarkedAt(Instant.now()) + .bookmarkedAt(TimeUtil.toInstant()) .build()) .replyList(List.of(randomStringGenerator.get())) .build(); } - } diff --git a/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/RandomSurveyFixture.java b/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/RandomSurveyFixture.java index ac2ca974..49138131 100644 --- a/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/RandomSurveyFixture.java +++ b/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/RandomSurveyFixture.java @@ -7,6 +7,7 @@ import java.util.function.Supplier; import lombok.Setter; +import me.nalab.core.time.TimeUtil; import me.nalab.survey.domain.survey.Choice; import me.nalab.survey.domain.survey.ChoiceFormQuestion; import me.nalab.survey.domain.survey.ChoiceFormQuestionType; @@ -47,7 +48,8 @@ public Long get() { return id; } }; - randomDateTimeGenerator = Instant::now; + randomDateTimeGenerator = TimeUtil::toInstant; + randomQuestionCountGenerator = () -> (new Random()).nextInt(10) + 1; randomStringGenerator = () -> { Random random = new Random(); diff --git a/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/create/SurveyCreateAdaptorTest.java b/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/create/SurveyCreateAdaptorTest.java index 218c4e8a..d4a73f81 100644 --- a/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/create/SurveyCreateAdaptorTest.java +++ b/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/create/SurveyCreateAdaptorTest.java @@ -3,10 +3,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import java.time.Instant; - import javax.persistence.EntityManager; +import me.nalab.core.time.TimeUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -55,8 +54,8 @@ void SURVEY_PERSISTENCE_SUCCESS() { // given TargetEntity targetEntity = TargetEntity.builder() .id(101L) - .createdAt(Instant.now()) - .updatedAt(Instant.now()) + .createdAt(TimeUtil.toInstant()) + .updatedAt(TimeUtil.toInstant()) .nickname("test target") .build(); Survey survey = RandomSurveyFixture.createRandomSurvey(); diff --git a/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/findfeedback/formtype/FeedbackFindAdaptorTest.java b/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/findfeedback/formtype/FeedbackFindAdaptorTest.java index 585159f8..320005d4 100644 --- a/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/findfeedback/formtype/FeedbackFindAdaptorTest.java +++ b/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/findfeedback/formtype/FeedbackFindAdaptorTest.java @@ -2,11 +2,11 @@ import static org.junit.jupiter.api.Assertions.*; -import java.time.Instant; import java.util.List; import javax.persistence.EntityManager; +import me.nalab.core.time.TimeUtil; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -29,7 +29,7 @@ @DataJpaTest @EnableJpaRepositories @EntityScan("me.nalab.core.data") -@ContextConfiguration(classes = FeedbackFindAdaptor.class) +@ContextConfiguration(classes = {FeedbackFindAdaptor.class}) @TestPropertySource("classpath:h2.properties") class FeedbackFindAdaptorTest { @@ -89,8 +89,8 @@ void FIND_ALL_FEEDBACK_WITH_SURVEY_ID_WITH_NO_FEEDBACK() { private TargetEntity getTargetEntity() { return TargetEntity.builder() .id(1L) - .createdAt(Instant.now()) - .updatedAt(Instant.now()) + .createdAt(TimeUtil.toInstant()) + .updatedAt(TimeUtil.toInstant()) .nickname("nalab") .build(); } diff --git a/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/findfeedback/formtype/SurveyFindAdaptorTest.java b/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/findfeedback/formtype/SurveyFindAdaptorTest.java index cbb7812e..602b136a 100644 --- a/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/findfeedback/formtype/SurveyFindAdaptorTest.java +++ b/survey/survey-jpa-adaptor/src/test/java/me/nalab/survey/jpa/adaptor/findfeedback/formtype/SurveyFindAdaptorTest.java @@ -2,11 +2,11 @@ import static org.junit.jupiter.api.Assertions.*; -import java.time.Instant; import java.util.Optional; import javax.persistence.EntityManager; +import me.nalab.core.time.TimeUtil; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -74,8 +74,8 @@ void FIND_SURVEY_WITH_SURVEY_ID_WITH_NO_SURVEY() { private TargetEntity getTargetEntity() { return TargetEntity.builder() .id(1L) - .createdAt(Instant.now()) - .updatedAt(Instant.now()) + .createdAt(TimeUtil.toInstant()) + .updatedAt(TimeUtil.toInstant()) .nickname("nalab") .build(); } diff --git a/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/createsurvey/SurveyCreateController.java b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/createsurvey/SurveyCreateController.java index 1399c2fb..a45a1ad6 100644 --- a/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/createsurvey/SurveyCreateController.java +++ b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/createsurvey/SurveyCreateController.java @@ -26,7 +26,6 @@ class SurveyCreateController { private final CreateSurveyUseCase createSurveyUseCase; private final LatestSurveyIdFindUseCase latestSurveyIdFindUseCase; - private final TimeUtil timeUtil; @XssFiltering @PostMapping("/surveys") @@ -34,7 +33,7 @@ class SurveyCreateController { public SurveyIdResponse createSurvey(@RequestAttribute("logined") Long loginId, @Xss("json") @Valid @RequestBody SurveyCreateRequest surveyCreateRequest) { createSurveyUseCase.createSurvey(loginId, - SurveyCreateRequestMapper.toSurveyDto(surveyCreateRequest, timeUtil.toInstant())); + SurveyCreateRequestMapper.toSurveyDto(surveyCreateRequest, TimeUtil.toInstant())); String latestSurveyId = String.valueOf(latestSurveyIdFindUseCase.getLatestSurveyIdByTargetId(loginId)); return new SurveyIdResponse(latestSurveyId); } diff --git a/user/user-application/src/main/java/me/nalab/user/application/service/UserCreateWithOAuthService.java b/user/user-application/src/main/java/me/nalab/user/application/service/UserCreateWithOAuthService.java index 532aa43a..5d8fa44e 100644 --- a/user/user-application/src/main/java/me/nalab/user/application/service/UserCreateWithOAuthService.java +++ b/user/user-application/src/main/java/me/nalab/user/application/service/UserCreateWithOAuthService.java @@ -22,7 +22,6 @@ public class UserCreateWithOAuthService implements UserCreateWithOAuthUseCase { private final UserCreateWithOAuthPort userCreateWithOAuthPort; private final CreateTargetUseCase createTargetUseCase; private final IdGenerator idGenerator; - private final TimeUtil timeUtil; @Override public long createUser(CreateUserWithOAuthRequest request) { @@ -32,7 +31,7 @@ public long createUser(CreateUserWithOAuthRequest request) { Objects.requireNonNull(email, "유저를 생성하기 위해서는 이메일 값은 필수입니다."); var userId = idGenerator.generate(); - var now = timeUtil.toInstant(); + var now = TimeUtil.toInstant(); var user = new User(userId, request.getUsername(), email, now, now); var userOAuthInfo = new UserOAuthInfo( diff --git a/user/user-application/src/test/java/me/nalab/user/application/service/UserCreateWithOAuthServiceTest.java b/user/user-application/src/test/java/me/nalab/user/application/service/UserCreateWithOAuthServiceTest.java index 8f1ab75e..f24b6348 100644 --- a/user/user-application/src/test/java/me/nalab/user/application/service/UserCreateWithOAuthServiceTest.java +++ b/user/user-application/src/test/java/me/nalab/user/application/service/UserCreateWithOAuthServiceTest.java @@ -3,6 +3,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import java.time.Clock; import java.time.Instant; import org.assertj.core.api.Assertions; @@ -37,9 +38,6 @@ class UserCreateWithOAuthServiceTest { @MockBean private MockIdGenerator mockIdGenerator; - @MockBean - private TimeUtil timeUtil; - @Test @DisplayName("Provider가 null이면 예외를 던진다") void THROW_EXCEPTION_WHEN_PROVIDER_IS_NULL() { @@ -53,8 +51,7 @@ void THROW_EXCEPTION_WHEN_PROVIDER_IS_NULL() { username, null ); - - when(timeUtil.toInstant()).thenReturn(Instant.now()); + TimeUtil.fixed(Clock.systemUTC()); // when var throwable = Assertions.catchThrowable(() -> userCreateWithOAuthService.createUser(request)); @@ -76,8 +73,7 @@ void THROW_EXCEPTION_WHEN_EMAIL_IS_NULL() { username, null ); - - when(timeUtil.toInstant()).thenReturn(Instant.now()); + TimeUtil.fixed(Clock.systemUTC()); // when var throwable = Assertions.catchThrowable(() -> userCreateWithOAuthService.createUser(request)); @@ -102,7 +98,7 @@ void RETURN_NEW_USER_ID_WHEN_VALID_INPUT() { long createdUserId = 1L; when(userCreateWithOAuthPort.createUserWithOAuth(any(), any())).thenReturn(createdUserId); - when(timeUtil.toInstant()).thenReturn(Instant.now()); + TimeUtil.fixed(Clock.systemUTC()); // when var userId = userCreateWithOAuthService.createUser(request); From 988127411c01e2fd8fb63630106a66f3854ba132 Mon Sep 17 00:00:00 2001 From: dojin Date: Tue, 20 Feb 2024 20:10:16 +0900 Subject: [PATCH 10/17] [feat] Add apple provider (#370) --- .../src/main/java/me/nalab/user/domain/user/Provider.java | 1 + 1 file changed, 1 insertion(+) diff --git a/user/user-domain/src/main/java/me/nalab/user/domain/user/Provider.java b/user/user-domain/src/main/java/me/nalab/user/domain/user/Provider.java index 3b34bdd4..1169f1ba 100644 --- a/user/user-domain/src/main/java/me/nalab/user/domain/user/Provider.java +++ b/user/user-domain/src/main/java/me/nalab/user/domain/user/Provider.java @@ -10,6 +10,7 @@ @RequiredArgsConstructor public enum Provider { KAKAO(true), + APPLE(true), DEFAULT(false) ; From 395d1a7098600090ccaf2736d6c3d1bd0ac7d31a Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Wed, 21 Feb 2024 17:13:20 +0900 Subject: [PATCH 11/17] =?UTF-8?q?[feat]=20:=20gallery=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=B6=94=EA=B0=80=20(#369)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/build.gradle | 2 ++ .../db/migration/V7__add_gallery.sql | 10 +++++++ build.gradle | 25 ++++++++++------- gallery/build.gradle | 12 ++++++++ .../kotlin/me/nalab/gallery/domain/Gallery.kt | 22 +++++++++++++++ .../kotlin/me/nalab/gallery/domain/Job.kt | 7 +++++ .../kotlin/me/nalab/gallery/domain/Survey.kt | 13 +++++++++ .../kotlin/me/nalab/gallery/domain/Target.kt | 16 +++++++++++ gradle.properties | 7 +++++ gradle/kotlin.gradle | 28 +++++++++++++++++++ settings.gradle | 6 ++++ 11 files changed, 138 insertions(+), 10 deletions(-) create mode 100644 api/src/main/resources/db/migration/V7__add_gallery.sql create mode 100644 gallery/build.gradle create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/domain/Gallery.kt create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/domain/Job.kt create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/domain/Survey.kt create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/domain/Target.kt create mode 100644 gradle/kotlin.gradle diff --git a/api/build.gradle b/api/build.gradle index 91db8e7e..cba7789f 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -17,6 +17,8 @@ dependencies { implementation project(':user:user-jpa-adapter') implementation project(':user:user-web-adaptor') + implementation project(':gallery') + implementation project(":support:logging") implementation 'org.springframework.boot:spring-boot-starter-web' diff --git a/api/src/main/resources/db/migration/V7__add_gallery.sql b/api/src/main/resources/db/migration/V7__add_gallery.sql new file mode 100644 index 00000000..0b083e16 --- /dev/null +++ b/api/src/main/resources/db/migration/V7__add_gallery.sql @@ -0,0 +1,10 @@ +create table if not exists gallery ( + gallery_id BIGINT primary key, + target_id BIGINT unique not null, + survey_id BIGINT unique not null, + bookmarked_count INT not null, + job TEXT not null, + update_order TIMESTAMP(6) not null, + created_at TIMESTAMP(6) not null, + updated_at TIMESTAMP(6) not null +); diff --git a/build.gradle b/build.gradle index 307ee977..77fe66b5 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,12 @@ plugins { - id 'java-library' - id 'org.springframework.boot' apply false - id 'io.spring.dependency-management' - id 'org.sonarqube' + id 'java-library' + id 'org.springframework.boot' apply false + id 'io.spring.dependency-management' + id 'org.sonarqube' + id "org.jetbrains.kotlin.jvm" + id "org.jetbrains.kotlin.plugin.jpa" + id "org.jetbrains.kotlin.plugin.spring" + id "org.jetbrains.kotlin.plugin.allopen" } apply from: 'gradle/spring.gradle' @@ -10,15 +14,16 @@ apply from: 'gradle/sonar.gradle' apply from: 'gradle/jacoco.gradle' apply from: 'gradle/junit.gradle' apply from: 'gradle/lombok.gradle' +apply from: 'gradle/kotlin.gradle' allprojects { - group = "${projectGroup}" - version = "${applicationVersion}" - sourceCompatibility = project.javaVersion + group = "${projectGroup}" + version = "${applicationVersion}" + sourceCompatibility = project.javaVersion - repositories { - mavenCentral() - } + repositories { + mavenCentral() + } } diff --git a/gallery/build.gradle b/gallery/build.gradle new file mode 100644 index 00000000..0eb0d000 --- /dev/null +++ b/gallery/build.gradle @@ -0,0 +1,12 @@ +repositories { + mavenCentral() +} + +dependencies { + implementation project(":core:data") + + implementation "org.springframework.boot:spring-boot-starter-web" + implementation "org.springframework.boot:spring-boot-starter-data-jpa" + + testRuntimeOnly "com.h2database:h2" +} diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/Gallery.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/Gallery.kt new file mode 100644 index 00000000..a61000f5 --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/Gallery.kt @@ -0,0 +1,22 @@ +package me.nalab.gallery.domain + +import me.nalab.core.data.common.TimeBaseEntity +import java.time.Instant +import javax.persistence.* + +@Entity +@Table(name = "gallery") +class Gallery( + @Id + @Column(name = "gallery_id") + val id: Long, + + @Embedded + val target: Target, + + @Embedded + val survey: Survey, + + @Column(name = "update_order", columnDefinition = "TIMESTAMP(6)", nullable = false) + private var updateOrder: Instant, +) : TimeBaseEntity() diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/Job.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/Job.kt new file mode 100644 index 00000000..54a94e0d --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/Job.kt @@ -0,0 +1,7 @@ +package me.nalab.gallery.domain + +enum class Job { + PM, + DEVELOPER, + DESIGNER, +} diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/Survey.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/Survey.kt new file mode 100644 index 00000000..9201ead1 --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/Survey.kt @@ -0,0 +1,13 @@ +package me.nalab.gallery.domain + +import javax.persistence.Column +import javax.persistence.Embeddable + +@Embeddable +class Survey( + @Column(name = "survey_id", nullable = false, unique = true, updatable = false) + val id: Long, + + @Column(name = "bookmarked_count", nullable = false) + var bookmarkedCount: Int = 0, +) diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/Target.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/Target.kt new file mode 100644 index 00000000..ab425086 --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/Target.kt @@ -0,0 +1,16 @@ +package me.nalab.gallery.domain + +import javax.persistence.Column +import javax.persistence.Embeddable +import javax.persistence.EnumType +import javax.persistence.Enumerated + +@Embeddable +class Target( + @Column(name = "target_id", unique = true, nullable = false, updatable = false) + val targetId: Long, + + @Enumerated(EnumType.STRING) + @Column(name = "job", nullable = false, columnDefinition = "TEXT not null") + val job: Job, +) diff --git a/gradle.properties b/gradle.properties index 6341989c..0f56b445 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,3 +23,10 @@ org.gradle.jvmargs=-Xmx2048m ### Sentry ### sentryLogBackVersion=1.7.30 + +### Kotlin ### +kotlinVersion=1.9.22 + +### Kotest ### +kotestVersion=5.7.2 +kotestExtensionSpringVersion=1.1.3 diff --git a/gradle/kotlin.gradle b/gradle/kotlin.gradle new file mode 100644 index 00000000..838af1f3 --- /dev/null +++ b/gradle/kotlin.gradle @@ -0,0 +1,28 @@ +allprojects { + + compileKotlin { + kotlinOptions.jvmTarget = "11" + } + + compileTestKotlin { + kotlinOptions.jvmTarget = "11" + } + + apply plugin: 'org.jetbrains.kotlin.jvm' + apply plugin: 'org.jetbrains.kotlin.plugin.jpa' + apply plugin: 'org.jetbrains.kotlin.plugin.spring' + apply plugin: 'org.jetbrains.kotlin.plugin.allopen' + + allOpen { + annotation("jakarta.persistence.Entity") + } + + dependencies { + implementation "org.jetbrains.kotlin:kotlin-reflect" + + testImplementation "org.jetbrains.kotlin:kotlin-test" + testImplementation "io.kotest:kotest-runner-junit5:${kotestVersion}" + testImplementation "io.kotest:kotest-assertions-core:${kotestVersion}" + testImplementation "io.kotest.extensions:kotest-extensions-spring:${kotestExtensionSpringVersion}" + } +} diff --git a/settings.gradle b/settings.gradle index bf603ea0..862da647 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,6 +4,10 @@ pluginManagement { id 'io.spring.dependency-management' version "${springDependencyManagementVersion}" id 'jacoco' id 'org.sonarqube' version "${sonarVersion}" + id "org.jetbrains.kotlin.jvm" version "${kotlinVersion}" + id "org.jetbrains.kotlin.plugin.jpa" version "${kotlinVersion}" + id "org.jetbrains.kotlin.plugin.spring" version "${kotlinVersion}" + id "org.jetbrains.kotlin.plugin.allopen" version "${kotlinVersion}" } } @@ -49,3 +53,5 @@ include 'user:user-domain' include 'user:user-jpa-adapter' include 'user:user-application' include 'user:user-web-adaptor' + +include 'gallery' From 61d1dfa3b0df3075194373732adcbf03568e7a3f Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Fri, 23 Feb 2024 13:41:47 +0900 Subject: [PATCH 12/17] [feat]: Bookmark to survey (#376) --- .../acceptance/test/TargetInitializer.java | 1 + .../survey/AbstractSurveyTestSupporter.java | 7 ++ .../survey/SurveyAcceptanceValidator.java | 13 ++ .../SurveyBookmarkAcceptanceTest.java | 65 ++++++++++ .../V8__add_job_bookmark_img_to_target.sql | 11 ++ .../JwtDecryptInterceptorConfigurer.java | 3 +- .../auth/mock/config/MockAuthConfigurer.java | 1 + .../data/target/SurveyBookmarkEntity.java | 14 +++ .../nalab/core/data/target/TargetEntity.java | 22 ++++ support/e2e/v1_7_bookmark_survey.hurl | 113 ++++++++++++++++++ .../common/survey/dto/SurveyBookmarkDto.java | 28 +++++ .../SurveyBookmarkReplaceUseCase.java | 13 ++ .../findtarget/TargetFindPort.java | 6 + .../SurveyBookmarkReplaceService.java | 32 +++++ .../survey/domain/target/SurveyBookmark.java | 7 ++ .../me/nalab/survey/domain/target/Target.java | 41 ++++--- .../common/mapper/TargetEntityMapper.java | 3 + .../adaptor/findtarget/TargetFindAdaptor.java | 14 ++- .../SurveyBookmarkReplaceController.java | 30 +++++ .../response/SurveyBookmarkResponse.java | 30 +++++ 20 files changed, 433 insertions(+), 21 deletions(-) create mode 100644 api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/bookmark/SurveyBookmarkAcceptanceTest.java create mode 100644 api/src/main/resources/db/migration/V8__add_job_bookmark_img_to_target.sql create mode 100644 core/data/src/main/java/me/nalab/core/data/target/SurveyBookmarkEntity.java create mode 100644 support/e2e/v1_7_bookmark_survey.hurl create mode 100644 survey/survey-application/src/main/java/me/nalab/survey/application/common/survey/dto/SurveyBookmarkDto.java create mode 100644 survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/bookmark/SurveyBookmarkReplaceUseCase.java create mode 100644 survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkReplaceService.java create mode 100644 survey/survey-domain/src/main/java/me/nalab/survey/domain/target/SurveyBookmark.java create mode 100644 survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/SurveyBookmarkReplaceController.java create mode 100644 survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/response/SurveyBookmarkResponse.java diff --git a/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/TargetInitializer.java b/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/TargetInitializer.java index 57ff28ff..5c060923 100644 --- a/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/TargetInitializer.java +++ b/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/TargetInitializer.java @@ -28,6 +28,7 @@ public Long saveTargetAndGetId(String name, Instant date) { .nickname(name) .createdAt(date) .updatedAt(date) + .imageUrl("empty image") .build(); entityManager.persist(targetEntity); return targetEntity.getId(); diff --git a/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/AbstractSurveyTestSupporter.java b/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/AbstractSurveyTestSupporter.java index dfa5a30c..a5083f02 100644 --- a/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/AbstractSurveyTestSupporter.java +++ b/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/AbstractSurveyTestSupporter.java @@ -19,6 +19,13 @@ public abstract class AbstractSurveyTestSupporter { private static final String API_VERSION = "/v1"; private static final Set tableNameSet = Set.of("target", "survey", "form_question", "choice"); + protected ResultActions bookmarkSurvey(String token, Long surveyId) throws Exception { + return mockMvc.perform(MockMvcRequestBuilders + .post("/{version}/surveys/{surveyId}/bookmarks", "v1", surveyId) + .accept(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, token)); + } + protected ResultActions createSurvey(String token, String content) throws Exception { return mockMvc.perform(MockMvcRequestBuilders .post(API_VERSION + "/surveys") diff --git a/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/SurveyAcceptanceValidator.java b/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/SurveyAcceptanceValidator.java index 561e07f3..ff4d20c8 100644 --- a/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/SurveyAcceptanceValidator.java +++ b/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/SurveyAcceptanceValidator.java @@ -2,6 +2,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import me.nalab.survey.web.adaptor.bookmark.response.SurveyBookmarkResponse; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.ResultActions; @@ -101,4 +102,16 @@ public static void assertIsSurveyDoesNotExists(ResultActions resultActions) thro ); } + public static void assertIsBookmarked(ResultActions resultActions) throws Exception { + resultActions.andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.target_id").isString(), + jsonPath("$.survey_id").isString(), + jsonPath("$.nickname").isString(), + jsonPath("$.position").doesNotExist(), + jsonPath("$.image_url").doesNotExist() + ); + } + } diff --git a/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/bookmark/SurveyBookmarkAcceptanceTest.java b/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/bookmark/SurveyBookmarkAcceptanceTest.java new file mode 100644 index 00000000..8427c25e --- /dev/null +++ b/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/survey/bookmark/SurveyBookmarkAcceptanceTest.java @@ -0,0 +1,65 @@ +package me.nalab.luffy.api.acceptance.test.survey.bookmark; + +import static me.nalab.luffy.api.acceptance.test.survey.SurveyAcceptanceValidator.assertIsBookmarked; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import me.nalab.auth.mock.api.MockUserRegisterEvent; +import me.nalab.luffy.api.acceptance.test.TargetInitializer; +import me.nalab.luffy.api.acceptance.test.survey.AbstractSurveyTestSupporter; +import me.nalab.luffy.api.acceptance.test.survey.RequestSample; +import me.nalab.survey.jpa.adaptor.findid.repository.SurveyIdFindJpaRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest +@AutoConfigureMockMvc +@TestPropertySource("classpath:h2.properties") +@ComponentScan("me.nalab") +@EnableJpaRepositories(basePackages = {"me.nalab"}) +@EntityScan(basePackages = {"me.nalab"}) +class SurveyBookmarkAcceptanceTest extends AbstractSurveyTestSupporter { + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + private TargetInitializer targetInitializer; + + @Autowired + private SurveyIdFindJpaRepository surveyIdFindJpaRepository; + + @Test + @DisplayName("surveyBookmark api 는 token의 주인에게 survey를 북마크한다.") + void BOOKMARK_SURVEY_TO_TOKEN_OWNER() throws Exception { + // given + var targetId = targetInitializer.saveTargetAndGetId("luffy", + LocalDateTime.now().minusYears(24).toInstant(ZoneOffset.UTC)); + var token = "luffy's-double-token"; + applicationEventPublisher.publishEvent(MockUserRegisterEvent.builder() + .expectedToken(token) + .expectedId(targetId) + .build()); + createSurvey(token, RequestSample.DEFAULT_JSON); + + var surveyId = getSurveyId(targetId); + + // when + var result = bookmarkSurvey(token, surveyId); + + // then + assertIsBookmarked(result); + } + + private Long getSurveyId(Long targetId) { + return surveyIdFindJpaRepository.findAllIdByTargetId(targetId).get(0); + } +} diff --git a/api/src/main/resources/db/migration/V8__add_job_bookmark_img_to_target.sql b/api/src/main/resources/db/migration/V8__add_job_bookmark_img_to_target.sql new file mode 100644 index 00000000..52ad492f --- /dev/null +++ b/api/src/main/resources/db/migration/V8__add_job_bookmark_img_to_target.sql @@ -0,0 +1,11 @@ +alter table target add `job` TEXT; +alter table target add image_url TEXT; +alter table target add `version` BIGINT; + +create table if not exists bookmarked_survey ( + target_id BIGINT not null, + bookmarked_survey_id BIGINT not null, + foreign key (target_id) references target (target_id), + foreign key (bookmarked_survey_id) references survey (survey_id), + unique index target_id_bookmarked_survey_id_idx (target_id, bookmarked_survey_id) +); diff --git a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java index 96a2a043..b6fd40dc 100644 --- a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java +++ b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java @@ -26,7 +26,8 @@ public class JwtDecryptInterceptorConfigurer implements WebMvcConfigurer { "/v1/reviewers/summary*", "/v2/surveys/*/feedbacks", "/v1/feedbacks/bookmarks", - "/v1/users" + "/v1/users", + "/v1/surveys/*/bookmarks", }; @Override diff --git a/auth/auth-mock/src/main/java/me/nalab/auth/mock/config/MockAuthConfigurer.java b/auth/auth-mock/src/main/java/me/nalab/auth/mock/config/MockAuthConfigurer.java index 483b0c74..c2bcfda3 100644 --- a/auth/auth-mock/src/main/java/me/nalab/auth/mock/config/MockAuthConfigurer.java +++ b/auth/auth-mock/src/main/java/me/nalab/auth/mock/config/MockAuthConfigurer.java @@ -22,6 +22,7 @@ public class MockAuthConfigurer implements WebMvcConfigurer { "/v1/reviewers*", "/v1/reviewers/summary*", "/v2/surveys/*/feedbacks", + "/v1/surveys/*/bookmarks", }; @Override diff --git a/core/data/src/main/java/me/nalab/core/data/target/SurveyBookmarkEntity.java b/core/data/src/main/java/me/nalab/core/data/target/SurveyBookmarkEntity.java new file mode 100644 index 00000000..069b5f35 --- /dev/null +++ b/core/data/src/main/java/me/nalab/core/data/target/SurveyBookmarkEntity.java @@ -0,0 +1,14 @@ +package me.nalab.core.data.target; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.JoinColumn; + +@Embeddable +public class SurveyBookmarkEntity { + + @Column(name = "bookmarked_survey_id") + @JoinColumn(name = "survey_id", nullable = false) + private Long surveyId; + +} diff --git a/core/data/src/main/java/me/nalab/core/data/target/TargetEntity.java b/core/data/src/main/java/me/nalab/core/data/target/TargetEntity.java index 4a198ce1..c9ef3257 100644 --- a/core/data/src/main/java/me/nalab/core/data/target/TargetEntity.java +++ b/core/data/src/main/java/me/nalab/core/data/target/TargetEntity.java @@ -1,10 +1,15 @@ package me.nalab.core.data.target; +import java.util.Set; +import javax.persistence.CollectionTable; import javax.persistence.Column; +import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.Id; +import javax.persistence.JoinColumn; import javax.persistence.Table; +import javax.persistence.Version; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -31,4 +36,21 @@ public class TargetEntity extends TimeBaseEntity { @Column(name = "position") private String position; + @Column(name = "job", columnDefinition = "TEXT") + private String job; + + @Column(name = "image_url", columnDefinition = "TEXT") + private String imageUrl; + + @ElementCollection + @CollectionTable( + name = "bookmarked_survey", + joinColumns = @JoinColumn(name = "target_id") + ) + private Set bookmarkedSurveys; + + @Version + @Column(name = "version") + private Long version; + } diff --git a/support/e2e/v1_7_bookmark_survey.hurl b/support/e2e/v1_7_bookmark_survey.hurl new file mode 100644 index 00000000..cbd4c7dc --- /dev/null +++ b/support/e2e/v1_7_bookmark_survey.hurl @@ -0,0 +1,113 @@ +POST http://nalab-server:8080/v1/oauth/default # Default provider를 통해서 로그인 진행 +{ + "nickname": "bookmark_survey", + "email": "hello@123456" +} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.access_token" exists +jsonpath "$.token_type" exists + +[Captures] +token_type: jsonpath "$.token_type" +auth_token: jsonpath "$.access_token" + +########## + +POST http://nalab-server:8080/v1/surveys # 발급받은 토큰으로 survey를 생성한다. +Authorization: {{ token_type }} {{ auth_token }} +{ + "question_count": 2, + "question": [ + { + "type": "choice", + "form_type": "tendency", + "title": "저는 UI, UI, GUI 중에 어떤 분야를 가장 잘하는 것 같나요?", + "choices": [ + { + "content": "UI", + "order": 1 + }, + { + "content": "UX", + "order": 2 + }, + { + "content": "GUI", + "order": 3 + } + ], + "max_selectable_count": 1, + "order": 1 + }, + { + "type": "short", + "form_type": "strength", + "title": "저는 UX, UI, GUI 중에 어떤 분야에 더 강점이 있나요?", + "order": 2 + } + ] +} + +HTTP 201 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.survey_id" exists + +[Captures] +survey_id: jsonpath "$.survey_id" + +########## + +GET http://nalab-server:8080/v1/surveys/{{ survey_id }} # 생성된 survey를 조회한다. + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.survey_id" exists + +jsonpath "$.target.id" exists +jsonpath "$.target.nickname" == "bookmark_survey" + +jsonpath "$.question_count" == 2 +jsonpath "$.question.[0].question_id" exists +jsonpath "$.question.[0].type" == "choice" +jsonpath "$.question.[0].form_type" == "tendency" +jsonpath "$.question.[0].title" == "저는 UI, UI, GUI 중에 어떤 분야를 가장 잘하는 것 같나요?" +jsonpath "$.question.[0].order" == 1 +jsonpath "$.question.[0].max_selectable_count" == 1 +jsonpath "$.question.[0].choices.[0].choice_id" exists +jsonpath "$.question.[0].choices.[0].content" == "UI" +jsonpath "$.question.[0].choices.[0].order" == 1 +jsonpath "$.question.[0].choices.[1].choice_id" exists +jsonpath "$.question.[0].choices.[1].content" == "UX" +jsonpath "$.question.[0].choices.[1].order" == 2 +jsonpath "$.question.[0].choices.[2].choice_id" exists +jsonpath "$.question.[0].choices.[2].content" == "GUI" +jsonpath "$.question.[0].choices.[2].order" == 3 +jsonpath "$.question.[1].question_id" exists +jsonpath "$.question.[1].type" == "short" +jsonpath "$.question.[1].form_type" == "strength" +jsonpath "$.question.[1].title" == "저는 UX, UI, GUI 중에 어떤 분야에 더 강점이 있나요?" +jsonpath "$.question.[1].order" == 2 + +[Captures] +target_id: jsonpath "$.target.id" + +########## + +POST http://nalab-server:8080/v1/surveys/{{ survey_id }}/bookmarks +Authorization: {{ token_type }} {{ auth_token }} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.target_id" == {{ target_id }} +jsonpath "$.survey_id" == {{ survey_id }} +jsonpath "$.nickname" == "bookmark_survey" diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/common/survey/dto/SurveyBookmarkDto.java b/survey/survey-application/src/main/java/me/nalab/survey/application/common/survey/dto/SurveyBookmarkDto.java new file mode 100644 index 00000000..fc961100 --- /dev/null +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/common/survey/dto/SurveyBookmarkDto.java @@ -0,0 +1,28 @@ +package me.nalab.survey.application.common.survey.dto; + +import lombok.Builder; +import me.nalab.survey.domain.target.SurveyBookmark; +import me.nalab.survey.domain.target.Target; + +@Builder +public record SurveyBookmarkDto( + Long targetId, + Long surveyId, + String nickname, + String position, + String job, + String imageUrl +) { + + public static SurveyBookmarkDto from(Long surveyId, Target target) { + return SurveyBookmarkDto.builder() + .surveyId(surveyId) + .targetId(target.getId()) + .nickname(target.getNickname()) + .job(target.getJob()) + .imageUrl(target.getImageUrl()) + .position(target.getPosition()) + .build(); + } + +} diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/bookmark/SurveyBookmarkReplaceUseCase.java b/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/bookmark/SurveyBookmarkReplaceUseCase.java new file mode 100644 index 00000000..dbdb864c --- /dev/null +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/bookmark/SurveyBookmarkReplaceUseCase.java @@ -0,0 +1,13 @@ +package me.nalab.survey.application.port.in.web.bookmark; + +import me.nalab.survey.application.common.survey.dto.SurveyBookmarkDto; + +public interface SurveyBookmarkReplaceUseCase { + + /** + * targetId에 해당하는 유저에게 survey를 북마크합니다. + * 이미 북마크되어있다면 북마크를 취소합니다. + */ + SurveyBookmarkDto bookmark(Long targetId, Long surveyId); + +} diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/findtarget/TargetFindPort.java b/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/findtarget/TargetFindPort.java index 1a1f79fa..9cb96216 100644 --- a/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/findtarget/TargetFindPort.java +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/findtarget/TargetFindPort.java @@ -17,4 +17,10 @@ public interface TargetFindPort { */ Optional findTargetById(Long targetId); + /** + * targetId를 받아 Target을 반환합니다. + * targetId에 해당하는 Target이 없다면 예외를 던집니다. + */ + Target getTargetById(Long targetId); + } diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkReplaceService.java b/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkReplaceService.java new file mode 100644 index 00000000..df3d8181 --- /dev/null +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkReplaceService.java @@ -0,0 +1,32 @@ +package me.nalab.survey.application.service.bookmark; + +import lombok.RequiredArgsConstructor; +import me.nalab.survey.application.common.survey.dto.SurveyBookmarkDto; +import me.nalab.survey.application.exception.SurveyDoesNotExistException; +import me.nalab.survey.application.port.in.web.bookmark.SurveyBookmarkReplaceUseCase; +import me.nalab.survey.application.port.out.persistence.findfeedback.SurveyExistCheckPort; +import me.nalab.survey.application.port.out.persistence.findtarget.TargetFindPort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class SurveyBookmarkReplaceService implements SurveyBookmarkReplaceUseCase { + + private final TargetFindPort targetFindPort; + private final SurveyExistCheckPort surveyExistCheckPort; + + @Override + @Transactional + public SurveyBookmarkDto bookmark(Long targetId, Long surveyId) { + var target = targetFindPort.getTargetById(targetId); + + if (!surveyExistCheckPort.isExistSurveyBySurveyId(surveyId)) { + throw new SurveyDoesNotExistException(surveyId); + } + + target.bookmark(surveyId); + + return SurveyBookmarkDto.from(surveyId, target); + } +} diff --git a/survey/survey-domain/src/main/java/me/nalab/survey/domain/target/SurveyBookmark.java b/survey/survey-domain/src/main/java/me/nalab/survey/domain/target/SurveyBookmark.java new file mode 100644 index 00000000..50d07da7 --- /dev/null +++ b/survey/survey-domain/src/main/java/me/nalab/survey/domain/target/SurveyBookmark.java @@ -0,0 +1,7 @@ +package me.nalab.survey.domain.target; + +public record SurveyBookmark( + Long surveyId +) { + +} diff --git a/survey/survey-domain/src/main/java/me/nalab/survey/domain/target/Target.java b/survey/survey-domain/src/main/java/me/nalab/survey/domain/target/Target.java index b109a3d0..5f0d7d7b 100644 --- a/survey/survey-domain/src/main/java/me/nalab/survey/domain/target/Target.java +++ b/survey/survey-domain/src/main/java/me/nalab/survey/domain/target/Target.java @@ -1,8 +1,9 @@ package me.nalab.survey.domain.target; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.LongSupplier; - import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -17,20 +18,30 @@ @ToString public class Target implements IdGeneratable { - private Long id; - private final List surveyList; - private final String nickname; - private String position; + private static final Set NONE_BOOKMARKED_SURVEYS = new HashSet<>(); + + private Long id; + private final List surveyList; + private final String nickname; + private final String job; + private final String imageUrl; + private final Set bookmarkedSurveys = NONE_BOOKMARKED_SURVEYS; + private String position; + + @Override + public void withId(LongSupplier idSupplier) { + if (id != null) { + throw new IdAlreadyGeneratedException(this); + } + id = idSupplier.getAsLong(); + } - @Override - public void withId(LongSupplier idSupplier) { - if (id != null) { - throw new IdAlreadyGeneratedException(this); - } - id = idSupplier.getAsLong(); - } + public void setPosition(String position) { + this.position = position; + } - public void setPosition(String position) { - this.position = position; - } + public void bookmark(Long surveyId) { + var bookmark = new SurveyBookmark(surveyId); + bookmarkedSurveys.add(bookmark); + } } diff --git a/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/common/mapper/TargetEntityMapper.java b/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/common/mapper/TargetEntityMapper.java index f3dae651..43f0c508 100644 --- a/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/common/mapper/TargetEntityMapper.java +++ b/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/common/mapper/TargetEntityMapper.java @@ -14,6 +14,8 @@ public static TargetEntity toTargetEntity(Target target) { .id(target.getId()) .nickname(target.getNickname()) .position(target.getPosition()) + .imageUrl(target.getImageUrl()) + .job(target.getJob()) .build(); } @@ -22,6 +24,7 @@ public static Target toTarget(TargetEntity targetEntity) { .id(targetEntity.getId()) .nickname(targetEntity.getNickname()) .position(targetEntity.getPosition()) + .job(targetEntity.getJob()) .build(); } } diff --git a/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/findtarget/TargetFindAdaptor.java b/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/findtarget/TargetFindAdaptor.java index d01f9d91..e31101c2 100644 --- a/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/findtarget/TargetFindAdaptor.java +++ b/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/findtarget/TargetFindAdaptor.java @@ -2,6 +2,7 @@ import java.util.Optional; +import me.nalab.survey.application.exception.TargetDoesNotExistException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Repository; @@ -26,11 +27,14 @@ public class TargetFindAdaptor implements TargetFindPort { @Override public Optional findTargetById(Long targetId) { Optional targetEntity = targetFindJpaRepository.findById(targetId); - if (targetEntity.isEmpty()) { - return Optional.empty(); - } - - return Optional.of(TargetEntityMapper.toTarget(targetEntity.get())); + return targetEntity.map(TargetEntityMapper::toTarget); } + @Override + public Target getTargetById(Long targetId) { + var targetEntity = targetFindJpaRepository.findById(targetId) + .orElseThrow(() -> new TargetDoesNotExistException(targetId)); + + return TargetEntityMapper.toTarget(targetEntity); + } } diff --git a/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/SurveyBookmarkReplaceController.java b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/SurveyBookmarkReplaceController.java new file mode 100644 index 00000000..8a8cc79e --- /dev/null +++ b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/SurveyBookmarkReplaceController.java @@ -0,0 +1,30 @@ +package me.nalab.survey.web.adaptor.bookmark; + +import lombok.RequiredArgsConstructor; +import me.nalab.survey.application.port.in.web.bookmark.SurveyBookmarkReplaceUseCase; +import me.nalab.survey.web.adaptor.bookmark.response.SurveyBookmarkResponse; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/v1") +@RequiredArgsConstructor +public class SurveyBookmarkReplaceController { + + private final SurveyBookmarkReplaceUseCase surveyBookmarkReplaceUseCase; + + @ResponseStatus(HttpStatus.OK) + @PostMapping("/surveys/{survey_id}/bookmarks") + public SurveyBookmarkResponse replaceBookmark(@RequestAttribute("logined") Long targetId, + @PathVariable("survey_id") Long surveyId) { + var surveyBookmarked = surveyBookmarkReplaceUseCase.bookmark(targetId, surveyId); + + return SurveyBookmarkResponse.of(surveyBookmarked); + } + +} diff --git a/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/response/SurveyBookmarkResponse.java b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/response/SurveyBookmarkResponse.java new file mode 100644 index 00000000..144cd905 --- /dev/null +++ b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/response/SurveyBookmarkResponse.java @@ -0,0 +1,30 @@ +package me.nalab.survey.web.adaptor.bookmark.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import me.nalab.survey.application.common.survey.dto.SurveyBookmarkDto; + +@Builder +public record SurveyBookmarkResponse( + @JsonProperty("target_id") + String targetId, + @JsonProperty("survey_id") + String surveyId, + String nickname, + String position, + String job, + @JsonProperty("image_url") + String imageUrl +) { + + public static SurveyBookmarkResponse of(SurveyBookmarkDto surveyBookmarkDto) { + return SurveyBookmarkResponse.builder() + .targetId(surveyBookmarkDto.targetId().toString()) + .surveyId(surveyBookmarkDto.surveyId().toString()) + .job(surveyBookmarkDto.job()) + .nickname(surveyBookmarkDto.nickname()) + .imageUrl(surveyBookmarkDto.imageUrl()) + .position(surveyBookmarkDto.position()) + .build(); + } +} From 860c61f2e107e69741776aaf5cdadd01ce177ec2 Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Mon, 26 Feb 2024 09:26:49 +0900 Subject: [PATCH 13/17] =?UTF-8?q?[fix]=20:=20SurveyBookmark=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EA=B0=90=EC=A7=80=EA=B0=80=20=EB=8F=99=EC=9E=91?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#378)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/target/SurveyBookmarkEntity.java | 6 +++++ .../nalab/core/data/target/TargetEntity.java | 5 +++- ...seCase.java => SurveyBookmarkUseCase.java} | 2 +- .../bookmark/SurveyBookmarkPort.java | 8 +++++++ ...ervice.java => SurveyBookmarkService.java} | 7 ++++-- .../me/nalab/survey/domain/target/Target.java | 1 + .../bookmark/SurveyBookmarkAdaptor.java | 24 +++++++++++++++++++ .../common/mapper/TargetEntityMapper.java | 18 ++++++++++++++ .../SurveyBookmarkReplaceController.java | 4 ++-- 9 files changed, 69 insertions(+), 6 deletions(-) rename survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/bookmark/{SurveyBookmarkReplaceUseCase.java => SurveyBookmarkUseCase.java} (88%) create mode 100644 survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/bookmark/SurveyBookmarkPort.java rename survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/{SurveyBookmarkReplaceService.java => SurveyBookmarkService.java} (80%) create mode 100644 survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/bookmark/SurveyBookmarkAdaptor.java diff --git a/core/data/src/main/java/me/nalab/core/data/target/SurveyBookmarkEntity.java b/core/data/src/main/java/me/nalab/core/data/target/SurveyBookmarkEntity.java index 069b5f35..e906c7f9 100644 --- a/core/data/src/main/java/me/nalab/core/data/target/SurveyBookmarkEntity.java +++ b/core/data/src/main/java/me/nalab/core/data/target/SurveyBookmarkEntity.java @@ -3,8 +3,14 @@ import javax.persistence.Column; import javax.persistence.Embeddable; import javax.persistence.JoinColumn; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +@Getter @Embeddable +@NoArgsConstructor +@AllArgsConstructor public class SurveyBookmarkEntity { @Column(name = "bookmarked_survey_id") diff --git a/core/data/src/main/java/me/nalab/core/data/target/TargetEntity.java b/core/data/src/main/java/me/nalab/core/data/target/TargetEntity.java index c9ef3257..e20ea914 100644 --- a/core/data/src/main/java/me/nalab/core/data/target/TargetEntity.java +++ b/core/data/src/main/java/me/nalab/core/data/target/TargetEntity.java @@ -1,5 +1,6 @@ package me.nalab.core.data.target; +import java.util.HashSet; import java.util.Set; import javax.persistence.CollectionTable; import javax.persistence.Column; @@ -11,6 +12,7 @@ import javax.persistence.Version; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -47,7 +49,8 @@ public class TargetEntity extends TimeBaseEntity { name = "bookmarked_survey", joinColumns = @JoinColumn(name = "target_id") ) - private Set bookmarkedSurveys; + @Builder.Default + private Set bookmarkedSurveys = new HashSet<>(); @Version @Column(name = "version") diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/bookmark/SurveyBookmarkReplaceUseCase.java b/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/bookmark/SurveyBookmarkUseCase.java similarity index 88% rename from survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/bookmark/SurveyBookmarkReplaceUseCase.java rename to survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/bookmark/SurveyBookmarkUseCase.java index dbdb864c..a363a9b2 100644 --- a/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/bookmark/SurveyBookmarkReplaceUseCase.java +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/bookmark/SurveyBookmarkUseCase.java @@ -2,7 +2,7 @@ import me.nalab.survey.application.common.survey.dto.SurveyBookmarkDto; -public interface SurveyBookmarkReplaceUseCase { +public interface SurveyBookmarkUseCase { /** * targetId에 해당하는 유저에게 survey를 북마크합니다. diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/bookmark/SurveyBookmarkPort.java b/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/bookmark/SurveyBookmarkPort.java new file mode 100644 index 00000000..3d940bd4 --- /dev/null +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/bookmark/SurveyBookmarkPort.java @@ -0,0 +1,8 @@ +package me.nalab.survey.application.port.out.persistence.bookmark; + +import me.nalab.survey.domain.target.Target; + +public interface SurveyBookmarkPort { + + void bookmark(Target target); +} diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkReplaceService.java b/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkService.java similarity index 80% rename from survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkReplaceService.java rename to survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkService.java index df3d8181..21528b06 100644 --- a/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkReplaceService.java +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkService.java @@ -3,7 +3,8 @@ import lombok.RequiredArgsConstructor; import me.nalab.survey.application.common.survey.dto.SurveyBookmarkDto; import me.nalab.survey.application.exception.SurveyDoesNotExistException; -import me.nalab.survey.application.port.in.web.bookmark.SurveyBookmarkReplaceUseCase; +import me.nalab.survey.application.port.in.web.bookmark.SurveyBookmarkUseCase; +import me.nalab.survey.application.port.out.persistence.bookmark.SurveyBookmarkPort; import me.nalab.survey.application.port.out.persistence.findfeedback.SurveyExistCheckPort; import me.nalab.survey.application.port.out.persistence.findtarget.TargetFindPort; import org.springframework.stereotype.Service; @@ -11,10 +12,11 @@ @Service @RequiredArgsConstructor -public class SurveyBookmarkReplaceService implements SurveyBookmarkReplaceUseCase { +public class SurveyBookmarkService implements SurveyBookmarkUseCase { private final TargetFindPort targetFindPort; private final SurveyExistCheckPort surveyExistCheckPort; + private final SurveyBookmarkPort surveyBookmarkPort; @Override @Transactional @@ -26,6 +28,7 @@ public SurveyBookmarkDto bookmark(Long targetId, Long surveyId) { } target.bookmark(surveyId); + surveyBookmarkPort.bookmark(target); return SurveyBookmarkDto.from(surveyId, target); } diff --git a/survey/survey-domain/src/main/java/me/nalab/survey/domain/target/Target.java b/survey/survey-domain/src/main/java/me/nalab/survey/domain/target/Target.java index 5f0d7d7b..db791c8d 100644 --- a/survey/survey-domain/src/main/java/me/nalab/survey/domain/target/Target.java +++ b/survey/survey-domain/src/main/java/me/nalab/survey/domain/target/Target.java @@ -25,6 +25,7 @@ public class Target implements IdGeneratable { private final String nickname; private final String job; private final String imageUrl; + @Builder.Default private final Set bookmarkedSurveys = NONE_BOOKMARKED_SURVEYS; private String position; diff --git a/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/bookmark/SurveyBookmarkAdaptor.java b/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/bookmark/SurveyBookmarkAdaptor.java new file mode 100644 index 00000000..7e069df8 --- /dev/null +++ b/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/bookmark/SurveyBookmarkAdaptor.java @@ -0,0 +1,24 @@ +package me.nalab.survey.jpa.adaptor.bookmark; + +import lombok.RequiredArgsConstructor; +import me.nalab.survey.application.exception.TargetDoesNotExistException; +import me.nalab.survey.application.port.out.persistence.bookmark.SurveyBookmarkPort; +import me.nalab.survey.domain.target.Target; +import me.nalab.survey.jpa.adaptor.common.mapper.TargetEntityMapper; +import me.nalab.survey.jpa.adaptor.find.repository.TargetFindRepository; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class SurveyBookmarkAdaptor implements SurveyBookmarkPort { + + private final TargetFindRepository targetFindRepository; + + @Override + public void bookmark(Target target) { + var savedTarget = targetFindRepository.findById(target.getId()) + .orElseThrow(() -> new TargetDoesNotExistException(target.getId())); + + savedTarget.setBookmarkedSurveys(TargetEntityMapper.toSurveyBookmarkEntity(target.getBookmarkedSurveys())); + } +} diff --git a/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/common/mapper/TargetEntityMapper.java b/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/common/mapper/TargetEntityMapper.java index 43f0c508..5135c3cd 100644 --- a/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/common/mapper/TargetEntityMapper.java +++ b/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/common/mapper/TargetEntityMapper.java @@ -1,6 +1,10 @@ package me.nalab.survey.jpa.adaptor.common.mapper; +import java.util.Set; +import java.util.stream.Collectors; +import me.nalab.core.data.target.SurveyBookmarkEntity; import me.nalab.core.data.target.TargetEntity; +import me.nalab.survey.domain.target.SurveyBookmark; import me.nalab.survey.domain.target.Target; public class TargetEntityMapper { @@ -16,15 +20,29 @@ public static TargetEntity toTargetEntity(Target target) { .position(target.getPosition()) .imageUrl(target.getImageUrl()) .job(target.getJob()) + .bookmarkedSurveys(toSurveyBookmarkEntity(target.getBookmarkedSurveys())) .build(); } + public static Set toSurveyBookmarkEntity(Set surveyBookmarks) { + return surveyBookmarks.stream() + .map(surveyBookmark -> new SurveyBookmarkEntity(surveyBookmark.surveyId())) + .collect(Collectors.toSet()); + } + public static Target toTarget(TargetEntity targetEntity) { return Target.builder() .id(targetEntity.getId()) .nickname(targetEntity.getNickname()) .position(targetEntity.getPosition()) .job(targetEntity.getJob()) + .bookmarkedSurveys(toSurveyBookmark(targetEntity.getBookmarkedSurveys())) .build(); } + + public static Set toSurveyBookmark(Set surveyBookmarkEntities) { + return surveyBookmarkEntities.stream() + .map(surveyBookmarkEntity -> new SurveyBookmark(surveyBookmarkEntity.getSurveyId())) + .collect(Collectors.toSet()); + } } diff --git a/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/SurveyBookmarkReplaceController.java b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/SurveyBookmarkReplaceController.java index 8a8cc79e..9daf08cf 100644 --- a/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/SurveyBookmarkReplaceController.java +++ b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/SurveyBookmarkReplaceController.java @@ -1,7 +1,7 @@ package me.nalab.survey.web.adaptor.bookmark; import lombok.RequiredArgsConstructor; -import me.nalab.survey.application.port.in.web.bookmark.SurveyBookmarkReplaceUseCase; +import me.nalab.survey.application.port.in.web.bookmark.SurveyBookmarkUseCase; import me.nalab.survey.web.adaptor.bookmark.response.SurveyBookmarkResponse; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PathVariable; @@ -16,7 +16,7 @@ @RequiredArgsConstructor public class SurveyBookmarkReplaceController { - private final SurveyBookmarkReplaceUseCase surveyBookmarkReplaceUseCase; + private final SurveyBookmarkUseCase surveyBookmarkReplaceUseCase; @ResponseStatus(HttpStatus.OK) @PostMapping("/surveys/{survey_id}/bookmarks") From 6c8a6eed5f4898666c98c91d7414e806067d8287 Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Mon, 26 Feb 2024 09:27:02 +0900 Subject: [PATCH 14/17] [feat]: Gallery Preview of logged user (#375) --- .../JwtDecryptInterceptorConfigurer.java | 1 + gallery/build.gradle | 3 + .../me/nalab/gallery/app/GalleryDtoMapper.kt | 97 +++++++++++ .../me/nalab/gallery/app/GalleryPreviewApp.kt | 23 +++ .../gallery/controller/GalleryController.kt | 23 +++ .../kotlin/me/nalab/gallery/domain/Job.kt | 1 + .../domain/response/GalleryPreviewDto.kt | 37 +++++ .../gallery/app/GalleryPreviewAppTest.kt | 113 +++++++++++++ .../me/nalab/gallery/app/SurveyFixture.kt | 145 +++++++++++++++++ .../domain/GalleryPreviewDtoFixture.kt | 51 ++++++ gradle.properties | 4 + support/e2e/v1_6_find_gallery_preview.hurl | 153 ++++++++++++++++++ .../in/web/survey/find/SurveyFindUseCase.java | 7 + .../survey/find/SurveyFindPort.java | 8 + .../service/find/SurveyFindService.java | 6 + .../jpa/adaptor/find/SurveyFindAdaptor.java | 13 +- .../find/repository/SurveyFindRepository.java | 3 + 17 files changed, 685 insertions(+), 3 deletions(-) create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/app/GalleryDtoMapper.kt create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/app/GalleryPreviewApp.kt create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/domain/response/GalleryPreviewDto.kt create mode 100644 gallery/src/test/kotlin/me/nalab/gallery/app/GalleryPreviewAppTest.kt create mode 100644 gallery/src/test/kotlin/me/nalab/gallery/app/SurveyFixture.kt create mode 100644 gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryPreviewDtoFixture.kt create mode 100644 support/e2e/v1_6_find_gallery_preview.hurl diff --git a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java index b6fd40dc..a5e357c8 100644 --- a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java +++ b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java @@ -27,6 +27,7 @@ public class JwtDecryptInterceptorConfigurer implements WebMvcConfigurer { "/v2/surveys/*/feedbacks", "/v1/feedbacks/bookmarks", "/v1/users", + "/v1/gallerys/previews", "/v1/surveys/*/bookmarks", }; diff --git a/gallery/build.gradle b/gallery/build.gradle index 0eb0d000..9673fa6d 100644 --- a/gallery/build.gradle +++ b/gallery/build.gradle @@ -4,9 +4,12 @@ repositories { dependencies { implementation project(":core:data") + implementation project(":survey:survey-application") implementation "org.springframework.boot:spring-boot-starter-web" implementation "org.springframework.boot:spring-boot-starter-data-jpa" + testImplementation "com.ninja-squad:springmockk:${springMockkVersion}" + testRuntimeOnly "com.h2database:h2" } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryDtoMapper.kt b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryDtoMapper.kt new file mode 100644 index 00000000..613fc679 --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryDtoMapper.kt @@ -0,0 +1,97 @@ +package me.nalab.gallery.app + +import me.nalab.gallery.domain.response.GalleryPreviewDto +import me.nalab.survey.application.common.feedback.dto.BookmarkDto +import me.nalab.survey.application.common.feedback.dto.ChoiceFormQuestionFeedbackDto +import me.nalab.survey.application.common.feedback.dto.FeedbackDto +import me.nalab.survey.application.common.feedback.dto.ShortFormQuestionFeedbackDto +import me.nalab.survey.application.common.survey.dto.ChoiceFormQuestionDto +import me.nalab.survey.application.common.survey.dto.ChoiceFormQuestionDtoType +import me.nalab.survey.application.common.survey.dto.SurveyDto +import me.nalab.survey.application.common.survey.dto.TargetDto + +fun toGalleryPreviewDto( + target: TargetDto, + survey: SurveyDto, + feedbacks: List, +): GalleryPreviewDto { + return GalleryPreviewDto( + target = GalleryPreviewDto.Target( + targetId = target.id.toString(), + nickname = target.nickname, + position = target.position, + ), + survey = GalleryPreviewDto.Survey( + surveyId = survey.id.toString(), + feedbackCount = feedbacks.size, + bookmarkedCount = 0, + feedbacks = findLatestBookmarkedReply(feedbacks), + tendencies = findTendencies(survey, feedbacks), + ) + ) +} + +private fun findLatestBookmarkedReply(feedbacks: List): List { + return feedbacks.filterBookmarkedFeedback() + .mapBookmarkedWithReply() + .sortedByDescending { (bookmark, _) -> bookmark.bookmarkedAt } + .firstOrNull() + ?.second ?: emptyList() +} + +private fun List.filterBookmarkedFeedback(): List { + return this.filter { feedback -> + feedback.formQuestionFeedbackDtoableList.any { formQuestionFeedback -> + formQuestionFeedback.bookmarkDto.isBookmarked + } + } +} + +private fun List.mapBookmarkedWithReply(): List>> { + return this.flatMap { bookmarkedFeedback -> + bookmarkedFeedback.formQuestionFeedbackDtoableList + .filterIsInstance() + .map { shortQuestionFeedback -> shortQuestionFeedback.bookmarkDto to shortQuestionFeedback.replyList } + } +} + +private fun findTendencies( + survey: SurveyDto, + feedbacks: List, +): List { + val tendencyQuestion = survey.formQuestionDtoableList + .filterIsInstance() + .find { it.choiceFormQuestionDtoType == ChoiceFormQuestionDtoType.TENDENCY } + ?: error("필수 유형 Tendency 를 찾을 수 없습니다.") + + val countPerTendency = mutableMapOf() + + feedbacks.mapTendencyFeedbacks(tendencyQuestion) + .forEach { tendencyFeedback -> + tendencyQuestion.choiceDtoList + .filter { choice -> + tendencyFeedback.selectedChoiceIdSet.contains(choice.id) + } + .map { it.content } + .forEach { tendencyContent -> + countPerTendency[tendencyContent] = + countPerTendency.getOrDefault(tendencyContent, 0) + 1 + } + } + + return countPerTendency.map { (name, count) -> + GalleryPreviewDto.Survey.Tendency(name, count) + } +} + +private fun List.mapTendencyFeedbacks( + tendencyQuestion: ChoiceFormQuestionDto, +): List { + return this.flatMap { + it.formQuestionFeedbackDtoableList + .filterIsInstance() + .filter { choiceQuestionFeedback -> + choiceQuestionFeedback.questionId == tendencyQuestion.id + } + } +} diff --git a/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryPreviewApp.kt b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryPreviewApp.kt new file mode 100644 index 00000000..66ba77e2 --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryPreviewApp.kt @@ -0,0 +1,23 @@ +package me.nalab.gallery.app + +import me.nalab.gallery.domain.response.GalleryPreviewDto +import me.nalab.survey.application.port.`in`.web.findfeedback.FeedbackFindUseCase +import me.nalab.survey.application.port.`in`.web.survey.find.SurveyFindUseCase +import me.nalab.survey.application.port.`in`.web.target.find.TargetFindUseCase +import org.springframework.stereotype.Service + +@Service +class GalleryPreviewApp( + private val targetFindUseCase: TargetFindUseCase, + private val surveyFindUseCase: SurveyFindUseCase, + private val feedbackFindUseCase: FeedbackFindUseCase, +) { + + fun findGalleryPreview(targetId: Long): GalleryPreviewDto { + val target = targetFindUseCase.findTarget(targetId) + val survey = surveyFindUseCase.getSurveyByTargetId(targetId) + val feedbacks = feedbackFindUseCase.findAllFeedbackDtoBySurveyId(survey.id) + + return toGalleryPreviewDto(target, survey, feedbacks) + } +} diff --git a/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt b/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt new file mode 100644 index 00000000..2dc0c479 --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt @@ -0,0 +1,23 @@ +package me.nalab.gallery.controller + +import me.nalab.gallery.app.GalleryPreviewApp +import me.nalab.gallery.domain.response.GalleryPreviewDto +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestAttribute +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/v1/gallerys") +class GalleryController( + private val galleryPreviewApp: GalleryPreviewApp, +) { + + @GetMapping("/previews") + @ResponseStatus(HttpStatus.OK) + fun getGalleryPreview(@RequestAttribute("logined") targetId: Long): GalleryPreviewDto = + galleryPreviewApp.findGalleryPreview(targetId) + +} diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/Job.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/Job.kt index 54a94e0d..058960b6 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/domain/Job.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/Job.kt @@ -4,4 +4,5 @@ enum class Job { PM, DEVELOPER, DESIGNER, + OTHERS, } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/response/GalleryPreviewDto.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/response/GalleryPreviewDto.kt new file mode 100644 index 00000000..a6769e4e --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/response/GalleryPreviewDto.kt @@ -0,0 +1,37 @@ +package me.nalab.gallery.domain.response + +import com.fasterxml.jackson.annotation.JsonProperty +import me.nalab.gallery.domain.Job + +data class GalleryPreviewDto( + val target: Target, + val survey: Survey, +) { + + data class Target( + @JsonProperty("target_id") + val targetId: String, + val nickname: String, + val position: String?, + val job: Job = Job.OTHERS, + @JsonProperty("image_url") + val imageUrl: String = "empty_image", + ) + + data class Survey( + @JsonProperty("survey_id") + val surveyId: String, + @JsonProperty("feedback_count") + val feedbackCount: Int, + @JsonProperty("bookmarked_count") + val bookmarkedCount: Int, + val feedbacks: List, + val tendencies: List, + ) { + data class Tendency( + val name: String, + val count: Int, + ) + } + +} diff --git a/gallery/src/test/kotlin/me/nalab/gallery/app/GalleryPreviewAppTest.kt b/gallery/src/test/kotlin/me/nalab/gallery/app/GalleryPreviewAppTest.kt new file mode 100644 index 00000000..0bf76dab --- /dev/null +++ b/gallery/src/test/kotlin/me/nalab/gallery/app/GalleryPreviewAppTest.kt @@ -0,0 +1,113 @@ +package me.nalab.gallery.app + +import com.ninjasquad.springmockk.MockkBean +import io.kotest.core.annotation.DisplayName +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.equality.shouldBeEqualUsingFields +import io.mockk.every +import me.nalab.gallery.domain.galleryPreviewDto +import me.nalab.gallery.domain.galleryPreviewDtoSurvey +import me.nalab.gallery.domain.galleryPreviewDtoTarget +import me.nalab.survey.application.port.`in`.web.findfeedback.FeedbackFindUseCase +import me.nalab.survey.application.port.`in`.web.survey.find.SurveyFindUseCase +import me.nalab.survey.application.port.`in`.web.target.find.TargetFindUseCase +import org.springframework.test.context.ContextConfiguration + +@ContextConfiguration( + classes = [ + GalleryPreviewApp::class, + ] +) +@DisplayName("GalleryPreviewApp 의") +internal class GalleryPreviewAppTest( + private val galleryPreviewApp: GalleryPreviewApp, + @MockkBean(relaxed = true) private val targetFindUseCase: TargetFindUseCase, + @MockkBean(relaxed = true) private val surveyFindUseCase: SurveyFindUseCase, + @MockkBean(relaxed = true) private val feedbackFindUseCase: FeedbackFindUseCase, +) : DescribeSpec({ + + beforeEach { + every { targetFindUseCase.findTarget(DEFAULT_TARGET_ID) } returns defaultTargetDto + every { surveyFindUseCase.getSurveyByTargetId(DEFAULT_TARGET_ID) } returns defaultSurveyDto + every { feedbackFindUseCase.findAllFeedbackDtoBySurveyId(DEFAULT_SURVEY_ID) } returns listOf( + defaultFeedbackDto + ) + } + + describe("getGalleryPreview 메소드는") { + context("존재하는 타겟의 id를 받으면,") { + val expected = galleryPreviewDto( + target = galleryPreviewDtoTarget(DEFAULT_TARGET_ID.toString()), + survey = galleryPreviewDtoSurvey( + surveyId = DEFAULT_SURVEY_ID.toString(), + bookmarkedCount = 0 + ), + ) + + it("GalleryPreviewDto를 반환한다.") { + val response = galleryPreviewApp.findGalleryPreview(DEFAULT_TARGET_ID) + + response shouldBeEqualUsingFields expected + } + } + + context("피드백이 하나도 없다면,") { + val expected = galleryPreviewDto( + target = galleryPreviewDtoTarget(DEFAULT_TARGET_ID.toString()), + survey = galleryPreviewDtoSurvey( + surveyId = DEFAULT_SURVEY_ID.toString(), + feedbackCount = 0, + bookmarkedCount = 0, + feedbacks = emptyList(), + tendencies = emptyList(), + ) + ) + + + it("feedback이 비어있는 GalleryPreviewDto를 반환한다.") { + every { surveyFindUseCase.getSurveyByTargetId(DEFAULT_TARGET_ID) } returns surveyDto( + id = DEFAULT_SURVEY_ID, formQuestionDtos = listOf(choiceFormQuestionDto()) + ) + every { feedbackFindUseCase.findAllFeedbackDtoBySurveyId(DEFAULT_SURVEY_ID) } returns emptyList() + + val response = galleryPreviewApp.findGalleryPreview(DEFAULT_TARGET_ID) + + response shouldBeEqualUsingFields expected + } + } + } + +}) { + private companion object { + private const val DEFAULT_TARGET_ID = 1L + private const val DEFAULT_SURVEY_ID = 2L + private const val DEFAULT_FEEDBACK_ID = 3L + private const val TENDENCY_QUESTION_ID = 4L + private const val CUSTOM_SHORT_FORM_QUESTION_ID = 5L + + private val defaultTargetDto = targetDto(id = DEFAULT_TARGET_ID) + + private val formQuestionDtos = listOf( + choiceFormQuestionDto(id = TENDENCY_QUESTION_ID), + shortFormQuestionDto(id = CUSTOM_SHORT_FORM_QUESTION_ID), + ) + + private val defaultSurveyDto = + surveyDto(id = DEFAULT_SURVEY_ID, formQuestionDtos = formQuestionDtos) + + private val formQuestionFeedbackDtos = listOf( + choiceFormQuestionFeedbackDto(questionId = TENDENCY_QUESTION_ID), + shortFormQuestionFeedbackDto( + questionId = CUSTOM_SHORT_FORM_QUESTION_ID, + bookmarkDto = bookmarkDto(isBookmarked = true) + ), + ) + + private val defaultFeedbackDto = + feedbackDto( + id = DEFAULT_FEEDBACK_ID, + surveyId = DEFAULT_SURVEY_ID, + formQuestionFeedbackDtos = formQuestionFeedbackDtos, + ) + } +} diff --git a/gallery/src/test/kotlin/me/nalab/gallery/app/SurveyFixture.kt b/gallery/src/test/kotlin/me/nalab/gallery/app/SurveyFixture.kt new file mode 100644 index 00000000..177ade82 --- /dev/null +++ b/gallery/src/test/kotlin/me/nalab/gallery/app/SurveyFixture.kt @@ -0,0 +1,145 @@ +package me.nalab.gallery.app + +import me.nalab.survey.application.common.feedback.dto.* +import me.nalab.survey.application.common.survey.dto.* +import java.time.Instant + +fun targetDto( + id: Long = 0L, + nickname: String = "이준영", + position: String = "백엔드 엔지니어", +): TargetDto { + return TargetDto.builder() + .id(id) + .nickname(nickname) + .position(position) + .build() +} + +fun surveyDto( + id: Long = 0L, + targetId: Long = 0L, + formQuestionDtos: List = listOf( + choiceFormQuestionDto(), + shortFormQuestionDto() + ), + time: Instant = Instant.now(), +): SurveyDto { + return SurveyDto.builder() + .id(id) + .targetId(targetId) + .formQuestionDtoableList(formQuestionDtos) + .createdAt(time) + .updatedAt(time) + .build() +} + +fun choiceFormQuestionDto( + id: Long = 0L, + time: Instant = Instant.now(), + type: ChoiceFormQuestionDtoType = ChoiceFormQuestionDtoType.TENDENCY, + choices: List = listOf(choiceDto()), + maxSelectableCount: Int = 5, + order: Int = 1, +): ChoiceFormQuestionDto { + return ChoiceFormQuestionDto.builder() + .id(id) + .createdAt(time) + .updatedAt(time) + .choiceFormQuestionDtoType(type) + .choiceDtoList(choices) + .maxSelectableCount(maxSelectableCount) + .order(order) + .build() +} + +fun choiceDto( + id: Long = 0L, + content: String = "경청하는", + order: Int = 1, +): ChoiceDto { + return ChoiceDto.builder() + .id(id) + .content(content) + .order(order) + .build() +} + +fun shortFormQuestionDto( + id: Long = 0L, + time: Instant = Instant.now(), + type: ShortFormQuestionDtoType = ShortFormQuestionDtoType.CUSTOM, + title: String = "제가 고쳐야할점을 알려주세요.", + order: Int = 1, +): ShortFormQuestionDto { + return ShortFormQuestionDto.builder() + .id(id) + .createdAt(time) + .updatedAt(time) + .shortFormQuestionDtoType(type) + .order(order) + .title(title) + .build() +} + +fun feedbackDto( + id: Long = 0L, + surveyId: Long = 0L, + time: Instant = Instant.now(), + formQuestionFeedbackDtos: List = listOf( + choiceFormQuestionFeedbackDto(), + shortFormQuestionFeedbackDto() + ), + isRead: Boolean = true, +): FeedbackDto { + return FeedbackDto.builder() + .id(id) + .surveyId(surveyId) + .createdAt(time) + .updatedAt(time) + .formQuestionFeedbackDtoableList(formQuestionFeedbackDtos) + .isRead(isRead) + .build() +} + +fun choiceFormQuestionFeedbackDto( + id: Long = 0L, + questionId: Long = 0L, + isRead: Boolean = false, + bookmarkDto: BookmarkDto = bookmarkDto(), + selectedChoiceIdSet: Set = setOf(0L), +): ChoiceFormQuestionFeedbackDto { + return ChoiceFormQuestionFeedbackDto.builder() + .id(id) + .questionId(questionId) + .bookmarkDto(bookmarkDto) + .selectedChoiceIdSet(selectedChoiceIdSet) + .isRead(isRead) + .build() +} + +fun shortFormQuestionFeedbackDto( + id: Long = 1L, + questionId: Long = 0L, + isRead: Boolean = false, + bookmarkDto: BookmarkDto = bookmarkDto(), + replies: List = listOf("제가 고쳐야할점을 알려주세요 질문의", "응답입니다.") +): ShortFormQuestionFeedbackDto { + return ShortFormQuestionFeedbackDto.builder() + .id(id) + .bookmarkDto(bookmarkDto) + .replyList(replies) + .isRead(isRead) + .questionId(questionId) + .build() +} + +fun bookmarkDto( + isBookmarked: Boolean = false, + time: Instant = Instant.now(), +): BookmarkDto { + return BookmarkDto.builder() + .isBookmarked(isBookmarked) + .bookmarkedAt(time) + .build() +} diff --git a/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryPreviewDtoFixture.kt b/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryPreviewDtoFixture.kt new file mode 100644 index 00000000..eb89cd70 --- /dev/null +++ b/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryPreviewDtoFixture.kt @@ -0,0 +1,51 @@ +package me.nalab.gallery.domain + +import me.nalab.gallery.domain.response.GalleryPreviewDto + +fun galleryPreviewDto( + target: GalleryPreviewDto.Target = galleryPreviewDtoTarget(), + survey: GalleryPreviewDto.Survey = galleryPreviewDtoSurvey(), +): GalleryPreviewDto { + return GalleryPreviewDto( + target = target, + survey = survey, + ) +} + +fun galleryPreviewDtoTarget( + targetId: String = "0", + nickname: String = "이준영", + position: String = "백엔드 엔지니어", +): GalleryPreviewDto.Target { + return GalleryPreviewDto.Target( + targetId = targetId, + nickname = nickname, + position = position, + ) +} + +fun galleryPreviewDtoSurvey( + surveyId: String = "0", + feedbackCount: Int = 1, + bookmarkedCount: Int = 1, + feedbacks: List = listOf("제가 고쳐야할점을 알려주세요 질문의", "응답입니다."), + tendencies: List = listOf(galleryPreviewDtoSurveyTendency()), +): GalleryPreviewDto.Survey { + return GalleryPreviewDto.Survey( + surveyId = surveyId, + feedbackCount = feedbackCount, + bookmarkedCount = bookmarkedCount, + feedbacks = feedbacks, + tendencies = tendencies, + ) +} + +fun galleryPreviewDtoSurveyTendency( + name: String = "경청하는", + count: Int = 1, +): GalleryPreviewDto.Survey.Tendency { + return GalleryPreviewDto.Survey.Tendency( + name = name, + count = count, + ) +} diff --git a/gradle.properties b/gradle.properties index 0f56b445..4275f174 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,6 +8,7 @@ javaVersion=17 ### Spring dependency versions ### springBootVersion=2.7.11 springDependencyManagementVersion=1.0.15.RELEASE +springMockkVersion=4.0.0 ### Sonar settings ### sonarVersion=4.0.0.2929 @@ -30,3 +31,6 @@ kotlinVersion=1.9.22 ### Kotest ### kotestVersion=5.7.2 kotestExtensionSpringVersion=1.1.3 + +### RestAssured ### +restAssuredVersion=5.3.0 diff --git a/support/e2e/v1_6_find_gallery_preview.hurl b/support/e2e/v1_6_find_gallery_preview.hurl new file mode 100644 index 00000000..1a53bba0 --- /dev/null +++ b/support/e2e/v1_6_find_gallery_preview.hurl @@ -0,0 +1,153 @@ +POST http://nalab-server:8080/v1/oauth/default # Default provider를 통해서 로그인 진행 +{ + "nickname": "find_gallery", + "email": "hello@1234567" +} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.access_token" exists +jsonpath "$.token_type" exists + +[Captures] +token_type: jsonpath "$.token_type" +auth_token: jsonpath "$.access_token" + +########## + +POST http://nalab-server:8080/v1/surveys # 발급받은 토큰으로 survey를 생성한다. +Authorization: {{ token_type }} {{ auth_token }} +{ + "question_count": 2, + "question": [ + { + "type": "choice", + "form_type": "tendency", + "title": "저는 UI, UI, GUI 중에 어떤 분야를 가장 잘하는 것 같나요?", + "choices": [ + { + "content": "UI", + "order": 1 + }, + { + "content": "UX", + "order": 2 + }, + { + "content": "GUI", + "order": 3 + } + ], + "max_selectable_count": 1, + "order": 1 + }, + { + "type": "short", + "form_type": "strength", + "title": "저는 UX, UI, GUI 중에 어떤 분야에 더 강점이 있나요?", + "order": 2 + } + ] +} + +HTTP 201 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.survey_id" exists + +[Captures] +survey_id: jsonpath "$.survey_id" + +########## + +GET http://nalab-server:8080/v1/surveys/{{ survey_id }} # 생성된 survey를 조회하고, feedback을 남기기 위해 id를 저장한다. + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +[Captures] +tendency_question_id: jsonpath "$.question.[0].question_id" +tendency_question_choice_id: jsonpath "$.question.[0].choices.[0].choice_id" +strength_question_id: jsonpath "$.question.[1].question_id" + +########## + +POST http://nalab-server:8080/v1/feedbacks # 생성된 survey에 feedback을 남긴다. + +[QueryStringParams] +survey-id: {{ survey_id }} + +{ + "reviewer": { + "collaboration_experience": true, + "position": "pm" + }, + "question_feedback": [ + { + "question_id": {{ tendency_question_id }}, + "type": "choice", + "choices": [ + {{ tendency_question_choice_id }} + ] + }, + { + "question_id": {{ strength_question_id }}, + "type": "short", + "reply": [ + "Hello world" + ] + } + ] +} + +HTTP 201 +[Asserts] + +########## + +GET http://nalab-server:8080/v1/feedbacks # 북마크를 위해 feedback id 저장 +Authorization: {{ token_type }} {{ auth_token }} + +[QueryStringParams] +survey-id: {{ survey_id }} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +[Captures] +form_question_feedback_id: jsonpath "$.question_feedback.[0].feedbacks.[0].form_question_feedback_id" + +########## + +PATCH http://nalab-server:8080/v1/feedbacks/bookmarks # 북마크를 진행한다. +Authorization: {{ token_type }} {{ auth_token }} + +[QueryStringParams] +form-question-feedback-id: {{form_question_feedback_id}} + +########## + +GET http://nalab-server:8080/v1/gallerys/previews # 생성된 유저의 Gallery Preview 조회 +Authorization: {{ token_type }} {{ auth_token }} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.target.target_id" exists +jsonpath "$.target.nickname" == "find_gallery" +jsonpath "$.target.position" == null +jsonpath "$.target.job" == "OTHERS" +jsonpath "$.target.image_url" == "empty_image" + +jsonpath "$.survey.survey_id" == {{ survey_id }} +jsonpath "$.survey.feedback_count" == 1 +jsonpath "$.survey.bookmarked_count" == 0 +jsonpath "$.survey.feedbacks.[0]" == "Hello world" +jsonpath "$.survey.tendencies.[0].name" == "UI" +jsonpath "$.survey.tendencies.[0].count" == 1 diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/survey/find/SurveyFindUseCase.java b/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/survey/find/SurveyFindUseCase.java index 82435dbc..e65d58d6 100644 --- a/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/survey/find/SurveyFindUseCase.java +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/survey/find/SurveyFindUseCase.java @@ -14,4 +14,11 @@ public interface SurveyFindUseCase { */ SurveyDto findSurvey(Long surveyId); + /** + * targetId로 surveyDto 조회 + * @param targetId target의 id + * @return SurveyDto + */ + SurveyDto getSurveyByTargetId(Long targetId); + } diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/survey/find/SurveyFindPort.java b/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/survey/find/SurveyFindPort.java index 0a8d3af3..c135c2d3 100644 --- a/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/survey/find/SurveyFindPort.java +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/survey/find/SurveyFindPort.java @@ -16,4 +16,12 @@ public interface SurveyFindPort { * @return Optional 만약, 어떠한 surveyId도 없을 경우, Optional.empty() 를 반환해야 합니다. */ Optional findSurvey(Long surveyId); + + /** + * targetId로 survey를 조회합니다. + * + * @param targetId survey를 조회할 target의 ID + * @return Optional 만약, 어떠한 surveyId도 없을 경우, Optional.empty() 를 반환해야 합니다. + */ + Survey getSurveyByTargetId(Long targetId); } diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/service/find/SurveyFindService.java b/survey/survey-application/src/main/java/me/nalab/survey/application/service/find/SurveyFindService.java index 00b9171d..742ce57a 100644 --- a/survey/survey-application/src/main/java/me/nalab/survey/application/service/find/SurveyFindService.java +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/service/find/SurveyFindService.java @@ -33,4 +33,10 @@ public SurveyDto findSurvey(Long surveyId) { return SurveyDtoMapper.toSurveyDto(targetId, survey); } + + @Override + public SurveyDto getSurveyByTargetId(Long targetId) { + var survey = surveyFindPort.getSurveyByTargetId(targetId); + return SurveyDtoMapper.toSurveyDto(targetId, survey); + } } diff --git a/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/find/SurveyFindAdaptor.java b/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/find/SurveyFindAdaptor.java index c19cdc29..9c705e5a 100644 --- a/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/find/SurveyFindAdaptor.java +++ b/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/find/SurveyFindAdaptor.java @@ -1,15 +1,14 @@ package me.nalab.survey.jpa.adaptor.find; import java.util.Optional; - -import org.springframework.stereotype.Repository; - import lombok.RequiredArgsConstructor; import me.nalab.core.data.survey.SurveyEntity; +import me.nalab.survey.application.exception.TargetDoesNotHasSurveyException; import me.nalab.survey.application.port.out.persistence.survey.find.SurveyFindPort; import me.nalab.survey.domain.survey.Survey; import me.nalab.survey.jpa.adaptor.common.mapper.SurveyEntityMapper; import me.nalab.survey.jpa.adaptor.find.repository.SurveyFindRepository; +import org.springframework.stereotype.Repository; @Repository @RequiredArgsConstructor @@ -27,4 +26,12 @@ public Optional findSurvey(Long surveyId) { return Optional.of(survey); } + + @Override + public Survey getSurveyByTargetId(Long targetId) { + var surveyEntity = surveyFindRepository.findByTargetId(targetId) + .orElseThrow(() -> new TargetDoesNotHasSurveyException(targetId)); + + return SurveyEntityMapper.toSurvey(surveyEntity); + } } diff --git a/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/find/repository/SurveyFindRepository.java b/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/find/repository/SurveyFindRepository.java index 1889603f..9447f38b 100644 --- a/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/find/repository/SurveyFindRepository.java +++ b/survey/survey-jpa-adaptor/src/main/java/me/nalab/survey/jpa/adaptor/find/repository/SurveyFindRepository.java @@ -1,8 +1,11 @@ package me.nalab.survey.jpa.adaptor.find.repository; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import me.nalab.core.data.survey.SurveyEntity; public interface SurveyFindRepository extends JpaRepository { + + Optional findByTargetId(Long targetId); } From ee4accf56dbe4ebb8f7088c68a48d167184af717 Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Wed, 28 Feb 2024 16:04:47 +0900 Subject: [PATCH 15/17] =?UTF-8?q?[feat]=20:=20=EB=82=B4=20Gallery=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#379)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dojin --- api/acceptance-test/build.gradle | 1 + .../java/me/nalab/api/NalabApplication.java | 2 + .../migration/V9__gallery_target_id_idx.sql | 3 + .../JwtDecryptInterceptorConfigurer.java | 1 + gallery/build.gradle | 2 + .../me/nalab/gallery/app/GalleryDtoMapper.kt | 24 ++- .../gallery/app/GalleryPreviewDtoMapper.kt | 97 +++++++++ .../nalab/gallery/app/GalleryRegisterApp.kt | 51 +++++ .../gallery/controller/GalleryController.kt | 19 +- .../request/GalleryRegisterRequest.kt | 9 + .../kotlin/me/nalab/gallery/domain/Gallery.kt | 26 ++- .../nalab/gallery/domain/GalleryRepository.kt | 14 ++ .../me/nalab/gallery/domain/GalleryService.kt | 24 +++ .../gallery/domain/response/GalleryDto.kt | 38 ++++ .../gallery/infra/SurveyBookmarkedListener.kt | 34 +++ .../me/nalab/gallery/domain/GalleryFixture.kt | 19 ++ .../gallery/domain/GalleryServiceTest.kt | 54 +++++ support/e2e/v1_9_register_gallery.hurl | 198 ++++++++++++++++++ .../common/survey/dto/TargetDto.java | 3 +- .../common/survey/mapper/TargetDtoMapper.java | 4 + .../bookmark/SurveyBookmarkListenPort.java | 6 + .../bookmark/SurveyBookmarkService.java | 4 + 22 files changed, 616 insertions(+), 17 deletions(-) create mode 100644 api/src/main/resources/db/migration/V9__gallery_target_id_idx.sql create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/app/GalleryPreviewDtoMapper.kt create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/app/GalleryRegisterApp.kt create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/controller/request/GalleryRegisterRequest.kt create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryRepository.kt create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/domain/response/GalleryDto.kt create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/infra/SurveyBookmarkedListener.kt create mode 100644 gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryFixture.kt create mode 100644 gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryServiceTest.kt create mode 100644 support/e2e/v1_9_register_gallery.hurl create mode 100644 survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/bookmark/SurveyBookmarkListenPort.java diff --git a/api/acceptance-test/build.gradle b/api/acceptance-test/build.gradle index e062598f..fddc6163 100644 --- a/api/acceptance-test/build.gradle +++ b/api/acceptance-test/build.gradle @@ -11,6 +11,7 @@ dependencies { testImplementation project(':user:user-application') testImplementation project(':user:user-jpa-adapter') testImplementation project(':user:user-web-adaptor') + testImplementation project(':gallery') testImplementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/api/src/main/java/me/nalab/api/NalabApplication.java b/api/src/main/java/me/nalab/api/NalabApplication.java index 48351bfd..6a7d55c5 100644 --- a/api/src/main/java/me/nalab/api/NalabApplication.java +++ b/api/src/main/java/me/nalab/api/NalabApplication.java @@ -5,7 +5,9 @@ import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.context.annotation.ComponentScan; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.scheduling.annotation.EnableAsync; +@EnableAsync @SpringBootApplication @ComponentScan("me.nalab") @EnableJpaRepositories(basePackages = {"me.nalab"}) diff --git a/api/src/main/resources/db/migration/V9__gallery_target_id_idx.sql b/api/src/main/resources/db/migration/V9__gallery_target_id_idx.sql new file mode 100644 index 00000000..4e2f84a2 --- /dev/null +++ b/api/src/main/resources/db/migration/V9__gallery_target_id_idx.sql @@ -0,0 +1,3 @@ +ALTER TABLE gallery ADD `version` BIGINT; + +CREATE UNIQUE INDEX galley_idx_target_id ON gallery(target_id) diff --git a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java index a5e357c8..fde209cd 100644 --- a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java +++ b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java @@ -29,6 +29,7 @@ public class JwtDecryptInterceptorConfigurer implements WebMvcConfigurer { "/v1/users", "/v1/gallerys/previews", "/v1/surveys/*/bookmarks", + "/v1/gallerys", }; @Override diff --git a/gallery/build.gradle b/gallery/build.gradle index 9673fa6d..1a0e5aae 100644 --- a/gallery/build.gradle +++ b/gallery/build.gradle @@ -4,6 +4,8 @@ repositories { dependencies { implementation project(":core:data") + implementation project(":core:id-generator:id-core") + implementation project(":core:time") implementation project(":survey:survey-application") implementation "org.springframework.boot:spring-boot-starter-web" diff --git a/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryDtoMapper.kt b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryDtoMapper.kt index 613fc679..8cd4b77c 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryDtoMapper.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryDtoMapper.kt @@ -1,6 +1,7 @@ package me.nalab.gallery.app -import me.nalab.gallery.domain.response.GalleryPreviewDto +import me.nalab.gallery.domain.Gallery +import me.nalab.gallery.domain.response.GalleryDto import me.nalab.survey.application.common.feedback.dto.BookmarkDto import me.nalab.survey.application.common.feedback.dto.ChoiceFormQuestionFeedbackDto import me.nalab.survey.application.common.feedback.dto.FeedbackDto @@ -10,21 +11,24 @@ import me.nalab.survey.application.common.survey.dto.ChoiceFormQuestionDtoType import me.nalab.survey.application.common.survey.dto.SurveyDto import me.nalab.survey.application.common.survey.dto.TargetDto -fun toGalleryPreviewDto( +fun toGalleryDto( + gallery: Gallery, target: TargetDto, survey: SurveyDto, feedbacks: List, -): GalleryPreviewDto { - return GalleryPreviewDto( - target = GalleryPreviewDto.Target( - targetId = target.id.toString(), +): GalleryDto { + return GalleryDto( + galleryId = gallery.id, + target = GalleryDto.Target( + targetId = gallery.getTargetId().toString(), nickname = target.nickname, + job = gallery.getJob(), position = target.position, ), - survey = GalleryPreviewDto.Survey( + survey = GalleryDto.Survey( surveyId = survey.id.toString(), feedbackCount = feedbacks.size, - bookmarkedCount = 0, + bookmarkedCount = gallery.getBookmarkedCount(), feedbacks = findLatestBookmarkedReply(feedbacks), tendencies = findTendencies(survey, feedbacks), ) @@ -58,7 +62,7 @@ private fun List.mapBookmarkedWithReply(): List, -): List { +): List { val tendencyQuestion = survey.formQuestionDtoableList .filterIsInstance() .find { it.choiceFormQuestionDtoType == ChoiceFormQuestionDtoType.TENDENCY } @@ -80,7 +84,7 @@ private fun findTendencies( } return countPerTendency.map { (name, count) -> - GalleryPreviewDto.Survey.Tendency(name, count) + GalleryDto.Survey.Tendency(name, count) } } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryPreviewDtoMapper.kt b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryPreviewDtoMapper.kt new file mode 100644 index 00000000..613fc679 --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryPreviewDtoMapper.kt @@ -0,0 +1,97 @@ +package me.nalab.gallery.app + +import me.nalab.gallery.domain.response.GalleryPreviewDto +import me.nalab.survey.application.common.feedback.dto.BookmarkDto +import me.nalab.survey.application.common.feedback.dto.ChoiceFormQuestionFeedbackDto +import me.nalab.survey.application.common.feedback.dto.FeedbackDto +import me.nalab.survey.application.common.feedback.dto.ShortFormQuestionFeedbackDto +import me.nalab.survey.application.common.survey.dto.ChoiceFormQuestionDto +import me.nalab.survey.application.common.survey.dto.ChoiceFormQuestionDtoType +import me.nalab.survey.application.common.survey.dto.SurveyDto +import me.nalab.survey.application.common.survey.dto.TargetDto + +fun toGalleryPreviewDto( + target: TargetDto, + survey: SurveyDto, + feedbacks: List, +): GalleryPreviewDto { + return GalleryPreviewDto( + target = GalleryPreviewDto.Target( + targetId = target.id.toString(), + nickname = target.nickname, + position = target.position, + ), + survey = GalleryPreviewDto.Survey( + surveyId = survey.id.toString(), + feedbackCount = feedbacks.size, + bookmarkedCount = 0, + feedbacks = findLatestBookmarkedReply(feedbacks), + tendencies = findTendencies(survey, feedbacks), + ) + ) +} + +private fun findLatestBookmarkedReply(feedbacks: List): List { + return feedbacks.filterBookmarkedFeedback() + .mapBookmarkedWithReply() + .sortedByDescending { (bookmark, _) -> bookmark.bookmarkedAt } + .firstOrNull() + ?.second ?: emptyList() +} + +private fun List.filterBookmarkedFeedback(): List { + return this.filter { feedback -> + feedback.formQuestionFeedbackDtoableList.any { formQuestionFeedback -> + formQuestionFeedback.bookmarkDto.isBookmarked + } + } +} + +private fun List.mapBookmarkedWithReply(): List>> { + return this.flatMap { bookmarkedFeedback -> + bookmarkedFeedback.formQuestionFeedbackDtoableList + .filterIsInstance() + .map { shortQuestionFeedback -> shortQuestionFeedback.bookmarkDto to shortQuestionFeedback.replyList } + } +} + +private fun findTendencies( + survey: SurveyDto, + feedbacks: List, +): List { + val tendencyQuestion = survey.formQuestionDtoableList + .filterIsInstance() + .find { it.choiceFormQuestionDtoType == ChoiceFormQuestionDtoType.TENDENCY } + ?: error("필수 유형 Tendency 를 찾을 수 없습니다.") + + val countPerTendency = mutableMapOf() + + feedbacks.mapTendencyFeedbacks(tendencyQuestion) + .forEach { tendencyFeedback -> + tendencyQuestion.choiceDtoList + .filter { choice -> + tendencyFeedback.selectedChoiceIdSet.contains(choice.id) + } + .map { it.content } + .forEach { tendencyContent -> + countPerTendency[tendencyContent] = + countPerTendency.getOrDefault(tendencyContent, 0) + 1 + } + } + + return countPerTendency.map { (name, count) -> + GalleryPreviewDto.Survey.Tendency(name, count) + } +} + +private fun List.mapTendencyFeedbacks( + tendencyQuestion: ChoiceFormQuestionDto, +): List { + return this.flatMap { + it.formQuestionFeedbackDtoableList + .filterIsInstance() + .filter { choiceQuestionFeedback -> + choiceQuestionFeedback.questionId == tendencyQuestion.id + } + } +} diff --git a/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryRegisterApp.kt b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryRegisterApp.kt new file mode 100644 index 00000000..a2bed44c --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryRegisterApp.kt @@ -0,0 +1,51 @@ +package me.nalab.gallery.app + +import me.nalab.core.idgenerator.idcore.IdGenerator +import me.nalab.core.time.TimeUtil +import me.nalab.gallery.domain.Gallery +import me.nalab.gallery.domain.GalleryService +import me.nalab.gallery.domain.Job +import me.nalab.gallery.domain.response.GalleryDto +import me.nalab.survey.application.common.survey.dto.SurveyDto +import me.nalab.survey.application.common.survey.dto.TargetDto +import me.nalab.survey.application.port.`in`.web.findfeedback.FeedbackFindUseCase +import me.nalab.survey.application.port.`in`.web.survey.find.SurveyFindUseCase +import me.nalab.survey.application.port.`in`.web.target.find.TargetFindUseCase +import org.springframework.stereotype.Service + +@Service +class GalleryRegisterApp( + private val idGenerator: IdGenerator, + private val targetFindUseCase: TargetFindUseCase, + private val surveyFindUseCase: SurveyFindUseCase, + private val feedbackFindUseCase: FeedbackFindUseCase, + private val galleryService: GalleryService, +) { + + fun registerGalleryByTargetId(targetId: Long, job: String): GalleryDto { + val target = targetFindUseCase.findTarget(targetId) + val survey = surveyFindUseCase.getSurveyByTargetId(targetId) + + val gallery = galleryService.registerGallery(toGallery(job, target, survey)) + + val feedbacks = feedbackFindUseCase.findAllFeedbackDtoBySurveyId(survey.id) + + return toGalleryDto(gallery, target, survey, feedbacks) + } + + private fun toGallery( + job: String, + target: TargetDto, + survey: SurveyDto, + ): Gallery { + return Gallery( + id = idGenerator.generate(), + targetId = target.id, + job = Job.valueOf(job.uppercase()), + surveyId = survey.id, + bookmarkedCount = target.bookmarkedSurveys.size, + updateOrder = TimeUtil.toInstant(), + ) + } + +} diff --git a/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt b/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt index 2dc0c479..96ef14cf 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt @@ -1,18 +1,18 @@ package me.nalab.gallery.controller import me.nalab.gallery.app.GalleryPreviewApp +import me.nalab.gallery.app.GalleryRegisterApp +import me.nalab.gallery.controller.request.GalleryRegisterRequest +import me.nalab.gallery.domain.response.GalleryDto import me.nalab.gallery.domain.response.GalleryPreviewDto import org.springframework.http.HttpStatus -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestAttribute -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.bind.annotation.* @RestController @RequestMapping("/v1/gallerys") class GalleryController( private val galleryPreviewApp: GalleryPreviewApp, + private val galleryRegisterApp: GalleryRegisterApp, ) { @GetMapping("/previews") @@ -20,4 +20,13 @@ class GalleryController( fun getGalleryPreview(@RequestAttribute("logined") targetId: Long): GalleryPreviewDto = galleryPreviewApp.findGalleryPreview(targetId) + @PostMapping + @ResponseStatus(HttpStatus.OK) + fun registerGallery( + @RequestAttribute("logined") targetId: Long, + @RequestBody request: GalleryRegisterRequest, + ): GalleryDto { + return galleryRegisterApp.registerGalleryByTargetId(targetId, request.job) + } + } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/controller/request/GalleryRegisterRequest.kt b/gallery/src/main/kotlin/me/nalab/gallery/controller/request/GalleryRegisterRequest.kt new file mode 100644 index 00000000..318feda5 --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/controller/request/GalleryRegisterRequest.kt @@ -0,0 +1,9 @@ +package me.nalab.gallery.controller.request + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty + +data class GalleryRegisterRequest @JsonCreator constructor( + @JsonProperty("job") + val job: String, +) diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/Gallery.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/Gallery.kt index a61000f5..bc268ef4 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/domain/Gallery.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/Gallery.kt @@ -1,6 +1,7 @@ package me.nalab.gallery.domain import me.nalab.core.data.common.TimeBaseEntity +import me.nalab.core.time.TimeUtil import java.time.Instant import javax.persistence.* @@ -19,4 +20,27 @@ class Gallery( @Column(name = "update_order", columnDefinition = "TIMESTAMP(6)", nullable = false) private var updateOrder: Instant, -) : TimeBaseEntity() + + @Version + private var version: Long? = null, +) : TimeBaseEntity() { + + constructor( + id: Long, + targetId: Long, + job: Job, + surveyId: Long, + bookmarkedCount: Int = 0, + updateOrder: Instant = TimeUtil.toInstant(), + ): this(id, Target(targetId, job), Survey(surveyId, bookmarkedCount), updateOrder) + + fun getTargetId(): Long = target.targetId + + fun getJob(): Job = target.job + + fun getBookmarkedCount(): Int = survey.bookmarkedCount + + fun increaseBookmarkedCount() { + survey.bookmarkedCount++ + } +} diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryRepository.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryRepository.kt new file mode 100644 index 00000000..508f51f0 --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryRepository.kt @@ -0,0 +1,14 @@ +package me.nalab.gallery.domain + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Lock +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param +import javax.persistence.LockModeType + +interface GalleryRepository : JpaRepository { + + @Lock(LockModeType.OPTIMISTIC) + @Query("select g from Gallery as g where g.target.targetId = :targetId") + fun findByTargetIdOrNull(@Param("targetId") targetId: Long): Gallery? +} diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt new file mode 100644 index 00000000..f22f3aa3 --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt @@ -0,0 +1,24 @@ +package me.nalab.gallery.domain + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class GalleryService( + private val galleryRepository: GalleryRepository, +) { + + @Transactional + fun registerGallery(gallery: Gallery): Gallery { + require(galleryRepository.findByTargetIdOrNull(gallery.getTargetId()) == null) { + "target \"${gallery.getTargetId()}\" 이 이미 Gallery에 등록되어있습니다." + } + + return galleryRepository.save(gallery) + } + + @Transactional + fun increaseBookmarkCount(targetId: Long) { + galleryRepository.findByTargetIdOrNull(targetId)?.increaseBookmarkedCount() + } +} diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/response/GalleryDto.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/response/GalleryDto.kt new file mode 100644 index 00000000..a54a1810 --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/response/GalleryDto.kt @@ -0,0 +1,38 @@ +package me.nalab.gallery.domain.response + +import com.fasterxml.jackson.annotation.JsonProperty +import me.nalab.gallery.domain.Job + +data class GalleryDto( + @JsonProperty("gallery_id") + val galleryId: Long, + val target: Target, + val survey: Survey, +) { + + data class Target( + @JsonProperty("target_id") + val targetId: String, + val nickname: String, + val position: String?, + val job: Job, + @JsonProperty("image_url") + val imageUrl: String = "empty_image", + ) + + data class Survey( + @JsonProperty("survey_id") + val surveyId: String, + @JsonProperty("feedback_count") + val feedbackCount: Int, + @JsonProperty("bookmarked_count") + val bookmarkedCount: Int, + val feedbacks: List, + val tendencies: List, + ) { + data class Tendency( + val name: String, + val count: Int, + ) + } +} diff --git a/gallery/src/main/kotlin/me/nalab/gallery/infra/SurveyBookmarkedListener.kt b/gallery/src/main/kotlin/me/nalab/gallery/infra/SurveyBookmarkedListener.kt new file mode 100644 index 00000000..1623d0e5 --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/infra/SurveyBookmarkedListener.kt @@ -0,0 +1,34 @@ +package me.nalab.gallery.infra + +import me.nalab.gallery.domain.GalleryService +import me.nalab.survey.application.port.out.persistence.bookmark.SurveyBookmarkListenPort +import org.springframework.dao.OptimisticLockingFailureException +import org.springframework.scheduling.annotation.Async +import org.springframework.stereotype.Service +import kotlin.random.Random + +@Service +class SurveyBookmarkedListener( + private val galleryService: GalleryService, +) : SurveyBookmarkListenPort { + + @Async + override fun listenBookmarked(targetId: Long) { + runCatching { + galleryService.increaseBookmarkCount(targetId) + }.recoverCatching { + when (it is OptimisticLockingFailureException) { + true -> { + waitJitter() + listenBookmarked(targetId) + } + + false -> throw it + } + } + } + + private fun waitJitter() { + Thread.sleep(Random.nextLong(500, 1000)) + } +} diff --git a/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryFixture.kt b/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryFixture.kt new file mode 100644 index 00000000..2728cd14 --- /dev/null +++ b/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryFixture.kt @@ -0,0 +1,19 @@ +package me.nalab.gallery.domain + +import java.time.Instant + +fun gallery( + id: Long = 0L, + targetId: Long = 0L, + job: Job = Job.OTHERS, + surveyId: Long = 0L, + bookmarkedCount: Int = 0, + updateOrder: Instant = Instant.now(), +): Gallery = Gallery( + id = id, + targetId = targetId, + job = job, + surveyId = surveyId, + bookmarkedCount = bookmarkedCount, + updateOrder = updateOrder, +) diff --git a/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryServiceTest.kt b/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryServiceTest.kt new file mode 100644 index 00000000..d19a2551 --- /dev/null +++ b/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryServiceTest.kt @@ -0,0 +1,54 @@ +package me.nalab.gallery.domain + +import io.kotest.assertions.throwables.shouldThrowMessage +import io.kotest.core.annotation.DisplayName +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.equality.shouldBeEqualUsingFields +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration + +@DataJpaTest +@EnableJpaRepositories +@EntityScan(value = ["me.nalab.core.data", "me.nalab.gallery.domain"]) +@DisplayName("GalleryService 클래스의") +@ContextConfiguration(classes = [GalleryService::class]) +internal class GalleryServiceTest( + private val galleryService: GalleryService, +) : DescribeSpec({ + + beforeSpec { + galleryService.registerGallery(gallery(id = EXIST_GALLERY_ID, targetId = EXIST_TARGET_ID, surveyId = EXIST_SURVEY_ID)) + } + + describe("registerGallery 메소드는") { + context("Gallery에 등록되지 않은 target의 Gallery를 입력받으면,") { + val gallery = gallery(targetId = NOT_EXIST_TARGET_ID) + + it("Gallery를 저장하고 반환한다.") { + val result = galleryService.registerGallery(gallery) + + result shouldBeEqualUsingFields gallery + } + } + + context("이미 Gallery에 등록된 target의 Gallery를 입력받으면,") { + val existGallery = gallery(targetId = EXIST_TARGET_ID) + + it("IllegalArgumentException을 던진다.") { + shouldThrowMessage("target \"$EXIST_GALLERY_ID\" 이 이미 Gallery에 등록되어있습니다.") { + galleryService.registerGallery(existGallery) + } + } + } + } +}) { + + companion object { + private const val NOT_EXIST_TARGET_ID = 1L + private const val EXIST_GALLERY_ID = 100L + private const val EXIST_TARGET_ID = 100L + private const val EXIST_SURVEY_ID = 100L + } +} diff --git a/support/e2e/v1_9_register_gallery.hurl b/support/e2e/v1_9_register_gallery.hurl new file mode 100644 index 00000000..c9108087 --- /dev/null +++ b/support/e2e/v1_9_register_gallery.hurl @@ -0,0 +1,198 @@ +POST http://nalab-server:8080/v1/oauth/default # Default provider를 통해서 로그인 진행 +{ + "nickname": "register_gallery", + "email": "registergallery@123456" +} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.access_token" exists +jsonpath "$.token_type" exists + +[Captures] +token_type: jsonpath "$.token_type" +auth_token: jsonpath "$.access_token" + +########## + +POST http://nalab-server:8080/v1/surveys # 발급받은 토큰으로 survey를 생성한다. +Authorization: {{ token_type }} {{ auth_token }} +{ + "question_count": 2, + "question": [ + { + "type": "choice", + "form_type": "tendency", + "title": "저는 UI, UI, GUI 중에 어떤 분야를 가장 잘하는 것 같나요?", + "choices": [ + { + "content": "UI", + "order": 1 + }, + { + "content": "UX", + "order": 2 + }, + { + "content": "GUI", + "order": 3 + } + ], + "max_selectable_count": 1, + "order": 1 + }, + { + "type": "short", + "form_type": "strength", + "title": "저는 UX, UI, GUI 중에 어떤 분야에 더 강점이 있나요?", + "order": 2 + } + ] +} + +HTTP 201 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.survey_id" exists + +[Captures] +survey_id: jsonpath "$.survey_id" + +########## + +GET http://nalab-server:8080/v1/surveys/{{ survey_id }} # 생성된 survey를 조회한다. + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.survey_id" exists + +jsonpath "$.target.id" exists +jsonpath "$.target.nickname" == "register_gallery" + +jsonpath "$.question_count" == 2 +jsonpath "$.question.[0].question_id" exists +jsonpath "$.question.[0].type" == "choice" +jsonpath "$.question.[0].form_type" == "tendency" +jsonpath "$.question.[0].title" == "저는 UI, UI, GUI 중에 어떤 분야를 가장 잘하는 것 같나요?" +jsonpath "$.question.[0].order" == 1 +jsonpath "$.question.[0].max_selectable_count" == 1 +jsonpath "$.question.[0].choices.[0].choice_id" exists +jsonpath "$.question.[0].choices.[0].content" == "UI" +jsonpath "$.question.[0].choices.[0].order" == 1 +jsonpath "$.question.[0].choices.[1].choice_id" exists +jsonpath "$.question.[0].choices.[1].content" == "UX" +jsonpath "$.question.[0].choices.[1].order" == 2 +jsonpath "$.question.[0].choices.[2].choice_id" exists +jsonpath "$.question.[0].choices.[2].content" == "GUI" +jsonpath "$.question.[0].choices.[2].order" == 3 +jsonpath "$.question.[1].question_id" exists +jsonpath "$.question.[1].type" == "short" +jsonpath "$.question.[1].form_type" == "strength" +jsonpath "$.question.[1].title" == "저는 UX, UI, GUI 중에 어떤 분야에 더 강점이 있나요?" +jsonpath "$.question.[1].order" == 2 + +[Captures] +target_id: jsonpath "$.target.id" +tendency_question_id: jsonpath "$.question.[0].question_id" +tendency_question_choice_id: jsonpath "$.question.[0].choices.[0].choice_id" +strength_question_id: jsonpath "$.question.[1].question_id" + + +########## + +POST http://nalab-server:8080/v1/surveys/{{ survey_id }}/bookmarks # survey_id를 북마크한다. +Authorization: {{ token_type }} {{ auth_token }} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.target_id" == {{ target_id }} +jsonpath "$.survey_id" == {{ survey_id }} +jsonpath "$.nickname" == "register_gallery" + +########## + +POST http://nalab-server:8080/v1/feedbacks # 생성된 survey에 feedback을 남긴다. + +[QueryStringParams] +survey-id: {{ survey_id }} + +{ + "reviewer": { + "collaboration_experience": true, + "position": "pm" + }, + "question_feedback": [ + { + "question_id": {{ tendency_question_id }}, + "type": "choice", + "choices": [ + {{ tendency_question_choice_id }} + ] + }, + { + "question_id": {{ strength_question_id }}, + "type": "short", + "reply": [ + "Hello world" + ] + } + ] +} + +HTTP 201 +[Asserts] + +########## + +GET http://nalab-server:8080/v1/feedbacks # 북마크를 위해 feedback id 저장 +Authorization: {{ token_type }} {{ auth_token }} + +[QueryStringParams] +survey-id: {{ survey_id }} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +[Captures] +form_question_feedback_id: jsonpath "$.question_feedback.[0].feedbacks.[0].form_question_feedback_id" + +########## + +PATCH http://nalab-server:8080/v1/feedbacks/bookmarks # 북마크를 진행한다. +Authorization: {{ token_type }} {{ auth_token }} + +[QueryStringParams] +form-question-feedback-id: {{form_question_feedback_id}} + +########## + +POST http://nalab-server:8080/v1/gallerys # gallery를 등록한다 +Authorization: {{ token_type }} {{ auth_token }} +{ + "job": "designer" +} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.target.target_id" == {{ target_id }} +jsonpath "$.target.nickname" == "register_gallery" +jsonpath "$.target.position" == null +jsonpath "$.target.job" == "DESIGNER" +jsonpath "$.target.image_url" == "empty_image" + +jsonpath "$.survey.survey_id" == {{ survey_id }} +jsonpath "$.survey.feedback_count" == 1 +jsonpath "$.survey.bookmarked_count" == 1 +jsonpath "$.survey.feedbacks.[0]" == "Hello world" +jsonpath "$.survey.tendencies.[0].name" == "UI" +jsonpath "$.survey.tendencies.[0].count" == 1 diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/common/survey/dto/TargetDto.java b/survey/survey-application/src/main/java/me/nalab/survey/application/common/survey/dto/TargetDto.java index 0ca96ae0..835ada55 100644 --- a/survey/survey-application/src/main/java/me/nalab/survey/application/common/survey/dto/TargetDto.java +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/common/survey/dto/TargetDto.java @@ -1,5 +1,6 @@ package me.nalab.survey.application.common.survey.dto; +import java.util.List; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -14,5 +15,5 @@ public class TargetDto { private final Long id; private final String nickname; private final String position; - + private final List bookmarkedSurveys; } diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/common/survey/mapper/TargetDtoMapper.java b/survey/survey-application/src/main/java/me/nalab/survey/application/common/survey/mapper/TargetDtoMapper.java index 9deb596d..a7ca0f9b 100644 --- a/survey/survey-application/src/main/java/me/nalab/survey/application/common/survey/mapper/TargetDtoMapper.java +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/common/survey/mapper/TargetDtoMapper.java @@ -1,6 +1,7 @@ package me.nalab.survey.application.common.survey.mapper; import me.nalab.survey.application.common.survey.dto.TargetDto; +import me.nalab.survey.domain.target.SurveyBookmark; import me.nalab.survey.domain.target.Target; public class TargetDtoMapper { @@ -21,6 +22,9 @@ public static TargetDto toTargetDto(Target target) { .id(target.getId()) .nickname(target.getNickname()) .position(target.getPosition()) + .bookmarkedSurveys(target.getBookmarkedSurveys().stream() + .map(SurveyBookmark::surveyId) + .toList()) .build(); } } diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/bookmark/SurveyBookmarkListenPort.java b/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/bookmark/SurveyBookmarkListenPort.java new file mode 100644 index 00000000..5935c6b5 --- /dev/null +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/port/out/persistence/bookmark/SurveyBookmarkListenPort.java @@ -0,0 +1,6 @@ +package me.nalab.survey.application.port.out.persistence.bookmark; + +public interface SurveyBookmarkListenPort { + + void listenBookmarked(Long targetId); +} diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkService.java b/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkService.java index 21528b06..afb3dc0e 100644 --- a/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkService.java +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkService.java @@ -4,6 +4,7 @@ import me.nalab.survey.application.common.survey.dto.SurveyBookmarkDto; import me.nalab.survey.application.exception.SurveyDoesNotExistException; import me.nalab.survey.application.port.in.web.bookmark.SurveyBookmarkUseCase; +import me.nalab.survey.application.port.out.persistence.bookmark.SurveyBookmarkListenPort; import me.nalab.survey.application.port.out.persistence.bookmark.SurveyBookmarkPort; import me.nalab.survey.application.port.out.persistence.findfeedback.SurveyExistCheckPort; import me.nalab.survey.application.port.out.persistence.findtarget.TargetFindPort; @@ -17,6 +18,7 @@ public class SurveyBookmarkService implements SurveyBookmarkUseCase { private final TargetFindPort targetFindPort; private final SurveyExistCheckPort surveyExistCheckPort; private final SurveyBookmarkPort surveyBookmarkPort; + private final SurveyBookmarkListenPort surveyBookmarkListener; @Override @Transactional @@ -30,6 +32,8 @@ public SurveyBookmarkDto bookmark(Long targetId, Long surveyId) { target.bookmark(surveyId); surveyBookmarkPort.bookmark(target); + surveyBookmarkListener.listenBookmarked(targetId); + return SurveyBookmarkDto.from(surveyId, target); } } From 3b0c8c633b8f2bcba24f63fe0761c55b9252f57d Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Fri, 1 Mar 2024 13:34:00 +0900 Subject: [PATCH 16/17] =?UTF-8?q?[feat]:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EC=9C=A0=EC=A0=80=EC=9D=98=20gallery=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#380)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../JwtDecryptInterceptorConfigurer.java | 1 + .../me/nalab/gallery/app/GalleryGetApp.kt | 26 +++ .../gallery/controller/GalleryController.kt | 7 + .../me/nalab/gallery/domain/GalleryService.kt | 6 + support/e2e/v1_10_get_logined_gallery.hurl | 221 ++++++++++++++++++ 5 files changed, 261 insertions(+) create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt create mode 100644 support/e2e/v1_10_get_logined_gallery.hurl diff --git a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java index fde209cd..84bc105d 100644 --- a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java +++ b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java @@ -29,6 +29,7 @@ public class JwtDecryptInterceptorConfigurer implements WebMvcConfigurer { "/v1/users", "/v1/gallerys/previews", "/v1/surveys/*/bookmarks", + "/v1/gallerys/logins", "/v1/gallerys", }; diff --git a/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt new file mode 100644 index 00000000..3c7f33d6 --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt @@ -0,0 +1,26 @@ +package me.nalab.gallery.app + +import me.nalab.gallery.domain.GalleryService +import me.nalab.gallery.domain.response.GalleryDto +import me.nalab.survey.application.port.`in`.web.findfeedback.FeedbackFindUseCase +import me.nalab.survey.application.port.`in`.web.survey.find.SurveyFindUseCase +import me.nalab.survey.application.port.`in`.web.target.find.TargetFindUseCase +import org.springframework.stereotype.Service + +@Service +class GalleryGetApp( + private val targetFindUseCase: TargetFindUseCase, + private val surveyFindUseCase: SurveyFindUseCase, + private val feedbackFindUseCase: FeedbackFindUseCase, + private val galleryService: GalleryService, +) { + + fun getGalleryByTargetId(targetId: Long): GalleryDto { + val gallery = galleryService.getGalleryByTargetId(targetId) + val target = targetFindUseCase.findTarget(targetId) + val survey = surveyFindUseCase.getSurveyByTargetId(targetId) + val feedbacks = feedbackFindUseCase.findAllFeedbackDtoBySurveyId(survey.id) + + return toGalleryDto(gallery, target, survey, feedbacks) + } +} diff --git a/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt b/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt index 96ef14cf..915e2862 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt @@ -1,5 +1,6 @@ package me.nalab.gallery.controller +import me.nalab.gallery.app.GalleryGetApp import me.nalab.gallery.app.GalleryPreviewApp import me.nalab.gallery.app.GalleryRegisterApp import me.nalab.gallery.controller.request.GalleryRegisterRequest @@ -11,6 +12,7 @@ import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/v1/gallerys") class GalleryController( + private val galleryGetApp: GalleryGetApp, private val galleryPreviewApp: GalleryPreviewApp, private val galleryRegisterApp: GalleryRegisterApp, ) { @@ -29,4 +31,9 @@ class GalleryController( return galleryRegisterApp.registerGalleryByTargetId(targetId, request.job) } + @GetMapping("/logins") + @ResponseStatus(HttpStatus.OK) + fun getGallery(@RequestAttribute("logined") targetId: Long): GalleryDto = + galleryGetApp.getGalleryByTargetId(targetId) + } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt index f22f3aa3..e98c79c8 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt @@ -4,10 +4,16 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service +@Transactional(readOnly = true) class GalleryService( private val galleryRepository: GalleryRepository, ) { + fun getGalleryByTargetId(targetId: Long): Gallery { + return galleryRepository.findByTargetIdOrNull(targetId) + ?: throw IllegalArgumentException("targetId \"$targetId\" 에 해당하는 Gallery를 찾을 수 없습니다.") + } + @Transactional fun registerGallery(gallery: Gallery): Gallery { require(galleryRepository.findByTargetIdOrNull(gallery.getTargetId()) == null) { diff --git a/support/e2e/v1_10_get_logined_gallery.hurl b/support/e2e/v1_10_get_logined_gallery.hurl new file mode 100644 index 00000000..49eb0ab5 --- /dev/null +++ b/support/e2e/v1_10_get_logined_gallery.hurl @@ -0,0 +1,221 @@ +POST http://nalab-server:8080/v1/oauth/default # Default provider를 통해서 로그인 진행 +{ + "nickname": "logined_gallery", + "email": "loginedgallery@123456" +} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.access_token" exists +jsonpath "$.token_type" exists + +[Captures] +token_type: jsonpath "$.token_type" +auth_token: jsonpath "$.access_token" + +########## + +POST http://nalab-server:8080/v1/surveys # 발급받은 토큰으로 survey를 생성한다. +Authorization: {{ token_type }} {{ auth_token }} +{ + "question_count": 2, + "question": [ + { + "type": "choice", + "form_type": "tendency", + "title": "저는 UI, UI, GUI 중에 어떤 분야를 가장 잘하는 것 같나요?", + "choices": [ + { + "content": "UI", + "order": 1 + }, + { + "content": "UX", + "order": 2 + }, + { + "content": "GUI", + "order": 3 + } + ], + "max_selectable_count": 1, + "order": 1 + }, + { + "type": "short", + "form_type": "strength", + "title": "저는 UX, UI, GUI 중에 어떤 분야에 더 강점이 있나요?", + "order": 2 + } + ] +} + +HTTP 201 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.survey_id" exists + +[Captures] +survey_id: jsonpath "$.survey_id" + +########## + +GET http://nalab-server:8080/v1/surveys/{{ survey_id }} # 생성된 survey를 조회한다. + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.survey_id" exists + +jsonpath "$.target.id" exists +jsonpath "$.target.nickname" == "logined_gallery" + +jsonpath "$.question_count" == 2 +jsonpath "$.question.[0].question_id" exists +jsonpath "$.question.[0].type" == "choice" +jsonpath "$.question.[0].form_type" == "tendency" +jsonpath "$.question.[0].title" == "저는 UI, UI, GUI 중에 어떤 분야를 가장 잘하는 것 같나요?" +jsonpath "$.question.[0].order" == 1 +jsonpath "$.question.[0].max_selectable_count" == 1 +jsonpath "$.question.[0].choices.[0].choice_id" exists +jsonpath "$.question.[0].choices.[0].content" == "UI" +jsonpath "$.question.[0].choices.[0].order" == 1 +jsonpath "$.question.[0].choices.[1].choice_id" exists +jsonpath "$.question.[0].choices.[1].content" == "UX" +jsonpath "$.question.[0].choices.[1].order" == 2 +jsonpath "$.question.[0].choices.[2].choice_id" exists +jsonpath "$.question.[0].choices.[2].content" == "GUI" +jsonpath "$.question.[0].choices.[2].order" == 3 +jsonpath "$.question.[1].question_id" exists +jsonpath "$.question.[1].type" == "short" +jsonpath "$.question.[1].form_type" == "strength" +jsonpath "$.question.[1].title" == "저는 UX, UI, GUI 중에 어떤 분야에 더 강점이 있나요?" +jsonpath "$.question.[1].order" == 2 + +[Captures] +target_id: jsonpath "$.target.id" +tendency_question_id: jsonpath "$.question.[0].question_id" +tendency_question_choice_id: jsonpath "$.question.[0].choices.[0].choice_id" +strength_question_id: jsonpath "$.question.[1].question_id" + + +########## + +POST http://nalab-server:8080/v1/surveys/{{ survey_id }}/bookmarks # survey_id를 북마크한다. +Authorization: {{ token_type }} {{ auth_token }} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.target_id" == {{ target_id }} +jsonpath "$.survey_id" == {{ survey_id }} +jsonpath "$.nickname" == "logined_gallery" + +########## + +POST http://nalab-server:8080/v1/feedbacks # 생성된 survey에 feedback을 남긴다. + +[QueryStringParams] +survey-id: {{ survey_id }} + +{ + "reviewer": { + "collaboration_experience": true, + "position": "pm" + }, + "question_feedback": [ + { + "question_id": {{ tendency_question_id }}, + "type": "choice", + "choices": [ + {{ tendency_question_choice_id }} + ] + }, + { + "question_id": {{ strength_question_id }}, + "type": "short", + "reply": [ + "Hello world" + ] + } + ] +} + +HTTP 201 +[Asserts] + +########## + +GET http://nalab-server:8080/v1/feedbacks # 북마크를 위해 feedback id 저장 +Authorization: {{ token_type }} {{ auth_token }} + +[QueryStringParams] +survey-id: {{ survey_id }} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +[Captures] +form_question_feedback_id: jsonpath "$.question_feedback.[0].feedbacks.[0].form_question_feedback_id" + +########## + +PATCH http://nalab-server:8080/v1/feedbacks/bookmarks # 북마크를 진행한다. +Authorization: {{ token_type }} {{ auth_token }} + +[QueryStringParams] +form-question-feedback-id: {{form_question_feedback_id}} + +########## + +POST http://nalab-server:8080/v1/gallerys # gallery를 등록한다 +Authorization: {{ token_type }} {{ auth_token }} +{ + "job": "designer" +} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.target.target_id" == {{ target_id }} +jsonpath "$.target.nickname" == "logined_gallery" +jsonpath "$.target.position" == null +jsonpath "$.target.job" == "DESIGNER" +jsonpath "$.target.image_url" == "empty_image" + +jsonpath "$.survey.survey_id" == {{ survey_id }} +jsonpath "$.survey.feedback_count" == 1 +jsonpath "$.survey.bookmarked_count" == 1 +jsonpath "$.survey.feedbacks.[0]" == "Hello world" +jsonpath "$.survey.tendencies.[0].name" == "UI" +jsonpath "$.survey.tendencies.[0].count" == 1 + +########## + +GET http://nalab-server:8080/v1/gallerys/logins # 내 gallery 를 조회한다 +Authorization: {{ token_type }} {{ auth_token }} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.target.target_id" == {{ target_id }} +jsonpath "$.target.nickname" == "logined_gallery" +jsonpath "$.target.position" == null +jsonpath "$.target.job" == "DESIGNER" +jsonpath "$.target.image_url" == "empty_image" + +jsonpath "$.survey.survey_id" == {{ survey_id }} +jsonpath "$.survey.feedback_count" == 1 +jsonpath "$.survey.bookmarked_count" == 1 +jsonpath "$.survey.feedbacks.[0]" == "Hello world" +jsonpath "$.survey.tendencies.[0].name" == "UI" +jsonpath "$.survey.tendencies.[0].count" == 1 + From f3cb8758c3a9013ad492d52ddef503c6f6f11552 Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Sun, 3 Mar 2024 22:08:09 +0900 Subject: [PATCH 17/17] =?UTF-8?q?[feat]=20:=20=EC=A0=84=EC=B2=B4=20Gallery?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(#381)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../db/migration/v10_gallery_covering_idx.sql | 6 + .../me/nalab/gallery/app/GalleryGetApp.kt | 34 ++++ .../gallery/controller/GalleryController.kt | 12 ++ .../nalab/gallery/domain/GalleryRepository.kt | 6 + .../me/nalab/gallery/domain/GalleryService.kt | 11 ++ .../gallery/domain/response/GalleriesDto.kt | 6 + .../me/nalab/gallery/app/SurveyFixture.kt | 11 +- .../me/nalab/gallery/domain/GalleryFixture.kt | 3 +- .../gallery/domain/GalleryServiceTest.kt | 152 +++++++++++++++++- 9 files changed, 233 insertions(+), 8 deletions(-) create mode 100644 api/src/main/resources/db/migration/v10_gallery_covering_idx.sql create mode 100644 gallery/src/main/kotlin/me/nalab/gallery/domain/response/GalleriesDto.kt diff --git a/api/src/main/resources/db/migration/v10_gallery_covering_idx.sql b/api/src/main/resources/db/migration/v10_gallery_covering_idx.sql new file mode 100644 index 00000000..4aa6605c --- /dev/null +++ b/api/src/main/resources/db/migration/v10_gallery_covering_idx.sql @@ -0,0 +1,6 @@ +CREATE INDEX gallery_idx_bookmark ON gallery(bookmarked_count) +CREATE INDEX gallery_idx_update_order ON gallery(update_order) +CREATE UNIQUE INDEX gallery_idx_survey_id ON gallery(survey_id) +CREATE INDEX gallery_idx_job ON gallery(job) +CREATE INDEX gallery_idx_created_at ON gallery(created_at) +CREATE INDEX gallery_idx_updated_at ON gallery(updated_at) diff --git a/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt index 3c7f33d6..4e02e8f0 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt @@ -1,10 +1,16 @@ package me.nalab.gallery.app +import me.nalab.gallery.domain.Gallery import me.nalab.gallery.domain.GalleryService +import me.nalab.gallery.domain.response.GalleriesDto import me.nalab.gallery.domain.response.GalleryDto import me.nalab.survey.application.port.`in`.web.findfeedback.FeedbackFindUseCase import me.nalab.survey.application.port.`in`.web.survey.find.SurveyFindUseCase import me.nalab.survey.application.port.`in`.web.target.find.TargetFindUseCase +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort import org.springframework.stereotype.Service @Service @@ -23,4 +29,32 @@ class GalleryGetApp( return toGalleryDto(gallery, target, survey, feedbacks) } + + fun getGalleries(job: String, page: Int, count: Int, orderType: String): GalleriesDto { + val pageable = getPage(page, count, orderType) + val galleries = galleryService.getGalleries(job, pageable) + + val galleryDtos = toGalleryDtos(galleries) + + return GalleriesDto(galleries.totalPages, galleryDtos) + } + + private fun toGalleryDtos(galleries: Page): List { + return galleries.asSequence() + .map { gallery -> + val target = targetFindUseCase.findTarget(gallery.getTargetId()) + val survey = surveyFindUseCase.getSurveyByTargetId(gallery.getTargetId()) + val feedbacks = feedbackFindUseCase.findAllFeedbackDtoBySurveyId(survey.id) + + toGalleryDto(gallery, target, survey, feedbacks) + }.toList() + } + + private fun getPage(page: Int, count: Int, orderType: String): Pageable { + return when (orderType.lowercase()) { + "update" -> PageRequest.of(page, count, Sort.by("updateOrder").descending()) + "job" -> PageRequest.of(page, count, Sort.by("survey.bookmarkedCount").descending()) + else -> throw IllegalArgumentException("orderType 은 update와 bookmark중 하나여야 합니다. 현재 orderType \"$orderType\"") + } + } } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt b/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt index 915e2862..be01c988 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt @@ -4,6 +4,7 @@ import me.nalab.gallery.app.GalleryGetApp import me.nalab.gallery.app.GalleryPreviewApp import me.nalab.gallery.app.GalleryRegisterApp import me.nalab.gallery.controller.request.GalleryRegisterRequest +import me.nalab.gallery.domain.response.GalleriesDto import me.nalab.gallery.domain.response.GalleryDto import me.nalab.gallery.domain.response.GalleryPreviewDto import org.springframework.http.HttpStatus @@ -36,4 +37,15 @@ class GalleryController( fun getGallery(@RequestAttribute("logined") targetId: Long): GalleryDto = galleryGetApp.getGalleryByTargetId(targetId) + @GetMapping + @ResponseStatus(HttpStatus.OK) + fun getGalleries( + @RequestParam(name = "job", defaultValue = "all") job: String, + @RequestParam(name = "page", defaultValue = "0") page: Int, + @RequestParam(name = "count", defaultValue = "5") count: Int, + @RequestParam(name = "order-type", defaultValue = "update") orderType: String + ): GalleriesDto { + return galleryGetApp.getGalleries(job, page, count, orderType) + } + } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryRepository.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryRepository.kt index 508f51f0..96db38a2 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryRepository.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryRepository.kt @@ -1,5 +1,7 @@ package me.nalab.gallery.domain +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Lock import org.springframework.data.jpa.repository.Query @@ -11,4 +13,8 @@ interface GalleryRepository : JpaRepository { @Lock(LockModeType.OPTIMISTIC) @Query("select g from Gallery as g where g.target.targetId = :targetId") fun findByTargetIdOrNull(@Param("targetId") targetId: Long): Gallery? + + @Query("select g from Gallery g where g.target.job in :job") + fun findGalleries(@Param("job") job: List, pageable: Pageable): Page + } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt index e98c79c8..78f2610c 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt @@ -1,5 +1,7 @@ package me.nalab.gallery.domain +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -27,4 +29,13 @@ class GalleryService( fun increaseBookmarkCount(targetId: Long) { galleryRepository.findByTargetIdOrNull(targetId)?.increaseBookmarkedCount() } + + fun getGalleries(job: String, pageable: Pageable): Page { + val jobs = when (job) { + "all" -> Job.entries.toList() + else -> listOf(Job.valueOf(job.uppercase())) + } + + return galleryRepository.findGalleries(jobs, pageable) + } } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/response/GalleriesDto.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/response/GalleriesDto.kt new file mode 100644 index 00000000..33cf3f0d --- /dev/null +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/response/GalleriesDto.kt @@ -0,0 +1,6 @@ +package me.nalab.gallery.domain.response + +data class GalleriesDto( + val totalPage: Int, + val galleries: List +) diff --git a/gallery/src/test/kotlin/me/nalab/gallery/app/SurveyFixture.kt b/gallery/src/test/kotlin/me/nalab/gallery/app/SurveyFixture.kt index 177ade82..39aec20b 100644 --- a/gallery/src/test/kotlin/me/nalab/gallery/app/SurveyFixture.kt +++ b/gallery/src/test/kotlin/me/nalab/gallery/app/SurveyFixture.kt @@ -1,5 +1,6 @@ package me.nalab.gallery.app +import me.nalab.core.time.TimeUtil import me.nalab.survey.application.common.feedback.dto.* import me.nalab.survey.application.common.survey.dto.* import java.time.Instant @@ -23,7 +24,7 @@ fun surveyDto( choiceFormQuestionDto(), shortFormQuestionDto() ), - time: Instant = Instant.now(), + time: Instant = TimeUtil.toInstant(), ): SurveyDto { return SurveyDto.builder() .id(id) @@ -36,7 +37,7 @@ fun surveyDto( fun choiceFormQuestionDto( id: Long = 0L, - time: Instant = Instant.now(), + time: Instant = TimeUtil.toInstant(), type: ChoiceFormQuestionDtoType = ChoiceFormQuestionDtoType.TENDENCY, choices: List = listOf(choiceDto()), maxSelectableCount: Int = 5, @@ -67,7 +68,7 @@ fun choiceDto( fun shortFormQuestionDto( id: Long = 0L, - time: Instant = Instant.now(), + time: Instant = TimeUtil.toInstant(), type: ShortFormQuestionDtoType = ShortFormQuestionDtoType.CUSTOM, title: String = "제가 고쳐야할점을 알려주세요.", order: Int = 1, @@ -85,7 +86,7 @@ fun shortFormQuestionDto( fun feedbackDto( id: Long = 0L, surveyId: Long = 0L, - time: Instant = Instant.now(), + time: Instant = TimeUtil.toInstant(), formQuestionFeedbackDtos: List = listOf( choiceFormQuestionFeedbackDto(), shortFormQuestionFeedbackDto() @@ -136,7 +137,7 @@ fun shortFormQuestionFeedbackDto( fun bookmarkDto( isBookmarked: Boolean = false, - time: Instant = Instant.now(), + time: Instant = TimeUtil.toInstant(), ): BookmarkDto { return BookmarkDto.builder() .isBookmarked(isBookmarked) diff --git a/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryFixture.kt b/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryFixture.kt index 2728cd14..b716c905 100644 --- a/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryFixture.kt +++ b/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryFixture.kt @@ -1,5 +1,6 @@ package me.nalab.gallery.domain +import me.nalab.core.time.TimeUtil import java.time.Instant fun gallery( @@ -8,7 +9,7 @@ fun gallery( job: Job = Job.OTHERS, surveyId: Long = 0L, bookmarkedCount: Int = 0, - updateOrder: Instant = Instant.now(), + updateOrder: Instant = TimeUtil.toInstant(), ): Gallery = Gallery( id = id, targetId = targetId, diff --git a/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryServiceTest.kt b/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryServiceTest.kt index d19a2551..d6ae45eb 100644 --- a/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryServiceTest.kt +++ b/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryServiceTest.kt @@ -3,11 +3,19 @@ package me.nalab.gallery.domain import io.kotest.assertions.throwables.shouldThrowMessage import io.kotest.core.annotation.DisplayName import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.equality.FieldsEqualityCheckConfig +import io.kotest.matchers.equality.shouldBeEqualToComparingFields import io.kotest.matchers.equality.shouldBeEqualUsingFields +import io.kotest.matchers.equals.shouldBeEqual +import me.nalab.core.time.TimeUtil import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.test.context.ContextConfiguration +import java.time.temporal.ChronoUnit +import kotlin.reflect.full.memberProperties @DataJpaTest @EnableJpaRepositories @@ -16,13 +24,24 @@ import org.springframework.test.context.ContextConfiguration @ContextConfiguration(classes = [GalleryService::class]) internal class GalleryServiceTest( private val galleryService: GalleryService, + private val galleryRepository: GalleryRepository, ) : DescribeSpec({ - beforeSpec { - galleryService.registerGallery(gallery(id = EXIST_GALLERY_ID, targetId = EXIST_TARGET_ID, surveyId = EXIST_SURVEY_ID)) + afterEach { + galleryRepository.deleteAll() } describe("registerGallery 메소드는") { + beforeEach { + galleryService.registerGallery( + gallery( + id = EXIST_GALLERY_ID, + targetId = EXIST_TARGET_ID, + surveyId = EXIST_SURVEY_ID + ) + ) + } + context("Gallery에 등록되지 않은 target의 Gallery를 입력받으면,") { val gallery = gallery(targetId = NOT_EXIST_TARGET_ID) @@ -43,6 +62,94 @@ internal class GalleryServiceTest( } } } + + describe("getGalleries 메소드는") { + beforeEach { + galleryService.registerGallery(oldDesignerGallery) + galleryService.registerGallery(midDeveloperGallery) + galleryService.registerGallery(latestPmGallery) + } + + context("job으로 all과 update순으로 정렬된 Pageable을 입력받으면,") { + val expected = listOf(latestPmGallery, midDeveloperGallery, oldDesignerGallery) + + it("update순으로 정렬된 Gallery를 반환한다.") { + val result = galleryService.getGalleries("all", updatePage).content + + result.shouldBeExactlyEqualToComparingFields(expected) + } + } + + context("job으로 designer와 update순으로 정렬된 Pageable을 입력받으면,") { + val expected = listOf(oldDesignerGallery) + + it("update순으로 정렬된 designer Gallery를 반환한다.") { + val result = galleryService.getGalleries("designer", updatePage).content + + result.shouldBeExactlyEqualToComparingFields(expected) + } + } + + context("job으로 pm과 update순으로 정렬된 Pageable을 입력받으면,") { + val expected = listOf(latestPmGallery) + + it("update순으로 정렬된 pm Gallery를 반환한다.") { + val result = galleryService.getGalleries("pm", updatePage).content + + result.shouldBeExactlyEqualToComparingFields(expected) + } + } + + context("job으로 developer와 update순으로 정렬된 Pageable을 입력받으면,") { + val expected = listOf(midDeveloperGallery) + + it("update순으로 정렬된 pm Gallery를 반환한다.") { + val result = galleryService.getGalleries("developer", updatePage).content + + result.shouldBeExactlyEqualToComparingFields(expected) + } + } + + context("job으로 all과 bookmark순으로 정렬된 Pageable을 받으면,") { + val expected = listOf(oldDesignerGallery, midDeveloperGallery, latestPmGallery) + + it("bookmark순으로 정렬된 Gallery를 반환한다") { + val result = galleryService.getGalleries("all", bookmarkPage).content + + result.shouldBeExactlyEqualToComparingFields(expected) + } + } + + context("job으로 designer와 bookmark순으로 정렬된 Pageable을 입력받으면,") { + val expected = listOf(oldDesignerGallery) + + it("update순으로 정렬된 designer Gallery를 반환한다.") { + val result = galleryService.getGalleries("designer", bookmarkPage).content + + result.shouldBeExactlyEqualToComparingFields(expected) + } + } + + context("job으로 pm과 bookmark순으로 정렬된 Pageable을 입력받으면,") { + val expected = listOf(latestPmGallery) + + it("update순으로 정렬된 pm Gallery를 반환한다.") { + val result = galleryService.getGalleries("pm", bookmarkPage).content + + result.shouldBeExactlyEqualToComparingFields(expected) + } + } + + context("job으로 developer와 bookmark순으로 정렬된 Pageable을 입력받으면,") { + val expected = listOf(midDeveloperGallery) + + it("update순으로 정렬된 pm Gallery를 반환한다.") { + val result = galleryService.getGalleries("developer", bookmarkPage).content + + result.shouldBeExactlyEqualToComparingFields(expected) + } + } + } }) { companion object { @@ -50,5 +157,46 @@ internal class GalleryServiceTest( private const val EXIST_GALLERY_ID = 100L private const val EXIST_TARGET_ID = 100L private const val EXIST_SURVEY_ID = 100L + + val updatePage: PageRequest = PageRequest.of(0, 5, Sort.by("updateOrder").descending()) + val bookmarkPage: PageRequest = + PageRequest.of(0, 5, Sort.by("survey.bookmarkedCount").descending()) + + val oldDesignerGallery = gallery( + id = 1, + targetId = 101, + surveyId = 101, + job = Job.DESIGNER, + updateOrder = TimeUtil.toInstant().minus(1, ChronoUnit.DAYS), + bookmarkedCount = 3 + ) + + val midDeveloperGallery = gallery( + id = 2, + targetId = 102, + surveyId = 102, + job = Job.DEVELOPER, + updateOrder = TimeUtil.toInstant(), + bookmarkedCount = 2 + ) + + val latestPmGallery = gallery( + id = 3, + targetId = 103, + surveyId = 103, + job = Job.PM, + updateOrder = TimeUtil.toInstant().plus(1, ChronoUnit.DAYS), + bookmarkedCount = 1 + ) + + private fun List.shouldBeExactlyEqualToComparingFields(galleries: List) { + this.size shouldBeEqual galleries.size + for (i in galleries.indices) { + this[i].shouldBeEqualToComparingFields( + galleries[i], + FieldsEqualityCheckConfig(propertiesToExclude = Gallery::class.memberProperties.filter { it.name == "createdAt" || it.name == "updatedAt" }) + ) + } + } } }