From fdf6bd60f6e199677bac6f49772ac9898cad5fc6 Mon Sep 17 00:00:00 2001 From: vectorch Date: Tue, 14 Nov 2023 14:24:27 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feature:=20Quests=EC=97=90=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=20=EC=97=AC=EB=B6=80=20=EA=B2=80=EC=A6=9D=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/daybyquest/quest/domain/Quests.java | 6 ++++++ .../daybyquest/quest/domain/QuestsTest.java | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/main/java/daybyquest/quest/domain/Quests.java b/src/main/java/daybyquest/quest/domain/Quests.java index f7146c7..1332c8d 100644 --- a/src/main/java/daybyquest/quest/domain/Quests.java +++ b/src/main/java/daybyquest/quest/domain/Quests.java @@ -31,6 +31,12 @@ public Quest getById(final Long id) { return questRepository.findById(id).orElseThrow(NotExistQuestException::new); } + public void validateExistentById(final Long id) { + if (!questRepository.existsById(id)) { + throw new NotExistQuestException(); + } + } + private void validateNotExistentByBadgeId(final Long badgeId) { if (questRepository.existsByBadgeId(badgeId)) { throw new InvalidDomainException(ALREADY_EXIST_REWARD); diff --git a/src/test/java/daybyquest/quest/domain/QuestsTest.java b/src/test/java/daybyquest/quest/domain/QuestsTest.java index d285c44..22f9149 100644 --- a/src/test/java/daybyquest/quest/domain/QuestsTest.java +++ b/src/test/java/daybyquest/quest/domain/QuestsTest.java @@ -96,4 +96,24 @@ public class QuestsTest { assertThatThrownBy(() -> quests.getById(1L)) .isInstanceOf(NotExistQuestException.class); } + + @Test + void ID를_통해_퀘스트_존재를_검증한다() { + // given + final Long questId = 1L; + given(questRepository.existsById(questId)).willReturn(true); + + // when + quests.validateExistentById(questId); + + // then + then(questRepository).should().existsById(questId); + } + + @Test + void ID를_통한_퀘스트_존재_검증_시_없다면_예외를_던진다() { + // given & when & then + assertThatThrownBy(() -> quests.validateExistentById(1L)) + .isInstanceOf(NotExistQuestException.class); + } } From 54f417f6b73ecd3bdc32abe18ca4f8edb1ca5157 Mon Sep 17 00:00:00 2001 From: vectorch Date: Tue, 14 Nov 2023 15:46:55 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feature:=20=ED=80=98=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=9D=BC=EB=B2=A8=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=EB=8F=85=EC=9D=84=20=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/InternalServerException.java | 10 +++ .../application/GroupQuestValidator.java | 26 +++++++ .../quest/application/QuestSseEmitters.java | 78 +++++++++++++++++++ .../application/SendQuestLabelsService.java | 29 +++++++ .../SubscribeGroupQuestLabelsService.java | 23 ++++++ .../SubscribeQuestLabelsService.java | 23 ++++++ .../quest/dto/request/QuestLabelsRequest.java | 12 +++ .../dto/response/QuestLabelsResponse.java | 7 ++ .../quest/presentation/QuestCommandApi.java | 47 ++++++++++- 9 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 src/main/java/daybyquest/global/error/exception/InternalServerException.java create mode 100644 src/main/java/daybyquest/quest/application/GroupQuestValidator.java create mode 100644 src/main/java/daybyquest/quest/application/QuestSseEmitters.java create mode 100644 src/main/java/daybyquest/quest/application/SendQuestLabelsService.java create mode 100644 src/main/java/daybyquest/quest/application/SubscribeGroupQuestLabelsService.java create mode 100644 src/main/java/daybyquest/quest/application/SubscribeQuestLabelsService.java create mode 100644 src/main/java/daybyquest/quest/dto/request/QuestLabelsRequest.java create mode 100644 src/main/java/daybyquest/quest/dto/response/QuestLabelsResponse.java diff --git a/src/main/java/daybyquest/global/error/exception/InternalServerException.java b/src/main/java/daybyquest/global/error/exception/InternalServerException.java new file mode 100644 index 0000000..da0afe5 --- /dev/null +++ b/src/main/java/daybyquest/global/error/exception/InternalServerException.java @@ -0,0 +1,10 @@ +package daybyquest.global.error.exception; + +import static daybyquest.global.error.ExceptionCode.INTERNAL_SERVER; + +public class InternalServerException extends CustomException { + + public InternalServerException() { + super(INTERNAL_SERVER); + } +} diff --git a/src/main/java/daybyquest/quest/application/GroupQuestValidator.java b/src/main/java/daybyquest/quest/application/GroupQuestValidator.java new file mode 100644 index 0000000..94b8b15 --- /dev/null +++ b/src/main/java/daybyquest/quest/application/GroupQuestValidator.java @@ -0,0 +1,26 @@ +package daybyquest.quest.application; + +import daybyquest.group.domain.GroupUsers; +import daybyquest.quest.domain.Quest; +import daybyquest.quest.domain.Quests; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +public class GroupQuestValidator { + + private final Quests quests; + + private final GroupUsers groupUsers; + + public GroupQuestValidator(final Quests quests, final GroupUsers groupUsers) { + this.quests = quests; + this.groupUsers = groupUsers; + } + + @Transactional(readOnly = true) + public void validate(final Long loginId, final Long questId) { + final Quest quest = quests.getById(questId); + groupUsers.validateGroupManager(loginId, quest.getGroupId()); + } +} diff --git a/src/main/java/daybyquest/quest/application/QuestSseEmitters.java b/src/main/java/daybyquest/quest/application/QuestSseEmitters.java new file mode 100644 index 0000000..35591c3 --- /dev/null +++ b/src/main/java/daybyquest/quest/application/QuestSseEmitters.java @@ -0,0 +1,78 @@ +package daybyquest.quest.application; + +import daybyquest.global.error.exception.InternalServerException; +import daybyquest.quest.dto.response.QuestLabelsResponse; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@Component +@Slf4j +public class QuestSseEmitters { + + private static final Long SSE_TIMEOUT = 10L * 1000 * 60; + + private static final String MESSAGE_NAME = "labels"; + + private static final String CONNECT_MESSAGE_NAME = "connect"; + + private static final String CONNECT_MESSAGE_CONTENT = "success"; + + private final Map emitters; + + private final Map cache; + + public QuestSseEmitters() { + this.emitters = new ConcurrentHashMap<>(); + this.cache = new ConcurrentHashMap<>(); + } + + public SseEmitter subscribe(final Long questId) { + final SseEmitter emitter = new SseEmitter(SSE_TIMEOUT); + emitter.onCompletion(() -> { + log.debug("QuestSseEmiiter finished with completiong. Quest ID: {}", questId); + emitters.remove(questId); + }); + emitter.onTimeout(() -> { + log.debug("QuestSseEmiiter finished with timeout. Quest ID: {}", questId); + emitter.complete(); + }); + emitters.put(questId, emitter); + sendConnectedMessage(emitter); + sendCachedMessage(questId); + return emitter; + } + + private void sendConnectedMessage(final SseEmitter emitter) { + try { + emitter.send(SseEmitter.event().name(CONNECT_MESSAGE_NAME).data(CONNECT_MESSAGE_CONTENT)); + } catch (final IOException e) { + log.error("IOException occurred while sending connected message."); + log.error(e.getMessage()); + } + } + + private void sendCachedMessage(final Long questId) { + if (cache.containsKey(questId)) { + send(questId, cache.get(questId)); + cache.remove(questId); + } + } + + public void send(final Long questId, final QuestLabelsResponse content) { + if (emitters.containsKey(questId)) { + final SseEmitter emitter = emitters.get(questId); + try { + emitter.send(SseEmitter.event().name(MESSAGE_NAME).data(content)); + } catch (final IOException e) { + log.error("IOException occurred while sending quest labels.: quest id {}", questId); + throw new InternalServerException(); + } + return; + } + cache.put(questId, content); + } +} diff --git a/src/main/java/daybyquest/quest/application/SendQuestLabelsService.java b/src/main/java/daybyquest/quest/application/SendQuestLabelsService.java new file mode 100644 index 0000000..779b471 --- /dev/null +++ b/src/main/java/daybyquest/quest/application/SendQuestLabelsService.java @@ -0,0 +1,29 @@ +package daybyquest.quest.application; + +import daybyquest.quest.domain.Quests; +import daybyquest.quest.dto.request.QuestLabelsRequest; +import daybyquest.quest.dto.response.QuestLabelsResponse; +import org.springframework.stereotype.Service; + +@Service +public class SendQuestLabelsService { + + private final Quests quests; + + private final QuestSseEmitters questSseEmitters; + + public SendQuestLabelsService(final Quests quests, final QuestSseEmitters questSseEmitters) { + this.quests = quests; + this.questSseEmitters = questSseEmitters; + } + + public void invoke(final Long questId, final QuestLabelsRequest request) { + quests.validateExistentById(questId); + questSseEmitters.send(questId, requestToResponse(request)); + } + + private QuestLabelsResponse requestToResponse(final QuestLabelsRequest request) { + return new QuestLabelsResponse(request.getLabels()); + } + +} diff --git a/src/main/java/daybyquest/quest/application/SubscribeGroupQuestLabelsService.java b/src/main/java/daybyquest/quest/application/SubscribeGroupQuestLabelsService.java new file mode 100644 index 0000000..bf4b973 --- /dev/null +++ b/src/main/java/daybyquest/quest/application/SubscribeGroupQuestLabelsService.java @@ -0,0 +1,23 @@ +package daybyquest.quest.application; + +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@Service +public class SubscribeGroupQuestLabelsService { + + private final GroupQuestValidator groupQuestValidator; + + private final QuestSseEmitters questSseEmitters; + + public SubscribeGroupQuestLabelsService(final GroupQuestValidator groupQuestValidator, + final QuestSseEmitters questSseEmitters) { + this.groupQuestValidator = groupQuestValidator; + this.questSseEmitters = questSseEmitters; + } + + public SseEmitter invoke(final Long loginId, final Long questId) { + groupQuestValidator.validate(loginId, questId); + return questSseEmitters.subscribe(questId); + } +} diff --git a/src/main/java/daybyquest/quest/application/SubscribeQuestLabelsService.java b/src/main/java/daybyquest/quest/application/SubscribeQuestLabelsService.java new file mode 100644 index 0000000..afa55c1 --- /dev/null +++ b/src/main/java/daybyquest/quest/application/SubscribeQuestLabelsService.java @@ -0,0 +1,23 @@ +package daybyquest.quest.application; + +import daybyquest.quest.domain.Quests; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@Service +public class SubscribeQuestLabelsService { + + private final Quests quests; + + private final QuestSseEmitters questSseEmitters; + + public SubscribeQuestLabelsService(final Quests quests, final QuestSseEmitters questSseEmitters) { + this.quests = quests; + this.questSseEmitters = questSseEmitters; + } + + public SseEmitter invoke(final Long questId) { + quests.validateExistentById(questId); + return questSseEmitters.subscribe(questId); + } +} diff --git a/src/main/java/daybyquest/quest/dto/request/QuestLabelsRequest.java b/src/main/java/daybyquest/quest/dto/request/QuestLabelsRequest.java new file mode 100644 index 0000000..cbeec1f --- /dev/null +++ b/src/main/java/daybyquest/quest/dto/request/QuestLabelsRequest.java @@ -0,0 +1,12 @@ +package daybyquest.quest.dto.request; + +import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class QuestLabelsRequest { + + private List labels; +} diff --git a/src/main/java/daybyquest/quest/dto/response/QuestLabelsResponse.java b/src/main/java/daybyquest/quest/dto/response/QuestLabelsResponse.java new file mode 100644 index 0000000..cdf8120 --- /dev/null +++ b/src/main/java/daybyquest/quest/dto/response/QuestLabelsResponse.java @@ -0,0 +1,7 @@ +package daybyquest.quest.dto.response; + +import java.util.List; + +public record QuestLabelsResponse(List labels) { + +} diff --git a/src/main/java/daybyquest/quest/presentation/QuestCommandApi.java b/src/main/java/daybyquest/quest/presentation/QuestCommandApi.java index 30f68cd..057623b 100644 --- a/src/main/java/daybyquest/quest/presentation/QuestCommandApi.java +++ b/src/main/java/daybyquest/quest/presentation/QuestCommandApi.java @@ -1,11 +1,17 @@ package daybyquest.quest.presentation; +import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE; + import daybyquest.auth.Authorization; import daybyquest.auth.domain.AccessUser; import daybyquest.quest.application.SaveGroupQuestDetailService; import daybyquest.quest.application.SaveGroupQuestService; import daybyquest.quest.application.SaveQuestDetailService; import daybyquest.quest.application.SaveQuestService; +import daybyquest.quest.application.SendQuestLabelsService; +import daybyquest.quest.application.SubscribeGroupQuestLabelsService; +import daybyquest.quest.application.SubscribeQuestLabelsService; +import daybyquest.quest.dto.request.QuestLabelsRequest; import daybyquest.quest.dto.request.SaveGroupQuestDetailRequest; import daybyquest.quest.dto.request.SaveGroupQuestRequest; import daybyquest.quest.dto.request.SaveQuestDetailRequest; @@ -14,12 +20,15 @@ import jakarta.validation.Valid; import java.util.List; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @RestController public class QuestCommandApi { @@ -32,14 +41,26 @@ public class QuestCommandApi { private final SaveGroupQuestDetailService saveGroupQuestDetailService; + private final SubscribeQuestLabelsService subscribeQuestLabelsService; + + private final SubscribeGroupQuestLabelsService subscribeGroupQuestLabelsService; + + private final SendQuestLabelsService sendQuestLabelsService; + public QuestCommandApi(final SaveQuestService saveQuestService, final SaveGroupQuestService saveGroupQuestService, final SaveQuestDetailService saveQuestDetailService, - final SaveGroupQuestDetailService saveGroupQuestDetailService) { + final SaveGroupQuestDetailService saveGroupQuestDetailService, + final SubscribeQuestLabelsService subscribeQuestLabelsService, + final SubscribeGroupQuestLabelsService subscribeGroupQuestLabelsService, + final SendQuestLabelsService sendQuestLabelsService) { this.saveQuestService = saveQuestService; this.saveGroupQuestService = saveGroupQuestService; this.saveQuestDetailService = saveQuestDetailService; this.saveGroupQuestDetailService = saveGroupQuestDetailService; + this.subscribeQuestLabelsService = subscribeQuestLabelsService; + this.subscribeGroupQuestLabelsService = subscribeGroupQuestLabelsService; + this.sendQuestLabelsService = sendQuestLabelsService; } @PostMapping("/quest") @@ -75,4 +96,28 @@ public ResponseEntity saveGroupQuestDetail(final AccessUser a saveGroupQuestDetailService.invoke(accessUser.getId(), questId, request); return ResponseEntity.ok(new SaveQuestResponse(questId)); } + + @GetMapping(value = "/quest/{questId}/labels", produces = TEXT_EVENT_STREAM_VALUE) + @Authorization(admin = true) + public ResponseEntity subscribeQuestLabels(final AccessUser accessUser, + @PathVariable final Long questId) { + final SseEmitter emitter = subscribeQuestLabelsService.invoke(questId); + return ResponseEntity.ok(emitter); + } + + @GetMapping(value = "/group/{questId}/quest/labels", produces = TEXT_EVENT_STREAM_VALUE) + @Authorization + public ResponseEntity subscribeGroupQuestLabels(final AccessUser accessUser, + @PathVariable final Long questId) { + final SseEmitter emitter = subscribeGroupQuestLabelsService.invoke(accessUser.getId(), questId); + return ResponseEntity.ok(emitter); + } + + @PatchMapping(value = "/quest/{questId}/shot") + @Authorization(admin = true) + public ResponseEntity sendQuestLabels(final AccessUser accessUser, + @PathVariable final Long questId, @RequestBody final QuestLabelsRequest request) { + sendQuestLabelsService.invoke(questId, request); + return ResponseEntity.ok().build(); + } } From 5c197677211e5d4acce007de4c90dbe8c76d83c8 Mon Sep 17 00:00:00 2001 From: vectorch Date: Tue, 14 Nov 2023 16:12:48 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20QuestStubClient=20=EB=82=B4=20?= =?UTF-8?q?=EC=9D=98=EB=AF=B8=EC=97=86=EB=8A=94=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=82=AD=EC=A0=9C=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/daybyquest/quest/infra/QuestStubClient.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/daybyquest/quest/infra/QuestStubClient.java b/src/main/java/daybyquest/quest/infra/QuestStubClient.java index 46b1631..51652c1 100644 --- a/src/main/java/daybyquest/quest/infra/QuestStubClient.java +++ b/src/main/java/daybyquest/quest/infra/QuestStubClient.java @@ -4,20 +4,12 @@ import java.util.List; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.client.WebClient; @Component @Profile("!prod") public class QuestStubClient implements QuestClient { - private final WebClient webClient; - - public QuestStubClient(final WebClient webClient) { - this.webClient = webClient; - } - @Override public void requestLabels(final Long questId, final List identifiers) { - return; } } From 2d9fdb7416708f8363d98b4c6c4817dbaa23f268 Mon Sep 17 00:00:00 2001 From: vectorch Date: Tue, 14 Nov 2023 16:13:08 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feature:=20=EA=B2=8C=EC=8B=9C=EB=AC=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=ED=80=98=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EA=B0=80=20=EB=A7=81=ED=81=AC=EB=90=98=EC=96=B4=20=EC=9E=88?= =?UTF-8?q?=EC=97=88=EB=8B=A4=EB=A9=B4=20=EB=9D=BC=EB=B2=A8=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=9A=94=EC=B2=AD=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/application/PostClient.java | 8 +++ .../post/application/SavePostService.java | 21 +++++- .../java/daybyquest/post/domain/Post.java | 4 ++ .../java/daybyquest/post/domain/Posts.java | 4 +- .../post/dto/request/PostJudgeRequest.java | 7 ++ .../daybyquest/post/infra/PostStubClient.java | 15 +++++ .../daybyquest/post/infra/PostWebClient.java | 38 +++++++++++ .../java/daybyquest/quest/domain/Quests.java | 4 ++ .../java/daybyquest/post/domain/PostTest.java | 65 +++++++++++++++++++ .../daybyquest/quest/domain/QuestsTest.java | 22 +++++++ 10 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 src/main/java/daybyquest/post/application/PostClient.java create mode 100644 src/main/java/daybyquest/post/dto/request/PostJudgeRequest.java create mode 100644 src/main/java/daybyquest/post/infra/PostStubClient.java create mode 100644 src/main/java/daybyquest/post/infra/PostWebClient.java diff --git a/src/main/java/daybyquest/post/application/PostClient.java b/src/main/java/daybyquest/post/application/PostClient.java new file mode 100644 index 0000000..8fccfbf --- /dev/null +++ b/src/main/java/daybyquest/post/application/PostClient.java @@ -0,0 +1,8 @@ +package daybyquest.post.application; + +import java.util.List; + +public interface PostClient { + + void requestJudge(final Long postId, final String label, final List identifiers); +} diff --git a/src/main/java/daybyquest/post/application/SavePostService.java b/src/main/java/daybyquest/post/application/SavePostService.java index 5feddba..91e79eb 100644 --- a/src/main/java/daybyquest/post/application/SavePostService.java +++ b/src/main/java/daybyquest/post/application/SavePostService.java @@ -7,6 +7,7 @@ import daybyquest.post.domain.Post; import daybyquest.post.domain.Posts; import daybyquest.post.dto.request.SavePostRequest; +import daybyquest.quest.domain.Quests; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,18 +24,25 @@ public class SavePostService { private final ImageIdentifierGenerator generator; + private final PostClient postClient; + + private final Quests quests; + public SavePostService(final Posts posts, final Images images, - final ImageIdentifierGenerator generator) { + final ImageIdentifierGenerator generator, final PostClient postClient, final Quests quests) { this.posts = posts; this.images = images; this.generator = generator; + this.postClient = postClient; + this.quests = quests; } @Transactional public Long invoke(final Long loginId, final SavePostRequest request, final List files) { - final Post post = toEntity(loginId, request, toImageList(files)); - return posts.save(post); + final Post post = posts.save(toEntity(loginId, request, toImageList(files))); + requestJudge(post); + return post.getId(); } private List toImageList(final List files) { @@ -47,4 +55,11 @@ private List toImageList(final List files) { private Post toEntity(final Long loginId, final SavePostRequest request, final List images) { return new Post(loginId, request.getQuestId(), request.getContent(), images); } + + private void requestJudge(final Post post) { + if (post.isQuestLinked()) { + postClient.requestJudge(post.getId(), quests.getLabelById(post.getQuestId()), + post.getImages().stream().map(Image::getIdentifier).toList()); + } + } } diff --git a/src/main/java/daybyquest/post/domain/Post.java b/src/main/java/daybyquest/post/domain/Post.java index 6bd3a61..0d70d1c 100644 --- a/src/main/java/daybyquest/post/domain/Post.java +++ b/src/main/java/daybyquest/post/domain/Post.java @@ -114,4 +114,8 @@ public void needCheck() { } state = NEED_CHECK; } + + public boolean isQuestLinked() { + return questId != null; + } } diff --git a/src/main/java/daybyquest/post/domain/Posts.java b/src/main/java/daybyquest/post/domain/Posts.java index 60e5051..c3f790f 100644 --- a/src/main/java/daybyquest/post/domain/Posts.java +++ b/src/main/java/daybyquest/post/domain/Posts.java @@ -20,12 +20,12 @@ public class Posts { this.participants = participants; } - public Long save(final Post post) { + public Post save(final Post post) { users.validateExistentById(post.getUserId()); if (post.getQuestId() != null) { participants.validateExistent(post.getUserId(), post.getQuestId()); } - return postRepository.save(post).getId(); + return postRepository.save(post); } public Post getById(final Long id) { diff --git a/src/main/java/daybyquest/post/dto/request/PostJudgeRequest.java b/src/main/java/daybyquest/post/dto/request/PostJudgeRequest.java new file mode 100644 index 0000000..e8a324e --- /dev/null +++ b/src/main/java/daybyquest/post/dto/request/PostJudgeRequest.java @@ -0,0 +1,7 @@ +package daybyquest.post.dto.request; + +import java.util.List; + +public record PostJudgeRequest(String label, List imageIdentifiers) { + +} diff --git a/src/main/java/daybyquest/post/infra/PostStubClient.java b/src/main/java/daybyquest/post/infra/PostStubClient.java new file mode 100644 index 0000000..4ba1cdf --- /dev/null +++ b/src/main/java/daybyquest/post/infra/PostStubClient.java @@ -0,0 +1,15 @@ +package daybyquest.post.infra; + +import daybyquest.post.application.PostClient; +import java.util.List; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile("!prod") +public class PostStubClient implements PostClient { + + @Override + public void requestJudge(final Long postId, final String label, final List identifiers) { + } +} diff --git a/src/main/java/daybyquest/post/infra/PostWebClient.java b/src/main/java/daybyquest/post/infra/PostWebClient.java new file mode 100644 index 0000000..b9d2f5c --- /dev/null +++ b/src/main/java/daybyquest/post/infra/PostWebClient.java @@ -0,0 +1,38 @@ +package daybyquest.post.infra; + +import daybyquest.global.error.exception.BadRequestException; +import daybyquest.post.application.PostClient; +import daybyquest.post.dto.request.PostJudgeRequest; +import java.util.List; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatusCode; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +@Component +@Profile("prod") +public class PostWebClient implements PostClient { + + private final WebClient webClient; + + public PostWebClient(final WebClient webClient) { + this.webClient = webClient; + } + + @Override + public void requestJudge(final Long postId, final String label, final List identifiers) { + final PostJudgeRequest request = new PostJudgeRequest(label, identifiers); + webClient.post() + .uri(uriBuilder -> uriBuilder.pathSegment("post", postId.toString(), "judge").build()) + .bodyValue(request) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, response -> { + throw new BadRequestException(); + }) + .onStatus(HttpStatusCode::is5xxServerError, response -> { + throw new BadRequestException(); + }) + .toBodilessEntity() + .block(); + } +} diff --git a/src/main/java/daybyquest/quest/domain/Quests.java b/src/main/java/daybyquest/quest/domain/Quests.java index 1332c8d..88a8fc1 100644 --- a/src/main/java/daybyquest/quest/domain/Quests.java +++ b/src/main/java/daybyquest/quest/domain/Quests.java @@ -42,4 +42,8 @@ private void validateNotExistentByBadgeId(final Long badgeId) { throw new InvalidDomainException(ALREADY_EXIST_REWARD); } } + + public String getLabelById(final Long id) { + return getById(id).getLabel(); + } } diff --git a/src/test/java/daybyquest/post/domain/PostTest.java b/src/test/java/daybyquest/post/domain/PostTest.java index ac1d8b4..e95b499 100644 --- a/src/test/java/daybyquest/post/domain/PostTest.java +++ b/src/test/java/daybyquest/post/domain/PostTest.java @@ -2,7 +2,9 @@ import static daybyquest.support.fixture.PostFixtures.POST_1; import static daybyquest.support.util.StringUtils.문자열을_만든다; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; import daybyquest.global.error.exception.InvalidDomainException; import daybyquest.image.domain.Image; @@ -52,4 +54,67 @@ class 생성자는 { .isInstanceOf(InvalidDomainException.class); } } + + @Test + void 퀘스트_링크를_성공_처리한다() { + // given + final Post post = POST_1.생성(1L, 2L); + + // when + post.success(); + + // then + assertThat(post.getState()).isEqualTo(PostState.SUCCESS); + } + + @Test + void 퀘스트_링크를_성공_처리_시_이미_판정된_퀘스트라면_예외를_던진다() { + // given + final Post post = POST_1.생성(1L, 2L); + post.needCheck(); + + // when + assertThatThrownBy(post::success) + .isInstanceOf(InvalidDomainException.class); + } + + @Test + void 퀘스트_링크를_확인_필요_처리_한다() { + // given + final Post post = POST_1.생성(1L, 2L); + + // when + post.needCheck(); + + // then + assertThat(post.getState()).isEqualTo(PostState.NEED_CHECK); + } + + @Test + void 퀘스트_링크를_확인_필요_처리_시_이미_판정된_퀘스트라면_예외를_던진다() { + // given + final Post post = POST_1.생성(1L, 2L); + post.success(); + + // when + assertThatThrownBy(post::needCheck) + .isInstanceOf(InvalidDomainException.class); + } + + @Test + void 퀘스트가_링크_되었는지_확인한다() { + // given + final Post linkedPost = POST_1.생성(1L, 2L); + final Post notLinkedPost = POST_1.생성(1L); + + // when + final boolean linkedActual = linkedPost.isQuestLinked(); + final boolean notLinkedActual = notLinkedPost.isQuestLinked(); + + // then + assertAll(() -> { + assertThat(linkedActual).isTrue(); + assertThat(notLinkedActual).isFalse(); + }); + } } diff --git a/src/test/java/daybyquest/quest/domain/QuestsTest.java b/src/test/java/daybyquest/quest/domain/QuestsTest.java index 22f9149..c3b1d72 100644 --- a/src/test/java/daybyquest/quest/domain/QuestsTest.java +++ b/src/test/java/daybyquest/quest/domain/QuestsTest.java @@ -116,4 +116,26 @@ public class QuestsTest { assertThatThrownBy(() -> quests.validateExistentById(1L)) .isInstanceOf(NotExistQuestException.class); } + + @Test + void ID를_통해_라벨을_조회한다() { + // given + final Long questId = 1L; + final Quest quest = QUEST_1.일반_퀘스트_생성(); + QUEST_1.보상_없이_세부사항을_설정한다(quest); + given(questRepository.findById(questId)).willReturn(Optional.of(quest)); + + // when + final String label = quests.getLabelById(questId); + + // then + assertThat(label).isEqualTo(QUEST_1.label); + } + + @Test + void ID를_통한_라벨_조회_시_퀘스트가_없다면_예외를_던진다() { + // given & when & then + assertThatThrownBy(() -> quests.getLabelById(1L)) + .isInstanceOf(NotExistQuestException.class); + } } From c32350d004f1de949b13189dc6692bea5aec641f Mon Sep 17 00:00:00 2001 From: vectorch Date: Tue, 14 Nov 2023 16:24:50 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refator:=20Post=20=ED=8C=90=EC=A0=95=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=8B=9C=20=EA=B2=80=EC=A6=9D=EC=9D=84=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=95=88=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=98=AE=EA=B8=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/error/ExceptionCode.java | 1 + .../post/application/JudgePostService.java | 4 --- .../java/daybyquest/post/domain/Post.java | 11 +++++++- .../java/daybyquest/post/domain/PostTest.java | 28 ++++++++++++++++--- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/main/java/daybyquest/global/error/ExceptionCode.java b/src/main/java/daybyquest/global/error/ExceptionCode.java index c19b295..cf5282e 100644 --- a/src/main/java/daybyquest/global/error/ExceptionCode.java +++ b/src/main/java/daybyquest/global/error/ExceptionCode.java @@ -42,6 +42,7 @@ public enum ExceptionCode { INVALID_POST_IMAGE("POT-05", BAD_REQUEST, "게시물 사진은 1~5장이어야 합니다"), INVALID_POST_CONTENT("POT-06", BAD_REQUEST, "게시물 내용은 500자 이하여야 합니다"), ALREADY_JUDGED_POST("POT-07", BAD_REQUEST, "이미 판정된 게시물 입니다"), + NOT_LINKED_POST("POT-08", BAD_REQUEST, "퀘스트와 관련이 없는 게시물 입니다"), // Comment NOT_EXIST_COMMENT("COM-00", BAD_REQUEST, "존재하지 않는 댓글입니다"), diff --git a/src/main/java/daybyquest/post/application/JudgePostService.java b/src/main/java/daybyquest/post/application/JudgePostService.java index 30a7264..8b5d1ba 100644 --- a/src/main/java/daybyquest/post/application/JudgePostService.java +++ b/src/main/java/daybyquest/post/application/JudgePostService.java @@ -1,6 +1,5 @@ package daybyquest.post.application; -import daybyquest.global.error.exception.BadRequestException; import daybyquest.post.domain.Judgement; import daybyquest.post.domain.Post; import daybyquest.post.domain.Posts; @@ -25,9 +24,6 @@ public JudgePostService(final Posts posts, final ApplicationEventPublisher publi @Transactional public void invoke(final Long postId, final JudgePostRequest request) { final Post post = posts.getById(postId); - if (post.getQuestId() == null) { - throw new BadRequestException(); - } final Judgement judgement = Judgement.valueOf(request.getJudgement()); if (judgement == Judgement.SUCCESS) { post.success(); diff --git a/src/main/java/daybyquest/post/domain/Post.java b/src/main/java/daybyquest/post/domain/Post.java index 0d70d1c..d96ffdd 100644 --- a/src/main/java/daybyquest/post/domain/Post.java +++ b/src/main/java/daybyquest/post/domain/Post.java @@ -4,6 +4,7 @@ import static daybyquest.global.error.ExceptionCode.INVALID_POST_CONTENT; import static daybyquest.global.error.ExceptionCode.INVALID_POST_IMAGE; import static daybyquest.global.error.ExceptionCode.NOT_EXIST_USER; +import static daybyquest.global.error.ExceptionCode.NOT_LINKED_POST; import static daybyquest.post.domain.PostState.NEED_CHECK; import static daybyquest.post.domain.PostState.NOT_DECIDED; import static daybyquest.post.domain.PostState.SUCCESS; @@ -79,7 +80,7 @@ public Post(Long userId, Long questId, String content, List images) { validateImages(); validateContent(); if (questId == null) { - success(); + this.state = SUCCESS; } } @@ -102,6 +103,7 @@ private void validateContent() { } public void success() { + validateQuestLink(); if (state != NOT_DECIDED) { throw new InvalidDomainException(ALREADY_JUDGED_POST); } @@ -109,6 +111,7 @@ public void success() { } public void needCheck() { + validateQuestLink(); if (state != NOT_DECIDED) { throw new InvalidDomainException(ALREADY_JUDGED_POST); } @@ -118,4 +121,10 @@ public void needCheck() { public boolean isQuestLinked() { return questId != null; } + + private void validateQuestLink() { + if (!isQuestLinked()) { + throw new InvalidDomainException(NOT_LINKED_POST); + } + } } diff --git a/src/test/java/daybyquest/post/domain/PostTest.java b/src/test/java/daybyquest/post/domain/PostTest.java index e95b499..4d57e55 100644 --- a/src/test/java/daybyquest/post/domain/PostTest.java +++ b/src/test/java/daybyquest/post/domain/PostTest.java @@ -68,12 +68,22 @@ class 생성자는 { } @Test - void 퀘스트_링크를_성공_처리_시_이미_판정된_퀘스트라면_예외를_던진다() { + void 퀘스트_링크_성공_처리_시_이미_판정된_퀘스트라면_예외를_던진다() { // given final Post post = POST_1.생성(1L, 2L); post.needCheck(); - // when + // when & then + assertThatThrownBy(post::success) + .isInstanceOf(InvalidDomainException.class); + } + + @Test + void 퀘스트_링크_성공_처리_시_퀘스트와_연관이_없다면_예외를_던진다() { + // given + final Post post = POST_1.생성(1L); + + // when & then assertThatThrownBy(post::success) .isInstanceOf(InvalidDomainException.class); } @@ -91,12 +101,22 @@ class 생성자는 { } @Test - void 퀘스트_링크를_확인_필요_처리_시_이미_판정된_퀘스트라면_예외를_던진다() { + void 퀘스트_링크_확인_필요_처리_시_이미_판정된_퀘스트라면_예외를_던진다() { // given final Post post = POST_1.생성(1L, 2L); post.success(); - // when + // when & then + assertThatThrownBy(post::needCheck) + .isInstanceOf(InvalidDomainException.class); + } + + @Test + void 퀘스트_링크_확인_필요_처리_시_퀘스트와_연관이_없다면_예외를_던진다() { + // given + final Post post = POST_1.생성(1L); + + // when & then assertThatThrownBy(post::needCheck) .isInstanceOf(InvalidDomainException.class); }