Skip to content

Refactor: 프로젝트 구조 리팩토링#11

Open
dungbik wants to merge 3 commits intomainfrom
refactor/project-structure
Open

Refactor: 프로젝트 구조 리팩토링#11
dungbik wants to merge 3 commits intomainfrom
refactor/project-structure

Conversation

@dungbik
Copy link
Copy Markdown
Contributor

@dungbik dungbik commented Apr 2, 2026

Summary by CodeRabbit

릴리스 노트

  • New Features

    • OAuth 소셜 로그인/연동 추가 (Google OAuth2 지원) 및 PKCE 기반 보안 강화
    • gRPC를 통한 사용자 조회 API 추가 및 gRPC 예외 처리 개선
  • Documentation

    • 서비스용 README 추가 (기술 스택, 환경 변수, 로컬/도커 빌드·실행, CI/CD 안내)

@dungbik dungbik requested a review from stoneTiger0912 April 2, 2026 05:45
@dungbik dungbik self-assigned this Apr 2, 2026
@dungbik dungbik added the enhancement New feature or request label Apr 2, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c25097d2-f452-42a2-913e-7b10e97ff1f4

📥 Commits

Reviewing files that changed from the base of the PR and between 5e6c7d7 and 187ae6a.

📒 Files selected for processing (1)
  • src/main/java/flipnote/user/user/presentation/grpc/GrpcUserQueryService.java
💤 Files with no reviewable changes (1)
  • src/main/java/flipnote/user/user/presentation/grpc/GrpcUserQueryService.java

Walkthrough

프로젝트의 패키지 계층을 대규모로 재배치(domain/application/infrastructure/interfaces)하고, OAuth2(클라이언트/토큰/유저정보) 및 gRPC 관련 서버·예외 매핑을 신규 추가했습니다. README 문서도 새로 추가되었습니다.

Changes

