@@ -169,25 +169,27 @@ public InterestsResponse getInterests(Long userId, int months) {
169169
170170 @ Transactional
171171 public QuestResponse getQuests (Long userId ) {
172- LocalDate today = LocalDate .now ();
173- LocalDateTime todayStart = today .atStartOfDay ();
174- LocalDateTime todayEnd = today .atTime (23 , 59 , 59 );
172+ LocalDate today = LocalDate .now ();
173+ LocalDateTime todayStart = today .atStartOfDay ();
174+ // 반열린 구간 [todayStart, tomorrowStart) — datetime(6) microsecond 누락 방지
175+ LocalDateTime tomorrowStart = today .plusDays (1 ).atStartOfDay ();
175176
176- List <WateringLog > todayLogs = wateringLogRepository .findByUserIdAndWateredAtBetween (userId , todayStart , todayEnd );
177+ List <WateringLog > todayLogs = wateringLogRepository
178+ .findByUserIdAndWateredAtGreaterThanEqualAndWateredAtLessThan (userId , todayStart , tomorrowStart );
177179
178180 // Q1: 오늘 TIL >= 1개
179181 boolean q1 = !todayLogs .isEmpty ();
180182
181183 // Q2: 오늘 TIL에 태그 >= 1개
182- long todayTagCount = tilTagRepository .countByUserTodayTil (userId , PostStatus .PUBLISHED , todayStart , todayEnd );
184+ long todayTagCount = tilTagRepository .countByUserTodayTil (userId , PostStatus .PUBLISHED , todayStart , tomorrowStart );
183185 boolean q2 = todayTagCount >= 1 ;
184186
185187 // Q3: 오늘 총 글자 수 >= 200
186188 int todayCharCount = todayLogs .stream ().mapToInt (WateringLog ::getContentLength ).sum ();
187189 boolean q3 = todayCharCount >= 200 ;
188190
189191 // 달성된 퀘스트에 대해 오늘 첫 달성이면 포인트 지급
190- awardQuestPoints (userId , q1 , q2 , q3 , todayStart , todayEnd );
192+ awardQuestPoints (userId , q1 , q2 , q3 , today );
191193
192194 List <QuestDto > quests = List .of (
193195 new QuestDto ("Q1" , "TIL 1개 작성하기" , q1 , 50 ),
@@ -201,17 +203,19 @@ public QuestResponse getQuests(Long userId) {
201203 return new QuestResponse (quests , earnedToday , totalToday );
202204 }
203205
204- private void awardQuestPoints (Long userId , boolean q1 , boolean q2 , boolean q3 ,
205- LocalDateTime from , LocalDateTime to ) {
206+ private static final Set <PointLogReason > QUEST_REASONS =
207+ Set .of (PointLogReason .QUEST_Q1 , PointLogReason .QUEST_Q2 , PointLogReason .QUEST_Q3 );
208+
209+ private void awardQuestPoints (Long userId , boolean q1 , boolean q2 , boolean q3 , LocalDate today ) {
206210 // 달성된 퀘스트가 없으면 DB 조회 없이 early return
207211 if (!q1 && !q2 && !q3 ) return ;
208212
209- // 오늘 이미 지급된 퀘스트 reason을 1번 쿼리로 조회 후 메모리에서 중복 체크
210- Set <PointLogReason > awardedToday = pointLogRepository .findReasonsByUserIdAndCreatedAtBetween (userId , from , to );
213+ // awardedDate 기준으로 오늘 이미 지급된 퀘스트 reason 조회 (createdAt BETWEEN 대신)
214+ Set <PointLogReason > awardedToday =
215+ pointLogRepository .findQuestReasonsByUserIdAndAwardedDate (userId , today , QUEST_REASONS );
211216
212217 // User 풀 로딩 없이 프록시 참조만 사용 (PointLog FK 저장용)
213218 User userRef = userRepository .getReferenceById (userId );
214- LocalDate today = from .toLocalDate ();
215219
216220 awardIfNew (userId , userRef , q1 , PointLogReason .QUEST_Q1 , 50 , awardedToday , today );
217221 awardIfNew (userId , userRef , q2 , PointLogReason .QUEST_Q2 , 30 , awardedToday , today );
@@ -225,12 +229,7 @@ private void awardIfNew(Long userId, User userRef, boolean done, PointLogReason
225229
226230 // 원자적 UPDATE — 동시 요청 시 lost update 방지
227231 userRepository .incrementPoint (userId , point );
228- pointLogRepository .save (PointLog .builder ()
229- .user (userRef )
230- .reason (reason )
231- .amount (point )
232- .awardedDate (awardedDate )
233- .build ());
232+ pointLogRepository .save (PointLog .forQuest (userRef , reason , point , awardedDate ));
234233 }
235234
236235 private int calculateMaxStreak (Set <LocalDate > dateSet ) {
0 commit comments