Skip to content

Commit

Permalink
Merge pull request #122 from Team-Umbba/feat/#121-after_seven_qna
Browse files Browse the repository at this point in the history
[FEAT] 7일 이후 문답 구현
  • Loading branch information
ddongseop authored Feb 9, 2024
2 parents c3423c8 + 3183f96 commit 92a07e3
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,17 @@ public ApiResponse<Exception> handlerNullPointerException(final NullPointerExcep
* CUSTOM_ERROR
*/
@ExceptionHandler(CustomException.class)
protected ResponseEntity<ApiResponse> handleCustomException(CustomException e) {
protected ResponseEntity<ApiResponse> handleCustomException(CustomException e, final HttpServletRequest request) {

log.error("CustomException occured: {}", e.getMessage(), e);

return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(e.getErrorType(), e.getMessage()));
if (e.getHttpStatus() == 501) {
notificationService.sendExceptionToSlack(e, request);
return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(ErrorType.NEED_MORE_QUESTION));
} else {
return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(e.getErrorType(), e.getMessage()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ public ApiResponse<SingleQnAResponseDto> getSingleQna(
qnAService.getSingleQna(JwtProvider.getUserFromPrincial(principal), qnaId));
}

@PatchMapping("/qna/restart")
@ResponseStatus(HttpStatus.OK)
public ApiResponse restartQna(Principal principal) {

qnAService.restartQna(JwtProvider.getUserFromPrincial(principal));
return ApiResponse.success(SuccessType.RESTART_QNA_SUCCESS);
}

@GetMapping("/home")
@ResponseStatus(HttpStatus.OK)
public ApiResponse<GetMainViewResponseDto> home(Principal principal) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,25 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import sopt.org.umbba.api.config.sqs.producer.SqsProducer;
import sopt.org.umbba.api.controller.qna.dto.request.TodayAnswerRequestDto;
import sopt.org.umbba.api.controller.qna.dto.response.*;
import sopt.org.umbba.api.service.notification.NotificationService;
import sopt.org.umbba.common.exception.ErrorType;
import sopt.org.umbba.common.exception.model.CustomException;
import sopt.org.umbba.domain.domain.parentchild.Parentchild;
import sopt.org.umbba.domain.domain.parentchild.dao.ParentchildDao;
import sopt.org.umbba.domain.domain.qna.OnboardingAnswer;
import sopt.org.umbba.domain.domain.qna.QnA;
import sopt.org.umbba.domain.domain.qna.Question;
import sopt.org.umbba.domain.domain.qna.*;
import sopt.org.umbba.domain.domain.qna.repository.QnARepository;
import sopt.org.umbba.domain.domain.qna.repository.QuestionRepository;
import sopt.org.umbba.domain.domain.user.SocialPlatform;
import sopt.org.umbba.domain.domain.user.User;
import sopt.org.umbba.domain.domain.user.repository.UserRepository;

import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;

import static sopt.org.umbba.common.exception.ErrorType.NEED_MORE_QUESTION;
import static sopt.org.umbba.domain.domain.qna.OnboardingAnswer.NO;
import static sopt.org.umbba.domain.domain.qna.OnboardingAnswer.YES;
import static sopt.org.umbba.domain.domain.qna.QuestionSection.*;
Expand Down Expand Up @@ -163,8 +159,8 @@ public void filterFirstQuestion(Long userId) {
.build();
qnARepository.save(newQnA);

parentchild.initQnA();
parentchild.addQnA(newQnA);
parentchild.initQna();
parentchild.setQna(newQnA);
}

@Transactional
Expand All @@ -188,7 +184,7 @@ public void filterAllQuestion(Long userId) {
.isChildAnswer(false)
.build();
qnARepository.save(newQnA);
parentchild.addQnA(newQnA);
parentchild.setQna(newQnA);
}
}

Expand Down Expand Up @@ -324,13 +320,66 @@ public GetMainViewResponseDto getMainInfo(Long userId) {
log.info("getCount(): {}", parentchild.getCount());

if (parentchild.getCount() == 7 && (currentQnA.isParentAnswer() && currentQnA.isChildAnswer())) {
return GetMainViewResponseDto.of(currentQnA, parentchild.getCount()+1); // 유효하지 않은 8로 반환 시 엔딩이벤트
return GetMainViewResponseDto.of(currentQnA, -1); // 유효하지 않은 -1로 반환 시 엔딩이벤트
}


return GetMainViewResponseDto.of(currentQnA, parentchild.getCount());
}

@Transactional
public void restartQna(Long userId) {
Parentchild parentchild = getParentchild(userId);

if (parentchild.getCount() == 8) {
// 상대측이 이미 답변 이어가기를 호출했다면 실행할 필요 X
return;
}

List<QnA> qnaList = getQnAListByParentchild(parentchild);

// 1. 메인 타입과 미사용 타입에 대해서 불러오기
List<QuestionType> types = Arrays.asList(MAIN, YET);

// 2. 내가 이미 주고받은 질문 제외하기
List<Long> doneQuestionIds = qnaList.stream()
.map(qna -> qna.getQuestion().getId())
.collect(Collectors.toList());

// 5. 이 경우 아예 추가될 질문이 없으므로 예외 발생시킴
List<Question> targetQuestions = questionRepository.findByTypeInAndIdNotIn(types, doneQuestionIds);
if (targetQuestions.isEmpty()) {
throw new CustomException(NEED_MORE_QUESTION);
}

QuestionSection section = qnaList.get(parentchild.getCount() - 1).getQuestion().getSection();
List<Question> differentSectionQuestions = targetQuestions.stream()
.filter(question -> !question.getSection().equals(section))
.collect(Collectors.toList());

Random random = new Random();
Question randomQuestion;
if (!differentSectionQuestions.isEmpty()) {
// 3. 최근에 주고받은 질문의 section과 다른 질문들 중에서 랜덤하게 추출
randomQuestion = differentSectionQuestions.get(random.nextInt(differentSectionQuestions.size()));
} else {
// 4. 없다면 동일한 section의 질문 중에서라도 랜덤하게 추출
List<Question> equalSectionQuestions = targetQuestions.stream()
.filter(question -> !question.getSection().equals(section))
.collect(Collectors.toList());
randomQuestion = equalSectionQuestions.get(random.nextInt(equalSectionQuestions.size()));
}

// 새로운 질문 추가!
QnA newQnA = QnA.builder()
.question(randomQuestion)
.isParentAnswer(false)
.isChildAnswer(false)
.build();
qnARepository.save(newQnA);
parentchild.addQna(newQnA);
parentchild.addCount();
}

@NotNull
private Parentchild getParentchild(Long userId) {
Parentchild parentchild = getUserById(userId).getParentChild();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ public enum ErrorType {
PARENTCHILD_HAVE_NO_OPPONENT(HttpStatus.NOT_FOUND, "부모자식 관계에 1명만 참여하고 있습니다."),
NOT_FOUND_SECTION(HttpStatus.NOT_FOUND, "해당 아이디와 일치하는 섹션이 없습니다."),


/**
* About Apple (HttpStatus 고민)
*/
Expand All @@ -82,7 +81,6 @@ public enum ErrorType {
FIREBASE_CONNECTION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파이어베이스 서버와의 연결에 실패했습니다."),
FAIL_TO_SEND_PUSH_ALARM(HttpStatus.INTERNAL_SERVER_ERROR, "푸시 알림 메세지 전송에 실패했습니다."),


// ETC
INDEX_OUT_OF_BOUNDS(HttpStatus.INTERNAL_SERVER_ERROR, "인덱스 범위를 초과했습니다."),
JWT_SERIALIZE(HttpStatus.INTERNAL_SERVER_ERROR, "JWT 라이브러리 직렬화에 실패했습니다."),
Expand All @@ -93,7 +91,10 @@ public enum ErrorType {
DATA_INTEGRITY_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "데이터 무결성 제약조건을 위반했습니다."),
NULL_POINTER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "NULL 포인터를 참조했습니다."),


/**
* 501 NOT_IMPLEMENTED
*/
NEED_MORE_QUESTION(HttpStatus.NOT_IMPLEMENTED, "남은 질문이 없습니다. 질문을 추가해주세요."),

;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public enum SuccessType {
PUSH_ALARM_PERIODIC_SUCCESS(HttpStatus.OK, "오늘의 질문 푸시알림 활성에 성공했습니다."),
REMIND_QUESTION_SUCCESS(HttpStatus.OK, "상대방에게 질문을 리마인드 하는 데 성공했습니다."),
TEST_SUCCESS(HttpStatus.OK, "데모데이 테스트용 API 호출에 성공했습니다."),
RESTART_QNA_SUCCESS(HttpStatus.OK, "7일 이후 문답이 정상적으로 시작되었습니다."),


/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,8 @@ public class Parentchild extends AuditingTimeEntity {
private int count;

public void addCount() {
if (this.count < 7) {
this.count += 1;
log.info("Parentchild - addCount() 호출: {}", this.count);
}
this.count += 1;
log.info("Parentchild - addCount() 호출: {}", this.count);
// 미답변 일수 필드 0으로 초기화
this.remindCnt = 0;
}
Expand Down Expand Up @@ -87,15 +85,19 @@ public void changeParentOnboardingAnswerList(List<OnboardingAnswer> onboardingAn

private boolean deleted = Boolean.FALSE;

public void initQnA() {
public void initQna() {
qnaList = new ArrayList<>();
}

public void addQnA(QnA qnA) {
public void setQna(QnA qnA) {
if (qnaList.size() >= 7) {
throw new CustomException(ErrorType.ALREADY_QNA_LIST_FULL);
}
qnaList.add(qnA);
}

public void addQna(QnA qnA) {
qnaList.add(qnA);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public interface QuestionRepository extends Repository<Question, Long> {

List<Question> findByType(QuestionType type);

List<Question> findByTypeInAndIdNotIn(List<QuestionType> types, List<Long> doneQuestionIds);

default List<Question> findBySectionAndTypeRandom(QuestionSection section, QuestionType type, int size) {
List<Question> matchingQuestions = findBySectionAndType(section, type);
List<Question> selectedQuestions = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sopt.org.umbba.common.exception.SuccessType;
import sopt.org.umbba.common.exception.dto.ApiResponse;
import sopt.org.umbba.common.sqs.dto.FCMPushRequestDto;
import sopt.org.umbba.notification.service.fcm.FCMService;
import sopt.org.umbba.notification.config.ScheduleConfig;
import sopt.org.umbba.notification.service.scheduler.FCMScheduler;

import java.io.IOException;
Expand All @@ -28,6 +28,7 @@ public class FCMController {
@PostMapping("/qna")
@ResponseStatus(HttpStatus.OK)
public ApiResponse sendTopicScheduledTest() {
ScheduleConfig.resetScheduler();
return ApiResponse.success(SuccessType.PUSH_ALARM_PERIODIC_SUCCESS, fcmScheduler.pushTodayQna());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@
import org.springframework.transaction.support.DefaultTransactionDefinition;
import sopt.org.umbba.common.exception.ErrorType;
import sopt.org.umbba.common.exception.model.CustomException;
import sopt.org.umbba.common.sqs.dto.PushMessage;
import sopt.org.umbba.domain.domain.parentchild.Parentchild;
import sopt.org.umbba.domain.domain.parentchild.dao.ParentchildDao;
import sopt.org.umbba.domain.domain.parentchild.repository.ParentchildRepository;
import sopt.org.umbba.domain.domain.qna.QnA;
import sopt.org.umbba.domain.domain.qna.Question;
import sopt.org.umbba.domain.domain.qna.QuestionSection;
import sopt.org.umbba.domain.domain.qna.QuestionType;
import sopt.org.umbba.domain.domain.qna.repository.QnARepository;
import sopt.org.umbba.domain.domain.qna.repository.QuestionRepository;
import sopt.org.umbba.domain.domain.user.SocialPlatform;
import sopt.org.umbba.domain.domain.user.User;
import sopt.org.umbba.domain.domain.user.repository.UserRepository;
Expand All @@ -38,7 +42,13 @@
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ScheduledFuture;
import java.util.stream.Collectors;

import static sopt.org.umbba.common.exception.ErrorType.NEED_MORE_QUESTION;
import static sopt.org.umbba.domain.domain.qna.QuestionType.MAIN;
import static sopt.org.umbba.domain.domain.qna.QuestionType.YET;

/**
* 서버에서 파이어베이스로 전송이 잘 이루어지는지 테스트하기 위한 컨트롤러
Expand All @@ -59,6 +69,8 @@ public class FCMService {

private final UserRepository userRepository;
private final ParentchildRepository parentchildRepository;
private final QnARepository qnARepository;
private final QuestionRepository questionRepository;
private final ParentchildDao parentchildDao;
private final ObjectMapper objectMapper;
private final TaskScheduler taskScheduler;
Expand Down Expand Up @@ -184,7 +196,6 @@ public void schedulePushAlarm(String cronExpression, Long parentchildId) {

scheduledFuture = taskScheduler.schedule(() -> {


try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Expand Down Expand Up @@ -247,7 +258,12 @@ public void schedulePushAlarm(String cronExpression, Long parentchildId) {
}

// 부모와 자식 모두 답변한 경우
else if (currentQnA.isParentAnswer() && currentQnA.isChildAnswer() && parentchild.getCount() < 7) {
else if (currentQnA.isParentAnswer() && currentQnA.isChildAnswer() && parentchild.getCount() != 7) {

// 8일 이후 (7일 + 엔딩페이지 API 통신으로 추가된 1일) 에는 스케줄링을 돌며 QnA 직접 추가
if (parentchild.getCount() >= 8) {
appendQna(parentchild);
}

log.info("둘 다 답변함 다음 질문으로 ㄱ {}", parentchild.getCount());
parentchild.addCount(); // 오늘의 질문 UP & 리마인드 카운트 초기화
Expand All @@ -269,8 +285,6 @@ else if (currentQnA.isParentAnswer() && currentQnA.isChildAnswer() && parentchil
todayQnA.getQuestion().getTopic()), parentchild.getId());
}
}


}
transactionManager.commit(transactionStatus);
} catch (PessimisticLockingFailureException | PessimisticLockException e) {
Expand All @@ -294,7 +308,60 @@ public static void clearScheduledTasks() {
log.info("ScheduledFuture: {}", scheduledFuture);
}

private void appendQna(Parentchild parentchild) {
List<QnA> qnaList = getQnAListByParentchild(parentchild);

// 1. 메인 타입과 미사용 타입에 대해서 불러오기
List<QuestionType> types = Arrays.asList(MAIN, YET);

// 2. 내가 이미 주고받은 질문 제외하기
List<Long> doneQuestionIds = qnaList.stream()
.map(qna -> qna.getQuestion().getId())
.collect(Collectors.toList());

// 5. 이 경우 아예 추가될 질문이 없으므로 예외 발생시킴
List<Question> targetQuestions = questionRepository.findByTypeInAndIdNotIn(types, doneQuestionIds);
if (targetQuestions.isEmpty()) {
// 충실한 유저가 추가될 수 있는 질문을 모두 수행했을 경우, 기획 측에서 알 수 있도록 500 에러로 처리
throw new CustomException(NEED_MORE_QUESTION);
}

QuestionSection section = qnaList.get(parentchild.getCount() - 1).getQuestion().getSection();
List<Question> differentSectionQuestions = targetQuestions.stream()
.filter(question -> !question.getSection().equals(section))
.collect(Collectors.toList());

Random random = new Random();
Question randomQuestion;
if (!differentSectionQuestions.isEmpty()) {
// 3. 최근에 주고받은 질문의 section과 다른 질문들 중에서 랜덤하게 추출
randomQuestion = differentSectionQuestions.get(random.nextInt(differentSectionQuestions.size()));
} else {
// 4. 없다면 동일한 section의 질문 중에서라도 랜덤하게 추출
List<Question> equalSectionQuestions = targetQuestions.stream()
.filter(question -> !question.getSection().equals(section))
.collect(Collectors.toList());
randomQuestion = equalSectionQuestions.get(random.nextInt(equalSectionQuestions.size()));
}

QnA newQnA = QnA.builder()
.question(randomQuestion)
.isParentAnswer(false)
.isChildAnswer(false)
.build();

qnARepository.save(newQnA);
parentchild.addQna(newQnA);
}

private List<QnA> getQnAListByParentchild(Parentchild parentchild) {
List<QnA> qnaList = parentchild.getQnaList();
if (qnaList == null || qnaList.isEmpty()) {
throw new CustomException(ErrorType.PARENTCHILD_HAVE_NO_QNALIST);
}

return qnaList;
}

/**
* 사용 안하는 함수들
Expand Down

0 comments on commit 92a07e3

Please sign in to comment.