-
Notifications
You must be signed in to change notification settings - Fork 2
hotfix: 랜덤 디펜스 검증 개선 & RestClient 타임아웃 설정 & Solved.ac Lazy Sync 반영 #114
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,8 +21,11 @@ public RestClient restClient(RestClient.Builder builder) { | |
| .connectTimeout(java.time.Duration.ofSeconds(10)) | ||
| .build(); | ||
|
|
||
| var requestFactory = new org.springframework.http.client.JdkClientHttpRequestFactory(httpClient); | ||
| requestFactory.setReadTimeout(java.time.Duration.ofSeconds(60)); | ||
|
Comment on lines
+24
to
+25
|
||
|
|
||
| return builder | ||
| .requestFactory(new org.springframework.http.client.JdkClientHttpRequestFactory(httpClient)) | ||
| .requestFactory(requestFactory) | ||
| .defaultHeader("Accept", "application/json") | ||
| .defaultHeader("Content-Type", "application/json") | ||
| .messageConverters(converters -> { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -111,53 +111,69 @@ public Integer verifyDefense(Long userId, Integer solvedProblemId) { | |||||||||
| } | ||||||||||
|
|
||||||||||
| if (user.getDefenseProblemId().equals(solvedProblemId)) { | ||||||||||
| // 실제 정답 제출이 있는지 확인 (runtime_ms, memory_kb가 있는 기록) | ||||||||||
| // 실제 정답 제출이 있는지 확인 (runtime_ms >= 0, memory_kb >= 0 인 기록) | ||||||||||
| // -1 등은 실패/오류로 간주하여 제외 | ||||||||||
| if (!algorithmRecordRepository.existsSuccessfulSubmission(userId, String.valueOf(solvedProblemId))) { | ||||||||||
| return null; // 성공적인 제출 기록이 없음 (오답 또는 컴파일 에러) | ||||||||||
| return null; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| Integer currentStreak = 0; | ||||||||||
|
|
||||||||||
| // 성공! 풀이 시간 계산 및 저장 | ||||||||||
| algorithmRecordRepository.findLatestSuccessfulByUserAndProblem(userId, String.valueOf(solvedProblemId)) | ||||||||||
| .ifPresent(record -> { | ||||||||||
| LocalDateTime startTime = user.getDefenseStartTime(); | ||||||||||
| LocalDateTime endTime = record.getCreatedAt() != null ? record.getCreatedAt() | ||||||||||
| : LocalDateTime.now(); | ||||||||||
| long elapsedSeconds = java.time.Duration.between(startTime, endTime).getSeconds(); | ||||||||||
| record.setElapsedTimeSeconds(elapsedSeconds); | ||||||||||
| algorithmRecordRepository.update(record); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| if ("GOLD".equals(user.getDefenseType())) { | ||||||||||
| user.setGoldStreak(user.getGoldStreak() + 1); | ||||||||||
| currentStreak = user.getGoldStreak(); | ||||||||||
| if (user.getGoldStreak() > user.getMaxGoldStreak()) { | ||||||||||
| user.setMaxGoldStreak(user.getGoldStreak()); | ||||||||||
| // 가장 최근의 성공 기록을 가져와서, 그 기록이 *현재 디펜스 시작 이후*에 생성된 것인지 확인 | ||||||||||
| var latestRecordOpt = algorithmRecordRepository.findLatestSuccessfulByUserAndProblem(userId, | ||||||||||
| String.valueOf(solvedProblemId)); | ||||||||||
|
|
||||||||||
| if (latestRecordOpt.isPresent()) { | ||||||||||
| var record = latestRecordOpt.get(); | ||||||||||
|
|
||||||||||
| // CRITICAL: 과거에 푼 기록이 아니라, "지금" 푼 기록이어야 함 | ||||||||||
| if (record.getCreatedAt().isBefore(user.getDefenseStartTime())) { | ||||||||||
| return null; | ||||||||||
| } | ||||||||||
| } else { | ||||||||||
| user.setSilverStreak(user.getSilverStreak() + 1); | ||||||||||
| currentStreak = user.getSilverStreak(); | ||||||||||
| if (user.getSilverStreak() > user.getMaxSilverStreak()) { | ||||||||||
| user.setMaxSilverStreak(user.getSilverStreak()); | ||||||||||
|
|
||||||||||
| // CRITICAL: 런타임/메모리가 유효한 값이어야 함 (0ms 포함, -1 제외) | ||||||||||
| if (record.getRuntimeMs() < 0 || record.getMemoryKb() < 0) { | ||||||||||
|
Comment on lines
+135
to
+136
|
||||||||||
| // CRITICAL: 런타임/메모리가 유효한 값이어야 함 (0ms 포함, -1 제외) | |
| if (record.getRuntimeMs() < 0 || record.getMemoryKb() < 0) { | |
| // CRITICAL: 런타임/메모리가 유효한 값이어야 함 (런타임은 0ms 포함, 메모리는 0KB 이상 불가, -1 등 오류 값 제외) | |
| if (record.getRuntimeMs() < 0 || record.getMemoryKb() <= 0) { |
Copilot
AI
Feb 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This null check creates inconsistent behavior. If record.getCreatedAt() is null (which shouldn't happen in normal operation since created_at has a DEFAULT CURRENT_TIMESTAMP), the elapsed time is calculated from defense start to "now" instead of to the actual submission time. However, a few lines above (line 131), the code checks if the record was created before the defense start time, which would fail if createdAt is null since null.isBefore() would throw a NullPointerException.
Either:
- Remove the null check here since line 131 already implies createdAt is not null
- Add an explicit null check before line 131 and return null if createdAt is null
Copilot
AI
Feb 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The validation logic now properly checks that records are created after defense start time and have valid runtime/memory values. However, there's a subtle issue: if existsSuccessfulSubmission returns true (line 116) but findLatestSuccessfulByUserAndProblem returns empty (line 124-127), the method returns null without explanation.
This could happen if:
- A record matching the criteria exists but was deleted between the two queries
- There's a race condition where another thread deletes/modifies the record
Consider adding logging when latestRecordOpt.isEmpty() to help diagnose why validation failed despite a successful submission existing.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,6 +11,7 @@ | |||||||||||||||||
| import com.ssafy.dash.study.application.StudyService; | ||||||||||||||||||
| import com.ssafy.dash.onboarding.domain.OnboardingRepository; | ||||||||||||||||||
| import com.ssafy.dash.user.domain.exception.UserNotFoundException; | ||||||||||||||||||
| import com.ssafy.dash.analytics.application.SolvedacSyncService; | ||||||||||||||||||
| import org.springframework.stereotype.Service; | ||||||||||||||||||
| import org.springframework.transaction.annotation.Transactional; | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -26,17 +27,20 @@ public class UserService { | |||||||||||||||||
| private final StudyRepository studyRepository; | ||||||||||||||||||
| private final StudyService studyService; | ||||||||||||||||||
| private final LearningPathCacheMapper learningPathCacheMapper; | ||||||||||||||||||
| private final SolvedacSyncService solvedacSyncService; | ||||||||||||||||||
|
|
||||||||||||||||||
| public UserService(UserRepository userRepository, | ||||||||||||||||||
| OnboardingRepository onboardingRepository, | ||||||||||||||||||
| StudyRepository studyRepository, | ||||||||||||||||||
| StudyService studyService, | ||||||||||||||||||
| LearningPathCacheMapper learningPathCacheMapper) { | ||||||||||||||||||
| LearningPathCacheMapper learningPathCacheMapper, | ||||||||||||||||||
| SolvedacSyncService solvedacSyncService) { | ||||||||||||||||||
| this.userRepository = userRepository; | ||||||||||||||||||
| this.onboardingRepository = onboardingRepository; | ||||||||||||||||||
| this.studyRepository = studyRepository; | ||||||||||||||||||
| this.studyService = studyService; | ||||||||||||||||||
| this.learningPathCacheMapper = learningPathCacheMapper; | ||||||||||||||||||
| this.solvedacSyncService = solvedacSyncService; | ||||||||||||||||||
|
Comment on lines
32
to
+43
|
||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| @Transactional | ||||||||||||||||||
|
|
@@ -72,6 +76,12 @@ public UserResult findById(Long id) { | |||||||||||||||||
| // 분석 데이터 존재 여부 확인 | ||||||||||||||||||
| boolean hasAnalysis = learningPathCacheMapper.findByUserId(id) != null; | ||||||||||||||||||
|
|
||||||||||||||||||
| // [Lazy Sync] Solved.ac 연동 정보가 있고, 마지막 동기화 후 1시간이 지났으면 갱신 | ||||||||||||||||||
| if (shouldUpdateSolvedacStats(u)) { | ||||||||||||||||||
| solvedacSyncService.updateTierAndStats(id); | ||||||||||||||||||
| u = userRepository.findById(id).orElse(u); | ||||||||||||||||||
|
Comment on lines
+81
to
+82
|
||||||||||||||||||
| solvedacSyncService.updateTierAndStats(id); | |
| u = userRepository.findById(id).orElse(u); | |
| // 잠재적 동시성 문제를 줄이기 위해 최신 사용자 정보를 한 번 더 조회하여 조건을 재확인 | |
| User latestUser = userRepository.findById(id).orElse(u); | |
| if (shouldUpdateSolvedacStats(latestUser)) { | |
| solvedacSyncService.updateTierAndStats(id); | |
| u = userRepository.findById(id).orElse(u); | |
| } |
Copilot
AI
Feb 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The user entity is refetched from the database after the sync operation, which is good. However, this refetch happens outside the transaction boundary of updateTierAndStats. If another operation modifies the user between the sync and this refetch, those changes could be lost or inconsistent data could be returned.
Consider either:
- Making the entire block (lines 79-83) run within a single transaction
- Returning the updated user from
updateTierAndStatsto avoid the second database call
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| INSERT INTO decorations (name, description, css_class, type, price, is_active) VALUES | ||
| ('Contributor''s Insight', 'DashHub 발전에 기여해주신 분들을 위한 특별한 선물입니다.', 'effect-contributor', 'SPECIAL', 0, true); | ||
|
Comment on lines
+1
to
+2
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,15 +77,15 @@ | |
|
|
||
| <select id="countSuccessfulSubmissionByUserIdAndProblemNumber" resultType="int"> SELECT COUNT(*) | ||
| FROM algorithm_records WHERE user_id = #{userId} AND record_type = 'USER_SOLUTION' AND | ||
| problem_number = #{problemNumber} AND runtime_ms IS NOT NULL AND memory_kb IS NOT NULL </select> | ||
| problem_number = #{problemNumber} AND runtime_ms >= 0 AND memory_kb >= 0 </select> | ||
|
|
||
| <select id="selectSolvedProblemNumbersByUserId" resultType="string" parameterType="long"> SELECT | ||
| DISTINCT problem_number FROM algorithm_records WHERE user_id = #{userId} AND (record_type = | ||
| 'SOLVED_AC_SYNC' OR (runtime_ms IS NOT NULL AND memory_kb IS NOT NULL)) </select> | ||
| 'SOLVED_AC_SYNC' OR (runtime_ms >= 0 AND memory_kb >= 0)) </select> | ||
|
Comment on lines
+80
to
+84
|
||
|
|
||
| <select id="selectLatestSuccessfulByUserAndProblem" resultType="AlgorithmRecord"> SELECT * FROM | ||
| algorithm_records WHERE user_id = #{userId} AND problem_number = #{problemNumber} AND runtime_ms | ||
| IS NOT NULL AND memory_kb IS NOT NULL ORDER BY created_at DESC LIMIT 1 </select> | ||
| >= 0 AND memory_kb >= 0 ORDER BY created_at DESC LIMIT 1 </select> | ||
|
|
||
| <update id="migrateStudyId"> UPDATE algorithm_records SET study_id = #{newStudyId} WHERE study_id | ||
| = #{oldStudyId} </update> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While swallowing exceptions here is intentional to prevent profile lookup failures, it could mask legitimate issues. The log message only captures
e.getMessage(), which may not provide enough context for debugging.Consider:
log.warn("Failed to lazy sync...", e)instead of just the message