Conversation
ON-79 ServerApplication 수정 - 웹 OAuth 제거 후 빈 설정 정리
Walkthrough이 PR은 OAuth2 기반 보안에서 JWT 기반 인증으로 마이그레이션하며, MongoDB 기반 아티클 관리, 퀴즈 시스템, 사용자 통계 추적, AI 백엔드 통합, Docker 오케스트레이션을 추가합니다. 또한 캐싱, CORS, 그리고 모바일 로그인 지원을 포함한 새로운 구성을 도입합니다. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
🤖 Fix all issues with AI agents
In @server/build.gradle:
- Around line 42-44: You upgraded jjwt dependency versions in build.gradle but
must not only change the version to 0.13.0 — refactor all JWT usage to follow
breaking API changes: replace mutable Claims/Header uses with
ClaimsBuilder/HeaderBuilder, construct parsers via JwtParserBuilder instead of
mutating JwtParser, replace SignatureAlgorithm usage with the new Jwts.SIG
algorithm constants, and update token build/sign method calls to the new names;
also ensure you pick >=0.12.6 (prefer 0.13.0) to avoid CVE-2024-31033 and
consult the jjwt migration guide to apply these changes across all codepaths
that create/parse/sign/verify tokens (look for usages of Claims, Header,
JwtParser, SignatureAlgorithm, and Jwts.build()/sign() sites).
In @server/netsh:
- Around line 1-8: Remove the accidentally committed diagnostic file
server/netsh from the repository and commit the removal (use git rm / stage
deletion and commit), then add an appropriate ignore pattern to .gitignore
(e.g., a pattern matching netsh or other process listing filenames) so future
process dumps aren’t tracked; if this file has sensitive data and must be purged
from history, run a history-rewrite tool (git filter-repo or BFG) targeting
server/netsh and force-push the cleaned branch.
In
@server/src/main/java/oba/backend/server/domain/quiz/controller/MyQuizController.java:
- Around line 19-27: The endpoints getSolved and getWrong accept userId as a
request parameter causing an IDOR; change both methods to obtain the
authenticated user's ID via @AuthenticationPrincipal instead of @RequestParam,
e.g. replace the Long userId param with an @AuthenticationPrincipal (your
security principal type, e.g. UserPrincipal or CustomUserDetails) and call
quizQueryService.getSolved(principal.getId()) and
quizQueryService.getWrong(principal.getId()) accordingly; remove the request
param usage and update imports and any tests to reflect the new parameter
signature.
In
@server/src/main/java/oba/backend/server/domain/quiz/repository/IncorrectArticlesRepository.java:
- Around line 9-14: The deleteByUserIdAndArticleId method in
IncorrectArticlesRepository is missing the required @Modifying annotation for
Spring Data JPA derived delete queries; update the IncorrectArticlesRepository
interface to annotate the deleteByUserIdAndArticleId(Long userId, Long
articleId) method with @Modifying (and import
org.springframework.data.jpa.repository.Modifying) and ensure the service method
that invokes this repository method is annotated with @Transactional so the
delete runs inside a transaction.
In
@server/src/main/java/oba/backend/server/domain/quiz/service/QuizResultService.java:
- Around line 19-29: saveQuizResult currently only persists articleId and
ignores QuizResultRequest.correct and QuizResultRequest.selectedOption; update
the persistence so IncorrectArticles contains and stores these fields: when
building the entity in saveQuizResult (IncorrectArticles.builder(...) in
QuizResultService.saveQuizResult) set the fields for correct and selectedOption
from request; if the IncorrectArticles entity lacks those fields, extend the
entity (add boolean correct and String selectedOption or appropriate types),
add/update repository methods if needed and create a DB migration to add the
columns, then persist via incorrectArticlesRepository.save(incorrectArticles).
In
@server/src/main/java/oba/backend/server/domain/quiz/service/QuizService.java:
- Line 1: The package declaration at the top of QuizService is misspelled as
oba.backend.server.doma.quiz.service; update it to
oba.backend.server.domain.quiz.service and update the related import statements
that reference oba.backend.server.doma.quiz.* (for symbols IncorrectQuiz,
QuizSubmitRequest, IncorrectQuizRepository) to use
oba.backend.server.domain.quiz.* so package names are consistent with the rest
of the project.
In
@server/src/main/java/oba/backend/server/global/auth/controller/AuthController.java:
- Around line 23-39: The mobileLogin endpoint currently trusts the
client-provided idToken and directly uses it to create/find a user and issue
tokens; add server-side ID token validation before calling
userService.findOrCreateUser and jwtProvider.generateTokens: validate signature,
issuer, audience (client ID), and expiration for the incoming
request.getIdToken() (e.g., using a provider-specific verifier or JWT library),
extract the authoritative subject/unique user id from the validated token to
form the identifier (do not use raw idToken), and only then call
userService.findOrCreateUser(...) and return
jwtProvider.generateTokens(user.getId(), user.getIdentifier()); on validation
failure, return an appropriate 401/invalid-token response.
In
@server/src/main/java/oba/backend/server/global/auth/controller/OAuthBridgeController.java:
- Around line 9-12: The oauthBridge() controller returns a non-existent view and
mismatches config/security and OAuth flow: create the "oauth-bridge" template
under server/src/main/resources/templates/ or change oauthBridge() to
return/redirect to the configured oauth.bridge-url, update the @GetMapping value
to match application.yml (or update application.yml to /oauth/bridge) so paths
align, add the route to SecurityConfig's permitAll() list so unauthenticated
callbacks are allowed, and modify oauthBridge(String code, String state, ...) to
accept and validate OAuth callback parameters (code, state, error) and handle
them (exchange code or redirect) per your OAuth flow.
In @server/src/main/java/oba/backend/server/global/auth/jwt/JwtProvider.java:
- Around line 31-58: The token creation currently omits a token type claim so
refresh tokens can be misused as access tokens; modify createToken (used by
createAccessToken/createRefreshToken) to add a "tokenType" claim set to "access"
or "refresh" depending on which factory called it, update
createAccessToken/createRefreshToken to pass the appropriate type flag, and
update validateToken (and any authentication filter/endpoint that calls it) to
verify the "tokenType" claim matches the expected value before accepting the
token; ensure TokenResponse still returns the two tokens unchanged.
- Around line 3-6: Update JwtProvider to be compatible with JJWT 0.13.x: bump
the JJWT dependency to 0.13.x, remove the SignatureAlgorithm parameter from the
JwtBuilder.signWith(...) call in JwtProvider (use signWith(key) or signWith(key,
Jwts.SIG.HS256) as appropriate), replace Jwts.parserBuilder().setSigningKey(key)
with Jwts.parser().verifyWith(key) in the verification logic, and change
parseClaimsJws(...) usages to parseSignedClaims(...) and use .getPayload()
instead of .getBody() when extracting claims (adjust any exception handling to
match the newer API signatures).
In @server/src/main/java/oba/backend/server/global/error/CustomException.java:
- Around line 3-4: CustomException is not usable as an exception because it
doesn't extend Exception or RuntimeException; change the class declaration to
extend RuntimeException, add standard constructors (no-arg, message,
message+cause, cause) delegating to super, and optionally add an ErrorCode field
(with constructor overloads and a getter) if you want to carry structured error
codes alongside the exception.
In
@server/src/main/java/oba/backend/server/global/error/GlobalExceptionHandler.java:
- Around line 3-4: The GlobalExceptionHandler class is empty and missing Spring
annotations and handler methods; annotate the class with @RestControllerAdvice
(or @ControllerAdvice plus @ResponseBody) and add @ExceptionHandler methods such
as a generic handler for Exception and specific handlers for known exceptions
(e.g., MethodArgumentNotValidException, EntityNotFoundException) that return
ResponseEntity or a standardized error DTO, include logging inside each handler,
and ensure handlers set appropriate HTTP status codes and response bodies so the
class actually intercepts and formats exceptions.
In @server/src/test/resources/application-test.yml:
- Around line 1-13: The test config is missing JWT, OAuth2 client, and MongoDB
settings causing bean creation failures for JwtProvider,
JwtAuthenticationFilter/SecurityConfig, CustomOAuth2UserService,
ArticleSummaryService and GptMongoRepository when ServerApplicationTests loads
the full context; fix by either (A) extending application-test.yml with the
required properties: add jwt.secret, jwt.access-token-expiration-ms,
jwt.refresh-token-expiration-ms; add minimal
spring.security.oauth2.client.registration.* entries (e.g.
client-id/client-secret/placeholders) and
spring.security.oauth2.client.provider.* as needed; and add MongoDB test URI
(spring.data.mongodb.uri) or enable an embedded MongoDB for tests, OR (B) update
ServerApplicationTests to avoid loading those real beans by adding @MockBean for
JwtProvider, CustomOAuth2UserService, GptMongoRepository/ArticleSummaryService
(or exclude Security/Mongo auto-config) so tests can load without full external
config.
🟠 Major comments (23)
server/Dockerfile-1-13 (1)
1-13:build/libs/*.jar글롭은 다중 JAR 매칭으로 Docker 빌드가 실패합니다Spring Boot Gradle의 기본 설정은 실행 가능한 bootJar와 plain JAR를 함께 생성합니다.
COPY --from=builder /app/build/libs/*.jar app.jar에서 glob이 2개 이상의 파일과 매칭되면 Docker는 단일 파일 대상으로의 다중 소스 복사를 거부하여 빌드가 실패합니다.추가로
gradle dependencies --no-daemon || true는 의존성 다운로드 실패를 무시해서 캐시 오염이나 원인 불명의 컴파일 오류를 유발할 수 있습니다.런타임 이미지는 JDK 대신 JRE를 사용하고 non-root 사용자를 추가하는 것이 표준 관행입니다.
제안 수정
FROM gradle:8.5-jdk17 AS builder WORKDIR /app COPY build.gradle settings.gradle ./ COPY gradle ./gradle -RUN gradle dependencies --no-daemon || true +RUN gradle dependencies --no-daemon COPY src ./src -RUN gradle bootJar --no-daemon +RUN gradle bootJar --no-daemon \ + && find build/libs -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' -exec cp {} /app/app.jar \; -FROM eclipse-temurin:17-jdk +FROM eclipse-temurin:17-jre WORKDIR /app -COPY --from=builder /app/build/libs/*.jar app.jar +COPY --from=builder /app/app.jar app.jar EXPOSE 8080 +RUN useradd -r -u 10001 appuser +USER appuser ENTRYPOINT ["java", "-jar", "app.jar"]docker-compose.yml-37-47 (1)
37-47: MongoDB 인증이 설정되지 않아 보안 위험이 있습니다.
ai_mongo서비스가 인증 없이 포트 27017에 노출되어 있습니다. 프로덕션 환경에서는MONGO_INITDB_ROOT_USERNAME과MONGO_INITDB_ROOT_PASSWORD환경 변수를 설정하거나, 최소한 포트 바인딩을127.0.0.1:27017:27017로 제한하는 것이 좋습니다.🔒 보안 강화를 위한 수정 제안
ai_mongo: image: mongo:7.0 container_name: ai_mongo ports: - - "27017:27017" + - "127.0.0.1:27017:27017" volumes: - mongo_data:/data/db environment: MONGO_INITDB_DATABASE: OneBitArticle + MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD} networks: - ai_networkserver/src/main/java/oba/backend/server/domain/user/entity/ProviderInfo.java-10-13 (1)
10-13: 잘못된 입력값에 대한 예외 처리가 필요합니다.
valueOf()메서드는 유효하지 않은 문자열이 전달될 경우IllegalArgumentException을 발생시킵니다. 현재 코드는 이를 처리하지 않아 런타임 오류가 발생할 수 있습니다.🛡️ 예외 처리를 추가한 수정안
public static ProviderInfo from(String providerName) { if (providerName == null) return LOCAL; - return ProviderInfo.valueOf(providerName.toUpperCase()); + try { + return ProviderInfo.valueOf(providerName.toUpperCase()); + } catch (IllegalArgumentException e) { + return LOCAL; // 또는 예외를 다시 던지거나 로깅 + } }server/src/main/java/oba/backend/server/domain/quiz/controller/QuizResultController.java-16-23 (1)
16-23: 토큰 헤더 처리의 일관성과 검증이 필요합니다.Line 21에서
token.replace("Bearer ", "")를 사용하는 방식은 다음과 같은 문제가 있습니다:
- "Bearer " 접두사가 없는 경우에도 예외가 발생하지 않아 잘못된 JWT가 서비스로 전달될 수 있습니다.
AuthController의/reissue엔드포인트(관련 스니펫 참조)는Const.BEARER_PREFIX를 사용하고startsWith()검증을 수행하는 반면, 이 코드는 그렇지 않아 일관성이 없습니다.🔧 일관된 토큰 처리 방식 제안
@PostMapping("/save") public ResponseEntity<?> saveQuizResult( @RequestHeader("Authorization") String token, @RequestBody QuizResultRequest request ) { - quizResultService.saveQuizResult(token.replace("Bearer ", ""), request); + if (token == null || !token.startsWith(Const.BEARER_PREFIX)) { + return ResponseEntity.badRequest().build(); + } + String jwt = token.substring(Const.BEARER_PREFIX.length()); + quizResultService.saveQuizResult(jwt, request); return ResponseEntity.ok().build(); }server/src/main/java/oba/backend/server/global/config/CorsConfig.java-11-23 (1)
11-23: 프로덕션 환경에서 CORS 정책 강화 필요모든 Origin을 허용하고
allowCredentials(true)를 설정하면 보안 위험이 있습니다. 최신 브라우저는allowCredentials(true)와 와일드카드 Origin 조합을 제한하며, 프로덕션 환경에서는 특정 도메인만 허용해야 합니다.🔒 환경별 CORS 설정 제안
+import org.springframework.beans.factory.annotation.Value; +import java.util.Arrays; + @Configuration public class CorsConfig { + + @Value("${cors.allowed-origins:http://localhost:3000}") + private String allowedOrigins; @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOriginPatterns("*") + .allowedOrigins(allowedOrigins.split(",")) .allowedMethods("*") .allowedHeaders("*") .allowCredentials(true); } }; } }
application.yml에서 환경별 설정:# application-prod.yml cors: allowed-origins: https://yourdomain.com,https://app.yourdomain.comCommittable suggestion skipped: line range outside the PR's diff.
server/src/main/java/oba/backend/server/domain/ai/service/AiService.java-13-22 (1)
13-22: 외부 마이크로서비스 호출에 타임아웃, 에러 처리, 응답 검증이 필수입니다.현재 코드(lines 19-21)는 다음 문제가 있습니다:
- 타임아웃 없음: 매일 0시 스케줄러(AiScheduler)에서 호출되는데, FastAPI가 응답하지 않으면 스케줄러 스레드가 무한 대기
- 응답 검증 없음: HTTP 상태 코드나 body null 여부를 확인하지 않음
- POST 재시도 위험: 네트워크 오류나 운영 레벨 재시도 시 중복 실행 가능
RestTemplate을 Bean으로 주입받아 공통 타임아웃/로깅을 적용하고, 응답 상태 검증과 기본 재시도 방지 로직(예: 날짜 기반 멱등성)을 추가하세요.
제안 diff
- private final RestTemplate restTemplate = new RestTemplate(); + // RestTemplateBuilder나 WebClient를 Bean으로 주입받아 타임아웃 설정 + private final RestTemplate restTemplate; + public AiService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } public String runDailyGptTask() { ResponseEntity<String> response = - restTemplate.postForEntity(fastApiUrl, null, String.class); + restTemplate.postForEntity(fastApiUrl, new HttpEntity.EMPTY, String.class); + if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) { + throw new IllegalStateException("AI server call failed: status=" + response.getStatusCode()); + } return response.getBody(); }Committable suggestion skipped: line range outside the PR's diff.
server/src/main/java/oba/backend/server/domain/article/repository/GptMongoRepository.java-14-14 (1)
14-14: Spring Data 메서드 네이밍이 올바르지 않습니다.
findByOrderByServingDateDesc는findBy다음에 필터링할 속성이 없어 Spring Data 쿼리 파생 규칙에 맞지 않습니다. 전체 문서를 정렬하여 조회하려면findAllByOrderByServingDateDesc로 변경하거나, 명시적인@Query어노테이션을 사용해야 합니다.🔧 수정 제안
- List<GptDocument> findByOrderByServingDateDesc(Pageable pageable); + List<GptDocument> findAllByOrderByServingDateDesc(Pageable pageable);server/src/main/java/oba/backend/server/global/auth/oauth/CustomOAuth2UserService.java-80-87 (1)
80-87:response가 null일 때 유효하지 않은 identifier가 생성됩니다.
response가 null이면 identifier가"naver:"가 되어 유효하지 않은 사용자가 생성될 수 있습니다. 필수 데이터가 없을 경우 예외를 던지거나, 명시적인 null 체크 후 적절한 처리가 필요합니다.🐛 수정 제안
@SuppressWarnings("unchecked") private static OAuthAttributes ofNaver(Map<String, Object> attributes) { Map<String, Object> response = (Map<String, Object>) attributes.get("response"); + if (response == null || response.get("id") == null) { + throw new IllegalArgumentException("Naver response is missing required 'id' field"); + } return new OAuthAttributes( - "naver:" + (response != null ? response.get("id") : ""), + "naver:" + response.get("id"), (response != null) ? (String) response.get("email") : null, (response != null) ? (String) response.get("name") : null, (response != null) ? (String) response.get("profile_image") : null ); }server/src/main/java/oba/backend/server/domain/ai/controller/AiController.java-15-19 (1)
15-19: POST /ai/generate/daily 엔드포인트에 인증/인가가 필요합니다.
POST /ai/generate/daily는 일일 AI 작업을 수동으로 트리거하는 관리자용 엔드포인트입니다. 그러나 SecurityConfig에서/ai/**가.permitAll()로 설정되어 있어 인증 없이 누구나 호출할 수 있습니다. 이를 통해 리소스 남용이나 서비스 장애가 발생할 수 있습니다. 관리자만 접근 가능하도록@PreAuthorize("hasRole('ADMIN')")또는 별도의 API 키 인증을 추가하세요.server/src/main/java/oba/backend/server/global/config/SecurityConfig.java-30-37 (1)
30-37: 프로덕션 환경에서 CORS 설정 강화 필요
setAllowedOriginPatterns(List.of("*"))와setAllowCredentials(true)조합은 개발 환경에서는 편리하지만, 프로덕션에서는 보안 위험이 됩니다. 자격 증명을 포함한 요청에 와일드카드 origin을 허용하면 CSRF 공격에 취약해질 수 있습니다.🔧 환경별 CORS 설정 분리 제안
@Value("${app.cors.allowed-origins}") private List<String> allowedOrigins; // ... config.setAllowedOriginPatterns(allowedOrigins);
application.yml에서 환경별로 설정:# local app.cors.allowed-origins: "*" # production app.cors.allowed-origins: "https://yourdomain.com,https://app.yourdomain.com"server/src/main/java/oba/backend/server/global/auth/controller/AuthController.java-42-59 (1)
42-59: Refresh Token과 Access Token 구분 없이 재발급 처리
/reissue엔드포인트에서 토큰 유효성만 검증하고, 해당 토큰이 Refresh Token인지 Access Token인지 구분하지 않습니다. 이로 인해 Access Token으로도 새 토큰 쌍을 발급받을 수 있어 보안이 약화됩니다.🔧 토큰 타입 구분을 위한 개선 방안
JwtProvider에서 토큰 생성 시 타입 클레임을 추가하고 검증하세요:// JwtProvider에 토큰 타입 클레임 추가 private String createToken(Long userId, String identifier, long validityMs, String tokenType) { return Jwts.builder() .claim("userId", userId) .claim("type", tokenType) // "access" 또는 "refresh" .setSubject(identifier) // ... } // AuthController에서 검증 public ResponseEntity<TokenResponse> reissue(...) { // ... Claims claims = jwtProvider.getClaims(token); if (!"refresh".equals(claims.get("type"))) { return ResponseEntity.status(401).build(); } // ... }server/src/main/java/oba/backend/server/domain/quiz/controller/QuizController.java-16-24 (1)
16-24: 토큰 추출 로직을 중앙화하세요.Line 21의
token.replace("Bearer ", "")는 여러 컨트롤러에서 반복되며 취약합니다:
- 토큰이 "Bearer "로 시작하지 않으면 전체 문자열이 그대로 전달됨
- 대소문자 구분으로 "bearer " 또는 "BEARER " 처리 안됨
- QuizResultController, AuthController에서도 동일한 패턴 사용
🔧 중앙화된 토큰 추출 제안
옵션 1: HandlerMethodArgumentResolver 사용 (권장)
JwtArgumentResolver 생성:
@Component public class JwtArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(JwtToken.class); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { String authorization = webRequest.getHeader("Authorization"); if (authorization == null || !authorization.toLowerCase().startsWith("bearer ")) { throw new IllegalArgumentException("Invalid authorization header"); } return authorization.substring(7); } }컨트롤러에서 사용:
@PostMapping("/submit") public ResponseEntity<?> submitQuiz( @JwtToken String jwt, @RequestBody QuizSubmitRequest request ) { quizService.submit(jwt, request); return ResponseEntity.ok().build(); }옵션 2: 유틸리티 메서드
public class JwtUtils { private static final String BEARER_PREFIX = "Bearer "; public static String extractToken(String authorizationHeader) { if (authorizationHeader == null || !authorizationHeader.startsWith(BEARER_PREFIX)) { throw new IllegalArgumentException("Invalid authorization header"); } return authorizationHeader.substring(BEARER_PREFIX.length()); } }server/src/main/java/oba/backend/server/domain/quiz/service/QuizService.java-17-31 (2)
17-31: @transactional 어노테이션을 추가하세요.데이터베이스 쓰기 작업을 수행하지만 트랜잭션 경계가 명시되어 있지 않습니다. 저장 중 예외 발생 시 일관성 있는 롤백을 보장하기 위해
@Transactional을 추가해야 합니다.+ @Transactional public void submit(String jwt, QuizSubmitRequest request) {
17-31: 중복 제출에 대한 예외 처리를 추가하세요.IncorrectQuiz 엔티티는 이미 userId와 articleId의 복합 기본키로 중복 제출을 데이터베이스 수준에서 방지하고 있습니다. 하지만 동일한 사용자의 같은 article에 대한 중복 제출 시도는
DataIntegrityViolationException을 발생시켜 500 에러를 반환하게 됩니다.이를 해결하기 위해 다음 중 하나를 고려하세요:
- 제출 전 기존 기록을 확인하고 업데이트하기
- 제외를 포착하여 의미 있는 응답 반환하기
- 비즈니스 요구사항에 따라 무시 또는 거절 로직 구현하기
현재 IncorrectQuizRepository에 deleteByUserIdAndArticleId() 메서드가 있으므로, 저장 전에 기존 기록을 찾아 업데이트하는 방식을 추천합니다.
server/src/main/java/oba/backend/server/domain/user/service/UserService.java-31-47 (1)
31-47: 캐시 무효화와 트랜잭션 순서 문제를 확인하세요.
@CacheEvict는 메서드 실행 전에 캐시를 삭제하지만, 트랜잭션은 메서드 종료 후 커밋됩니다. 트랜잭션이 롤백되면 캐시는 삭제되었지만 DB는 변경되지 않아 불일치가 발생할 수 있습니다.🔧 트랜잭션 커밋 후 캐시 무효화 제안
@Transactional - @CacheEvict(value = Const.CACHE_USER, key = "#identifier") + @CacheEvict(value = Const.CACHE_USER, key = "#identifier", beforeInvocation = false) public User findOrCreateUser(String identifier, String email, String name, String picture, ProviderInfo provider, Role role) {
beforeInvocation = false를 설정하면 메서드가 성공적으로 완료된 후에만 캐시가 무효화됩니다.server/src/main/java/oba/backend/server/domain/quiz/entity/IncorrectArticles.java-25-26 (1)
25-26: solDate의 null 안전성을 보장하세요.
nullable = false로 설정되었지만,@Builder를 사용하면 solDate 없이 객체를 생성할 수 있습니다. 이는 런타임에 constraint violation을 발생시킬 수 있습니다.🔧 기본값 설정 제안
옵션 1: @Builder.Default 사용
+import lombok.Builder.Default; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder @Table(name = "incorrect_articles") @IdClass(IncorrectArticlesId.class) public class IncorrectArticles { @Id @Column(name = "user_id") private Long userId; @Id @Column(name = "article_id") private Long articleId; + @Default @Column(name = "sol_date", nullable = false) - private LocalDateTime solDate; + private LocalDateTime solDate = LocalDateTime.now(); }옵션 2: @PrePersist 사용
@PrePersist public void prePersist() { if (this.solDate == null) { this.solDate = LocalDateTime.now(); } }Committable suggestion skipped: line range outside the PR's diff.
server/src/main/java/oba/backend/server/domain/user/entity/ProviderInfoConverter.java-14-17 (1)
14-17: DB의 잘못된 데이터에 대한 방어 로직이 필요합니다.
ProviderInfo.valueOf(dbData)는 dbData가 유효한 enum 값이 아닐 경우IllegalArgumentException을 던집니다. DB에 잘못된 데이터가 있거나 enum에서 값이 제거된 경우 애플리케이션이 실패할 수 있습니다.🛡️ 안전한 변환 로직 제안
+import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + @Converter(autoApply = true) public class ProviderInfoConverter implements AttributeConverter<ProviderInfo, String> { + + private static final Logger log = LoggerFactory.getLogger(ProviderInfoConverter.class); @Override public String convertToDatabaseColumn(ProviderInfo attribute) { return attribute == null ? null : attribute.name(); } @Override public ProviderInfo convertToEntityAttribute(String dbData) { - return dbData == null ? null : ProviderInfo.valueOf(dbData); + if (dbData == null) { + return null; + } + try { + return ProviderInfo.valueOf(dbData); + } catch (IllegalArgumentException e) { + log.error("Unknown ProviderInfo value in database: {}", dbData, e); + // 기본값 반환 또는 예외를 다시 던질지 결정 + return ProviderInfo.LOCAL; // 또는 throw new IllegalStateException("Invalid provider: " + dbData, e); + } } }server/src/main/java/oba/backend/server/domain/quiz/service/QuizService.java-18-18 (1)
18-18:jwtProvider.getUserId()호출 전에 JWT 검증 및 에러 처리가 필요합니다.
jwtProvider.getUserId(jwt)는 유효하지 않은 토큰(만료됨, 서명 오류, 형식 오류 등)에 대해JwtException을 throw합니다. 현재QuizService.submit()메서드에서 예외 처리가 없으므로, 잘못된 토큰이 전달될 경우 500 에러가 발생합니다.해결 방안:
getUserId()호출 전에jwtProvider.validateToken(jwt)로 먼저 검증하기- 또는
getUserId()호출을 try-catch로 감싸서 적절한 에러 응답(401/403) 반환하기server/src/main/java/oba/backend/server/domain/quiz/repository/IncorrectQuizRepository.java-13-13 (1)
13-13: Delete 메서드에 @Modifying과 @transactional 어노테이션 필요
deleteByUserIdAndArticleId는 DML 작업을 수행하는 메서드이므로@Modifying어노테이션이 필요하며, 트랜잭션 컨텍스트 내에서 실행되어야 합니다. 이 어노테이션이 없으면 Spring Data JPA가 삭제 쿼리를 실행하지 못할 수 있습니다.🔧 제안하는 수정
+import org.springframework.data.jpa.repository.Modifying; +import org.springframework.transaction.annotation.Transactional; + public interface IncorrectQuizRepository extends JpaRepository<IncorrectQuiz, IncorrectQuizId> { List<IncorrectQuiz> findByUserId(Long userId); + @Modifying + @Transactional void deleteByUserIdAndArticleId(Long userId, Long articleId); }server/src/main/java/oba/backend/server/domain/quiz/entity/IncorrectQuiz.java-23-27 (1)
23-27: boolean 원시 타입에 대해 @column(nullable=false) 추가 필수boolean 원시 타입을 사용하면서 @column(nullable=false) 명시가 없으면 JPA는 기본값으로 nullable=true를 설정합니다. 데이터베이스에서 null 값을 읽을 때 null을 boolean으로 언박싱할 수 없어 NullPointerException이 발생합니다. IncorrectArticles의 solDate 필드처럼 명시적으로 @column(nullable=false) 선언이 필요합니다.
@Column(name = "quiz1", nullable = false) private boolean quiz1; @Column(name = "quiz2", nullable = false) private boolean quiz2; @Column(name = "quiz3", nullable = false) private boolean quiz3; @Column(name = "quiz4", nullable = false) private boolean quiz4; @Column(name = "quiz5", nullable = false) private boolean quiz5;server/src/main/java/oba/backend/server/domain/article/service/ArticleDetailService.java-35-41 (1)
35-41:Map.of()로 인한 NPE 위험 수정 필요
Map.of()는 null 값을 허용하지 않아q.getQuestion(),q.getOptions(),q.getExplanation()중 하나라도 null이면NullPointerException이 발생합니다. (주:q.getAnswer()는QuizAnswerParser.toIndex()에서 null을 처리하므로 안전합니다)GptDocument.Quiz 필드들에는 @NotNull 같은 null 제약이 없으므로, 다음과 같이 null 체크를 추가하세요:
예시: null 필터링 추가
quizList = doc.getQuizzes().stream() + .filter(q -> q.getQuestion() != null && q.getOptions() != null + && q.getExplanation() != null) .map(q -> Map.of( "question", q.getQuestion(), "options", q.getOptions(), "answer", QuizAnswerParser.toIndex(q.getAnswer()), "explanation", q.getExplanation() )) .toList();Committable suggestion skipped: line range outside the PR's diff.
server/src/main/java/oba/backend/server/domain/article/controller/ArticleController.java-21-31 (1)
21-31:limit/id에 입력 검증(범위 제한)을 추가하세요.
limit가 음수/0/과도하게 큰 값이면 불필요한 부하나 예외가 발생할 수 있습니다.id도@Positive로 방어하는 편이 안전합니다.Proposed diff
import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Positive; import java.util.List; @RestController @RequestMapping("/articles") @RequiredArgsConstructor +@Validated public class ArticleController { @@ @GetMapping("/latest") public ResponseEntity<List<ArticleSummaryResponse>> getLatest( - @RequestParam(defaultValue = "5") int limit + @RequestParam(defaultValue = "5") @Min(1) @Max(50) int limit ) { return ResponseEntity.ok(summaryService.getLatestArticles(limit)); } @GetMapping("/{id}") - public ResponseEntity<ArticleDetailResponse> getDetail(@PathVariable Long id) { + public ResponseEntity<ArticleDetailResponse> getDetail(@PathVariable @Positive Long id) { return ResponseEntity.ok(detailService.getArticleDetail(id)); } }server/src/main/java/oba/backend/server/global/auth/jwt/JwtProvider.java-94-105 (1)
94-105: 권한(authorities) 문자열을 Spring Security 설정과 정합되게 맞추세요(ROLE_ prefix / roles()).
authorities("USER")는 보통hasRole("USER")/ROLE_USER기대와 불일치해서 인가가 깨질 수 있습니다. 또한 User 엔티티에Role이 있는데 토큰/Authentication에 반영이 전혀 없어, ADMIN 같은 상위 권한이 있다면 모두 USER로 강제됩니다. 최소한ROLE_USER로 맞추거나, role을 토큰 클레임으로 싣고 여기서 복원하세요.Proposed diff (최소 수정)
public org.springframework.security.core.Authentication getAuthentication(String identifier) { org.springframework.security.core.userdetails.UserDetails user = org.springframework.security.core.userdetails.User.builder() .username(identifier) .password("") // not used in JWT auth - .authorities("USER") + .roles("USER") .build();
🟡 Minor comments (4)
server/src/main/java/oba/backend/server/global/auth/dto/LoginRequest.java-5-7 (1)
5-7: Jackson 역직렬화를 위해 기본 생성자가 필요합니다.
@RequestBody로 바인딩할 때 Jackson이 기본 생성자를 사용합니다.@NoArgsConstructor를 추가하거나,idToken필드에@NotBlank같은 유효성 검증도 고려해 주세요.수정 제안
package oba.backend.server.global.auth.dto; import lombok.Getter; +import lombok.NoArgsConstructor; +import jakarta.validation.constraints.NotBlank; @Getter +@NoArgsConstructor public class LoginRequest { + @NotBlank(message = "idToken is required") private String idToken; }server/src/main/java/oba/backend/server/global/auth/oauth/CustomOAuth2UserService.java-67-77 (1)
67-77: Kakao의id가 null일 경우 identifier가"kakao:null"이 됩니다.
attributes.get("id")가 null인 경우 문자열 연결 결과가"kakao:null"이 됩니다. Naver와 동일하게 필수 필드에 대한 null 체크를 추가하는 것을 권장합니다.server/src/main/java/oba/backend/server/global/auth/oauth/CustomOAuth2UserService.java-53-53 (1)
53-53:OAuth2AuthenticationException으로 래핑해야 합니다.
IllegalArgumentException은 Spring Security의 OAuth2 예외 처리 흐름에서 적절히 처리되지 않을 수 있습니다.OAuth2AuthenticationException으로 래핑하면 일관된 인증 실패 응답을 보장할 수 있습니다.🔧 수정 제안
- default -> throw new IllegalArgumentException("Unsupported provider: " + provider); + default -> throw new OAuth2AuthenticationException("Unsupported provider: " + provider);그리고 import 추가:
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;server/src/main/java/oba/backend/server/domain/quiz/service/QuizQueryService.java-23-26 (1)
23-26:getSolDate()가 null일 경우 NPE 발생 가능
a.getSolDate().toString()호출 시solDate가 null이면NullPointerException이 발생합니다.🐛 Null 안전성을 위한 수정 제안
.map(a -> SolvedArticleResponse.builder() .articleId(a.getArticleId()) - .solvedAt(a.getSolDate().toString()) + .solvedAt(a.getSolDate() != null ? a.getSolDate().toString() : null) .build())또는
Optional을 사용하거나, 엔티티 레벨에서solDate가 항상 non-null임을 보장하세요.
🧹 Nitpick comments (40)
server/src/main/java/oba/backend/server/global/auth/controller/OAuthBridgeController.java (1)
6-7: 클래스 문서화가 필요합니다.OAuth 브릿지 컨트롤러의 목적과 OAuth 플로우에서의 역할을 설명하는 JavaDoc을 추가하는 것이 좋습니다. 특히 모바일 앱 인증으로의 마이그레이션 컨텍스트에서 이 브릿지가 어떤 역할을 하는지 명확히 하면 유지보수에 도움이 됩니다.
.idea/vcs.xml (1)
1-6:.idea커밋 정책 확인 + VCS 매핑 경로 명시화 권장팀/레포 정책상
.idea를 커밋하지 않는 경우가 많아(개인 설정 충돌/노이즈) 유지 여부를 먼저 합의하는 게 좋아 보입니다. 유지한다면directory=""대신$PROJECT_DIR$로 명시하는 편이 안전합니다.제안 diff
<project version="4"> <component name="VcsDirectoryMappings"> - <mapping directory="" vcs="Git" /> + <mapping directory="$PROJECT_DIR$" vcs="Git" /> </component> </project>docker-compose.yml (2)
1-1:version키는 Docker Compose V2에서 더 이상 사용되지 않습니다.최신 Docker Compose에서는
version필드가 무시됩니다. 제거해도 무방합니다.
15-17:depends_on만으로는 서비스가 준비될 때까지 대기하지 않습니다.
depends_on은 컨테이너 시작 순서만 보장하며, 서비스의 실제 준비 상태(healthcheck)를 기다리지 않습니다.ai_mongo가 완전히 시작되기 전에 Spring 앱이 연결을 시도할 수 있습니다.healthcheck 조건 추가 예시
depends_on: - - ai_backend - - ai_mongo + ai_backend: + condition: service_started + ai_mongo: + condition: service_healthy그리고
ai_mongo서비스에 healthcheck를 추가하세요:ai_mongo: image: mongo:7.0 healthcheck: test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] interval: 10s timeout: 5s retries: 5server/src/main/java/oba/backend/server/global/auth/oauth/CustomOAuth2User.java (1)
17-20: 생성자 파라미터에 대한 null 검증을 고려해 주세요.
delegate또는user가 null인 경우 이후 메서드 호출 시NullPointerException이 발생할 수 있습니다.null 검증 추가 예시
+import java.util.Objects; + public CustomOAuth2User(OAuth2User delegate, User user) { - this.delegate = delegate; - this.user = user; + this.delegate = Objects.requireNonNull(delegate, "delegate must not be null"); + this.user = Objects.requireNonNull(user, "user must not be null"); }server/build.gradle (1)
52-54: 테스트에서 Lombok 사용 시testAnnotationProcessor도 필요합니다.테스트 코드에서 Lombok을 사용하는 경우
testCompileOnly와testAnnotationProcessor를 추가해야 합니다.테스트용 Lombok 설정 추가
compileOnly 'org.projectlombok:lombok:1.18.34' annotationProcessor 'org.projectlombok:lombok:1.18.34' + testCompileOnly 'org.projectlombok:lombok:1.18.34' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.34'server/src/main/java/oba/backend/server/domain/article/entity/GptDocument.java (2)
62-68: null 대신 빈 컬렉션을 반환하는 것이 좋습니다.
getKeywords()와getQuizzes()가 null을 반환하면 호출하는 쪽에서 매번 null 체크가 필요합니다.Collections.emptyList()를 반환하면 NPE를 방지하고 코드가 간결해집니다.빈 컬렉션 반환으로 수정
+import java.util.Collections; + public List<GptResult.Keyword> getKeywords() { - return gptResult != null ? gptResult.getKeywords() : null; + return gptResult != null && gptResult.getKeywords() != null + ? gptResult.getKeywords() + : Collections.emptyList(); } public List<GptResult.Quiz> getQuizzes() { - return gptResult != null ? gptResult.getQuizzes() : null; + return gptResult != null && gptResult.getQuizzes() != null + ? gptResult.getQuizzes() + : Collections.emptyList(); }
10-12: MongoDB 엔티티에@Data사용 시 주의가 필요합니다.
@Data가 생성하는equals()와hashCode()는 모든 필드를 포함하므로 가변 필드가 있을 때 문제가 될 수 있습니다. 특히 컬렉션에 저장된 후 필드가 변경되면 해시 기반 컬렉션에서 객체를 찾지 못할 수 있습니다.server/src/main/java/oba/backend/server/domain/quiz/dto/QuizSubmitRequest.java (1)
7-12: Quiz 제출 DTO들 간의 명확한 구분이 필요합니다.현재 코드베이스에는 두 가지 퀴즈 제출 DTO가 존재합니다:
QuizSubmitRequest: 5개 퀴즈 답안의 리스트를 포함 (현재 파일)QuizResultRequest: 단일 퀴즈 결과(articleId, correct, selectedOption)를 포함이들의 용도가 다르지만 네이밍만으로는 구분이 명확하지 않습니다. 예를 들어:
QuizSubmitRequest→MultipleQuizSubmitRequest또는BulkQuizSubmitRequestQuizResultRequest→SingleQuizResultRequest와 같이 명명하면 각각의 목적이 더 명확해집니다.
💡 클래스명 개선 제안
@Getter @NoArgsConstructor -public class QuizSubmitRequest { +public class MultipleQuizSubmitRequest { private Long articleId; - private List<Integer> answers; // 0 or 1 + private List<Integer> answers; // 5개 퀴즈의 정답 여부 (0 or 1) }또는 Javadoc 주석을 추가하여 목적을 명확히 할 수 있습니다:
+/** + * 하나의 기사에 대한 5개 퀴즈의 전체 답안을 제출하기 위한 DTO + */ @Getter @NoArgsConstructor public class QuizSubmitRequest { private Long articleId; private List<Integer> answers; // 0 or 1 }server/src/main/java/oba/backend/server/domain/quiz/dto/QuizResultRequest.java (2)
6-12: 입력 검증 추가 권장DTO에 입력 검증이 없어 유효하지 않은 데이터가 서비스 레이어로 전달될 수 있습니다.
♻️ 검증 애너테이션 추가 제안
+import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + @Getter @NoArgsConstructor public class QuizResultRequest { + @NotNull(message = "Article ID는 필수입니다") private Long articleId; + private boolean correct; + + @Min(value = 0, message = "선택 옵션은 0 이상이어야 합니다") + @Max(value = 3, message = "선택 옵션은 3 이하여야 합니다") private int selectedOption; }컨트롤러에서
@Valid애너테이션을 추가하여 검증을 활성화해야 합니다.
6-12: 일관성을 위해 Builder 패턴 고려같은 quiz 도메인의
SolvedArticleResponse와WrongArticleResponse는@Builder를 사용하는데, 이 DTO는 사용하지 않아 일관성이 떨어집니다.♻️ Builder 패턴 추가
+import lombok.AllArgsConstructor; +import lombok.Builder; @Getter +@Builder @NoArgsConstructor +@AllArgsConstructor public class QuizResultRequest { private Long articleId; private boolean correct; private int selectedOption; }server/src/main/java/oba/backend/server/domain/article/service/QuizAnswerParser.java (1)
8-18: 파싱 실패 시 로깅 추가 권장파싱 실패 시 조용히 0을 반환하면 GPT 응답 오류나 데이터 문제를 디버깅하기 어렵습니다. 프로젝트 학습 내용에 따르면 SLF4J/Logback을 사용하므로 로깅을 추가하는 것이 좋습니다.
Based on learnings, the project uses SLF4J/Logback for logging instead of System.out.
♻️ 로깅 및 개선사항 추가
package oba.backend.server.domain.article.service; +import lombok.extern.slf4j.Slf4j; + +@Slf4j public class QuizAnswerParser { + + private static final int MIN_OPTION = 0; + private static final int MAX_OPTION = 3; + private static final int DEFAULT_OPTION = 0; /** * GPT가 보낸 answer 문자열(예: "2", "정답: 2", "2번")을 0~3 인덱스로 변환 */ public static int toIndex(String answer) { - if (answer == null) return 0; + if (answer == null) { + log.warn("Quiz answer is null, returning default: {}", DEFAULT_OPTION); + return DEFAULT_OPTION; + } + String cleaned = answer.replaceAll("[^0-9]", "").trim(); + + if (cleaned.isEmpty()) { + log.warn("Quiz answer has no digits after cleanup: '{}', returning default: {}", answer, DEFAULT_OPTION); + return DEFAULT_OPTION; + } try { int num = Integer.parseInt(cleaned); - return Math.max(0, Math.min(3, num - 1)); // 1~4 → 0~3 - } catch (Exception e) { - return 0; + int result = Math.max(MIN_OPTION, Math.min(MAX_OPTION, num - 1)); + if (num < 1 || num > 4) { + log.warn("Quiz answer out of expected range 1-4: '{}' (parsed: {}), clamped to: {}", answer, num, result); + } + return result; + } catch (NumberFormatException e) { + log.error("Failed to parse quiz answer: '{}', returning default: {}", answer, DEFAULT_OPTION, e); + return DEFAULT_OPTION; } } }server/src/main/java/oba/backend/server/global/common/Const.java (1)
3-8: 상수 클래스 구조 양호상수를 중앙화하여 관리하는 접근 방식이 좋습니다.
♻️ 인스턴스화 방지 및 구조 개선 제안 (선택사항)
1. 인스턴스화 방지:
public class Const { + private Const() { + throw new AssertionError("Cannot instantiate constants class"); + } + public static final String CACHE_USER = "userCache"; public static final String CACHE_ARTICLE_DETAIL = "articleDetail"; public static final String CACHE_LATEST_ARTICLES = "latestArticles"; public static final String BEARER_PREFIX = "Bearer "; }2. 관심사별 구조화 (선택사항):
public final class CacheConst { private CacheConst() {} public static final String USER = "userCache"; public static final String ARTICLE_DETAIL = "articleDetail"; public static final String LATEST_ARTICLES = "latestArticles"; } public final class AuthConst { private AuthConst() {} public static final String BEARER_PREFIX = "Bearer "; }server/src/main/java/oba/backend/server/domain/quiz/dto/SolvedArticleResponse.java (1)
16-16: 솔브 시각을 타임스탬프 타입으로 변경 고려
solvedAt을String으로 선언하면 타입 안정성과 유효성 검사가 부족합니다.WrongArticleResponse도 동일한 패턴을 사용하지만,LocalDateTime이나Instant를 사용하는 것이 더 나은 설계입니다.♻️ 타임스탬프 타입 개선
+import java.time.LocalDateTime; +import com.fasterxml.jackson.annotation.JsonFormat; + @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class SolvedArticleResponse { private Long articleId; private String title; private String summary; - private String solvedAt; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime solvedAt; }같은 변경을
WrongArticleResponse에도 적용하여 일관성을 유지하세요.server/src/main/java/oba/backend/server/global/config/CorsConfig.java (1)
8-23: CORS 설정 중복 제거 필요
SecurityConfig의http.cors()설정과CorsConfig의WebMvcConfigurer에서 동일한 CORS 정책이 중복으로 구성되고 있습니다. 두 설정 모두 동일하게 모든 출처(*), 메서드, 헤더를 허용하고 credentials를 활성화하고 있으므로, 하나의 설정으로 통합하고 역할을 명확히 분리하는 것을 권장합니다.server/src/main/java/oba/backend/server/domain/ai/service/AiService.java (1)
13-16:RestTemplate직접 생성 대신 Bean 주입(및 타임아웃 설정)으로 통일하는 편이 안전합니다.
Line 13처럼 직접 생성하면 커넥션/리드 타임아웃, 인터셉터(트레이싱/로깅), 커넥션 풀 등 공통 설정을 적용하기 어렵습니다.RestTemplateBuilder또는WebClient(권장) 기반으로 전환을 검토해주세요.server/src/main/java/oba/backend/server/global/auth/dto/TokenResponse.java (1)
6-11: 토큰 DTO는 불변으로 두는 걸 권장합니다.
현재는private String ...(Line 9-10)로 가변 객체라, 실수로 값이 변경될 여지가 있습니다.final적용 또는 Java 17record전환을 고려해 주세요.server/src/main/java/oba/backend/server/domain/quiz/dto/WrongArticleResponse.java (1)
12-18:incorrectAnswers(배열)와solvedAt(String) 타입은 장기적으로 유지보수 리스크가 큽니다.
boolean[](Line 16)는 가변이라 DTO 경계에서 값이 바뀔 수 있어List<Boolean>등으로 바꾸거나(또는 방어적 복사) 규약을 명확히 해두는 편이 안전합니다.solvedAt(Line 17)은OffsetDateTime/Instant같은 타입으로 두고 Jackson 포맷을 통일하는 걸 권장합니다.server/src/main/java/oba/backend/server/domain/quiz/entity/IncorrectArticlesId.java (1)
3-11:serialVersionUID추가 및 lombok 와일드카드 import 정리 권장입니다.
IncorrectArticlesId implements Serializable(Line 11)이므로private static final long serialVersionUID = 1L;를 두는 편이 안전하고,import lombok.*;(Line 3)는 필요한 어노테이션만 명시하는 걸 권장합니다.server/src/main/java/oba/backend/server/domain/article/dto/ArticleDetailResponse.java (2)
17-18:Object타입 사용은 타입 안전성을 저하시킵니다.
content와subtitle필드가Object로 선언되어 있어 API 계약이 불명확하고, JSON 직렬화/역직렬화 시 예상치 못한 동작이 발생할 수 있습니다. 실제 데이터 구조에 맞는 구체적인 타입이나 DTO를 정의하는 것을 권장합니다.
23-23:quizzes필드에 타입이 지정된 DTO 사용을 권장합니다.
List<Map<String, Object>>는 너무 일반적이어서 API 스펙 문서화가 어렵고, 클라이언트 측에서 타입 추론이 불가능합니다.QuizResponse와 같은 전용 DTO 클래스를 생성하면 유지보수성과 API 명세 가독성이 향상됩니다.server/src/main/java/oba/backend/server/domain/quiz/entity/IncorrectQuizId.java (2)
7-8: 복합 키 클래스에서@Setter제거를 권장합니다.JPA 복합 키는 일반적으로 불변(immutable)이어야 합니다.
@Setter를 제거하고, 값 설정은@AllArgsConstructor를 통해서만 하도록 하면 의도치 않은 키 값 변경을 방지할 수 있습니다.
6-28: 구조적으로 완전히 동일한 복합 키 클래스가 중복되어 있습니다.
IncorrectQuizId와IncorrectArticlesId는userId와articleId필드,equals(),hashCode()구현이 정확히 동일합니다. 그러나 단순 통합 전에 주의가 필요합니다: 각각 다른 엔티티의@IdClass로 바인딩되어 있어 JPA 관점에서 직접 하나로 병합하면 타입 혼동이 발생할 수 있습니다.공통 부모 클래스 추출 또는
@Embeddable패턴 사용 등 구조적 개선을 검토해보시기 바랍니다.server/src/main/resources/application.yml (2)
17-18: 프로덕션 환경에서ddl-auto: update와show-sql: true는 위험합니다.주석에도 언급되어 있지만, 프로덕션에서
ddl-auto: update는 예기치 않은 스키마 변경을 유발할 수 있습니다. 또한show-sql: true는 성능 저하와 로그 과다를 유발합니다. 프로파일별로 분리하거나 환경 변수로 제어하는 것을 권장합니다.♻️ 프로파일 분리 예시
# application.yml (공통) spring: jpa: hibernate: ddl-auto: ${JPA_DDL_AUTO:validate} show-sql: ${JPA_SHOW_SQL:false}
86-90: 하드코딩된 URL들을 환경 변수로 분리해야 합니다.
oauth.bridge-url과app.mobile-redirect가 하드코딩되어 있어 개발/스테이징/프로덕션 환경 간 전환 시 문제가 될 수 있습니다. 다른 설정값들처럼 환경 변수로 관리하는 것을 권장합니다.♻️ 수정 제안
oauth: - bridge-url: "http://localhost:9000/oauth2/bridge" + bridge-url: ${OAUTH_BRIDGE_URL:http://localhost:9000/oauth2/bridge} app: - mobile-redirect: "myapp://oauth/naver" + mobile-redirect: ${APP_MOBILE_REDIRECT:myapp://oauth/callback}server/src/main/java/oba/backend/server/domain/quiz/service/QuizQueryService.java (1)
35-41: 하드코딩된 퀴즈 플래그 배열 구조 개선 고려
quiz1~quiz5플래그를 boolean 배열로 수동 매핑하는 방식은 퀴즈 개수가 변경되면 수정이 필요합니다. 현재는 동작하지만, 엔티티에getIncorrectFlags()같은 메서드를 추가하여 캡슐화하면 유지보수성이 향상됩니다.server/src/main/java/oba/backend/server/domain/ai/scheduler/AiScheduler.java (1)
37-42: 캐시 조회 실패 시 NPE 발생 가능성 고려
Objects.requireNonNull(cacheManager.getCache(...))는 캐시가 존재하지 않으면NullPointerException을 발생시킵니다.evictCaches()가try-catch블록 내에서 호출되므로 예외는 잡히지만, 캐시 무효화 실패가 일반 에러 로그에 묻힐 수 있습니다.♻️ 캐시 무효화 실패를 명확히 처리하는 방안
private void evictCaches() { - Objects.requireNonNull(cacheManager.getCache(Const.CACHE_LATEST_ARTICLES)).clear(); - Objects.requireNonNull(cacheManager.getCache(Const.CACHE_ARTICLE_DETAIL)).clear(); + clearCacheIfPresent(Const.CACHE_LATEST_ARTICLES); + clearCacheIfPresent(Const.CACHE_ARTICLE_DETAIL); log.info("[Cache] Article caches evicted."); } + +private void clearCacheIfPresent(String cacheName) { + var cache = cacheManager.getCache(cacheName); + if (cache != null) { + cache.clear(); + } else { + log.warn("[Cache] Cache not found: {}", cacheName); + } +}server/src/main/java/oba/backend/server/domain/user/service/UserService.java (2)
21-25: null 반환 대신 Optional 사용을 고려하세요.
findByIdentifier메서드가 null을 반환하면 호출자에서 NPE가 발생할 수 있습니다. 더 안전한 API를 위해Optional<User>를 반환하는 것을 권장합니다.♻️ Optional 반환 제안
- @Cacheable(value = Const.CACHE_USER, key = "#identifier", unless = "#result == null") + @Cacheable(value = Const.CACHE_USER, key = "#identifier") @Transactional(readOnly = true) - public User findByIdentifier(String identifier) { - return userRepository.findByIdentifier(identifier).orElse(null); + public Optional<User> findByIdentifier(String identifier) { + return userRepository.findByIdentifier(identifier); }참고: Spring Cache는 Optional을 지원하므로
unless조건도 제거할 수 있습니다.
41-42: 입력 검증 추가를 권장합니다.identifier, email, name에 대한 유효성 검증이 없습니다. 악의적이거나 잘못된 형식의 데이터가 DB에 저장될 수 있습니다.
🛡️ 입력 검증 예시
메서드 시작 부분에 검증 로직 추가:
public User findOrCreateUser(String identifier, String email, String name, String picture, ProviderInfo provider, Role role) { if (identifier == null || identifier.trim().isEmpty()) { throw new IllegalArgumentException("Identifier cannot be null or empty"); } // 이메일 형식 검증 (email이 제공된 경우) if (email != null && !email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) { throw new IllegalArgumentException("Invalid email format"); } return userRepository.findByIdentifier(identifier) .map(user -> { user.updateInfo(email, name, picture); return user; }) // ... 나머지 코드 }server/src/main/java/oba/backend/server/domain/user/entity/ProviderInfoConverter.java (1)
6-6: autoApply = true 사용의 영향을 검토하세요.
autoApply = true는 이 컨버터가 모든ProviderInfo필드에 자동으로 적용됨을 의미합니다. 대부분의 경우 올바른 선택이지만, 특정 필드에서 다른 변환 로직이 필요한 경우 문제가 될 수 있습니다.현재 사용 사례에서는 적절해 보이지만, 향후 확장성을 위해 명시적으로
@Convert(converter = ProviderInfoConverter.class)를 사용하는 것도 고려할 수 있습니다.server/src/main/java/oba/backend/server/domain/quiz/entity/IncorrectArticles.java (1)
13-14: @EmbeddedId 사용을 권장합니다.현재
@IdClass패턴을 사용하고 있지만,@EmbeddedId와@Embeddable을 사용하면 더 타입 안전하고 명확한 복합 키 표현이 가능합니다.♻️ @EmbeddedId 리팩토링 예시
IncorrectArticlesId를 @embeddable로 변경:
@Embeddable @Getter @NoArgsConstructor @AllArgsConstructor public class IncorrectArticlesId implements Serializable { @Column(name = "user_id") private Long userId; @Column(name = "article_id") private Long articleId; // equals, hashCode 유지 }IncorrectArticles 엔티티:
@Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder @Table(name = "incorrect_articles") public class IncorrectArticles { @EmbeddedId private IncorrectArticlesId id; @Column(name = "sol_date", nullable = false) private LocalDateTime solDate; }이는 optional 개선사항이며 현재 구현도 작동합니다.
server/src/main/java/oba/backend/server/domain/quiz/controller/QuizController.java (3)
17-24: 입력 검증과 에러 처리를 추가하세요.현재 구현은 다음 문제가 있습니다:
- token이나 request가 null일 경우 처리 없음
- 서비스 레이어에서 발생하는 예외가 적절히 처리되지 않음
- 클라이언트에게 명확한 에러 응답 제공 안됨
🛡️ 검증 및 에러 처리 추가
@PostMapping("/submit") public ResponseEntity<?> submitQuiz( - @RequestHeader("Authorization") String token, - @RequestBody QuizSubmitRequest request + @RequestHeader(value = "Authorization", required = true) String token, + @RequestBody @Valid QuizSubmitRequest request ) { + if (token == null || token.isBlank()) { + return ResponseEntity.badRequest().body("Authorization header is required"); + } + if (!token.startsWith("Bearer ")) { + return ResponseEntity.badRequest().body("Invalid authorization format"); + } + String jwt = token.replace("Bearer ", ""); - quizService.submit(jwt, request); - return ResponseEntity.ok().build(); + try { + quizService.submit(jwt, request); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } }참고:
@Valid를 사용하려면 QuizSubmitRequest에 javax.validation 어노테이션이 필요합니다.
23-23: HTTP 상태 코드를 201 Created로 변경하세요.리소스를 생성하는 POST 요청이므로
200 OK대신201 Created가 더 적절한 HTTP 상태 코드입니다.- return ResponseEntity.ok().build(); + return ResponseEntity.status(HttpStatus.CREATED).build();
9-12: API 문서화 어노테이션 추가를 고려하세요.Swagger/OpenAPI 문서화를 위해
@Tag,@Operation,@ApiResponse등의 어노테이션을 추가하면 API 사용성이 향상됩니다.@RestController @RequestMapping("/quiz") @RequiredArgsConstructor @Tag(name = "Quiz", description = "퀴즈 제출 API") public class QuizController { // ... @Operation(summary = "퀴즈 답안 제출", description = "사용자의 퀴즈 답안을 제출합니다") @ApiResponses({ @ApiResponse(responseCode = "201", description = "제출 성공"), @ApiResponse(responseCode = "400", description = "잘못된 요청"), @ApiResponse(responseCode = "401", description = "인증 실패") }) @PostMapping("/submit") public ResponseEntity<?> submitQuiz(/* ... */) { // ... } }server/src/main/java/oba/backend/server/domain/article/service/ArticleSummaryService.java (2)
24-25: MongoDB 쿼리에 @transactional 불필요MongoDB 저장소는 Spring의
@Transactional이 JPA처럼 동작하지 않습니다. MongoDB는 자체적인 트랜잭션 메커니즘을 사용하며, 단순 읽기 쿼리에는 이 어노테이션이 불필요합니다.♻️ 제안하는 수정
@Cacheable(value = Const.CACHE_LATEST_ARTICLES, key = "#limit") - @Transactional(readOnly = true) public List<ArticleSummaryResponse> getLatestArticles(int limit) {
35-35: 정규식 패턴 개선 권장문자 클래스 내에서 점(
.)을 이스케이프할 필요가 없습니다.[\\.|·|\\n]는[.·\n]로 단순화할 수 있습니다.♻️ 제안하는 수정
- bullets = Arrays.stream(doc.getSummary().split("[\\.|·|\\n]")) + bullets = Arrays.stream(doc.getSummary().split("[.·\n]"))server/src/main/java/oba/backend/server/domain/article/service/ArticleDetailService.java (2)
22-23: 범용 RuntimeException 대신 커스텀 예외 사용 권장
RuntimeException을 직접 던지는 대신 도메인별 커스텀 예외(예:ArticleNotFoundException)를 사용하면 예외 처리가 더 명확해지고 API 응답 메시지를 일관되게 관리할 수 있습니다.♻️ 제안하는 수정
먼저 커스텀 예외를 정의하세요:
package oba.backend.server.domain.article.exception; public class ArticleNotFoundException extends RuntimeException { public ArticleNotFoundException(Long articleId) { super("Article not found: " + articleId); } }그런 다음 서비스 코드를 수정하세요:
+import oba.backend.server.domain.article.exception.ArticleNotFoundException; + GptDocument doc = gptMongoRepository.findByArticleId(articleId) - .orElseThrow(() -> new RuntimeException("Article not found: " + articleId)); + .orElseThrow(() -> new ArticleNotFoundException(articleId));
25-25: null 초기화 대신 빈 리스트 사용 권장
keywordList와quizList를null로 초기화하는 대신 빈 리스트(Collections.emptyList()또는List.of())로 초기화하면 null 체크 부담이 줄어들고 API 응답이 더 일관됩니다.♻️ 제안하는 수정
- List<String> keywordList = null; + List<String> keywordList = Collections.emptyList(); if (doc.getKeywords() != null) { keywordList = doc.getKeywords().stream() .map(GptDocument.GptResult.Keyword::getKeyword) .toList(); } - List<Map<String, Object>> quizList = null; + List<Map<String, Object>> quizList = Collections.emptyList(); if (doc.getQuizzes() != null) {필요 시 import 추가:
+import java.util.Collections;Also applies to: 32-32
server/src/main/java/oba/backend/server/global/auth/jwt/JwtAuthenticationFilter.java (1)
31-36: 예외 처리 및 로깅 추가 권장토큰 검증 또는 인증 객체 생성 중 예외가 발생할 경우 필터가 실패할 수 있습니다. 예외 처리와 로깅을 추가하면 디버깅과 모니터링이 개선됩니다.
Based on learnings, JwtProvider에서 authorities가 비어있을 때 기본값 처리가 되어 있는지 확인하세요.
♻️ 제안하는 수정
+import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { + private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class); private final JwtProvider jwtProvider; @Override @@ -29,10 +31,16 @@ String token = jwtProvider.resolveToken(request); - if (token != null && jwtProvider.validateToken(token)) { + if (token != null && jwtProvider.validateToken(token)) { + try { Claims claims = jwtProvider.getClaims(token); String identifier = claims.getSubject(); Authentication auth = jwtProvider.getAuthentication(identifier); SecurityContextHolder.getContext().setAuthentication(auth); + } catch (Exception e) { + log.warn("JWT authentication failed: {}", e.getMessage()); + SecurityContextHolder.clearContext(); + } + } else if (token != null) { + log.debug("Invalid or expired JWT token"); }server/src/main/java/oba/backend/server/domain/user/entity/User.java (1)
9-17: Entity에@AllArgsConstructor+@Builder조합은 “id 포함 생성”을 유도할 수 있어 위험합니다.JPA 엔티티는 보통
id가 DB에서 생성되므로, 빌더/전체생성자가 외부에서id까지 세팅하는 경로를 열어두면 오작동/혼선을 만들기 쉽습니다(테스트/픽스처에서 특히). 정적 팩토리로 필요한 필드만 받도록 제한하는 쪽을 권장합니다.Proposed diff
@Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Builder @Table(name = "users") public class User { @@ @Column(nullable = false, unique = true) private String identifier; @@ private Role role; + + @Builder + private User(String identifier, String email, String name, String picture, ProviderInfo authProvider, Role role) { + this.identifier = identifier; + this.email = email; + this.name = name; + this.picture = picture; + this.authProvider = authProvider; + this.role = role; + } }Also applies to: 19-37
| implementation 'io.jsonwebtoken:jjwt-api:0.11.5' | ||
| runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' | ||
| runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
jjwt latest version 2025 0.12 security updates
💡 Result:
-
As of repository releases, the latest 0.12.x release published on the JJWT GitHub releases page is 0.12.4. [1]
-
A reported vulnerability (CVE-2024-31033) affecting JJWT "through 0.12.5" (claims that certain characters may be ignored when generating/signing keys) is tracked by multiple advisory databases; the vendor has publicly disputed the practical impact. See OSV/NVD and GitLab advisory entries for details. [2][3]
Sources
- JJWT releases (GitHub). [1]
- OSV / CVE-2024-31033 entry. [2]
- GitLab advisory for CVE-2024-31033. [3]
🌐 Web query:
jjwt library release history 0.11.5 0.12 breaking changes
💡 Result:
Short summary — main breaking changes when upgrading from jjwt 0.11.5 → 0.12.x
- 0.12.0 is the project’s first breaking-release; it adds JWE/JWK support and other major API redesigns. [1]
- Claims and Header types are now immutable; you must use the new builder APIs (ClaimsBuilder / HeaderBuilder) to create/modify them. Several setters were removed (e.g., JwsHeader#setAlgorithm, Header#setCompressionAlgorithm). [2][7]
- JwtParser is now immutable and configured exclusively via JwtParserBuilder (Jwts.parser() now returns a builder). Parse method names were changed/renamed. [3][2]
- SignatureAlgorithm enum/usage was replaced: the old SignatureAlgorithm usages were deprecated/removed in favor of the new algorithm interfaces/registries (Jwts.SIG) and new signWith(...) overloads — update how you build/sign tokens. [2][6]
- Header/JWT builder APIs changed: some Jwts.header()/jwsHeader(...) overloads were removed in favor of the new header() → HeaderBuilder flow. [7][2]
- Misc: JSON serializer package moves, improved HSM/PKCS11 support, and other API renames — consult the CHANGELOG/migration notes for a full list. [2][1]
- Security note: a CVE/GHSA was reported against jjwt-impl:0.11.5 (CVE referenced in issue discussion); that is one reason many projects consider upgrading despite the breaking changes. [4]
Sources
- Release notes / CHANGELOG (jwtk/jjwt releases). [1]
- 0.12 migration / breaking-changes summary (CHANGELOG / migration guide). [2]
- GitHub discussion / guidance about parser immutability and usage examples. [3]
- GitHub issue reporting CVE vs 0.11.5. [4]
If you want, I can extract the exact changelog entries and list the concrete method/class renames you need to change in your code (I can produce a diff-style mapping).
🌐 Web query:
jjwt io.jsonwebtoken current version January 2026
💡 Result:
As of January 9, 2026 the current released version of JJWT (io.jsonwebtoken) is 0.13.0. (javadoc.io)
Citations:
jjwt 라이브러리 업그레이드 시 주의: 주요 breaking changes 필요
0.11.5에서 최신 버전(0.13.0)으로 업그레이드하려면 단순 버전 변경만으로는 부족합니다. 0.12.0부터 중대한 API 변경이 도입되었습니다:
Claims와Header가 불변(immutable)으로 변경됨 →ClaimsBuilder,HeaderBuilder사용 필요JwtParser가 불변으로 변경됨 →JwtParserBuilder로만 설정 가능SignatureAlgorithm대체: 새로운 알고리즘 인터페이스(Jwts.SIG) 사용 필요- 토큰 구축 및 서명 메서드 이름 변경
또한 CVE-2024-31033이 0.12.5 이하 버전에 영향을 미치므로, 업그레이드 시 마이그레이션 가이드를 참고하여 광범위한 코드 리팩토링이 필요합니다.
🤖 Prompt for AI Agents
In @server/build.gradle around lines 42 - 44, You upgraded jjwt dependency
versions in build.gradle but must not only change the version to 0.13.0 —
refactor all JWT usage to follow breaking API changes: replace mutable
Claims/Header uses with ClaimsBuilder/HeaderBuilder, construct parsers via
JwtParserBuilder instead of mutating JwtParser, replace SignatureAlgorithm usage
with the new Jwts.SIG algorithm constants, and update token build/sign method
calls to the new names; also ensure you pick >=0.12.6 (prefer 0.13.0) to avoid
CVE-2024-31033 and consult the jjwt migration guide to apply these changes
across all codepaths that create/parse/sign/verify tokens (look for usages of
Claims, Header, JwtParser, SignatureAlgorithm, and Jwts.build()/sign() sites).
| PID PPID PGID WINPID TTY UID STIME COMMAND | ||
| 1483 1 1483 319596 cons2 197609 20:42:39 /usr/bin/bash | ||
| 2954 1 2954 336312 cons3 197609 03:29:20 /usr/bin/bash | ||
| 3495 3487 3487 276496 cons2 197609 10:33:18 /c/Program Files/nodejs/node | ||
| 3487 1483 3487 386764 cons2 197609 10:33:16 /usr/bin/bash | ||
| 3646 2954 3646 394660 cons3 197609 10:40:08 /usr/bin/PS | ||
| 1394 1 1394 226624 cons0 197609 20:42:33 /usr/bin/bash | ||
| 1399 1 1399 324196 cons1 197609 20:42:34 /usr/bin/bash |
There was a problem hiding this comment.
실수로 커밋된 시스템 진단 출력 파일을 제거해야 합니다.
이 파일은 프로세스 목록 명령어의 출력 결과로 보이며, 실수로 버전 관리에 포함된 것으로 판단됩니다. 다음과 같은 문제가 있습니다:
- 소스 코드가 아닙니다: 애플리케이션 코드나 설정 파일이 아닌 시스템 진단 데이터입니다.
- 보안/프라이버시 우려: 개발 환경의 프로세스 ID, 사용자 ID(197609), 파일 경로 등 로컬 시스템 정보가 노출됩니다.
- 저장소 오염: 이러한 임시 진단 파일은 버전 관리에 포함되어서는 안 됩니다.
이 파일을 PR에서 제거하고, 유사한 파일이 실수로 커밋되지 않도록 .gitignore에 적절한 패턴을 추가하는 것을 권장합니다.
🗑️ 파일 제거 방법
# 파일 삭제 및 커밋
git rm server/netsh
git commit -m "Remove accidentally committed process listing file"또한 .gitignore에 다음 패턴 추가를 고려하세요:
# System diagnostic outputs
netsh
ps_output
process_list
🤖 Prompt for AI Agents
In @server/netsh around lines 1 - 8, Remove the accidentally committed
diagnostic file server/netsh from the repository and commit the removal (use git
rm / stage deletion and commit), then add an appropriate ignore pattern to
.gitignore (e.g., a pattern matching netsh or other process listing filenames)
so future process dumps aren’t tracked; if this file has sensitive data and must
be purged from history, run a history-rewrite tool (git filter-repo or BFG)
targeting server/netsh and force-push the cleaned branch.
| @GetMapping("/solved") | ||
| public ResponseEntity<List<SolvedArticleResponse>> getSolved(@RequestParam Long userId) { | ||
| return ResponseEntity.ok(quizQueryService.getSolved(userId)); | ||
| } | ||
|
|
||
| @GetMapping("/wrong") | ||
| public ResponseEntity<List<WrongArticleResponse>> getWrong(@RequestParam Long userId) { | ||
| return ResponseEntity.ok(quizQueryService.getWrong(userId)); | ||
| } |
There was a problem hiding this comment.
IDOR 취약점: 인증된 사용자 ID를 사용해야 합니다.
userId를 요청 파라미터로 받으면 인증된 사용자가 다른 사용자의 데이터를 조회할 수 있는 IDOR(Insecure Direct Object Reference) 취약점이 발생합니다. @AuthenticationPrincipal을 사용하여 현재 로그인한 사용자의 ID를 추출해야 합니다.
🔒 수정 제안
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import oba.backend.server.global.auth.oauth.CustomOAuth2User;
+
@RestController
@RequestMapping("/api/my")
@RequiredArgsConstructor
public class MyQuizController {
private final QuizQueryService quizQueryService;
@GetMapping("/solved")
- public ResponseEntity<List<SolvedArticleResponse>> getSolved(@RequestParam Long userId) {
+ public ResponseEntity<List<SolvedArticleResponse>> getSolved(@AuthenticationPrincipal CustomOAuth2User principal) {
+ Long userId = principal.getUser().getId();
return ResponseEntity.ok(quizQueryService.getSolved(userId));
}
@GetMapping("/wrong")
- public ResponseEntity<List<WrongArticleResponse>> getWrong(@RequestParam Long userId) {
+ public ResponseEntity<List<WrongArticleResponse>> getWrong(@AuthenticationPrincipal CustomOAuth2User principal) {
+ Long userId = principal.getUser().getId();
return ResponseEntity.ok(quizQueryService.getWrong(userId));
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @GetMapping("/solved") | |
| public ResponseEntity<List<SolvedArticleResponse>> getSolved(@RequestParam Long userId) { | |
| return ResponseEntity.ok(quizQueryService.getSolved(userId)); | |
| } | |
| @GetMapping("/wrong") | |
| public ResponseEntity<List<WrongArticleResponse>> getWrong(@RequestParam Long userId) { | |
| return ResponseEntity.ok(quizQueryService.getWrong(userId)); | |
| } | |
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | |
| import oba.backend.server.global.auth.oauth.CustomOAuth2User; | |
| @GetMapping("/solved") | |
| public ResponseEntity<List<SolvedArticleResponse>> getSolved(@AuthenticationPrincipal CustomOAuth2User principal) { | |
| Long userId = principal.getUser().getId(); | |
| return ResponseEntity.ok(quizQueryService.getSolved(userId)); | |
| } | |
| @GetMapping("/wrong") | |
| public ResponseEntity<List<WrongArticleResponse>> getWrong(@AuthenticationPrincipal CustomOAuth2User principal) { | |
| Long userId = principal.getUser().getId(); | |
| return ResponseEntity.ok(quizQueryService.getWrong(userId)); | |
| } |
🤖 Prompt for AI Agents
In
@server/src/main/java/oba/backend/server/domain/quiz/controller/MyQuizController.java
around lines 19 - 27, The endpoints getSolved and getWrong accept userId as a
request parameter causing an IDOR; change both methods to obtain the
authenticated user's ID via @AuthenticationPrincipal instead of @RequestParam,
e.g. replace the Long userId param with an @AuthenticationPrincipal (your
security principal type, e.g. UserPrincipal or CustomUserDetails) and call
quizQueryService.getSolved(principal.getId()) and
quizQueryService.getWrong(principal.getId()) accordingly; remove the request
param usage and update imports and any tests to reflect the new parameter
signature.
| public interface IncorrectArticlesRepository extends JpaRepository<IncorrectArticles, IncorrectArticlesId> { | ||
|
|
||
| List<IncorrectArticles> findByUserId(Long userId); | ||
|
|
||
| void deleteByUserIdAndArticleId(Long userId, Long articleId); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Search for where deleteByUserIdAndArticleId is called
rg -n "deleteByUserIdAndArticleId" --type java -B 5 -A 2Repository: OneBiteArticle/oba_backend
Length of output: 1880
🏁 Script executed:
# Also search for the service class that likely uses this repository
rg -n "IncorrectArticlesRepository" --type java -B 2 -A 10Repository: OneBiteArticle/oba_backend
Length of output: 6872
🏁 Script executed:
# Check the IncorrectArticlesRepository file itself for any annotations
cat -n server/src/main/java/oba/backend/server/domain/quiz/repository/IncorrectArticlesRepository.javaRepository: OneBiteArticle/oba_backend
Length of output: 670
🏁 Script executed:
# Search for actual method calls (not just the interface definition)
rg -n "\.deleteByUserIdAndArticleId\(" --type java -B 3 -A 3Repository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Check the full QuizResultService file to see all methods
wc -l server/src/main/java/oba/backend/server/domain/quiz/service/QuizResultService.javaRepository: OneBiteArticle/oba_backend
Length of output: 155
🏁 Script executed:
# If the file is reasonable size, read it fully
cat -n server/src/main/java/oba/backend/server/domain/quiz/service/QuizResultService.javaRepository: OneBiteArticle/oba_backend
Length of output: 1315
🏁 Script executed:
# Also check if there are any other service files that might use this method
fd -t f "Service\.java" server/src/main/java/oba/backend/server/domain/quiz/service/ | head -20Repository: OneBiteArticle/oba_backend
Length of output: 311
🏁 Script executed:
# Check QuizService.java for deleteByUserIdAndArticleId usage
cat -n server/src/main/java/oba/backend/server/domain/quiz/service/QuizService.javaRepository: OneBiteArticle/oba_backend
Length of output: 1477
🏁 Script executed:
# Search more broadly for deleteByUserIdAndArticleId with different patterns
rg -n "deleteByUserIdAndArticleId" --type java -C 5Repository: OneBiteArticle/oba_backend
Length of output: 1880
🏁 Script executed:
# Check if @Modifying annotation is imported or used anywhere in quiz module
rg -n "@Modifying" server/src/main/java/oba/backend/server/domain/quiz/ --type javaRepository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Check the entire codebase outside quiz module for deleteByUserIdAndArticleId
rg -n "deleteByUserIdAndArticleId" --type javaRepository: OneBiteArticle/oba_backend
Length of output: 395
🏁 Script executed:
# Verify Spring Data JPA documentation pattern - check if @Modifying is needed
# Search for similar delete methods in the codebase to see how they're implemented
rg -n "void delete" server/src/main/java/oba/backend/server/domain/ --type java -B 1 -A 1Repository: OneBiteArticle/oba_backend
Length of output: 792
🏁 Script executed:
# Check the entire codebase for any calls to deleteByUserIdAndArticleId
find . -type f -name "*.java" -exec grep -l "deleteByUserIdAndArticleId" {} \;Repository: OneBiteArticle/oba_backend
Length of output: 261
🏁 Script executed:
# Search for @Modifying usage patterns in repositories to understand how deletes are done
rg -n "@Modifying" --type java -B 2 -A 2Repository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Look for @Transactional annotations in the service layer
rg -n "@Transactional" server/src/main/java/oba/backend/server/domain/quiz/service/ --type java -B 2 -A 2Repository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Check Spring Data JPA import statements to see what's available
rg -n "import.*springframework.data.jpa" server/src/main/java/oba/backend/server/domain/quiz/repository/ --type javaRepository: OneBiteArticle/oba_backend
Length of output: 385
🏁 Script executed:
# Check examples of custom query methods with @Modifying in the entire codebase
rg -n "@Query" --type java -B 3 -A 3 | head -50Repository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Verify if the delete method is actually being used anywhere in the codebase
git grep -n "deleteByUserIdAndArticleId" 2>/dev/null || echo "Git grep not available, using alternative"
rg -n "deleteByUserIdAndArticleId\(" --type javaRepository: OneBiteArticle/oba_backend
Length of output: 721
deleteByUserIdAndArticleId 메서드에 @Modifying 어노테이션이 필수입니다.
Spring Data JPA 파생 삭제 쿼리 메서드는 @Modifying 어노테이션을 반드시 선언해야 합니다. 이 어노테이션이 없으면 런타임에 쿼리 실행이 실패합니다. 추가로 이 메서드를 호출하는 서비스 계층의 메서드에도 @Transactional 어노테이션이 필요합니다.
🤖 Prompt for AI Agents
In
@server/src/main/java/oba/backend/server/domain/quiz/repository/IncorrectArticlesRepository.java
around lines 9 - 14, The deleteByUserIdAndArticleId method in
IncorrectArticlesRepository is missing the required @Modifying annotation for
Spring Data JPA derived delete queries; update the IncorrectArticlesRepository
interface to annotate the deleteByUserIdAndArticleId(Long userId, Long
articleId) method with @Modifying (and import
org.springframework.data.jpa.repository.Modifying) and ensure the service method
that invokes this repository method is annotated with @Transactional so the
delete runs inside a transaction.
| public void saveQuizResult(String jwt, QuizResultRequest request) { | ||
| Long userId = jwtProvider.getUserId(jwt); | ||
|
|
||
| IncorrectArticles incorrectArticles = IncorrectArticles.builder() | ||
| .userId(userId) | ||
| .articleId(request.getArticleId()) | ||
| .solDate(LocalDateTime.now()) | ||
| .build(); | ||
|
|
||
| incorrectArticlesRepository.save(incorrectArticles); | ||
| } |
There was a problem hiding this comment.
요청 데이터의 중요한 필드들이 무시되고 있습니다.
QuizResultRequest는 articleId, correct, selectedOption 세 개의 필드를 포함하지만(관련 스니펫의 QuizResultRequest.java 참조), 현재 구현은 articleId만 사용하고 correct와 selectedOption은 완전히 무시합니다.
이는 다음과 같은 문제를 야기합니다:
- 사용자가 정답을 맞췄는지 여부를 저장하지 않음
- 어떤 선택지를 선택했는지 기록하지 않음
- 의미 있는 피드백이나 분석 제공 불가능
IncorrectArticles 엔티티가 이러한 필드를 지원하는지 확인하고, 지원한다면 모든 요청 데이터를 저장하도록 수정해야 합니다. 지원하지 않는다면 엔티티 스키마를 확장해야 합니다.
📝 요청 데이터를 완전히 활용하는 수정안
public void saveQuizResult(String jwt, QuizResultRequest request) {
Long userId = jwtProvider.getUserId(jwt);
IncorrectArticles incorrectArticles = IncorrectArticles.builder()
.userId(userId)
.articleId(request.getArticleId())
+ .correct(request.isCorrect())
+ .selectedOption(request.getSelectedOption())
.solDate(LocalDateTime.now())
.build();
incorrectArticlesRepository.save(incorrectArticles);
}참고: IncorrectArticles 엔티티에 해당 필드가 존재해야 합니다.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
@server/src/main/java/oba/backend/server/domain/quiz/service/QuizResultService.java
around lines 19 - 29, saveQuizResult currently only persists articleId and
ignores QuizResultRequest.correct and QuizResultRequest.selectedOption; update
the persistence so IncorrectArticles contains and stores these fields: when
building the entity in saveQuizResult (IncorrectArticles.builder(...) in
QuizResultService.saveQuizResult) set the fields for correct and selectedOption
from request; if the IncorrectArticles entity lacks those fields, extend the
entity (add boolean correct and String selectedOption or appropriate types),
add/update repository methods if needed and create a DB migration to add the
columns, then persist via incorrectArticlesRepository.save(incorrectArticles).
| import io.jsonwebtoken.*; | ||
| import io.jsonwebtoken.io.Decoders; | ||
| import io.jsonwebtoken.security.Keys; | ||
| import oba.backend.server.global.auth.dto.TokenResponse; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "pom.xml" -o -name "build.gradle" | head -10Repository: OneBiteArticle/oba_backend
Length of output: 91
🏁 Script executed:
cat -n server/src/main/java/oba/backend/server/global/auth/jwt/JwtProvider.java | head -100Repository: OneBiteArticle/oba_backend
Length of output: 4230
🌐 Web query:
JJWT io.jsonwebtoken 2026 current version recommended API Jwts.parserBuilder deprecated
💡 Result:
Short answer: Use the newer JwtParserBuilder-style API (configure then build) — but note the specific methods changed in the 0.12.x+ line: Jwts.parserBuilder() was removed/changed and Jwts.parser() now returns a builder; parser/setSigningKey-style mutation methods were replaced by builder methods such as verifyWith(...). The current published JJWT artifacts (as of Jan 2026) are in the 0.13.x series. [1][2][3]
Recommended pattern (example):
- For 0.12.x+ / 0.13.x: call Jwts.parser(), configure with verifyWith(...)/other builder methods, call build(), then parse.
Example:
Claims claims = Jwts.parser()
.verifyWith(key) // or other builder config
.build()
.parseSignedClaims(jwt)
.getPayload();
If you are on an older 0.11.x release you can use Jwts.parserBuilder() -> build() -> parse, but upgrade to 0.12+ / 0.13.x and switch to the new builder/verifyWith pattern. [2][3][1]
Sources:
- JJWT changelog / GitHub discussion describing 0.12.x breaking changes (parser()/parserBuilder/immutable parser, verifyWith). [2]
- StackOverflow threads showing migration examples (parser() returns builder; use verifyWith(...).build().parseSignedClaims(...)). [3]
- Javadoc / Maven index showing current published versions (0.13.x). [1]
🌐 Web query:
JJWT 0.12.x migration guide signWith key setting deprecated
💡 Result:
Summary — what changed and how to migrate
-
Deprecated overloads: methods that accept a SignatureAlgorithm (io.jsonwebtoken.SignatureAlgorithm) or setSigningKey(...) were deprecated in 0.12.x. Use the new algorithm registry and new builder methods instead. [1][2][3]
-
Signing: replace old patterns
- Old (deprecated): .signWith(SignatureAlgorithm.HS256, key) or .signWith(SignatureAlgorithm, Key/bytes)
- New (recommended): build a Key and call .signWith(Key) OR .signWith(Key, SecureDigestAlgorithm) when you need to force a particular algorithm. You can obtain keys via Keys.hmacShaKeyFor(byte[]) or via Jwts.SIG registry (e.g. Jwts.SIG.HS256). Example:
- Secret HMAC: Key k = Keys.hmacShaKeyFor(secretBytes); Jwts.builder().signWith(k).compact();
- Explicit alg: Jwts.builder().signWith(k, Jwts.SIG.HS256).compact();
Reference: JwtBuilder javadoc & migration notes. [1][3][6]
-
Parsing / verification: replace setSigningKey(...) with verifyWith(SecretKey) on the JwtParserBuilder, then build the parser. Example:
- Claims c = Jwts.parser().verifyWith(secretKey).build().parseClaimsJws(compact).getBody();
Reference: JwtParserBuilder deprecation notes. [5]
- Claims c = Jwts.parser().verifyWith(secretKey).build().parseClaimsJws(compact).getBody();
-
Algorithm names / registry: SignatureAlgorithm enum usage was superseded by Jwts.SIG (the new SecureDigestAlgorithm registry). Use Jwts.SIG.* when you need an algorithm constant. [1][6]
-
Common pitfalls
- Do not pass a raw string as a key; use Keys.hmacShaKeyFor(...) for HMAC secrets to get a SecretKey. Old convenience overloads that accepted Strings/byte[] are deprecated/removed. [3]
- If you need to force a JCA algorithm name when constructing SecretKeySpec, use the correct JCA algorithm (e.g. "HmacSHA256"/"HmacSHA512") — but prefer Keys.hmacShaKeyFor(...) or Jwts.SIG.key builders. (Discussion / examples). [4]
Sources
- JJWT JwtBuilder javadoc (0.12.x) — deprecation and signWith(Key, SecureDigestAlgorithm) guidance. [1]
- JJWT JwtBuilder javadoc (0.12.2) — deprecated signWith overloads and Keys.hmacShaKeyFor recommendation. [3]
- JJWT docs / migration examples (migration guide / Jwts.SIG usage). [6]
- StackOverflow / GitHub discussions with concrete examples and pitfalls (parser.verifyWith and correct key construction). [4][5]
🏁 Script executed:
cat server/build.gradle | grep -A 20 -B 5 "jjwt\|jsonwebtoken"Repository: OneBiteArticle/oba_backend
Length of output: 1154
JJWT 0.11.5는 outdated 버전이며, 현재 API는 0.12.x 이상에서 deprecated 됨
프로젝트는 JJWT 0.11.5를 사용 중인데, 현재(2026년) 최신 버전은 0.13.x입니다. 더 중요하게는, 현재 코드의 API 패턴들이 0.12.x부터 deprecated 되었습니다:
- Line 41:
signWith(key, SignatureAlgorithm.HS256)→ 0.13.x에서는signWith(key)또는signWith(key, Jwts.SIG.HS256)사용 - Lines 74-78:
Jwts.parserBuilder().setSigningKey(key)→ 0.13.x에서는Jwts.parser().verifyWith(key)사용, 또한parseClaimsJws()→parseSignedClaims()변경
권장사항:
- JJWT를 0.13.x로 업그레이드
signWith()호출 시SignatureAlgorithm매개변수 제거Jwts.parserBuilder().setSigningKey(key)를Jwts.parser().verifyWith(key)로 변경parseClaimsJws()를parseSignedClaims()로 변경하고,.getBody()대신.getPayload()사용
🤖 Prompt for AI Agents
In @server/src/main/java/oba/backend/server/global/auth/jwt/JwtProvider.java
around lines 3 - 6, Update JwtProvider to be compatible with JJWT 0.13.x: bump
the JJWT dependency to 0.13.x, remove the SignatureAlgorithm parameter from the
JwtBuilder.signWith(...) call in JwtProvider (use signWith(key) or signWith(key,
Jwts.SIG.HS256) as appropriate), replace Jwts.parserBuilder().setSigningKey(key)
with Jwts.parser().verifyWith(key) in the verification logic, and change
parseClaimsJws(...) usages to parseSignedClaims(...) and use .getPayload()
instead of .getBody() when extracting claims (adjust any exception handling to
match the newer API signatures).
| private String createToken(Long userId, String identifier, long validityMs) { | ||
| long now = System.currentTimeMillis(); | ||
| Date issuedAt = new Date(now); | ||
| Date expiry = new Date(now + validityMs); | ||
|
|
||
| return Jwts.builder() | ||
| .claim("userId", userId) | ||
| .setSubject(identifier) | ||
| .setIssuedAt(issuedAt) | ||
| .setExpiration(expiry) | ||
| .signWith(key, SignatureAlgorithm.HS256) | ||
| .compact(); | ||
| } | ||
|
|
||
| public String createAccessToken(Long userId, String identifier) { | ||
| return createToken(userId, identifier, accessTokenValidity); | ||
| } | ||
|
|
||
| public String createRefreshToken(Long userId, String identifier) { | ||
| return createToken(userId, identifier, refreshTokenValidity); | ||
| } | ||
|
|
||
| public TokenResponse generateTokens(Long userId, String identifier) { | ||
| return new TokenResponse( | ||
| createAccessToken(userId, identifier), | ||
| createRefreshToken(userId, identifier) | ||
| ); | ||
| } |
There was a problem hiding this comment.
Access/Refresh 토큰 구분(claim)과 검증이 없어 refresh 토큰이 access처럼 사용될 수 있습니다.
현재 validateToken()은 서명/만료만 통과하면 OK라서, 클라이언트가 refresh 토큰을 Authorization에 넣어도 인증이 성립할 가능성이 큽니다(필터가 타입을 구분하지 않으면). 토큰에 tokenType(access/refresh) 클레임을 넣고, 엔드포인트/필터에서 기대 타입을 검증하는 형태가 안전합니다.
Proposed diff
- private String createToken(Long userId, String identifier, long validityMs) {
+ private String createToken(Long userId, String identifier, String tokenType, long validityMs) {
long now = System.currentTimeMillis();
Date issuedAt = new Date(now);
Date expiry = new Date(now + validityMs);
return Jwts.builder()
.claim("userId", userId)
+ .claim("tokenType", tokenType) // "access" | "refresh"
.setSubject(identifier)
.setIssuedAt(issuedAt)
.setExpiration(expiry)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
public String createAccessToken(Long userId, String identifier) {
- return createToken(userId, identifier, accessTokenValidity);
+ return createToken(userId, identifier, "access", accessTokenValidity);
}
public String createRefreshToken(Long userId, String identifier) {
- return createToken(userId, identifier, refreshTokenValidity);
+ return createToken(userId, identifier, "refresh", refreshTokenValidity);
}
+
+ public boolean isAccessToken(String token) {
+ return "access".equals(getClaims(token).get("tokenType", String.class));
+ }
+
+ public boolean isRefreshToken(String token) {
+ return "refresh".equals(getClaims(token).get("tokenType", String.class));
+ }Also applies to: 60-86
🤖 Prompt for AI Agents
In @server/src/main/java/oba/backend/server/global/auth/jwt/JwtProvider.java
around lines 31 - 58, The token creation currently omits a token type claim so
refresh tokens can be misused as access tokens; modify createToken (used by
createAccessToken/createRefreshToken) to add a "tokenType" claim set to "access"
or "refresh" depending on which factory called it, update
createAccessToken/createRefreshToken to pass the appropriate type flag, and
update validateToken (and any authentication filter/endpoint that calls it) to
verify the "tokenType" claim matches the expected value before accepting the
token; ensure TokenResponse still returns the two tokens unchanged.
| public class CustomException { | ||
| } |
There was a problem hiding this comment.
Exception 또는 RuntimeException을 상속해야 합니다.
Java에서 예외 클래스로 사용되려면 Exception 또는 RuntimeException을 상속해야 합니다. 현재 상태로는 이 클래스를 throw하거나 catch할 수 없습니다.
비즈니스 로직 예외의 경우 일반적으로 RuntimeException을 상속합니다.
🔧 CustomException 구현 제안
package oba.backend.server.global.error;
-public class CustomException {
+public class CustomException extends RuntimeException {
+
+ private final ErrorCode errorCode;
+
+ public CustomException(ErrorCode errorCode) {
+ super(errorCode.getMessage());
+ this.errorCode = errorCode;
+ }
+
+ public CustomException(ErrorCode errorCode, String message) {
+ super(message);
+ this.errorCode = errorCode;
+ }
+
+ public ErrorCode getErrorCode() {
+ return errorCode;
+ }
}ErrorCode enum과 함께 완전한 예외 처리 구조를 구현해 드릴까요?
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public class CustomException { | |
| } | |
| public class CustomException extends RuntimeException { | |
| public CustomException(String message) { | |
| super(message); | |
| } | |
| public CustomException(String message, Throwable cause) { | |
| super(message, cause); | |
| } | |
| } |
🤖 Prompt for AI Agents
In @server/src/main/java/oba/backend/server/global/error/CustomException.java
around lines 3 - 4, CustomException is not usable as an exception because it
doesn't extend Exception or RuntimeException; change the class declaration to
extend RuntimeException, add standard constructors (no-arg, message,
message+cause, cause) delegating to super, and optionally add an ErrorCode field
(with constructor overloads and a getter) if you want to carry structured error
codes alongside the exception.
| public class GlobalExceptionHandler { | ||
| } |
There was a problem hiding this comment.
Spring 어노테이션과 핸들러 메서드가 누락되었습니다.
GlobalExceptionHandler가 실제로 예외를 처리하려면 다음이 필요합니다:
@RestControllerAdvice또는@ControllerAdvice어노테이션- 예외를 처리할
@ExceptionHandler메서드들
현재 상태로는 이 클래스가 예외를 가로채지 못합니다.
🔧 전역 예외 핸들러 구현 제안
package oba.backend.server.global.error;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
public class GlobalExceptionHandler {
+
+ @ExceptionHandler(CustomException.class)
+ public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
+ // CustomException 처리 로직
+ return ResponseEntity
+ .status(HttpStatus.BAD_REQUEST)
+ .body(new ErrorResponse(e.getMessage()));
+ }
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity<ErrorResponse> handleGeneralException(Exception e) {
+ // 일반 예외 처리 로직
+ return ResponseEntity
+ .status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .body(new ErrorResponse("An unexpected error occurred"));
+ }
}전체 에러 핸들링 인프라 구현을 도와드릴까요?
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
@server/src/main/java/oba/backend/server/global/error/GlobalExceptionHandler.java
around lines 3 - 4, The GlobalExceptionHandler class is empty and missing Spring
annotations and handler methods; annotate the class with @RestControllerAdvice
(or @ControllerAdvice plus @ResponseBody) and add @ExceptionHandler methods such
as a generic handler for Exception and specific handlers for known exceptions
(e.g., MethodArgumentNotValidException, EntityNotFoundException) that return
ResponseEntity or a standardized error DTO, include logging inside each handler,
and ensure handlers set appropriate HTTP status codes and response bodies so the
class actually intercepts and formats exceptions.
| spring: | ||
| datasource: | ||
| url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 | ||
| driver-class-name: org.h2.Driver | ||
| username: sa | ||
| password: | ||
| jpa: | ||
| hibernate: | ||
| ddl-auto: create-drop | ||
| show-sql: false | ||
| properties: | ||
| hibernate: | ||
| dialect: org.hibernate.dialect.H2Dialect |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify MongoDB repositories and external service configurations
# MongoDB 저장소 인터페이스 검색
echo "=== MongoDB 저장소 ==="
rg -n --type java "extends\s+MongoRepository" -A 2
# OAuth2 관련 빈 검색
echo -e "\n=== OAuth2 관련 컴포넌트 ==="
rg -n --type java "(OAuth2|OAuth)" --type java | head -20
# JWT 관련 빈 검색
echo -e "\n=== JWT 관련 컴포넌트 ==="
rg -n --type java "Jwt" --type java | head -20
# ServerApplicationTests 검색
echo -e "\n=== ServerApplicationTests 파일 ==="
find . -name "*ServerApplicationTests*" -type f
# AI 서비스 설정 검색
echo -e "\n=== AI 서비스 설정 ==="
rg -n --type java "FastAPI\|GptDocument\|AI.*Service" | head -20Repository: OneBiteArticle/oba_backend
Length of output: 6115
🏁 Script executed:
#!/bin/bash
# Check ServerApplicationTests to understand context loading
echo "=== ServerApplicationTests 내용 ==="
cat server/src/test/java/oba/backend/server/ServerApplicationTests.java
echo -e "\n=== 메인 application.yml ==="
find . -path "*/main/resources/application.yml" -type f | head -1 | xargs cat
echo -e "\n=== 조건부 bean 로드 확인 (@ConditionalOn*) ==="
rg -n "@ConditionalOn" --type java | head -20
echo -e "\n=== JwtProvider 설정 필수 여부 ==="
rg -n "JwtProvider" --type java | grep -E "(@Bean|@Configuration|constructor)" -B 2 | head -20
echo -e "\n=== SecurityConfig에서 필요한 설정 ==="
cat server/src/main/java/oba/backend/server/global/config/SecurityConfig.java | head -40Repository: OneBiteArticle/oba_backend
Length of output: 5117
🏁 Script executed:
#!/bin/bash
# Check for test-specific configurations and JwtProvider requirements
echo "=== @TestConfiguration 클래스 검색 ==="
find . -name "*.java" -path "*/test/*" -type f | xargs grep -l "@TestConfiguration" 2>/dev/null
echo -e "\n=== JwtProvider 생성자 ==="
cat server/src/main/java/oba/backend/server/global/auth/jwt/JwtProvider.java | head -50
echo -e "\n=== application-test.yml 전체 내용 확인 ==="
cat server/src/test/resources/application-test.yml
echo -e "\n=== 테스트 클래스에서 MongoDB 사용 여부 ==="
find . -path "*/test/java/*" -name "*.java" -type f | xargs grep -l "MongoRepository\|GptMongoRepository" 2>/dev/null | head -5Repository: OneBiteArticle/oba_backend
Length of output: 2306
🏁 Script executed:
#!/bin/bash
# Check if JwtAuthenticationFilter and related beans are always instantiated during context load
echo "=== JwtAuthenticationFilter 클래스 ==="
cat server/src/main/java/oba/backend/server/global/auth/jwt/JwtAuthenticationFilter.java | head -40
echo -e "\n=== CustomOAuth2UserService 빈 처리 ==="
cat server/src/main/java/oba/backend/server/global/auth/oauth/CustomOAuth2UserService.java | head -30
echo -e "\n=== SecurityConfig의 빈 의존성 ==="
cat server/src/main/java/oba/backend/server/global/config/SecurityConfig.java | head -60Repository: OneBiteArticle/oba_backend
Length of output: 5463
🏁 Script executed:
#!/bin/bash
# Verify if the test context will actually fail without JWT properties
echo "=== Spring 부트 테스트 시작 시뮬레이션 ==="
echo "SecurityConfig가 로드되면서:"
echo "1. JwtAuthenticationFilter를 빈으로 등록하려고 시도"
echo "2. JwtAuthenticationFilter는 JwtProvider 의존성 필요"
echo "3. JwtProvider는 @Component이며 생성자에서 @Value로 jwt.secret 주입 필요"
echo ""
echo "=== application-test.yml에서 jwt.* 속성 확인 ==="
grep -i "jwt" server/src/test/resources/application-test.yml || echo "jwt 속성 없음"
echo -e "\n=== 다른 테스트 설정 파일 존재 여부 ==="
find server/src/test/resources -name "application*.yml" -o -name "application*.properties" 2>/dev/null
echo -e "\n=== OAuth2 ClientRegistration 로드 시 필요한 속성 ==="
echo "SecurityConfig에서 OAuth2Login 설정:"
grep -A 5 "oauth2Login" server/src/main/java/oba/backend/server/global/config/SecurityConfig.java
echo -e "\n=== MongoDB 빈 자동 로드 여부 ==="
rg -n "@EnableMongoRepositories|@EnableAutoConfiguration" --type java server/src/main/java | head -10Repository: OneBiteArticle/oba_backend
Length of output: 820
🏁 Script executed:
#!/bin/bash
# Final verification: Check if there's any test-specific bean override or if these are eagerly loaded
echo "=== 테스트에서 @MockBean 또는 @SpyBean 사용 여부 ==="
find server/src/test -name "*.java" -type f | xargs grep -l "@MockBean\|@SpyBean" 2>/dev/null | head -5
echo -e "\n=== 빈 로드 우선순위: SecurityConfig 반드시 로드되는가? ==="
grep -r "SecurityConfig" server/src/main/java --include="*.java" | grep -i "import\|@Import" | head -5
echo -e "\n=== OAuth2 클라이언트 ID가 없으면 어떻게 되나? ==="
echo "Spring Security OAuth2는 spring.security.oauth2.client.registration.* 속성이 없으면 IllegalArgumentException 발생"
echo -e "\n=== AI 서비스 및 MongoDB 의존성 있는 빈 찾기 ==="
rg -n "@Component|@Service|@Repository" --type java server/src/main/java | grep -E "Gpt|Ai|Article" | head -10Repository: OneBiteArticle/oba_backend
Length of output: 716
🏁 Script executed:
#!/bin/bash
# Final verification: Check if context loading would actually fail
echo "=== AiService 의존성 확인 ==="
cat server/src/main/java/oba/backend/server/domain/ai/service/AiService.java | head -30
echo -e "\n=== ArticleSummaryService 의존성 확인 ==="
cat server/src/main/java/oba/backend/server/domain/article/service/ArticleSummaryService.java | head -30
echo -e "\n=== 테스트 실행 시 필요한 프로파일별 설정 정리 ==="
echo "application-test.yml에 있는 것:"
cat server/src/test/resources/application-test.yml
echo -e "\n=== 메인 application.yml에 있는데 테스트에 없는 것 ==="
echo "- jwt.secret (필수: JwtProvider @Component 생성 시)"
echo "- jwt.access-token-expiration-ms (필수: JwtProvider @Component 생성 시)"
echo "- jwt.refresh-token-expiration-ms (필수: JwtProvider @Component 생성 시)"
echo "- spring.security.oauth2.client.registration.* (OAuth2 설정)"
echo "- spring.data.mongodb.* (MongoDB 설정)"
echo "- ai.server.url (AI 서비스 설정)"Repository: OneBiteArticle/oba_backend
Length of output: 2686
테스트 설정에서 필수 구성 요소들이 누락되어 컨텍스트 로드 실패 위험이 있습니다.
application-test.yml은 H2 인메모리 데이터베이스와 JPA 설정만 포함하고 있으나, 프로젝트의 여러 필수 컴포넌트에서 다음 설정들이 필요합니다:
-
JWT 설정 (필수): JwtProvider는
@Component로jwt.secret,jwt.access-token-expiration-ms,jwt.refresh-token-expiration-ms를 @value로 주입받습니다. JwtAuthenticationFilter → SecurityConfig로 의존성이 연결되어 있어 이 설정이 없으면 빈 생성 실패로 컨텍스트 로드가 실패합니다. -
OAuth2 클라이언트 설정 (필수): CustomOAuth2UserService가 SecurityConfig에 주입되며, Spring Security OAuth2는 클라이언트 등록 정보 없이 로드되지 않습니다.
-
MongoDB 설정 (필수): ArticleSummaryService가 GptMongoRepository에 의존하며, @service로 스캔되어 컨텍스트에 로드될 때 MongoDB 설정이 필요합니다.
ServerApplicationTests는 @SpringBootTest에서 전체 애플리케이션 컨텍스트를 로드하므로, 위 설정들을 application-test.yml에 추가하거나 테스트 전용 모킹 전략을 도입해야 합니다.
🤖 Prompt for AI Agents
In @server/src/test/resources/application-test.yml around lines 1 - 13, The test
config is missing JWT, OAuth2 client, and MongoDB settings causing bean creation
failures for JwtProvider, JwtAuthenticationFilter/SecurityConfig,
CustomOAuth2UserService, ArticleSummaryService and GptMongoRepository when
ServerApplicationTests loads the full context; fix by either (A) extending
application-test.yml with the required properties: add jwt.secret,
jwt.access-token-expiration-ms, jwt.refresh-token-expiration-ms; add minimal
spring.security.oauth2.client.registration.* entries (e.g.
client-id/client-secret/placeholders) and
spring.security.oauth2.client.provider.* as needed; and add MongoDB test URI
(spring.data.mongodb.uri) or enable an embedded MongoDB for tests, OR (B) update
ServerApplicationTests to avoid loading those real beans by adding @MockBean for
JwtProvider, CustomOAuth2UserService, GptMongoRepository/ArticleSummaryService
(or exclude Security/Mongo auto-config) so tests can load without full external
config.
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In
@server/src/main/java/oba/backend/server/domain/article/dto/ArticleDetailResponse.java:
- Around line 19-21: The content field in ArticleDetailResponse is currently
typed as List<Object>, which is unsafe; replace it with a typed structure by
introducing a ContentElement type and using List<ContentElement> for the content
field: either define a sealed interface ContentElement with implementations
TextContent and ImageContent (or similar names) and change
ArticleDetailResponse.content to List<ContentElement>, or implement a tagged
union ContentElement class with a ContentType enum and fields for text/image and
use List<ContentElement> in ArticleDetailResponse; update any
serializers/deserializers and builders/DTO mapping code that construct or
consume ArticleDetailResponse to create the appropriate concrete ContentElement
instances.
In
@server/src/main/java/oba/backend/server/domain/article/service/ArticleDetailService.java:
- Around line 20-59: Remove the @Transactional(readOnly = true) annotation from
ArticleDetailService.getArticleDetail (Spring Data MongoDB does not use JPA
transactions) and replace the IllegalArgumentException thrown by
gptMongoRepository.findById(...) with a more appropriate not-found exception
(either throw a new ArticleNotFoundException that you create extending
RuntimeException with a message containing the id, or throw
EntityNotFoundException) so the controller advice can map it to 404; keep the
rest of the method logic unchanged.
- Around line 62-76: parseAnswerIndex currently swallows failures and duplicates
logic in QuizAnswerParser; replace its logic by delegating to
QuizAnswerParser.toIndex(answerStr) and add logging on failures: call
QuizAnswerParser.toIndex(answerStr) inside parseAnswerIndex, catch any
exceptions around that single call and use the class logger (e.g., logger.error
or process-equivalent) to log the answerStr, options and exception, then return
the parsed index or a safe default; remove the internal numeric/text parsing to
eliminate duplication and ensure all parse errors are logged with context.
In
@server/src/main/java/oba/backend/server/domain/article/service/ArticleSummaryService.java:
- Around line 23-32: Remove the ineffective @Transactional(readOnly = true)
annotation from getLatestArticles and add validation on the limit parameter
before calling gptMongoRepository.findByOrderByServingDateDesc: ensure limit is
positive and clamp it to a reasonable maximum (e.g., 100) or throw an
IllegalArgumentException for invalid values; keep the existing
Cacheable(Const.CACHE_LATEST_ARTICLES, key = "#limit") and mapToSummary usage
but perform the validation at the start of getLatestArticles to prevent negative
or excessively large page requests.
In @server/src/main/resources/application.yml:
- Around line 15-22: The current global setting jpa.hibernate.ddl-auto: update
is unsafe for non-local environments; change the default application.yml to a
safe value (e.g., validate) and move environment-specific settings into profile
files (e.g., application-local.yml with jpa.hibernate.ddl-auto: create-drop or
update for local testing, and application-prod.yml/application-dev.yml with
jpa.hibernate.ddl-auto: validate), and ensure DB migrations are managed by
Flyway or Liquibase rather than relying on jpa.hibernate.ddl-auto in
dev/staging/prod.
- Around line 86-87: The oauth.bridge-url is hardcoded to
"http://localhost:9000/oauth2/bridge"; change it to read from an environment
variable so staging/production can override it by using the property key
oauth.bridge-url and binding it to OAUTH_BRIDGE_URL (with an optional safe
default for local dev), update application.yml to use the env variable
placeholder for oauth.bridge-url, and ensure OAUTH_BRIDGE_URL is documented in
your deployment/.env as suggested.
🧹 Nitpick comments (7)
server/src/main/resources/application.yml (1)
1-90: 프로필별 설정 파일 분리를 권장합니다.현재 단일
application.yml에 모든 환경 설정이 포함되어 있습니다. 환경별로 설정을 분리하면 관리가 용이하고 실수를 방지할 수 있습니다.예를 들어:
application.yml: 공통 설정application-local.yml: 로컬 개발 설정 (ddl-auto: create-drop, verbose logging)application-dev.yml: 개발 서버 설정application-prod.yml: 프로덕션 설정 (ddl-auto: validate, minimal logging)server/src/main/java/oba/backend/server/domain/article/service/ArticleSummaryService.java (1)
34-55: 요약 문장 분리 로직 개선을 고려하세요.현재
[.·\\n]패턴으로 분리하는 방식은 다음과 같은 엣지 케이스를 제대로 처리하지 못할 수 있습니다:
- "..." (연속된 마침표)
- "Dr. Smith" 또는 "U.S.A" (약어의 마침표)
- "1.5배 증가" (숫자 내 마침표)
GPT가 생성하는 요약 형식이 일관적이라면 현재 로직도 충분하지만, 더 견고한 처리를 위해 다음을 고려해보세요:
- GPT 프롬프트에서 bullet point 구분자를 명시적으로 지정 (예:
\n-)- 또는 서버 측에서 정규표현식을
\\n(줄바꿈) 기준으로만 분리💡 개선 예시
if (summaryText != null && !summaryText.isBlank()) { - // 마침표(.), 가운데점(·), 줄바꿈(\n) 기준으로 문장 분리 - bullets = Arrays.stream(summaryText.split("[.·\\n]")) + // 줄바꿈 기준으로만 분리 (GPT가 bullet point로 반환하도록 프롬프트 조정 권장) + bullets = Arrays.stream(summaryText.split("\\n")) .map(String::trim) + .map(s -> s.replaceFirst("^[-•·]\\s*", "")) // 앞의 bullet 기호 제거 .filter(s -> !s.isBlank()) .limit(3) .toList();server/src/main/java/oba/backend/server/domain/article/entity/GptDocument.java (3)
10-13: @DaTa 사용 대신 명시적인 Lombok 애노테이션을 권장합니다.엔티티 클래스에
@Data를 사용하면 다음과 같은 문제가 발생할 수 있습니다:
- toString 성능 문제: 대량의
content나 중첩된 컬렉션을 자동으로 문자열화하면 메모리/CPU를 과도하게 사용할 수 있습니다.- equals/hashCode 의미론: MongoDB 문서의 동등성 비교가 모든 필드 기반으로 이루어지는 것이 항상 적절하지는 않습니다. (예:
id기반 비교가 더 적절할 수 있음)- 불변성 깨짐: 자동 생성된 setter가 엔티티를 변경 가능하게 만듭니다.
더 명확한 방법:
@Getter @Setter // 필요한 경우만 @ToString(exclude = {"content", "gptResult"}) // 큰 필드 제외 @EqualsAndHashCode(of = "id") // ID 기반 동등성또는 불변 엔티티가 필요하다면:
@Getter @Builder @AllArgsConstructor @NoArgsConstructor
29-30: List 타입은 타입 안정성을 해칩니다.
content필드가List<Object>로 선언되어 있어 컴파일 타임 타입 검사를 받을 수 없고, 런타임에ClassCastException이 발생할 위험이 있습니다.가능하다면 더 구체적인 타입을 사용하세요:
- AI 서버가 항상 문자열을 반환한다면
List<String>- 구조화된 데이터라면 별도의 DTO 클래스 정의 (예:
List<ContentBlock>)- 혼합 타입이 불가피하다면 최소한
List<?>또는 문서화 주석으로 예상 타입 명시💡 개선 예시
구조화된 타입 사용:
@Data public static class ContentBlock { private String type; // "text", "image", "quote" 등 private String value; private Map<String, Object> metadata; } @Field("content_col") private List<ContentBlock> content;또는 최소한:
/** * AI 서버가 반환하는 컨텐츠 블록 리스트. * 각 요소는 Map<String, Object> 형태로 type과 value 키를 포함합니다. */ @Field("content_col") private List<Object> content;
60-70: 편의 메서드의 null 반환과 방어적 복사를 개선하세요.현재 구현의 문제점:
null 반환:
gptResult가 null일 때 각 메서드가 null을 반환하여 호출자가 null 체크를 해야 합니다. 빈 컬렉션을 반환하는 것이 더 안전합니다.가변 참조 노출:
getKeywords()와getQuizzes()가 내부 리스트의 직접 참조를 반환하여 외부에서 리스트를 수정할 수 있습니다.♻️ 개선 제안
public String getSummary() { - return gptResult != null ? gptResult.getSummary() : null; + return gptResult != null ? gptResult.getSummary() : ""; } public List<GptResult.Keyword> getKeywords() { - return gptResult != null ? gptResult.getKeywords() : null; + if (gptResult == null || gptResult.getKeywords() == null) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(gptResult.getKeywords()); } public List<GptResult.Quiz> getQuizzes() { - return gptResult != null ? gptResult.getQuizzes() : null; + if (gptResult == null || gptResult.getQuizzes() == null) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(gptResult.getQuizzes()); }또는 Java 16+ 사용 시:
public List<GptResult.Keyword> getKeywords() { return gptResult != null && gptResult.getKeywords() != null ? List.copyOf(gptResult.getKeywords()) : List.of(); }server/src/main/java/oba/backend/server/domain/article/dto/ArticleDetailResponse.java (2)
27-34: QuizDto 구조 개선 제안중첩 클래스 구조는 적절하나, 다음 개선사항을 고려할 수 있습니다:
answer필드가options리스트의 유효한 인덱스 범위 내에 있는지 보장할 수 없습니다- 불변성 강화를 위해 필드를
final로 선언하는 것이 좋습니다♻️ 개선 제안
@Getter @Builder public static class QuizDto { - private String question; - private List<String> options; - private int answer; // 프론트엔드에서는 인덱스(0, 1, 2, 3)를 기대함 - private String explanation; + private final String question; + private final List<String> options; + private final int answer; // 프론트엔드에서는 인덱스(0, 1, 2, 3)를 기대함 + private final String explanation; }추가로 서비스 레이어에서
answer값이options.size()범위 내에 있는지 검증하는 것을 권장합니다.
8-35: 불변성 강화를 위한 final 키워드 추가 권장응답 DTO의 필드들이
final로 선언되지 않아 완전한 불변성이 보장되지 않습니다.@Builder와@Getter만으로는 리플렉션이나 의도치 않은 필드 수정을 방지할 수 없습니다.♻️ 불변 필드 적용 제안
@Getter @Builder public class ArticleDetailResponse { // 🚨 수정됨: Long -> String - private String articleId; + private final String articleId; - private String title; - private String publishTime; - private String servingDate; + private final String title; + private final String publishTime; + private final String servingDate; // 본문은 텍스트와 이미지 태그가 섞여 있으므로 Object 리스트 - private List<Object> content; - private List<String> subtitle; + private final List<Object> content; + private final List<String> subtitle; - private String summary; - private List<String> keywords; - private List<QuizDto> quizzes; + private final String summary; + private final List<String> keywords; + private final List<QuizDto> quizzes;참고: 리스트 필드의 경우
List.copyOf()를 사용하여 방어적 복사를 수행하면 더욱 안전합니다.📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
server/src/main/java/oba/backend/server/domain/article/controller/ArticleController.javaserver/src/main/java/oba/backend/server/domain/article/dto/ArticleDetailResponse.javaserver/src/main/java/oba/backend/server/domain/article/dto/ArticleSummaryResponse.javaserver/src/main/java/oba/backend/server/domain/article/entity/GptDocument.javaserver/src/main/java/oba/backend/server/domain/article/service/ArticleDetailService.javaserver/src/main/java/oba/backend/server/domain/article/service/ArticleSummaryService.javaserver/src/main/java/oba/backend/server/domain/quiz/service/QuizService.javaserver/src/main/java/oba/backend/server/global/config/MongoCheckRunner.javaserver/src/main/resources/application.yml🚧 Files skipped from review as they are similar to previous changes (3)
- server/src/main/java/oba/backend/server/domain/quiz/service/QuizService.java
- server/src/main/java/oba/backend/server/domain/article/controller/ArticleController.java
- server/src/main/java/oba/backend/server/domain/article/dto/ArticleSummaryResponse.java
🧰 Additional context used
🧬 Code graph analysis (2)
server/src/main/java/oba/backend/server/domain/article/service/ArticleSummaryService.java (2)
server/src/main/java/oba/backend/server/global/common/Const.java (1)
Const(3-8)server/src/main/java/oba/backend/server/domain/article/service/ArticleDetailService.java (1)
Service(13-77)server/src/main/java/oba/backend/server/domain/article/dto/ArticleDetailResponse.java (9)
server/src/main/java/oba/backend/server/domain/article/dto/ArticleSummaryResponse.java (1)
Getter(8-17)server/src/main/java/oba/backend/server/domain/quiz/dto/WrongArticleResponse.java (1)
Getter(8-18)server/src/main/java/oba/backend/server/domain/quiz/dto/SolvedArticleResponse.java (1)
Getter(8-17)server/src/main/java/oba/backend/server/domain/quiz/dto/QuizResultRequest.java (1)
Getter(6-12)server/src/main/java/oba/backend/server/domain/quiz/dto/QuizSubmitRequest.java (1)
Getter(7-12)server/src/main/java/oba/backend/server/domain/quiz/entity/IncorrectQuizId.java (1)
Getter(7-29)server/src/main/java/oba/backend/server/global/auth/oauth/CustomOAuth2User.java (1)
Getter(11-36)server/src/main/java/oba/backend/server/global/auth/dto/TokenResponse.java (1)
Getter(6-11)server/src/main/java/oba/backend/server/global/auth/dto/LoginRequest.java (1)
Getter(5-8)
| // 🚨 수정됨: Long -> String | ||
| private String articleId; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
echo "=== Long articleId를 사용하는 클래스 검색 ==="
rg -n 'Long\s+articleId' --type java
echo -e "\n=== String articleId를 사용하는 클래스 검색 ==="
rg -n 'String\s+articleId' --type java
echo -e "\n=== 퀴즈 관련 DTO 및 엔티티에서 articleId 검색 ==="
fd -e java | xargs rg -l 'articleId' | grep -iE '(quiz|article)' | head -20
echo -e "\n=== 주요 파일들의 articleId 타입 상세 확인 ==="
for file in $(fd -e java | xargs rg -l 'articleId' | grep -iE '(QuizSubmitRequest|QuizResultRequest|SolvedArticle|WrongArticle|ArticleDetail)'); do
echo "--- $file ---"
rg -n -B 2 -A 2 'articleId' "$file" | head -30
doneRepository: OneBiteArticle/oba_backend
Length of output: 4997
모든 articleId 참조가 String으로 일관되게 업데이트되어야 함
ArticleDetailResponse와 ArticleSummaryResponse에서 articleId가 String으로 변경되었으나, 퀴즈 관련 DTO들(QuizSubmitRequest, QuizResultRequest, SolvedArticleResponse, WrongArticleResponse)과 퀴즈 엔티티(IncorrectQuiz, IncorrectQuizId, IncorrectArticles, IncorrectArticlesId) 및 GptDocument는 여전히 Long articleId를 사용 중입니다. 이로 인해 아티클 조회 응답에서 반환된 String 타입의 articleId를 퀴즈 작업(제출, 결과 저장 등)으로 전달할 때 타입 불일치 에러가 발생합니다.
퀴즈 도메인의 모든 articleId 필드와 관련 메서드 매개변수를 Long에서 String으로 일관되게 업데이트하거나, 아티클 응답 DTO를 Long으로 롤백해야 합니다.
| // 본문은 텍스트와 이미지 태그가 섞여 있으므로 Object 리스트 | ||
| private List<Object> content; | ||
| private List<String> subtitle; |
There was a problem hiding this comment.
타입 안전성 개선을 위한 content 필드 구조화 권장
List<Object> 타입은 컴파일 타임 타입 안전성을 제공하지 않아 런타임에 ClassCastException 위험이 있습니다. 텍스트와 이미지 태그가 혼합되어 있다면, 명시적인 타입 구조를 사용하는 것이 좋습니다.
📐 타입 안전 대안 제안
방안 1: Sealed interface 사용 (Java 17+)
public sealed interface ContentElement permits TextContent, ImageContent {}
@Getter
@Builder
public final class TextContent implements ContentElement {
private String text;
}
@Getter
@Builder
public final class ImageContent implements ContentElement {
private String imageUrl;
private String altText;
}
// ArticleDetailResponse에서:
private List<ContentElement> content;방안 2: 태그된 유니온 패턴
@Getter
@Builder
public static class ContentElement {
private ContentType type; // TEXT, IMAGE
private String text; // TEXT일 때 사용
private String imageUrl; // IMAGE일 때 사용
private String altText; // IMAGE일 때 사용
}
public enum ContentType {
TEXT, IMAGE
}
// ArticleDetailResponse에서:
private List<ContentElement> content;🤖 Prompt for AI Agents
In
@server/src/main/java/oba/backend/server/domain/article/dto/ArticleDetailResponse.java
around lines 19 - 21, The content field in ArticleDetailResponse is currently
typed as List<Object>, which is unsafe; replace it with a typed structure by
introducing a ContentElement type and using List<ContentElement> for the content
field: either define a sealed interface ContentElement with implementations
TextContent and ImageContent (or similar names) and change
ArticleDetailResponse.content to List<ContentElement>, or implement a tagged
union ContentElement class with a ContentType enum and fields for text/image and
use List<ContentElement> in ArticleDetailResponse; update any
serializers/deserializers and builders/DTO mapping code that construct or
consume ArticleDetailResponse to create the appropriate concrete ContentElement
instances.
| @Transactional(readOnly = true) | ||
| public ArticleDetailResponse getArticleDetail(String id) { | ||
|
|
||
| // MongoDB _id(String)로 조회 | ||
| GptDocument doc = gptMongoRepository.findById(id) | ||
| .orElseThrow(() -> new IllegalArgumentException("해당 ID의 기사를 찾을 수 없습니다: " + id)); | ||
|
|
||
| // 키워드 리스트 매핑 | ||
| List<String> keywordList = Collections.emptyList(); | ||
| if (doc.getKeywords() != null) { | ||
| keywordList = doc.getKeywords().stream() | ||
| .map(GptDocument.GptResult.Keyword::getKeyword) | ||
| .toList(); | ||
| } | ||
|
|
||
| // 퀴즈 리스트 매핑 | ||
| List<ArticleDetailResponse.QuizDto> quizList = Collections.emptyList(); | ||
| if (doc.getQuizzes() != null) { | ||
| quizList = doc.getQuizzes().stream() | ||
| .map(q -> ArticleDetailResponse.QuizDto.builder() | ||
| .question(q.getQuestion()) | ||
| .options(q.getOptions()) | ||
| .answer(parseAnswerIndex(q.getAnswer(), q.getOptions())) // 정답 인덱스 변환 로직 | ||
| .explanation(q.getExplanation()) | ||
| .build()) | ||
| .toList(); | ||
| } | ||
|
|
||
| return ArticleDetailResponse.builder() | ||
| .articleId(doc.getId()) // String ID 사용 | ||
| .title(doc.getTitle()) | ||
| .publishTime(doc.getPublishTime()) | ||
| .servingDate(doc.getServingDate()) | ||
| .content(doc.getContent()) // List<Object> | ||
| .subtitle(doc.getSubtitle()) // List<String> | ||
| .summary(doc.getSummary()) | ||
| .keywords(keywordList) | ||
| .quizzes(quizList) | ||
| .build(); | ||
| } |
There was a problem hiding this comment.
MongoDB 조회에 @transactional이 불필요하며, 예외 처리를 개선하세요.
-
@transactional 제거: Spring Data MongoDB는 JPA 트랜잭션을 사용하지 않으므로
@Transactional(readOnly = true)가 효과가 없습니다. -
예외 타입 개선: Line 25의
IllegalArgumentException은 "not found" 상황에 적합하지 않습니다. REST API에서는 일반적으로 다음을 사용합니다:- 커스텀
ArticleNotFoundException(권장) - 또는
EntityNotFoundException - 이를 통해
@ControllerAdvice에서 404 상태 코드로 자동 변환 가능
- 커스텀
♻️ 개선 제안
커스텀 예외 생성:
public class ArticleNotFoundException extends RuntimeException {
public ArticleNotFoundException(String id) {
super("해당 ID의 기사를 찾을 수 없습니다: " + id);
}
}서비스 수정:
- @Transactional(readOnly = true)
public ArticleDetailResponse getArticleDetail(String id) {
GptDocument doc = gptMongoRepository.findById(id)
- .orElseThrow(() -> new IllegalArgumentException("해당 ID의 기사를 찾을 수 없습니다: " + id));
+ .orElseThrow(() -> new ArticleNotFoundException(id));🤖 Prompt for AI Agents
In
@server/src/main/java/oba/backend/server/domain/article/service/ArticleDetailService.java
around lines 20 - 59, Remove the @Transactional(readOnly = true) annotation from
ArticleDetailService.getArticleDetail (Spring Data MongoDB does not use JPA
transactions) and replace the IllegalArgumentException thrown by
gptMongoRepository.findById(...) with a more appropriate not-found exception
(either throw a new ArticleNotFoundException that you create extending
RuntimeException with a message containing the id, or throw
EntityNotFoundException) so the controller advice can map it to 404; keep the
rest of the method logic unchanged.
| private int parseAnswerIndex(String answerStr, List<String> options) { | ||
| try { | ||
| // 1. 숫자만 있는 경우 ("0", "1" 등) | ||
| if (answerStr.matches("\\d+")) { | ||
| return Integer.parseInt(answerStr); | ||
| } | ||
| // 2. 정답 텍스트 자체가 들어있는 경우 -> 보기 리스트에서 찾기 | ||
| int idx = options.indexOf(answerStr); | ||
| if (idx != -1) return idx; | ||
|
|
||
| return 0; // 기본값 (에러 방지) | ||
| } catch (Exception e) { | ||
| return 0; | ||
| } | ||
| } |
There was a problem hiding this comment.
정답 파싱 실패 시 로깅을 추가하고 중복 코드를 제거하세요.
-
침묵하는 실패: Line 72-74에서 정답 파싱이 실패해도
0을 반환하며 로그를 남기지 않아 데이터 품질 문제를 감지하기 어렵습니다. GPT 응답 형식이 변경되거나 잘못된 데이터가 들어와도 알 수 없습니다. -
코드 중복: 컨텍스트에
QuizAnswerParser.toIndex(String answer)메서드가 있는 것으로 보이며, 동일한 로직을 구현하고 있습니다. 중복을 제거하고 하나의 구현을 공유하세요.
♻️ 개선 제안
로깅 추가:
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
@Service
@RequiredArgsConstructor
public class ArticleDetailService {
+ private static final Logger log = LoggerFactory.getLogger(ArticleDetailService.class);
private int parseAnswerIndex(String answerStr, List<String> options) {
try {
if (answerStr.matches("\\d+")) {
return Integer.parseInt(answerStr);
}
int idx = options.indexOf(answerStr);
if (idx != -1) return idx;
+ log.warn("정답을 파싱할 수 없어 기본값 0 반환: answerStr={}, options={}", answerStr, options);
return 0;
} catch (Exception e) {
+ log.error("정답 파싱 중 예외 발생, 기본값 0 반환: answerStr={}", answerStr, e);
return 0;
}
}또는 QuizAnswerParser로 통합:
- private int parseAnswerIndex(String answerStr, List<String> options) {
- // ... 중복 코드 제거
- }
+ private int parseAnswerIndex(String answerStr, List<String> options) {
+ return QuizAnswerParser.toIndex(answerStr, options);
+ }🤖 Prompt for AI Agents
In
@server/src/main/java/oba/backend/server/domain/article/service/ArticleDetailService.java
around lines 62 - 76, parseAnswerIndex currently swallows failures and
duplicates logic in QuizAnswerParser; replace its logic by delegating to
QuizAnswerParser.toIndex(answerStr) and add logging on failures: call
QuizAnswerParser.toIndex(answerStr) inside parseAnswerIndex, catch any
exceptions around that single call and use the class logger (e.g., logger.error
or process-equivalent) to log the answerStr, options and exception, then return
the parsed index or a safe default; remove the internal numeric/text parsing to
eliminate duplication and ensure all parse errors are logged with context.
| @Cacheable(value = Const.CACHE_LATEST_ARTICLES, key = "#limit") | ||
| @Transactional(readOnly = true) | ||
| public List<ArticleSummaryResponse> getLatestArticles(int limit) { | ||
| // MongoDB에서 servingDate 기준 내림차순 조회 | ||
| List<GptDocument> docs = gptMongoRepository.findByOrderByServingDateDesc(PageRequest.of(0, limit)); | ||
|
|
||
| return docs.stream() | ||
| .map(this::mapToSummary) | ||
| .toList(); | ||
| } |
There was a problem hiding this comment.
MongoDB 조회에 @transactional이 불필요합니다.
Spring Data MongoDB는 기본적으로 JPA 트랜잭션을 사용하지 않으므로 @Transactional(readOnly = true) 애노테이션이 효과가 없습니다. MongoDB는 자체적인 트랜잭션 모델을 사용하며, 단순 조회에는 트랜잭션이 필요하지 않습니다.
추가로 limit 파라미터에 대한 검증이 없어 음수나 과도하게 큰 값이 전달될 수 있습니다.
♻️ 개선 제안
- @Cacheable(value = Const.CACHE_LATEST_ARTICLES, key = "#limit")
- @Transactional(readOnly = true)
public List<ArticleSummaryResponse> getLatestArticles(int limit) {
+ if (limit <= 0 || limit > 100) {
+ throw new IllegalArgumentException("limit은 1~100 사이여야 합니다: " + limit);
+ }
+
// MongoDB에서 servingDate 기준 내림차순 조회
List<GptDocument> docs = gptMongoRepository.findByOrderByServingDateDesc(PageRequest.of(0, limit));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Cacheable(value = Const.CACHE_LATEST_ARTICLES, key = "#limit") | |
| @Transactional(readOnly = true) | |
| public List<ArticleSummaryResponse> getLatestArticles(int limit) { | |
| // MongoDB에서 servingDate 기준 내림차순 조회 | |
| List<GptDocument> docs = gptMongoRepository.findByOrderByServingDateDesc(PageRequest.of(0, limit)); | |
| return docs.stream() | |
| .map(this::mapToSummary) | |
| .toList(); | |
| } | |
| @Cacheable(value = Const.CACHE_LATEST_ARTICLES, key = "#limit") | |
| public List<ArticleSummaryResponse> getLatestArticles(int limit) { | |
| if (limit <= 0 || limit > 100) { | |
| throw new IllegalArgumentException("limit은 1~100 사이여야 합니다: " + limit); | |
| } | |
| // MongoDB에서 servingDate 기준 내림차순 조회 | |
| List<GptDocument> docs = gptMongoRepository.findByOrderByServingDateDesc(PageRequest.of(0, limit)); | |
| return docs.stream() | |
| .map(this::mapToSummary) | |
| .toList(); | |
| } |
🤖 Prompt for AI Agents
In
@server/src/main/java/oba/backend/server/domain/article/service/ArticleSummaryService.java
around lines 23 - 32, Remove the ineffective @Transactional(readOnly = true)
annotation from getLatestArticles and add validation on the limit parameter
before calling gptMongoRepository.findByOrderByServingDateDesc: ensure limit is
positive and clamp it to a reasonable maximum (e.g., 100) or throw an
IllegalArgumentException for invalid values; keep the existing
Cacheable(Const.CACHE_LATEST_ARTICLES, key = "#limit") and mapToSummary usage
but perform the validation at the start of getLatestArticles to prevent negative
or excessively large page requests.
| @Override | ||
| public void run(String... args) throws Exception { | ||
| System.out.println("=========================================="); | ||
| System.out.println("[MongoDB 연결 확인]"); | ||
|
|
||
| // 1. 현재 연결된 데이터베이스 이름 출력 | ||
| try { | ||
| String dbName = mongoTemplate.getDb().getName(); | ||
| System.out.println("연결된 DB 이름: " + dbName); | ||
| } catch (Exception e) { | ||
| System.out.println("DB 연결 실패: " + e.getMessage()); | ||
| } | ||
|
|
||
| // 2. Repository를 통해 데이터 개수 조회 | ||
| try { | ||
| long count = repository.count(); | ||
| System.out.println("👉 'Selected_Articles' 컬렉션 데이터 개수: " + count + "개"); | ||
|
|
||
| if (count == 0) { | ||
| System.out.println("데이터가 0개입니다. 컬렉션 이름(@Document)이나 DB 주소를 확인하세요!"); | ||
| System.out.println("현재 DB에 존재하는 컬렉션 목록: " + mongoTemplate.getCollectionNames()); | ||
| } else { | ||
| System.out.println("데이터가 존재합니다! API 조회를 다시 시도해보세요."); | ||
| } | ||
| } catch (Exception e) { | ||
| System.out.println("조회 중 에러 발생: " + e.getMessage()); | ||
| } | ||
|
|
||
| System.out.println("=========================================="); | ||
| } |
There was a problem hiding this comment.
System.out.println 대신 로거를 사용하고 예외 처리를 개선하세요.
-
부적절한 로깅:
System.out.println은 로그 레벨 제어, 로그 파일 저장, 구조화된 로깅이 불가능합니다. Spring Boot 환경에서는 SLF4J 로거를 사용해야 합니다. -
과도하게 넓은 예외 처리: Line 25, 40의
catch (Exception e)는 너무 광범위합니다. 구체적인 예외 타입을 잡거나 최소한 의미 있는 로그를 남겨야 합니다. -
불필요한 throws 선언: Line 17의
throws Exception은 불필요합니다.CommandLineRunner.run()의 throws 선언을 구현할 필요는 있지만, 실제로 checked exception을 던지지 않는다면 제거하는 것이 명확합니다.
♻️ 개선 제안
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
@Component
@RequiredArgsConstructor
public class MongoCheckRunner implements CommandLineRunner {
+ private static final Logger log = LoggerFactory.getLogger(MongoCheckRunner.class);
private final MongoTemplate mongoTemplate;
private final GptMongoRepository repository;
@Override
- public void run(String... args) throws Exception {
- System.out.println("==========================================");
- System.out.println("[MongoDB 연결 확인]");
+ public void run(String... args) {
+ log.info("==========================================");
+ log.info("[MongoDB 연결 확인]");
try {
String dbName = mongoTemplate.getDb().getName();
- System.out.println("연결된 DB 이름: " + dbName);
+ log.info("연결된 DB 이름: {}", dbName);
} catch (Exception e) {
- System.out.println("DB 연결 실패: " + e.getMessage());
+ log.error("DB 연결 실패", e);
}
try {
long count = repository.count();
- System.out.println("👉 'Selected_Articles' 컬렉션 데이터 개수: " + count + "개");
+ log.info("👉 'Selected_Articles' 컬렉션 데이터 개수: {}개", count);
if (count == 0) {
- System.out.println("데이터가 0개입니다. 컬렉션 이름(@Document)이나 DB 주소를 확인하세요!");
- System.out.println("현재 DB에 존재하는 컬렉션 목록: " + mongoTemplate.getCollectionNames());
+ log.warn("데이터가 0개입니다. 컬렉션 이름(@Document)이나 DB 주소를 확인하세요!");
+ log.info("현재 DB에 존재하는 컬렉션 목록: {}", mongoTemplate.getCollectionNames());
} else {
- System.out.println("데이터가 존재합니다! API 조회를 다시 시도해보세요.");
+ log.info("데이터가 존재합니다! API 조회를 다시 시도해보세요.");
}
} catch (Exception e) {
- System.out.println("조회 중 에러 발생: " + e.getMessage());
+ log.error("조회 중 에러 발생", e);
}
- System.out.println("==========================================");
+ log.info("==========================================");
}
}| jpa: | ||
| hibernate: | ||
| ddl-auto: update # 운영에서는 validate 권장 | ||
| show-sql: true | ||
| properties: | ||
| hibernate: | ||
| format_sql: true | ||
| dialect: org.hibernate.dialect.MySQLDialect |
There was a problem hiding this comment.
ddl-auto: update는 모든 환경에서 위험합니다.
ddl-auto: update는 프로덕션뿐만 아니라 개발/테스트 환경에서도 예상치 못한 스키마 변경이나 데이터 손실을 초래할 수 있습니다. 특히 엔티티 필드 타입 변경 시 컬럼이 DROP/ADD될 수 있습니다.
프로필별 설정 분리를 권장합니다:
local프로필:create-drop또는update(주의하여 사용)dev/staging/prod프로필:validate+ Flyway/Liquibase를 사용한 명시적 마이그레이션
📝 프로필별 설정 분리 예시
application-local.yml:
spring:
jpa:
hibernate:
ddl-auto: create-drop # 로컬 개발용application-prod.yml:
spring:
jpa:
hibernate:
ddl-auto: validate # 프로덕션용🤖 Prompt for AI Agents
In @server/src/main/resources/application.yml around lines 15 - 22, The current
global setting jpa.hibernate.ddl-auto: update is unsafe for non-local
environments; change the default application.yml to a safe value (e.g.,
validate) and move environment-specific settings into profile files (e.g.,
application-local.yml with jpa.hibernate.ddl-auto: create-drop or update for
local testing, and application-prod.yml/application-dev.yml with
jpa.hibernate.ddl-auto: validate), and ensure DB migrations are managed by
Flyway or Liquibase rather than relying on jpa.hibernate.ddl-auto in
dev/staging/prod.
| oauth: | ||
| bridge-url: "http://localhost:9000/oauth2/bridge" |
There was a problem hiding this comment.
하드코딩된 localhost URL이 배포 환경에서 실패합니다.
OAuth bridge URL이 localhost:9000으로 고정되어 있어 staging, production 환경에서 OAuth 플로우가 작동하지 않습니다.
🔧 환경 변수 기반 설정으로 수정
oauth:
- bridge-url: "http://localhost:9000/oauth2/bridge"
+ bridge-url: ${OAUTH_BRIDGE_URL:http://localhost:9000/oauth2/bridge}.env 파일에 추가:
OAUTH_BRIDGE_URL=https://your-domain.com/oauth2/bridge📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| oauth: | |
| bridge-url: "http://localhost:9000/oauth2/bridge" | |
| oauth: | |
| bridge-url: ${OAUTH_BRIDGE_URL:http://localhost:9000/oauth2/bridge} |
🤖 Prompt for AI Agents
In @server/src/main/resources/application.yml around lines 86 - 87, The
oauth.bridge-url is hardcoded to "http://localhost:9000/oauth2/bridge"; change
it to read from an environment variable so staging/production can override it by
using the property key oauth.bridge-url and binding it to OAUTH_BRIDGE_URL (with
an optional safe default for local dev), update application.yml to use the env
variable placeholder for oauth.bridge-url, and ensure OAUTH_BRIDGE_URL is
documented in your deployment/.env as suggested.
There was a problem hiding this comment.
Actionable comments posted: 15
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🤖 Fix all issues with AI agents
In
`@server/src/main/java/oba/backend/server/domain/article/entity/SelectedArticle.java`:
- Around line 85-103: The getAnswerIndex() method can NPE when options is null
and uses a magic number for digit length; add a null/empty check for options
(and return -1 if options == null || options.isEmpty()) before using
options.size(), and replace the hard-coded numericPart.length() < 3 with a
clearly named constant (e.g., MAX_INDEX_DIGITS = 2) or a self-documenting
condition (numericPart.length() <= MAX_INDEX_DIGITS) so the numeric-parse branch
is explicit; keep the rest of the logic in getAnswerIndex() (answer null check,
numeric parsing, and option-matching loop) intact.
In
`@server/src/main/java/oba/backend/server/domain/article/repository/GptMongoRepository.java`:
- Line 13: Change the repository method name from findByOrderByServingDateDesc
to findAllByOrderByServingDateDesc in GptMongoRepository to comply with Spring
Data naming rules (use "All" when no filter follows find), and update any
callers — specifically the invocation in ArticleSummaryService.java — to use the
new method name so compilation and query behavior remain correct.
In
`@server/src/main/java/oba/backend/server/domain/quiz/dto/QuizResultRequest.java`:
- Around line 7-11: The DTO QuizResultRequest currently only has `@Getter` so
Jackson cannot populate private fields articleId and results during
deserialization; add setters by annotating the class with `@Setter` (or
individually add setArticleId and setResults) so Jackson can set those fields,
ensuring QuizResultRequest has public setter methods for articleId and results.
In
`@server/src/main/java/oba/backend/server/domain/quiz/entity/IncorrectQuiz.java`:
- Around line 42-49: 현재 setQuizResults 메서드는 입력이 null이거나 길이가 5가 아니면 조용히 무시하고,
리스트에 null 요소가 있으면 언박싱 시 NPE가 발생할 수 있습니다; 수정 방법은 setQuizResults에서 results가
null이거나 results.size() != 5인 경우 IllegalArgumentException을 던져 실패를 명시적으로 알리고, 각
요소가 null인지(예: results.get(i) == null) 검사하여 null 요소가 있으면 역시
IllegalArgumentException을 던지도록 한 다음 유효성이 통과되면 quiz1, quiz2, quiz3, quiz4, quiz5에
각각 할당하는 방식으로 변경하세요 (메서드명 setQuizResults와 필드 quiz1..quiz5를 수정 대상으로 참조).
In
`@server/src/main/java/oba/backend/server/domain/quiz/service/QuizQueryService.java`:
- Around line 53-72: The loop in QuizQueryService iterates IncorrectQuiz records
and calls record.getQuizResults().contains(false) without guarding for null,
risking an NPE; update the check to first handle null or empty results (e.g.,
treat null as no incorrect answers) before calling contains(false). Specifically
adjust the logic around IncorrectQuiz#getQuizResults in the for-loop so it
verifies record.getQuizResults() != null (or uses
Collections.emptyList()/Optional.ofNullable) before calling contains(false), and
only proceeds to fetch the SelectedArticle and build WrongArticleResponse when
the safe contains(false) check is true.
In
`@server/src/main/java/oba/backend/server/domain/quiz/service/QuizService.java`:
- Around line 40-46: In QuizService.java, before comparing sizes you must
null-check request.getAnswers() and article.getQuizzes() to avoid NPEs: validate
that userAnswers (from request.getAnswers()) and quizzes (from
article.getQuizzes()) are not null and throw a clear IllegalArgumentException
(or handle appropriately) if either is null, then proceed with the existing size
comparison; reference the variables userAnswers and quizzes and the methods
request.getAnswers() and article.getQuizzes() when applying the checks.
- Around line 56-65: The current save flow always creates a new IncorrectQuiz
via IncorrectQuiz.builder(), sets results with
incorrectQuiz.setQuizResults(results), and calls
incorrectQuizRepository.save(incorrectQuiz), which allows duplicate records for
the same user/article; change this to perform an upsert by first querying the
repository for an existing record via
incorrectQuizRepository.findByUserIdAndArticleId(userId, numericArticleId)
(matching the pattern used in QuizResultService.saveQuizResult), then if present
update that entity’s quizResults and save it, otherwise build and save a new
IncorrectQuiz; alternatively, if this service is intended to always create new
records, update the method name and Javadoc to make that intent explicit.
In `@server/src/main/java/oba/backend/server/domain/stats/entity/UserStats.java`:
- Around line 41-55: The updateStreak method in UserStats relies on
LocalDate.now() (server timezone) which can miscompute "today"/"yesterday" for
users in other timezones; change updateStreak(LocalDate userLocalDate) (or add
an overload) to accept the user's current date (calculated in service layer
using the user's timezone) instead of calling LocalDate.now(), then use that
parameter when comparing lastLearnedAt, incrementing currentStreak, and updating
maxStreak/lastLearnedAt so all logic in updateStreak (references: updateStreak,
lastLearnedAt, currentStreak, maxStreak) is based on the timezone-correct date
passed in.
In
`@server/src/main/java/oba/backend/server/domain/user/controller/UserController.java`:
- Around line 34-36: The current null check in UserController is unreachable
because UserService.findByIdentifier(...) throws IllegalArgumentException when a
user is missing; fix by either wrapping the call in a try-catch in
UserController to catch IllegalArgumentException and return
ResponseEntity.notFound().build(), or change UserService to provide a
non-throwing lookup (e.g., add findByIdentifierOptional(String) returning
Optional<User>) and update UserController to use that Optional instead of
relying on a null check; adjust the method references accordingly
(UserController -> calling UserService.findByIdentifier or
UserService.findByIdentifierOptional).
- Around line 27-29: Remove the redundant manual 401 check in the UserController
method that handles the /api/users/me endpoint: delete the if (userDetails ==
null) { return ResponseEntity.status(401).build(); } block (and any now-unused
imports) because authentication is enforced by Spring Security (.authenticated()
in SecurityConfig), so unauthenticated requests never reach this controller.
In `@server/src/main/java/oba/backend/server/domain/user/entity/User.java`:
- Around line 54-58: updateInfo currently assigns OAuth2UserInfo fields
directly, risking null overwrites of non-nullable columns; modify
User.updateInfo to check each incoming value (email, name, picture) from the
OAuth2UserInfo source and only set the field when the value is non-null (or
non-empty for name/email), preserving existing values otherwise; for email and
name (annotated `@Column`(nullable = false)) ensure you either skip null updates
or validate and throw a clear exception so the DB constraint isn't violated.
In
`@server/src/main/java/oba/backend/server/domain/user/service/UserService.java`:
- Line 36: Wrap the direct enum lookup in UserService where
AuthProvider.valueOf(info.getProvider().toUpperCase()) is called with defensive
handling: validate or normalize info.getProvider(), then call
AuthProvider.valueOf(...) inside a try/catch that catches
IllegalArgumentException (and NullPointerException if needed), log the invalid
provider value, and respond by throwing a controlled exception (e.g.,
InvalidAuthProviderException or an appropriate application exception) or mapping
to a safe default enum value; consider adding a static
AuthProvider.fromString(String) helper to centralize parsing/validation and
update UserService to use that helper.
In
`@server/src/main/java/oba/backend/server/global/config/RestTemplateConfig.java`:
- Around line 10-13: The RestTemplate bean in RestTemplateConfig currently
returns a plain new RestTemplate() without timeouts; update the restTemplate()
method to configure sensible connection and read timeouts (e.g.,
connectionTimeout and readTimeout) by creating and setting a
ClientHttpRequestFactory (e.g., SimpleClientHttpRequestFactory or
HttpComponentsClientHttpRequestFactory) with timeouts before constructing the
RestTemplate, or replace the method to use RestTemplateBuilder with
.setConnectTimeout(...) and .setReadTimeout(...); alternatively add a TODO
comment suggesting migration to WebClient for non-blocking calls if needed.
In `@server/src/main/java/oba/backend/server/global/config/SecurityConfig.java`:
- Around line 36-44: In SecurityConfig replace the custom CORS lambda (the
.cors(cors -> cors.configurationSource(...)) block that sets
allowedOriginPatterns("*") and allowCredentials(true)) with
.cors(Customizer.withDefaults()) so Spring Security will defer to your
CorsConfig.java property-based settings (cors.allowed-origins like
http://localhost:3000, https://onebitearticle.com) and avoid using a wildcard
origin with credentials; alternatively, if you must keep a custom source, load
explicit allowed origins from CorsConfig and call config.setAllowedOrigins(...)
(not allowedOriginPatterns("*")) and setAllowCredentials(true) only when origins
are explicit.
- Around line 46-74: Current oauth2Login configuration is incompatible with
SessionCreationPolicy.STATELESS because the default HttpSession-based
authorization request repository is blocked; update the oauth2Login setup in
SecurityConfig to set a cookie-based authorizationRequestRepository (e.g.,
instantiate and use HttpCookieOAuth2AuthorizationRequestRepository) via
oauth.authorizationEndpoint(...authorizationRequestRepository(...)) while
keeping the existing userInfoEndpoint(userService(customOAuth2UserService)) and
successHandler(oAuth2LoginSuccessHandler) so OAuth2 state is preserved without
server sessions.
♻️ Duplicate comments (2)
server/src/main/java/oba/backend/server/global/auth/controller/AuthController.java (1)
23-35: 서버 측 ID 토큰 검증 누락
클라이언트 제공idToken을 그대로 식별자/이메일로 쓰면 위조 토큰으로 계정 생성이 가능하고, 토큰 회전 시 계정이 분리될 수 있습니다. 서버에서 서명·issuer·audience·exp 검증 후 안정적인 subject/email로 사용자 식별을 하도록 수정해주세요.server/src/main/java/oba/backend/server/domain/article/dto/ArticleDetailResponse.java (1)
17-17:articleId타입 불일치 이슈 (기존 리뷰 참조)이전 리뷰에서 지적된 대로,
SelectedArticle.articleId는Long타입이고 이 DTO는String타입을 사용합니다. 퀴즈 관련 DTO(QuizSubmitRequest,QuizResultRequest)도String을 사용하므로 일관성 있는 타입 결정이 필요합니다.
🟡 Minor comments (7)
server/src/main/java/oba/backend/server/domain/quiz/service/QuizResultService.java-36-45 (1)
36-45: numericArticleId null 가능성 방어 필요
Mongo 문서에articleId가 비어 있으면ArticleLogId/IncorrectQuiz저장이 깨질 수 있습니다. null이면 즉시 예외 처리 또는 대체 경로를 두는 편이 안전합니다.server/src/main/java/oba/backend/server/domain/quiz/service/QuizResultService.java-47-61 (1)
47-61: 결과 리스트 null/빈 값 처리 필요
request.getResults()가 null이거나 비어 있으면 NPE 또는 “무응답인데 해결 처리”가 발생할 수 있습니다. 유효성 검사 후 처리하세요.✅ 예시 수정
-// 정답 여부 체크 (모두 true일 때 해결 처리) -if (!request.getResults().contains(false)) { +List<Boolean> results = request.getResults(); +if (results == null || results.isEmpty()) { + throw new IllegalArgumentException("quiz results가 비어 있습니다."); +} +// 정답 여부 체크 (모두 true일 때 해결 처리) +if (!results.contains(false)) { log.markAsResolved(); } @@ -quizRecord.setQuizResults(request.getResults()); +quizRecord.setQuizResults(results);server/src/main/java/oba/backend/server/domain/log/entity/ArticleLog.java-37-39 (1)
37-39: resolveAt 재설정 방지 권장
markAsResolved()가 재호출되면 기존 해결 시각이 덮어씌워질 수 있습니다. 최초 해결 시각을 보존하도록 가드 처리하는 편이 안전합니다.🔧 제안 수정
public void markAsResolved() { - this.isResolved = true; - this.resolveAt = LocalDateTime.now(); + if (this.isResolved) return; + this.isResolved = true; + this.resolveAt = LocalDateTime.now(); }server/src/main/java/oba/backend/server/domain/stats/entity/UserCategoryStats.java-38-40 (1)
38-40: addScore 입력 검증 부재로 통계 무결성 위험
total/correct가 음수이거나correct > total인 경우 누적 통계가 왜곡됩니다. 최소한의 검증을 권장합니다.🔧 제안 수정
public void addScore(int total, int correct) { + if (total < 0 || correct < 0 || correct > total) { + throw new IllegalArgumentException("Invalid score values"); + } this.totalQuizzes += total; this.correctQuizzes += correct; }server/src/main/java/oba/backend/server/domain/article/service/ArticleDetailService.java-40-45 (1)
40-45:getKeywordItems()null 체크가 필요합니다.
doc.getKeywordItems()가 null을 반환하면.stream()호출 시 NPE가 발생합니다.ArticleSummaryService에서getSummaryBullets()사용 패턴처럼 null-safe 처리를 추가하세요.🛡️ 수정 제안
- List<ArticleDetailResponse.KeywordDto> keywordDtos = doc.getKeywordItems().stream() + List<SelectedArticle.KeywordItem> keywordItems = doc.getKeywordItems(); + List<ArticleDetailResponse.KeywordDto> keywordDtos = (keywordItems != null ? keywordItems : Collections.<SelectedArticle.KeywordItem>emptyList()).stream() .map(item -> ArticleDetailResponse.KeywordDto.builder() .keyword(item.getKeyword()) .description(item.getDescription()) // 설명 필드 매핑 .build()) .collect(Collectors.toList());server/src/main/java/oba/backend/server/domain/quiz/controller/QuizController.java-29-31 (1)
29-31: 토큰 유효성 검증이 누락되었습니다.
parseClaimsJws()는 유효하지 않거나 만료된 토큰에 대해JwtException을 발생시킵니다.ArticleController에서 보여주는 패턴처럼jwtProvider.validateToken(jwt)으로 검증한 후getUserId를 호출해야 합니다.server/src/main/java/oba/backend/server/domain/article/entity/SelectedArticle.java-60-62 (1)
60-62:getQuizzes()에서 null 처리 불일치 및 잠재적 NPE
gptResult.quizzes가 null일 경우 그대로 null이 반환됩니다.getKeywordItems()와 동일한 패턴으로 null 안전하게 처리해야 합니다.🐛 수정 제안
public List<QuizItem> getQuizzes() { - return (gptResult != null) ? gptResult.quizzes : new ArrayList<>(); + if (gptResult == null || gptResult.getQuizzes() == null) return new ArrayList<>(); + return gptResult.getQuizzes(); }
🧹 Nitpick comments (14)
server/src/main/java/oba/backend/server/domain/user/entity/AuthProvider.java (1)
3-8: AuthProvider vs ProviderInfo 중복 위험 확인 필요.
server/src/main/java/oba/backend/server/domain/user/ProviderInfo.java와 역할이 겹칩니다. 두 enum이 동시에 사용되면 매핑 누락/불일치가 생길 수 있으니 단일 소스로 통일되는지 확인해 주세요.server/src/main/java/oba/backend/server/domain/quiz/entity/IncorrectQuizId.java (1)
11-28: Serializable에는 serialVersionUID 추가 권장.직렬화 경고와 호환성 이슈를 줄이려면
serialVersionUID정의를 권장합니다.♻️ 제안 변경
public class IncorrectQuizId implements Serializable { + private static final long serialVersionUID = 1L; private Long userId; private String articleId;server/src/main/java/oba/backend/server/domain/quiz/entity/IncorrectArticlesId.java (1)
1-29: LGTM! 복합 키 클래스가 올바르게 구현되었습니다.
equals()와hashCode()구현이 정확하며, JPA 복합 키 요구사항을 충족합니다.선택적 개선사항:
ArticleLogId에서 사용된 것처럼@EqualsAndHashCodeLombok 어노테이션을 사용하면 보일러플레이트 코드를 줄일 수 있습니다. 또한,Serializable클래스에는serialVersionUID필드 추가를 권장합니다.server/src/main/java/oba/backend/server/domain/quiz/dto/WrongArticleResponse.java (1)
5-15: 일관성을 위해@NoArgsConstructor추가를 고려하세요.
SolvedArticleResponse와 달리@NoArgsConstructor가 없습니다. 현재 응답 전용 DTO로 사용되므로 문제가 없지만, 향후 JSON 역직렬화나 프레임워크 호환성을 위해 추가하는 것이 좋습니다.♻️ 제안된 수정
`@Getter` `@Builder` +@NoArgsConstructor `@AllArgsConstructor` public class WrongArticleResponse {server/src/main/java/oba/backend/server/domain/user/entity/User.java (1)
66-70:userStats가null일 때updateStreak()가 조용히 실패합니다.현재 구현은
userStats가null이면 아무 동작도 하지 않습니다. 이것이 의도된 동작이라면 괜찮지만, 호출자가 streak 업데이트가 성공했다고 가정할 수 있어 버그로 이어질 수 있습니다. 로깅을 추가하거나 예외를 던지는 것을 고려하세요.♻️ 제안된 수정 (로깅 추가)
public void updateStreak() { if (this.userStats != null) { this.userStats.updateStreak(); + } else { + // log.warn("UserStats not initialized for user: {}", this.id); } }server/src/main/java/oba/backend/server/domain/user/dto/UserResponse.java (1)
24-37: LGTM!null처리가 잘 구현되었습니다.
userStats와authProvider에 대한null체크가 적절하게 처리되어 있습니다.선택적 개선:
weeklyLog에 대해서도null체크를 추가하면 더 방어적인 코드가 됩니다.♻️ 제안된 수정
public static UserResponse from(User user, List<Boolean> weeklyLog) { int streak = (user.getUserStats() != null) ? user.getUserStats().getCurrentStreak() : 0; return UserResponse.builder() .userId(user.getId()) .email(user.getEmail()) .name(user.getName()) .picture(user.getPicture()) .authProvider(user.getAuthProvider() != null ? user.getAuthProvider().name() : "UNKNOWN") .consecutiveDays(streak) - .weeklyLog(weeklyLog) + .weeklyLog(weeklyLog != null ? weeklyLog : List.of()) .build(); }server/src/main/java/oba/backend/server/domain/user/service/UserService.java (1)
20-29: 로깅 레벨 검토가 필요합니다.
findByIdentifier()호출마다 INFO 로그가 출력됩니다. 프로덕션 환경에서는 DEBUG 레벨이 더 적절할 수 있습니다. 또한,identifier가 사용자 식별 정보(PII)일 수 있으므로 로깅 시 주의가 필요합니다.♻️ 제안된 수정
public User findByIdentifier(String identifier) { - log.info("[UserService] findByIdentifier 호출됨. 찾는 ID: '{}'", identifier); + log.debug("[UserService] findByIdentifier 호출됨"); return userRepository.findByIdentifier(identifier) .orElseThrow(() -> { - log.error(" [UserService] DB 조회 실패! ID: '{}' 인 유저가 테이블에 없습니다.", identifier); + log.warn("[UserService] 유저를 찾을 수 없음"); return new IllegalArgumentException("유저를 찾을 수 없습니다."); }); }server/src/main/java/oba/backend/server/domain/quiz/service/QuizResultService.java (1)
28-30: 토큰 정규화(“Bearer ” 제거) 방어 로직 추가 권장
컨트롤러에서 항상 접두어를 제거하지 않으면jwtProvider.getUserId가 실패할 수 있습니다. 서비스 내에서 정규화하거나 호출부를 일관화해 주세요.♻️ 예시 수정
public void saveQuizResult(String token, QuizResultRequest request) { - Long userId = jwtProvider.getUserId(token); + String jwt = token.startsWith(Const.BEARER_PREFIX) + ? token.substring(Const.BEARER_PREFIX.length()) + : token; + Long userId = jwtProvider.getUserId(jwt);server/src/main/java/oba/backend/server/global/config/MongoCheckRunner.java (1)
9-36: 프로덕션에서 불필요한 DB 호출/권한 문제 가능시작 시 컬렉션 목록 조회와
countDocuments()는 프로덕션에서 권한 문제나 성능 지연을 유발할 수 있습니다. 로컬/진단 전용으로 한정하거나 조건부 실행을 권장합니다.🔧 제안 수정
import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.stereotype.Component; `@Slf4j` +@Profile("local") `@Component` `@RequiredArgsConstructor` public class MongoCheckRunner implements CommandLineRunner { @@ - if (exists) { - long count = mongoTemplate.getCollection(collectionName).countDocuments(); + if (exists) { + long count = mongoTemplate.getCollection(collectionName).estimatedDocumentCount(); log.info("Collection '{}' FOUND. (Docs: {} count)", collectionName, count); } else { log.error("Collection '{}' NOT FOUND!", collectionName);server/src/main/java/oba/backend/server/domain/quiz/service/QuizQueryService.java (2)
42-43:solvedAt이 실제 풀이 시간이 아닌 현재 날짜입니다.
LocalDate.now().toString()은 API 호출 시점의 날짜를 반환합니다. 실제 퀴즈를 푼 날짜를 표시하려면IncorrectQuiz엔티티에createdAt타임스탬프를 추가하고 이를 사용하는 것이 정확합니다.
68-68: 하드코딩된 카테고리 값.
"Tech"카테고리가 하드코딩되어 있습니다. 실제 기사의 카테고리를 사용하거나, 추후 연동 예정이라면 TODO 주석을 남겨두세요.server/src/main/java/oba/backend/server/domain/quiz/controller/QuizController.java (1)
29-29: 토큰 추출 로직이 중복됩니다.
MyQuizController.extractUserId()와 동일한 Bearer 토큰 파싱 로직이 반복됩니다. 공통 유틸리티 메서드로 추출하거나@AuthenticationPrincipal을 사용하여 Spring Security에서 처리하는 것을 고려하세요.Also applies to: 39-39
server/src/main/java/oba/backend/server/domain/quiz/controller/MyQuizController.java (1)
37-41: 토큰 유효성 검증 추가를 권장합니다.
extractUserId에서 토큰 검증 없이getUserId를 호출합니다.ArticleController의 패턴처럼validateToken호출을 추가하거나, 유효하지 않은 토큰에 대한 예외 처리를 명확히 하세요.♻️ 수정 제안
// 토큰 파싱 헬퍼 메서드 private Long extractUserId(String token) { String jwt = token.startsWith("Bearer ") ? token.substring(7) : token; + if (!jwtProvider.validateToken(jwt)) { + throw new IllegalArgumentException("유효하지 않은 토큰입니다."); + } return jwtProvider.getUserId(jwt); }server/src/main/java/oba/backend/server/domain/article/dto/ArticleDetailResponse.java (1)
23-23: DTO에서 엔티티 내부 클래스 직접 참조는 결합도를 높임
SelectedArticle.QuizItem을 DTO에서 직접 사용하면 엔티티 변경 시 API 응답에 영향을 줄 수 있습니다. 별도의QuizItemDto를 정의하여 계층 간 결합을 분리하는 것이 좋습니다.♻️ 별도 DTO 클래스 제안
`@Getter` `@Builder` `@NoArgsConstructor` `@AllArgsConstructor` public static class QuizItemDto { private String question; private List<String> options; private String answer; private String explanation; }그 후 필드를 변경:
- private List<SelectedArticle.QuizItem> quizzes; + private List<QuizItemDto> quizzes;
| public int getAnswerIndex() { | ||
| try { | ||
| if (answer == null) return -1; | ||
| String numericPart = answer.replaceAll("[^0-9]", ""); | ||
| if (!numericPart.isEmpty() && numericPart.length() < 3) { | ||
| return Integer.parseInt(numericPart) - 1; | ||
| } | ||
| for (int i = 0; i < options.size(); i++) { | ||
| String option = options.get(i).trim(); | ||
| String cleanAnswer = answer.trim(); | ||
| if (option.equals(cleanAnswer) || option.contains(cleanAnswer) || cleanAnswer.contains(option)) { | ||
| return i; | ||
| } | ||
| } | ||
| } catch (Exception e) { | ||
| return -1; | ||
| } | ||
| return -1; | ||
| } |
There was a problem hiding this comment.
getAnswerIndex()에서 options null 체크 누락 및 매직 넘버 사용
Line 92에서 options가 null이면 NullPointerException이 발생합니다. 또한 Line 89의 numericPart.length() < 3 조건은 의도가 불명확합니다.
🐛 수정 제안
public int getAnswerIndex() {
try {
if (answer == null) return -1;
+ if (options == null || options.isEmpty()) return -1;
String numericPart = answer.replaceAll("[^0-9]", "");
- if (!numericPart.isEmpty() && numericPart.length() < 3) {
+ // 1~99 범위의 숫자만 인덱스로 간주 (예: "1", "2번", "정답: 3")
+ if (!numericPart.isEmpty() && numericPart.length() <= 2) {
return Integer.parseInt(numericPart) - 1;
}
for (int i = 0; i < options.size(); i++) {🤖 Prompt for AI Agents
In
`@server/src/main/java/oba/backend/server/domain/article/entity/SelectedArticle.java`
around lines 85 - 103, The getAnswerIndex() method can NPE when options is null
and uses a magic number for digit length; add a null/empty check for options
(and return -1 if options == null || options.isEmpty()) before using
options.size(), and replace the hard-coded numericPart.length() < 3 with a
clearly named constant (e.g., MAX_INDEX_DIGITS = 2) or a self-documenting
condition (numericPart.length() <= MAX_INDEX_DIGITS) so the numeric-parse branch
is explicit; keep the rest of the logic in getAnswerIndex() (answer null check,
numeric parsing, and option-matching loop) intact.
|
|
||
| @Repository | ||
| public interface GptMongoRepository extends MongoRepository<SelectedArticle, String> { | ||
| List<SelectedArticle> findByOrderByServingDateDesc(Pageable pageable); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: findByOrderByServingDateDesc 메서드가 사용되는 곳과 실제 동작 확인
echo "=== Repository 메서드 사용처 검색 ==="
rg -n 'findByOrderByServingDateDesc' --type java
echo -e "\n=== 다른 Spring Data 리포지토리의 OrderBy 패턴 검색 ==="
rg -n 'findAllByOrderBy|findByOrderBy' --type java
echo -e "\n=== GptMongoRepository 전체 내용 확인 ==="
cat -n server/src/main/java/oba/backend/server/domain/article/repository/GptMongoRepository.java
echo -e "\n=== MongoRepository 사용 패턴 검색 ==="
rg -n 'findByOrderBy|findAllByOrderBy' --type java -A 2Repository: OneBiteArticle/oba_backend
Length of output: 2517
Spring Data 메서드 명명 규칙 위반 - 변경 필요
findByOrderByServingDateDesc는 Spring Data 명명 규칙을 위반합니다. findBy 다음에는 필터 조건이 와야 하지만, 현재는 조건 없이 바로 OrderBy가 사용되고 있습니다. 모든 문서를 조회하고 정렬만 적용하려면 findAllByOrderByServingDateDesc로 수정해야 합니다.
변경 사항
- List<SelectedArticle> findByOrderByServingDateDesc(Pageable pageable);
+ List<SelectedArticle> findAllByOrderByServingDateDesc(Pageable pageable);ArticleSummaryService.java에서도 메서드 호출을 함께 업데이트해야 합니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| List<SelectedArticle> findByOrderByServingDateDesc(Pageable pageable); | |
| List<SelectedArticle> findAllByOrderByServingDateDesc(Pageable pageable); |
🤖 Prompt for AI Agents
In
`@server/src/main/java/oba/backend/server/domain/article/repository/GptMongoRepository.java`
at line 13, Change the repository method name from findByOrderByServingDateDesc
to findAllByOrderByServingDateDesc in GptMongoRepository to comply with Spring
Data naming rules (use "All" when no filter follows find), and update any
callers — specifically the invocation in ArticleSummaryService.java — to use the
new method name so compilation and query behavior remain correct.
| @Getter | ||
| @NoArgsConstructor | ||
| public class QuizResultRequest { | ||
| private String articleId; | ||
| private List<Boolean> results; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the QuizResultRequest file and examine its complete content
find . -type f -name "QuizResultRequest.java" | head -5Repository: OneBiteArticle/oba_backend
Length of output: 150
🏁 Script executed:
# Also search for Jackson configuration in the project
rg -l "ObjectMapper|Jackson2ObjectMapperBuilder" --type=java | head -10Repository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Check for build configuration files
find . -type f \( -name "pom.xml" -o -name "build.gradle" \) | head -5Repository: OneBiteArticle/oba_backend
Length of output: 91
🏁 Script executed:
# Read the complete QuizResultRequest.java file
cat -n ./server/src/main/java/oba/backend/server/domain/quiz/dto/QuizResultRequest.javaRepository: OneBiteArticle/oba_backend
Length of output: 404
🏁 Script executed:
# Check build.gradle for Jackson and Spring Boot dependencies
cat ./server/build.gradle | head -100Repository: OneBiteArticle/oba_backend
Length of output: 2419
🏁 Script executed:
# Search for Jackson/Spring configuration files
rg -l "spring.jackson|@Configuration|ObjectMapper" --type=java | head -10Repository: OneBiteArticle/oba_backend
Length of output: 362
🏁 Script executed:
# Find where QuizResultRequest is used in controllers/services
rg -n "QuizResultRequest" --type=java -B2 -A2Repository: OneBiteArticle/oba_backend
Length of output: 3271
🏁 Script executed:
# Final verification: Check for Spring Boot Jackson configuration properties
find . -type f \( -name "application.properties" -o -name "application.yml" -o -name "application.yaml" \) -exec cat {} \;Repository: OneBiteArticle/oba_backend
Length of output: 2889
🏁 Script executed:
# Also check if there's any custom Jackson HttpMessageConverter or ObjectMapper bean in other config files
rg -n "Jackson|ObjectMapper|HttpMessageConverter" --type=java server/src/main/java/oba/backend/server/global/config/Repository: OneBiteArticle/oba_backend
Length of output: 52
요청 DTO에 setter 필요
@Getter만 있고 @Setter가 없어서 Jackson 역직렬화 시 articleId와 results 필드가 null로 남습니다. Spring Boot의 기본 Jackson 설정에서는 private 필드에 값을 할당하려면 setter가 필수입니다.
🔧 제안 수정
import lombok.Getter;
import lombok.NoArgsConstructor;
+import lombok.Setter;
import java.util.List;
`@Getter`
+@Setter
`@NoArgsConstructor`
public class QuizResultRequest {
private String articleId;
private List<Boolean> results;
}🤖 Prompt for AI Agents
In
`@server/src/main/java/oba/backend/server/domain/quiz/dto/QuizResultRequest.java`
around lines 7 - 11, The DTO QuizResultRequest currently only has `@Getter` so
Jackson cannot populate private fields articleId and results during
deserialization; add setters by annotating the class with `@Setter` (or
individually add setArticleId and setResults) so Jackson can set those fields,
ensuring QuizResultRequest has public setter methods for articleId and results.
| // Helper 메서드 | ||
| public void setQuizResults(List<Boolean> results) { | ||
| if (results == null || results.size() < 5) return; | ||
| this.quiz1 = results.get(0); | ||
| this.quiz2 = results.get(1); | ||
| this.quiz3 = results.get(2); | ||
| this.quiz4 = results.get(3); | ||
| this.quiz5 = results.get(4); |
There was a problem hiding this comment.
잘못된 결과 입력이 조용히 무시되거나 NPE 발생 가능
results가 5개가 아니면 아무 것도 갱신되지 않고, null 요소가 있으면 언박싱 시 NPE가 발생합니다. 유효성 검증 후 명시적으로 실패시키는 편이 안전합니다.
🔧 제안 수정
public void setQuizResults(List<Boolean> results) {
- if (results == null || results.size() < 5) return;
+ if (results == null || results.size() != 5 || results.contains(null)) {
+ throw new IllegalArgumentException("results must contain 5 non-null items");
+ }
this.quiz1 = results.get(0);
this.quiz2 = results.get(1);
this.quiz3 = results.get(2);
this.quiz4 = results.get(3);
this.quiz5 = results.get(4);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Helper 메서드 | |
| public void setQuizResults(List<Boolean> results) { | |
| if (results == null || results.size() < 5) return; | |
| this.quiz1 = results.get(0); | |
| this.quiz2 = results.get(1); | |
| this.quiz3 = results.get(2); | |
| this.quiz4 = results.get(3); | |
| this.quiz5 = results.get(4); | |
| // Helper 메서드 | |
| public void setQuizResults(List<Boolean> results) { | |
| if (results == null || results.size() != 5 || results.contains(null)) { | |
| throw new IllegalArgumentException("results must contain 5 non-null items"); | |
| } | |
| this.quiz1 = results.get(0); | |
| this.quiz2 = results.get(1); | |
| this.quiz3 = results.get(2); | |
| this.quiz4 = results.get(3); | |
| this.quiz5 = results.get(4); | |
| } |
🤖 Prompt for AI Agents
In
`@server/src/main/java/oba/backend/server/domain/quiz/entity/IncorrectQuiz.java`
around lines 42 - 49, 현재 setQuizResults 메서드는 입력이 null이거나 길이가 5가 아니면 조용히 무시하고,
리스트에 null 요소가 있으면 언박싱 시 NPE가 발생할 수 있습니다; 수정 방법은 setQuizResults에서 results가
null이거나 results.size() != 5인 경우 IllegalArgumentException을 던져 실패를 명시적으로 알리고, 각
요소가 null인지(예: results.get(i) == null) 검사하여 null 요소가 있으면 역시
IllegalArgumentException을 던지도록 한 다음 유효성이 통과되면 quiz1, quiz2, quiz3, quiz4, quiz5에
각각 할당하는 방식으로 변경하세요 (메서드명 setQuizResults와 필드 quiz1..quiz5를 수정 대상으로 참조).
| for (IncorrectQuiz record : records) { | ||
| // 오답이 하나라도 있으면 | ||
| if (record.getQuizResults().contains(false)) { | ||
| // Long ID -> Mongo Document | ||
| SelectedArticle article = gptMongoRepository.findByArticleId(record.getArticleId()) | ||
| .orElse(null); | ||
|
|
||
| if (article != null) { | ||
| String summary = (article.getSummaryBullets() != null && !article.getSummaryBullets().isEmpty()) | ||
| ? article.getSummaryBullets().get(0) : "요약 없음"; | ||
|
|
||
| responseList.add(WrongArticleResponse.builder() | ||
| .articleId(article.getId()) // Mongo ID (String) | ||
| .title(article.getTitle()) | ||
| .summary(summary) | ||
| .category("Tech") | ||
| .solvedAt(LocalDate.now().toString()) | ||
| .build()); | ||
| } | ||
| } |
There was a problem hiding this comment.
getQuizResults() null 체크가 필요합니다.
record.getQuizResults()가 null인 경우 .contains(false) 호출 시 NPE가 발생합니다.
🛡️ 수정 제안
for (IncorrectQuiz record : records) {
+ List<Boolean> results = record.getQuizResults();
// 오답이 하나라도 있으면
- if (record.getQuizResults().contains(false)) {
+ if (results != null && results.contains(false)) {
// Long ID -> Mongo Document📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for (IncorrectQuiz record : records) { | |
| // 오답이 하나라도 있으면 | |
| if (record.getQuizResults().contains(false)) { | |
| // Long ID -> Mongo Document | |
| SelectedArticle article = gptMongoRepository.findByArticleId(record.getArticleId()) | |
| .orElse(null); | |
| if (article != null) { | |
| String summary = (article.getSummaryBullets() != null && !article.getSummaryBullets().isEmpty()) | |
| ? article.getSummaryBullets().get(0) : "요약 없음"; | |
| responseList.add(WrongArticleResponse.builder() | |
| .articleId(article.getId()) // Mongo ID (String) | |
| .title(article.getTitle()) | |
| .summary(summary) | |
| .category("Tech") | |
| .solvedAt(LocalDate.now().toString()) | |
| .build()); | |
| } | |
| } | |
| for (IncorrectQuiz record : records) { | |
| List<Boolean> results = record.getQuizResults(); | |
| // 오답이 하나라도 있으면 | |
| if (results != null && results.contains(false)) { | |
| // Long ID -> Mongo Document | |
| SelectedArticle article = gptMongoRepository.findByArticleId(record.getArticleId()) | |
| .orElse(null); | |
| if (article != null) { | |
| String summary = (article.getSummaryBullets() != null && !article.getSummaryBullets().isEmpty()) | |
| ? article.getSummaryBullets().get(0) : "요약 없음"; | |
| responseList.add(WrongArticleResponse.builder() | |
| .articleId(article.getId()) // Mongo ID (String) | |
| .title(article.getTitle()) | |
| .summary(summary) | |
| .category("Tech") | |
| .solvedAt(LocalDate.now().toString()) | |
| .build()); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
`@server/src/main/java/oba/backend/server/domain/quiz/service/QuizQueryService.java`
around lines 53 - 72, The loop in QuizQueryService iterates IncorrectQuiz
records and calls record.getQuizResults().contains(false) without guarding for
null, risking an NPE; update the check to first handle null or empty results
(e.g., treat null as no incorrect answers) before calling contains(false).
Specifically adjust the logic around IncorrectQuiz#getQuizResults in the
for-loop so it verifies record.getQuizResults() != null (or uses
Collections.emptyList()/Optional.ofNullable) before calling contains(false), and
only proceeds to fetch the SelectedArticle and build WrongArticleResponse when
the safe contains(false) check is true.
| public void updateInfo(String email, String name, String picture) { | ||
| this.email = email; | ||
| this.name = name; | ||
| this.picture = picture; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# updateInfo() 호출 지점 찾기
rg -n "updateInfo\(" --type java -C 3Repository: OneBiteArticle/oba_backend
Length of output: 1645
🏁 Script executed:
#!/bin/bash
# UserService.java의 info 객체 타입과 context 확인
cat -n server/src/main/java/oba/backend/server/domain/user/service/UserService.java | head -70 | tail -30Repository: OneBiteArticle/oba_backend
Length of output: 1475
🏁 Script executed:
#!/bin/bash
# UserService의 메서드 시그니처 찾기
cat -n server/src/main/java/oba/backend/server/domain/user/service/UserService.java | head -40Repository: OneBiteArticle/oba_backend
Length of output: 1928
🏁 Script executed:
#!/bin/bash
# OAuth2UserInfo 찾기
fd -t f "OAuth2UserInfo" | head -5Repository: OneBiteArticle/oba_backend
Length of output: 147
🏁 Script executed:
#!/bin/bash
# OAuth2UserInfo 파일 읽기
cat -n server/src/main/java/oba/backend/server/global/auth/oauth/OAuth2UserInfo.javaRepository: OneBiteArticle/oba_backend
Length of output: 574
🏁 Script executed:
#!/bin/bash
# User.java 읽기 - 필드 정의와 제약 확인
cat -n server/src/main/java/oba/backend/server/domain/user/entity/User.java | head -60Repository: OneBiteArticle/oba_backend
Length of output: 2056
updateInfo() 메서드가 null 값을 검증하지 않아 DB 제약 조건을 위반할 수 있습니다.
OAuth2UserInfo 클래스의 필드들(email, name, picture)은 null 값을 가질 수 있으며, updateInfo() 메서드는 이들 값을 검증 없이 직접 할당합니다. User 엔티티의 email과 name 필드는 @Column(nullable = false) 제약이 있으므로, OAuth 제공자로부터 null 값이 반환되면 기존 데이터가 null로 덮어씌워져 DB 제약 조건 위반이 발생합니다. 부분적인 OAuth 정보 업데이트 시 안전한 필드 검증 로직이 필요합니다.
🤖 Prompt for AI Agents
In `@server/src/main/java/oba/backend/server/domain/user/entity/User.java` around
lines 54 - 58, updateInfo currently assigns OAuth2UserInfo fields directly,
risking null overwrites of non-nullable columns; modify User.updateInfo to check
each incoming value (email, name, picture) from the OAuth2UserInfo source and
only set the field when the value is non-null (or non-empty for name/email),
preserving existing values otherwise; for email and name (annotated
`@Column`(nullable = false)) ensure you either skip null updates or validate and
throw a clear exception so the DB constraint isn't violated.
| public User registerOrUpdateUser(OAuth2UserInfo info) { | ||
| log.info(" [UserService] registerOrUpdateUser 호출됨. Provider: {}, ID: {}", info.getProvider(), info.getId()); | ||
|
|
||
| AuthProvider providerEnum = AuthProvider.valueOf(info.getProvider().toUpperCase()); |
There was a problem hiding this comment.
AuthProvider.valueOf()가 유효하지 않은 provider에 대해 예외를 던질 수 있습니다.
info.getProvider()가 AuthProvider enum에 정의되지 않은 값이면 IllegalArgumentException이 발생합니다. 외부 OAuth 응답을 신뢰할 수 없으므로 방어적 처리가 필요합니다.
🐛 제안된 수정
- AuthProvider providerEnum = AuthProvider.valueOf(info.getProvider().toUpperCase());
+ AuthProvider providerEnum;
+ try {
+ providerEnum = AuthProvider.valueOf(info.getProvider().toUpperCase());
+ } catch (IllegalArgumentException e) {
+ log.error("[UserService] 알 수 없는 Provider: {}", info.getProvider());
+ throw new IllegalArgumentException("지원하지 않는 인증 제공자입니다: " + info.getProvider());
+ }🤖 Prompt for AI Agents
In `@server/src/main/java/oba/backend/server/domain/user/service/UserService.java`
at line 36, Wrap the direct enum lookup in UserService where
AuthProvider.valueOf(info.getProvider().toUpperCase()) is called with defensive
handling: validate or normalize info.getProvider(), then call
AuthProvider.valueOf(...) inside a try/catch that catches
IllegalArgumentException (and NullPointerException if needed), log the invalid
provider value, and respond by throwing a controlled exception (e.g.,
InvalidAuthProviderException or an appropriate application exception) or mapping
to a safe default enum value; consider adding a static
AuthProvider.fromString(String) helper to centralize parsing/validation and
update UserService to use that helper.
| @Bean | ||
| public RestTemplate restTemplate() { | ||
| return new RestTemplate(); | ||
| } |
There was a problem hiding this comment.
외부 서비스 호출 시 타임아웃 설정이 필요합니다.
기본 RestTemplate은 연결(connection) 및 읽기(read) 타임아웃이 설정되어 있지 않습니다. AI 서비스(GPT 호출)가 응답이 느리거나 응답하지 않는 경우, 호출 스레드가 무한정 블로킹되어 스레드 풀 고갈 및 연쇄 장애를 유발할 수 있습니다.
⏱️ 타임아웃 설정 추가 제안
package oba.backend.server.global.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
`@Configuration`
public class RestTemplateConfig {
`@Bean`
public RestTemplate restTemplate() {
- return new RestTemplate();
+ SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+ factory.setConnectTimeout(5000); // 5초
+ factory.setReadTimeout(30000); // 30초 (AI 처리 시간 고려)
+ return new RestTemplate(factory);
}
}또는 더 고급 기능이 필요한 경우 RestTemplateBuilder를 사용하거나 WebClient로의 마이그레이션을 고려해 보세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Bean | |
| public RestTemplate restTemplate() { | |
| return new RestTemplate(); | |
| } | |
| package oba.backend.server.global.config; | |
| import org.springframework.context.annotation.Bean; | |
| import org.springframework.context.annotation.Configuration; | |
| import org.springframework.http.client.SimpleClientHttpRequestFactory; | |
| import org.springframework.web.client.RestTemplate; | |
| `@Configuration` | |
| public class RestTemplateConfig { | |
| `@Bean` | |
| public RestTemplate restTemplate() { | |
| SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); | |
| factory.setConnectTimeout(5000); // 5초 | |
| factory.setReadTimeout(30000); // 30초 (AI 처리 시간 고려) | |
| return new RestTemplate(factory); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
`@server/src/main/java/oba/backend/server/global/config/RestTemplateConfig.java`
around lines 10 - 13, The RestTemplate bean in RestTemplateConfig currently
returns a plain new RestTemplate() without timeouts; update the restTemplate()
method to configure sensible connection and read timeouts (e.g.,
connectionTimeout and readTimeout) by creating and setting a
ClientHttpRequestFactory (e.g., SimpleClientHttpRequestFactory or
HttpComponentsClientHttpRequestFactory) with timeouts before constructing the
RestTemplate, or replace the method to use RestTemplateBuilder with
.setConnectTimeout(...) and .setReadTimeout(...); alternatively add a TODO
comment suggesting migration to WebClient for non-blocking calls if needed.
| // CORS 설정 | ||
| .cors(cors -> cors.configurationSource(request -> { | ||
| CorsConfiguration config = new CorsConfiguration(); | ||
| config.setAllowedOriginPatterns(List.of("*")); // 개발용 전체 허용 | ||
| config.setAllowedHeaders(List.of("*")); | ||
| config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); | ||
| config.setAllowCredentials(true); | ||
| return config; | ||
| })) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find the SecurityConfig and CorsConfig files
fd -t f -n "SecurityConfig.java\|CorsConfig.java" server/Repository: OneBiteArticle/oba_backend
Length of output: 300
🏁 Script executed:
# Read the SecurityConfig.java file to see the full context
cat -n server/src/main/java/oba/backend/server/global/config/SecurityConfig.javaRepository: OneBiteArticle/oba_backend
Length of output: 4043
🏁 Script executed:
# Read the CorsConfig.java file to understand the existing CORS allowlist
cat -n server/src/main/java/oba/backend/server/global/config/CorsConfig.javaRepository: OneBiteArticle/oba_backend
Length of output: 1334
🏁 Script executed:
# Search for AuthorizationRequestRepository customization
rg -t java "AuthorizationRequestRepository" server/Repository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Search for application properties file to see CORS configuration
fd -t f "application" server/ | grep -E "\.(yml|yaml|properties)$"Repository: OneBiteArticle/oba_backend
Length of output: 158
🏁 Script executed:
# Check if both CORS configurations can coexist or if one overrides
rg -t java "WebMvcConfigurer|CorsConfigurationSource" server/Repository: OneBiteArticle/oba_backend
Length of output: 441
🏁 Script executed:
# Read application.yml to see the CORS configuration
cat server/src/main/resources/application.ymlRepository: OneBiteArticle/oba_backend
Length of output: 2889
🏁 Script executed:
# Search for cors configuration properties
rg -i "cors\.|allowed.origin" server/src/main/resources/Repository: OneBiteArticle/oba_backend
Length of output: 234
자격증명 허용 상태에서 allowedOriginPatterns("*")는 CORS 명세를 위반합니다.
allowCredentials(true)와 allowedOriginPatterns("*")의 조합은 CORS 명세에서 명시적으로 금지됩니다. 이 설정으로 인해 어떤 오리진에서도 인증 정보를 포함한 요청이 허용되어 보안 위험이 발생합니다. 특히 CorsConfig.java에서 정의한 명시적 허용 목록(cors.allowed-origins: http://localhost:3000, https://onebitearticle.com 등)이 Spring Security 레벨에서 무시됩니다.
SecurityConfig의 CORS 설정을 Customizer.withDefaults()로 변경하면 CorsConfig의 속성 기반 허용 목록을 활용하도록 통일됩니다.
🔧 수정 제안
- .cors(cors -> cors.configurationSource(request -> {
- CorsConfiguration config = new CorsConfiguration();
- config.setAllowedOriginPatterns(List.of("*")); // 개발용 전체 허용
- config.setAllowedHeaders(List.of("*"));
- config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
- config.setAllowCredentials(true);
- return config;
- }))
+ .cors(Customizer.withDefaults())🤖 Prompt for AI Agents
In `@server/src/main/java/oba/backend/server/global/config/SecurityConfig.java`
around lines 36 - 44, In SecurityConfig replace the custom CORS lambda (the
.cors(cors -> cors.configurationSource(...)) block that sets
allowedOriginPatterns("*") and allowCredentials(true)) with
.cors(Customizer.withDefaults()) so Spring Security will defer to your
CorsConfig.java property-based settings (cors.allowed-origins like
http://localhost:3000, https://onebitearticle.com) and avoid using a wildcard
origin with credentials; alternatively, if you must keep a custom source, load
explicit allowed origins from CorsConfig and call config.setAllowedOrigins(...)
(not allowedOriginPatterns("*")) and setAllowCredentials(true) only when origins
are explicit.
| // 세션 관리 (Stateless) | ||
| .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) | ||
|
|
||
| // API 접근 권한 설정 | ||
| .authorizeHttpRequests(auth -> auth | ||
| // 기사 조회(GET)는 로그인 없이 허용 | ||
| .requestMatchers(HttpMethod.GET, "/api/articles/**").permitAll() | ||
|
|
||
| // 로그인/인증 관련 경로는 모두 허용 | ||
| .requestMatchers( | ||
| "/oauth2/**", | ||
| "/login/**", | ||
| "/auth/**", | ||
| "/error", | ||
| "/favicon.ico" | ||
| ).permitAll() | ||
|
|
||
| // 퀴즈 풀기, 마이페이지 등은 인증 필요 | ||
| .requestMatchers("/api/quiz/**", "/api/users/me").authenticated() | ||
|
|
||
| // 그 외 모든 요청은 인증 필요 | ||
| .anyRequest().authenticated() | ||
| ) | ||
|
|
||
| // OAuth2 로그인 설정 | ||
| .oauth2Login(oauth -> oauth | ||
| .userInfoEndpoint(info -> info.userService(customOAuth2UserService)) | ||
| .successHandler(oAuth2LoginSuccessHandler) | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's locate and examine the SecurityConfig file
fd "SecurityConfig.java" --type fRepository: OneBiteArticle/oba_backend
Length of output: 143
🏁 Script executed:
# Read the SecurityConfig file to see the complete oauth2Login configuration
cat -n server/src/main/java/oba/backend/server/global/config/SecurityConfig.javaRepository: OneBiteArticle/oba_backend
Length of output: 4043
🏁 Script executed:
# Search for any authorizationRequestRepository implementations in the codebase
rg -i "authorizationRequestRepository|CookieAuthorizationRequestRepository" --type javaRepository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Search for bean definitions related to OAuth2 configuration
rg -i "@Bean" server/src/main/java/oba/backend/server/global/config/SecurityConfig.java -A 2Repository: OneBiteArticle/oba_backend
Length of output: 161
🏁 Script executed:
# Check for any other OAuth2 related configurations or custom repositories
rg -i "oauth.*repository|authorization.*endpoint" --type java -iRepository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Search for test files related to SecurityConfig or OAuth2
find . -name "*Test.java" -o -name "*Tests.java" | xargs grep -l "SecurityConfig\|OAuth2" 2>/dev/null | head -20Repository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Look for any custom AuthorizationRequestRepository implementations
rg -i "class.*AuthorizationRequestRepository|implements.*AuthorizationRequestRepository" --type javaRepository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Check if there are any other configuration files that might handle OAuth2
fd ".*Config.java" --type f | xargs grep -l "oauth2\|OAuth2" 2>/dev/nullRepository: OneBiteArticle/oba_backend
Length of output: 143
🏁 Script executed:
# Search for any ServletContext session configuration or bean definitions
rg -i "HttpSessionOAuth2AuthorizationRequestRepository\|HttpSessionSecurityContextRepository" --type javaRepository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Check Spring Boot application properties/yaml for session configuration
fd "application.*\.(yml|yaml|properties)" -type f | xargs cat 2>/dev/nullRepository: OneBiteArticle/oba_backend
Length of output: 240
🏁 Script executed:
# Find application configuration files with correct syntax
find . -name "application*" -type f | head -20Repository: OneBiteArticle/oba_backend
Length of output: 162
🏁 Script executed:
# Check pom.xml to understand Spring Boot/Security version
cat pom.xml | grep -A 5 -B 5 "spring-security\|spring-boot"Repository: OneBiteArticle/oba_backend
Length of output: 109
🏁 Script executed:
# Search for any OAuth2-related configuration or bean definitions
rg "oauth2\|OAuth2" --type java -l | head -20Repository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Check if there's any session configuration or SecurityContextRepository bean
rg "SecurityContextRepository\|SessionCreationPolicy" --type java -B 2 -A 2Repository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Read the application.yml file
cat server/src/main/resources/application.ymlRepository: OneBiteArticle/oba_backend
Length of output: 2889
🏁 Script executed:
# Look for build configuration files (Maven or Gradle)
find . -maxdepth 2 -name "pom.xml" -o -name "build.gradle" -o -name "build.gradle.kts" | head -5Repository: OneBiteArticle/oba_backend
Length of output: 91
🏁 Script executed:
# Search for Spring Security version information in parent directory
find . -maxdepth 3 -type f \( -name "pom.xml" -o -name "build.gradle*" \) -exec grep -l "spring" {} \;Repository: OneBiteArticle/oba_backend
Length of output: 91
🏁 Script executed:
# Check if OAuth2LoginSuccessHandler has any custom session handling
rg -i "oauth2loginsuccess" --type java -B 5 -A 15Repository: OneBiteArticle/oba_backend
Length of output: 9115
🏁 Script executed:
# Check the build.gradle file to see Spring Security version
cat server/build.gradle | grep -A 20 -B 5 "spring"Repository: OneBiteArticle/oba_backend
Length of output: 2419
🏁 Script executed:
# Read OAuth2LoginSuccessHandler to understand the full flow
cat -n server/src/main/java/oba/backend/server/global/auth/oauth/OAuth2LoginSuccessHandler.javaRepository: OneBiteArticle/oba_backend
Length of output: 2439
🏁 Script executed:
# Check if there's custom OAuth2 authorization endpoint handling
rg "authorizationEndpoint|authorizationRequestRepository" --type java -B 3 -A 3Repository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Check if there's any custom SecurityContextRepository or session configuration
rg "SecurityContextRepository|NoOpSecurityContextRepository" --type java -B 2 -A 2Repository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Search for any documentation or comments about OAuth2 session handling
rg "oauth|session|stateless" --type java -i | grep -i "comment\|//\|/\*" | head -20Repository: OneBiteArticle/oba_backend
Length of output: 532
🏁 Script executed:
# Check Spring Security version more explicitly
grep -r "spring-security" server/build.gradleRepository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Look for any integration tests that might test the OAuth2 flow
find server -name "*Test.java" -o -name "*Tests.java" | xargs grep -l "oauth2\|OAuth2\|SecurityConfig" 2>/dev/nullRepository: OneBiteArticle/oba_backend
Length of output: 54
🏁 Script executed:
# Check Spring Security auto-configuration details
rg "HttpSessionOAuth2\|CookieOAuth2\|authorizationRequest" --type java -B 2 -A 2Repository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Look for any explicit session configuration that might override STATELESS
rg "sessionManagement\|SessionCreationPolicy" --type java -B 3 -A 3Repository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Check if there's any custom AuthenticationProvider or OAuth2 filter configuration
rg "OAuth2AuthenticationFilter\|OAuth2LoginAuthenticationFilter\|AuthenticationProvider" --type javaRepository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Verify Spring Security version from dependencies
grep "spring-boot-starter-security" server/build.gradleRepository: OneBiteArticle/oba_backend
Length of output: 144
🏁 Script executed:
# Check if there's a fallback mechanism in the success handler or auth flow
rg "session\|request\.getSession\|httpSession" --type java -iRepository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Search for any implicit OAuth2 configuration or custom beans
find server/src/main/java -name "*.java" -type f | xargs grep -l "oauth\|OAuth" -iRepository: OneBiteArticle/oba_backend
Length of output: 727
🏁 Script executed:
# Check CustomOAuth2UserService to understand the complete flow
cat -n server/src/main/java/oba/backend/server/global/auth/oauth/CustomOAuth2UserService.java | head -50Repository: OneBiteArticle/oba_backend
Length of output: 2534
🏁 Script executed:
# Look for any WebSecurityCustomizer or additional filter configurations
rg "WebSecurityCustomizer\|addFilterAfter\|addFilterBefore" --type java -B 2 -A 3Repository: OneBiteArticle/oba_backend
Length of output: 52
🌐 Web query:
Spring Security 6 STATELESS session OAuth2 authorization request repository
💡 Result:
Short answer: Spring Security 6 still persists the OAuth2 Authorization Request (and its state) by default in the HttpSession (HttpSessionOAuth2AuthorizationRequestRepository). To run oauth2Login in a stateless app you must provide a custom AuthorizationRequestRepository that stores the OAuth2AuthorizationRequest somewhere other than the session (common choices: secure cookie, signed/encrypted cookie, or external store like Redis) and register it with the oauth2Login configuration.
How to do it (high-level)
- Implement AuthorizationRequestRepository (load/save/remove). The default implementation is HttpSessionOAuth2AuthorizationRequestRepository. (docs.spring.io) [1]
- Or reuse an existing cookie-based example (store the serialized OAuth2AuthorizationRequest and state in a same-site, secure cookie) and verify signature/expiration on callback. The Spring docs show the pattern and examples of using a cookie-based repo. (docs.spring.io) [2]
- Wire it into your security config:
http.oauth2Login(o -> o.authorizationEndpoint(a -> a.authorizationRequestRepository(yourRepo)));
This replaces the session-backed repo with your stateless repo. (docs.spring.io) [2][1] - Use SessionCreationPolicy.STATELESS for request handling, but be aware: you still need some place to store the transient authorization-request data (cookie or external store). See community discussion and examples explaining why session must be replaced. (stackoverflow.com) [3][4]
Security notes
- Keep the cookie/value signed and encrypted or store only an opaque id that maps to server-side cache (Redis) to avoid tampering.
- Ensure SameSite, Secure, HttpOnly flags and short expiry; validate the state parameter on callback.
- Consider CSRF and token storage trade-offs (JWT/local storage vs. cookies) and revocation implications. (stackoverflow.com) [4][5]
If you want, I can:
- paste a minimal cookie-based AuthorizationRequestRepository implementation and the security config for Spring Security 6, or
- show an example using Redis (stateless API + small server-side transient store).
Citations:
- 1: https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepository.html?utm_source=openai
- 2: https://docs.spring.io/spring-security/site/docs/5.1.8.RELEASE/reference/htmlsingle/?utm_source=openai
- 3: https://docs.spring.io/spring-security/site/docs/5.1.8.RELEASE/reference/htmlsingle/?utm_source=openai
- 4: https://stackoverflow.com/questions/71785114/unable-to-make-the-oauth2login-as-stateless?utm_source=openai
- 5: https://stackoverflow.com/questions/77591848/spring-security-6-use-jwt-tokens-instead-of-jsessionid-after-oauth2-login?utm_source=openai
🌐 Web query:
Spring Security 6.x OAuth2 login stateless session policy compatibility
💡 Result:
Short answer: oauth2Login (authorization-code flow) is not compatible with SessionCreationPolicy.STATELESS out-of-the-box because the client flow needs to persist the OAuth2AuthorizationRequest / state across the redirect — Spring Security's default persistence is HttpSession. Setting STATELESS prevents that storage and will break the redirect/callback (redirect loops / missing state). [1][2]
Options / fixes:
- Keep a session during the OAuth2 redirect flow (IF_REQUIRED / NEVER or use Spring Session to externalize sessions). [1][2]
- Replace the session-backed AuthorizationRequestRepository with a stateless store (common choice: an AuthorizationRequestRepository that puts the OAuth2AuthorizationRequest into a cookie, e.g. HttpCookieOAuth2AuthorizationRequestRepository). Configure it via oauth2Login().authorizationEndpoint().authorizationRequestRepository(...). This preserves stateless request handling while allowing the code+state callback to validate. [1][3]
- Alternative architecture: perform OAuth2 login to mint your own JWT on success (or use the upstream access token), then use stateless Resource Server (JWT) for subsequent requests — i.e. keep the login flow stateful or cookie-backed only for the short redirect, then switch to JWT for stateless API calls. [2][6]
References:
- Spring Security docs — AuthorizationRequestRepository / default HttpSessionOAuth2AuthorizationRequestRepository and cookie option. [1]
- Discussion/issue: stateless session breaks oauth2Login (redirect loop explanation). [2]
- Example / pattern: storing authorization request in cookie (HttpCookieOAuth2AuthorizationRequestRepository). [3]
🏁 Script executed:
# Check if there's any explicit session configuration for OAuth2 paths
rg "stateless.*oauth|oauth.*stateless" --type java -iRepository: OneBiteArticle/oba_backend
Length of output: 52
🏁 Script executed:
# Look for any request attributes or session handling in the OAuth2 flow
rg "setAttribute\|getAttribute\|getSession" server/src/main/java/oba/backend/server/global/auth/ --type javaRepository: OneBiteArticle/oba_backend
Length of output: 52
OAuth2 로그인이 SessionCreationPolicy.STATELESS와 호환되지 않습니다.
Spring Security 6에서 기본 OAuth2 클라이언트는 HttpSessionOAuth2AuthorizationRequestRepository를 사용하여 인증 요청 상태를 저장합니다. SessionCreationPolicy.STATELESS는 세션 생성을 전면 차단하므로, OAuth2 리다이렉트 흐름에서 상태 검증이 실패하게 됩니다. 다음과 같이 쿠키 기반 저장소를 지정하여 해결하세요:
수정 방법
.oauth2Login(oauth -> oauth
.authorizationEndpoint(authorization ->
authorization.authorizationRequestRepository(new HttpCookieOAuth2AuthorizationRequestRepository())
)
.userInfoEndpoint(info -> info.userService(customOAuth2UserService))
.successHandler(oAuth2LoginSuccessHandler)
)🤖 Prompt for AI Agents
In `@server/src/main/java/oba/backend/server/global/config/SecurityConfig.java`
around lines 46 - 74, Current oauth2Login configuration is incompatible with
SessionCreationPolicy.STATELESS because the default HttpSession-based
authorization request repository is blocked; update the oauth2Login setup in
SecurityConfig to set a cookie-based authorizationRequestRepository (e.g.,
instantiate and use HttpCookieOAuth2AuthorizationRequestRepository) via
oauth.authorizationEndpoint(...authorizationRequestRepository(...)) while
keeping the existing userInfoEndpoint(userService(customOAuth2UserService)) and
successHandler(oAuth2LoginSuccessHandler) so OAuth2 state is preserved without
server sessions.
OneBitArticle (OBA) - Backend Server
OneBitArticle은 바쁜 IT 취업준비생들이 최신 기술 트렌드를 효율적으로 학습할 수 있도록 돕는 서비스입니다.
매일 업데이트되는 방대한 IT 뉴스를 AI가 핵심만 요약해주고, 학습 내용을 점검할 수 있는 퀴즈와 해설을 자동으로 생성하여 제공합니다.
Key Features
1. Daily AI Briefing & Automation
자동화된 뉴스 수집: 매일 자정 스케줄러가 동작하여 최신 IT 뉴스를 수집합니다.
AI 요약 및 키워드 추출: OpenAI GPT 모델(FastAPI 연동)을 통해 기사의 3줄 요약과 핵심 기술 키워드를 추출하여 MongoDB에 적재합니다.
2. Smart Quiz System (New)
자동 퀴즈 생성: 기사 내용을 바탕으로 4지선다형 퀴즈를 생성합니다.
채점 및 해설: 사용자가 제출한 답안을 채점하고, 정답에 대한 상세한 해설을 즉시 제공합니다.
오답 노트: 틀린 문제는 자동으로 오답 노트에 기록되어 언제든 다시 학습할 수 있습니다.
3. User Statistics & Growth (New)
학습 연속일(Streak): 매일 학습 시 연속 학습일이 갱신되어 꾸준한 학습을 독려합니다.
학습 통계: 카테고리별 정답률, 총 학습한 기사 수 등 개인화된 통계를 제공합니다.
4. Hybrid Database Architecture
MySQL: 사용자 정보, 학습 이력, 오답 기록, 통계 등 정형 데이터의 무결성을 보장합니다.
MongoDB: 기사 원문, AI 요약 결과, 퀴즈 데이터 등 구조가 가변적인 데이터를 유연하게 저장합니다.
5. Secure Mobile Authentication
OAuth 2.0: Google, Kakao, Naver 소셜 로그인을 지원합니다.
JWT Authentication: Access/Refresh Token 기반의 Stateless 인증 방식을 적용하여 모바일 앱 환경에 최적화되었습니다.
System Architecture
Spring Boot(메인 서버)와 FastAPI(AI 서버)가 분리된 구조이며, 데이터 특성에 따라 RDBMS와 NoSQL을 적절히 활용하는 Polyglot Persistence 전략을 채택했습니다.
Scheduler: Spring Boot 서버의 스케줄러가 주기적으로 AI 작업을 트리거합니다.
AI Processing: FastAPI 서버가 GPT API를 호출하여 데이터를 가공하고 MongoDB에 적재합니다.
Service Layer: 클라이언트 요청 시 Spring Boot가 MySQL(회원/기록)과 MongoDB(콘텐츠)의 데이터를 조합하여 응답합니다.
Directory Structure
도메인 주도 설계(DDD)를 기반으로 비즈니스 로직의 응집도를 높인 패키지 구조입니다.
API Overview
Authentication
POST /auth/mobile/login: 모바일 ID 토큰 기반 로그인 및 JWT 발급POST /auth/reissue: Access Token 재발급Article
GET /api/articles/latest: 최신 기사 목록 조회 (요약 포함)GET /api/articles/{id}: 기사 상세 조회 (본문, 키워드, 퀴즈 포함)Quiz & Logs
POST /api/quiz/result: 퀴즈 풀이 결과 제출 및 채점 (UserStats 갱신)GET /api/quiz/wrong: 사용자의 오답 노트 조회GET /api/my/solved: 내가 푼 문제 목록 조회User
GET /api/users/me: 내 프로필 및 학습 통계 조회Getting Started
1. Prerequisites
Java 17 (JDK 17+)
MySQL (Port: 3306)
MongoDB Atlas or Local MongoDB (Port: 27017)
2. Environment Variables
프로젝트 루트 경로에
.env파일을 생성하고 아래 변수를 설정하세요.3. Run Application
Mac/Linux
Windows
Recent Updates (Release Note)
Latest Feature: Quiz & Stats Integration (
feat/ON-79)퀴즈 채점 시스템 구현: 사용자가 퀴즈를 풀면 정답 여부를 판단하고 DB에 저장합니다.
오답 노트 기능: 틀린 문제만 모아서 볼 수 있는 API를 추가했습니다.
사용자 통계 고도화:
연속 학습일(Streak) 자동 계산 로직 추가
카테고리별 학습 통계 (
UserCategoryStats) 엔티티 설계데이터 매핑 강화: MongoDB의 비정형 데이터(퀴즈/키워드)와 MySQL의 정형 데이터(사용자 풀이 기록) 간의 매핑 로직을 최적화했습니다.