From 869161e926e25922094e64392f14e640c23b4e16 Mon Sep 17 00:00:00 2001 From: ryuwldnjs Date: Sun, 1 Feb 2026 23:53:14 +0900 Subject: [PATCH 1/8] =?UTF-8?q?solvedac:=20Client/Service=20=EC=97=AD?= =?UTF-8?q?=ED=95=A0=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20dead=20code=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 역할 분리 - SolvedAcClient: 순수 HTTP 호출만 담당 - searchProblems(query, sort, direction) 시그니처 변경 - hasUserSolvedProblem, getSolvedProblemsRaw 제거 - SolvedAcService: 비즈니스 로직 통합 - 쿼리 조합, 응답 가공, 결과 판단을 Service에서 처리 ## 정리 - TestController 삭제 (프로덕션에 있던 테스트용 컨트롤러) - fetchSolvedProblems, recommendTest 삭제 (dead code) - 주석 처리된 커넥션 풀 설정 코드 제거 --- .../studyhelper/solvedac/SolvedAcService.java | 33 ++-------- .../studyhelper/solvedac/TestController.java | 23 ------- .../solvedac/api/SolvedAcClient.java | 66 ++++--------------- 3 files changed, 18 insertions(+), 104 deletions(-) delete mode 100644 src/main/java/com/ryu/studyhelper/solvedac/TestController.java diff --git a/src/main/java/com/ryu/studyhelper/solvedac/SolvedAcService.java b/src/main/java/com/ryu/studyhelper/solvedac/SolvedAcService.java index afc933b..4f1d87f 100644 --- a/src/main/java/com/ryu/studyhelper/solvedac/SolvedAcService.java +++ b/src/main/java/com/ryu/studyhelper/solvedac/SolvedAcService.java @@ -10,7 +10,6 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -90,9 +89,10 @@ public List recommendUnsolvedProblems(List handles, int cou } log.debug("solved.ac 추천 쿼리: {}", query); - ProblemSearchResponse response = solvedAcClient.searchProblems(query, count); + ProblemSearchResponse response = solvedAcClient.searchProblems(query, "random", "asc"); return response.items().stream() .map(ProblemInfo::withUrl) + .limit(count) // Service에서 개수 제한 .toList(); } @@ -131,13 +131,6 @@ private String buildTagFilter(List tagKeys) { return "(" + tagConditions + ")"; } - public List fetchSolvedProblems(String handle) { - ProblemSearchResponse response = solvedAcClient.getSolvedProblemsRaw(handle); - return response.items().stream() - .map(ProblemInfo::withUrl) - .toList(); - } - /** * 특정 사용자가 특정 문제를 풀었는지 확인 * @param handle 사용자 핸들 @@ -146,7 +139,11 @@ public List fetchSolvedProblems(String handle) { */ public boolean hasUserSolvedProblem(String handle, Long problemId) { try { - return solvedAcClient.hasUserSolvedProblem(handle, problemId); + // 쿼리 조합: 문제 ID와 사용자 해결 여부 조건 + String query = "id:" + problemId + " s@" + handle; + ProblemSearchResponse response = solvedAcClient.searchProblems(query, "id", "asc"); + // 결과 판단: 검색 결과가 있으면 해결한 것 + return response.items() != null && !response.items().isEmpty(); } catch (Exception e) { log.error("Failed to check if user {} solved problem {}", handle, problemId, e); throw new CustomException(CustomResponseStatus.SOLVED_AC_API_ERROR); @@ -155,20 +152,4 @@ public boolean hasUserSolvedProblem(String handle, Long problemId) { - - - - - /** - * solved ac api 응답 시간 측정용 - * */ - public List recommendTest(List handles, int totalCount) { - List results = new ArrayList<>(); - - int retry = 0; - for(int i=0;i<10;i++){ - results.addAll(recommendUnsolvedProblems(handles, totalCount)); - } - return results; - } } \ No newline at end of file diff --git a/src/main/java/com/ryu/studyhelper/solvedac/TestController.java b/src/main/java/com/ryu/studyhelper/solvedac/TestController.java deleted file mode 100644 index 5939beb..0000000 --- a/src/main/java/com/ryu/studyhelper/solvedac/TestController.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.ryu.studyhelper.solvedac; - -import com.ryu.studyhelper.solvedac.dto.ProblemInfo; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RequiredArgsConstructor -@RestController -public class TestController { - private final SolvedAcService solvedAcService; - - @GetMapping("/test/{handle}") - public void test(@PathVariable String handle) { - solvedAcService.getUserInfo(handle); - } - - @PostMapping("/recommend") - public List recommend(@RequestBody List handles) { - return solvedAcService.recommendUnsolvedProblems(handles, 3); // 기본 추천 개수 3 - } -} diff --git a/src/main/java/com/ryu/studyhelper/solvedac/api/SolvedAcClient.java b/src/main/java/com/ryu/studyhelper/solvedac/api/SolvedAcClient.java index 25e72cd..45bdd5e 100644 --- a/src/main/java/com/ryu/studyhelper/solvedac/api/SolvedAcClient.java +++ b/src/main/java/com/ryu/studyhelper/solvedac/api/SolvedAcClient.java @@ -1,18 +1,12 @@ package com.ryu.studyhelper.solvedac.api; import com.ryu.studyhelper.solvedac.dto.BojVerificationDto; -import com.ryu.studyhelper.solvedac.dto.ProblemInfo; import com.ryu.studyhelper.solvedac.dto.ProblemSearchResponse; import com.ryu.studyhelper.solvedac.dto.SolvedAcUserResponse; -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; -import java.time.Duration; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; + import java.util.Map; @Component @@ -20,20 +14,7 @@ public class SolvedAcClient { private final RestClient rest; public SolvedAcClient() { -// PoolingHttpClientConnectionManager pool = PoolingHttpClientConnectionManagerBuilder.create() -// .setMaxConnTotal(200) -// .setMaxConnPerRoute(50) -// .build(); -// CloseableHttpClient http = HttpClients.custom() -// .setConnectionManager(pool) -// .evictExpiredConnections() -// .build(); -// HttpComponentsClientHttpRequestFactory rf = new HttpComponentsClientHttpRequestFactory(http); -// rf.setConnectTimeout(Duration.ofSeconds(5)); -// rf.setReadTimeout(Duration.ofSeconds(5)); - this.rest = RestClient.builder() -// .requestFactory(rf) .baseUrl("https://solved.ac/api/v3") .defaultHeader("User-Agent", "studyhelper/1.0") .build(); @@ -59,44 +40,19 @@ public SolvedAcUserResponse getUserInfo(String handle) { } - public ProblemSearchResponse searchProblems(String query, int count) { - ProblemSearchResponse resp = get("/search/problem", Map.of( - "query", query, - "sort", "random", - "direction", "asc" - ), ProblemSearchResponse.class); - - //count개 만 리턴 - var limited = resp.items().stream() - .map(ProblemInfo::withUrl) - .limit(count) - .toList(); - - return new ProblemSearchResponse(limited); - } - - - - public ProblemSearchResponse getSolvedProblemsRaw(String handle) { - return get("/search/problem", Map.of( - "query", "s@" + handle, - "sort", "id", - "direction", "asc" - ), ProblemSearchResponse.class); - } - /** - * 특정 사용자가 특정 문제를 풀었는지 확인 - * @param handle 사용자 핸들 - * @param problemId 문제 번호 - * @return 해결 여부 + * 문제 검색 API 호출 (순수 HTTP 호출만 담당) + * @param query 검색 쿼리 + * @param sort 정렬 기준 (id, level, title, solved, random 등) + * @param direction 정렬 방향 (asc, desc) + * @return API 응답 원본 */ - public boolean hasUserSolvedProblem(String handle, Long problemId) { - // solved.ac 쿼리에서 조건은 공백으로 구분 (URL에서 +는 공백으로 해석됨) - ProblemSearchResponse resp = get("/search/problem", Map.of( - "query", "id:" + problemId + " s@" + handle + public ProblemSearchResponse searchProblems(String query, String sort, String direction) { + return get("/search/problem", Map.of( + "query", query, + "sort", sort, + "direction", direction ), ProblemSearchResponse.class); - return resp.items() != null && !resp.items().isEmpty(); } /** From 9eacddc48124c7d19ce544721a4a77e958503356 Mon Sep 17 00:00:00 2001 From: ryuwldnjs Date: Mon, 2 Feb 2026 01:35:38 +0900 Subject: [PATCH 2/8] =?UTF-8?q?solvedac:=20Service=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=EB=B0=8F=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 패키지 구조 - api/ → client/ 폴더명 변경 (역할 명확화) ## SolvedAcService 개선 - 오버로드 메서드 제거 → 단일 메서드로 통합 - 예외 처리 일관성 추가 (try-catch) - null 체크 일관성 추가 - 기본 난이도 범위 변경: 골드(11-15) → 전체(1-30) - private 메서드 인라인화 (depth 3 → 2) - 로그 레벨 조정: 유저 못 찾음 warn → info ## ProblemService 개선 - 오버로드 메서드 정리 --- .../problem/service/ProblemService.java | 29 ++-- .../studyhelper/solvedac/SolvedAcService.java | 149 +++++++----------- .../{api => client}/SolvedAcClient.java | 2 +- 3 files changed, 63 insertions(+), 117 deletions(-) rename src/main/java/com/ryu/studyhelper/solvedac/{api => client}/SolvedAcClient.java (97%) diff --git a/src/main/java/com/ryu/studyhelper/problem/service/ProblemService.java b/src/main/java/com/ryu/studyhelper/problem/service/ProblemService.java index 1f7006d..17b4f39 100644 --- a/src/main/java/com/ryu/studyhelper/problem/service/ProblemService.java +++ b/src/main/java/com/ryu/studyhelper/problem/service/ProblemService.java @@ -27,33 +27,22 @@ public class ProblemService { private final MailSendService mailSendService; /** - * 핸들 목록을 기반으로 문제 추천 - */ - public List recommend(List handles, int count) { - return solvedAcService.recommendUnsolvedProblems(handles, count); - } - - /** - * 핸들 목록과 난이도 범위를 기반으로 문제 추천 - */ - public List recommend(List handles, int count, Integer minLevel, Integer maxLevel) { - return solvedAcService.recommendUnsolvedProblems(handles, count, minLevel, maxLevel); - } - - /** - * 핸들 목록, 난이도 범위, 태그 필터를 기반으로 문제 추천 + * 문제 추천 */ public List recommend(List handles, int count, Integer minLevel, Integer maxLevel, List tagKeys) { return solvedAcService.recommendUnsolvedProblems(handles, count, minLevel, maxLevel, tagKeys); } - - public List recommend(ProblemRecommendRequest request, int count) { - return recommend(request.handles(), count); + return recommend(request.handles(), count, null, null, null); } - public List recommend(String handle , int count) { - return recommend(List.of(handle), count); + + public List recommend(String handle, int count) { + return recommend(List.of(handle), count, null, null, null); + } + + public List recommend(List handles, int count) { + return recommend(handles, count, null, null, null); } // 팀원 전체 문제 추천 diff --git a/src/main/java/com/ryu/studyhelper/solvedac/SolvedAcService.java b/src/main/java/com/ryu/studyhelper/solvedac/SolvedAcService.java index 4f1d87f..bd9ef35 100644 --- a/src/main/java/com/ryu/studyhelper/solvedac/SolvedAcService.java +++ b/src/main/java/com/ryu/studyhelper/solvedac/SolvedAcService.java @@ -2,32 +2,34 @@ import com.ryu.studyhelper.common.enums.CustomResponseStatus; import com.ryu.studyhelper.common.exception.CustomException; -import com.ryu.studyhelper.solvedac.api.SolvedAcClient; +import com.ryu.studyhelper.solvedac.client.SolvedAcClient; import com.ryu.studyhelper.solvedac.dto.ProblemInfo; import com.ryu.studyhelper.solvedac.dto.ProblemSearchResponse; import com.ryu.studyhelper.solvedac.dto.SolvedAcUserResponse; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; +import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.StringJoiner; @Service @Slf4j +@RequiredArgsConstructor public class SolvedAcService { - private final SolvedAcClient solvedAcClient; + private static final int MIN_LEVEL = 1; + private static final int MAX_LEVEL = 30; + private static final int MIN_SOLVED_COUNT = 1000; - public SolvedAcService(SolvedAcClient solvedAcClient) { - this.solvedAcClient = solvedAcClient; - } + private final SolvedAcClient solvedAcClient; public SolvedAcUserResponse getUserInfo(String handle) { try { return solvedAcClient.getUserInfo(handle); } catch (HttpClientErrorException.NotFound e) { - log.warn("solved.ac user not found: {}", handle); + log.info("solved.ac user not found: {}", handle); throw new CustomException(CustomResponseStatus.SOLVED_AC_USER_NOT_FOUND); } catch (Exception e) { log.error("Failed to fetch user info from solved.ac: {}", handle, e); @@ -36,120 +38,75 @@ public SolvedAcUserResponse getUserInfo(String handle) { } /** - * 주어진 사용자 핸들에 대해, 1000명이상이 푼 골드문제중 풀지 않은 문제를 추천합니다. + * 풀지 않은 문제를 추천합니다. * @param handles 추천할 사용자 핸들 목록 * @param count 추천할 문제 개수 - * @return 추천된 문제 목록 + * @param minLevel 최소 난이도 (1~30, null이면 1) + * @param maxLevel 최대 난이도 (1~30, null이면 30) + * @param tagKeys 포함할 태그 키 목록 (null 또는 빈 목록이면 필터 없음) */ - public List recommendUnsolvedProblems(List handles, int count) { - return recommendUnsolvedProblems(handles, count, null, null); + public List recommendUnsolvedProblems(List handles, int count, + Integer minLevel, Integer maxLevel, + List tagKeys) { + try { + String query = buildRecommendQuery(handles, minLevel, maxLevel, tagKeys); + log.debug("solved.ac 추천 쿼리: {}", query); + + ProblemSearchResponse response = solvedAcClient.searchProblems(query, "random", "asc"); + if (response.items() == null) { + return List.of(); + } + + return response.items().stream() + .map(ProblemInfo::withUrl) + .limit(count) + .toList(); + } catch (Exception e) { + log.error("Failed to recommend problems for handles: {}", handles, e); + throw new CustomException(CustomResponseStatus.SOLVED_AC_API_ERROR); + } } - /** - * 주어진 사용자 핸들과 난이도 범위에 대해 풀지 않은 문제를 추천합니다. - * @param handles 추천할 사용자 핸들 목록 - * @param count 추천할 문제 개수 - * @param minLevel 최소 난이도 (1~30, null이면 기본값 적용) - * @param maxLevel 최대 난이도 (1~30, null이면 기본값 적용) - * @return 추천된 문제 목록 - */ - public List recommendUnsolvedProblems(List handles, int count, Integer minLevel, Integer maxLevel) { - return recommendUnsolvedProblems(handles, count, minLevel, maxLevel, null); - } + private String buildRecommendQuery(List handles, Integer minLevel, Integer maxLevel, List tagKeys) { + List conditions = new ArrayList<>(); - /** - * 주어진 사용자 핸들, 난이도 범위, 태그 필터에 대해 풀지 않은 문제를 추천합니다. - * @param handles 추천할 사용자 핸들 목록 - * @param count 추천할 문제 개수 - * @param minLevel 최소 난이도 (1~30, null이면 기본값 적용) - * @param maxLevel 최대 난이도 (1~30, null이면 기본값 적용) - * @param tagKeys 포함할 태그 키 목록 (null 또는 빈 목록이면 필터 없음) - * @return 추천된 문제 목록 - */ - public List recommendUnsolvedProblems(List handles, int count, Integer minLevel, Integer maxLevel, List tagKeys) { - // 난이도 범위 설정 (기본값: 골드 5 ~ 골드 1) - String levelRange = buildLevelRange(minLevel, maxLevel); - - // 태그 필터 생성 (예: "(tag:dp|tag:greedy)") - String tagFilter = buildTagFilter(tagKeys); - - // 쿼리 조합 - Stream baseQuery = Stream.of(levelRange, "s#1000..", "lang:ko"); - Stream userExclusions = handles.stream().map(h -> "!s@" + h); - - String query; - if (tagFilter != null) { - query = Stream.concat( - Stream.concat(baseQuery, Stream.of(tagFilter)), - userExclusions - ).collect(Collectors.joining("+")); - } else { - query = Stream.concat(baseQuery, userExclusions) - .collect(Collectors.joining("+")); - } + // 난이도 범위 + int min = (minLevel != null && minLevel >= MIN_LEVEL && minLevel <= MAX_LEVEL) ? minLevel : MIN_LEVEL; + int max = (maxLevel != null && maxLevel >= MIN_LEVEL && maxLevel <= MAX_LEVEL) ? maxLevel : MAX_LEVEL; + conditions.add(String.format("*%d..%d", Math.min(min, max), Math.max(min, max))); - log.debug("solved.ac 추천 쿼리: {}", query); - ProblemSearchResponse response = solvedAcClient.searchProblems(query, "random", "asc"); - return response.items().stream() - .map(ProblemInfo::withUrl) - .limit(count) // Service에서 개수 제한 - .toList(); - } + // 기본 조건: 1000명 이상 풀이, 한국어 + conditions.add("s#" + MIN_SOLVED_COUNT + ".."); + conditions.add("lang:ko"); - /** - * 난이도 범위 쿼리 문자열 생성 - */ - private String buildLevelRange(Integer minLevel, Integer maxLevel) { - // 기본값: 골드 5(11) ~ 골드 1(15) - int min = (minLevel != null && minLevel >= 1 && minLevel <= 30) ? minLevel : 11; - int max = (maxLevel != null && maxLevel >= 1 && maxLevel <= 30) ? maxLevel : 15; - - // 범위 검증 - if (min > max) { - min = 11; - max = 15; + // 태그 필터 (선택) + if (tagKeys != null && !tagKeys.isEmpty()) { + conditions.add(buildTagFilter(tagKeys)); } - return String.format("*%d..%d", min, max); + // 사용자 제외 조건 + handles.forEach(h -> conditions.add("!s@" + h)); + + return String.join("+", conditions); } - /** - * 태그 필터 쿼리 문자열 생성 - * 예: ["dp", "greedy"] → "(tag:dp|tag:greedy)" - * @param tagKeys 태그 키 목록 - * @return 태그 필터 문자열 (빈 목록이면 null) - */ private String buildTagFilter(List tagKeys) { - if (tagKeys == null || tagKeys.isEmpty()) { - return null; - } - - String tagConditions = tagKeys.stream() - .map(key -> "tag:" + key) - .collect(Collectors.joining("|")); - - return "(" + tagConditions + ")"; + StringJoiner joiner = new StringJoiner("|", "(", ")"); + tagKeys.forEach(key -> joiner.add("tag:" + key)); + return joiner.toString(); } /** * 특정 사용자가 특정 문제를 풀었는지 확인 - * @param handle 사용자 핸들 - * @param problemId 문제 번호 - * @return 해결 여부 */ public boolean hasUserSolvedProblem(String handle, Long problemId) { try { - // 쿼리 조합: 문제 ID와 사용자 해결 여부 조건 String query = "id:" + problemId + " s@" + handle; ProblemSearchResponse response = solvedAcClient.searchProblems(query, "id", "asc"); - // 결과 판단: 검색 결과가 있으면 해결한 것 return response.items() != null && !response.items().isEmpty(); } catch (Exception e) { log.error("Failed to check if user {} solved problem {}", handle, problemId, e); throw new CustomException(CustomResponseStatus.SOLVED_AC_API_ERROR); } } - - - } \ No newline at end of file diff --git a/src/main/java/com/ryu/studyhelper/solvedac/api/SolvedAcClient.java b/src/main/java/com/ryu/studyhelper/solvedac/client/SolvedAcClient.java similarity index 97% rename from src/main/java/com/ryu/studyhelper/solvedac/api/SolvedAcClient.java rename to src/main/java/com/ryu/studyhelper/solvedac/client/SolvedAcClient.java index 45bdd5e..25c0d19 100644 --- a/src/main/java/com/ryu/studyhelper/solvedac/api/SolvedAcClient.java +++ b/src/main/java/com/ryu/studyhelper/solvedac/client/SolvedAcClient.java @@ -1,4 +1,4 @@ -package com.ryu.studyhelper.solvedac.api; +package com.ryu.studyhelper.solvedac.client; import com.ryu.studyhelper.solvedac.dto.BojVerificationDto; import com.ryu.studyhelper.solvedac.dto.ProblemSearchResponse; From 8a57fa93431b76034b0cb4f8d6a90fce6ceecf4f Mon Sep 17 00:00:00 2001 From: ryuwldnjs Date: Mon, 2 Feb 2026 01:36:39 +0900 Subject: [PATCH 3/8] =?UTF-8?q?refactor:=20BojVerificationFacade=20import?= =?UTF-8?q?=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(api=20=E2=86=92?= =?UTF-8?q?=20client)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyhelper/member/verification/BojVerificationFacade.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/ryu/studyhelper/member/verification/BojVerificationFacade.java b/src/main/java/com/ryu/studyhelper/member/verification/BojVerificationFacade.java index 0a6ba33..9936cbb 100644 --- a/src/main/java/com/ryu/studyhelper/member/verification/BojVerificationFacade.java +++ b/src/main/java/com/ryu/studyhelper/member/verification/BojVerificationFacade.java @@ -6,7 +6,7 @@ import com.ryu.studyhelper.member.verification.dto.GenerateHashResponse; import com.ryu.studyhelper.member.verification.dto.VerifyBojResponse; import com.ryu.studyhelper.member.domain.Member; -import com.ryu.studyhelper.solvedac.api.SolvedAcClient; +import com.ryu.studyhelper.solvedac.client.SolvedAcClient; import com.ryu.studyhelper.solvedac.dto.BojVerificationDto; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; From 27d9a09561d9389b476e441a427dc03d5bf4bd7c Mon Sep 17 00:00:00 2001 From: ryuwldnjs Date: Sun, 8 Feb 2026 19:06:51 +0900 Subject: [PATCH 4/8] =?UTF-8?q?solvedac:=20Service=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SolvedAcService → SolvedAcClient (@Service → @Component) - SolvedAcClient → SolvedAcRestClient (client/ 하위 유지) - BojVerificationDto → SolvedAcUserBioResponse - SolvedAcClient에 getUserBio() 메서드 추가 (예외 처리 포함) - BojVerificationFacade: RestClient 직접 호출 → SolvedAcClient 경유 - MemberService 필드명 solvedacService → solvedAcClient 통일 - solvedac/ → infrastructure/solvedac/ 패키지 이동 - 전체 import 경로 업데이트 --- .../solvedac/SolvedAcClient.java} | 40 +++++++++++++------ .../solvedac/client/SolvedAcRestClient.java} | 16 ++++---- .../solvedac/dto/ProblemInfo.java | 2 +- .../solvedac/dto/ProblemSearchResponse.java | 2 +- .../solvedac/dto/SolvedAcTagInfo.java | 2 +- .../solvedac/dto/SolvedAcUserBioResponse.java | 11 +++++ .../solvedac/dto/SolvedAcUserResponse.java | 2 +- .../dto/SolvedAcVerificationResponse.java | 2 +- .../ryu/studyhelper/member/MemberService.java | 8 ++-- .../verification/BojVerificationFacade.java | 6 +-- .../problem/ProblemController.java | 2 +- .../dto/TeamProblemRecommendResponse.java | 2 +- .../problem/service/ProblemService.java | 8 ++-- .../problem/service/ProblemSyncService.java | 4 +- .../recommendation/RecommendationService.java | 2 +- .../solvedac/dto/BojVerificationDto.java | 12 ------ .../studyhelper/member/MemberServiceTest.java | 12 +++--- 17 files changed, 74 insertions(+), 59 deletions(-) rename src/main/java/com/ryu/studyhelper/{solvedac/SolvedAcService.java => infrastructure/solvedac/SolvedAcClient.java} (73%) rename src/main/java/com/ryu/studyhelper/{solvedac/client/SolvedAcClient.java => infrastructure/solvedac/client/SolvedAcRestClient.java} (77%) rename src/main/java/com/ryu/studyhelper/{ => infrastructure}/solvedac/dto/ProblemInfo.java (93%) rename src/main/java/com/ryu/studyhelper/{ => infrastructure}/solvedac/dto/ProblemSearchResponse.java (75%) rename src/main/java/com/ryu/studyhelper/{ => infrastructure}/solvedac/dto/SolvedAcTagInfo.java (96%) create mode 100644 src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/SolvedAcUserBioResponse.java rename src/main/java/com/ryu/studyhelper/{ => infrastructure}/solvedac/dto/SolvedAcUserResponse.java (85%) rename src/main/java/com/ryu/studyhelper/{ => infrastructure}/solvedac/dto/SolvedAcVerificationResponse.java (73%) delete mode 100644 src/main/java/com/ryu/studyhelper/solvedac/dto/BojVerificationDto.java diff --git a/src/main/java/com/ryu/studyhelper/solvedac/SolvedAcService.java b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/SolvedAcClient.java similarity index 73% rename from src/main/java/com/ryu/studyhelper/solvedac/SolvedAcService.java rename to src/main/java/com/ryu/studyhelper/infrastructure/solvedac/SolvedAcClient.java index bd9ef35..d2514e5 100644 --- a/src/main/java/com/ryu/studyhelper/solvedac/SolvedAcService.java +++ b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/SolvedAcClient.java @@ -1,33 +1,34 @@ -package com.ryu.studyhelper.solvedac; +package com.ryu.studyhelper.infrastructure.solvedac; import com.ryu.studyhelper.common.enums.CustomResponseStatus; import com.ryu.studyhelper.common.exception.CustomException; -import com.ryu.studyhelper.solvedac.client.SolvedAcClient; -import com.ryu.studyhelper.solvedac.dto.ProblemInfo; -import com.ryu.studyhelper.solvedac.dto.ProblemSearchResponse; -import com.ryu.studyhelper.solvedac.dto.SolvedAcUserResponse; +import com.ryu.studyhelper.infrastructure.solvedac.client.SolvedAcRestClient; +import com.ryu.studyhelper.infrastructure.solvedac.dto.SolvedAcUserBioResponse; +import com.ryu.studyhelper.infrastructure.solvedac.dto.ProblemInfo; +import com.ryu.studyhelper.infrastructure.solvedac.dto.ProblemSearchResponse; +import com.ryu.studyhelper.infrastructure.solvedac.dto.SolvedAcUserResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import org.springframework.web.client.HttpClientErrorException; import java.util.ArrayList; import java.util.List; import java.util.StringJoiner; -@Service +@Component @Slf4j @RequiredArgsConstructor -public class SolvedAcService { +public class SolvedAcClient { private static final int MIN_LEVEL = 1; private static final int MAX_LEVEL = 30; private static final int MIN_SOLVED_COUNT = 1000; - private final SolvedAcClient solvedAcClient; + private final SolvedAcRestClient solvedAcRestClient; public SolvedAcUserResponse getUserInfo(String handle) { try { - return solvedAcClient.getUserInfo(handle); + return solvedAcRestClient.getUserInfo(handle); } catch (HttpClientErrorException.NotFound e) { log.info("solved.ac user not found: {}", handle); throw new CustomException(CustomResponseStatus.SOLVED_AC_USER_NOT_FOUND); @@ -52,7 +53,7 @@ public List recommendUnsolvedProblems(List handles, int cou String query = buildRecommendQuery(handles, minLevel, maxLevel, tagKeys); log.debug("solved.ac 추천 쿼리: {}", query); - ProblemSearchResponse response = solvedAcClient.searchProblems(query, "random", "asc"); + ProblemSearchResponse response = solvedAcRestClient.searchProblems(query, "random", "asc"); if (response.items() == null) { return List.of(); } @@ -96,13 +97,28 @@ private String buildTagFilter(List tagKeys) { return joiner.toString(); } + /** + * 백준 핸들 인증용 사용자 bio 조회 + */ + public SolvedAcUserBioResponse getUserBio(String handle) { + try { + return solvedAcRestClient.getUserBio(handle); + } catch (HttpClientErrorException.NotFound e) { + log.info("solved.ac user not found: {}", handle); + throw new CustomException(CustomResponseStatus.SOLVED_AC_USER_NOT_FOUND); + } catch (Exception e) { + log.error("Failed to fetch user bio from solved.ac: {}", handle, e); + throw new CustomException(CustomResponseStatus.SOLVED_AC_API_ERROR); + } + } + /** * 특정 사용자가 특정 문제를 풀었는지 확인 */ public boolean hasUserSolvedProblem(String handle, Long problemId) { try { String query = "id:" + problemId + " s@" + handle; - ProblemSearchResponse response = solvedAcClient.searchProblems(query, "id", "asc"); + ProblemSearchResponse response = solvedAcRestClient.searchProblems(query, "id", "asc"); return response.items() != null && !response.items().isEmpty(); } catch (Exception e) { log.error("Failed to check if user {} solved problem {}", handle, problemId, e); diff --git a/src/main/java/com/ryu/studyhelper/solvedac/client/SolvedAcClient.java b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/client/SolvedAcRestClient.java similarity index 77% rename from src/main/java/com/ryu/studyhelper/solvedac/client/SolvedAcClient.java rename to src/main/java/com/ryu/studyhelper/infrastructure/solvedac/client/SolvedAcRestClient.java index 25c0d19..f2cb786 100644 --- a/src/main/java/com/ryu/studyhelper/solvedac/client/SolvedAcClient.java +++ b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/client/SolvedAcRestClient.java @@ -1,8 +1,8 @@ -package com.ryu.studyhelper.solvedac.client; +package com.ryu.studyhelper.infrastructure.solvedac.client; -import com.ryu.studyhelper.solvedac.dto.BojVerificationDto; -import com.ryu.studyhelper.solvedac.dto.ProblemSearchResponse; -import com.ryu.studyhelper.solvedac.dto.SolvedAcUserResponse; +import com.ryu.studyhelper.infrastructure.solvedac.dto.SolvedAcUserBioResponse; +import com.ryu.studyhelper.infrastructure.solvedac.dto.ProblemSearchResponse; +import com.ryu.studyhelper.infrastructure.solvedac.dto.SolvedAcUserResponse; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; @@ -10,10 +10,10 @@ import java.util.Map; @Component -public class SolvedAcClient { +public class SolvedAcRestClient { private final RestClient rest; - public SolvedAcClient() { + public SolvedAcRestClient() { this.rest = RestClient.builder() .baseUrl("https://solved.ac/api/v3") .defaultHeader("User-Agent", "studyhelper/1.0") @@ -60,8 +60,8 @@ public ProblemSearchResponse searchProblems(String query, String sort, String di * @param handle 백준 핸들 * @return 핸들과 bio 정보 */ - public BojVerificationDto getUserBio(String handle) { - return get("/user/show", Map.of("handle", handle), BojVerificationDto.class); + public SolvedAcUserBioResponse getUserBio(String handle) { + return get("/user/show", Map.of("handle", handle), SolvedAcUserBioResponse.class); } } \ No newline at end of file diff --git a/src/main/java/com/ryu/studyhelper/solvedac/dto/ProblemInfo.java b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/ProblemInfo.java similarity index 93% rename from src/main/java/com/ryu/studyhelper/solvedac/dto/ProblemInfo.java rename to src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/ProblemInfo.java index a149915..a769696 100644 --- a/src/main/java/com/ryu/studyhelper/solvedac/dto/ProblemInfo.java +++ b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/ProblemInfo.java @@ -1,4 +1,4 @@ -package com.ryu.studyhelper.solvedac.dto; +package com.ryu.studyhelper.infrastructure.solvedac.dto; import com.fasterxml.jackson.annotation.JsonProperty; import com.ryu.studyhelper.common.util.ProblemUrlUtils; diff --git a/src/main/java/com/ryu/studyhelper/solvedac/dto/ProblemSearchResponse.java b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/ProblemSearchResponse.java similarity index 75% rename from src/main/java/com/ryu/studyhelper/solvedac/dto/ProblemSearchResponse.java rename to src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/ProblemSearchResponse.java index 3f044d8..bcd30f7 100644 --- a/src/main/java/com/ryu/studyhelper/solvedac/dto/ProblemSearchResponse.java +++ b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/ProblemSearchResponse.java @@ -1,4 +1,4 @@ -package com.ryu.studyhelper.solvedac.dto; +package com.ryu.studyhelper.infrastructure.solvedac.dto; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/ryu/studyhelper/solvedac/dto/SolvedAcTagInfo.java b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/SolvedAcTagInfo.java similarity index 96% rename from src/main/java/com/ryu/studyhelper/solvedac/dto/SolvedAcTagInfo.java rename to src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/SolvedAcTagInfo.java index 63f51ee..4da163a 100644 --- a/src/main/java/com/ryu/studyhelper/solvedac/dto/SolvedAcTagInfo.java +++ b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/SolvedAcTagInfo.java @@ -1,4 +1,4 @@ -package com.ryu.studyhelper.solvedac.dto; +package com.ryu.studyhelper.infrastructure.solvedac.dto; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/SolvedAcUserBioResponse.java b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/SolvedAcUserBioResponse.java new file mode 100644 index 0000000..28199ad --- /dev/null +++ b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/SolvedAcUserBioResponse.java @@ -0,0 +1,11 @@ +package com.ryu.studyhelper.infrastructure.solvedac.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * solved.ac 사용자 bio 응답 DTO + */ +public record SolvedAcUserBioResponse( + @JsonProperty("handle") String handle, + @JsonProperty("bio") String bio +) {} \ No newline at end of file diff --git a/src/main/java/com/ryu/studyhelper/solvedac/dto/SolvedAcUserResponse.java b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/SolvedAcUserResponse.java similarity index 85% rename from src/main/java/com/ryu/studyhelper/solvedac/dto/SolvedAcUserResponse.java rename to src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/SolvedAcUserResponse.java index 9424a52..a7c4059 100644 --- a/src/main/java/com/ryu/studyhelper/solvedac/dto/SolvedAcUserResponse.java +++ b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/SolvedAcUserResponse.java @@ -1,4 +1,4 @@ -package com.ryu.studyhelper.solvedac.dto; +package com.ryu.studyhelper.infrastructure.solvedac.dto; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/ryu/studyhelper/solvedac/dto/SolvedAcVerificationResponse.java b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/SolvedAcVerificationResponse.java similarity index 73% rename from src/main/java/com/ryu/studyhelper/solvedac/dto/SolvedAcVerificationResponse.java rename to src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/SolvedAcVerificationResponse.java index 8f9915d..27c63f1 100644 --- a/src/main/java/com/ryu/studyhelper/solvedac/dto/SolvedAcVerificationResponse.java +++ b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/dto/SolvedAcVerificationResponse.java @@ -1,4 +1,4 @@ -package com.ryu.studyhelper.solvedac.dto; +package com.ryu.studyhelper.infrastructure.solvedac.dto; public record SolvedAcVerificationResponse( String handle, diff --git a/src/main/java/com/ryu/studyhelper/member/MemberService.java b/src/main/java/com/ryu/studyhelper/member/MemberService.java index 2b2674d..b90b95b 100644 --- a/src/main/java/com/ryu/studyhelper/member/MemberService.java +++ b/src/main/java/com/ryu/studyhelper/member/MemberService.java @@ -5,6 +5,7 @@ import com.ryu.studyhelper.config.security.jwt.JwtUtil; import com.ryu.studyhelper.infrastructure.mail.MailSendService; import com.ryu.studyhelper.infrastructure.mail.dto.MailHtmlSendDto; +import com.ryu.studyhelper.infrastructure.solvedac.SolvedAcClient; import com.ryu.studyhelper.member.domain.Member; import com.ryu.studyhelper.member.domain.MemberSolvedProblem; import com.ryu.studyhelper.member.dto.response.DailySolvedResponse; @@ -14,7 +15,6 @@ import com.ryu.studyhelper.member.repository.MemberSolvedProblemRepository; import com.ryu.studyhelper.problem.repository.ProblemRepository; import com.ryu.studyhelper.problem.domain.Problem; -import com.ryu.studyhelper.solvedac.SolvedAcService; import com.ryu.studyhelper.team.repository.TeamMemberRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -41,7 +41,7 @@ public class MemberService { private final ProblemRepository problemRepository; private final MemberSolvedProblemRepository memberSolvedProblemRepository; private final TeamMemberRepository teamMemberRepository; - private final SolvedAcService solvedacService; + private final SolvedAcClient solvedAcClient; private final JwtUtil jwtUtil; private final MailSendService mailSendService; private final Clock clock; @@ -83,7 +83,7 @@ public List getVerifiedHandles() { */ public Member verifySolvedAcHandle(Long memberId, String handle) { // 1. solved.ac에 존재하는지 확인 (예외 발생 시 검증 실패) - solvedacService.getUserInfo(handle); + solvedAcClient.getUserInfo(handle); // 2. 회원 엔티티에 핸들 저장 (중복 허용, isVerified는 false 유지) Member member = getById(memberId); @@ -191,7 +191,7 @@ public void verifyProblemSolved(Long memberId, Long problemId) { } // 5. solved.ac API로 실제 해결 여부 검증 - boolean isSolved = solvedacService.hasUserSolvedProblem(member.getHandle(), problemId); + boolean isSolved = solvedAcClient.hasUserSolvedProblem(member.getHandle(), problemId); if (!isSolved) { throw new CustomException(CustomResponseStatus.PROBLEM_NOT_SOLVED_YET); diff --git a/src/main/java/com/ryu/studyhelper/member/verification/BojVerificationFacade.java b/src/main/java/com/ryu/studyhelper/member/verification/BojVerificationFacade.java index 9936cbb..c3876a7 100644 --- a/src/main/java/com/ryu/studyhelper/member/verification/BojVerificationFacade.java +++ b/src/main/java/com/ryu/studyhelper/member/verification/BojVerificationFacade.java @@ -2,12 +2,12 @@ import com.ryu.studyhelper.common.enums.CustomResponseStatus; import com.ryu.studyhelper.common.exception.CustomException; +import com.ryu.studyhelper.infrastructure.solvedac.SolvedAcClient; import com.ryu.studyhelper.member.repository.MemberRepository; import com.ryu.studyhelper.member.verification.dto.GenerateHashResponse; import com.ryu.studyhelper.member.verification.dto.VerifyBojResponse; import com.ryu.studyhelper.member.domain.Member; -import com.ryu.studyhelper.solvedac.client.SolvedAcClient; -import com.ryu.studyhelper.solvedac.dto.BojVerificationDto; +import com.ryu.studyhelper.infrastructure.solvedac.dto.SolvedAcUserBioResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -70,7 +70,7 @@ public VerifyBojResponse verifyBojHandle(Long memberId, String handle) { .orElseThrow(() -> new CustomException(CustomResponseStatus.VERIFICATION_HASH_NOT_FOUND)); // 4. solved.ac API에서 사용자 bio 조회 - BojVerificationDto userInfo; + SolvedAcUserBioResponse userInfo; try { userInfo = solvedAcClient.getUserBio(handle); } catch (Exception e) { diff --git a/src/main/java/com/ryu/studyhelper/problem/ProblemController.java b/src/main/java/com/ryu/studyhelper/problem/ProblemController.java index d845b16..21c3876 100644 --- a/src/main/java/com/ryu/studyhelper/problem/ProblemController.java +++ b/src/main/java/com/ryu/studyhelper/problem/ProblemController.java @@ -7,7 +7,7 @@ import com.ryu.studyhelper.problem.dto.ProblemRecommendRequest; import com.ryu.studyhelper.problem.dto.TeamProblemRecommendResponse; import com.ryu.studyhelper.problem.service.ProblemService; -import com.ryu.studyhelper.solvedac.dto.ProblemInfo; +import com.ryu.studyhelper.infrastructure.solvedac.dto.ProblemInfo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import jakarta.validation.Valid; diff --git a/src/main/java/com/ryu/studyhelper/problem/dto/TeamProblemRecommendResponse.java b/src/main/java/com/ryu/studyhelper/problem/dto/TeamProblemRecommendResponse.java index f8f1664..94bc359 100644 --- a/src/main/java/com/ryu/studyhelper/problem/dto/TeamProblemRecommendResponse.java +++ b/src/main/java/com/ryu/studyhelper/problem/dto/TeamProblemRecommendResponse.java @@ -1,6 +1,6 @@ package com.ryu.studyhelper.problem.dto; -import com.ryu.studyhelper.solvedac.dto.ProblemInfo; +import com.ryu.studyhelper.infrastructure.solvedac.dto.ProblemInfo; import java.util.List; diff --git a/src/main/java/com/ryu/studyhelper/problem/service/ProblemService.java b/src/main/java/com/ryu/studyhelper/problem/service/ProblemService.java index 17b4f39..82b3fd4 100644 --- a/src/main/java/com/ryu/studyhelper/problem/service/ProblemService.java +++ b/src/main/java/com/ryu/studyhelper/problem/service/ProblemService.java @@ -3,10 +3,10 @@ import com.ryu.studyhelper.common.enums.CustomResponseStatus; import com.ryu.studyhelper.common.exception.CustomException; import com.ryu.studyhelper.infrastructure.mail.MailSendService; +import com.ryu.studyhelper.infrastructure.solvedac.SolvedAcClient; import com.ryu.studyhelper.problem.dto.ProblemRecommendRequest; import com.ryu.studyhelper.problem.dto.TeamProblemRecommendResponse; -import com.ryu.studyhelper.solvedac.SolvedAcService; -import com.ryu.studyhelper.solvedac.dto.ProblemInfo; +import com.ryu.studyhelper.infrastructure.solvedac.dto.ProblemInfo; import com.ryu.studyhelper.team.repository.TeamMemberRepository; import com.ryu.studyhelper.team.repository.TeamRepository; import com.ryu.studyhelper.infrastructure.mail.dto.MailTxtSendDto; @@ -21,7 +21,7 @@ @Service @Transactional public class ProblemService { - private final SolvedAcService solvedAcService; + private final SolvedAcClient solvedAcClient; private final TeamRepository teamRepository; private final TeamMemberRepository teamMemberRepository; private final MailSendService mailSendService; @@ -30,7 +30,7 @@ public class ProblemService { * 문제 추천 */ public List recommend(List handles, int count, Integer minLevel, Integer maxLevel, List tagKeys) { - return solvedAcService.recommendUnsolvedProblems(handles, count, minLevel, maxLevel, tagKeys); + return solvedAcClient.recommendUnsolvedProblems(handles, count, minLevel, maxLevel, tagKeys); } public List recommend(ProblemRecommendRequest request, int count) { diff --git a/src/main/java/com/ryu/studyhelper/problem/service/ProblemSyncService.java b/src/main/java/com/ryu/studyhelper/problem/service/ProblemSyncService.java index 8c8ac14..026f1d5 100644 --- a/src/main/java/com/ryu/studyhelper/problem/service/ProblemSyncService.java +++ b/src/main/java/com/ryu/studyhelper/problem/service/ProblemSyncService.java @@ -6,8 +6,8 @@ import com.ryu.studyhelper.problem.domain.Tag; import com.ryu.studyhelper.problem.repository.ProblemTagRepository; import com.ryu.studyhelper.problem.repository.TagRepository; -import com.ryu.studyhelper.solvedac.dto.ProblemInfo; -import com.ryu.studyhelper.solvedac.dto.SolvedAcTagInfo; +import com.ryu.studyhelper.infrastructure.solvedac.dto.ProblemInfo; +import com.ryu.studyhelper.infrastructure.solvedac.dto.SolvedAcTagInfo; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/ryu/studyhelper/recommendation/RecommendationService.java b/src/main/java/com/ryu/studyhelper/recommendation/RecommendationService.java index 8dd4d4f..d30d265 100644 --- a/src/main/java/com/ryu/studyhelper/recommendation/RecommendationService.java +++ b/src/main/java/com/ryu/studyhelper/recommendation/RecommendationService.java @@ -24,7 +24,7 @@ import com.ryu.studyhelper.recommendation.repository.MemberRecommendationRepository; import com.ryu.studyhelper.recommendation.repository.RecommendationProblemRepository; import com.ryu.studyhelper.recommendation.repository.RecommendationRepository; -import com.ryu.studyhelper.solvedac.dto.ProblemInfo; +import com.ryu.studyhelper.infrastructure.solvedac.dto.ProblemInfo; import com.ryu.studyhelper.team.repository.TeamMemberRepository; import com.ryu.studyhelper.team.repository.TeamRepository; import com.ryu.studyhelper.team.domain.Team; diff --git a/src/main/java/com/ryu/studyhelper/solvedac/dto/BojVerificationDto.java b/src/main/java/com/ryu/studyhelper/solvedac/dto/BojVerificationDto.java deleted file mode 100644 index 656dbd0..0000000 --- a/src/main/java/com/ryu/studyhelper/solvedac/dto/BojVerificationDto.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.ryu.studyhelper.solvedac.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * 백준 핸들 인증용 DTO - * solved.ac API에서 사용자의 bio(상태 메시지)만 가져옴 - */ -public record BojVerificationDto( - @JsonProperty("handle") String handle, - @JsonProperty("bio") String bio -) {} \ No newline at end of file diff --git a/src/test/java/com/ryu/studyhelper/member/MemberServiceTest.java b/src/test/java/com/ryu/studyhelper/member/MemberServiceTest.java index fade73f..2f5812f 100644 --- a/src/test/java/com/ryu/studyhelper/member/MemberServiceTest.java +++ b/src/test/java/com/ryu/studyhelper/member/MemberServiceTest.java @@ -8,7 +8,7 @@ import com.ryu.studyhelper.member.repository.MemberSolvedProblemRepository; import com.ryu.studyhelper.problem.repository.ProblemRepository; import com.ryu.studyhelper.problem.domain.Problem; -import com.ryu.studyhelper.solvedac.SolvedAcService; +import com.ryu.studyhelper.infrastructure.solvedac.SolvedAcClient; import com.ryu.studyhelper.team.repository.TeamMemberRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -54,7 +54,7 @@ class MemberServiceTest { private TeamMemberRepository teamMemberRepository; @Mock - private SolvedAcService solvedAcService; + private SolvedAcClient solvedAcClient; private Member member; private Problem problem; @@ -89,7 +89,7 @@ void success() { given(memberRepository.findById(1L)).willReturn(Optional.of(member)); given(problemRepository.findById(1000L)).willReturn(Optional.of(problem)); given(memberSolvedProblemRepository.existsByMemberIdAndProblemId(1L, 1000L)).willReturn(false); - given(solvedAcService.hasUserSolvedProblem("testuser", 1000L)).willReturn(true); + given(solvedAcClient.hasUserSolvedProblem("testuser", 1000L)).willReturn(true); given(memberSolvedProblemRepository.save(any(MemberSolvedProblem.class))) .willAnswer(invocation -> invocation.getArgument(0)); @@ -166,7 +166,7 @@ void fail_notSolvedYet() { given(memberRepository.findById(1L)).willReturn(Optional.of(member)); given(problemRepository.findById(1000L)).willReturn(Optional.of(problem)); given(memberSolvedProblemRepository.existsByMemberIdAndProblemId(1L, 1000L)).willReturn(false); - given(solvedAcService.hasUserSolvedProblem("testuser", 1000L)).willReturn(false); + given(solvedAcClient.hasUserSolvedProblem("testuser", 1000L)).willReturn(false); // when & then assertThatThrownBy(() -> memberService.verifyProblemSolved(1L, 1000L)) @@ -269,7 +269,7 @@ void setUp() { problemRepository, memberSolvedProblemRepository, teamMemberRepository, - solvedAcService, + solvedAcClient, null, // jwtUtil null, // mailSendService clock @@ -374,7 +374,7 @@ class GetDailySolvedRangeValidationTest { void setUp() { service = new MemberService( memberRepository, problemRepository, memberSolvedProblemRepository, - teamMemberRepository, solvedAcService, null, null, Clock.systemDefaultZone() + teamMemberRepository, solvedAcClient, null, null, Clock.systemDefaultZone() ); } From 6bc0dd6b1df6f0ce7955062dd8014420030941c6 Mon Sep 17 00:00:00 2001 From: ryuwldnjs Date: Sun, 8 Feb 2026 19:17:29 +0900 Subject: [PATCH 5/8] =?UTF-8?q?solvedac:=20fix=20ResponseType=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=EB=AA=85=20lowerCamelCase=20?= =?UTF-8?q?=EC=9C=84=EB=B0=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/solvedac/client/SolvedAcRestClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/client/SolvedAcRestClient.java b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/client/SolvedAcRestClient.java index f2cb786..3053111 100644 --- a/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/client/SolvedAcRestClient.java +++ b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/client/SolvedAcRestClient.java @@ -22,7 +22,7 @@ public SolvedAcRestClient() { - private T get(String path, Map params, Class ResponseType) { + private T get(String path, Map params, Class responseType) { return rest.get() .uri(b -> { var ub = b.path(path); @@ -31,7 +31,7 @@ private T get(String path, Map params, Class ResponseType }) .accept(MediaType.APPLICATION_JSON) .retrieve() - .body(ResponseType); + .body(responseType); } From f7d262b97cf5bb461503a26c3cdb097542e0fda0 Mon Sep 17 00:00:00 2001 From: ryuwldnjs Date: Sun, 8 Feb 2026 19:20:41 +0900 Subject: [PATCH 6/8] =?UTF-8?q?solvedac:=20=EC=BF=BC=EB=A6=AC=20=EA=B5=AC?= =?UTF-8?q?=EB=B6=84=EC=9E=90=20+=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ryu/studyhelper/infrastructure/solvedac/SolvedAcClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/SolvedAcClient.java b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/SolvedAcClient.java index d2514e5..86fa845 100644 --- a/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/SolvedAcClient.java +++ b/src/main/java/com/ryu/studyhelper/infrastructure/solvedac/SolvedAcClient.java @@ -117,7 +117,7 @@ public SolvedAcUserBioResponse getUserBio(String handle) { */ public boolean hasUserSolvedProblem(String handle, Long problemId) { try { - String query = "id:" + problemId + " s@" + handle; + String query = "id:" + problemId + "+s@" + handle; ProblemSearchResponse response = solvedAcRestClient.searchProblems(query, "id", "asc"); return response.items() != null && !response.items().isEmpty(); } catch (Exception e) { From c9325f18ad3f3dba53c60f23dc6719ab2f5d7384 Mon Sep 17 00:00:00 2001 From: ryuwldnjs Date: Sun, 8 Feb 2026 19:31:15 +0900 Subject: [PATCH 7/8] =?UTF-8?q?solvedac:=20fix=20ProblemService=20?= =?UTF-8?q?=EB=A8=B8=EC=A7=80=20=EC=B6=A9=EB=8F=8C=20=ED=9B=84=20=EC=9E=94?= =?UTF-8?q?=EC=97=AC=20import=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ryu/studyhelper/problem/service/ProblemService.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/ryu/studyhelper/problem/service/ProblemService.java b/src/main/java/com/ryu/studyhelper/problem/service/ProblemService.java index 8a6642d..ae41d7d 100644 --- a/src/main/java/com/ryu/studyhelper/problem/service/ProblemService.java +++ b/src/main/java/com/ryu/studyhelper/problem/service/ProblemService.java @@ -1,10 +1,9 @@ package com.ryu.studyhelper.problem.service; -import com.ryu.studyhelper.problem.dto.ProblemRecommendRequest; -import com.ryu.studyhelper.solvedac.SolvedAcService; import com.ryu.studyhelper.infrastructure.solvedac.SolvedAcClient; -import com.ryu.studyhelper.solvedac.dto.ProblemInfo; +import com.ryu.studyhelper.infrastructure.solvedac.dto.ProblemInfo; +import com.ryu.studyhelper.problem.dto.ProblemRecommendRequest; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; From bb71a501ad46c8294f27b4fa81159d2664365af9 Mon Sep 17 00:00:00 2001 From: ryuwldnjs Date: Sun, 8 Feb 2026 19:34:30 +0900 Subject: [PATCH 8/8] =?UTF-8?q?member:=20fix=20MemberServiceTest=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=9E=90=20=EC=9D=B8=EC=9E=90=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/com/ryu/studyhelper/member/MemberServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/ryu/studyhelper/member/MemberServiceTest.java b/src/test/java/com/ryu/studyhelper/member/MemberServiceTest.java index 5ac8756..cc72831 100644 --- a/src/test/java/com/ryu/studyhelper/member/MemberServiceTest.java +++ b/src/test/java/com/ryu/studyhelper/member/MemberServiceTest.java @@ -375,7 +375,7 @@ class GetDailySolvedRangeValidationTest { void setUp() { service = new MemberService( memberRepository, problemRepository, memberSolvedProblemRepository, - teamMemberRepository, solvedAcClient, null, null, Clock.systemDefaultZone() + teamMemberRepository, solvedAcClient, null, null, null, Clock.systemDefaultZone() ); }