Skip to content

feat: 로그아웃, 회원탈퇴, 회원복구 기능 추가#48

Merged
kon28289 merged 13 commits intodevfrom
feat/user
Dec 2, 2025
Merged

feat: 로그아웃, 회원탈퇴, 회원복구 기능 추가#48
kon28289 merged 13 commits intodevfrom
feat/user

Conversation

@kon28289
Copy link
Copy Markdown
Contributor

@kon28289 kon28289 commented Dec 2, 2025

🚀 1. 개요

  • 사용자의 로그아웃, 회원탈퇴, 회원복구 기능을 추가합니다.
  • 사용자는 특정 oauth 계정 회원탈퇴 후 해당 oauth 계정으로 동일한 정보를 사용해 재로그인 시 탈퇴한 계정을 복구할 수 있습니다.

📝 2. 주요 변경 사항

  • 회원탈퇴 시 deletedAt=now()로 하는 소프트삭제를 진행하고, 사용자의 닉네임, 학번, 이메일, 학교 이메일을 사용해 다른 oauth 계정을 통해 가입할 수 있도록 delete_ 접두사를 붙여 저장합니다.
  • Jwt 생성 시 사용되는 클레임에 탈퇴 여부를 확인할 수 있는 withdrawn 필드를 추가합니다.
  • 사용자는 탈퇴한 oauth 계정으로 재로그인 시 탈퇴한 계정의 accessToken을 받고, 이 accessToken은 회원복구를 위한 엔드포인트 /restore를 제외한 다른 api를 호출할 수 없습니다.

📸 3. 스크린샷 (API 테스트 결과)

Summary by CodeRabbit

  • New Features

    • 사용자 로그아웃, 계정 탈퇴, 탈퇴 계정 복구 API 추가 — 복구 시 개인정보 복원 및 토큰 재발급, 로그아웃/탈퇴 시 토큰 무효화
    • OAuth2 인증 실패 핸들러 등록 및 OAuth2 로그인 흐름 개선
    • JWT에 탈퇴 상태 포함하여 복구 허용 흐름 반영
    • 엔티티 소프트 삭제 및 복구 기능 추가
  • Bug Fixes

    • 탈퇴한 계정의 접근 차단 로직 추가(복구 엔드포인트 예외)

✏️ Tip: You can customize this high-level summary in your review settings.

@kon28289 kon28289 requested a review from Juhye0k December 2, 2025 10:01
@kon28289 kon28289 self-assigned this Dec 2, 2025
@kon28289 kon28289 added documentation Improvements or additions to documentation enhancement New feature or request labels Dec 2, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Dec 2, 2025

Walkthrough

탈퇴 상태를 JWT 클레임에 추가하고 인증 필터에서 탈퇴 사용자를 차단하되 복구 엔드포인트는 허용합니다. 로그아웃·탈퇴·복구 API와 소프트 삭제/복구 로직, OAuth2 핸들러·보안 설정 연동이 추가되었습니다.

Changes