Cohort / File(s) Summary
문서
README.md
FlipNote User Service 백엔드용 README 추가(스택, env, 빌드/도커/CI 지침).
애플리케이션 계층
src/main/java/flipnote/user/application/*, src/main/java/flipnote/user/application/AuthService.java, src/main/java/flipnote/user/application/OAuthService.java, src/main/java/flipnote/user/application/UserService.java
패키지 이동 및 import 재정렬; OAuthService 신규 추가(인증 URI 생성, 소셜 로그인/연결, PKCE/state 처리), UserService에 조회 유틸 메서드 추가.
도메인: 오류/상수/공통
src/main/java/flipnote/user/domain/{AuthErrorCode,ImageErrorCode,PasswordResetConstants,TokenClaims,TokenPair,UserErrorCode,VerificationConstants}.java, src/main/java/flipnote/user/domain/common/*
여러 에러 코드·상수·공통 예외 타입을 domain/domain.common으로 이동 및 import 경로 변경.
도메인: 엔티티/이벤트/리포지토리
src/main/java/flipnote/user/domain/entity/*, src/main/java/flipnote/user/domain/event/*, src/main/java/flipnote/user/domain/repository/*
엔티티(User, OAuthLink 등), 이벤트 레코드, JPA 리포지토리 패키지 이동 및 import 정리.
인프라: 설정/JWT/메일/리틀러너스
src/main/java/flipnote/user/infrastructure/config/*, .../jwt/*, .../mail/*, .../listener/*
설정 및 JWT·메일·리스너 관련 클래스들을 infrastructure.*로 이동; 관련 import 및 참조 조정.
인프라: OAuth 클라이언트/설정/유틸
src/main/java/flipnote/user/infrastructure/oauth/{OAuthApiClient,OAuthProperties,GoogleUserInfo,OAuth2UserInfo,PkceUtil}.java
OAuthApiClient 신규 추가(토큰 요청, 유저정보 조회, 인증 URI 생성), OAuthProperties 추가(설정 바인딩), OAuth 관련 유틸/인터페이스 추가·이동.
인프라: Redis 관련
src/main/java/flipnote/user/infrastructure/redis/*
Redis 기반 토큰/저장소 클래스 패키지 이동(import 경로 변경).
인터페이스(HTTP)
src/main/java/flipnote/user/interfaces/http/*, .../dto/*
HTTP 컨트롤러(AuthController, UserController) 및 DTO 패키지 재배치; OAuthController 신규 추가(redirect, callback, 토큰 쿠키 설정, 소셜 로그인/연결 라우팅).
인터페이스(HTTP 공통)
src/main/java/flipnote/user/interfaces/http/common/*
ApiResponse, 예외 어드바이스, CookieUtil, HttpConstants 등 공통 유틸 패키지로 이동 및 import 조정.
인터페이스(gRPC)
src/main/java/flipnote/user/interfaces/grpc/{GrpcExceptionHandlerImpl,GrpcUserQueryService}.java
gRPC 예외 핸들러(GrpcExceptionHandlerImpl) 추가(gRPC용 BizException → Status 매핑). gRPC 사용자 조회 서비스(GrpcUserQueryService) 신규 구현으로 기존 서비스 제거/대체.
제거
src/main/java/flipnote/user/user/presentation/grpc/GrpcUserQueryService.java
이전 위치의 gRPC 서비스 클래스 삭제(대체된 새로운 gRPC 서비스가 추가됨).

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Ctrl as OAuthController
    participant Svc as OAuthService
    participant Api as OAuthApiClient
    participant Redis as SocialLinkTokenRepository
    participant Domain as Domain (OAuthLink/User)

    rect rgba(100, 150, 200, 0.5)
    Client->>Ctrl: GET /oauth2/authorization/{provider}
    Ctrl->>Svc: getAuthorizationUri(provider, request, userId)
    Svc->>Redis: save state token (optional)
    Svc->>Api: buildAuthorizeUri(provider, codeChallenge, state)
    Api-->>Svc: authorizeUri
    Svc-->>Ctrl: AuthorizationRedirect (URI + verifier cookie)
    Ctrl-->>Client: 302 Redirect with verifier cookie
    end

    rect rgba(150, 200, 100, 0.5)
    Client->>Ctrl: GET /oauth2/callback/{provider}?code=...&state=...
    Ctrl->>Svc: socialLogin(...) or linkSocialAccount(...)
    Svc->>Api: requestAccessToken(provider, code, codeVerifier)
    Api-->>Svc: access_token
    Svc->>Api: requestUserInfo(provider, access_token)
    Api-->>Svc: userInfo
    Svc->>Domain: find OAuthLink by provider/providerId
    Domain-->>Svc: OAuthLink (or empty)
    Svc-->>Ctrl: TokenPair or success
    Ctrl-->>Client: 302 Redirect with token cookies or success page
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • stoneTiger0912

Poem

🐰 패키지 정원을 가꾸는 토끼의 노래 🌿
auth에서 domain으로, global에서 infrastructure로,
흩어진 파일들 모아 길을 내니 새 구조 빛나네.
OAuth 씨앗 뿌리고 gRPC 길 닦고,
토끼가 한숨에 정리해드렸어요. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 프로젝트 구조 리팩토링이라는 변경사항의 핵심을 명확하게 요약하며, 광범위한 패키지 재구성을 정확히 반영합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/project-structure

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (7)
src/main/java/flipnote/user/domain/common/BizException.java (1)

3-3: 동일 패키지 내 불필요한 import

ErrorCodeBizException과 동일한 패키지(flipnote.user.domain.common)에 있으므로 이 import 문은 불필요합니다.

♻️ 제안된 수정
 package flipnote.user.domain.common;
 
-import flipnote.user.domain.common.ErrorCode;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/flipnote/user/domain/common/BizException.java` at line 3,
Remove the unnecessary import of ErrorCode from BizException since ErrorCode is
in the same package (flipnote.user.domain.common); in the BizException class
delete the line "import flipnote.user.domain.common.ErrorCode" and ensure the
class still compiles (no other missing imports) and build passes for
BizException and any usages.
src/main/java/flipnote/user/infrastructure/jwt/JwtProvider.java (1)

1-6: 불필요한 import 제거 권장

Line 6의 flipnote.user.infrastructure.jwt.JwtProperties import는 동일 패키지 내 클래스이므로 불필요합니다.

♻️ 수정 제안
 package flipnote.user.infrastructure.jwt;

 import flipnote.user.domain.TokenClaims;
 import flipnote.user.domain.TokenPair;
 import flipnote.user.domain.entity.User;
-import flipnote.user.infrastructure.jwt.JwtProperties;
 import io.jsonwebtoken.Claims;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/flipnote/user/infrastructure/jwt/JwtProvider.java` around lines
1 - 6, Remove the redundant import of JwtProperties in JwtProvider.java (the
line importing flipnote.user.infrastructure.jwt.JwtProperties) since
JwtProperties is in the same package; delete that import statement and run a
quick compile to ensure no remaining unresolved references in the JwtProvider
class or other classes in this file.
src/main/java/flipnote/user/application/AuthService.java (1)

22-27: 계층 간 의존성 방향 검토 권장

Application 계층(flipnote.user.application)에서 Interfaces 계층(flipnote.user.interfaces.http.dto.*)의 DTO를 직접 참조하고 있습니다. Clean Architecture 관점에서 내부 계층이 외부 계층에 의존하는 것은 의존성 역전 원칙에 위배됩니다.

권장 방안:

  • Application 계층에 별도의 Command/Query DTO를 정의하고, Interfaces 계층에서 변환하거나
  • 공유 DTO 모듈을 별도로 분리하는 것을 고려해 주세요.

현재 동작에는 문제가 없으나, 향후 확장성과 테스트 용이성을 위해 검토가 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/flipnote/user/application/AuthService.java` around lines 22 -
27, AuthService currently imports external HTTP DTOs (ChangePasswordRequest,
LoginRequest, SignupRequest, SocialLinksResponse, TokenValidateResponse,
UserResponse) from flipnote.user.interfaces.http.dto.*, violating dependency
direction; to fix, introduce application-layer DTOs/commands/queries (e.g.,
AuthSignUpCommand, AuthLoginCommand, ChangePasswordCommand, AuthUserDto,
TokenValidationResult) and update AuthService method signatures to use those
internal types, then move the conversion/mapping between HTTP DTOs and
application DTOs into the Interfaces layer (controllers/assemblers) or a shared
mapping component so flipnote.user.application no longer depends on
flipnote.user.interfaces.
src/main/java/flipnote/user/domain/AuthErrorCode.java (1)

25-27: 에러 코드 순번 확인 권장

AUTH_014 코드가 누락되어 있습니다 (AUTH_013AUTH_015). 의도된 것인지 확인이 필요합니다. 이전에 제거된 코드가 있었다면 향후 혼란 방지를 위해 주석으로 기록해두는 것이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/flipnote/user/domain/AuthErrorCode.java` around lines 25 - 27,
The enum in AuthErrorCode has a missing sequential code (AUTH_014) between
INVALID_OAUTH_PROVIDER ("AUTH_013") and BLACKLISTED_TOKEN ("AUTH_015"); either
renumber BLACKLISTED_TOKEN to "AUTH_014" (update the string in the
BLACKLISTED_TOKEN enum entry) or add a commented placeholder enum entry labeled
AUTH_014 with a short note about why it was removed, so the enum sequence and
history are clear; refer to the enum constants INVALID_OAUTH_PROVIDER,
BLACKLISTED_TOKEN, and INVALIDATED_SESSION when making the change.
src/main/java/flipnote/user/application/UserService.java (1)

45-48: getUserInfofindActiveUser 간 코드 중복 고려.

getUserInfo 메서드(Line 46-47)와 findActiveUser 메서드(Line 103-104)가 동일한 로직을 수행합니다. getUserInfo에서 findActiveUser를 재사용하면 중복을 줄일 수 있습니다.

♻️ 제안된 리팩토링
 public UserInfoResponse getUserInfo(Long userId) {
-  User user = userRepository.findByIdAndStatus(userId, User.Status.ACTIVE)
-    .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND));
+  User user = findActiveUser(userId);
   return UserInfoResponse.from(user);
 }

Also applies to: 102-105

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/flipnote/user/application/UserService.java` around lines 45 -
48, Refactor getUserInfo to reuse the existing findActiveUser method to remove
duplicated lookup/error handling: replace the direct repository call in
UserService.getUserInfo with a call to findActiveUser(userId) and then convert
the returned User to UserInfoResponse via UserInfoResponse.from(...), keeping
existing exceptions (BizException with UserErrorCode.USER_NOT_FOUND) intact by
relying on findActiveUser’s behavior.
src/main/java/flipnote/user/infrastructure/mail/ResendMailService.java (1)

7-7: 불필요한 동일 패키지 import

ResendProperties는 현재 파일과 동일한 패키지(flipnote.user.infrastructure.mail)에 있으므로 이 import 문은 불필요합니다.

♻️ 제안된 수정
 import flipnote.user.domain.common.EmailSendException;
-import flipnote.user.infrastructure.mail.ResendProperties;
 import lombok.RequiredArgsConstructor;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/flipnote/user/infrastructure/mail/ResendMailService.java` at
line 7, ResendMailService에 있는 불필요한 import 문을 제거하세요: 현재 파일과 동일한 패키지인
flipnote.user.infrastructure.mail에서 가져오는 ResendProperties import는 제거해도 됩니다;
ResendMailService 클래스의 import 목록에서 해당 import 줄을 삭제하여 컴파일과 동작에 영향이 없도록 정리하세요.
src/main/java/flipnote/user/interfaces/http/AuthController.java (1)

1-1: 패키지 경로 불일치 가능성

AuthControllerflipnote.user.interfaces 패키지에 있지만, UserController도 동일한 패키지에 있고 HTTP 관련 유틸리티는 flipnote.user.interfaces.http.common에 있습니다. HTTP 컨트롤러들을 flipnote.user.interfaces.http 패키지로 이동하는 것이 레이어드 아키텍처 구조와 더 일관성 있을 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/flipnote/user/interfaces/http/AuthController.java` at line 1,
AuthController is currently declared in package flipnote.user.interfaces but
HTTP controllers belong under flipnote.user.interfaces.http; update the package
declaration in AuthController to flipnote.user.interfaces.http, relocate the
source file to the corresponding package directory, and adjust any
imports/usages (e.g., references from UserController or common utilities in
flipnote.user.interfaces.http.common) to the new package so compilation and
layered architecture remain consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 46-81: Several fenced code blocks in the README are missing a
language specifier (causing MD040 lint warnings); update each triple-backtick
block that contains shell/env/text or file listings to include an appropriate
language tag such as bash or text (e.g., change ``` to ```bash for environment
variable blocks and to ```text for file path examples). Locate the blocks around
the env vars and the file/path examples (the blocks containing
DB_URL/REDIS/GIT/JWT/APP_CLIENT_URL and the blocks showing src/main/java/... and
FlipNote-User/) and add the language tokens consistently to silence the linter.
- Around line 95-97: The README's stated local port (http://localhost:8081 and
Swagger URL /users/swagger-ui.html) conflicts with the Docker example which maps
8081:8080; update the Docker run/compose example to map the same internal port
(e.g., change 8081:8080 to 8081:8081) or change the README URLs so they match
the container mapping, and apply the same fix to the other occurrence of the
port mismatch later in the file (the other block that references the local port
and Docker mapping).
- Around line 17-18: The TOC fragment link may break because the heading uses
emoji and GitHub's slug can differ; fix by adding an explicit HTML anchor before
the heading (e.g., add <a name="execution-and-deployment"></a> immediately above
the "## 🖥️ 실행 및 배포" heading) and update the TOC entry link to point to that
anchor (change the link target in the list entry that currently references
"#-실행-및-배포" to the new "#execution-and-deployment"); do the same for "## 프로젝트
구조" if needed, using clear anchor names to ensure stable TOC links.

In `@src/main/java/flipnote/user/application/OAuthService.java`:
- Around line 93-106: 현재 로직은 특정 사용자(userId)에 대한 중복 링크만 확인하므로 동일한 (provider,
providerId)를 다른 사용자에게도 다시 연결할 수 있습니다;
oAuthLinkRepository.existsByUser_IdAndProviderAndProviderId(...) 체크에 더해 먼저
oAuthLinkRepository.existsByProviderAndProviderId(userInfo.getProvider(),
userInfo.getProviderId())로 전역 중복을 확인하고 중복인 경우
BizException(AuthErrorCode.ALREADY_LINKED_SOCIAL_ACCOUNT)을 던지도록 수정하고, OAuthLink
엔티티(DB 스키마)에 (provider, providerId)에 대한 unique 제약을 추가하며 동시에 save 시 DB 충돌(유니크 위반)
발생 가능성을 고려해 save 호출 주위에서 데이터베이스 고유 제약 위반 예외를 잡아 적절히
AuthErrorCode.ALREADY_LINKED_SOCIAL_ACCOUNT으로 변환하도록 처리하세요 (참조:
oAuthLinkRepository.existsByProviderAndProviderId, oAuthLinkRepository.save,
OAuthLink builder, UserRepository.findByIdAndStatus).

In `@src/main/java/flipnote/user/infrastructure/oauth/OAuthApiClient.java`:
- Around line 33-34: The code reconstructs the redirect URI from the incoming
request (via buildRedirectUri(request, provider.getRedirectUri())), which is
unsafe; change both the token request (where params.add("redirect_uri",
buildRedirectUri(...)) is used) and the authorization request construction (the
block around lines building the auth URL) to use a fixed, pre-registered
redirect URI instead (e.g., provider.getRedirectUri() or a dedicated config
value), and remove/replace any logic in buildRedirectUri that uses
ServletUriComponentsBuilder.fromRequestUri(request) so the redirect URI is not
derived from the request host/scheme.
- Around line 17-18: OAuthApiClient.java의 잘못된 Jackson 패키지 import를 수정하세요: 현재
import lines에서 tools.jackson.core.type.TypeReference와
tools.jackson.databind.ObjectMapper를
com.fasterxml.jackson.core.type.TypeReference와
com.fasterxml.jackson.databind.ObjectMapper로 변경하여 Spring Boot 4 / Jackson 2.x와
일치시키고 다른 클래스(SocialLinkResponse, MyInfoResponse)에서 사용하는
com.fasterxml.jackson.annotation.JsonFormat와 패키지명을 일관되게 유지하세요.

In `@src/main/java/flipnote/user/infrastructure/oauth/OAuthProperties.java`:
- Around line 11-27: Annotate the OAuthProperties configuration so Spring
validates values at startup: add `@Validated` to the OAuthProperties class and
apply javax.validation constraints to Provider fields (e.g., mark clientId,
clientSecret, redirectUri, authorizationUri, tokenUri, userInfoUri with
`@NotBlank` and scope with `@NotEmpty`) so missing or empty values fail fast; keep
the nested class name Provider and the
OAuthProperties/@ConfigurationProperties(prefix = "app.oauth2") setup so
validation triggers on application start.

In `@src/main/java/flipnote/user/interfaces/grpc/GrpcExceptionHandlerImpl.java`:
- Around line 26-32: When handling gRPC errors in GrpcExceptionHandlerImpl,
avoid converting StatusRuntimeException to a new StatusException (which drops
trailers) and also handle StatusException explicitly; instead, if t is a
StatusRuntimeException return the original exception (preserving getTrailers()),
if t is a StatusException return it as-is, otherwise log and return
Status.INTERNAL.withDescription(...).asException(); update the instanceof
branches around the StatusRuntimeException check to return the original
exception objects rather than calling e.getStatus().asException().

In `@src/main/java/flipnote/user/interfaces/http/OAuthController.java`:
- Around line 45-60: The controller currently declares the code RequestParam and
the cookie (HttpConstants.OAUTH_VERIFIER_COOKIE) as required which causes Spring
to return 400 before handleCallback runs; change the signature of handleCallback
so code and codeVerifier are optional (make the `@RequestParam` and `@CookieValue`
non-required / nullable) and add an explicit branch at the top of handleCallback
that detects a missing code or missing codeVerifier and immediately redirects to
your login/link failure URL (reuse the same failure redirect logic used in
handleSocialLogin and handleSocialLink) before deleting the cookie and before
calling handleSocialLogin or handleSocialLink so the existing failure handling
is executed.

---

Nitpick comments:
In `@src/main/java/flipnote/user/application/AuthService.java`:
- Around line 22-27: AuthService currently imports external HTTP DTOs
(ChangePasswordRequest, LoginRequest, SignupRequest, SocialLinksResponse,
TokenValidateResponse, UserResponse) from flipnote.user.interfaces.http.dto.*,
violating dependency direction; to fix, introduce application-layer
DTOs/commands/queries (e.g., AuthSignUpCommand, AuthLoginCommand,
ChangePasswordCommand, AuthUserDto, TokenValidationResult) and update
AuthService method signatures to use those internal types, then move the
conversion/mapping between HTTP DTOs and application DTOs into the Interfaces
layer (controllers/assemblers) or a shared mapping component so
flipnote.user.application no longer depends on flipnote.user.interfaces.

In `@src/main/java/flipnote/user/application/UserService.java`:
- Around line 45-48: Refactor getUserInfo to reuse the existing findActiveUser
method to remove duplicated lookup/error handling: replace the direct repository
call in UserService.getUserInfo with a call to findActiveUser(userId) and then
convert the returned User to UserInfoResponse via UserInfoResponse.from(...),
keeping existing exceptions (BizException with UserErrorCode.USER_NOT_FOUND)
intact by relying on findActiveUser’s behavior.

In `@src/main/java/flipnote/user/domain/AuthErrorCode.java`:
- Around line 25-27: The enum in AuthErrorCode has a missing sequential code
(AUTH_014) between INVALID_OAUTH_PROVIDER ("AUTH_013") and BLACKLISTED_TOKEN
("AUTH_015"); either renumber BLACKLISTED_TOKEN to "AUTH_014" (update the string
in the BLACKLISTED_TOKEN enum entry) or add a commented placeholder enum entry
labeled AUTH_014 with a short note about why it was removed, so the enum
sequence and history are clear; refer to the enum constants
INVALID_OAUTH_PROVIDER, BLACKLISTED_TOKEN, and INVALIDATED_SESSION when making
the change.

In `@src/main/java/flipnote/user/domain/common/BizException.java`:
- Line 3: Remove the unnecessary import of ErrorCode from BizException since
ErrorCode is in the same package (flipnote.user.domain.common); in the
BizException class delete the line "import
flipnote.user.domain.common.ErrorCode" and ensure the class still compiles (no
other missing imports) and build passes for BizException and any usages.

In `@src/main/java/flipnote/user/infrastructure/jwt/JwtProvider.java`:
- Around line 1-6: Remove the redundant import of JwtProperties in
JwtProvider.java (the line importing
flipnote.user.infrastructure.jwt.JwtProperties) since JwtProperties is in the
same package; delete that import statement and run a quick compile to ensure no
remaining unresolved references in the JwtProvider class or other classes in
this file.

In `@src/main/java/flipnote/user/infrastructure/mail/ResendMailService.java`:
- Line 7: ResendMailService에 있는 불필요한 import 문을 제거하세요: 현재 파일과 동일한 패키지인
flipnote.user.infrastructure.mail에서 가져오는 ResendProperties import는 제거해도 됩니다;
ResendMailService 클래스의 import 목록에서 해당 import 줄을 삭제하여 컴파일과 동작에 영향이 없도록 정리하세요.

In `@src/main/java/flipnote/user/interfaces/http/AuthController.java`:
- Line 1: AuthController is currently declared in package
flipnote.user.interfaces but HTTP controllers belong under
flipnote.user.interfaces.http; update the package declaration in AuthController
to flipnote.user.interfaces.http, relocate the source file to the corresponding
package directory, and adjust any imports/usages (e.g., references from
UserController or common utilities in flipnote.user.interfaces.http.common) to
the new package so compilation and layered architecture remain consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4122d2f3-c3b4-43be-ba8a-88093b06b6b1

📥 Commits

Reviewing files that changed from the base of the PR and between 9d8831e and 5e6c7d7.

📒 Files selected for processing (72)
  • README.md
  • src/main/java/flipnote/user/application/AuthService.java
  • src/main/java/flipnote/user/application/OAuthService.java
  • src/main/java/flipnote/user/application/UserService.java
  • src/main/java/flipnote/user/domain/AuthErrorCode.java
  • src/main/java/flipnote/user/domain/ImageErrorCode.java
  • src/main/java/flipnote/user/domain/PasswordResetConstants.java
  • src/main/java/flipnote/user/domain/TokenClaims.java
  • src/main/java/flipnote/user/domain/TokenPair.java
  • src/main/java/flipnote/user/domain/UserErrorCode.java
  • src/main/java/flipnote/user/domain/VerificationConstants.java
  • src/main/java/flipnote/user/domain/common/BizException.java
  • src/main/java/flipnote/user/domain/common/EmailSendException.java
  • src/main/java/flipnote/user/domain/common/ErrorCode.java
  • src/main/java/flipnote/user/domain/entity/BaseEntity.java
  • src/main/java/flipnote/user/domain/entity/OAuthLink.java
  • src/main/java/flipnote/user/domain/entity/User.java
  • src/main/java/flipnote/user/domain/event/EmailVerificationSendEvent.java
  • src/main/java/flipnote/user/domain/event/PasswordResetCreateEvent.java
  • src/main/java/flipnote/user/domain/repository/OAuthLinkRepository.java
  • src/main/java/flipnote/user/domain/repository/UserRepository.java
  • src/main/java/flipnote/user/infrastructure/config/AppConfig.java
  • src/main/java/flipnote/user/infrastructure/config/ClientProperties.java
  • src/main/java/flipnote/user/infrastructure/config/GrpcClientConfig.java
  • src/main/java/flipnote/user/infrastructure/config/JpaAuditingConfig.java
  • src/main/java/flipnote/user/infrastructure/config/SwaggerConfig.java
  • src/main/java/flipnote/user/infrastructure/jwt/JwtProperties.java
  • src/main/java/flipnote/user/infrastructure/jwt/JwtProvider.java
  • src/main/java/flipnote/user/infrastructure/listener/EmailVerificationEventListener.java
  • src/main/java/flipnote/user/infrastructure/listener/PasswordResetEventListener.java
  • src/main/java/flipnote/user/infrastructure/mail/MailService.java
  • src/main/java/flipnote/user/infrastructure/mail/ResendConfig.java
  • src/main/java/flipnote/user/infrastructure/mail/ResendMailService.java
  • src/main/java/flipnote/user/infrastructure/mail/ResendProperties.java
  • src/main/java/flipnote/user/infrastructure/oauth/GoogleUserInfo.java
  • src/main/java/flipnote/user/infrastructure/oauth/OAuth2UserInfo.java
  • src/main/java/flipnote/user/infrastructure/oauth/OAuthApiClient.java
  • src/main/java/flipnote/user/infrastructure/oauth/OAuthProperties.java
  • src/main/java/flipnote/user/infrastructure/oauth/PkceUtil.java
  • src/main/java/flipnote/user/infrastructure/redis/EmailVerificationRepository.java
  • src/main/java/flipnote/user/infrastructure/redis/PasswordResetRepository.java
  • src/main/java/flipnote/user/infrastructure/redis/PasswordResetTokenGenerator.java
  • src/main/java/flipnote/user/infrastructure/redis/SessionInvalidationRepository.java
  • src/main/java/flipnote/user/infrastructure/redis/SocialLinkTokenRepository.java
  • src/main/java/flipnote/user/infrastructure/redis/TokenBlacklistRepository.java
  • src/main/java/flipnote/user/infrastructure/redis/VerificationCodeGenerator.java
  • src/main/java/flipnote/user/interfaces/grpc/GrpcExceptionHandlerImpl.java
  • src/main/java/flipnote/user/interfaces/grpc/GrpcUserQueryService.java
  • src/main/java/flipnote/user/interfaces/http/AuthController.java
  • src/main/java/flipnote/user/interfaces/http/OAuthController.java
  • src/main/java/flipnote/user/interfaces/http/UserController.java
  • src/main/java/flipnote/user/interfaces/http/common/ApiResponse.java
  • src/main/java/flipnote/user/interfaces/http/common/ApiResponseAdvice.java
  • src/main/java/flipnote/user/interfaces/http/common/CookieUtil.java
  • src/main/java/flipnote/user/interfaces/http/common/GlobalExceptionHandler.java
  • src/main/java/flipnote/user/interfaces/http/common/HttpConstants.java
  • src/main/java/flipnote/user/interfaces/http/dto/request/ChangePasswordRequest.java
  • src/main/java/flipnote/user/interfaces/http/dto/request/EmailVerificationRequest.java
  • src/main/java/flipnote/user/interfaces/http/dto/request/EmailVerifyRequest.java
  • src/main/java/flipnote/user/interfaces/http/dto/request/LoginRequest.java
  • src/main/java/flipnote/user/interfaces/http/dto/request/PasswordResetCreateRequest.java
  • src/main/java/flipnote/user/interfaces/http/dto/request/PasswordResetRequest.java
  • src/main/java/flipnote/user/interfaces/http/dto/request/SignupRequest.java
  • src/main/java/flipnote/user/interfaces/http/dto/request/TokenValidateRequest.java
  • src/main/java/flipnote/user/interfaces/http/dto/request/UpdateProfileRequest.java
  • src/main/java/flipnote/user/interfaces/http/dto/response/MyInfoResponse.java
  • src/main/java/flipnote/user/interfaces/http/dto/response/SocialLinkResponse.java
  • src/main/java/flipnote/user/interfaces/http/dto/response/SocialLinksResponse.java
  • src/main/java/flipnote/user/interfaces/http/dto/response/TokenValidateResponse.java
  • src/main/java/flipnote/user/interfaces/http/dto/response/UserInfoResponse.java
  • src/main/java/flipnote/user/interfaces/http/dto/response/UserResponse.java
  • src/main/java/flipnote/user/interfaces/http/dto/response/UserUpdateResponse.java

Comment on lines +17 to +18
- [실행 및 배포](#-실행-및-배포)
- [프로젝트 구조](#-프로젝트-구조)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

TOC 앵커(fragment) 깨짐 가능성이 있습니다.

이모지가 포함된 제목(## 🖥️ 실행 및 배포)을 #-실행-및-배포로 직접 참조하면 GitHub slug와 불일치할 수 있어 목차 링크가 동작하지 않을 수 있습니다. 명시적 HTML 앵커를 두고 그 앵커를 링크하도록 고정하는 방식이 안전합니다.

수정 예시
- - [실행 및 배포](`#-실행-및-배포`)
+ - [실행 및 배포](`#run-and-deploy`)

- ## 🖥️ 실행 및 배포
+ <a id="run-and-deploy"></a>
+ ## 🖥️ 실행 및 배포
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 17-17: Link fragments should be valid

(MD051, link-fragments)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 17 - 18, The TOC fragment link may break because the
heading uses emoji and GitHub's slug can differ; fix by adding an explicit HTML
anchor before the heading (e.g., add <a name="execution-and-deployment"></a>
immediately above the "## 🖥️ 실행 및 배포" heading) and update the TOC entry link to
point to that anchor (change the link target in the list entry that currently
references "#-실행-및-배포" to the new "#execution-and-deployment"); do the same for
"## 프로젝트 구조" if needed, using clear anchor names to ensure stable TOC links.

Comment on lines +46 to +81
```
# ─── Database ───────────────────────────────────────────
DB_URL=jdbc:mysql://localhost:3306/flipnote_user
DB_USERNAME=
DB_PASSWORD=

# ─── Redis ──────────────────────────────────────────────
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=

# ─── JPA ────────────────────────────────────────────────
# create | create-drop | update | validate | none
DDL_AUTO=update

# ─── gRPC ───────────────────────────────────────────────
GRPC_PORT=9092

# ─── JWT ────────────────────────────────────────────────
JWT_SECRET=
# 액세스 토큰 만료 시간 (ms), 기본값 900000 (15분)
JWT_ACCESS_EXPIRATION=900000
# 리프레시 토큰 만료 시간 (ms), 기본값 604800000 (7일)
JWT_REFRESH_EXPIRATION=604800000

# ─── Email (Resend) ─────────────────────────────────────
APP_RESEND_API_KEY=

# ─── Client ─────────────────────────────────────────────
# 프론트엔드 URL (CORS, 리다이렉트에 사용)
APP_CLIENT_URL=http://localhost:3000

# ─── Google OAuth2 ──────────────────────────────────────
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

펜스 코드블록에 언어를 지정해 주세요.

현재 여러 코드블록에 language 지정이 없어 린트 경고(MD040)가 발생합니다. 최소 bash/text를 지정해 주세요.

수정 예시
- ```
+ ```bash
# ─── Database ───────────────────────────────────────────
...
- ```
+ ```

-    ```
+    ```text
    src/main/java/flipnote/user/
    ...
-    ```
+    ```

- ```
+ ```text
FlipNote-User/
...
- ```
+ ```

Also applies to: 148-154, 156-228

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 46-46: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 46 - 81, Several fenced code blocks in the README are
missing a language specifier (causing MD040 lint warnings); update each
triple-backtick block that contains shell/env/text or file listings to include
an appropriate language tag such as bash or text (e.g., change ``` to ```bash
for environment variable blocks and to ```text for file path examples). Locate
the blocks around the env vars and the file/path examples (the blocks containing
DB_URL/REDIS/GIT/JWT/APP_CLIENT_URL and the blocks showing src/main/java/... and
FlipNote-User/) and add the language tokens consistently to silence the linter.

Comment on lines +95 to +97
기본적으로 `http://localhost:8081`에서 실행됩니다.
Swagger UI는 `http://localhost:8081/users/swagger-ui.html`에서 확인할 수 있습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

로컬 기본 포트 안내와 Docker 포트 매핑이 서로 다릅니다.

문서상 기본 실행 포트는 8081인데 Docker 예시는 8081:8080으로 되어 있어, 독자가 그대로 따라 하면 접속 실패를 겪을 수 있습니다. 애플리케이션 컨테이너 내부 포트와 설명을 동일하게 맞춰 주세요.

수정 예시 (앱이 8081로 뜨는 경우)
-docker run -p 8081:8080 \
+docker run -p 8081:8081 \
  -e DB_URL=... \
  -e JWT_SECRET=... \
  flipnote-user

Also applies to: 119-123

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 95 - 97, The README's stated local port
(http://localhost:8081 and Swagger URL /users/swagger-ui.html) conflicts with
the Docker example which maps 8081:8080; update the Docker run/compose example
to map the same internal port (e.g., change 8081:8080 to 8081:8081) or change
the README URLs so they match the container mapping, and apply the same fix to
the other occurrence of the port mismatch later in the file (the other block
that references the local port and Docker mapping).

Comment on lines +93 to +106
if (oAuthLinkRepository.existsByUser_IdAndProviderAndProviderId(
userId, userInfo.getProvider(), userInfo.getProviderId())) {
throw new BizException(AuthErrorCode.ALREADY_LINKED_SOCIAL_ACCOUNT);
}

User user = userRepository.findByIdAndStatus(userId, User.Status.ACTIVE)
.orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND));

OAuthLink link = OAuthLink.builder()
.provider(userInfo.getProvider())
.providerId(userInfo.getProviderId())
.user(user)
.build();
oAuthLinkRepository.save(link);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

같은 소셜 계정을 다른 사용자에게 다시 연결할 수 있습니다.

지금 체크는 "이 사용자에게 이미 같은 링크가 있나"만 봅니다. 다른 사용자에게 이미 연결된 (provider, providerId) 는 서비스 레벨에서 막지 못해서, DB 제약이 없으면 중복 링크가 생성되고 제약이 있어도 여기서는 ALREADY_LINKED_SOCIAL_ACCOUNT 대신 DB 예외로 떨어질 가능성이 큽니다. 여기서는 provider + providerId 전역 기준으로 검사하고, DB에도 같은 unique constraint를 두는 편이 맞습니다.

수정 방향
-        if (oAuthLinkRepository.existsByUser_IdAndProviderAndProviderId(
-                userId, userInfo.getProvider(), userInfo.getProviderId())) {
+        if (oAuthLinkRepository.existsByProviderAndProviderId(
+                userInfo.getProvider(), userInfo.getProviderId())) {
             throw new BizException(AuthErrorCode.ALREADY_LINKED_SOCIAL_ACCOUNT);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/flipnote/user/application/OAuthService.java` around lines 93 -
106, 현재 로직은 특정 사용자(userId)에 대한 중복 링크만 확인하므로 동일한 (provider, providerId)를 다른
사용자에게도 다시 연결할 수 있습니다;
oAuthLinkRepository.existsByUser_IdAndProviderAndProviderId(...) 체크에 더해 먼저
oAuthLinkRepository.existsByProviderAndProviderId(userInfo.getProvider(),
userInfo.getProviderId())로 전역 중복을 확인하고 중복인 경우
BizException(AuthErrorCode.ALREADY_LINKED_SOCIAL_ACCOUNT)을 던지도록 수정하고, OAuthLink
엔티티(DB 스키마)에 (provider, providerId)에 대한 unique 제약을 추가하며 동시에 save 시 DB 충돌(유니크 위반)
발생 가능성을 고려해 save 호출 주위에서 데이터베이스 고유 제약 위반 예외를 잡아 적절히
AuthErrorCode.ALREADY_LINKED_SOCIAL_ACCOUNT으로 변환하도록 처리하세요 (참조:
oAuthLinkRepository.existsByProviderAndProviderId, oAuthLinkRepository.save,
OAuthLink builder, UserRepository.findByIdAndStatus).

Comment on lines +17 to +18
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
fd -a 'pom\.xml|build\.gradle(\.kts)?|settings\.gradle(\.kts)?|gradle\.properties' . \
  -x sh -c 'echo "### $1"; sed -n "1,220p" "$1"' sh {}
rg -n -C2 'tools\.jackson|com\.fasterxml\.jackson|jackson' .

Repository: FlipNoteTeam/FlipNote-User

Length of output: 4918


🌐 Web query:

Spring Boot 4.0.2 Jackson dependency version

💡 Result:

Spring Boot 4.0.2 manages Jackson 3 (default) at version 2.21.2 for core modules like jackson-core, jackson-databind, and jackson-annotations at 2.21. Jackson 2 support is available separately via spring-boot-jackson2 (deprecated) with BOM upgrades tracked in issues like #48910 (2.20.2 for 4.0.2) and later #49389 (2.21.1).

Citations:


🏁 Script executed:

rg -i 'jackson' build.gradle.kts pom.xml --no-heading -B2 -A2

Repository: FlipNoteTeam/FlipNote-User

Length of output: 393


올바른 Jackson 패키지명으로 변경해 주세요.

OAuthApiClient.java 17-18번 줄의 import 문이 잘못되었습니다. 프로젝트는 Spring Boot 4.0.2를 사용하며, 이는 Jackson 2.21.2를 포함합니다. Jackson 2.x의 패키지명은 com.fasterxml.jackson.*입니다.

현재 코드는 Jackson 3.x 패키지명인 tools.jackson.*을 사용하고 있어서 컴파일되지 않습니다. 다른 파일들(SocialLinkResponse.java, MyInfoResponse.java)에서도 com.fasterxml.jackson.annotation.JsonFormat를 사용하고 있으니 일관성을 맞춰야 합니다.

수정 필요 부분
- import tools.jackson.core.type.TypeReference;
- import tools.jackson.databind.ObjectMapper;
+ import com.fasterxml.jackson.core.type.TypeReference;
+ import com.fasterxml.jackson.databind.ObjectMapper;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/flipnote/user/infrastructure/oauth/OAuthApiClient.java` around
lines 17 - 18, OAuthApiClient.java의 잘못된 Jackson 패키지 import를 수정하세요: 현재 import
lines에서 tools.jackson.core.type.TypeReference와
tools.jackson.databind.ObjectMapper를
com.fasterxml.jackson.core.type.TypeReference와
com.fasterxml.jackson.databind.ObjectMapper로 변경하여 Spring Boot 4 / Jackson 2.x와
일치시키고 다른 클래스(SocialLinkResponse, MyInfoResponse)에서 사용하는
com.fasterxml.jackson.annotation.JsonFormat와 패키지명을 일관되게 유지하세요.

Comment on lines +33 to +34
params.add("redirect_uri", buildRedirectUri(request, provider.getRedirectUri()));
params.add("code", code);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/main/java/flipnote/user/infrastructure/oauth/OAuthApiClient.java | head -150

Repository: FlipNoteTeam/FlipNote-User

Length of output: 4806


redirect URI를 요청 Host에서 재구성하지 마세요.

buildRedirectUri() 메서드가 ServletUriComponentsBuilder.fromRequestUri(request)를 사용하여 HTTP 요청의 host/scheme에서 redirect URI를 동적으로 구성하고 있습니다. OAuth 2.0 사양에 따르면 redirect URI는 OAuth 제공자(Google 등)에 사전 등록된 값과 정확히 일치해야 합니다.

요청 헤더에 기반한 구성은 다음 문제를 야기합니다:

  • Host 헤더 주입을 통한 공격 가능성
  • 리버스 프록시/로드 밸런서의 X-Forwarded-* 헤더가 등록된 URI와 다를 경우 OAuth 흐름 실패
  • CSRF 및 열린 리다이렉트 취약점 위험

신뢰된 설정값(절대 URI 또는 설정에서 읽은 고정 base URL)으로 redirect URI를 고정하세요. 이는 authorization 요청(73-77)과 토큰 요청(33-34) 양쪽 모두에 적용됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/flipnote/user/infrastructure/oauth/OAuthApiClient.java` around
lines 33 - 34, The code reconstructs the redirect URI from the incoming request
(via buildRedirectUri(request, provider.getRedirectUri())), which is unsafe;
change both the token request (where params.add("redirect_uri",
buildRedirectUri(...)) is used) and the authorization request construction (the
block around lines building the auth URL) to use a fixed, pre-registered
redirect URI instead (e.g., provider.getRedirectUri() or a dedicated config
value), and remove/replace any logic in buildRedirectUri that uses
ServletUriComponentsBuilder.fromRequestUri(request) so the redirect URI is not
derived from the request host/scheme.

Comment on lines +11 to +27
@Getter
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "app.oauth2")
public class OAuthProperties {

private final Map<String, Provider> providers;

@Getter
@Setter
public static class Provider {
private String clientId;
private String clientSecret;
private String redirectUri;
private String authorizationUri;
private String tokenUri;
private String userInfoUri;
private List<String> scope;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/main/java/flipnote/user/infrastructure/oauth/OAuthProperties.java

Repository: FlipNoteTeam/FlipNote-User

Length of output: 1007


🏁 Script executed:

# Search for OAuthApiClient and how these properties are used
find . -type f -name "*.java" | xargs grep -l "OAuthApiClient\|OAuthProperties" | head -20

Repository: FlipNoteTeam/FlipNote-User

Length of output: 272


🏁 Script executed:

# Check for any Spring validation configuration or `@Validated` annotations
rg -A 5 "@Validated|@Valid|validation" --type java src/main/java/flipnote/user/infrastructure/oauth/ 2>/dev/null || echo "No validation annotations found in oauth module"

Repository: FlipNoteTeam/FlipNote-User

Length of output: 117


🏁 Script executed:

# Look for application configuration files
find . -type f \( -name "application*.yml" -o -name "application*.yaml" -o -name "application*.properties" \) | head -10

Repository: FlipNoteTeam/FlipNote-User

Length of output: 143


🏁 Script executed:

# Check the actual usage of OAuthProperties to see if there are null checks
rg -B 3 -A 8 "providers\.|Provider\s" src/main/java/flipnote/user/infrastructure/oauth/ --type java | head -50

Repository: FlipNoteTeam/FlipNote-User

Length of output: 5465


🏁 Script executed:

cat -n src/main/java/flipnote/user/infrastructure/oauth/OAuthApiClient.java

Repository: FlipNoteTeam/FlipNote-User

Length of output: 4806


🏁 Script executed:

cat -n src/main/resources/application.yml

Repository: FlipNoteTeam/FlipNote-User

Length of output: 2875


🏁 Script executed:

# Check OAuthService for any validation or `@Validated` annotation
cat -n src/main/java/flipnote/user/application/OAuthService.java

Repository: FlipNoteTeam/FlipNote-User

Length of output: 6648


🏁 Script executed:

# Search for `@Validated` in the codebase
rg "@Validated" --type java src/main/java/

Repository: FlipNoteTeam/FlipNote-User

Length of output: 150


OAuth 설정 필드에 필수 검증을 추가하세요.

현재 redirectUri, authorizationUri, tokenUri, userInfoUri, scope 같은 값이 비어있거나 null이어도 애플리케이션이 정상적으로 시작됩니다. 문제는 첫 OAuth 요청 시점에 발생합니다:

  • 라인 79의 String.join(" ", provider.getScope())는 scope가 null일 때 NPE 발생
  • 라인 39, 55, 75의 .uri() 호출들은 null/empty 값으로 요청 실패
  • 라인 33, 77의 redirectUri 문제로 redirect 실패

application.yml에서 ${GOOGLE_CLIENT_ID:} 같은 설정이 환경 변수로 외부화되어 있어, 누락 시 빈 값이 로드될 수 있습니다. Provider 클래스에 @Validated를 활성화하고 필드에 @NotBlank, @NotEmpty 어노테이션을 추가하여 시작 시점에 설정 오류를 감지하는 것이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/flipnote/user/infrastructure/oauth/OAuthProperties.java` around
lines 11 - 27, Annotate the OAuthProperties configuration so Spring validates
values at startup: add `@Validated` to the OAuthProperties class and apply
javax.validation constraints to Provider fields (e.g., mark clientId,
clientSecret, redirectUri, authorizationUri, tokenUri, userInfoUri with
`@NotBlank` and scope with `@NotEmpty`) so missing or empty values fail fast; keep
the nested class name Provider and the
OAuthProperties/@ConfigurationProperties(prefix = "app.oauth2") setup so
validation triggers on application start.

Comment on lines +26 to +32
if (t instanceof StatusRuntimeException e) {
log.warn("gRPC StatusRuntimeException: status={}, description={}",
e.getStatus().getCode(), e.getStatus().getDescription());
return e.getStatus().asException();
}
log.error("gRPC Unhandled exception", t);
return Status.INTERNAL.withDescription("Internal server error").asException();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and inspect the actual file
find . -type f -name "GrpcExceptionHandlerImpl.java" 2>/dev/null

Repository: FlipNoteTeam/FlipNote-User

Length of output: 145


🏁 Script executed:

# Let's also search for any gRPC related files to understand the codebase structure
fd -e java | grep -i grpc | head -20

Repository: FlipNoteTeam/FlipNote-User

Length of output: 285


🏁 Script executed:

# Search for imports and usage of StatusException and StatusRuntimeException
rg "StatusException|StatusRuntimeException" --type java -B 2 -A 5

Repository: FlipNoteTeam/FlipNote-User

Length of output: 3126


🏁 Script executed:

# Let's read the complete file to understand the full context
cat -n src/main/java/flipnote/user/interfaces/grpc/GrpcExceptionHandlerImpl.java

Repository: FlipNoteTeam/FlipNote-User

Length of output: 2208


🌐 Web query:

gRPC StatusRuntimeException getTrailers() method API documentation

💡 Result:

The gRPC Java class io.grpc.StatusRuntimeException has a method getTrailers that returns the received trailers as a Metadata object. Method signature: @Nullable public final Metadata getTrailers Description: Returns the received trailers. This method was introduced in gRPC Java version 1.0.0 and is available in recent versions like 1.80.0. This exception is used to propagate gRPC status information, including trailers, via exceptions. Trailers can be provided at construction: public StatusRuntimeException(Status status, @Nullable Metadata trailers) To extract trailers from a caught exception: try { // gRPC call } catch (StatusRuntimeException e) { Metadata trailers = e.getTrailers; } Note: When throwing StatusRuntimeException on the server side, trailers may not propagate correctly unless handled via ServerCall.onError; direct throws lose trailers due to exception handling mechanics (as discussed in grpc/grpc-java issue #6683).

Citations:


🌐 Web query:

gRPC Status.asException() method with Metadata trailers parameter

💡 Result:

The gRPC Java library provides the Status.asException(Metadata trailers) method in the io.grpc.Status class. This static factory method converts a Status object to a StatusException (a checked exception) that includes the provided Metadata as trailers. There is also a corresponding Status.asRuntimeException(Metadata trailers) method that returns a StatusRuntimeException (unchecked exception) with the trailers. Usage example from official patterns: Metadata trailers = new Metadata; trailers.put(Metadata.Key.of("custom-key", Metadata.ASCII_STRING_MARSHALLER), "custom-value"); Status status = Status.INVALID_ARGUMENT.withDescription("Invalid input"); throw status.asException(trailers); // or .asRuntimeException(trailers) On the receiving end (client or server error handling), extract trailers with exception.getTrailers or Status.trailersFromThrowable(exception). This is available since gRPC-Java 1.0.0 and stable as of recent versions (1.80.0 API docs). Trailers are sent as HTTP/2 trailers in error responses, useful for additional error details, tracing, or metrics. Note: When throwing these exceptions from server implementations, gRPC extracts and transmits the trailers correctly if handled properly (e.g., via ServerCall.onError or direct exception propagation in supported contexts). Some frameworks like Quarkus had early bugs but are resolved.

Citations:


gRPC 상태와 메타데이터가 손실되고 있습니다.

StatusRuntimeException을 처리할 때 e.getStatus().asException()으로 변환하면서 trailers(메타데이터)가 사라집니다. 또한 StatusException을 별도로 처리하지 않아서, 이미 구성된 gRPC 상태도 라인 32의 INTERNAL 오류로 덮어씌워집니다.

수정 예시:

권장 수정
+        if (t instanceof StatusException e) {
+            log.warn("gRPC StatusException: status={}, description={}",
+                    e.getStatus().getCode(), e.getStatus().getDescription());
+            return e;
+        }
         if (t instanceof StatusRuntimeException e) {
             log.warn("gRPC StatusRuntimeException: status={}, description={}",
                     e.getStatus().getCode(), e.getStatus().getDescription());
-            return e.getStatus().asException();
+            return e.getStatus().asException(e.getTrailers());
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/flipnote/user/interfaces/grpc/GrpcExceptionHandlerImpl.java`
around lines 26 - 32, When handling gRPC errors in GrpcExceptionHandlerImpl,
avoid converting StatusRuntimeException to a new StatusException (which drops
trailers) and also handle StatusException explicitly; instead, if t is a
StatusRuntimeException return the original exception (preserving getTrailers()),
if t is a StatusException return it as-is, otherwise log and return
Status.INTERNAL.withDescription(...).asException(); update the instanceof
branches around the StatusRuntimeException check to return the original
exception objects rather than calling e.getStatus().asException().

Comment on lines +45 to +60
@GetMapping("/oauth2/callback/{provider}")
public ResponseEntity<Void> handleCallback(
@PathVariable String provider,
@RequestParam String code,
@RequestParam(required = false) String state,
@CookieValue(HttpConstants.OAUTH_VERIFIER_COOKIE) String codeVerifier,
HttpServletRequest request,
HttpServletResponse response) {

CookieUtil.deleteCookie(response, HttpConstants.OAUTH_VERIFIER_COOKIE);

boolean isSocialLinkRequest = StringUtils.hasText(state);
if (isSocialLinkRequest) {
return handleSocialLink(provider, code, state, codeVerifier, request);
}
return handleSocialLogin(provider, code, codeVerifier, request, response);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate the OAuthController file and examine its structure
find . -name "OAuthController.java" -type f 2>/dev/null

Repository: FlipNoteTeam/FlipNote-User

Length of output: 136


🏁 Script executed:

# Check the file size before reading
wc -l src/main/java/flipnote/user/interfaces/http/OAuthController.java 2>/dev/null || echo "File not found in this path, searching..."

Repository: FlipNoteTeam/FlipNote-User

Length of output: 138


🏁 Script executed:

# Search for OAuthController across the repository
rg "class OAuthController" --type java

Repository: FlipNoteTeam/FlipNote-User

Length of output: 165


🏁 Script executed:

cat -n src/main/java/flipnote/user/interfaces/http/OAuthController.java

Repository: FlipNoteTeam/FlipNote-User

Length of output: 5644


필수 파라미터로 인해 콜백 실패 케이스가 컨트롤러에 도달하기 전에 400으로 끝납니다.

현재 code 파라미터(48줄)와 verifier cookie(50줄)를 필수로 설정했으므로, OAuth 제공자의 error=access_denied 콜백이나 만료된 cookie 케이스에서 Spring이 요청을 거부하고 이 메서드는 실행되지 않습니다. 따라서 6379줄의 handleSocialLogin과 8298줄의 handleSocialLink에 구현된 실패 리다이렉트 로직이 우회되어 사용자가 적절한 실패 페이지로 이동하지 못합니다.

codecodeVerifier를 모두 선택적으로 변경하고, 누락 시 로그인/연동 실패 URL로 직접 리다이렉트하는 분기를 추가하세요.

수정 예시
-            `@RequestParam` String code,
+            `@RequestParam`(required = false) String code,
             `@RequestParam`(required = false) String state,
-            `@CookieValue`(HttpConstants.OAUTH_VERIFIER_COOKIE) String codeVerifier,
+            `@CookieValue`(value = HttpConstants.OAUTH_VERIFIER_COOKIE, required = false) String codeVerifier,
             HttpServletRequest request,
             HttpServletResponse response) {
+        if (!StringUtils.hasText(code) || !StringUtils.hasText(codeVerifier)) {
+            String failurePath = StringUtils.hasText(state)
+                    ? clientProperties.getPaths().getSocialLinkFailure()
+                    : clientProperties.getPaths().getSocialLoginFailure();
+            return ResponseEntity.status(HttpStatus.FOUND)
+                    .location(URI.create(clientProperties.getUrl() + failurePath))
+                    .build();
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/flipnote/user/interfaces/http/OAuthController.java` around
lines 45 - 60, The controller currently declares the code RequestParam and the
cookie (HttpConstants.OAUTH_VERIFIER_COOKIE) as required which causes Spring to
return 400 before handleCallback runs; change the signature of handleCallback so
code and codeVerifier are optional (make the `@RequestParam` and `@CookieValue`
non-required / nullable) and add an explicit branch at the top of handleCallback
that detects a missing code or missing codeVerifier and immediately redirects to
your login/link failure URL (reuse the same failure redirect logic used in
handleSocialLogin and handleSocialLink) before deleting the cookie and before
calling handleSocialLogin or handleSocialLink so the existing failure handling
is executed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant