Skip to content

Commit ca1c720

Browse files
jaeyeonlingwoowahan-neogracefulBrown
authored
feat: 피드백 기능 프론트엔드 구현 및 API 연동 (#1640)
* feat: ignore empty contents * feat(answer-feedback): add visible column * feat(answer-feedback): implement query answer feedback * feat(answer-feedback): return feedback response with answer * add temp controller * add temp transactional * feat(answer-feedback): retry query OpenAI on non-JSON response type * feat(feedback): remove final keyword * feat(feedback): remove temp code * fix: 셀프 체크 작성 글자 수 제한 수정 * fix: 프론트 배포 스크립트 수정 * fix: 프론트 배포 스크립트 수정 * feat: upgrade react-lottie version from 1.2.3 to ^1.2.10 * feat: upgrade node version from 16.4.0 to 16.20.2 * feat: exclude OpenAiAutoConfiguration * fix: resolve babel-loader version conflict * feat: initial new data and fix bug * feat(studylog): add feedback section to answers * refactor(studylog): hide feedback body when not feedback --------- Co-authored-by: woowahan-neo <[email protected]> Co-authored-by: gracefulBrown <[email protected]>
1 parent 2c9551d commit ca1c720

23 files changed

+6743
-6275
lines changed

backend/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ dependencies {
4141
implementation 'org.springframework.boot:spring-boot-starter-actuator'
4242
implementation 'org.springframework.ai:spring-ai-openai'
4343
implementation 'org.springframework.ai:spring-ai-azure-openai-spring-boot-starter'
44+
implementation 'org.springframework.retry:spring-retry'
4445

4546
testImplementation 'org.springframework.boot:spring-boot-starter-test'
4647

backend/src/main/java/wooteco/prolog/DataLoaderApplicationListener.java

+295-27
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package wooteco.prolog;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
import org.springframework.retry.annotation.EnableRetry;
5+
6+
@Configuration
7+
@EnableRetry
8+
public class RetryConfig {
9+
}

backend/src/main/java/wooteco/prolog/session/application/AnswerFeedbackService.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import org.slf4j.Logger;
44
import org.slf4j.LoggerFactory;
5-
import org.springframework.context.event.EventListener;
65
import org.springframework.scheduling.annotation.Async;
76
import org.springframework.stereotype.Service;
87
import org.springframework.transaction.annotation.Propagation;
@@ -15,6 +14,8 @@
1514
import wooteco.prolog.session.domain.QnaFeedbackRequest;
1615
import wooteco.prolog.session.domain.repository.AnswerFeedbackRepository;
1716

17+
import java.util.List;
18+
1819
@Service
1920
public class AnswerFeedbackService {
2021

@@ -38,6 +39,11 @@ public void handleMemberUpdatedEvent(final AnswerUpdatedEvent event) {
3839
log.debug("AnswerUpdatedEvent: {}", event);
3940

4041
final var answer = event.getAnswer();
42+
if (answer.getContent().isEmpty() || answer.getQuestion().getMission().getGoal().isEmpty()) {
43+
log.debug("Answer content or mission goal is empty: {}", answer);
44+
return;
45+
}
46+
4147
final var feedbackRequest = new QnaFeedbackRequest(
4248
answer.getQuestion().getMission().getGoal(),
4349
answer.getQuestion().getContent(),
@@ -52,5 +58,11 @@ public void handleMemberUpdatedEvent(final AnswerUpdatedEvent event) {
5258
);
5359

5460
answerFeedbackRepository.save(answerFeedback);
61+
log.debug("AnswerFeedback saved: {}", answerFeedback);
62+
}
63+
64+
@Transactional(readOnly = true)
65+
public List<AnswerFeedback> findRecentByMemberIdAndQuestionIds(final Long memberId, final List<Long> questionIds) {
66+
return answerFeedbackRepository.findRecentByMemberIdAndQuestionIdsAndVisible(memberId, questionIds);
5567
}
5668
}

backend/src/main/java/wooteco/prolog/session/domain/AnswerFeedback.java

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package wooteco.prolog.session.domain;
22

3+
import jakarta.persistence.Column;
34
import jakarta.persistence.Embedded;
45
import jakarta.persistence.Entity;
56
import jakarta.persistence.GeneratedValue;
@@ -31,10 +32,41 @@ public class AnswerFeedback {
3132
@Embedded
3233
private QnaFeedbackContents contents;
3334

34-
public AnswerFeedback(Question question, Long memberId, QnaFeedbackRequest request, QnaFeedbackContents contents) {
35+
@Column
36+
private boolean visible;
37+
38+
public AnswerFeedback(
39+
final Question question,
40+
final Long memberId,
41+
final QnaFeedbackRequest request,
42+
final QnaFeedbackContents contents
43+
) {
44+
this(question, memberId, request, contents, false);
45+
}
46+
47+
public AnswerFeedback(
48+
final Question question,
49+
final Long memberId,
50+
final QnaFeedbackRequest request,
51+
final QnaFeedbackContents contents,
52+
final boolean visible
53+
) {
3554
this.question = question;
3655
this.memberId = memberId;
3756
this.request = request;
3857
this.contents = contents;
58+
this.visible = visible;
59+
}
60+
61+
public String getStrengths() {
62+
return contents.strengths();
63+
}
64+
65+
public String getImprovementPoints() {
66+
return contents.improvementPoints();
67+
}
68+
69+
public String getAdditionalLearning() {
70+
return contents.additionalLearning();
3971
}
4072
}

backend/src/main/java/wooteco/prolog/session/domain/Question.java

+4
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@ public class Question {
2121
@ManyToOne
2222
private Mission mission;
2323

24+
public Question(final String content, final Mission mission) {
25+
this.content = content;
26+
this.mission = mission;
27+
}
2428
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
11
package wooteco.prolog.session.domain.repository;
22

33
import org.springframework.data.jpa.repository.JpaRepository;
4+
import org.springframework.data.jpa.repository.Query;
45
import wooteco.prolog.session.domain.AnswerFeedback;
56

7+
import java.util.List;
8+
69
public interface AnswerFeedbackRepository extends JpaRepository<AnswerFeedback, Long> {
710

11+
@Query("""
12+
SELECT af
13+
FROM AnswerFeedback af
14+
JOIN (
15+
SELECT MAX(id) AS id
16+
FROM AnswerFeedback
17+
WHERE memberId = :memberId AND question.id IN (:questionIds) AND visible = TRUE
18+
GROUP BY question.id
19+
) sub
20+
ON af.id = sub.id
21+
""")
22+
List<AnswerFeedback> findRecentByMemberIdAndQuestionIdsAndVisible(Long memberId, List<Long> questionIds);
823
}

backend/src/main/java/wooteco/prolog/session/infrastructure/AzureOpenAiFeedbackProvider.java

+22-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
import org.springframework.beans.factory.annotation.Value;
1313
import org.springframework.context.annotation.Profile;
1414
import org.springframework.core.io.Resource;
15+
import org.springframework.retry.annotation.Backoff;
16+
import org.springframework.retry.annotation.Recover;
17+
import org.springframework.retry.annotation.Retryable;
1518
import org.springframework.stereotype.Component;
1619
import wooteco.prolog.session.domain.QnaFeedbackContents;
1720
import wooteco.prolog.session.domain.QnaFeedbackProvider;
@@ -22,7 +25,7 @@
2225

2326
@Profile({"prod", "dev"})
2427
@Component
25-
public final class AzureOpenAiFeedbackProvider implements QnaFeedbackProvider {
28+
public class AzureOpenAiFeedbackProvider implements QnaFeedbackProvider {
2629

2730
private static final Logger log = LoggerFactory.getLogger(AzureOpenAiFeedbackProvider.class);
2831

@@ -61,6 +64,12 @@ public final class AzureOpenAiFeedbackProvider implements QnaFeedbackProvider {
6164
this.template = new PromptTemplate(resource);
6265
}
6366

67+
@Retryable(
68+
retryFor = RuntimeJsonProcessingException.class,
69+
backoff = @Backoff(delay = 1000),
70+
maxAttempts = 4,
71+
recover = "logging"
72+
)
6473
@Override
6574
public QnaFeedbackContents evaluate(final QnaFeedbackRequest request) {
6675
log.debug("Requesting feedback evaluation [request={}]", request);
@@ -88,7 +97,18 @@ public QnaFeedbackContents evaluate(final QnaFeedbackRequest request) {
8897
return objectMapper.readValue(responseText, QnaFeedbackContents.class);
8998
} catch (final JsonProcessingException e) {
9099
log.error("Failed to parse response from chat model [responseText={}]", responseText, e);
91-
throw new RuntimeException("Invalid response format from AI model", e);
100+
throw new RuntimeJsonProcessingException("Invalid response format from AI model", e);
101+
}
102+
}
103+
104+
@Recover
105+
void logging(final RuntimeException e, final QnaFeedbackRequest request) {
106+
log.error("Failed to evaluate feedback [request={}]", request, e);
107+
}
108+
109+
private static class RuntimeJsonProcessingException extends RuntimeException {
110+
public RuntimeJsonProcessingException(final String message, final Throwable cause) {
111+
super(message, cause);
92112
}
93113
}
94114
}

backend/src/main/java/wooteco/prolog/studylog/application/StudylogService.java

+29-20
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,5 @@
11
package wooteco.prolog.studylog.application;
22

3-
import static java.time.temporal.TemporalAdjusters.firstDayOfMonth;
4-
import static java.time.temporal.TemporalAdjusters.lastDayOfMonth;
5-
import static java.util.stream.Collectors.toList;
6-
import static java.util.stream.Collectors.toMap;
7-
import static wooteco.prolog.common.exception.BadRequestCode.MEMBER_NOT_ALLOWED;
8-
import static wooteco.prolog.common.exception.BadRequestCode.STUDYLOG_ARGUMENT;
9-
import static wooteco.prolog.common.exception.BadRequestCode.STUDYLOG_DOCUMENT_NOT_FOUND;
10-
import static wooteco.prolog.common.exception.BadRequestCode.STUDYLOG_NOT_FOUND;
11-
import static wooteco.prolog.common.exception.BadRequestCode.STUDYLOG_SCRAP_NOT_EXIST_EXCEPTION;
12-
13-
import java.time.LocalDate;
14-
import java.time.LocalDateTime;
15-
import java.time.LocalTime;
16-
import java.util.ArrayList;
17-
import java.util.List;
18-
import java.util.Map;
19-
import java.util.Objects;
203
import lombok.AllArgsConstructor;
214
import org.slf4j.Logger;
225
import org.slf4j.LoggerFactory;
@@ -33,10 +16,12 @@
3316
import wooteco.prolog.member.domain.Member;
3417
import wooteco.prolog.member.domain.Role;
3518
import wooteco.prolog.organization.application.OrganizationService;
19+
import wooteco.prolog.session.application.AnswerFeedbackService;
3620
import wooteco.prolog.session.application.AnswerService;
3721
import wooteco.prolog.session.application.MissionService;
3822
import wooteco.prolog.session.application.SessionService;
3923
import wooteco.prolog.session.domain.Answer;
24+
import wooteco.prolog.session.domain.AnswerFeedback;
4025
import wooteco.prolog.session.domain.AnswerTemp;
4126
import wooteco.prolog.session.domain.Mission;
4227
import wooteco.prolog.session.domain.Session;
@@ -66,6 +51,24 @@
6651
import wooteco.prolog.studylog.domain.repository.dto.CommentCount;
6752
import wooteco.prolog.studylog.event.StudylogDeleteEvent;
6853

54+
import java.time.LocalDate;
55+
import java.time.LocalDateTime;
56+
import java.time.LocalTime;
57+
import java.util.ArrayList;
58+
import java.util.List;
59+
import java.util.Map;
60+
import java.util.Objects;
61+
62+
import static java.time.temporal.TemporalAdjusters.firstDayOfMonth;
63+
import static java.time.temporal.TemporalAdjusters.lastDayOfMonth;
64+
import static java.util.stream.Collectors.toList;
65+
import static java.util.stream.Collectors.toMap;
66+
import static wooteco.prolog.common.exception.BadRequestCode.MEMBER_NOT_ALLOWED;
67+
import static wooteco.prolog.common.exception.BadRequestCode.STUDYLOG_ARGUMENT;
68+
import static wooteco.prolog.common.exception.BadRequestCode.STUDYLOG_DOCUMENT_NOT_FOUND;
69+
import static wooteco.prolog.common.exception.BadRequestCode.STUDYLOG_NOT_FOUND;
70+
import static wooteco.prolog.common.exception.BadRequestCode.STUDYLOG_SCRAP_NOT_EXIST_EXCEPTION;
71+
6972
@Service
7073
@AllArgsConstructor
7174
@Transactional(readOnly = true)
@@ -78,6 +81,7 @@ public class StudylogService {
7881
private final MemberService memberService;
7982
private final TagService tagService;
8083
private final AnswerService answerService;
84+
private final AnswerFeedbackService answerFeedbackService;
8185
private final SessionService sessionService;
8286
private final MissionService missionService;
8387
private final OrganizationService organizationService;
@@ -311,10 +315,15 @@ public StudylogResponse retrieveStudylogById(LoginMember loginMember, Long study
311315
Studylog studylog = findStudylogById(studylogId);
312316

313317
List<Answer> answers = answerService.findAnswersByStudylogId(studylog.getId());
318+
List<Long> questionIds = answers.stream()
319+
.mapToLong(it -> it.getQuestion().getId())
320+
.boxed()
321+
.toList();
322+
List<AnswerFeedback> answerFeedbacks = answerFeedbackService.findRecentByMemberIdAndQuestionIds(loginMember.getId(), questionIds);
314323

315324
onStudylogRetrieveEvent(loginMember, studylog, isViewed);
316325

317-
return toStudylogResponse(loginMember, studylog, answers);
326+
return toStudylogResponse(loginMember, studylog, answers, answerFeedbacks);
318327
}
319328

320329
@Transactional
@@ -362,14 +371,14 @@ private void onStudylogRetrieveEvent(LoginMember loginMember, Studylog studylog,
362371
}
363372
}
364373

365-
private StudylogResponse toStudylogResponse(LoginMember loginMember, Studylog studylog, List<Answer> answers) {
374+
private StudylogResponse toStudylogResponse(LoginMember loginMember, Studylog studylog, List<Answer> answers, List<AnswerFeedback> answerFeedbacks) {
366375
boolean liked = studylog.likedByMember(loginMember.getId());
367376
boolean read = studylogReadRepository.findByMemberIdAndStudylogId(loginMember.getId(), studylog.getId())
368377
.isPresent();
369378
boolean scraped = studylogScrapRepository.findByMemberIdAndStudylogId(loginMember.getId(), studylog.getId())
370379
.isPresent();
371380

372-
return StudylogResponse.of(studylog, answers, scraped, read, liked);
381+
return StudylogResponse.of(studylog, answers, answerFeedbacks, scraped, read, liked);
373382
}
374383

375384
public StudylogResponse findByIdAndReturnStudylogResponse(Long id) {
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
package wooteco.prolog.studylog.application.dto;
22

3-
import java.util.ArrayList;
4-
import java.util.List;
5-
import java.util.stream.Collectors;
63
import lombok.AllArgsConstructor;
74
import lombok.Getter;
85
import lombok.NoArgsConstructor;
6+
import org.springframework.lang.Nullable;
97
import wooteco.prolog.session.domain.Answer;
8+
import wooteco.prolog.session.domain.AnswerFeedback;
109
import wooteco.prolog.session.domain.AnswerTemp;
1110

11+
import java.util.ArrayList;
12+
import java.util.List;
13+
import java.util.Map;
14+
import java.util.stream.Collectors;
15+
1216
@AllArgsConstructor
1317
@NoArgsConstructor
1418
@Getter
@@ -18,24 +22,63 @@ public class AnswerResponse {
1822
private String answerContent;
1923
private Long questionId;
2024
private String questionContent;
25+
@Nullable
26+
private String strengths;
27+
@Nullable
28+
private String improvementPoints;
29+
@Nullable
30+
private String additionalLearning;
31+
32+
public AnswerResponse(
33+
final Long id,
34+
final String answerContent,
35+
final Long questionId,
36+
final String questionContent
37+
) {
38+
this.id = id;
39+
this.answerContent = answerContent;
40+
this.questionId = questionId;
41+
this.questionContent = questionContent;
42+
}
2143

2244
public static List<AnswerResponse> emptyListOf() {
2345
return new ArrayList<>();
2446
}
2547

2648
public static List<AnswerResponse> listOf(List<Answer> answers) {
49+
return listOf(answers, List.of());
50+
}
51+
52+
public static List<AnswerResponse> listOf(List<Answer> answers, List<AnswerFeedback> answerFeedbacks) {
53+
return listOf(answers, answerFeedbacks.stream()
54+
.collect(Collectors.toMap(answerFeedback -> answerFeedback.getQuestion().getId(), answerFeedback -> answerFeedback)));
55+
}
56+
57+
public static List<AnswerResponse> listOf(List<Answer> answers, Map<Long, AnswerFeedback> answerFeedbacks) {
2758
if (answers == null || answers.isEmpty()) {
2859
return emptyListOf();
2960
}
3061

3162
return answers.stream()
32-
.map(answer -> new AnswerResponse(answer.getId(), answer.getContent(), answer.getQuestion().getId(),
33-
answer.getQuestion().getContent()))
63+
.map(answer -> of(answer, answerFeedbacks.get(answer.getQuestion().getId())))
3464
.collect(Collectors.toList());
3565
}
3666

3767
public static AnswerResponse of(AnswerTemp answerTemp) {
3868
return new AnswerResponse(answerTemp.getId(), answerTemp.getContent(), answerTemp.getQuestion().getId(),
3969
answerTemp.getQuestion().getContent());
4070
}
71+
72+
public static AnswerResponse of(Answer answer) {
73+
return new AnswerResponse(answer.getId(), answer.getContent(), answer.getQuestion().getId(),
74+
answer.getQuestion().getContent());
75+
}
76+
77+
public static AnswerResponse of(Answer answer, AnswerFeedback answerFeedback) {
78+
if (answerFeedback == null) {
79+
return of(answer);
80+
}
81+
return new AnswerResponse(answer.getId(), answer.getContent(), answer.getQuestion().getId(),
82+
answer.getQuestion().getContent(), answerFeedback.getStrengths(), answerFeedback.getImprovementPoints(), answerFeedback.getAdditionalLearning());
83+
}
4184
}

0 commit comments

Comments
 (0)