Cohort / File(s) 변경 요약
JWT 및 인증 확장
src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtAuthentication.java, src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtHandler.java, src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtUserClaim.java, src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtAuthenticationFilter.java, src/main/java/com/gpt/geumpumtabackend/global/jwt/TokenProvider.java
JWT에 withdrawn(탈퇴 상태) 필드 추가 및 토큰 생성/파싱 반영. 필터에서 탈퇴 사용자 검사하여 복구 엔드포인트(/api/v1/user/restore)는 예외로 처리. TokenProvider 내부 역할 검증명·예외 처리 일부 변경 및 UserRepository 사용 추가.
보안 설정 및 OAuth2 핸들러
src/main/java/com/gpt/geumpumtabackend/global/config/security/SecurityConfig.java, src/main/java/com/gpt/geumpumtabackend/global/oauth/handler/OAuth2AuthenticationFailureHandler.java, src/main/java/com/gpt/geumpumtabackend/global/oauth/handler/OAuth2AuthenticationSuccessHandler.java
SecurityConfig에 OAuth2 실패 핸들러 연결(.failureHandler). FailureHandler에 @Component 등록. SuccessHandler가 사용자 탈퇴 여부를 판단해 JwtUserClaim에 전달하도록 변경.
OAuth2 사용자 서비스 리팩토링
src/main/java/com/gpt/geumpumtabackend/global/oauth/service/CustomOAuth2UserService.java
프로바이더 결정 로직을 resolveProvider()로 분리하고 신규 사용자 생성 로직을 createNewUser()로 추출해 정리.
API·컨트롤러: 로그아웃/탈퇴/복구
src/main/java/com/gpt/geumpumtabackend/user/api/UserApi.java, src/main/java/com/gpt/geumpumtabackend/user/controller/UserController.java
사용자 대상 엔드포인트 추가: logout(DELETE), withdraw(DELETE), restore(POST). 각 엔드포인트는 할당된 사용자 ID로 동작하고 적절한 응답 타입 반환.
User 엔티티·리포지토리: 소프트 삭제·복구
src/main/java/com/gpt/geumpumtabackend/user/domain/User.java, src/main/java/com/gpt/geumpumtabackend/user/repository/UserRepository.java, src/main/java/com/gpt/geumpumtabackend/global/base/BaseEntity.java
@SQLDelete로 소프트 삭제 쿼리 추가(필드에 deleted_ 접두사 적용). restore(...) 및 BaseEntity.restore() 추가. Repository에 탈퇴 상태 관련 조회 메서드 추가.
UserService: 비즈니스 로직 추가
src/main/java/com/gpt/geumpumtabackend/user/service/UserService.java
logout, withdrawUser, restoreUser, removeDeletedPrefix 등 추가. RefreshTokenRepository 주입으로 토큰 무효화 및 복구 시 재발급 로직 구현.
예외 타입 확장
src/main/java/com/gpt/geumpumtabackend/global/exception/ExceptionType.java
USER_WITHDRAWN 예외 유형 추가(코드 U005, FORBIDDEN).
리소스(서브모듈) 변경
src/main/resources/security/*
보안 관련 서브모듈 커밋 참조 업데이트(트래킹 커밋 변경).
sequenceDiagram
    actor Client
    participant Filter as JwtAuthenticationFilter
    participant Token as JwtHandler/TokenProvider
    participant Auth as SecurityContext
    participant API as UserController / UserApi
    participant Service as UserService
    participant Repo as UserRepository
    participant RTokenRepo as RefreshTokenRepository

    Client->>Filter: 요청 + JWT
    Filter->>Token: 토큰 파싱 -> JwtUserClaim(withdrawn)
    Token-->>Filter: JwtAuthentication(withdrawn=true)
    Filter->>Filter: isWithdrawn() 검사 && !isRestoreEndpoint()
    alt withdrawn && non-restore
        Filter-->>Client: USER_WITHDRAWN 예외 응답
    else restore endpoint or not withdrawn
        Filter->>Auth: SecurityContext에 인증 설정
        Client->>API: 컨트롤러 호출 진행
    end

    Note over Client,Service: 복구 흐름
    Client->>API: POST /api/v1/user/restore
    API->>Service: restoreUser(userId)
    Service->>Repo: 탈퇴 사용자 조회
    Repo-->>Service: User(삭제 상태, prefixed fields)
    Service->>Service: removeDeletedPrefix -> user.restore(...)
    Service->>RTokenRepo: RefreshToken 생성/저장
    Service-->>API: TokenResponse 반환
    API-->>Client: 토큰 포함 응답

    Note over Client,Service: 탈퇴 흐름
    Client->>API: DELETE /api/v1/user/withdraw
    API->>Service: withdrawUser(userId)
    Service->>RTokenRepo: 사용자 관련 RefreshToken 삭제
    Service->>Repo: soft delete 실행
    Service-->>API: 성공 응답
    API-->>Client: 성공 응답
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45분

  • 집중 검토 필요 항목:
    • JwtAuthenticationFilter의 isRestoreEndpoint 판별 로직과 예외 흐름
    • User 엔티티의 @SQLDelete 쿼리(프리픽스 적용)와 데이터 무결성/마이그레이션 영향
    • UserService의 withdraw/restore에서 토큰 무효화·재발급 흐름 및 removeDeletedPrefix 구현
    • JwtHandler/JwtUserClaim 확대(withdrawn)로 인한 모든 토큰 생성·파싱 경로 영향
    • TokenProvider의 역할 검증명 변경 및 UserRepository 주입 사용

Possibly related PRs

Suggested reviewers

  • Juhye0k
  • patulus

Poem

🐰 탈퇴한 흔적에 당근 한 토막 남기며,
토큰에 작은 깃발 꽂아 잠시 쉬어요.
문은 닫혔지만 열쇠는 품 안에,
복구하면 다시 폴짝—당근 축하해요 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.22% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 로그아웃, 회원탈퇴, 회원복구 기능 추가라는 주요 변경사항을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 필수 템플릿의 모든 섹션을 포함하고 있으며, 각 섹션이 의미 있게 작성되었습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/user

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
Contributor

@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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/gpt/geumpumtabackend/global/jwt/TokenProvider.java (1)

59-66: 주석 업데이트 필요

Line 62의 주석이 "FARMER"를 언급하고 있지만 실제 코드는 ADMIN 역할을 검증합니다. 주석을 현재 로직에 맞게 수정해 주세요.

     private void validateAdminRole(JwtUserClaim claims) {
         Long userId = claims.userId();
 
-        // 토큰의 권한은 FARMER지만 DB에 저장된 권한이 FARMER가 아닌 경우 예외 반환
+        // 토큰의 권한은 ADMIN이지만 DB에 저장된 권한이 ADMIN이 아닌 경우 예외 반환
         if (UserRole.ADMIN.equals(claims.role()) && !userService.isAdmin(userId)) {
             throw new JwtAccessDeniedException();
         }
     }
🧹 Nitpick comments (6)
src/main/java/com/gpt/geumpumtabackend/user/repository/UserRepository.java (1)

15-21: findByProviderIdAndDeletedAtIsNull 반환 타입 일관성 검토 제안

existsBy.../findByProviderAndProviderIdAndDeletedAtIsNullOptional<User>로 null 가능성을 표현하고 있는데, findByProviderIdAndDeletedAtIsNullUser를 직접 반환하고 있습니다. 항상 존재한다는 강한 보장이 있는게 아니라면, 이 메서드도 Optional<User>로 통일해 두는 편이 NPE나 실수 방지에 유리해 보입니다.

src/main/java/com/gpt/geumpumtabackend/global/config/security/SecurityConfig.java (1)

6-7: OAuth2 실패 핸들러 리다이렉트 전략 점검 권장

OAuth2AuthenticationFailureHandlerfailureHandler로 연결한 것은 적절해 보입니다. 다만 현재 실패 시 항상 "/login?error"로 리다이렉트하고 있어서, 프론트엔드가 SPA/외부 도메인 콜백(예: redirectUri 기반)으로 동작하는 경우 UX가 어색해질 수 있습니다.
추후 필요하다면 성공 핸들러와 유사하게 state/redirectUri를 활용해 실패 시에도 프론트 콜백으로 돌려보내는 구조를 검토해 보셔도 좋겠습니다.

Also applies to: 33-38, 63-64

src/main/java/com/gpt/geumpumtabackend/global/oauth/handler/OAuth2AuthenticationFailureHandler.java (1)

7-8: 실패 로그 출력 방식 개선 제안 (System.out → 로거)

@Component로 빈 등록한 변경은 SecurityConfig 연동 목적에 잘 맞습니다.
다만 onAuthenticationFailure 내부에서 System.out.printlnexception.printStackTrace()를 사용하고 있어 운영 환경에서는 로그 레벨/포맷 제어가 어렵습니다.

추후 다음과 같이 Slf4j 등의 로거를 사용하도록 정리하는 것을 권장드립니다.

@Slf4j
@Component
public class OAuth2AuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {
        log.warn("❌ OAuth2 로그인 실패: {}", exception.getMessage(), exception);
        response.sendRedirect("/login?error");
    }
}

Also applies to: 11-22

src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtAuthenticationFilter.java (1)

5-6: 탈퇴 계정의 API 접근 차단 로직은 명확하나, 복구 엔드포인트 매칭 방식은 조금 더 유연하게 하는 것이 좋습니다

JwtAuthentication인 경우에만 jwtAuth.isWithdrawn()을 검사하고, /api/v1/user/restore(POST)에 대해서만 예외를 두는 구조는 PR 설명과 정확히 일치합니다.

다만:

return method.equalsIgnoreCase("POST")
        && uri.equals("/api/v1/user/restore");
  • 앱에 contextPath가 붙거나, REST URL 버전이 변경되거나, trailing slash 차이(/api/v1/user/restore/)가 생기면 매칭이 쉽게 깨질 수 있습니다.

가능하다면

  • 컨트롤러에서 사용하는 URL을 상수로 분리해 공유하거나,
  • AntPathRequestMatcher("/api/v1/user/restore", "POST") 같은 RequestMatcher를 활용하는 쪽이 유지보수에 더 안전해 보입니다.

정책 자체는 좋은데, URI 문자열 하드코딩 부분만 한 번 더 점검해 보시면 좋겠습니다.

Also applies to: 57-63, 89-95

src/main/java/com/gpt/geumpumtabackend/user/service/UserService.java (1)

126-131: 메서드 가시성을 private으로 변경 권장

removeDeletedPrefix는 클래스 내부에서만 사용되므로 private으로 선언하는 것이 캡슐화에 더 적합합니다.

-    public String removeDeletedPrefix(String value) {
+    private String removeDeletedPrefix(String value) {
src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtUserClaim.java (1)

7-16: withdrawn 필드는 Boolean 보다는 원시형 boolean 으로 고정하는 편이 안전해 보입니다.

도메인 상 “탈퇴 여부”는 null 이 의미가 없는 값이라 항상 true/false 둘 중 하나여야 할 것 같습니다. 그런데 현재 시그니처는 Boolean 을 사용하고 있어, create(Long userId, UserRole role, Boolean withdrawn) 호출 시 null 이 들어가면 이후 언박싱 과정에서 NPE 가 발생할 수 있습니다(예: JwtAuthentication 생성 시 boolean 으로 다루는 경우).

선호하시는 방향 두 가지 중 하나를 추천드립니다.

  1. null 을 허용하지 않는 경우(권장)
    레코드와 팩토리 메서드 시그니처를 모두 boolean withdrawn 으로 변경해 null 을 모델링하지 않도록 강제합니다.

  • public record JwtUserClaim(
  •    Long userId,
    
  •    UserRole role,
    
  •    Boolean withdrawn
    
  • ) {
  • public record JwtUserClaim(
  •    Long userId,
    
  •    UserRole role,
    
  •    boolean withdrawn
    
  • ) {
    @@
  • public static JwtUserClaim create(Long userId, UserRole role, Boolean withdrawn) {
  • public static JwtUserClaim create(Long userId, UserRole role, boolean withdrawn) {
    return new JwtUserClaim(userId, role, withdrawn);
    }

2. **이전 토큰 호환 등으로 null 을 일부 허용해야 하는 경우**  
타입은 `Boolean` 을 유지하되, `JwtAuthentication` / 필터에서 `null` 을 명시적으로 처리해 주세요(예: `Boolean.TRUE.equals(claim.withdrawn())` 형태로만 비교).

둘 중 어떤 전략을 쓸지 결정하신 뒤, 해당 의미에 맞춰 전체 사용처에서 일관되게 처리되는지 한 번 더 확인해 주시면 좋겠습니다.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: CodeRabbit UI

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 781595a33836d0d3564b0744c7278e95b60371bb and 00ccf1814745bfd4489306c231db212100553d23.

</details>

<details>
<summary>📒 Files selected for processing (15)</summary>

* `src/main/java/com/gpt/geumpumtabackend/global/config/security/SecurityConfig.java` (3 hunks)
* `src/main/java/com/gpt/geumpumtabackend/global/exception/ExceptionType.java` (1 hunks)
* `src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtAuthentication.java` (2 hunks)
* `src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtAuthenticationFilter.java` (3 hunks)
* `src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtHandler.java` (3 hunks)
* `src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtUserClaim.java` (1 hunks)
* `src/main/java/com/gpt/geumpumtabackend/global/jwt/TokenProvider.java` (4 hunks)
* `src/main/java/com/gpt/geumpumtabackend/global/oauth/handler/OAuth2AuthenticationFailureHandler.java` (1 hunks)
* `src/main/java/com/gpt/geumpumtabackend/global/oauth/handler/OAuth2AuthenticationSuccessHandler.java` (1 hunks)
* `src/main/java/com/gpt/geumpumtabackend/global/oauth/service/CustomOAuth2UserService.java` (1 hunks)
* `src/main/java/com/gpt/geumpumtabackend/user/api/UserApi.java` (2 hunks)
* `src/main/java/com/gpt/geumpumtabackend/user/controller/UserController.java` (1 hunks)
* `src/main/java/com/gpt/geumpumtabackend/user/domain/User.java` (2 hunks)
* `src/main/java/com/gpt/geumpumtabackend/user/repository/UserRepository.java` (2 hunks)
* `src/main/java/com/gpt/geumpumtabackend/user/service/UserService.java` (5 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>📚 Learning: 2025-11-10T08:36:54.339Z</summary>

Learnt from: kon28289
Repo: Geumpumta/backend PR: 14
File: src/main/java/com/gpt/geumpumtabackend/user/api/UserApi.java:54-73
Timestamp: 2025-11-10T08:36:54.339Z
Learning: In the Geumpumta backend project (Java/Spring Boot), a hierarchical role structure is configured where ADMIN > USER. This means ADMIN users automatically inherit USER role permissions through Spring Security's RoleHierarchy, so endpoints with PreAuthorize("hasRole('USER')") are accessible by both USER and ADMIN users.


**Applied to files:**
- `src/main/java/com/gpt/geumpumtabackend/global/jwt/TokenProvider.java`

</details>

</details><details>
<summary>🧬 Code graph analysis (3)</summary>

<details>
<summary>src/main/java/com/gpt/geumpumtabackend/user/controller/UserController.java (1)</summary><blockquote>

<details>
<summary>src/main/java/com/gpt/geumpumtabackend/global/response/ResponseUtil.java (1)</summary>

* `ResponseUtil` (5-28)

</details>

</blockquote></details>
<details>
<summary>src/main/java/com/gpt/geumpumtabackend/global/oauth/handler/OAuth2AuthenticationFailureHandler.java (1)</summary><blockquote>

<details>
<summary>src/main/java/com/gpt/geumpumtabackend/global/oauth/handler/OAuth2AuthenticationSuccessHandler.java (1)</summary>

* `Component` (24-59)

</details>

</blockquote></details>
<details>
<summary>src/main/java/com/gpt/geumpumtabackend/global/jwt/TokenProvider.java (2)</summary><blockquote>

<details>
<summary>src/main/java/com/gpt/geumpumtabackend/global/jwt/exception/JwtAccessDeniedException.java (1)</summary>

* `JwtAccessDeniedException` (6-10)

</details>
<details>
<summary>src/main/java/com/gpt/geumpumtabackend/global/jwt/exception/JwtTokenInvalidException.java (1)</summary>

* `JwtTokenInvalidException` (6-14)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)</summary>

* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>src/main/java/com/gpt/geumpumtabackend/global/exception/ExceptionType.java (1)</summary><blockquote>

`35-36`: **`USER_WITHDRAWN` 예외 타입 추가 방향 적절**

탈퇴 사용자 상태를 구분하는 `USER_WITHDRAWN(FORBIDDEN, "U005", "탈퇴한 사용자입니다.")` 추가는 JWT 필터/인증 흐름과도 잘 맞고, 코드/메시지/상태값 구성도 일관적입니다.

</blockquote></details>
<details>
<summary>src/main/java/com/gpt/geumpumtabackend/global/oauth/handler/OAuth2AuthenticationSuccessHandler.java (1)</summary><blockquote>

`42-48`: **탈퇴 상태 플래그 전달 방식 적절**

`principal.getUser().getDeletedAt() != null`로 탈퇴 여부를 계산해 `JwtUserClaim`에 함께 넣는 구조는 이후 `JwtAuthenticationFilter`에서 `/api/v1/user/restore`만 허용하는 정책과 잘 맞습니다.  
토큰 재발급/수명 전략(예: 탈퇴 계정용 액세스 토큰 TTL을 더 짧게 가져갈지 등)은 운영 정책에 따라 조정하시면 될 것 같고, 현재 구현 자체에는 큰 문제 없어 보입니다.

</blockquote></details>
<details>
<summary>src/main/java/com/gpt/geumpumtabackend/global/oauth/service/CustomOAuth2UserService.java (2)</summary><blockquote>

`108-128`: **LGTM!**

`resolveProvider`의 switch 표현식과 `createNewUser`의 빌더 패턴 사용이 깔끔하고 가독성이 좋습니다. 기존 인라인 로직을 별도 메서드로 분리하여 코드 구조가 개선되었습니다.

---

`100-106`: **No issue found - withdrawn user handling is correctly implemented**

The code properly handles withdrawn users as intended. `findByProviderAndProviderId` correctly returns all users (including withdrawn ones), and `OAuth2AuthenticationSuccessHandler.onAuthenticationSuccess` (line 45) explicitly checks `deletedAt` and flags withdrawn users in the JWT claim. The recovery token is issued with `isWithdrawn=true` in the `JwtUserClaim`, which aligns with the PR objective.

</blockquote></details>
<details>
<summary>src/main/java/com/gpt/geumpumtabackend/user/controller/UserController.java (1)</summary><blockquote>

`79-107`: **LGTM!**

새로운 엔드포인트들이 기존 패턴을 일관되게 따르고 있으며, `UserService`로의 위임과 응답 처리가 적절합니다.

</blockquote></details>
<details>
<summary>src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtAuthentication.java (1)</summary><blockquote>

`11-23`: **LGTM!**

`withdrawn` 필드 추가와 `JwtUserClaim`으로부터의 생성자 매핑이 적절하게 구현되었습니다. 기존 `Authentication` 인터페이스와의 통합도 잘 유지되고 있습니다.

</blockquote></details>
<details>
<summary>src/main/java/com/gpt/geumpumtabackend/user/service/UserService.java (1)</summary><blockquote>

`110-124`: No action needed. The User entity does not have a `@Where` annotation, so `findById()` will successfully return soft-deleted users. The soft-delete implementation uses only `@SQLDelete` to mark deletions, and does not filter out deleted records at the ORM level. The `restoreUser()` method works as intended.


> Likely an incorrect or invalid review comment.

</blockquote></details>
<details>
<summary>src/main/java/com/gpt/geumpumtabackend/user/api/UserApi.java (1)</summary><blockquote>

`168-173`: The `/restore` endpoint security is properly implemented. `JwtAuthenticationFilter` explicitly handles withdrawn users by checking `if (jwtAuth.isWithdrawn() && !isRestoreEndpoint(request))` at lines 59-61. This allows withdrawn users to access only the `/restore` endpoint while blocking them from other endpoints. The `@PreAuthorize("hasRole('USER')")` constraint works correctly since withdrawn users retain their USER role. No additional exception handling is needed—the existing filter logic already enforces the intended behavior from the PR objectives.



> Likely an incorrect or invalid review comment.

</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Copy link
Copy Markdown
Contributor

@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: 0

🧹 Nitpick comments (1)
src/main/java/com/gpt/geumpumtabackend/global/jwt/TokenProvider.java (1)

42-43: ADMIN 권한 검증 로직과 주석을 일치시키는 것이 좋겠습니다.

validateAdminRole 에서는 ADMIN 권한 토큰에 대해서만 userService.isAdmin 을 확인하는데, 주석에는 여전히 FARMER 기준으로 적혀 있어 의미가 조금 혼동될 수 있습니다. 주석을 ADMIN 기준으로 수정해 두면 이후 유지보수 시 의도가 더 명확할 것 같습니다.

Also applies to: 58-65

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0dc4407 and c637ad3.

📒 Files selected for processing (6)
  • src/main/java/com/gpt/geumpumtabackend/global/base/BaseEntity.java (1 hunks)
  • src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtHandler.java (3 hunks)
  • src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtUserClaim.java (1 hunks)
  • src/main/java/com/gpt/geumpumtabackend/global/jwt/TokenProvider.java (3 hunks)
  • src/main/java/com/gpt/geumpumtabackend/user/api/UserApi.java (2 hunks)
  • src/main/java/com/gpt/geumpumtabackend/user/domain/User.java (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/gpt/geumpumtabackend/user/api/UserApi.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-10T08:36:54.339Z
Learnt from: kon28289
Repo: Geumpumta/backend PR: 14
File: src/main/java/com/gpt/geumpumtabackend/user/api/UserApi.java:54-73
Timestamp: 2025-11-10T08:36:54.339Z
Learning: In the Geumpumta backend project (Java/Spring Boot), a hierarchical role structure is configured where ADMIN > USER. This means ADMIN users automatically inherit USER role permissions through Spring Security's RoleHierarchy, so endpoints with PreAuthorize("hasRole('USER')") are accessible by both USER and ADMIN users.

Applied to files:

  • src/main/java/com/gpt/geumpumtabackend/global/jwt/TokenProvider.java
🧬 Code graph analysis (1)
src/main/java/com/gpt/geumpumtabackend/user/domain/User.java (1)
src/main/java/com/gpt/geumpumtabackend/global/base/BaseEntity.java (1)
  • Getter (16-36)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (4)
src/main/java/com/gpt/geumpumtabackend/global/base/BaseEntity.java (1)

33-35: 소프트 삭제 복구용 restore() 구현이 요구사항과 잘 맞습니다.

deletedAt 만 null 로 명확히 초기화해 상속 엔티티들이 공통으로 복구 동작을 사용할 수 있어 보입니다. User.restore()와의 연계도 자연스럽습니다.

src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtHandler.java (1)

30-31: WITHDRAWN 클레임 처리 방식이 구 토큰까지 고려해 잘 정리되었습니다.

  • createClaims 에서 IS_WITHDRAWN 클레임을 추가해 새 토큰에 탈퇴 여부가 포함되고,
  • convert 에서 claims.get(IS_WITHDRAWN, Boolean.class) 결과를 withdrawn != null ? withdrawn : false 로 기본값 처리해 예전 토큰에서도 NPE 없이 false 로 동작하도록 되어 있습니다.

토큰 스키마 변경 시 필요한 backward compatibility 를 잘 충족하고 있어 보입니다.

Also applies to: 65-70, 91-97

src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtUserClaim.java (1)

7-16: withdrawn 플래그 생성 로직이 삭제 여부와 일관되게 수정되었습니다.

create(User user) 에서 user.getDeletedAt() != null 로 withdrawn 을 세팅하고, 별도 팩토리 메서드로 수동 생성도 명시적으로 처리해 JWT 쪽 isWithdrawn() 의미와 잘 맞습니다.

src/main/java/com/gpt/geumpumtabackend/user/domain/User.java (1)

11-24: User 소프트 삭제/복구 흐름이 deletedAt·고유값 정책과 잘 맞습니다.

  • @SQLDelete 로 삭제 시 deleted_at 타임스탬프를 찍고, 이메일/학교이메일/닉네임/학번에 "deleted_" prefix 를 부여해 이후 동일 값으로 다른 계정 가입이 가능해집니다.
  • restore(...) 에서 전달받은 값으로 필드를 되살린 뒤 super.restore() 를 호출하여 deletedAt 을 null 로 초기화하므로, JWT 측에서 다시 활성 사용자로 인식할 수 있습니다.

탈퇴 → 재로그인 → 복구까지의 도메인 정책을 엔티티 레벨에서 잘 반영한 구현으로 보입니다.

Also applies to: 90-96

Copy link
Copy Markdown
Contributor

@Juhye0k Juhye0k left a comment

Choose a reason for hiding this comment

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

LGTM!

@kon28289 kon28289 merged commit bddd18a into dev Dec 2, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants