Skip to content

Commit ecb2a9a

Browse files
authored
Merge pull request #236 from DevKor-github/develop
#235까지 반영해 배포
2 parents 0bd4f5c + 17fe26a commit ecb2a9a

46 files changed

Lines changed: 4398 additions & 51 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/DailyReportService.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,14 @@ public CreateDailyReportResponse generateDailyReport(Long userId, DailyReportReq
8888
throw new BadRequestException(ErrorCode.DAILY_QUESTION_MISMATCH);
8989
}
9090

91-
PrepareDailyResultDto prep = dailyReportTxService.prepareDaily(user, question, request.answer(), isDayPassed, request.objectKey());
91+
PrepareDailyResultDto prep = dailyReportTxService.prepareDaily(
92+
user,
93+
question,
94+
udq,
95+
request.answer(),
96+
isDayPassed,
97+
request.objectKey()
98+
);
9299

93100
AnswerEntry answerEntry = prep.entry();
94101
ModelCandidate modelCandidate = dailyReportModelSelector.select();
@@ -104,7 +111,7 @@ public CreateDailyReportResponse generateDailyReport(Long userId, DailyReportReq
104111
AiDailyReportResultDto dto;
105112
try {
106113
LlmGenerationResult<AiDailyReportResultDto> generationResult =
107-
dailyReportLlmClient.generate(question.getQuestionText(), answerEntry, modelCandidate);
114+
dailyReportLlmClient.generate(prep.questionText(), answerEntry, modelCandidate);
108115
dto = generationResult.content();
109116
LlmTokenUsage tokenUsage = generationResult.tokenUsage();
110117
reportGenerationLogRecorder.recordTokenUsage(

src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/DailyReportTxService.java

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
import com.devkor.ifive.nadab.domain.dailyreport.core.service.AnswerEntryService;
1414
import com.devkor.ifive.nadab.domain.dailyreport.core.service.PendingDailyReportService;
1515
import com.devkor.ifive.nadab.domain.question.core.entity.DailyQuestion;
16+
import com.devkor.ifive.nadab.domain.question.core.entity.DailyQuestionRevision;
17+
import com.devkor.ifive.nadab.domain.question.core.entity.UserDailyQuestion;
18+
import com.devkor.ifive.nadab.domain.question.core.service.DailyQuestionExposureService;
1619
import com.devkor.ifive.nadab.domain.user.core.entity.Interest;
1720
import com.devkor.ifive.nadab.domain.user.core.entity.InterestCode;
1821
import com.devkor.ifive.nadab.domain.user.core.entity.User;
@@ -45,20 +48,42 @@ public class DailyReportTxService {
4548

4649
private final AnswerEntryService answerEntryService;
4750
private final PendingDailyReportService pendingDailyReportService;
51+
private final DailyQuestionExposureService dailyQuestionExposureService;
4852

4953

5054
private static final long DAILY_REPORT_REWARD = 10L;
5155

52-
protected PrepareDailyResultDto prepareDaily(User user, DailyQuestion dq, String answerText, boolean isDayPassed,
53-
@Nullable String imageKey) {
56+
protected PrepareDailyResultDto prepareDaily(
57+
User user,
58+
DailyQuestion dq,
59+
UserDailyQuestion assignment,
60+
String answerText,
61+
boolean isDayPassed,
62+
@Nullable String imageKey
63+
) {
64+
65+
DailyQuestionRevision questionRevision = dailyQuestionExposureService
66+
.recordAnswer(assignment)
67+
.orElse(null);
5468

5569
// AnswerEntry 생성 또는 조회 (별도의 트랜잭션)
56-
AnswerEntry entry = answerEntryService.getOrCreateTodayAnswerEntry(user, dq, answerText, isDayPassed, imageKey);
70+
AnswerEntry entry = answerEntryService.getOrCreateTodayAnswerEntry(
71+
user,
72+
dq,
73+
questionRevision,
74+
answerText,
75+
isDayPassed,
76+
imageKey
77+
);
5778

5879
// DailyReport PENDING 생성 또는 조회 (별도의 트랜잭션)
5980
DailyReport report = pendingDailyReportService.getOrCreatePendingDailyReport(entry, isDayPassed);
6081

61-
return new PrepareDailyResultDto(entry, report.getId(), user.getId());
82+
String questionText = entry.getQuestionRevision() != null
83+
? entry.getQuestionRevision().getQuestionText()
84+
: dq.getQuestionText();
85+
86+
return new PrepareDailyResultDto(entry, report.getId(), user.getId(), questionText);
6287
}
6388

6489
protected ConfirmDailyAndRewardDto confirmDailyAndReward(

src/main/java/com/devkor/ifive/nadab/domain/dailyreport/core/dto/PrepareDailyResultDto.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
public record PrepareDailyResultDto(
66
AnswerEntry entry,
77
Long reportId,
8-
Long userId
8+
Long userId,
9+
String questionText
910
) {
1011
}

src/main/java/com/devkor/ifive/nadab/domain/dailyreport/core/entity/AnswerEntry.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.devkor.ifive.nadab.domain.dailyreport.core.entity;
22

33
import com.devkor.ifive.nadab.domain.question.core.entity.DailyQuestion;
4+
import com.devkor.ifive.nadab.domain.question.core.entity.DailyQuestionRevision;
45
import com.devkor.ifive.nadab.domain.user.core.entity.User;
56
import com.devkor.ifive.nadab.global.shared.entity.AuditableEntity;
67
import jakarta.persistence.*;
@@ -34,6 +35,10 @@ public class AnswerEntry extends AuditableEntity {
3435
@JoinColumn(name = "question_id", nullable = false)
3536
private DailyQuestion question;
3637

38+
@ManyToOne(fetch = FetchType.LAZY)
39+
@JoinColumn(name = "question_revision_id")
40+
private DailyQuestionRevision questionRevision;
41+
3742
@Column(name = "content", length = 500, nullable = false)
3843
private String content;
3944

@@ -45,9 +50,15 @@ public class AnswerEntry extends AuditableEntity {
4550

4651
public static AnswerEntry create(User user, DailyQuestion question, String content, LocalDate date,
4752
@Nullable String imageKey) {
53+
return create(user, question, null, content, date, imageKey);
54+
}
55+
56+
public static AnswerEntry create(User user, DailyQuestion question, @Nullable DailyQuestionRevision questionRevision,
57+
String content, LocalDate date, @Nullable String imageKey) {
4858
AnswerEntry e = new AnswerEntry();
4959
e.user = user;
5060
e.question = question;
61+
e.questionRevision = questionRevision;
5162
e.content = content;
5263
e.date = date;
5364
e.imageKey = imageKey;
@@ -64,4 +75,4 @@ public void updateImageKey(String imageKey) {
6475
this.imageKey = imageKey;
6576
onUpdate();
6677
}
67-
}
78+
}

src/main/java/com/devkor/ifive/nadab/domain/dailyreport/core/service/AnswerEntryService.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.devkor.ifive.nadab.domain.dailyreport.core.entity.AnswerEntry;
44
import com.devkor.ifive.nadab.domain.dailyreport.core.repository.AnswerEntryRepository;
55
import com.devkor.ifive.nadab.domain.question.core.entity.DailyQuestion;
6+
import com.devkor.ifive.nadab.domain.question.core.entity.DailyQuestionRevision;
67
import com.devkor.ifive.nadab.domain.user.core.entity.User;
78
import com.devkor.ifive.nadab.global.shared.util.TodayDateTimeProvider;
89
import lombok.RequiredArgsConstructor;
@@ -22,8 +23,14 @@ public class AnswerEntryService {
2223

2324

2425
@Transactional
25-
public AnswerEntry getOrCreateTodayAnswerEntry(User user, DailyQuestion dq, String answerText, boolean isDayPassed,
26-
@Nullable String imageKey) {
26+
public AnswerEntry getOrCreateTodayAnswerEntry(
27+
User user,
28+
DailyQuestion dq,
29+
@Nullable DailyQuestionRevision questionRevision,
30+
String answerText,
31+
boolean isDayPassed,
32+
@Nullable String imageKey
33+
) {
2734

2835
LocalDate targetDate =
2936
isDayPassed ? TodayDateTimeProvider.getTodayDate().minusDays(1) : TodayDateTimeProvider.getTodayDate();
@@ -35,7 +42,14 @@ public AnswerEntry getOrCreateTodayAnswerEntry(User user, DailyQuestion dq, Stri
3542
})
3643
.orElseGet(() -> {
3744
try {
38-
return answerEntryRepository.save(AnswerEntry.create(user, dq, answerText, targetDate, imageKey));
45+
return answerEntryRepository.save(AnswerEntry.create(
46+
user,
47+
dq,
48+
questionRevision,
49+
answerText,
50+
targetDate,
51+
imageKey
52+
));
3953
} catch (DataIntegrityViolationException e) {
4054
// 동시 요청에서 이미 누가 만들었을 수 있음 -> 재조회로 멱등 처리
4155
return answerEntryRepository.findByUserAndDate(user, targetDate)

src/main/java/com/devkor/ifive/nadab/domain/question/application/QuestionCommandService.java

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
import com.devkor.ifive.nadab.domain.question.application.helper.DailyQuestionSelector;
55
import com.devkor.ifive.nadab.domain.question.application.helper.QuestionLevelPolicy;
66
import com.devkor.ifive.nadab.domain.question.core.entity.DailyQuestion;
7+
import com.devkor.ifive.nadab.domain.question.core.entity.DailyQuestionRevision;
78
import com.devkor.ifive.nadab.domain.question.core.entity.UserDailyQuestion;
89
import com.devkor.ifive.nadab.domain.question.core.repository.UserDailyQuestionRepository;
10+
import com.devkor.ifive.nadab.domain.question.core.service.DailyQuestionExposureService;
911
import com.devkor.ifive.nadab.domain.dailyreport.core.repository.AnswerEntryRepository;
1012
import com.devkor.ifive.nadab.domain.user.core.entity.User;
1113
import com.devkor.ifive.nadab.domain.user.core.repository.UserInterestRepository;
@@ -33,6 +35,7 @@ public class QuestionCommandService {
3335

3436
private final QuestionLevelPolicy questionLevelPolicy;
3537
private final DailyQuestionSelector dailyQuestionSelector;
38+
private final DailyQuestionExposureService dailyQuestionExposureService;
3639

3740
public DailyQuestionResponse getOrCreateTodayQuestion(Long userId) {
3841

@@ -44,16 +47,21 @@ public DailyQuestionResponse getOrCreateTodayQuestion(Long userId) {
4447
.orElseGet(() -> this.createTodayQuestion(userId, today));
4548

4649
DailyQuestion question = udq.getDailyQuestion();
50+
DailyQuestionRevision revision = dailyQuestionExposureService
51+
.findLatestRevision(udq)
52+
.orElse(null);
4753

4854
boolean answered = answerEntryRepository.existsActiveAnswer(userId, question.getId());
4955

5056
return new DailyQuestionResponse(
5157
question.getId(),
52-
question.getInterest().getCode().toString(),
53-
question.getQuestionText(),
54-
question.getEmpathyGuide(),
55-
question.getHintGuide(),
56-
question.getLeadingQuestionGuide(),
58+
revision != null
59+
? revision.getInterest().getCode().toString()
60+
: question.getInterest().getCode().toString(),
61+
revision != null ? revision.getQuestionText() : question.getQuestionText(),
62+
revision != null ? revision.getEmpathyGuide() : question.getEmpathyGuide(),
63+
revision != null ? revision.getHintGuide() : question.getHintGuide(),
64+
revision != null ? revision.getLeadingQuestionGuide() : question.getLeadingQuestionGuide(),
5765
answered,
5866
udq.isRerollUsed()
5967
);
@@ -68,25 +76,28 @@ public DailyQuestionResponse getOrCreateTodayQuestion(Long userId) {
6876
public UserDailyQuestion createTodayQuestion(Long userId, LocalDate todayKst) {
6977
// 동시성: 여러 요청이 동시에 들어오면 UNIQUE(user_id, date)로 한 번만 성공해야 함
7078
// -> insert 시도 후 unique 위반이면 다시 조회해서 반환
71-
try {
72-
User user = userRepository.findById(userId)
73-
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));
79+
User user = userRepository.findById(userId)
80+
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));
7481

75-
Long userInterestId = userInterestRepository.findInterestIdByUserId(userId)
76-
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_INTEREST_NOT_FOUND));
82+
Long userInterestId = userInterestRepository.findInterestIdByUserId(userId)
83+
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_INTEREST_NOT_FOUND));
7784

78-
Integer levelOnly = questionLevelPolicy.levelOnlyFor(user, OffsetDateTime.now());
85+
Integer levelOnly = questionLevelPolicy.levelOnlyFor(user, OffsetDateTime.now());
7986

80-
DailyQuestion picked = dailyQuestionSelector.pickFirst(user.getId(), userInterestId, levelOnly);
87+
DailyQuestion picked = dailyQuestionSelector.pickFirst(user.getId(), userInterestId, levelOnly);
8188

89+
UserDailyQuestion saved;
90+
try {
8291
UserDailyQuestion udq = UserDailyQuestion.create(user, todayKst, picked);
83-
return userDailyQuestionRepository.save(udq);
84-
92+
saved = userDailyQuestionRepository.save(udq);
8593
} catch (DataIntegrityViolationException e) {
8694
// 이미 생성됨(경합 상황)
8795
return userDailyQuestionRepository.findByUserIdAndDate(userId, todayKst)
8896
.orElseThrow(() -> e);
8997
}
98+
99+
dailyQuestionExposureService.recordInitialAssignment(saved);
100+
return saved;
90101
}
91102

92103
/**
@@ -126,6 +137,7 @@ public DailyQuestionResponse rerollTodayQuestion(Long userId) {
126137
);
127138

128139
udq.rerollTo(newQ);
140+
dailyQuestionExposureService.recordReroll(udq, newQ);
129141

130142
return new DailyQuestionResponse(
131143
newQ.getId(),

src/main/java/com/devkor/ifive/nadab/domain/question/application/QuestionCommandServiceV2.java

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import com.devkor.ifive.nadab.domain.question.application.helper.DailyQuestionSelector;
66
import com.devkor.ifive.nadab.domain.question.application.helper.QuestionLevelPolicy;
77
import com.devkor.ifive.nadab.domain.question.core.entity.DailyQuestion;
8+
import com.devkor.ifive.nadab.domain.question.core.entity.DailyQuestionRevision;
89
import com.devkor.ifive.nadab.domain.question.core.entity.UserDailyQuestion;
910
import com.devkor.ifive.nadab.domain.question.core.repository.UserDailyQuestionRepository;
11+
import com.devkor.ifive.nadab.domain.question.core.service.DailyQuestionExposureService;
1012
import com.devkor.ifive.nadab.domain.user.core.entity.User;
1113
import com.devkor.ifive.nadab.domain.user.core.repository.UserInterestRepository;
1214
import com.devkor.ifive.nadab.domain.user.core.repository.UserRepository;
@@ -33,6 +35,7 @@ public class QuestionCommandServiceV2 {
3335

3436
private final QuestionLevelPolicy questionLevelPolicy;
3537
private final DailyQuestionSelector dailyQuestionSelector;
38+
private final DailyQuestionExposureService dailyQuestionExposureService;
3639

3740
public DailyQuestionResponseV2 getOrCreateTodayQuestion(Long userId) {
3841

@@ -44,16 +47,21 @@ public DailyQuestionResponseV2 getOrCreateTodayQuestion(Long userId) {
4447
.orElseGet(() -> this.createTodayQuestion(userId, today));
4548

4649
DailyQuestion question = udq.getDailyQuestion();
50+
DailyQuestionRevision revision = dailyQuestionExposureService
51+
.findLatestRevision(udq)
52+
.orElse(null);
4753

4854
boolean answered = answerEntryRepository.existsActiveAnswer(userId, question.getId());
4955

5056
return new DailyQuestionResponseV2(
5157
question.getId(),
52-
question.getInterest().getCode().toString(),
53-
question.getQuestionText(),
54-
question.getEmpathyGuide(),
55-
question.getHintGuide(),
56-
question.getLeadingQuestionGuide(),
58+
revision != null
59+
? revision.getInterest().getCode().toString()
60+
: question.getInterest().getCode().toString(),
61+
revision != null ? revision.getQuestionText() : question.getQuestionText(),
62+
revision != null ? revision.getEmpathyGuide() : question.getEmpathyGuide(),
63+
revision != null ? revision.getHintGuide() : question.getHintGuide(),
64+
revision != null ? revision.getLeadingQuestionGuide() : question.getLeadingQuestionGuide(),
5765
answered,
5866
udq.getRerollLeft()
5967
);
@@ -62,27 +70,30 @@ public DailyQuestionResponseV2 getOrCreateTodayQuestion(Long userId) {
6270
public UserDailyQuestion createTodayQuestion(Long userId, LocalDate todayKst) {
6371
// 동시성: 여러 요청이 동시에 들어오면 UNIQUE(user_id, date)로 한 번만 성공해야 함
6472
// -> insert 시도 후 unique 위반이면 다시 조회해서 반환
65-
try {
66-
User user = userRepository.findById(userId)
67-
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));
73+
User user = userRepository.findById(userId)
74+
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));
6875

69-
Long userInterestId = userInterestRepository.findInterestIdByUserId(userId)
70-
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_INTEREST_NOT_FOUND));
76+
Long userInterestId = userInterestRepository.findInterestIdByUserId(userId)
77+
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_INTEREST_NOT_FOUND));
7178

72-
boolean isFirstQuestion = !(userDailyQuestionRepository.existsByUserId(userId));
79+
boolean isFirstQuestion = !(userDailyQuestionRepository.existsByUserId(userId));
7380

74-
Integer levelOnly = questionLevelPolicy.levelOnlyForFirstTime(isFirstQuestion);
81+
Integer levelOnly = questionLevelPolicy.levelOnlyForFirstTime(isFirstQuestion);
7582

76-
DailyQuestion picked = dailyQuestionSelector.pickFirst(user.getId(), userInterestId, levelOnly);
83+
DailyQuestion picked = dailyQuestionSelector.pickFirst(user.getId(), userInterestId, levelOnly);
7784

85+
UserDailyQuestion saved;
86+
try {
7887
UserDailyQuestion udq = UserDailyQuestion.create(user, todayKst, picked);
79-
return userDailyQuestionRepository.save(udq);
80-
88+
saved = userDailyQuestionRepository.save(udq);
8189
} catch (DataIntegrityViolationException e) {
8290
// 이미 생성됨(경합 상황)
8391
return userDailyQuestionRepository.findByUserIdAndDate(userId, todayKst)
8492
.orElseThrow(() -> e);
8593
}
94+
95+
dailyQuestionExposureService.recordInitialAssignment(saved);
96+
return saved;
8697
}
8798

8899
public DailyQuestionResponseV2 rerollTodayQuestion(Long userId) {
@@ -113,6 +124,7 @@ public DailyQuestionResponseV2 rerollTodayQuestion(Long userId) {
113124
);
114125

115126
udq.rerollTo(newQ);
127+
dailyQuestionExposureService.recordReroll(udq, newQ);
116128

117129
return new DailyQuestionResponseV2(
118130
newQ.getId(),

src/main/java/com/devkor/ifive/nadab/domain/question/core/entity/DailyQuestion.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,7 @@ public class DailyQuestion extends SoftDeletableEntity {
3333

3434
@Column(name = "leading_question_guide", length = 100)
3535
private String leadingQuestionGuide;
36+
37+
@Column(name = "current_revision_no", nullable = false)
38+
private int currentRevisionNo;
3639
}

0 commit comments

Comments
 (0)