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 ff4d20c8..cf28f572 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 @@ -103,15 +103,7 @@ public static void assertIsSurveyDoesNotExists(ResultActions resultActions) thro } public static void assertIsBookmarked(ResultActions resultActions) throws Exception { - resultActions.andExpectAll( - status().isOk(), - content().contentType(MediaType.APPLICATION_JSON), - jsonPath("$.target_id").isString(), - jsonPath("$.survey_id").isString(), - jsonPath("$.nickname").isString(), - jsonPath("$.position").doesNotExist(), - jsonPath("$.image_url").doesNotExist() - ); + resultActions.andExpectAll(status().isOk()); } } diff --git a/auth/auth-interceptor/build.gradle b/auth/auth-interceptor/build.gradle index 2b138d2b..4b178849 100644 --- a/auth/auth-interceptor/build.gradle +++ b/auth/auth-interceptor/build.gradle @@ -1,5 +1,6 @@ dependencies { implementation project(':auth:auth-application') - + implementation project(':core:exception-handler') + implementation 'org.springframework.boot:spring-boot-starter-web' } diff --git a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/AuthExceptionAdvice.java b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/AuthExceptionAdvice.java new file mode 100644 index 00000000..84140b65 --- /dev/null +++ b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/AuthExceptionAdvice.java @@ -0,0 +1,17 @@ +package me.nalab.auth.interceptor; + +import me.nalab.core.exception.handler.ErrorTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class AuthExceptionAdvice { + + @ExceptionHandler(CannotValidTokenException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public ErrorTemplate handleCannotValidTokenException(CannotValidTokenException exception) { + return ErrorTemplate.of(exception.getMessage()); + } +} diff --git a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/CannotValidMockTokenException.java b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/CannotValidMockTokenException.java deleted file mode 100644 index 93c1c6bc..00000000 --- a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/CannotValidMockTokenException.java +++ /dev/null @@ -1,4 +0,0 @@ -package me.nalab.auth.interceptor; - -public class CannotValidMockTokenException extends RuntimeException { -} diff --git a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/CannotValidTokenException.java b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/CannotValidTokenException.java new file mode 100644 index 00000000..5498f7db --- /dev/null +++ b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/CannotValidTokenException.java @@ -0,0 +1,8 @@ +package me.nalab.auth.interceptor; + +public class CannotValidTokenException extends RuntimeException { + + public CannotValidTokenException(String message) { + super(message); + } +} diff --git a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptor.java b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptor.java index 4604ad64..d3e1a7fc 100644 --- a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptor.java +++ b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptor.java @@ -30,12 +30,20 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons if (!isExcludedURI(request)) { String token = request.getHeader("Authorization"); throwIfCannotValidToken(token); - Long targetId = targetIdGetPort.getTargetId(token.split(" ")[1]); + Long targetId = getTargetId(token); request.setAttribute("logined", targetId); } return true; } + private Long getTargetId(String token) { + try { + return targetIdGetPort.getTargetId(token.split(" ")[1]); + } catch (Exception exception) { + throw new CannotValidTokenException(exception.getMessage()); + } + } + private boolean isPreflight(HttpServletRequest request) { return request.getMethod().equals("OPTIONS"); } @@ -63,7 +71,7 @@ private boolean isExcludedURI(HttpServletRequest httpServletRequest) { private void throwIfCannotValidToken(String token) { if (token == null) { - throw new CannotValidMockTokenException(); + throw new CannotValidTokenException("Null token"); } } 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 84bc105d..b57b22da 100644 --- a/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java +++ b/auth/auth-interceptor/src/main/java/me/nalab/auth/interceptor/JwtDecryptInterceptorConfigurer.java @@ -29,8 +29,10 @@ public class JwtDecryptInterceptorConfigurer implements WebMvcConfigurer { "/v1/users", "/v1/gallerys/previews", "/v1/surveys/*/bookmarks", + "/v1/surveys/*/bookmarks/cancels", "/v1/gallerys/logins", "/v1/gallerys", + "/v1/surveys/bookmarks*", }; @Override diff --git a/auth/auth-interceptor/src/main/java/module-info.java b/auth/auth-interceptor/src/main/java/module-info.java deleted file mode 100644 index ecae26d5..00000000 --- a/auth/auth-interceptor/src/main/java/module-info.java +++ /dev/null @@ -1,7 +0,0 @@ -module luffy.auth.auth.interceptor.main { - requires spring.webmvc; - requires spring.context; - requires spring.beans; - requires luffy.auth.auth.application.main; - requires org.apache.tomcat.embed.core; -} diff --git a/gallery/build.gradle b/gallery/build.gradle index 1a0e5aae..4847c734 100644 --- a/gallery/build.gradle +++ b/gallery/build.gradle @@ -4,9 +4,10 @@ repositories { dependencies { implementation project(":core:data") - implementation project(":core:id-generator:id-core") implementation project(":core:time") + implementation project(":core:exception-handler") implementation project(":survey:survey-application") + implementation project(":core:id-generator:id-core") implementation "org.springframework.boot:spring-boot-starter-web" implementation "org.springframework.boot:spring-boot-starter-data-jpa" diff --git a/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt index 4e02e8f0..5df4ff6b 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/app/GalleryGetApp.kt @@ -51,9 +51,9 @@ class GalleryGetApp( } 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()) + return when (orderType.uppercase()) { + "UPDATE" -> PageRequest.of(page, count, Sort.by("updateOrder").descending()) + "BOOKMARK" -> PageRequest.of(page, count, Sort.by("survey.bookmarkedCount").descending()) else -> throw IllegalArgumentException("orderType 은 update와 bookmark중 하나여야 합니다. 현재 orderType \"$orderType\"") } } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt b/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt index be01c988..5aa0ea2b 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/controller/GalleryController.kt @@ -1,5 +1,6 @@ package me.nalab.gallery.controller +import me.nalab.core.exception.handler.ErrorTemplate import me.nalab.gallery.app.GalleryGetApp import me.nalab.gallery.app.GalleryPreviewApp import me.nalab.gallery.app.GalleryRegisterApp @@ -40,12 +41,16 @@ class GalleryController( @GetMapping @ResponseStatus(HttpStatus.OK) fun getGalleries( - @RequestParam(name = "job", defaultValue = "all") job: String, + @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 + @RequestParam(name = "order-type", defaultValue = "UPDATE") orderType: String ): GalleriesDto { return galleryGetApp.getGalleries(job, page, count, orderType) } + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(IllegalArgumentException::class) + fun handleIllegalArgumentException(exception: IllegalArgumentException): ErrorTemplate = + ErrorTemplate.of(exception.message ?: "잘못된 요청입니다.") } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/Gallery.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/Gallery.kt index bc268ef4..c5848d8a 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/domain/Gallery.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/Gallery.kt @@ -40,7 +40,13 @@ class Gallery( fun getBookmarkedCount(): Int = survey.bookmarkedCount - fun increaseBookmarkedCount() { + fun increaseBookmarkCount() { survey.bookmarkedCount++ } + + fun decreaseBookmarkCount() { + survey.bookmarkedCount-- + } + + } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt index 78f2610c..2ded8e7f 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/domain/GalleryService.kt @@ -27,12 +27,17 @@ class GalleryService( @Transactional fun increaseBookmarkCount(targetId: Long) { - galleryRepository.findByTargetIdOrNull(targetId)?.increaseBookmarkedCount() + galleryRepository.findByTargetIdOrNull(targetId)?.increaseBookmarkCount() + } + + @Transactional + fun decreaseBookmarkCount(targetId: Long) { + galleryRepository.findByTargetIdOrNull(targetId)?.decreaseBookmarkCount() } fun getGalleries(job: String, pageable: Pageable): Page { - val jobs = when (job) { - "all" -> Job.entries.toList() + val jobs = when (job.uppercase()) { + "ALL" -> Job.entries.toList() else -> listOf(Job.valueOf(job.uppercase())) } diff --git a/gallery/src/main/kotlin/me/nalab/gallery/infra/SurveyBookmarkedListener.kt b/gallery/src/main/kotlin/me/nalab/gallery/infra/SurveyBookmarkedListener.kt index 1623d0e5..c6ab1249 100644 --- a/gallery/src/main/kotlin/me/nalab/gallery/infra/SurveyBookmarkedListener.kt +++ b/gallery/src/main/kotlin/me/nalab/gallery/infra/SurveyBookmarkedListener.kt @@ -13,14 +13,14 @@ class SurveyBookmarkedListener( ) : SurveyBookmarkListenPort { @Async - override fun listenBookmarked(targetId: Long) { + override fun increaseBookmarked(targetId: Long) { runCatching { galleryService.increaseBookmarkCount(targetId) }.recoverCatching { when (it is OptimisticLockingFailureException) { true -> { waitJitter() - listenBookmarked(targetId) + increaseBookmarked(targetId) } false -> throw it @@ -28,6 +28,23 @@ class SurveyBookmarkedListener( } } + @Async + override fun decreaseBookmarked(targetId: Long) { + runCatching { + galleryService.decreaseBookmarkCount(targetId) + }.recoverCatching { + when (it is OptimisticLockingFailureException) { + true -> { + waitJitter() + decreaseBookmarked(targetId) + } + + false -> throw it + } + } + } + + private fun waitJitter() { Thread.sleep(Random.nextLong(500, 1000)) } diff --git a/support/e2e/v1_10_get_logined_gallery.hurl b/support/e2e/v1_10_get_logined_gallery.hurl index 49eb0ab5..549e2f38 100644 --- a/support/e2e/v1_10_get_logined_gallery.hurl +++ b/support/e2e/v1_10_get_logined_gallery.hurl @@ -110,11 +110,6 @@ 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" ########## diff --git a/support/e2e/v1_11_cancel_bookmark_survey.hurl b/support/e2e/v1_11_cancel_bookmark_survey.hurl new file mode 100644 index 00000000..fa240f16 --- /dev/null +++ b/support/e2e/v1_11_cancel_bookmark_survey.hurl @@ -0,0 +1,116 @@ +POST http://nalab-server:8080/v1/oauth/default # Default provider를 통해서 로그인 진행 +{ + "nickname": "cancel_bookmark_survey", + "email": "cancel_bookmark_survey@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" == "cancel_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] + +########## + +POST http://nalab-server:8080/v1/surveys/{{ survey_id }}/bookmarks/cancels +Authorization: {{ token_type }} {{ auth_token }} + +HTTP 200 +[Asserts] diff --git a/support/e2e/v1_12_find_bookmarked_survey.hurl b/support/e2e/v1_12_find_bookmarked_survey.hurl new file mode 100644 index 00000000..b2405d05 --- /dev/null +++ b/support/e2e/v1_12_find_bookmarked_survey.hurl @@ -0,0 +1,174 @@ +POST http://nalab-server:8080/v1/oauth/default # Default provider를 통해서 로그인 진행 +{ + "nickname": "find_bookmakred_survey1", + "email": "find_bookmakred_survey1@12345" +} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.access_token" exists +jsonpath "$.token_type" exists + +[Captures] +token_type_1: jsonpath "$.token_type" +auth_token_1: jsonpath "$.access_token" + +########## + +POST http://nalab-server:8080/v1/surveys # 발급받은 토큰으로 survey를 생성한다. +Authorization: {{ token_type_1 }} {{ auth_token_1 }} +{ + "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_1: jsonpath "$.survey_id" + +########## + +POST http://nalab-server:8080/v1/oauth/default # Default provider를 통해서 로그인 진행 +{ + "nickname": "find_bookmakred_survey2", + "email": "find_bookmakred_survey2@12345" +} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.access_token" exists +jsonpath "$.token_type" exists + +[Captures] +token_type_2: jsonpath "$.token_type" +auth_token_2: jsonpath "$.access_token" + +########## + +POST http://nalab-server:8080/v1/surveys # 발급받은 토큰으로 survey를 생성한다. +Authorization: {{ token_type_2 }} {{ auth_token_2 }} +{ + "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_2: jsonpath "$.survey_id" + +########## + +POST http://nalab-server:8080/v1/surveys/{{ survey_id_1 }}/bookmarks +Authorization: {{ token_type_1 }} {{ auth_token_1 }} + +HTTP 200 +[Asserts] + +########## + +POST http://nalab-server:8080/v1/surveys/{{ survey_id_2 }}/bookmarks +Authorization: {{ token_type_1 }} {{ auth_token_1 }} + +HTTP 200 +[Asserts] + +########## + +GET http://nalab-server:8080/v1/surveys/bookmarks +Authorization: {{ token_type_1 }} {{ auth_token_1 }} + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.bookmarked_surveys.[0].survey_id" == {{ survey_id_1 }} +jsonpath "$.bookmarked_surveys.[0].nickname" == "find_bookmakred_survey1" + +jsonpath "$.bookmarked_surveys.[1].survey_id" == {{ survey_id_2 }} +jsonpath "$.bookmarked_surveys.[1].nickname" == "find_bookmakred_survey2" + +########## + +GET http://nalab-server:8080/v1/surveys/bookmarks +Authorization: {{ token_type_1 }} {{ auth_token_1 }} + +[QueryStringParams] +last-survey-id: {{ survey_id_1 }} +count: 1 + +HTTP 200 +[Asserts] +header "Content-type" == "application/json" + +jsonpath "$.bookmarked_surveys.[0].survey_id" == {{ survey_id_2 }} +jsonpath "$.bookmarked_surveys.[0].nickname" == "find_bookmakred_survey2" diff --git a/support/e2e/v1_7_bookmark_survey.hurl b/support/e2e/v1_7_bookmark_survey.hurl index 22f20ec8..5893f033 100644 --- a/support/e2e/v1_7_bookmark_survey.hurl +++ b/support/e2e/v1_7_bookmark_survey.hurl @@ -106,8 +106,3 @@ 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 index c9108087..5816f9c6 100644 --- a/support/e2e/v1_9_register_gallery.hurl +++ b/support/e2e/v1_9_register_gallery.hurl @@ -110,11 +110,6 @@ 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" ########## diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/bookmark/BookmarkedSurveyFindUseCase.java b/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/bookmark/BookmarkedSurveyFindUseCase.java new file mode 100644 index 00000000..cee2568c --- /dev/null +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/port/in/web/bookmark/BookmarkedSurveyFindUseCase.java @@ -0,0 +1,13 @@ +package me.nalab.survey.application.port.in.web.bookmark; + +import java.util.List; +import me.nalab.survey.application.common.survey.dto.SurveyBookmarkDto; + +public interface BookmarkedSurveyFindUseCase { + + /** + * targetId를 입력받아 SurveyBookmarkDto를 반환합니다. + */ + List findBookmarkedSurveysByTargetId(Long targetId, Long lastSurveyId, Integer count); + +} 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 index a363a9b2..1c32fcc9 100644 --- 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 @@ -1,13 +1,15 @@ 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); + void bookmark(Long targetId, Long surveyId); + + /** + * targetId에 해당하는 유저에게 survey를 북마크 취소합니다. 북마크 되어있지 않다면, 아무동작도 하지 않습니다. + */ + void cancelBookmark(Long targetId, Long surveyId); } 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 index 5935c6b5..16678c5c 100644 --- 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 @@ -2,5 +2,7 @@ public interface SurveyBookmarkListenPort { - void listenBookmarked(Long targetId); + void increaseBookmarked(Long targetId); + + void decreaseBookmarked(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 index 3d940bd4..ccfb8648 100644 --- 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 @@ -4,5 +4,5 @@ public interface SurveyBookmarkPort { - void bookmark(Target target); + void updateBookmark(Target target); } 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 9cb96216..dcbb1ada 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 @@ -1,7 +1,8 @@ package me.nalab.survey.application.port.out.persistence.findtarget; +import java.util.List; import java.util.Optional; - +import java.util.Set; import me.nalab.survey.domain.target.Target; /** @@ -23,4 +24,8 @@ public interface TargetFindPort { */ Target getTargetById(Long targetId); + /** + * surveyId 들로 Target들을 조회합니다. + */ + List findAllTargetBySurveyIds(Set surveyIds); } diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/BookmarkedSurveyFindService.java b/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/BookmarkedSurveyFindService.java new file mode 100644 index 00000000..bcba499e --- /dev/null +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/BookmarkedSurveyFindService.java @@ -0,0 +1,44 @@ +package me.nalab.survey.application.service.bookmark; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import me.nalab.survey.application.common.survey.dto.SurveyBookmarkDto; +import me.nalab.survey.application.port.in.web.bookmark.BookmarkedSurveyFindUseCase; +import me.nalab.survey.application.port.out.persistence.findtarget.TargetFindPort; +import me.nalab.survey.application.port.out.persistence.survey.find.SurveyFindPort; +import me.nalab.survey.domain.target.SurveyBookmark; +import me.nalab.survey.domain.target.Target; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class BookmarkedSurveyFindService implements BookmarkedSurveyFindUseCase { + + private final TargetFindPort targetFindPort; + private final SurveyFindPort surveyFindPort; + + @Override + @Transactional(readOnly = true) + public List findBookmarkedSurveysByTargetId(Long targetId, Long lastSurveyId, Integer count) { + var target = targetFindPort.getTargetById(targetId); + var bookmarkedTargets = targetFindPort.findAllTargetBySurveyIds(getSurveyIds(target, lastSurveyId, count)); + + return bookmarkedTargets.stream() + .map(bookmarkedTarget -> { + var surveyId = bookmarkedTarget.getSurveyList().get(0).getId(); + return SurveyBookmarkDto.from(surveyId, bookmarkedTarget); + }) + .toList(); + } + + private Set getSurveyIds(Target target, Long lastSurveyId, Integer count) { + return target.getBookmarkedSurveys().stream() + .map(SurveyBookmark::surveyId) + .filter(aLong -> aLong > lastSurveyId) + .limit(count) + .collect(Collectors.toSet()); + } +} diff --git a/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkService.java b/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkService.java index afb3dc0e..9b60e3b1 100644 --- a/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkService.java +++ b/survey/survey-application/src/main/java/me/nalab/survey/application/service/bookmark/SurveyBookmarkService.java @@ -1,7 +1,6 @@ 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; @@ -22,7 +21,7 @@ public class SurveyBookmarkService implements SurveyBookmarkUseCase { @Override @Transactional - public SurveyBookmarkDto bookmark(Long targetId, Long surveyId) { + public void bookmark(Long targetId, Long surveyId) { var target = targetFindPort.getTargetById(targetId); if (!surveyExistCheckPort.isExistSurveyBySurveyId(surveyId)) { @@ -30,10 +29,23 @@ public SurveyBookmarkDto bookmark(Long targetId, Long surveyId) { } target.bookmark(surveyId); - surveyBookmarkPort.bookmark(target); + surveyBookmarkPort.updateBookmark(target); - surveyBookmarkListener.listenBookmarked(targetId); + surveyBookmarkListener.increaseBookmarked(targetId); + } + + @Override + @Transactional + public void cancelBookmark(Long targetId, Long surveyId) { + var target = targetFindPort.getTargetById(targetId); + + if (!surveyExistCheckPort.isExistSurveyBySurveyId(surveyId)) { + throw new SurveyDoesNotExistException(surveyId); + } + + target.cancelBookmark(surveyId); + surveyBookmarkPort.updateBookmark(target); - return SurveyBookmarkDto.from(surveyId, target); + surveyBookmarkListener.decreaseBookmarked(targetId); } } 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 db791c8d..530e2878 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 @@ -2,6 +2,7 @@ import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.function.LongSupplier; import lombok.Builder; @@ -45,4 +46,11 @@ public void bookmark(Long surveyId) { var bookmark = new SurveyBookmark(surveyId); bookmarkedSurveys.add(bookmark); } + + public void cancelBookmark(Long surveyId) { + bookmarkedSurveys.stream() + .filter(surveyBookmark -> Objects.equals(surveyBookmark.surveyId(), surveyId)) + .findFirst() + .ifPresent(bookmarkedSurveys::remove); + } } 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 index 7e069df8..961d418b 100644 --- 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 @@ -15,7 +15,7 @@ public class SurveyBookmarkAdaptor implements SurveyBookmarkPort { private final TargetFindRepository targetFindRepository; @Override - public void bookmark(Target target) { + public void updateBookmark(Target target) { var savedTarget = targetFindRepository.findById(target.getId()) .orElseThrow(() -> new TargetDoesNotExistException(target.getId())); 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 5135c3cd..a3bfca35 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,7 +1,9 @@ package me.nalab.survey.jpa.adaptor.common.mapper; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import me.nalab.core.data.survey.SurveyEntity; import me.nalab.core.data.target.SurveyBookmarkEntity; import me.nalab.core.data.target.TargetEntity; import me.nalab.survey.domain.target.SurveyBookmark; @@ -40,6 +42,17 @@ public static Target toTarget(TargetEntity targetEntity) { .build(); } + public static Target toTargetWithSurvey(TargetEntity targetEntity, SurveyEntity surveyEntity) { + return Target.builder() + .id(targetEntity.getId()) + .surveyList(List.of(SurveyEntityMapper.toSurvey(surveyEntity))) + .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())) 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 e31101c2..8dcb823d 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 @@ -1,8 +1,13 @@ package me.nalab.survey.jpa.adaptor.findtarget; +import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.Set; +import me.nalab.core.data.survey.SurveyEntity; import me.nalab.survey.application.exception.TargetDoesNotExistException; +import me.nalab.survey.jpa.adaptor.findtarget.repository.TargetIdFindJpaRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Repository; @@ -17,11 +22,14 @@ public class TargetFindAdaptor implements TargetFindPort { private final TargetFindJpaRepository targetFindJpaRepository; + private final TargetIdFindJpaRepository targetIdFindJpaRepository; @Autowired TargetFindAdaptor( - @Qualifier("findtarget.TargetFindJpaRepository") TargetFindJpaRepository targetFindJpaRepository) { + @Qualifier("findtarget.TargetFindJpaRepository") TargetFindJpaRepository targetFindJpaRepository, + @Qualifier("findtarget.TargetIdFindJpaRepository") TargetIdFindJpaRepository targetIdFindJpaRepository) { this.targetFindJpaRepository = targetFindJpaRepository; + this.targetIdFindJpaRepository = targetIdFindJpaRepository; } @Override @@ -37,4 +45,29 @@ public Target getTargetById(Long targetId) { return TargetEntityMapper.toTarget(targetEntity); } + + @Override + public List findAllTargetBySurveyIds(Set surveyIds) { + var surveys = targetIdFindJpaRepository.findAllById(surveyIds); + var targetIds = surveys + .stream() + .map(SurveyEntity::getTargetId) + .toList(); + + return targetFindJpaRepository.findAllById(targetIds) + .stream() + .map(targetEntity -> { + var surveyEntity = getSurveyEntityByTargetId(targetEntity.getId(), surveys); + return TargetEntityMapper.toTargetWithSurvey(targetEntity, surveyEntity); + }) + .toList(); + } + + private SurveyEntity getSurveyEntityByTargetId(Long targetId, List surveyEntities) { + return surveyEntities.stream() + .filter(entity -> Objects.equals(entity.getTargetId(), targetId)) + .findFirst().orElseThrow( + () -> new IllegalStateException("Cannot find exist survey") + ); + } } diff --git a/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/BookmarkedSurveyFindController.java b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/BookmarkedSurveyFindController.java new file mode 100644 index 00000000..c65105f9 --- /dev/null +++ b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/BookmarkedSurveyFindController.java @@ -0,0 +1,33 @@ +package me.nalab.survey.web.adaptor.bookmark; + +import lombok.RequiredArgsConstructor; +import me.nalab.survey.application.port.in.web.bookmark.BookmarkedSurveyFindUseCase; +import me.nalab.survey.web.adaptor.bookmark.response.SurveyBookmarksResponse; +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.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/v1") +@RequiredArgsConstructor +public class BookmarkedSurveyFindController { + + private final BookmarkedSurveyFindUseCase bookmarkedSurveyFindUseCase; + + @GetMapping("/surveys/bookmarks") + @ResponseStatus(HttpStatus.OK) + public SurveyBookmarksResponse findBookmarkedSurveys( + @RequestAttribute("logined") Long loginedTargetId, + @RequestParam(value = "last-survey-id", defaultValue = "0") Long lastSurveyId, + @RequestParam(value = "count", defaultValue = "20") Integer count + ) { + var bookmarkedSurveys = bookmarkedSurveyFindUseCase.findBookmarkedSurveysByTargetId(loginedTargetId, + lastSurveyId, count); + return SurveyBookmarksResponse.of(bookmarkedSurveys); + } + +} 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/SurveyBookmarkController.java similarity index 64% rename from survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/SurveyBookmarkReplaceController.java rename to survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/SurveyBookmarkController.java index 9daf08cf..1ffe0b8f 100644 --- a/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/SurveyBookmarkReplaceController.java +++ b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/SurveyBookmarkController.java @@ -2,7 +2,6 @@ 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; @@ -14,17 +13,21 @@ @RestController @RequestMapping("/v1") @RequiredArgsConstructor -public class SurveyBookmarkReplaceController { +public class SurveyBookmarkController { private final SurveyBookmarkUseCase surveyBookmarkReplaceUseCase; @ResponseStatus(HttpStatus.OK) @PostMapping("/surveys/{survey_id}/bookmarks") - public SurveyBookmarkResponse replaceBookmark(@RequestAttribute("logined") Long targetId, + public void bookmark(@RequestAttribute("logined") Long targetId, @PathVariable("survey_id") Long surveyId) { - var surveyBookmarked = surveyBookmarkReplaceUseCase.bookmark(targetId, surveyId); - - return SurveyBookmarkResponse.of(surveyBookmarked); + surveyBookmarkReplaceUseCase.bookmark(targetId, surveyId); } + @ResponseStatus(HttpStatus.OK) + @PostMapping("/surveys/{survey_id}/bookmarks/cancels") + public void cancelBookmark(@RequestAttribute("logined") Long targetId, + @PathVariable("survey_id") Long surveyId) { + surveyBookmarkReplaceUseCase.cancelBookmark(targetId, surveyId); + } } diff --git a/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/response/SurveyBookmarkedResponse.java b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/response/SurveyBookmarkedResponse.java new file mode 100644 index 00000000..5194c150 --- /dev/null +++ b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/response/SurveyBookmarkedResponse.java @@ -0,0 +1,13 @@ +package me.nalab.survey.web.adaptor.bookmark.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record SurveyBookmarkedResponse( + @JsonProperty("survey_id") + String surveyId +) { + + public static SurveyBookmarkedResponse of(Long surveyId) { + return new SurveyBookmarkedResponse(surveyId.toString()); + } +} diff --git a/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/response/SurveyBookmarksResponse.java b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/response/SurveyBookmarksResponse.java new file mode 100644 index 00000000..dcd711a1 --- /dev/null +++ b/survey/survey-web-adaptor/src/main/java/me/nalab/survey/web/adaptor/bookmark/response/SurveyBookmarksResponse.java @@ -0,0 +1,20 @@ +package me.nalab.survey.web.adaptor.bookmark.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import me.nalab.survey.application.common.survey.dto.SurveyBookmarkDto; + +public record SurveyBookmarksResponse( + @JsonProperty("bookmarked_surveys") + List bookmarkedSurveys +) { + + public static SurveyBookmarksResponse of(List surveyBookmarkDtos) { + return new SurveyBookmarksResponse( + surveyBookmarkDtos.stream() + .map(SurveyBookmarkResponse::of) + .toList() + ); + } + +}