Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] AI 서버와의 연동 로직을 작성한다 #106

Merged
merged 5 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/java/daybyquest/global/error/ExceptionCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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, "존재하지 않는 댓글입니다"),
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/daybyquest/post/application/PostClient.java
Original file line number Diff line number Diff line change
@@ -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<String> identifiers);
}
21 changes: 18 additions & 3 deletions src/main/java/daybyquest/post/application/SavePostService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<MultipartFile> 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<Image> toImageList(final List<MultipartFile> files) {
Expand All @@ -47,4 +55,11 @@ private List<Image> toImageList(final List<MultipartFile> files) {
private Post toEntity(final Long loginId, final SavePostRequest request, final List<Image> 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());
}
}
}
15 changes: 14 additions & 1 deletion src/main/java/daybyquest/post/domain/Post.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -79,7 +80,7 @@ public Post(Long userId, Long questId, String content, List<Image> images) {
validateImages();
validateContent();
if (questId == null) {
success();
this.state = SUCCESS;
}
}

Expand All @@ -102,16 +103,28 @@ private void validateContent() {
}

public void success() {
validateQuestLink();
if (state != NOT_DECIDED) {
throw new InvalidDomainException(ALREADY_JUDGED_POST);
}
state = SUCCESS;
}

public void needCheck() {
validateQuestLink();
if (state != NOT_DECIDED) {
throw new InvalidDomainException(ALREADY_JUDGED_POST);
}
state = NEED_CHECK;
}

public boolean isQuestLinked() {
return questId != null;
}

private void validateQuestLink() {
if (!isQuestLinked()) {
throw new InvalidDomainException(NOT_LINKED_POST);
}
}
}
4 changes: 2 additions & 2 deletions src/main/java/daybyquest/post/domain/Posts.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package daybyquest.post.dto.request;

import java.util.List;

public record PostJudgeRequest(String label, List<String> imageIdentifiers) {

}
15 changes: 15 additions & 0 deletions src/main/java/daybyquest/post/infra/PostStubClient.java
Original file line number Diff line number Diff line change
@@ -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<String> identifiers) {
}
}
38 changes: 38 additions & 0 deletions src/main/java/daybyquest/post/infra/PostWebClient.java
Original file line number Diff line number Diff line change
@@ -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<String> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
78 changes: 78 additions & 0 deletions src/main/java/daybyquest/quest/application/QuestSseEmitters.java
Original file line number Diff line number Diff line change
@@ -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<Long, SseEmitter> emitters;

private final Map<Long, QuestLabelsResponse> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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());
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading