diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 027089f6..8167bb73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,4 +39,6 @@ jobs: run: chmod +x gradlew - name: Build (includes React build) + env: + CI: false run: ./gradlew clean build --no-daemon diff --git a/.gitignore b/.gitignore index 99dc1614..c71403fe 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ src/main/resources/application-dev.yml .env *.tar +/.claude/ diff --git a/build.gradle b/build.gradle index 02d8e48e..c0ff3e17 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.2.1' + id 'io.spring.dependency-management' version '1.1.4' } repositories { @@ -23,12 +24,6 @@ dependencies { implementation libs.org.mybatis.spring.boot.mybatis.spring.boot.starter runtimeOnly libs.org.springframework.boot.spring.boot.devtools - // Test - testImplementation libs.org.junit.jupiter.junit.jupiter - testImplementation libs.org.mockito.mockito.junit.jupiter - testImplementation libs.org.assertj.assertj.core - - // JPA implementation libs.org.springframework.boot.spring.boot.starter.data.jpa @@ -83,57 +78,38 @@ tasks.withType(Javadoc) { options.encoding = 'UTF-8' } -tasks.test { - useJUnitPlatform() -} - def frontendDir = "$projectDir/src/main/frontend" -def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile -def reactBuildDir = layout.buildDirectory.dir("frontend").get().asFile sourceSets { main { - java { - srcDir querydslDir - } resources { srcDirs = ["$projectDir/src/main/resources"] } } } -// ✅ (추가) Q 클래스 생성 위치를 querydslDir로 고정 -tasks.withType(JavaCompile).configureEach { - options.generatedSourceOutputDirectory = querydslDir +bootJar { + dependsOn "copyReactBuildFiles" } -tasks.named("jar") { - dependsOn("copyReactBuildFiles") - from(reactBuildDir) { - into("static") // classpath:/static/ 로 들어감 - } +processResources { + dependsOn "copyReactBuildFiles" } -tasks.named("bootJar") { - dependsOn("copyReactBuildFiles") - from(reactBuildDir) { - into("static") - } -} - -tasks.register('installReact', Exec) { +task installReact(type: Exec) { workingDir "$frontendDir" inputs.dir "$frontendDir" group = BasePlugin.BUILD_GROUP if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) { - commandLine 'npm.cmd', 'ci' + commandLine "npm.cmd", "audit", "fix" + commandLine 'npm.cmd', 'install' } else { - commandLine 'npm', 'ci' + commandLine "npm", "audit", "fix" + commandLine 'npm', 'install' } } -tasks.register('buildReact', Exec) { - environment "CI", "false" +task buildReact(type: Exec) { dependsOn "installReact" workingDir "$frontendDir" inputs.dir "$frontendDir" @@ -144,8 +120,9 @@ tasks.register('buildReact', Exec) { commandLine "npm", "run-script", "build" } } -tasks.register('copyReactBuildFiles', Copy) { + +task copyReactBuildFiles(type: Copy) { dependsOn "buildReact" from "$frontendDir/build" - into reactBuildDir + into "$buildDir/resources/main/static" } diff --git a/src/main/frontend/package-lock.json b/src/main/frontend/package-lock.json index 21b53200..60db1fb7 100644 --- a/src/main/frontend/package-lock.json +++ b/src/main/frontend/package-lock.json @@ -16487,23 +16487,6 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, - "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/src/main/frontend/src/App.js b/src/main/frontend/src/App.js index ec025d96..642e2741 100644 --- a/src/main/frontend/src/App.js +++ b/src/main/frontend/src/App.js @@ -1,5 +1,4 @@ import React from 'react'; -import logo from './logo.svg'; import './App.css'; import {Helmet} from "react-helmet-async"; import ModalContainer from "./js/setup/modal/ModalContainer"; @@ -9,28 +8,28 @@ import {Route, Routes} from "react-router-dom"; import MainPage from "./js/components/MainPage"; function App() { - // 전역 툴팁 함수들 등록 - React.useEffect(() => { - registerGlobalTooltip(); - }, []); + // 전역 툴팁 함수들 등록 + React.useEffect(() => { + registerGlobalTooltip(); + }, []); - return ( -
- - Baektracker - - - - - - - }> + return ( +
+ + Baektracker + + + + + + + }> - - - -
- ); +
+
+
+
+ ); } export default App; diff --git a/src/main/frontend/src/css/styles.module.css b/src/main/frontend/src/css/styles.module.css index d82e9f24..5f49254f 100644 --- a/src/main/frontend/src/css/styles.module.css +++ b/src/main/frontend/src/css/styles.module.css @@ -485,8 +485,24 @@ .userProgressName .rating_text { margin: 0 6px; - font-size: 14px; + font-size: 16px; + cursor: pointer; +} + +.userProgressName .increase_icon { + margin-left: 4px; + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 10px solid #1e90ff; /* 파란색 */ +} + +.userProgressName .increased_rating_text { + margin: 0 3px 0 2px; + font-size: 13px; cursor: pointer; + color: #484848; } .userProgressName .pass_text { diff --git a/src/main/frontend/src/js/components/Header.jsx b/src/main/frontend/src/js/components/Header.jsx index 1211a11c..15d6df8e 100644 --- a/src/main/frontend/src/js/components/Header.jsx +++ b/src/main/frontend/src/js/components/Header.jsx @@ -26,7 +26,7 @@ export function Header({fromDate, toDate, setFromDate, setToDate}) { }) } - const setWeekDates = (date: Date) => { + const setWeekDates = (date) => { const fd = DateUtils.getFirstDateOfWeek(date); const td = DateUtils.getLastDateOfWeek(date); setFromDate(DateUtils.dateToStringYYMMdd(fd)) @@ -93,21 +93,21 @@ export function Header({fromDate, toDate, setFromDate, setToDate}) { ) } -// FineInfo 컴포넌트 -const FineInfo = ({totalFine, onShowReceipt, className}) => { - const formatCurrency = (amount) => { - return new Intl.NumberFormat("ko-KR").format(amount) - } - - return ( -
-
- 누적 벌금 - ₩{formatCurrency(totalFine)} -
- -
- ) -} +// // FineInfo 컴포넌트 +// const FineInfo = ({totalFine, onShowReceipt, className}) => { +// const formatCurrency = (amount) => { +// return new Intl.NumberFormat("ko-KR").format(amount) +// } +// +// return ( +//
+//
+// 누적 벌금 +// ₩{formatCurrency(totalFine)} +//
+// +//
+// ) +// } diff --git a/src/main/frontend/src/js/components/MarkedProblemItem.jsx b/src/main/frontend/src/js/components/MarkedProblemItem.jsx index 072beca4..5a4cbc5c 100644 --- a/src/main/frontend/src/js/components/MarkedProblemItem.jsx +++ b/src/main/frontend/src/js/components/MarkedProblemItem.jsx @@ -29,7 +29,7 @@ export function MarkedProblemItem({problem, index}) { window.open(`https://www.acmicpc.net/problem/${problem.problemId}`, "_blank") }} className={cm(`${styles.userProgressProblemItem} ${problem.solved ? styles.userProgressSolved : styles.userProgressUnsolved}`, - `${problem.is_shared_problem == 1 && styles.is_shared_problem}`)} + `${problem.is_shared_problem === 1 && styles.is_shared_problem}`)} onMouseEnter={tooltip.onMouseEnter} onMouseLeave={tooltip.onMouseLeave} > @@ -44,4 +44,4 @@ export function MarkedProblemItem({problem, index}) { ) -} \ No newline at end of file +} diff --git a/src/main/frontend/src/js/components/UserProgress.jsx b/src/main/frontend/src/js/components/UserProgress.jsx index 7692d5e2..d12c95d2 100644 --- a/src/main/frontend/src/js/components/UserProgress.jsx +++ b/src/main/frontend/src/js/components/UserProgress.jsx @@ -24,11 +24,13 @@ export function UserProgress({fromDate, toDate}) { const threeDotsRefs = useRef({}) const longPressTimer = useRef(null) + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { getAllUsers() initLoad(); }, []); + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { getWeeklyUsersProgress(); }, [fromDate, toDate]); @@ -248,6 +250,7 @@ export function UserProgress({fromDate, toDate}) { const userProgress = problems[user.id]; const score = userProgress ? userProgress.score : 0; const isWeekPass = userProgress?.isWeekPass ?? false + const increasedRating = userProgress.increasedRating > 0 ? userProgress.increasedRating : null; const problemList = userProgress?.problems; return ( @@ -266,7 +269,16 @@ export function UserProgress({fromDate, toDate}) { {user.rating} + onMouseLeave={tooltip.onMouseLeave}>{user.rating} + { + increasedRating && <> + + {increasedRating} + + } + + {isWeekPass ? PASS : ''} diff --git a/src/main/frontend/src/js/components/WeekProblems.jsx b/src/main/frontend/src/js/components/WeekProblems.jsx index 425ac9b9..84d5947a 100644 --- a/src/main/frontend/src/js/components/WeekProblems.jsx +++ b/src/main/frontend/src/js/components/WeekProblems.jsx @@ -16,6 +16,7 @@ export function WeekProblems({fromDate, toDate}) { const [weeklyProblemInputs, setWeeklyProblemInputs] = useState([-1, -1, -1]) + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { getWeeklyProblems(); }, [fromDate, toDate]); @@ -112,4 +113,4 @@ export function WeekProblems({fromDate, toDate}) { ) -} \ No newline at end of file +} diff --git a/src/main/frontend/src/js/modal/FineReceiptModal.jsx b/src/main/frontend/src/js/modal/FineReceiptModal.jsx index b2fde467..8f69d3ac 100644 --- a/src/main/frontend/src/js/modal/FineReceiptModal.jsx +++ b/src/main/frontend/src/js/modal/FineReceiptModal.jsx @@ -19,10 +19,12 @@ export function FineReceiptModal(props) { const [totalSum, setTotalSum] = useState(0) const [fold, setFold] = useState(true) + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { getTotalFine() }, []); + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { getMonthFine() // getWeeklyResult(); diff --git a/src/main/frontend/src/js/modal/LayerModal.js b/src/main/frontend/src/js/modal/LayerModal.js index 45cc39db..aff7b257 100644 --- a/src/main/frontend/src/js/modal/LayerModal.js +++ b/src/main/frontend/src/js/modal/LayerModal.js @@ -7,15 +7,18 @@ import {ScrollUtils} from "../utils/ScrollUtils"; Usage ex) */ -export const LayerModal = ({modalRef, scrollable, children, top, left, width, backgroundColor, - height, windowBlocked, minWidth, maxWidth, minHeight, maxHeight, paddingBottom}) => { +export const LayerModal = ({ + modalRef, scrollable, children, top, left, width, backgroundColor, + height, windowBlocked, minWidth, maxWidth, minHeight, maxHeight, paddingBottom + }) => { const [fadeIn, setFadeIn] = useState(false); const scrollRef = useRef(null) + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { let prevScrollY = null; - if(!scrollable){ + if (!scrollable) { prevScrollY = ScrollUtils.preventScroll(scrollRef.current.body); } const timer = setTimeout(() => { @@ -23,7 +26,7 @@ export const LayerModal = ({modalRef, scrollable, children, top, left, width, ba }, 100) return () => { - if(prevScrollY){ + if (prevScrollY) { ScrollUtils.allowScroll(scrollRef.current.body, prevScrollY) } prevScrollY = null; @@ -34,10 +37,10 @@ export const LayerModal = ({modalRef, scrollable, children, top, left, width, ba useEffect(() => { let prevScrollY = null; - if(windowBlocked){ + if (windowBlocked) { prevScrollY = ScrollUtils.preventScroll(document.body); } - return ()=>{ + return () => { ScrollUtils.allowScroll(document.body, prevScrollY) } }, []); diff --git a/src/main/frontend/src/js/setup/utils/ObjectUtil.js b/src/main/frontend/src/js/setup/utils/ObjectUtil.js index 7d9af6e9..99b2433d 100644 --- a/src/main/frontend/src/js/setup/utils/ObjectUtil.js +++ b/src/main/frontend/src/js/setup/utils/ObjectUtil.js @@ -1,47 +1,46 @@ - export const ObjectUtils = { - isEmpty: (value)=>{ + isEmpty: (value) => { const result = value === null || value === undefined || value === '' || value.length === 0; - if(result){ + if (result) { return true; } - if(typeof value === 'object'){ - return Object.keys(value).length === 0; + if (typeof value === 'object') { + return Object.keys(value).length === 0; } return false; }, - isEmptyArray: (array)=>{ - return !array || typeof array !== "object" || array.length === 0 - }, - isEmptyMap(map){ - for(const key in map){ - if(!ObjectUtils.isEmpty(map[key])){ - return false; - } - } - return true; - }, - convertBooleanArrayToString(arr){ - let str = ''; - arr.map((value)=>{ - str += (value) ? '1' : '0'; - }) - return str; - }, - transposeArray: (arr) => arr.reduce( - (result, row) => row.map((_, i) => [...(result[i] || []), row[i]]), - [] - ), - toggleOf, - varToString, -} - -function toggleOf(arr, index){ - for(let i in arr){ - arr[i] = (i == index); - } + // isEmptyArray: (array)=>{ + // return !array || typeof array !== "object" || array.length === 0 + // }, + // isEmptyMap(map){ + // for(const key in map){ + // if(!ObjectUtils.isEmpty(map[key])){ + // return false; + // } + // } + // return true; + // }, + // convertBooleanArrayToString(arr){ + // let str = ''; + // arr.map((value)=>{ + // str += (value) ? '1' : '0'; + // }) + // return str; + // }, + // transposeArray: (arr) => arr.reduce( + // (result, row) => row.map((_, i) => [...(result[i] || []), row[i]]), + // [] + // ), + // toggleOf, + // varToString, } - -function varToString(_var){ - return Object.keys(_var)[0]; -} \ No newline at end of file +// +// function toggleOf(arr, index){ +// for(let i in arr){ +// arr[i] = (i == index); +// } +// } +// +// function varToString(_var){ +// return Object.keys(_var)[0]; +// } diff --git a/src/main/java/com/baektracker/domain/baekjoon/service/BaekjoonService.java b/src/main/java/com/baektracker/domain/baekjoon/service/BaekjoonService.java index ccad2753..27998a7d 100644 --- a/src/main/java/com/baektracker/domain/baekjoon/service/BaekjoonService.java +++ b/src/main/java/com/baektracker/domain/baekjoon/service/BaekjoonService.java @@ -8,6 +8,7 @@ import com.baektracker.domain.problem.service.BaekjoonProblemService; import com.baektracker.domain.user.model.User; import com.baektracker.domain.user.repository.UserRepository; +import com.baektracker.domain.user.service.UserService; import com.baektracker.global.code.ApiResponseCode; import com.baektracker.global.exception.CustomException; import java.io.IOException; @@ -29,6 +30,7 @@ @RequiredArgsConstructor public class BaekjoonService { private final BaekjoonProblemService problemService; + private final UserService userService; private final SolvedProblemRepository solvedProblemRepository; private final UserRepository userRepository; private static final String gRecaptchaResponse = "03AFcWeA5mQSaepuaaR7U57_-xDPkMdoyyCHIw-GzIjiJwJ7dc85B0DUuLfj25b1CsGB_9zUOTXZ4P6eAbCq-ySOvJt1hLaCt_lXXzQPclXypdWyy4cOHmb9AmWrneWc6oDQ2eeGlWyg_ziSQlaLJgZDHAIE2T1h5NG5PXasZgvRv8q7bsOqv_tI4YqnjDJW0qyeDb-NveASxddrqUYRB0jKb2je7g-OBRafdGjNgwrhWP8Wgw7i_twMZjYdS2VOhkQtUwu2N88qbcNlt9sboHFKBHEDkcIE2cfDQ5dB7s7lYH5H1CJPGmqqNxAiF6A-ssV5z0Cpd8tg-d_clgwVDC-QsKZ6RQLJmAG0h5_IIXH-CsVoFeTTcodslWXcTJofA7eKN5OhGo8oOTjYUs7kjvhDJi4if6_ohi4oriDa1R_vm_R9mCH3bxSlXTWXBEkf37n6_PiVwfVYk5oucLgDORbhGllQ_7DEZr3wF3NGqlcL5y9ITpNH-cWJYpPA6ZA-_a_Pq_WbDgr5DTbwsN5j258djDcdD_vB4bElyaKWS812nNLFYTyFTyssfLiTE13w5k0qpm2L-gAbrOrJaxdIOZj6WMhKiLmm4K0pEZ-hEDNNQZ_UU835Ia95ArXHm5gl9ruzvF8PZBm4kKsosP3y3eRuSdT0NBALJ4eRGbxp56RPmMjMkqR7_19moOIgqN0p-q2cSRatqj9kH7qKeYNh7a39aE7kPSuX5XVIBVekrqKwPlAOdb2ueFS6iC5qa0xq9a4epJRhZfLZKI0-t6H6fdupuEQY54co-bEK0ypEMxYSS7VKNHMiwqsJfR0aH2P9V4TLIZlIpzHILioY9SzOwDMM6N26B_zpXh_etnVSG4Qya0HTVNC-45a_DVe4La9RM5oDKCzfyGhF1FUfFX-Zm9z7Ye7Fp-Z9r6hKDT_I7o4WlQC_OSVUCveuYFkeH5sEbxzJ6G6hVXgI0A-RZBHwKxdBH1MG1UizNWdw\n"; @@ -63,6 +65,13 @@ public int loadBaekjoonProblemStatus() { user.updateLastReadTime(LocalDateTime.now()); solvedProblemRepository.saveAll(scrappedProblems); count += scrappedProblems.size(); + + long correctCount = scrappedProblems.stream() + .filter(v -> v.getResultId().equals(4)).count(); + if (correctCount > 0) { + userService.updateUserInfoFromSolvedAc(user); + } + } } return count; diff --git a/src/main/java/com/baektracker/domain/problem/dto/WeeklyUserProgress.java b/src/main/java/com/baektracker/domain/problem/dto/WeeklyUserProgress.java index 500ad477..4a7318ed 100644 --- a/src/main/java/com/baektracker/domain/problem/dto/WeeklyUserProgress.java +++ b/src/main/java/com/baektracker/domain/problem/dto/WeeklyUserProgress.java @@ -6,7 +6,8 @@ public record WeeklyUserProgress( Long userId, Integer score, Boolean isWeekPass, + Integer increasedRating, List problems ) { -} \ No newline at end of file +} diff --git a/src/main/java/com/baektracker/domain/problem/service/BaekjoonProblemService.java b/src/main/java/com/baektracker/domain/problem/service/BaekjoonProblemService.java index d613981f..3fc71cc7 100644 --- a/src/main/java/com/baektracker/domain/problem/service/BaekjoonProblemService.java +++ b/src/main/java/com/baektracker/domain/problem/service/BaekjoonProblemService.java @@ -85,10 +85,10 @@ public WeeklyUsersProgressResponse getWeeklyUsersProgress(LocalDate fromDate) { return WeeklyUsersProgressResponse.from(progresses); } Map> userMap = new HashMap<>(); - Map weekPassMap = weeklyResults.stream() + Map weekPassMap = weeklyResults.stream() .collect(Collectors.toMap( WeeklyResult::getUserId, - this::isWeekPass + (v) -> v )); Map> coSolverMap = coSolvedUsers.stream() @@ -106,13 +106,14 @@ public WeeklyUsersProgressResponse getWeeklyUsersProgress(LocalDate fromDate) { userMap.put(userId, list); } for (User user : users) { + WeeklyResult wr = weekPassMap.get(user.getId()); boolean isWeekPass = false; if (weekPassMap.containsKey(user.getId())) { - isWeekPass = weekPassMap.get(user.getId()); + isWeekPass = isWeekPass(wr); } List userProblems = userMap.get(user.getId()); if (userProblems == null || userProblems.isEmpty()) { - progresses.add(new WeeklyUserProgress(user.getId(), 0, isWeekPass, null)); + progresses.add(new WeeklyUserProgress(user.getId(), 0, isWeekPass, 0, null)); continue; } List details = userProblems.stream() @@ -126,7 +127,8 @@ public WeeklyUsersProgressResponse getWeeklyUsersProgress(LocalDate fromDate) { .map(this::mapProblemToScore) .reduce(Integer::sum) .orElse(0); - progresses.add(new WeeklyUserProgress(user.getId(), score, isWeekPass, details)); + int increasedRating = user.getRating() - wr.getLastRating(); + progresses.add(new WeeklyUserProgress(user.getId(), score, isWeekPass, increasedRating, details)); } return WeeklyUsersProgressResponse.from(progresses); } diff --git a/src/main/java/com/baektracker/domain/test/controller/TestController.java b/src/main/java/com/baektracker/domain/test/controller/TestController.java index 0560f7da..3013ffc4 100644 --- a/src/main/java/com/baektracker/domain/test/controller/TestController.java +++ b/src/main/java/com/baektracker/domain/test/controller/TestController.java @@ -31,4 +31,11 @@ public ResponseEntity testUserLevelJob() { userService.updateUserInfoFromSolvedAc(); return ResponseEntity.noContent().build(); } + + @GetMapping("/job/streak") + public ResponseEntity testUpdateStreak(@RequestParam LocalDate date) { + weeklyResultService.updateUserStreaks(date); + return ResponseEntity.noContent().build(); + } + } diff --git a/src/main/java/com/baektracker/domain/user/service/UserService.java b/src/main/java/com/baektracker/domain/user/service/UserService.java index 9214941f..f9539af6 100644 --- a/src/main/java/com/baektracker/domain/user/service/UserService.java +++ b/src/main/java/com/baektracker/domain/user/service/UserService.java @@ -44,15 +44,19 @@ public List getUsers() { public void updateUserInfoFromSolvedAc() { List users = userRepository.findAll(); for (User user : users) { - SolvedAcUser solvedAcUser = solvedAcService.searchUser(user.getUsername()); - - try { - user.setLevel(solvedAcUser.items().get(0).tier()); - user.setRating(solvedAcUser.items().get(0).rating()); - } catch (NullPointerException e) { - e.printStackTrace(); - continue; - } + updateUserInfoFromSolvedAc(user); + } + } + + @Transactional + public void updateUserInfoFromSolvedAc(User user) { + SolvedAcUser solvedAcUser = solvedAcService.searchUser(user.getUsername()); + + try { + user.setLevel(solvedAcUser.items().get(0).tier()); + user.setRating(solvedAcUser.items().get(0).rating()); + } catch (NullPointerException e) { + throw CustomException.of(ApiResponseCode.SOLVED_AC_USER_INFO_ERROR, "username: " + user.getUsername()); } } diff --git a/src/main/java/com/baektracker/domain/weekly_result/model/WeeklyResult.java b/src/main/java/com/baektracker/domain/weekly_result/model/WeeklyResult.java index 0c882277..36dd15e0 100644 --- a/src/main/java/com/baektracker/domain/weekly_result/model/WeeklyResult.java +++ b/src/main/java/com/baektracker/domain/weekly_result/model/WeeklyResult.java @@ -53,6 +53,9 @@ public class WeeklyResult { @JoinColumn(name = "user_id") private User user; + @Column(name = "last_rating") + private Integer lastRating; + public Long getUserId() { return user.getId(); } @@ -80,6 +83,7 @@ public static WeeklyResult from(LocalDate date, User user) { .score(0) .state(WeeklyResultState.None) .fine(0) + .lastRating(user.getRating()) .user(user) .build(); } diff --git a/src/main/java/com/baektracker/domain/weekly_result/service/WeeklyResultService.java b/src/main/java/com/baektracker/domain/weekly_result/service/WeeklyResultService.java index 0ddd9dbb..5d873534 100644 --- a/src/main/java/com/baektracker/domain/weekly_result/service/WeeklyResultService.java +++ b/src/main/java/com/baektracker/domain/weekly_result/service/WeeklyResultService.java @@ -115,6 +115,7 @@ public void updateWeekPass(WeekPassRequestDto dto) { weeklyResult.setState(state); } + @Transactional public void insertInitialWeeklyResults(LocalDate date) { List users = userRepository.findAll(); List weeklyResults = users.stream() @@ -138,6 +139,7 @@ public void updateWeeklyResults(LocalDate fromDate, LocalDate toDate) { for (WeeklyResult weeklyResult : weeklyResults) { int score = userScoreMap.getOrDefault(weeklyResult.getUserId(), 0); int fine = 0; +// weeklyResult.setLastRating(weeklyResult.getUser().getRating()); weeklyResult.setScore(score); if (weeklyResult.getState() == WeeklyResultState.None) { WeeklyResultState state = getWeeklyResultState(score); diff --git a/src/main/java/com/baektracker/global/code/ApiResponseCode.java b/src/main/java/com/baektracker/global/code/ApiResponseCode.java index d883fbea..6066d8df 100644 --- a/src/main/java/com/baektracker/global/code/ApiResponseCode.java +++ b/src/main/java/com/baektracker/global/code/ApiResponseCode.java @@ -70,6 +70,7 @@ public enum ApiResponseCode { TOKEN_PARSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "토큰 파싱 중 오류가 발생했습니다."), BAEKJOON_PAGE_SCRAP_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "백준 페이지 스크랩 중 오류가 발생했습니다."), SOLVED_AC_PROBLEM_SEARCH_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "[Solved-ac] 문제 정보를 가져오는 중 오류가 발생했습니다"), + SOLVED_AC_USER_INFO_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "[Solved-ac] 사용자 정보를 가져오는 중 오류가 발생했습니다"), SOLVED_AC_NOT_FOUND_USER(HttpStatus.INTERNAL_SERVER_ERROR, "[Solved-ac] 사용자 정보를 찾을 수 없습니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/com/baektracker/global/config/JobConfig.java b/src/main/java/com/baektracker/global/config/JobConfig.java index 4f093e9d..e0ee7517 100644 --- a/src/main/java/com/baektracker/global/config/JobConfig.java +++ b/src/main/java/com/baektracker/global/config/JobConfig.java @@ -1,6 +1,5 @@ package com.baektracker.global.config; -import com.baektracker.global.job.RecordUserLevelJob; import com.baektracker.global.job.RecordWeeklyResultJob; import jakarta.annotation.PostConstruct; import java.util.HashMap; @@ -24,11 +23,11 @@ public class JobConfig { @PostConstruct // '의존성 주입이 완료 된 후 실행되는 메소드'를 지정하는 어노테이션 public void run() { JobDetail recordWeeklyResultJob = getJobDetail(RecordWeeklyResultJob.class, new HashMap()); - JobDetail userLevelJob = getJobDetail(RecordUserLevelJob.class, new HashMap()); +// JobDetail userLevelJob = getJobDetail(RecordUserLevelJob.class, new HashMap()); try { scheduler.scheduleJob(recordWeeklyResultJob, getCronTrigger("0 59 23 ? * SUN")); // 매주 일요일 23:59 - scheduler.scheduleJob(userLevelJob, getCronTrigger("0 1 0 ? * *")); // 매일 23:59 +// scheduler.scheduleJob(userLevelJob, getCronTrigger("0 1 0 ? * *")); // 매일 23:59 } catch (SchedulerException e) { throw new RuntimeException(e); } diff --git a/src/main/java/com/baektracker/global/job/RecordWeeklyResultJob.java b/src/main/java/com/baektracker/global/job/RecordWeeklyResultJob.java index e7c841fd..a3aad17a 100644 --- a/src/main/java/com/baektracker/global/job/RecordWeeklyResultJob.java +++ b/src/main/java/com/baektracker/global/job/RecordWeeklyResultJob.java @@ -37,6 +37,6 @@ public void execute(JobExecutionContext jobExecutionContext) throws JobExecution weeklyResultService.insertInitialWeeklyResults(nextWeekDate); // 각 사용자별 스트릭 갱신 - weeklyResultService.updateUserStreaks(fromDate); + weeklyResultService.updateUserStreaks(nextWeekDate); } } diff --git a/src/main/resources/db/migration/V8__add_column_weekly_results.sql b/src/main/resources/db/migration/V8__add_column_weekly_results.sql new file mode 100644 index 00000000..fcc15817 --- /dev/null +++ b/src/main/resources/db/migration/V8__add_column_weekly_results.sql @@ -0,0 +1,2 @@ +alter table weekly_results + add column last_rating int default 0 diff --git a/src/main/resources/mapper/WeeklyResultMapper.xml b/src/main/resources/mapper/WeeklyResultMapper.xml index 8d879ecb..742e58dd 100644 --- a/src/main/resources/mapper/WeeklyResultMapper.xml +++ b/src/main/resources/mapper/WeeklyResultMapper.xml @@ -4,25 +4,30 @@ "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> - # 해당 쿼리를 실행하려는 날짜 기준, 이전 주까지의 스트릭을 갱신 -# 이번 주의 주간 결과는 포함 안함 - set @start_date := (select date_sub(#{date}, interval weekday(#{date}) day)); + WITH RECURSIVE + base AS (SELECT u.id AS user_id, + DATE_SUB(#{date}, INTERVAL WEEKDAY(#{date}) DAY) AS dt, + 0 AS cnt, + 0 AS state + FROM users u - update users u - set streak=(with recursive base as (select @start_date as dt, - 0 as cnt, - 0 as state - union all - select date_sub(dt, interval 1 week) as dt, - cnt + (ws.state = 1) as cnt, - ws.state as state - from base - left join weekly_results ws - on ws.year_week = - date_format(date_sub(dt, interval 1 week), '%Y-%u') and - ws.user_id = u.id - where ws.state in (1, 3)) - select max(cnt) - from base) + UNION ALL + + SELECT b.user_id, + DATE_SUB(b.dt, INTERVAL 1 WEEK) AS dt, + b.cnt + (ws.state = 1) AS cnt, + ws.state AS state + FROM base b + LEFT JOIN weekly_results ws + ON ws.user_id = b.user_id + AND ws.year_week = DATE_FORMAT(DATE_SUB(b.dt, INTERVAL 1 WEEK), '%Y-%u') + WHERE ws.state IN (1, 3)), + agg AS (SELECT user_id, MAX(cnt) AS streak + FROM base + GROUP BY user_id) + UPDATE users u + JOIN agg ON agg.user_id = u.id + SET u.streak = agg.streak +