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 a40d1ea3..3f2f66f5 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -13,19 +13,40 @@ jobs:
steps:
- 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
+
- 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/.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/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/)
+
+---
+
+
+
+
+
+
+
+> 나의 커리어 브랜딩을 완성해주는 기본질문을 통해 손쉽게 질문폼을 만들 수 있어요
+> 새로운 질문을 추가하고 싶다면 객관식, 주관식으로 자유롭게 질문을 만들어보세요!
+
+
+
+
+
+
+
+
+> 부담스러웠던 동료 평가의 경험을 마치 친구와 심리테스트 하듯 즐겁게 할 수 있도록 설계했어요
+> 나랩의 연구를 책임지는 Dr. 왓슨 박사님과 함께 채팅으로 대화하며 익명으로 피드백을 남길 수 있어요
+
+
+
+
+
+
+
+
+
+> 많은 사람들의 답변 속에서 정말 나에게 도움이 되는 피드백은 어느 것일까요?
+> 나랩은 유저가 개별 답변에 대한 이해도를 높이며 의미 있는 피드백을 얻을 수 있도록 결과를 정리했어요
+
+
+
+
+
+
+
+> 피드백 결과를 통해 나의 커리어 연구 결과를 확인할 수 있고,
+> 동료들의 피드백을 저장해 나만의 커리어 명함을 만들 수 있어요
+
+
+
+
+
+
+![추가이미지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
+
+
+
+
+
+
+
+![추가이미지3](https://github.com/depromeet/na-lab-server/assets/71487608/09a06bb1-4f06-4513-977d-e6fe49bd8f06)
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/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/feedback/find/FeedbackFindAcceptanceTest.java b/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/feedback/find/FeedbackFindAcceptanceTest.java
index 06d9de75..b8de5cce 100644
--- a/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/feedback/find/FeedbackFindAcceptanceTest.java
+++ b/api/acceptance-test/src/test/java/me/nalab/luffy/api/acceptance/test/feedback/find/FeedbackFindAcceptanceTest.java
@@ -11,13 +11,6 @@
import me.nalab.luffy.api.acceptance.test.feedback.create.response.SurveyFindResponse;
import me.nalab.luffy.api.acceptance.test.survey.RequestSample;
import org.json.JSONObject;
-
-import static me.nalab.luffy.api.acceptance.test.feedback.FeedbackAcceptanceValidator.*;
-import static me.nalab.luffy.api.acceptance.test.feedback.FeedbackCreateRequestFixture.*;
-
-import java.time.Instant;
-import java.util.Map;
-
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
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..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")
@@ -53,6 +60,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..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;
@@ -84,4 +85,33 @@ 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)
+ );
+ }
+
+ 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/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);
+ }
+
+}
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/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/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/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/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/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/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..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
@@ -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",
@@ -25,7 +26,11 @@ public class JwtDecryptInterceptorConfigurer implements WebMvcConfigurer {
"/v1/reviewers/summary*",
"/v2/surveys/*/feedbacks",
"/v1/feedbacks/bookmarks",
- "/v1/users"
+ "/v1/users",
+ "/v1/gallerys/previews",
+ "/v1/surveys/*/bookmarks",
+ "/v1/gallerys/logins",
+ "/v1/gallerys",
};
@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 0f26147c..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
@@ -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",
@@ -21,6 +22,7 @@ public class MockAuthConfigurer implements WebMvcConfigurer {
"/v1/reviewers*",
"/v1/reviewers/summary*",
"/v2/surveys/*/feedbacks",
+ "/v1/surveys/*/bookmarks",
};
@Override
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/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..e906c7f9
--- /dev/null
+++ b/core/data/src/main/java/me/nalab/core/data/target/SurveyBookmarkEntity.java
@@ -0,0 +1,20 @@
+package me.nalab.core.data.target;
+
+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")
+ @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..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,11 +1,18 @@
package me.nalab.core.data.target;
+import java.util.HashSet;
+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.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@@ -31,4 +38,22 @@ 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")
+ )
+ @Builder.Default
+ private Set bookmarkedSurveys = new HashSet<>();
+
+ @Version
+ @Column(name = "version")
+ private Long version;
+
}
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/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/gallery/build.gradle b/gallery/build.gradle
new file mode 100644
index 00000000..1a0e5aae
--- /dev/null
+++ b/gallery/build.gradle
@@ -0,0 +1,17 @@
+repositories {
+ mavenCentral()
+}
+
+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"
+ 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..8cd4b77c
--- /dev/null
+++ b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryDtoMapper.kt
@@ -0,0 +1,101 @@
+package me.nalab.gallery.app
+
+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
+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 toGalleryDto(
+ gallery: Gallery,
+ target: TargetDto,
+ survey: SurveyDto,
+ feedbacks: List,
+): GalleryDto {
+ return GalleryDto(
+ galleryId = gallery.id,
+ target = GalleryDto.Target(
+ targetId = gallery.getTargetId().toString(),
+ nickname = target.nickname,
+ job = gallery.getJob(),
+ position = target.position,
+ ),
+ survey = GalleryDto.Survey(
+ surveyId = survey.id.toString(),
+ feedbackCount = feedbacks.size,
+ bookmarkedCount = gallery.getBookmarkedCount(),
+ 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) ->
+ GalleryDto.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/GalleryGetApp.kt b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt
new file mode 100644
index 00000000..4e02e8f0
--- /dev/null
+++ b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt
@@ -0,0 +1,60 @@
+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
+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)
+ }
+
+ 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/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/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
new file mode 100644
index 00000000..be01c988
--- /dev/null
+++ b/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt
@@ -0,0 +1,51 @@
+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
+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
+import org.springframework.web.bind.annotation.*
+
+@RestController
+@RequestMapping("/v1/gallerys")
+class GalleryController(
+ private val galleryGetApp: GalleryGetApp,
+ private val galleryPreviewApp: GalleryPreviewApp,
+ private val galleryRegisterApp: GalleryRegisterApp,
+) {
+
+ @GetMapping("/previews")
+ @ResponseStatus(HttpStatus.OK)
+ 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)
+ }
+
+ @GetMapping("/logins")
+ @ResponseStatus(HttpStatus.OK)
+ 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/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
new file mode 100644
index 00000000..bc268ef4
--- /dev/null
+++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/Gallery.kt
@@ -0,0 +1,46 @@
+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.*
+
+@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,
+
+ @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..96db38a2
--- /dev/null
+++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryRepository.kt
@@ -0,0 +1,20 @@
+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
+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?
+
+ @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
new file mode 100644
index 00000000..78f2610c
--- /dev/null
+++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt
@@ -0,0 +1,41 @@
+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
+
+@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) {
+ "target \"${gallery.getTargetId()}\" 이 이미 Gallery에 등록되어있습니다."
+ }
+
+ return galleryRepository.save(gallery)
+ }
+
+ @Transactional
+ 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/Job.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/Job.kt
new file mode 100644
index 00000000..058960b6
--- /dev/null
+++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/Job.kt
@@ -0,0 +1,8 @@
+package me.nalab.gallery.domain
+
+enum class Job {
+ PM,
+ DEVELOPER,
+ DESIGNER,
+ OTHERS,
+}
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/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/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/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/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/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..39aec20b
--- /dev/null
+++ b/gallery/src/test/kotlin/me/nalab/gallery/app/SurveyFixture.kt
@@ -0,0 +1,146 @@
+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
+
+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 = TimeUtil.toInstant(),
+): SurveyDto {
+ return SurveyDto.builder()
+ .id(id)
+ .targetId(targetId)
+ .formQuestionDtoableList(formQuestionDtos)
+ .createdAt(time)
+ .updatedAt(time)
+ .build()
+}
+
+fun choiceFormQuestionDto(
+ id: Long = 0L,
+ time: Instant = TimeUtil.toInstant(),
+ 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 = TimeUtil.toInstant(),
+ 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 = TimeUtil.toInstant(),
+ 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 = TimeUtil.toInstant(),
+): BookmarkDto {
+ return BookmarkDto.builder()
+ .isBookmarked(isBookmarked)
+ .bookmarkedAt(time)
+ .build()
+}
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..b716c905
--- /dev/null
+++ b/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryFixture.kt
@@ -0,0 +1,20 @@
+package me.nalab.gallery.domain
+
+import me.nalab.core.time.TimeUtil
+import java.time.Instant
+
+fun gallery(
+ id: Long = 0L,
+ targetId: Long = 0L,
+ job: Job = Job.OTHERS,
+ surveyId: Long = 0L,
+ bookmarkedCount: Int = 0,
+ updateOrder: Instant = TimeUtil.toInstant(),
+): 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/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/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..d6ae45eb
--- /dev/null
+++ b/gallery/src/test/kotlin/me/nalab/gallery/domain/GalleryServiceTest.kt
@@ -0,0 +1,202 @@
+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
+@EntityScan(value = ["me.nalab.core.data", "me.nalab.gallery.domain"])
+@DisplayName("GalleryService 클래스의")
+@ContextConfiguration(classes = [GalleryService::class])
+internal class GalleryServiceTest(
+ private val galleryService: GalleryService,
+ private val galleryRepository: GalleryRepository,
+) : DescribeSpec({
+
+ 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)
+
+ 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)
+ }
+ }
+ }
+ }
+
+ 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 {
+ 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
+
+ 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" })
+ )
+ }
+ }
+ }
+}
diff --git a/gradle.properties b/gradle.properties
index e247c399..4275f174 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,11 +3,12 @@ applicationVersion=0.0.1-SNAPSHOT
### Project configs ###
projectGroup=me.nalab
-javaVersion=11
+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
@@ -23,3 +24,13 @@ org.gradle.jvmargs=-Xmx2048m
### Sentry ###
sentryLogBackVersion=1.7.30
+
+### Kotlin ###
+kotlinVersion=1.9.22
+
+### Kotest ###
+kotestVersion=5.7.2
+kotestExtensionSpringVersion=1.1.3
+
+### RestAssured ###
+restAssuredVersion=5.3.0
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/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'
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
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
+
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 }}
{
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/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/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/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/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/in/web/bookmark/SurveyBookmarkUseCase.java b/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/bookmark/SurveyBookmarkUseCase.java
new file mode 100644
index 00000000..a363a9b2
--- /dev/null
+++ b/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/bookmark/SurveyBookmarkUseCase.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 SurveyBookmarkUseCase {
+
+ /**
+ * targetId에 해당하는 유저에게 survey를 북마크합니다.
+ * 이미 북마크되어있다면 북마크를 취소합니다.
+ */
+ SurveyBookmarkDto bookmark(Long targetId, Long surveyId);
+
+}
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..fc4b31c2
--- /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 existSurveyByTargetId(Long targetId);
+
+}
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/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/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/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/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/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/bookmark/SurveyBookmarkService.java b/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkService.java
new file mode 100644
index 00000000..afb3dc0e
--- /dev/null
+++ b/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkService.java
@@ -0,0 +1,39 @@
+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.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;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class SurveyBookmarkService implements SurveyBookmarkUseCase {
+
+ private final TargetFindPort targetFindPort;
+ private final SurveyExistCheckPort surveyExistCheckPort;
+ private final SurveyBookmarkPort surveyBookmarkPort;
+ private final SurveyBookmarkListenPort surveyBookmarkListener;
+
+ @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);
+ surveyBookmarkPort.bookmark(target);
+
+ surveyBookmarkListener.listenBookmarked(targetId);
+
+ return SurveyBookmarkDto.from(surveyId, target);
+ }
+}
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..baa876a5
--- /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 existSurveyByTargetId(Long targetId) {
+ return surveyExistPort.isSurveyExistByTargetId(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-application/src/main/java/module-info.java b/survey/survey-application/src/main/java/module-info.java
index 0528180f..74682830 100644
--- a/survey/survey-application/src/main/java/module-info.java
+++ b/survey/survey-application/src/main/java/module-info.java
@@ -36,6 +36,8 @@
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;
+ 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-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..330e79e4
--- /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.existSurveyByTargetId(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.existSurveyByTargetId(targetId);
+
+ // then
+ Assertions.assertThat(result).isFalse();
+ }
+
+}
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..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
@@ -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,31 @@
@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;
+ @Builder.Default
+ 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/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/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 f3dae651..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 {
@@ -14,14 +18,31 @@ public static TargetEntity toTargetEntity(Target target) {
.id(target.getId())
.nickname(target.getNickname())
.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-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/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);
}
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-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/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 {
+
+}
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/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..9daf08cf
--- /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.SurveyBookmarkUseCase;
+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 SurveyBookmarkUseCase 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();
+ }
+}
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/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;
+
+}
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);
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)
;