Skip to content

Feat/sw 65 - 세션 기반 인증에서 JWT 기반 인증 전환 #38

Merged
jin2304 merged 22 commits intodevfrom
feat/SW-65
Apr 4, 2026
Merged

Feat/sw 65 - 세션 기반 인증에서 JWT 기반 인증 전환 #38
jin2304 merged 22 commits intodevfrom
feat/SW-65

Conversation

@jin2304
Copy link
Copy Markdown
Member

@jin2304 jin2304 commented Mar 29, 2026

💡 이슈

resolve {#37}

🤩 개요

JWT/OAuth2 인증 체계 도입 및 프로젝트 구조 최적화 (Phase 1 완료)

  • 인증 고도화: Spring Security 기반 JWT 및 Google 소셜 로그인 연동 완료
  • 보안 강화: 기존 소유권 확인 로직(OwnerCheckAspect)의 인증 체계 통합 및 보안 유틸리티 고도화
  • 구조 최적화: 사용하지 않는 레거시 도메인 정리 및 서비스 파라미터 표준화

🧑‍💻 작업 사항

  • 인증 아키텍처 구축: Spring Security 기반 JWT(Access/Refresh Token) 발급 및 Google OAuth2 연동
  • 프론트엔드 인증 연동: AuthProvider, authStore 도입 및 fetchClient를 통한 자동 토큰 갱신 로직 구현
  • 도메인 정리 및 최적화: 사용하지 않는 레거시 도메인(comment, likes, main) 삭제 및 관련 코드 백업 처리
  • 소유권 확인 로직 개선: OwnerCheckAspect 내 사용자 식별 로직을 SecurityUtils 기반으로 리팩토링하여 인증 체계 통합
  • 서비스 파라미터 표준화: folder, bookmark 등 주요 서비스의 파라미터명을 memberId로 통일하여 도메인 일관성 확보
  • 문서화: JWT 아키텍처 가이드 및 E2E 테스트 시나리오 문서 추가

📖 참고 사항

  • 시나리오 테스트 완료: 03. jwt-oauth2-test-scenarios.md의 **Part A(S1~S5 시나리오 기반 E2E 테스트)**만 우선적으로 수행하여 핵심 플로우(로그인/자동갱신/로그아웃)의 정상 작동을 확인했습니다. (Part B 상세 컴포넌트 테스트는 추후 진행 예정)
  • Phase 로드맵: 본 작업은 전체 인증 개편 로드맵 중 **Phase 1(로컬 개발 안정화)**에 해당하며, 향후 Nginx 도입(Phase 2) 및 단일 Origin 완성(Phase 3)이 예정되어 있습니다.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • OAuth2 기반 소셜 로그인 (구글 등) 추가
    • JWT 토큰 기반 인증 시스템 도입
    • 자동 토큰 새로고침 기능 추가
    • 로그아웃 기능 추가
    • 사용자 프로필 표시 개선
  • 문서

    • JWT/OAuth2 아키텍처 가이드 추가
    • 인증 테스트 시나리오 문서 추가
  • 제거된 기능

    • 게시판, 댓글, 좋아요 기능 제거
    • 기존 로그인 방식 폐지

@jin2304 jin2304 self-assigned this Mar 29, 2026
@jin2304 jin2304 added ✨ feat 새로운 기능을 추가 ♻️ refactor 코드 리팩토링 labels Mar 29, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 29, 2026

Walkthrough

이 PR은 세션 기반 인증을 JWT + OAuth2 기반의 상태 비저장(stateless) 인증으로 전환합니다. 백엔드에 JWT 유틸리티, 인증 필터/엔트리포인트, refresh-token 회전 및 DB 저장/락 로직, OAuth2 성공/실패 핸들러, 인증 컨트롤러(/api/auth/refresh, /api/auth/logout, /api/auth/member)와 관련 DAO/도메인/매퍼를 추가합니다. 프론트엔드에는 Zustand 인증 스토어, fetch 클라이언트의 자동 토큰 주입·리프레시·재시도 로직, OAuth2 콜백 페이지, AuthProvider 및 관련 UI 변경이 포함됩니다. 여러 레거시 MVC/DAO/서비스(게시글·댓글·메인·템플릿 등)가 비활성화 또는 제거되었습니다.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser
    participant Frontend as Frontend (Next.js)
    participant FetchClient as Fetch Client (with JWT)
    participant Backend as Backend (Spring)
    participant OAuth2 as OAuth2 Provider
    participant Database as Database

    rect rgba(100, 150, 200, 0.5)
    Note over Browser,OAuth2: OAuth2 로그인 및 리프레시 발급
    Browser->>Frontend: "Login with Google" 클릭
    Frontend->>Backend: GET /oauth2/authorization/google
    Backend->>OAuth2: 인증 요청 (redirect)
    OAuth2->>Browser: OAuth2 로그인 페이지
    Browser->>OAuth2: 사용자 인증
    OAuth2->>Backend: 콜백(authorization code)
    Backend->>Backend: CustomOAuth2MemberService.loadUser()
    Backend->>Database: Member 조회/생성
    Backend->>Backend: issueRefreshToken(memberId)
    Backend->>Database: refresh_token 삽입
    Backend->>Browser: Set-Cookie (HttpOnly refreshToken) + redirect /auth/callback
    Browser->>Frontend: /auth/callback 로드
    end

    rect rgba(150, 200, 100, 0.5)
    Note over Frontend,Backend: 앱 초기화 및 토큰 복구
    Frontend->>Frontend: AuthProvider.initialize()
    Frontend->>FetchClient: POST /api/auth/refresh (쿠키 포함)
    FetchClient->>Backend: /api/auth/refresh
    Backend->>Database: findByTokenHashForUpdate(해시)
    Database-->>Backend: 토큰 레코드
    Backend->>Backend: 회전/유예기간 검사 및 새 토큰 생성
    Backend->>Database: markRotated / insert successor
    Backend->>Browser: Set-Cookie (새 refreshToken)
    Backend-->>FetchClient: { accessToken }
    FetchClient->>Frontend: useAuthStore.setAccessToken(...)
    Frontend->>FetchClient: GET /api/auth/member (Bearer)
    Backend->>Database: member 조회
    Backend-->>Frontend: member info
    end

    rect rgba(200, 150, 100, 0.5)
    Note over Frontend,Backend: 보호된 API 요청 + 자동 리프레시
    Browser->>Frontend: 사용자가 API 호출
    Frontend->>FetchClient: 요청 (Authorization 포함)
    FetchClient->>Backend: 요청 전송
    alt 응답 401 (토큰 만료)
        Backend-->>FetchClient: 401
        FetchClient->>FetchClient: refreshAccessToken() (singleton promise)
        FetchClient->>Backend: POST /api/auth/refresh
        Backend->>Database: 토큰 검증/회전
        Backend-->>FetchClient: { accessToken }
        FetchClient->>Frontend: store new accessToken
        FetchClient->>Backend: 원래 요청 재시도 (새 토큰)
        Backend-->>FetchClient: 200 + 데이터
    else 정상
        Backend-->>FetchClient: 200 + 데이터
    end
    FetchClient-->>Frontend: 응답 전달
    end

    rect rgba(200, 100, 100, 0.5)
    Note over Browser,Backend: 로그아웃
    Browser->>Frontend: 로그아웃 클릭
    Frontend->>FetchClient: POST /api/auth/logout
    FetchClient->>Backend: /api/auth/logout (쿠키 포함)
    Backend->>Database: deleteByTokenHash(해시)
    Backend->>Browser: Set-Cookie (refreshToken maxAge=0)
    FetchClient->>Frontend: useAuthStore.logout() -> 상태 초기화
    Frontend->>Browser: 라우트 /login
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • ekdh0858

Poem

🐰 토끼가 말하길,
새 토큰이 달빛처럼 반짝여요.
쿠키는 숨겨진 보물, 키는 비밀열쇠,
회전은 춤추듯 안전을 지키고,
프론트와 백은 함께 노래하네 ✨

✨ 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 feat/SW-65

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2cb1273234

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// 💡 [E2E 테스트용 임시 로그] 터미널 콘솔에서 복사해서 .http 파일에 붙여넣으세요.
System.out.println("\n" + "=".repeat(80));
System.out.println("[E2E TEST] Generated Refresh Token:");
System.out.println(refreshToken);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Stop logging raw refresh tokens

Printing the full refresh token to stdout on every OAuth2 login leaks a bearer credential to application logs, which enables session hijacking for anyone with log access (local logs, centralized log pipelines, support dumps). This should be removed or heavily redacted before shipping.

Useful? React with 👍 / 👎.

Comment on lines 28 to 33
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
implementation 'org.springframework.boot:spring-boot-starter-validation'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep a template engine for remaining MVC view endpoints

This dependency block removes Thymeleaf, but the app still has MVC controllers that return view names (for example MyPageController#myPage returns "mypage/myPage" and templates are still present under src/main/resources/templates). Without a view engine, those routes will fail at runtime (typically 404/500 on view resolution), so either retain Thymeleaf or migrate those controllers to REST responses.

Useful? React with 👍 / 👎.

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

Caution

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

⚠️ Outside diff range comments (8)
frontend/next.config.ts (1)

4-14: 🧹 Nitpick | 🔵 Trivial

프론트엔드의 auth API 호출이 모두 buildBackendUrl()으로 절대 경로화되어 있으므로 현재는 문제없습니다.

검증 결과, 상대 경로 /api/auth/* 호출은 0건이며, 모든 인증 엔드포인트가 buildBackendUrl()을 통해 http://localhost:8080/api/auth/... 형태의 절대 URL로 변환되어 Next.js rewrite를 우회하고 있습니다. 일반 API(/api/folders, /api/tags 등)는 fetchClient()로 감싸져 상대 경로로 호출되어 rewrite를 거치는 패턴이 일관되게 유지되고 있습니다.

다만 이 아키텍처가 개발 관례(convention) 에만 의존하고 있으므로, 향후 실수를 방지하기 위해 rewrite 규칙에서 /api/auth/*를 명시적으로 제외하면 더욱 견고할 것입니다.

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

In `@frontend/next.config.ts` around lines 4 - 14, Add an explicit exclusion for
auth endpoints in the rewrites() function: before the generic { source:
'/api/:path*', destination: 'http://localhost:8080/api/:path*' } rule, insert a
specific rule for '/api/auth/:path*' that leaves the request unrewritten (e.g. {
source: '/api/auth/:path*', destination: '/api/auth/:path*' }) so auth calls are
not captured by the generic rewrite; update the rewrites() return ordering so
the auth rule appears before the generic '/api/:path*' entry.
src/main/java/com/web/SearchWeb/member/dto/CustomOAuth2Member.java (1)

49-52: 🧹 Nitpick | 🔵 Trivial

getAttributes()가 빈 Map을 반환함

OAuth2User.getAttributes()가 빈 맵을 반환하면 OAuth2 속성에 접근하려는 다른 코드에서 문제가 발생할 수 있습니다. 의도적인 설계라면 괜찮지만, 다운스트림 코드에서 attributes 접근 시 NullPointerException이나 예상치 못한 동작이 없는지 확인해 주세요.

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

In `@src/main/java/com/web/SearchWeb/member/dto/CustomOAuth2Member.java` around
lines 49 - 52, CustomOAuth2Member currently returns an empty Map from
getAttributes(), which breaks downstream code expecting the OAuth2 attributes;
update the class to store the incoming attributes in a final Map field (e.g.
attributes) during construction (or accept them in builder/factory) and have
getAttributes() return an immutable/defensive copy (Map.copyOf(attributes) or
Collections.unmodifiableMap(attributes)) instead of Map.of(); locate and modify
the CustomOAuth2Member constructor/field and the getAttributes() method to
return that stored map and ensure it never returns null.
frontend/src/app/login/page.tsx (1)

164-194: ⚠️ Potential issue | 🟡 Minor

이메일/비밀번호 폼이 미구현 상태임

현재 폼의 onSubmite.preventDefault()만 수행하여 실제 로그인 기능이 없습니다. 사용자가 입력 후 버튼을 클릭해도 아무 동작이 없어 혼란을 줄 수 있습니다. 구현 예정이 아니라면 UI를 비활성화하거나 숨기는 것을 고려해 주세요.

,
이 폼의 로그인 기능 구현이 필요하시면 도움을 드릴 수 있습니다. 새 이슈를 생성할까요?

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

In `@frontend/src/app/login/page.tsx` around lines 164 - 194, The form currently
only calls e.preventDefault() in its onSubmit and has no login logic; replace
the noop with a real submit handler (e.g., handleLogin) that reads the
email/password values from the Input components (use controlled state or refs
for Input with ids "email" and "password"), validates them, calls your
authentication API/service, handles loading and error states, and redirects on
success; if you don't want to implement auth now, disable the submit button (or
hide the form) and update the button/NextLink UI accordingly so users aren't
able to click a non-functional "Log in" button.
frontend/src/components/dialogs/CreateFolderDialog.tsx (1)

184-191: ⚠️ Potential issue | 🟡 Minor

버튼 비활성화 조건에 memberId 체크를 추가하세요.

Line 42에서 memberId가 없으면 함수가 조기 반환되지만, 버튼의 disabled 속성에는 memberId 체크가 없습니다. 인증되지 않은 사용자에게는 버튼이 비활성화되어야 더 나은 UX를 제공합니다.

🔧 수정 제안
 <button
   type="button"
   onClick={handleCreateFolder}
-  disabled={!folderName.trim() || createFolderMutation.isPending}
+  disabled={!folderName.trim() || !memberId || createFolderMutation.isPending}
   className="px-5 py-2.5 rounded-xl bg-gradient-to-r from-purple-600 to-indigo-600 text-white text-sm font-semibold shadow-lg shadow-purple-500/30 hover:shadow-purple-500/50 hover:scale-[1.02] transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/dialogs/CreateFolderDialog.tsx` around lines 184 -
191, The Create Folder button's disabled logic should also check for
authentication: update the disabled prop to include a check for the presence of
memberId (alongside !folderName.trim() and createFolderMutation.isPending) so
unauthenticated users cannot click it; locate the JSX button that calls
handleCreateFolder and references folderName and createFolderMutation.isPending
and add a guard for memberId (the same memberId checked earlier where
handleCreateFolder returns early).
frontend/src/components/my-links/RightPanel.tsx (1)

101-101: ⚠️ Potential issue | 🟡 Minor

useEffect 의존성 배열에 핸들러 함수가 누락되어 있습니다.

handleNoteEditComplete, handleTitleEditComplete, handleTagEditComplete 함수들이 useEffect 내부에서 호출되지만 의존성 배열에 포함되어 있지 않습니다. 이 함수들이 상태에 의존하므로 stale closure 문제가 발생할 수 있습니다.

React의 exhaustive-deps 규칙을 따르거나, useCallback으로 함수를 메모이제이션하세요.

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

In `@frontend/src/components/my-links/RightPanel.tsx` at line 101, The effect in
RightPanel.tsx currently calls handleNoteEditComplete, handleTitleEditComplete,
and handleTagEditComplete but they are missing from the dependency array,
risking stale closures; fix by either memoizing those handlers with useCallback
(wrap handleNoteEditComplete, handleTitleEditComplete, handleTagEditComplete in
useCallback with their internal dependencies) or add them to the useEffect
dependency array so React can track changes, then run linting to ensure
exhaustive-deps is satisfied.
src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java (1)

39-53: 🧹 Nitpick | 🔵 Trivial

주석 처리된 레거시 코드 제거를 권장합니다.

주석 처리된 코드는 버전 관리 시스템(Git)에서 히스토리로 추적 가능하므로, 코드베이스에 남겨둘 필요가 없습니다. 주석 코드가 남아있으면 코드 가독성이 떨어지고 유지보수 시 혼란을 줄 수 있습니다.

♻️ 레거시 코드 완전 제거 제안
     // URL로부터 페이지 제목 추출
     String extractTitle(String url);
-    
-    // ========== Legacy Board-Bookmark Methods (Commented Out) ==========
-    
-    /*
-    //게시글 북마크 확인
-    int checkBoardBookmark(BoardBookmarkCheckDto checkDto);
-    
-    //게시글 북마크 여부 확인 (for boardDetail)
-    int isBookmarked(Long boardId, Long memberId);
-    
-    //게시글 북마크 추가
-    int insertBookmarkForBoard(Long boardId, BookmarkDto bookmarkDto);
-    
-    //게시글 북마크 삭제
-    int deleteBookmarkBoard(BoardBookmarkCheckDto checkDto);
-    */
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java` around
lines 39 - 53, Remove the commented-out "Legacy Board-Bookmark Methods" block in
BookmarkService.java: delete the entire multi-line comment that contains the
placeholder method signatures (checkBoardBookmark, isBookmarked,
insertBookmarkForBoard, deleteBookmarkBoard) and the surrounding "Legacy
Board-Bookmark Methods (Commented Out)" header so the interface/class no longer
contains dead commented code; rely on VCS history for retrieval if needed.
src/main/java/com/web/SearchWeb/member/service/MemberServiceImpl.java (1)

61-77: 🧹 Nitpick | 🔵 Trivial

주석 처리된 로직 블록 제거 필요

updateMember 내의 주석 처리된 코드 블록도 동일하게 정리가 필요합니다. 이 로직이 향후 복원 예정이라면 TODO 주석이나 이슈 트래커 참조를 남겨두는 것이 좋습니다.

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

In `@src/main/java/com/web/SearchWeb/member/service/MemberServiceImpl.java` around
lines 61 - 77, Remove the commented-out block inside updateMember that updates
Comment user profiles via commentDao and UpdateUserProfileCommentDto (the code
referencing result, memberId, memberUpdateDto, Comment,
commentDao.updateCommentUserProfile); either delete the entire commented section
or replace it with a single TODO comment referencing the issue/PR number if this
behavior will be restored later, so the method no longer contains stale
commented logic.
src/main/java/com/web/SearchWeb/folder/service/MemberFolderServiceImpl.java (1)

22-65: ⚠️ Potential issue | 🟡 Minor

DB 제약이 존재하지만 경쟁 조건 발생 시 예외 처리가 없어 500 에러가 반환됩니다.

uq_member_folder_parent_name 고유 인덱스(owner_member_id, parent_folder_id, folder_name)가 루트 폴더 케이스(parentFolderId IS NULL)까지 포함하여 완벽하게 구성되어 있으므로, 동시 요청 시 중복 폴더 생성 자체는 차단됩니다.

하지만 create(), update(), move() 메서드가 애플리케이션 레벨의 exists 체크만 의존하고 DB 제약 위반으로 인한 DataIntegrityViolationException을 처리하지 않으므로, 경쟁 조건 발생 시 unhandled 예외로 500 에러가 반환됩니다. BookmarkServiceImpl처럼 이 예외를 catch하여 DUPLICATE_FOLDER_NAME으로 매핑해야 합니다.

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

In `@src/main/java/com/web/SearchWeb/folder/service/MemberFolderServiceImpl.java`
around lines 22 - 65, MemberFolderServiceImpl currently relies only on exists
checks and doesn't handle DB unique constraint failures
(uq_member_folder_parent_name), so wrap the persistence calls in create(), and
similarly in update() and move(), with a try-catch that catches
DataIntegrityViolationException (or the Spring root cause) and throws new
FolderException(FolderErrorCode.DUPLICATE_FOLDER_NAME); specifically, around
memberFolderJpaRepository.save(...) in create() (and the corresponding
save/update calls in update()/move()), detect violations caused by the unique
index (including parentFolderId IS NULL root-case) and map them to the
DUPLICATE_FOLDER_NAME FolderException like BookmarkServiceImpl does.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.gitignore:
- Around line 54-58: 현재 .gitignore에 있는 `status.txt`와 `status_utf8.txt` 같은 파일명만
적은 패턴은 하위 디렉터리의 동일 이름 파일까지 모두 무시하므로, 루트 산출물만 무시하려면 파일명 패턴을 루트 기준으로 고정하세요; 즉
`.gitignore`에서 `status.txt`, `status_utf8.txt` 항목을 루트 앵커링(루트 경로 기준) 형태로 변경하고
필요하면 `.gradle-user/`와 `docs/Backend/memo/`도 루트 기준인지 확인해 일관되게 수정해서 의도치 않은 하위 디렉터리
파일 무시를 방지하세요.

In `@docs/Backend/03`. jwt-oauth2-test-scenarios.md:
- Around line 28-37: 문서의 A004 설명이 구현과 불일치하므로 A004 행을 수정해 refresh()의 첫 단계에서 처리되는
만료(A002)를 제외하고, A004를 "Refresh Token 쿠키 부재 / DB 미존재 / 재사용 초과"로 좁혀 작성하세요; 참조되는 구현
심볼은 refresh(), AuthController, AuthServiceImpl이며 JwtAuthenticationFilter의
ExpiredJwtException 처리(A002)는 별도로 유지합니다.

In `@frontend/src/components/layout/Sidebar.tsx`:
- Around line 50-55: The onClick handler currently awaits logout() then calls
router.push('/login'), but if logout() throws the redirect is skipped; change
the handler for the button so you call logout() inside a try/catch (or just try)
and then call router.push('/login') inside a finally block to guarantee
navigation; reference the existing onClick arrow function, the logout() call and
router.push('/login') so you can wrap logout() in try/finally and optionally log
or handle the error in catch before re-routing in finally.

In `@frontend/src/components/providers/AuthProvider.tsx`:
- Around line 29-41: The protected content can flash before navigation
completes; update AuthProvider to track a redirecting state: inside the
useEffect that currently calls router.replace('/login') set a local state (e.g.,
isRedirecting) to true before calling router.replace and clear it after the
navigation promise resolves/rejects, and update the component render to withhold
rendering children while isInitializing || isRedirecting || (!isAuthenticated &&
!isPublicPath); modify the effect referencing
isInitializing/isAuthenticated/authError/router to set
sessionStorage('authError') then set isRedirecting and await
router.replace('/login') (or attach .then/.catch) so children are not rendered
during redirect.
- Around line 31-33: The current isPublicPath computation using PUBLIC_PATHS and
pathname with startsWith misclassifies routes like "/login-other"; update the
matching in the isPublicPath logic (the variable/function name isPublicPath and
the PUBLIC_PATHS array, using pathname) to only treat a path as public when it
exactly equals the candidate or when it is a proper prefix followed by a path
separator (e.g., pathname === path || pathname.startsWith(path + '/'));
alternatively, replace startsWith logic with regex matching built from
PUBLIC_PATHS if you need more flexible patterns.

In `@frontend/src/lib/api/fetchClient.ts`:
- Around line 101-105: The empty-response branch currently returns "undefined as
unknown as T" which is unsafe; change the API to surface this possibility by
updating the async function's return type from Promise<T> to Promise<T |
undefined> (or explicitly handle 204/empty responses by returning a typed
sentinel or throwing a specific error), update the signature where the function
is declared and all callers to handle undefined, and replace the line that reads
"const text = await response.text(); if (!text) { return undefined as unknown as
T; }" with a typed return (e.g., "return undefined;" with the new union return
type) or an explicit 204 handler so the code is type-safe and callers must
account for absent body.
- Around line 25-27: The catch block currently swallows exceptions (in the
function containing the try/catch in frontend/src/lib/api/fetchClient.ts);
change it to log the caught error instead of returning silently—capture the
error object in the catch (e.g., catch (err)) and call the existing logger (or
console.error) to emit the error (include a clear message like "fetchClient
error") and then return false; optionally gate verbose logging behind a
development check (NODE_ENV === 'development') if needed; update the catch in
this file so the error reference and logging happen before the final return.

In `@frontend/src/lib/api/folderApi.ts`:
- Around line 44-48: The useFolders hook uses a non-null assertion
(ownerMemberId!) in queryFn while enabling the query with !!ownerMemberId, which
risks passing undefined if refetch runs when enabled is false; modify useFolders
(queryFn and enabled) to avoid the assertion by making enabled explicitly check
for undefined/null (e.g., ownerMemberId !== undefined && ownerMemberId !== null)
and change queryFn to guard against missing IDs (e.g., return an empty result or
throw early when ownerMemberId is null/undefined) before calling
fetchRootFolders(ownerMemberId), so fetchRootFolders is only invoked with a
valid number.

In `@frontend/src/lib/api/tagApi.ts`:
- Around line 32-36: The query is disabled incorrectly when ownerMemberId is 0
because enabled uses a boolean cast; update the enabled check in useTags to
explicitly test for null/undefined (e.g., ownerMemberId != null) so that 0 is
treated as a valid ID; locate the useTags function (and its useQuery
call/queryFn that calls fetchTags) and replace enabled: !!ownerMemberId with
enabled: ownerMemberId != null.

In `@frontend/src/lib/config/backend.ts`:
- Around line 5-7: rawBackendOrigin is set to an empty string in production when
NEXT_PUBLIC_BACKEND_ORIGIN is not provided, which can hide misconfiguration;
update the logic around rawBackendOrigin to detect when process.env.NODE_ENV ===
'production' and the env var is missing or empty and emit a clear warning (e.g.,
console.warn or a logger) that NEXT_PUBLIC_BACKEND_ORIGIN is not set and the app
will use an empty origin, so operators can catch misconfiguration early;
reference the rawBackendOrigin initialization in
frontend/src/lib/config/backend.ts and add the conditional warning immediately
after computing rawBackendOrigin.

In `@frontend/src/lib/store/authStore.ts`:
- Line 33: 현재 race에서 initialize()가 진행되는 동안 logout()이 완료되면 뒤늦게 끝난 initialize()가
accessToken/member/isAuthenticated를 덮어써서 로그아웃 상태를 복원할 수 있으므로 initialize() 시작 시의
세션 버전(tokenVersion 등)을 캡처하고, logout()에서 그 버전을 변경(increment)하도록 하여 initialize()가
완료되기 직전에 현재 버전과 캡처한 버전을 비교해 다르면 쓰기를 취소하도록 수정하세요; 구체적으로 authStore의
initializePromise/initialize()와 logout() 경로에 전역 숫자 버전(예:
initVersion/lastAuthVersion)을 추가하고 initialize()가 시작할 때 const myVersion =
lastAuthVersion; 로 저장한 후 accessToken/member/isAuthenticated 등을 설정하기 전에 if
(myVersion !== lastAuthVersion) return; 로 stale write를 방지하세요.

In `@src/main/java/com/web/SearchWeb/auth/controller/AuthController.java`:
- Around line 73-75: The token reissue response in AuthController returns an
access token in the body but lacks cache-control headers; update the token
reissue endpoint (method in AuthController that builds the ResponseEntity with
cookie and ApiResponse.success(response)) to add appropriate headers to prevent
caching (e.g., "Cache-Control: no-store, no-cache, must-revalidate, max-age=0"
and "Pragma: no-cache" and optionally "Expires: 0") so the response (and any
access token in the body) is not stored by browsers or intermediate caches.
- Around line 60-61: The current debug log in AuthController exposes a fragment
of the refreshToken (refreshToken.substring(...)); remove any logging of the
token or its prefix and replace it with a presence-only message. In the method
handling refresh tokens (where the variable refreshToken is inspected and the
log.debug call exists), change the log call to indicate only that a refresh
token was received or missing (e.g., "refresh token received" or "no refresh
token provided") and ensure no part of refreshToken is ever concatenated into
logs.

In `@src/main/java/com/web/SearchWeb/auth/dao/MybatisRefreshTokenDao.java`:
- Around line 47-57: 현재 deleteByTokenHash와 deleteByMemberId 같은 토큰 상태 변경 메서드가
void라 변경된 행 수를 알 수 없으니 각 메서드(deleteByTokenHash, deleteByMemberId 및 리뷰에 언급된 회전/삭제
관련 메서드들)의 반환형을 void에서 int로 바꾸고 내부에서 sqlSession.delete(...) 또는
sqlSession.update(...)의 반환값을 그대로 반환하도록 수정하세요; 또한 해당 DAO 인터페이스(및 이를 호출하는 서비스/테스트
코드)의 시그니처를 함께 업데이트하고 호출 지점에서 반환된 int를 검사해 0이면 실패(예: 토큰 없음)로 처리하도록 수정하십시오.

In `@src/main/java/com/web/SearchWeb/auth/domain/RefreshToken.java`:
- Around line 12-20: The RefreshToken class is annotated only with `@Builder` and
lacks a no-arg constructor, causing MyBatis resultMap injections (used by
refresh-token-mapper.xml's refreshTokenMap) to fail for methods like
findByTokenHash and findByTokenHashForUpdate; fix by either adding a no-arg
constructor to RefreshToken (e.g., annotate class with `@NoArgsConstructor`) so
MyBatis can instantiate and set properties, or keep the builder and update
refreshTokenMap to use an explicit <constructor> mapping that matches the
builder/constructor parameter order.

In `@src/main/java/com/web/SearchWeb/auth/service/AuthService.java`:
- Line 12: 현재 AuthService 인터페이스의 issueTokens(Long memberId, String role) 시그니처는
외부에서 역할을 주입받게 되어 보안 리스크가 있으므로 issueTokens(Long memberId)로 변경하고 구현체와 호출부를 모두
리팩토링하세요; AuthService 인터페이스의 method 시그니처를 제거된 role 파라미터로 수정하고 해당 구현 클래스(예:
AuthServiceImpl)의 issueTokens 구현에서 getMemberRole(memberId)를 내부 호출해 역할을 조회한 뒤 토큰을
발급하도록 옮기며, 기존에 role을 넘기던 모든 호출부(현재 getMemberRole(memberId)를 사용하던 지점 포함)를 새 시그니처에
맞게 호출을 변경하고 컴파일/테스트를 통과시키세요.

In `@src/main/java/com/web/SearchWeb/auth/service/AuthServiceImpl.java`:
- Around line 101-119: The reuse-in-grace-period branch currently only deletes
the incoming rotated token hash (refreshTokenDao.deleteByTokenHash(hashedToken))
and then issues a new token via issueTokens(...), which leaves any previously
issued successor (e.g., RT-B) active; change this so the reuse path is
idempotent: either return the same successor token if it already exists or,
before calling issueTokens, atomically remove/expire any existing active rotated
successors for the same member. Concretely, in the block checking
storedRefreshToken.getRotatedAt(), query for any refresh tokens for
storedRefreshToken.getMemberId() that are marked as successors/active and delete
them (or fetch and return the existing successor token if present) prior to
issuing a new token, and ensure refreshTokenDao.updateRotatedAt(hashedToken) and
refreshTokenDao.deleteByTokenHash(hashedToken) are adjusted so only one
successor remains; use the symbols storedRefreshToken, getRotatedAt(),
refreshTokenDao.deleteByTokenHash, refreshTokenDao.updateRotatedAt, issueTokens,
and getMemberRole to locate and modify the logic.

In
`@src/main/java/com/web/SearchWeb/auth/service/RefreshTokenCleanupScheduler.java`:
- Line 22: The `@Scheduled` annotation on RefreshTokenCleanupScheduler relies on
the JVM default timezone; update the annotation to include an explicit zone
attribute (for example zone = "UTC" or zone = "Asia/Seoul") to ensure consistent
execution across environments. Locate the `@Scheduled`(...) on the
RefreshTokenCleanupScheduler class and add the appropriate zone value that
matches your deployment expectations, then run tests or verify schedule with
your target timezone to confirm correct timing.

In `@src/main/java/com/web/SearchWeb/board/controller/BoardController.java`:
- Around line 1-190: The file contains a fully commented-out controller class
BoardController (class name BoardController, methods like boardPage,
insertBoard, boardDetail, getBoards, updateBoard, deleteBoard, toggleLike,
toggleBookmark) and should be removed; delete the entire BoardController.java
file from the repository, ensure no live references/imports to BoardController
remain elsewhere (search for "BoardController" and usages of methods like
insertBoard/getBoards/toggleBookmark), and commit the deletion so the codebase
is cleaned while history remains recoverable in Git.

In `@src/main/java/com/web/SearchWeb/board/dao/MybatisBoardDao.java`:
- Around line 1-139: The file contains a fully commented-out MybatisBoardDao
class (symbols: MybatisBoardDao, mapper, insertBoard, selectBoardPage,
countBoardList, etc.); remove this dead file entirely from the repo (or move it
to a dedicated archive branch/tag) and ensure no other code imports or
references MybatisBoardDao or its mapper; if you need to retain history, create
a branch/tag or note the commit hash in the PR instead of keeping commented code
in the working tree.

In `@src/main/java/com/web/SearchWeb/board/domain/Board.java`:
- Around line 1-30: This file contains a fully commented-out legacy Board class
(Board, fields like boardId, memberMemberId, nickname, title, etc.) which should
be removed from src/main/java to avoid IDE/search noise; either delete the file
from the repository or move the commented backup to a dedicated archive location
outside the main source tree (e.g., docs/legacy or an archival branch) and
commit that change, ensuring no lingering commented-out class remains in
src/main/java.

In `@src/main/java/com/web/SearchWeb/board/service/BoardService.java`:
- Around line 1-137: This file contains the entire BoardService class commented
out (class BoardService and its methods like insertBoard, selectBoardPage,
selectBoard, updateBoard, deleteBoard, incrementBookmarkCount,
decrementBookmarkCount, findMemberIdByBoardId) and should be removed; delete
this commented-out file from the repository (same treatment as
MybatisBoardDao.java) and ensure no active imports or references expect it—if
anything still references BoardService, restore/replace with the intended
implementation instead of keeping a commented copy.

In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java`:
- Around line 333-359: Remove the large commented legacy block inside class
BookmarkServiceImpl that contains the unused board-bookmark methods
(checkBoardBookmark, isBookmarked, insertBookmarkForBoard, deleteBookmarkBoard);
delete the entire commented-out section to improve readability, commit the
change (rely on Git history to recover if needed), and run the build/tests to
ensure no callers or interface contracts require these methods before
finalizing.

In `@src/main/java/com/web/SearchWeb/config/jwt/JwtAuthenticationEntryPoint.java`:
- Line 23: The JwtAuthenticationEntryPoint currently creates a private final
ObjectMapper with new ObjectMapper(); change this to receive the
application-wide ObjectMapper from Spring (inject the ObjectMapper bean) instead
— e.g., add an ObjectMapper parameter to the JwtAuthenticationEntryPoint
constructor (or use `@Autowired`) and assign it to the existing objectMapper
field, removing the direct new ObjectMapper() instantiation so the class uses
the shared, configured ObjectMapper.

In `@src/main/java/com/web/SearchWeb/config/jwt/JwtAuthenticationFilter.java`:
- Around line 66-68: The current JwtAuthenticationFilter token extraction only
accepts "Bearer " with exact casing; update the extraction logic in
JwtAuthenticationFilter (the method that checks the bearer header and returns
bearer.substring(7)) to be case-insensitive and tolerant of extra whitespace by
trimming the header first, performing a case-insensitive check for the "bearer"
scheme (e.g., compare lowercased prefix or use regionMatches with ignoreCase),
and then return the remainder after the scheme and single space (or split on
whitespace and take the token part) so headers like "bearer token", "BEARER  
token", or " Bearer token " are handled correctly.

In `@src/main/java/com/web/SearchWeb/config/jwt/JwtMemberPrincipal.java`:
- Around line 11-20: JwtMemberPrincipal currently allows memberId to be null
(causing getName() to return "null"); add a compact canonical constructor in the
JwtMemberPrincipal record to validate inputs (at least require memberId != null,
optionally role != null) and throw an appropriate runtime exception (e.g.,
IllegalArgumentException or NullPointerException) when violated so the record
cannot be created with a null memberId; keep getName() unchanged after
validation since memberId will be non-null.

In `@src/main/java/com/web/SearchWeb/config/jwt/JwtProperties.java`:
- Around line 15-18: Update the JwtProperties data fields to validate the
Base64-encoded secret and cookie path: add `@Size`(min=44) to the secret field in
JwtProperties to enforce a minimum Base64 length (32 bytes -> 44 chars) and add
a `@Pattern`(regexp="^/.*") (or equivalent) to refreshTokenPath to require a
leading '/'; keep the existing `@NotBlank/`@Positive annotations and ensure
imports for the validation annotations are present so these constraints run at
startup.

In `@src/main/java/com/web/SearchWeb/config/jwt/JwtUtils.java`:
- Around line 72-77: parseAccessToken currently calls claims.get("role",
String.class) which may return null and lead to downstream authorization
failures; update parseAccessToken to validate the retrieved role (from
claims.get("role", String.class")) and handle the missing case explicitly—either
throw a clear runtime/JWT-specific exception (e.g., IllegalArgumentException or
JwtException) with a message like "missing role claim" or substitute a safe
default role before constructing JwtMemberPrincipal(memberId, role); ensure the
check is in parseAccessToken so JwtMemberPrincipal is never created with a null
role.

In `@src/main/java/com/web/SearchWeb/config/jwt/OAuth2FailureHandler.java`:
- Around line 40-47: The code in OAuth2FailureHandler currently uses
exception.getLocalizedMessage() (assigned to errorMessage) which can leak
internal details; change the logic to not expose raw exception messages:
instead, set errorMessage to a safe generic default like "인증에 실패했습니다. 다시
시도해주세요." then only override it when you can reliably detect a known condition
(e.g., when exception.getLocalizedMessage() contains
"authorization_request_not_found" keep the user-friendly message "인증 요청 정보가
만료되었습니다. 다시 로그인해주세요."); also add a null check on exception.getLocalizedMessage()
before calling contains to avoid NPEs and do not forward the original exception
text to the response or logs visible to users (keep detailed exception logged
only to secure server logs).

In `@src/main/java/com/web/SearchWeb/config/jwt/OAuth2SuccessHandler.java`:
- Around line 61-65: Remove the plaintext refresh token prints in
OAuth2SuccessHandler: eliminate the System.out.println block that prints
refreshToken, and if you truly need a test-only trace, replace it with a masked
identifier or hash (e.g., log only a fixed prefix plus first/last N chars or a
one-way digest) and guard it behind an environment/profile check (local/test) so
production never logs tokens; ensure all references to refreshToken in that
logging block are removed or replaced with the masked value.
- Around line 58-84: You are issuing and storing the refresh token
(authService.issueRefreshToken + building ResponseCookie and response.addHeader)
before validating the redirect target (determineTargetUrl) and clearing the
temporary OAuth2 cookies
(authorizationRequestRepository.removeAuthorizationRequestCookies), so a failed
determineTargetUrl would leave a persisted token and cookie; move the call to
determineTargetUrl(request, response, authentication) and the
authorizationRequestRepository.removeAuthorizationRequestCookies(request,
response) to before calling authService.issueRefreshToken, and only after
determineTargetUrl succeeds create the refresh token, build the ResponseCookie
and addHeader, then call response.sendRedirect(targetUrl) so no token/cookie is
issued or stored if target validation fails.

In `@src/main/java/com/web/SearchWeb/config/security/CookieUtils.java`:
- Around line 95-106: deserialize(Cookie, Class<T>) currently swallows
exceptions and returns null unlike serialize(...), causing inconsistent error
handling; change deserialize to stop returning null and instead rethrow the
original exception (or wrap it in a runtime exception like
IllegalStateException) after logging so both serialize and deserialize have
consistent behavior, and update call sites to handle the thrown exception
accordingly; reference the deserialize(Cookie, Class<T>) method and the
serialize(...) method when making this change.
- Around line 81-91: The serialize method currently calls json.getBytes()
without a charset; update CookieUtils.serialize to use an explicit charset by
replacing json.getBytes() with json.getBytes(StandardCharsets.UTF_8) (and add
the import for java.nio.charset.StandardCharsets if missing) so the
Base64-encoding is consistent across platforms; reference the serialize method
and objectMapper in your change.

In
`@src/main/java/com/web/SearchWeb/config/security/HttpCookieOAuth2AuthorizationRequestRepository.java`:
- Around line 43-46: The code stores the redirect URI parameter into a cookie
without early validation; update HttpCookieOAuth2AuthorizationRequestRepository
where redirectUriAfterLogin is read to validate it before calling
CookieUtils.addCookie: reuse the same validation logic as
OAuth2SuccessHandler.isAuthorizedRedirectUri (treat values starting with "/" as
allowed relative paths, otherwise require scheme/host/port match against
configured authorized redirect URIs) or extract that logic into a shared
validator method and call it here, and only add the cookie when validation
passes (preserving COOKIE_EXPIRE_SECONDS and cookieSecure).

In `@src/main/java/com/web/SearchWeb/config/security/SecurityConfig.java`:
- Around line 72-85: The current authorizeHttpRequests configuration leaves
anyRequest().permitAll() as the default, exposing any new /api/** endpoints;
update SecurityConfig's authorizeHttpRequests lambda so API endpoints are
protected by default by adding a catch-all API matcher like
.requestMatchers("/api/**").authenticated() after the explicit public API
matchers (e.g., keep .requestMatchers("/api/auth/refresh",
"/api/auth/logout").permitAll() and .requestMatchers("/oauth2/**",
"/login/oauth2/**").permitAll() first), then enforce
.requestMatchers("/api/**").authenticated() and keep .anyRequest().permitAll()
for non-API public routes; also ensure role checks (e.g.,
.requestMatchers("/admin").hasRole("ADMIN")) remain in place and consider
changing to "/admin/**" if needed.

In `@src/main/java/com/web/SearchWeb/config/security/SecurityUtils.java`:
- Around line 64-74: The hashToken method can NPE when input is null; add a
defensive null check at the start of SecurityUtils.hashToken (e.g., if input ==
null) and handle it by throwing an appropriate runtime exception with a clear
message (for example IllegalArgumentException("input must not be null")) or
another project-consistent error; keep the existing SHA-256 logic unchanged and
ensure tests or callers expect the documented behavior.

In `@src/main/java/com/web/SearchWeb/member/domain/Member.java`:
- Around line 14-17: Member currently uses `@ToString`(callSuper = true) which
will include the sensitive passwordHash field in logs; to fix, exclude that
field from generated toString by annotating the passwordHash field in the Member
class with `@ToString.Exclude` (or remove it from Lombok's toString via a custom
toString override), keep `@SuperBuilder` usage as-is (BaseEntity already has
`@SuperBuilder`), and consider also marking passwordHash with
serialization/security annotations (e.g., `@JsonIgnore`) if appropriate for your
app.

In
`@src/main/java/com/web/SearchWeb/member/service/CustomOAuth2MemberService.java`:
- Line 77: The current social-login branch in CustomOAuth2MemberService sets a
dummy password with UUID.randomUUID().toString(); change this to a recognizable
marker or a hashed marker so social accounts are clearly distinguishable from
normal-password accounts. Update the code that builds the Member (the
.passwordHash(...) call) to use a constant like "SOCIAL_LOGIN_NO_PASSWORD" (or
its secure hash) instead of a raw UUID, and ensure any authentication or
account-update logic that checks password state references this marker when
determining social-login accounts.
- Around line 82-96: After catching DataIntegrityViolationException during
memberDao.insertSocialMember, you should avoid always calling
syncSocialIdentityIfChanged; after retrieving existMember via
memberDao.findByLoginId(loginId), compare the fetched member's socialName and
socialEmail with the incoming socialName/socialEmail and only invoke
syncSocialIdentityIfChanged(existMember, socialName, socialEmail) when at least
one value differs; reference memberDao.insertSocialMember, findByLoginId,
existMember, syncSocialIdentityIfChanged and DataIntegrityViolationException to
locate the change.

In `@src/main/java/com/web/SearchWeb/member/service/MemberServiceImpl.java`:
- Around line 4-7: Remove the leftover commented legacy imports and any related
commented-out code in MemberServiceImpl: delete the commented import lines for
BoardDao, CommentDao, Comment, and UpdateUserProfileCommentDto and scan the
class for any other commented legacy blocks to remove (e.g., commented methods
or fields referring to those symbols) so the file only contains active imports
and code; rely on Git history if the code needs to be restored later.

In `@src/main/resources/application-prod.properties`:
- Line 7: 현재 app.oauth2.redirect-uri에 localhost 기본값이 포함되어 있어 운영에서 환경변수 누락 시 로컬로
리다이렉트되는 문제가 있습니다; 수정 방법은 프로덕션 설정에서 기본값을 제거하고 환경변수에 의존하도록 app.oauth2.redirect-uri
항목을 변경(예: app.oauth2.redirect-uri=${PROD_FRONTEND_URL}/auth/callback)하여
PROD_FRONTEND_URL이 없으면 애플리케이션이 시작 시 실패하도록 하세요; 대상 식별자: 설정 키
app.oauth2.redirect-uri.

In `@src/main/resources/db/init_postgres.sql`:
- Around line 558-560: The current single-column indexes on refresh_token don't
optimize the cleanup query that filters on both expires_at and rotated_at; add a
composite or partial index to support the condition used by your batch job.
Specifically, create an index on the refresh_token table referencing
(expires_at, rotated_at) or a partial index on expires_at WHERE rotated_at IS
NOT NULL to accelerate queries like "expires_at < now() AND rotated_at IS NOT
NULL"; update the SQL in src/main/resources/db/init_postgres.sql to add one of
these indexes (e.g., idx_refresh_token_expires_rotated or a partial
idx_refresh_token_expires_notnull_rotated) so the cleanup job uses the index.

In `@src/main/resources/mapper/member-mapper.xml`:
- Around line 32-35: insertSocialMember INSERT includes nick_name but
CustomOAuth2MemberService's Member.builder() never sets nickName, so NULL is
inserted; fix by setting nickName in the builder (e.g.,
.nickName(member.getMemberName() != null ? member.getMemberName() :
member.getLoginId()) or similar) before persisting the member in
CustomOAuth2MemberService, or alternatively remove nick_name from the
insertSocialMember SQL if you intentionally don't want to store a display name.

---

Outside diff comments:
In `@frontend/next.config.ts`:
- Around line 4-14: Add an explicit exclusion for auth endpoints in the
rewrites() function: before the generic { source: '/api/:path*', destination:
'http://localhost:8080/api/:path*' } rule, insert a specific rule for
'/api/auth/:path*' that leaves the request unrewritten (e.g. { source:
'/api/auth/:path*', destination: '/api/auth/:path*' }) so auth calls are not
captured by the generic rewrite; update the rewrites() return ordering so the
auth rule appears before the generic '/api/:path*' entry.

In `@frontend/src/app/login/page.tsx`:
- Around line 164-194: The form currently only calls e.preventDefault() in its
onSubmit and has no login logic; replace the noop with a real submit handler
(e.g., handleLogin) that reads the email/password values from the Input
components (use controlled state or refs for Input with ids "email" and
"password"), validates them, calls your authentication API/service, handles
loading and error states, and redirects on success; if you don't want to
implement auth now, disable the submit button (or hide the form) and update the
button/NextLink UI accordingly so users aren't able to click a non-functional
"Log in" button.

In `@frontend/src/components/dialogs/CreateFolderDialog.tsx`:
- Around line 184-191: The Create Folder button's disabled logic should also
check for authentication: update the disabled prop to include a check for the
presence of memberId (alongside !folderName.trim() and
createFolderMutation.isPending) so unauthenticated users cannot click it; locate
the JSX button that calls handleCreateFolder and references folderName and
createFolderMutation.isPending and add a guard for memberId (the same memberId
checked earlier where handleCreateFolder returns early).

In `@frontend/src/components/my-links/RightPanel.tsx`:
- Line 101: The effect in RightPanel.tsx currently calls handleNoteEditComplete,
handleTitleEditComplete, and handleTagEditComplete but they are missing from the
dependency array, risking stale closures; fix by either memoizing those handlers
with useCallback (wrap handleNoteEditComplete, handleTitleEditComplete,
handleTagEditComplete in useCallback with their internal dependencies) or add
them to the useEffect dependency array so React can track changes, then run
linting to ensure exhaustive-deps is satisfied.

In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java`:
- Around line 39-53: Remove the commented-out "Legacy Board-Bookmark Methods"
block in BookmarkService.java: delete the entire multi-line comment that
contains the placeholder method signatures (checkBoardBookmark, isBookmarked,
insertBookmarkForBoard, deleteBookmarkBoard) and the surrounding "Legacy
Board-Bookmark Methods (Commented Out)" header so the interface/class no longer
contains dead commented code; rely on VCS history for retrieval if needed.

In `@src/main/java/com/web/SearchWeb/folder/service/MemberFolderServiceImpl.java`:
- Around line 22-65: MemberFolderServiceImpl currently relies only on exists
checks and doesn't handle DB unique constraint failures
(uq_member_folder_parent_name), so wrap the persistence calls in create(), and
similarly in update() and move(), with a try-catch that catches
DataIntegrityViolationException (or the Spring root cause) and throws new
FolderException(FolderErrorCode.DUPLICATE_FOLDER_NAME); specifically, around
memberFolderJpaRepository.save(...) in create() (and the corresponding
save/update calls in update()/move()), detect violations caused by the unique
index (including parentFolderId IS NULL root-case) and map them to the
DUPLICATE_FOLDER_NAME FolderException like BookmarkServiceImpl does.

In `@src/main/java/com/web/SearchWeb/member/dto/CustomOAuth2Member.java`:
- Around line 49-52: CustomOAuth2Member currently returns an empty Map from
getAttributes(), which breaks downstream code expecting the OAuth2 attributes;
update the class to store the incoming attributes in a final Map field (e.g.
attributes) during construction (or accept them in builder/factory) and have
getAttributes() return an immutable/defensive copy (Map.copyOf(attributes) or
Collections.unmodifiableMap(attributes)) instead of Map.of(); locate and modify
the CustomOAuth2Member constructor/field and the getAttributes() method to
return that stored map and ensure it never returns null.

In `@src/main/java/com/web/SearchWeb/member/service/MemberServiceImpl.java`:
- Around line 61-77: Remove the commented-out block inside updateMember that
updates Comment user profiles via commentDao and UpdateUserProfileCommentDto
(the code referencing result, memberId, memberUpdateDto, Comment,
commentDao.updateCommentUserProfile); either delete the entire commented section
or replace it with a single TODO comment referencing the issue/PR number if this
behavior will be restored later, so the method no longer contains stale
commented logic.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 99637773-5459-48b8-82b8-86a447c35c22

📥 Commits

Reviewing files that changed from the base of the PR and between 59f22b9 and 2cb1273.

📒 Files selected for processing (106)
  • .gitignore
  • build.gradle
  • docs/Backend/02. jwt-oauth2-architecture.md
  • docs/Backend/03. jwt-oauth2-test-scenarios.md
  • frontend/next.config.ts
  • frontend/src/app/auth/callback/page.tsx
  • frontend/src/app/layout.tsx
  • frontend/src/app/login/page.tsx
  • frontend/src/app/my-links/page.tsx
  • frontend/src/components/dialogs/CreateFolderDialog.tsx
  • frontend/src/components/dialogs/SaveLinkDialog.tsx
  • frontend/src/components/layout/Sidebar.tsx
  • frontend/src/components/my-links/RightPanel.tsx
  • frontend/src/components/providers/AuthProvider.tsx
  • frontend/src/lib/api/fetchClient.ts
  • frontend/src/lib/api/folderApi.ts
  • frontend/src/lib/api/tagApi.ts
  • frontend/src/lib/auth/currentUser.ts
  • frontend/src/lib/config/backend.ts
  • frontend/src/lib/store/authStore.ts
  • src/main/java/com/web/SearchWeb/SearchWebApplication.java
  • src/main/java/com/web/SearchWeb/aop/OwnerCheckAspect.java
  • src/main/java/com/web/SearchWeb/auth/controller/AuthController.java
  • src/main/java/com/web/SearchWeb/auth/controller/dto/AuthResponses.java
  • src/main/java/com/web/SearchWeb/auth/dao/MybatisRefreshTokenDao.java
  • src/main/java/com/web/SearchWeb/auth/dao/RefreshTokenDao.java
  • src/main/java/com/web/SearchWeb/auth/domain/RefreshToken.java
  • src/main/java/com/web/SearchWeb/auth/error/AuthErrorCode.java
  • src/main/java/com/web/SearchWeb/auth/error/AuthException.java
  • src/main/java/com/web/SearchWeb/auth/service/AuthService.java
  • src/main/java/com/web/SearchWeb/auth/service/AuthServiceImpl.java
  • src/main/java/com/web/SearchWeb/auth/service/RefreshTokenCleanupScheduler.java
  • src/main/java/com/web/SearchWeb/board/controller/BoardController.java
  • src/main/java/com/web/SearchWeb/board/dao/BoardDao.java
  • src/main/java/com/web/SearchWeb/board/dao/MybatisBoardDao.java
  • src/main/java/com/web/SearchWeb/board/domain/Board.java
  • src/main/java/com/web/SearchWeb/board/dto/BoardDto.java
  • src/main/java/com/web/SearchWeb/board/service/BoardService.java
  • src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java
  • src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java
  • src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java
  • src/main/java/com/web/SearchWeb/comment/controller/CommentApiController.java
  • src/main/java/com/web/SearchWeb/comment/dao/CommentDao.java
  • src/main/java/com/web/SearchWeb/comment/dao/MybatisCommentDao.java
  • src/main/java/com/web/SearchWeb/comment/domain/Comment.java
  • src/main/java/com/web/SearchWeb/comment/dto/CommentDto.java
  • src/main/java/com/web/SearchWeb/comment/dto/UpdateUserProfileCommentDto.java
  • src/main/java/com/web/SearchWeb/comment/service/CommentService.java
  • src/main/java/com/web/SearchWeb/config/jwt/JwtAuthenticationEntryPoint.java
  • src/main/java/com/web/SearchWeb/config/jwt/JwtAuthenticationFilter.java
  • src/main/java/com/web/SearchWeb/config/jwt/JwtMemberPrincipal.java
  • src/main/java/com/web/SearchWeb/config/jwt/JwtProperties.java
  • src/main/java/com/web/SearchWeb/config/jwt/JwtUtils.java
  • src/main/java/com/web/SearchWeb/config/jwt/OAuth2FailureHandler.java
  • src/main/java/com/web/SearchWeb/config/jwt/OAuth2SuccessHandler.java
  • src/main/java/com/web/SearchWeb/config/security/CookieUtils.java
  • src/main/java/com/web/SearchWeb/config/security/CurrentMemberId.java
  • src/main/java/com/web/SearchWeb/config/security/CurrentMemberIdArgumentResolver.java
  • src/main/java/com/web/SearchWeb/config/security/CustomAuthenticationFailureHandler.java
  • src/main/java/com/web/SearchWeb/config/security/CustomAuthenticationSuccessHandler.java
  • src/main/java/com/web/SearchWeb/config/security/HttpCookieOAuth2AuthorizationRequestRepository.java
  • src/main/java/com/web/SearchWeb/config/security/SecurityConfig.java
  • src/main/java/com/web/SearchWeb/config/security/SecurityUtils.java
  • src/main/java/com/web/SearchWeb/config/security/WebMvcConfig.java
  • src/main/java/com/web/SearchWeb/folder/controller/MemberFolderController.java
  • src/main/java/com/web/SearchWeb/folder/dao/MemberFolderJpaDao.java
  • src/main/java/com/web/SearchWeb/folder/service/MemberFolderService.java
  • src/main/java/com/web/SearchWeb/folder/service/MemberFolderServiceImpl.java
  • src/main/java/com/web/SearchWeb/likes/dao/LikesDao.java
  • src/main/java/com/web/SearchWeb/likes/dao/MybatisLikesDao.java
  • src/main/java/com/web/SearchWeb/likes/service/LikesService.java
  • src/main/java/com/web/SearchWeb/linkanalysis/controller/LinkAnalysisController.java
  • src/main/java/com/web/SearchWeb/main/controller/MainController.java
  • src/main/java/com/web/SearchWeb/main/dao/MainDao.java
  • src/main/java/com/web/SearchWeb/main/dao/MybatisMainDao.java
  • src/main/java/com/web/SearchWeb/main/domain/Website.java
  • src/main/java/com/web/SearchWeb/main/service/MainService.java
  • src/main/java/com/web/SearchWeb/main/service/MainServiceImpl.java
  • src/main/java/com/web/SearchWeb/member/controller/MemberController.java
  • src/main/java/com/web/SearchWeb/member/dao/MemberDao.java
  • src/main/java/com/web/SearchWeb/member/dao/MybatisMemberDao.java
  • src/main/java/com/web/SearchWeb/member/domain/Member.java
  • src/main/java/com/web/SearchWeb/member/dto/CustomOAuth2Member.java
  • src/main/java/com/web/SearchWeb/member/dto/CustomUserDetails.java
  • src/main/java/com/web/SearchWeb/member/dto/MemberDto.java
  • src/main/java/com/web/SearchWeb/member/dto/Response/OAuth2Response.java
  • src/main/java/com/web/SearchWeb/member/error/MemberErrorCode.java
  • src/main/java/com/web/SearchWeb/member/error/MemberException.java
  • src/main/java/com/web/SearchWeb/member/service/CustomOAuth2MemberService.java
  • src/main/java/com/web/SearchWeb/member/service/CustomOAuth2UserService.java
  • src/main/java/com/web/SearchWeb/member/service/CustomerUserDetailService.java
  • src/main/java/com/web/SearchWeb/member/service/MemberService.java
  • src/main/java/com/web/SearchWeb/member/service/MemberServiceImpl.java
  • src/main/resources/application-dev.properties
  • src/main/resources/application-local.properties
  • src/main/resources/application-prod.properties
  • src/main/resources/application.properties
  • src/main/resources/db/init_postgres.sql
  • src/main/resources/mapper/board-mapper.xml
  • src/main/resources/mapper/comment-mapper.xml
  • src/main/resources/mapper/likes-mapper.xml
  • src/main/resources/mapper/main-mapper.xml
  • src/main/resources/mapper/member-mapper.xml
  • src/main/resources/mapper/refresh-token-mapper.xml
  • src/main/resources/templates/member/join.html
  • src/main/resources/templates/member/login.html
💤 Files with no reviewable changes (31)
  • frontend/src/lib/auth/currentUser.ts
  • src/main/java/com/web/SearchWeb/config/security/CustomAuthenticationFailureHandler.java
  • src/main/java/com/web/SearchWeb/config/security/CustomAuthenticationSuccessHandler.java
  • src/main/java/com/web/SearchWeb/comment/dao/CommentDao.java
  • src/main/java/com/web/SearchWeb/comment/dao/MybatisCommentDao.java
  • src/main/java/com/web/SearchWeb/comment/domain/Comment.java
  • src/main/java/com/web/SearchWeb/comment/dto/CommentDto.java
  • src/main/java/com/web/SearchWeb/comment/dto/UpdateUserProfileCommentDto.java
  • src/main/java/com/web/SearchWeb/likes/dao/LikesDao.java
  • src/main/java/com/web/SearchWeb/likes/service/LikesService.java
  • src/main/java/com/web/SearchWeb/main/controller/MainController.java
  • src/main/java/com/web/SearchWeb/main/domain/Website.java
  • src/main/java/com/web/SearchWeb/member/controller/MemberController.java
  • src/main/java/com/web/SearchWeb/member/dto/CustomUserDetails.java
  • src/main/java/com/web/SearchWeb/comment/controller/CommentApiController.java
  • src/main/java/com/web/SearchWeb/comment/service/CommentService.java
  • src/main/java/com/web/SearchWeb/member/service/CustomerUserDetailService.java
  • src/main/java/com/web/SearchWeb/likes/dao/MybatisLikesDao.java
  • src/main/java/com/web/SearchWeb/main/dao/MainDao.java
  • src/main/java/com/web/SearchWeb/main/dao/MybatisMainDao.java
  • src/main/java/com/web/SearchWeb/main/service/MainService.java
  • src/main/java/com/web/SearchWeb/main/service/MainServiceImpl.java
  • src/main/java/com/web/SearchWeb/member/dto/MemberDto.java
  • src/main/resources/mapper/comment-mapper.xml
  • src/main/resources/templates/member/join.html
  • src/main/java/com/web/SearchWeb/member/service/MemberService.java
  • src/main/resources/mapper/main-mapper.xml
  • src/main/resources/mapper/board-mapper.xml
  • src/main/resources/mapper/likes-mapper.xml
  • src/main/resources/templates/member/login.html
  • src/main/java/com/web/SearchWeb/member/service/CustomOAuth2UserService.java

Comment on lines +54 to +58
### Temporary Files ###
.gradle-user/
status.txt
status_utf8.txt
docs/Backend/memo/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

임시 파일 ignore 범위를 루트로 고정하는 편이 안전합니다.

status.txt/status_utf8.txt처럼 파일명만 적은 패턴은 하위 디렉터리의 동명 파일까지 모두 숨깁니다. 로컬 산출물만 막을 목적이면 루트 기준으로 anchoring 해 두는 편이 부작용이 적습니다.

제안 수정
-.gradle-user/
-status.txt
-status_utf8.txt
-docs/Backend/memo/
+/.gradle-user/
+/status.txt
+/status_utf8.txt
+/docs/Backend/memo/
📝 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.

Suggested change
### Temporary Files ###
.gradle-user/
status.txt
status_utf8.txt
docs/Backend/memo/
### Temporary Files ###
/.gradle-user/
/status.txt
/status_utf8.txt
/docs/Backend/memo/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore around lines 54 - 58, 현재 .gitignore에 있는 `status.txt`와
`status_utf8.txt` 같은 파일명만 적은 패턴은 하위 디렉터리의 동일 이름 파일까지 모두 무시하므로, 루트 산출물만 무시하려면 파일명
패턴을 루트 기준으로 고정하세요; 즉 `.gitignore`에서 `status.txt`, `status_utf8.txt` 항목을 루트
앵커링(루트 경로 기준) 형태로 변경하고 필요하면 `.gradle-user/`와 `docs/Backend/memo/`도 루트 기준인지 확인해
일관되게 수정해서 의도치 않은 하위 디렉터리 파일 무시를 방지하세요.

Comment on lines +28 to +37
### 에러 코드 레퍼런스
| Code | HTTP Status | Message | 발생 위치 |
|------|------------|---------|----------|
| A001 | 401 | 인증이 필요합니다 | JwtAuthenticationEntryPoint (기본값) |
| A002 | 401 | 토큰이 만료되었습니다 | JwtAuthenticationFilter (ExpiredJwtException) |
| A003 | 401 | 유효하지 않은 토큰입니다 | JwtAuthenticationFilter (JwtException / IllegalArgumentException) |
| A004 | 401 | Refresh Token이 없습니다 | AuthController, AuthServiceImpl (토큰 미존재/만료/재사용 초과) |
| A005 | 403 | 접근 권한이 없습니다 | OwnerCheckAspect (리소스 소유권 불일치) |
| A006 | 400 | 유효하지 않은 리다이렉트 주소입니다 | OAuth2SuccessHandler (Open Redirect 차단) |
| A007 | 400 | 지원하지 않는 소셜 로그인 제공자입니다 | CustomOAuth2MemberService |
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

상단 에러 코드 표의 A004 설명이 구현과 다릅니다.

현재 표는 A004를 “토큰 미존재/만료/재사용 초과”로 묶고 있는데, 구현상 Refresh Token의 JWT 자체 만료는 refresh() 첫 단계에서 A002로 처리됩니다. A004는 쿠키 부재·DB 미존재·재사용 초과 쪽으로 좁혀 적어야 QA 기대값이 어긋나지 않습니다.

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 28-28: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 29-29: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

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

In `@docs/Backend/03`. jwt-oauth2-test-scenarios.md around lines 28 - 37, 문서의 A004
설명이 구현과 불일치하므로 A004 행을 수정해 refresh()의 첫 단계에서 처리되는 만료(A002)를 제외하고, A004를 "Refresh
Token 쿠키 부재 / DB 미존재 / 재사용 초과"로 좁혀 작성하세요; 참조되는 구현 심볼은 refresh(), AuthController,
AuthServiceImpl이며 JwtAuthenticationFilter의 ExpiredJwtException 처리(A002)는 별도로
유지합니다.

Comment on lines +29 to +41
useEffect(() => {
if (isInitializing) return; // 아직 초기화 중이면 판단 보류
const isPublicPath = PUBLIC_PATHS.some((path) =>
path === '/' ? pathname === '/' : pathname.startsWith(path),
);
if (!isAuthenticated && !isPublicPath) {
// 서버/네트워크 오류 메시지가 있으면 로그인 페이지에서 보여주기 위해 저장
if (authError) {
sessionStorage.setItem('authError', authError);
}
router.replace('/login');
}
}, [isInitializing, isAuthenticated, pathname, authError, router]);
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

보호된 콘텐츠가 리다이렉트 전에 잠깐 노출될 수 있습니다.

isInitializingfalse가 된 후 router.replace('/login')이 완료되기 전까지 children이 렌더링됩니다. 미인증 사용자에게 보호된 페이지 내용이 순간적으로 보일 수 있습니다.

🛡️ 리다이렉트 중 상태 추가 제안
+ const [isRedirecting, setIsRedirecting] = useState(false);

  useEffect(() => {
    if (isInitializing) return;
    const isPublicPath = PUBLIC_PATHS.some((path) =>
      path === '/' ? pathname === '/' : pathname.startsWith(path),
    );
    if (!isAuthenticated && !isPublicPath) {
      if (authError) {
        sessionStorage.setItem('authError', authError);
      }
+     setIsRedirecting(true);
      router.replace('/login');
    }
  }, [isInitializing, isAuthenticated, pathname, authError, router]);

- if (isInitializing) {
+ if (isInitializing || isRedirecting) {
    return (
      // loading UI...
    );
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/providers/AuthProvider.tsx` around lines 29 - 41, The
protected content can flash before navigation completes; update AuthProvider to
track a redirecting state: inside the useEffect that currently calls
router.replace('/login') set a local state (e.g., isRedirecting) to true before
calling router.replace and clear it after the navigation promise
resolves/rejects, and update the component render to withhold rendering children
while isInitializing || isRedirecting || (!isAuthenticated && !isPublicPath);
modify the effect referencing isInitializing/isAuthenticated/authError/router to
set sessionStorage('authError') then set isRedirecting and await
router.replace('/login') (or attach .then/.catch) so children are not rendered
during redirect.

Comment on lines +82 to +96
try {
// 소셜 회원가입
memberDao.insertSocialMember(member);
existMember = member;
} catch (DataIntegrityViolationException e) {
// 동시 가입 시도로 인한 예외 발생 시 다시 조회
existMember = memberDao.findByLoginId(loginId);
if (existMember == null) {
throw new OAuth2AuthenticationException(
new OAuth2Error("social_login_failed"), MemberErrorCode.MEMBER_SOCIAL_LOGIN_FAILED.getMessage()
);
}
// 소셜 제공자 정보가 실제로 바뀐 경우에만 최소 필드만 동기화(변경감지)
syncSocialIdentityIfChanged(existMember, socialName, socialEmail);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

동시 가입 처리 후 불필요한 동기화 호출 가능성

DataIntegrityViolationException 발생 후 findByLoginId로 조회한 직후에 syncSocialIdentityIfChanged를 호출하고 있습니다. 동시에 가입을 시도한 다른 요청이 방금 같은 소셜 정보로 생성했다면, 조회된 데이터와 현재 소셜 정보가 동일할 가능성이 높습니다. 이 경우 syncSocialIdentityIfChanged 내부의 비교 로직으로 인해 실제 업데이트는 수행되지 않겠지만, 의도가 명확하지 않습니다.

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

In
`@src/main/java/com/web/SearchWeb/member/service/CustomOAuth2MemberService.java`
around lines 82 - 96, After catching DataIntegrityViolationException during
memberDao.insertSocialMember, you should avoid always calling
syncSocialIdentityIfChanged; after retrieving existMember via
memberDao.findByLoginId(loginId), compare the fetched member's socialName and
socialEmail with the incoming socialName/socialEmail and only invoke
syncSocialIdentityIfChanged(existMember, socialName, socialEmail) when at least
one value differs; reference memberDao.insertSocialMember, findByLoginId,
existMember, syncSocialIdentityIfChanged and DataIntegrityViolationException to
locate the change.

Comment on lines +4 to +7
// import com.web.SearchWeb.board.dao.BoardDao;
// import com.web.SearchWeb.comment.dao.CommentDao;
// import com.web.SearchWeb.comment.domain.Comment;
// import com.web.SearchWeb.comment.dto.UpdateUserProfileCommentDto;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

주석 처리된 코드 정리 필요

주석 처리된 import 문과 관련 코드가 여러 곳에 남아있습니다. 레거시 코드 제거가 완료되면 이러한 주석들을 삭제하여 코드 가독성을 개선하는 것이 좋습니다. 향후 참조가 필요하다면 Git 히스토리를 활용할 수 있습니다.

♻️ 주석 처리된 코드 제거 제안
 package com.web.SearchWeb.member.service;
 
 
-// import com.web.SearchWeb.board.dao.BoardDao;
-// import com.web.SearchWeb.comment.dao.CommentDao;
-// import com.web.SearchWeb.comment.domain.Comment;
-// import com.web.SearchWeb.comment.dto.UpdateUserProfileCommentDto;
 import com.web.SearchWeb.member.dao.MemberDao;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/web/SearchWeb/member/service/MemberServiceImpl.java` around
lines 4 - 7, Remove the leftover commented legacy imports and any related
commented-out code in MemberServiceImpl: delete the commented import lines for
BoardDao, CommentDao, Comment, and UpdateUserProfileCommentDto and scan the
class for any other commented legacy blocks to remove (e.g., commented methods
or fields referring to those symbols) so the file only contains active imports
and code; rely on Git history if the code needs to be restored later.

spring.datasource.password=${PROD_DB_PASSWORD}

## Prod App settings
app.oauth2.redirect-uri=${PROD_FRONTEND_URL:http://localhost:3000}/auth/callback
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

운영 프로필에서 localhost fallback은 제거해야 합니다.

Line 7의 기본값 http://localhost:3000은 운영 환경 변수 누락 시 OAuth 리다이렉트를 로컬로 보내 로그인 실패를 유발합니다. 운영에서는 fail-fast로 두는 게 안전합니다.

🔧 제안 수정안
-app.oauth2.redirect-uri=${PROD_FRONTEND_URL:http://localhost:3000}/auth/callback
+app.oauth2.redirect-uri=${PROD_FRONTEND_URL}/auth/callback
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/application-prod.properties` at line 7, 현재
app.oauth2.redirect-uri에 localhost 기본값이 포함되어 있어 운영에서 환경변수 누락 시 로컬로 리다이렉트되는 문제가
있습니다; 수정 방법은 프로덕션 설정에서 기본값을 제거하고 환경변수에 의존하도록 app.oauth2.redirect-uri 항목을 변경(예:
app.oauth2.redirect-uri=${PROD_FRONTEND_URL}/auth/callback)하여 PROD_FRONTEND_URL이
없으면 애플리케이션이 시작 시 실패하도록 하세요; 대상 식별자: 설정 키 app.oauth2.redirect-uri.

Comment on lines +558 to +560
CREATE INDEX IF NOT EXISTS idx_refresh_token_member ON "refresh_token" ("member_id");
CREATE INDEX IF NOT EXISTS idx_refresh_token_hash ON "refresh_token" ("token_hash");
CREATE INDEX IF NOT EXISTS idx_refresh_token_expires ON "refresh_token" ("expires_at");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

만료된 토큰 정리를 위한 복합 인덱스를 고려해보세요.

현재 인덱스는 개별 컬럼에 대한 것입니다. 만약 주기적으로 만료된 토큰을 정리하는 배치 작업이 expires_at < now() AND rotated_at IS NOT NULL 조건을 사용한다면, 복합 인덱스가 성능에 도움이 될 수 있습니다.

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

In `@src/main/resources/db/init_postgres.sql` around lines 558 - 560, The current
single-column indexes on refresh_token don't optimize the cleanup query that
filters on both expires_at and rotated_at; add a composite or partial index to
support the condition used by your batch job. Specifically, create an index on
the refresh_token table referencing (expires_at, rotated_at) or a partial index
on expires_at WHERE rotated_at IS NOT NULL to accelerate queries like
"expires_at < now() AND rotated_at IS NOT NULL"; update the SQL in
src/main/resources/db/init_postgres.sql to add one of these indexes (e.g.,
idx_refresh_token_expires_rotated or a partial
idx_refresh_token_expires_notnull_rotated) so the cleanup job uses the index.

Comment on lines +32 to 35
<insert id="insertSocialMember" parameterType="com.web.SearchWeb.member.domain.Member" useGeneratedKeys="true" keyProperty="memberId" keyColumn="member_id">
INSERT INTO member (login_id, password_hash, member_name, nick_name, role, email)
VALUES (#{loginId}, #{passwordHash}, #{memberName}, #{nickName}, #{role}, #{email})
</insert>
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:

#!/bin/bash
# Description: nick_name 컬럼 제약조건 및 다른 코드에서의 nickName 사용 확인

# 스키마 파일에서 nick_name 제약조건 확인
fd -e sql | xargs grep -i "nick_name" 2>/dev/null

# CustomOAuth2MemberService에서 Member 생성 확인
rg -A 10 "Member.builder" --type java

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 1424


🏁 Script executed:

# Check the complete schema definition for nick_name
cat src/main/resources/db/init_postgres.sql | grep -A 5 -B 5 "nick_name"

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 472


🏁 Script executed:

# Search for all usages of nickName in the codebase to understand if it's expected to have a value
rg "nickName|nick_name" --type java --type xml -C 3

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 2879


🏁 Script executed:

# Check if there are other Member.builder() calls and how they handle nickName
rg "Member\.builder\(\)" --type java -A 15

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 1996


🏁 Script executed:

# Find all INSERT statements in member-mapper.xml
rg -B 2 "INSERT INTO member" src/main/resources/mapper/member-mapper.xml -A 3

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 450


🏁 Script executed:

# Check the complete updateSocialIdentity statement
rg -A 10 "updateSocialIdentity" src/main/resources/mapper/member-mapper.xml

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 373


🏁 Script executed:

# Check if nickName is set anywhere else after social member creation
rg "setNickName|nickName\s*=" --type java

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 54


insertSocialMember에서 nickName이 설정되지 않아 NULL이 삽입됨

insertSocialMember INSERT 문은 명시적으로 nick_name 컬럼을 포함하지만, CustomOAuth2MemberServiceMember.builder()에서는 nickName을 설정하지 않습니다. 결과적으로 데이터베이스에 NULL이 저장됩니다.

nick_name은 "화면 표시용"으로 설계되었는데 NULL 값을 가지면 UI에서 문제가 될 수 있습니다. nickNamememberName이나 loginId 값으로 초기화하거나, INSERT 문에서 nick_name 컬럼을 제거하세요.

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

In `@src/main/resources/mapper/member-mapper.xml` around lines 32 - 35,
insertSocialMember INSERT includes nick_name but CustomOAuth2MemberService's
Member.builder() never sets nickName, so NULL is inserted; fix by setting
nickName in the builder (e.g., .nickName(member.getMemberName() != null ?
member.getMemberName() : member.getLoginId()) or similar) before persisting the
member in CustomOAuth2MemberService, or alternatively remove nick_name from the
insertSocialMember SQL if you intentionally don't want to store a display name.

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.

Review continued from previous batch...

jin2304 added 8 commits April 2, 2026 00:17
- JwtUtils 내 토큰 유효성 검사 및 클레임 추출 로직 엄격화

- JwtAuthenticationFilter와 EntryPoint를 통한 인증 실패 예외의 통합 처리 구조 마련

- SecurityUtils 유틸리티 연동으로 코드 중복 제거 및 보안 설정 최적화
- OAuth2SuccessHandler 내 안전한 토큰 전달 및 리다이렉트 흐름 구축

- 인증 실패 시 상세 에러 로깅 및 전용 예외 핸들러 인터페이스 개선

- 권한 부여 요청(Authorization Request) 정보 보관용 쿠키 유틸리티 최적화
- 기존 RTR 체계에 세션 ID 및 버전 관리 방식을 도입하여 보안성 강화

- 멱등성(Idempotency) 보장: 유예 기간(Grace Period) 내 중복 요청 시 직전 발급 토큰을 재반환하여 네트워크 불안정 대응

- 토큰 영속성 레이어(MyBatis DAO/Mapper) 및 만료 토큰 정리 스케줄러 통합
- fetchClient 인터셉터를 구축하여 401 에러(만료) 시 조용한 자동 토큰 갱신 수행

- Zustand 기반 authStore 연동으로 앱 전역의 인증 세션 동기화 로직 강화

- AuthProvider 내 초기 인증 확인 및 세션 유지 로직 최적화

- (Backport) 리프레시 토큰 DAO 인터페이스 최신화 반영
- folderApi, tagApi 등 개별 API 모듈의 사용자 식별자(memberId) 바인딩 리팩토링

- Sidebar 및 쿼리 관리(QueryProvider) 내 인증 상태 기반 렌더링 최적화

- 백엔드 연동 환경 설정(backend.ts) 최신화
- 리프레시 토큰 도메인 내 세션 ID, 버전 관리 필드 추가

- 인증 실패 시의 세분화된 에러 코드 정의 추가

- AuthService 인터페이스 내 신규 인증 프로세스 규격 반영
- src/test/api-test/auth-test.http: 테스트 파일 이름 변경 및 시나리오 최신화

- docs/Backend/03. jwt-oauth2-test-scenarios.md: 신규 파일 이름(auth-test.http)으로 참조 수정

- docs/Backend/02. jwt-oauth2-architecture.md: 멱등성 및 로테이션 설계 상세 문서화
- 기존 jwt-oauth2-e2e.http 파일을 auth-test.http로 명칭 변경

- 로테이션 및 멱등성 보장 로직 관련 테스트 케이스 상세 가이드 보완
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8d1dd7c2d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +61 to +62
// 1. 토큰 서명 및 유효성 검증
jwtUtils.validateToken(refreshToken);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Map refresh-token parse failures to auth errors

refresh() directly calls jwtUtils.validateToken(refreshToken) and lets ExpiredJwtException/JwtException escape, so invalid or expired refresh tokens are handled by the global Exception handler as HTTP 500 instead of an auth 401. In this codebase that changes client behavior (the auth store treats 5xx as temporary server failure), so users can get misleading errors and broken re-login flow when their refresh token is simply expired or malformed.

Useful? React with 👍 / 👎.

{/* Google Sign In - Purple Gradient Background */}
<button type="button" className="group relative mb-6 flex h-11 w-full items-center justify-center gap-3 rounded-lg border-t border-white/20 bg-[linear-gradient(135deg,#6d28d9,#8b5cf6)] text-white shadow-md transition-all hover:brightness-110 hover:shadow-primary/25">
{/* 소셜 로그인 시작도 백엔드 직통으로 보내야 OAuth 콜백과 쿠키 발급 주체가 일관된다. */}
<button type="button" onClick={() => { window.location.href = buildBackendUrl('/oauth2/authorization/google'); }} className="group relative mb-6 flex h-11 w-full items-center justify-center gap-3 rounded-lg border-t border-white/20 bg-[linear-gradient(135deg,#6d28d9,#8b5cf6)] text-white shadow-md transition-all hover:brightness-110 hover:shadow-primary/25">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ensure OAuth2 login URL works in relative-path fallback

The login button always navigates to buildBackendUrl('/oauth2/authorization/google'); when NEXT_PUBLIC_BACKEND_ORIGIN is unset (a mode the config explicitly supports), this becomes a relative path on the Next.js app. Since rewrites only proxy /api/:path*, this OAuth2 start route is not forwarded to Spring and can 404, blocking social login in that fallback deployment mode.

Useful? React with 👍 / 👎.

@jin2304 jin2304 changed the title Feat/sw 65 Feat/sw 65 - 세션 기반 인증에서 JWT 기반 인증 전환 Apr 2, 2026
@jin2304
Copy link
Copy Markdown
Member Author

jin2304 commented Apr 2, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8d1dd7c2d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Override
public AuthResponses.TokenPair refresh(String refreshToken) {
// 1. 토큰 서명 및 유효성 검증
jwtUtils.validateToken(refreshToken);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Map refresh JWT parse failures to auth errors

refresh() calls jwtUtils.validateToken(refreshToken) directly, but ExpiredJwtException/JwtException from that call are not translated into BusinessException. In those common cases (expired or malformed refresh cookie), the exception falls through to the global catch-all handler and returns HTTP 500 instead of an auth 401, so clients mis-handle normal token expiry as a server fault and skip the intended re-login path.

Useful? React with 👍 / 👎.

jin2304 and others added 2 commits April 2, 2026 21:49
JWT 검증 예외(ExpiredJwtException, JwtException)를 catch하여
HTTP 401 Unauthorized로 응답하도록 수정.
기존에는 예외가 처리되지 않아 HTTP 500 Internal Server Error를 반환했으므로,
프론트엔드의 인증 스토어가 일시적 서버 오류로 잘못 인식하고
재로그인 흐름이 깨지는 문제가 있었음.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
/oauth2 경로를 백엔드로 명시적으로 리라이트하도록 next.config.ts에 추가.
NEXT_PUBLIC_BACKEND_ORIGIN 환경 변수가 미설정된 경우에도 상대 경로가 올바르게 포워딩됨.
이를 통해 배포 환경에서 OAuth2 소셜 로그인이 정상 작동함.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@jin2304
Copy link
Copy Markdown
Member Author

jin2304 commented Apr 2, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d6f62b5da

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (isAuthorizedRedirectUri(url)) {
return url;
}
throw AuthException.of(AuthErrorCode.AUTH_INVALID_REDIRECT_URI);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Route invalid redirect URIs through failure handling

When redirect_uri is present but fails validation, determineTargetUrl throws AuthException from inside onAuthenticationSuccess, and this path does not catch runtime exceptions before returning control to Spring Security. In practice, a stale or crafted redirect_uri cookie can turn an otherwise successful OAuth provider login into a server error response (instead of a controlled auth failure redirect), breaking the login flow for affected users.

Useful? React with 👍 / 👎.

},
{
source: '/oauth2/:path*',
destination: 'http://localhost:8080/oauth2/:path*',
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Parameterize OAuth rewrites instead of localhost

The new OAuth rewrites are hardcoded to http://localhost:8080, which makes these routes environment-specific. This conflicts with the PR’s own production fallback mode (frontend/src/lib/config/backend.ts) and causes social-login routes to fail when Next.js is deployed separately from a local backend process. These rewrite destinations should be environment-driven or limited to local development only.

Useful? React with 👍 / 👎.

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

Caution

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

⚠️ Outside diff range comments (4)
frontend/src/components/dialogs/CreateFolderDialog.tsx (1)

31-42: ⚠️ Potential issue | 🟡 Minor

인증 정보가 없을 때 생성 액션이 침묵 실패합니다.

핸들러는 memberId가 없으면 바로 반환하지만, UI는 그 상태를 비활성화나 안내로 드러내지 않아 사용자가 클릭해도 아무 반응이 없습니다. memberId == null을 동일하게 반영하고, 필요하면 로그인 안내를 붙여 주세요.

💡 제안 수정
-  const handleCreateFolder = () => {
-    // 유효성 검사: 이름이 비어있으면 중단
-    if (!folderName.trim() || !memberId) return;
+  const handleCreateFolder = () => {
+    // 유효성 검사: 이름이 비어있거나 인증 정보가 없으면 중단
+    if (!folderName.trim() || memberId == null) return;
-            disabled={!folderName.trim() || createFolderMutation.isPending} // 이름이 없거나 생성 중이면 비활성화
+            disabled={!folderName.trim() || memberId == null || createFolderMutation.isPending} // 이름이 없거나 인증 정보가 없거나 생성 중이면 비활성화
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/dialogs/CreateFolderDialog.tsx` around lines 31 - 42,
The handler currently returns silently when memberId is missing (in
handleCreateFolder using memberId from useAuthStore), causing no UI feedback;
update handleCreateFolder to surface the condition by either showing a login
prompt/toast/modal (call your existing openLoginModal or dispatch a login-needed
toast) before returning, and also update the Create button's disabled/tooltip
state to depend on memberId (tie disabled prop to !memberId and show a message
like "Please log in to create a folder"); ensure createFolderMutation and
folderName validation remain intact so you only add explicit user feedback when
memberId is null.
frontend/src/components/dialogs/SaveLinkDialog.tsx (1)

236-257: 🧹 Nitpick | 🔵 Trivial

memberId 누락 시 사용자 피드백 추가를 권장합니다.

Line 239에서 memberId가 없으면 조용히 반환되어 사용자가 저장 실패 원인을 알 수 없습니다. 토스트나 에러 메시지로 인증이 필요함을 알려주는 것이 좋습니다.

💡 사용자 피드백 추가 제안
   const handleSave = () => {
     const trimmedUrl = url.trim();
     if (!trimmedUrl || !(trimmedUrl.startsWith('http://') || trimmedUrl.startsWith('https://'))) return;
-    if (!memberId) return; // 방어 코드: 실제 memberId가 없는 경우 실행 방지
+    if (!memberId) {
+      // TODO: 토스트 또는 알림으로 로그인 필요 안내
+      console.warn('memberId가 없어 저장할 수 없습니다. 로그인이 필요합니다.');
+      return;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/dialogs/SaveLinkDialog.tsx` around lines 236 - 257,
The handleSave function silently returns when memberId is missing; change it to
show user feedback before returning by invoking the app's UI notification (e.g.,
toast.error or setError) with a clear message like "Authentication required to
save bookmarks" and then exit; update the guard in handleSave (the `if
(!memberId) return;` branch) to display the notification and abort, and ensure
this covers both the createFolderMutation and saveBookmark paths so users know
why save failed.
src/main/java/com/web/SearchWeb/aop/OwnerCheckAspect.java (1)

82-88: ⚠️ Potential issue | 🔴 Critical

boardService 사용처가 여전히 존재하여 런타임 에러가 발생합니다.

BoardController.java의 114, 125번 줄에서 @OwnerCheck(service = "boardService")가 여전히 사용 중입니다. 현재 OwnerCheckAspect의 스위치 문에는 memberService만 처리되고 나머지는 IllegalArgumentException을 던지므로, 이 코드는 요청 시 500 에러를 발생시킵니다. 머지 전에 BoardController@OwnerCheck 애너테이션을 제거하거나 OwnerCheckAspect에서 boardService 케이스를 복구하십시오.

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

In `@src/main/java/com/web/SearchWeb/aop/OwnerCheckAspect.java` around lines 82 -
88, OwnerCheckAspect currently only handles "memberService" in its switch and
throws on other values, but BoardController still uses `@OwnerCheck`(service =
"boardService"), causing runtime 500s; fix by restoring the original
"boardService" branch in OwnerCheckAspect to resolve the board owner lookup
(call the same lookup used before, e.g., invoke
boardService.findMemberIdByBoardId(targetId)) or alternatively update
BoardController to remove/replace the `@OwnerCheck`(service = "boardService")
annotations—locate OwnerCheckAspect and the switch over service, and either
re-add the case for "boardService" (and similarly "commentService" if needed,
referencing commentService.findMemberIdByCommentId) or remove the annotation
usages in BoardController to keep behavior consistent.
src/main/java/com/web/SearchWeb/folder/controller/MemberFolderController.java (1)

53-73: 🧹 Nitpick | 🔵 Trivial

ownerMemberId는 이제 URL에서 중복 상태입니다.

Line 53-73의 두 엔드포인트는 @CurrentMemberId와 서비스 검증 때문에 결국 현재 로그인한 사용자 자신의 폴더만 조회할 수 있습니다. 지금처럼 ownerMemberId를 경로에 남겨 두면 클라이언트가 같은 값을 URL과 토큰 양쪽에서 관리해야 해서 403만 늘리니, 인증 주체 기반 경로로 단순화하는 편이 API 계약이 더 명확합니다.

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

In
`@src/main/java/com/web/SearchWeb/folder/controller/MemberFolderController.java`
around lines 53 - 73, Both listRoot and listChildren expose a redundant
ownerMemberId path variable while authorization forces the current authenticated
user (via `@CurrentMemberId`) to be the effective owner; remove ownerMemberId from
the URL mappings and method signatures in MemberFolderController (update the
`@GetMapping` values for listRoot and listChildren to no longer include
/owners/{ownerMemberId} and remove the `@PathVariable` Long ownerMemberId
parameter), and call memberFolderService.listRootFolders(memberId, memberId) or
memberFolderService.listChildren(memberId, memberId, parentFolderId) (i.e., pass
the current memberId as the owner) so the service receives the authenticated
user only and the route reflects that.
♻️ Duplicate comments (8)
.gitignore (1)

54-58: ⚠️ Potential issue | 🟡 Minor

임시 파일 ignore 패턴을 루트 기준으로 고정해 주세요.

Line 55~58은 파일명/경로만으로 작성되어 하위 디렉터리의 동명 파일까지 함께 무시됩니다. 루트 산출물만 대상으로 하려면 / 앵커를 붙이는 편이 안전합니다.

제안 수정
-.gradle-user/
-status.txt
-status_utf8.txt
-docs/Backend/memo/
+/.gradle-user/
+/status.txt
+/status_utf8.txt
+/docs/Backend/memo/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore around lines 54 - 58, The .gitignore entries "status.txt",
"status_utf8.txt" and "docs/Backend/memo/" are unanchored and will ignore
matching names in subdirectories; update these patterns to be root-anchored by
prefixing them with a slash (e.g., change "status.txt" -> "/status.txt",
"status_utf8.txt" -> "/status_utf8.txt", and "docs/Backend/memo/" ->
"/docs/Backend/memo/") so only the repository root artifacts are ignored; apply
the same anchoring to any other intended root-only patterns (such as
".gradle-user/" if needed).
src/main/java/com/web/SearchWeb/config/security/HttpCookieOAuth2AuthorizationRequestRepository.java (1)

43-46: 🧹 Nitpick | 🔵 Trivial

redirect_uri는 쿠키에 넣기 전에 먼저 검증해 주세요.

최종 사용 시점에 다시 검증하더라도, 여기서 무효한 값을 저장해 두면 로그인 완료 단계에서야 실패해서 원인 추적이 어려워집니다. OAuth2SuccessHandler와 동일한 허용 규칙을 공용 메서드로 빼서 저장 전에도 재사용하는 편이 좋겠습니다.

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

In
`@src/main/java/com/web/SearchWeb/config/security/HttpCookieOAuth2AuthorizationRequestRepository.java`
around lines 43 - 46, Validate the redirect_uri before storing it in the cookie:
in HttpCookieOAuth2AuthorizationRequestRepository, before calling
CookieUtils.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME, ...), run the
same whitelist/validation logic used in OAuth2SuccessHandler (extract that logic
into a shared utility method, e.g., isValidRedirectUri(String uri) in a common
class) and only add the cookie when isValidRedirectUri(redirectUriAfterLogin)
returns true; reference REDIRECT_URI_PARAM_COOKIE_NAME, COOKIE_EXPIRE_SECONDS
and cookieSecure so the cookie creation remains unchanged when validation
passes.
src/main/java/com/web/SearchWeb/board/dao/MybatisBoardDao.java (1)

1-139: 🧹 Nitpick | 🔵 Trivial

주석 처리된 DAO 파일은 저장소에서 제거해 주세요.

이 파일은 컴파일에도 참여하지 않는 데드 코드만 남겨서 검색·리팩토링 비용만 키웁니다. 과거 구현은 Git 히스토리로 복구할 수 있으니 워킹트리에서는 삭제하거나 별도 아카이브로 분리하는 편이 낫습니다.

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

In `@src/main/java/com/web/SearchWeb/board/dao/MybatisBoardDao.java` around lines
1 - 139, Remove the dead, fully-commented DAO class MybatisBoardDao from the
working tree (or move it to an archive folder) to avoid clutter and unused code;
this affects the class MybatisBoardDao and its methods (insertBoard,
selectBoardPage, countBoardList, selectBoardListByMemberId, selectBoard,
updateBoard, updateBoardProfile, deleteBoard, updateBookmarkCount,
incrementViewCount, incrementLikeCount, decrementLikeCount,
incrementCommentCount, decrementCommentCount) — keep history in Git if you need
to restore it, and ensure no other code references MybatisBoardDao before
deleting.
src/main/java/com/web/SearchWeb/config/jwt/JwtMemberPrincipal.java (1)

11-20: ⚠️ Potential issue | 🟡 Minor

Principal 생성 시 null 방어를 추가하세요.

String.valueOf(memberId)memberId == null일 때 문자열 "null"을 반환합니다. 인증 식별자 객체에서는 생성 시점에 검증하는 것이 안전합니다.

🔧 제안 수정안
 import java.security.Principal;
+import java.util.Objects;
 
 public record JwtMemberPrincipal(Long memberId, String role) implements Principal {
+    public JwtMemberPrincipal {
+        Objects.requireNonNull(memberId, "memberId must not be null");
+        Objects.requireNonNull(role, "role must not be null");
+    }
     
     `@Override`
     public String getName() {
-        return String.valueOf(memberId);
+        return memberId.toString();
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/web/SearchWeb/config/jwt/JwtMemberPrincipal.java` around
lines 11 - 20, JwtMemberPrincipal currently allows memberId to be null
(String.valueOf(memberId) yields "null"); add null checks in the record's
constructor so the principal cannot be created with a null memberId (and
optionally null role). Implement a compact/canonical constructor for
JwtMemberPrincipal that uses Objects.requireNonNull(memberId, "memberId must not
be null") (and requireNonNull for role if desired) or throws
IllegalArgumentException, so getName() can safely return
String.valueOf(memberId) without producing "null".
frontend/src/components/layout/Sidebar.tsx (1)

52-58: 🧹 Nitpick | 🔵 Trivial

try/finally 패턴이 잘 적용되었습니다.

이전 리뷰 피드백대로 logout() 실패 시에도 /login으로 리다이렉트가 보장됩니다. 다만 로그아웃 후 뒤로가기로 인증 화면 재진입을 방지하려면 router.replace를 고려해볼 수 있습니다.

선택적 개선 제안
             } finally {
-              router.push('/login');
+              router.replace('/login');
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/layout/Sidebar.tsx` around lines 52 - 58, In the
Sidebar component's onClick handler, keep the existing try/finally around
logout() but replace router.push('/login') with router.replace('/login') so the
navigation replaces history entry (preventing back-button re-entry to the
authenticated view); locate the anonymous async onClick that calls logout() and
router.push and change the push call to replace while preserving the finally
block.
src/main/java/com/web/SearchWeb/board/controller/BoardController.java (1)

1-190: 🛠️ Refactor suggestion | 🟠 Major

주석 처리된 레거시 컨트롤러는 파일째 삭제하세요.

Git 히스토리로 복구할 수 있는 코드를 저장소에 주석 상태로 남겨두면 검색·리팩터링 노이즈만 늘어납니다.

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

In `@src/main/java/com/web/SearchWeb/board/controller/BoardController.java` around
lines 1 - 190, Remove the entire legacy, commented-out BoardController class
(named BoardController) from the repository instead of leaving it commented;
delete this file and ensure any references to its beans or imports
(BoardService, MemberService, LikesService, BookmarkService, JwtMemberPrincipal,
OwnerCheck annotation usages) are not required elsewhere or are resolved by
updating configuration/test code accordingly so the project still builds and
tests pass.
frontend/src/lib/api/fetchClient.ts (1)

104-108: ⚠️ Potential issue | 🟠 Major

빈 응답 경로가 타입 계약을 깨고 있습니다.

여기서는 실제로 undefined를 반환하면서 시그니처는 계속 T를 약속합니다. 204/빈 바디 응답을 쓰는 호출부가 결과를 값으로 다루기 시작하면 런타임 오류로 이어질 수 있으니, Promise<T | undefined>로 승격하거나 void 전용 helper로 분리하는 편이 안전합니다.

🛠️ 타입 계약 정리 예시
 export async function fetchClient<T>(
   url: string,
   options?: RequestInit,
-): Promise<T> {
+): Promise<T | undefined> {
@@
-async function parseResponse<T>(response: Response): Promise<T> {
+async function parseResponse<T>(response: Response): Promise<T | undefined> {
@@
   const text = await response.text();
   if (!text) {
-    return undefined as unknown as T;
+    return undefined;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/api/fetchClient.ts` around lines 104 - 108, The code path
that returns undefined on empty/204 responses breaks the function's declared
contract of returning T; update the function signature to return Promise<T |
undefined> (or create a dedicated void-return helper) and adjust all
callers/types accordingly so they handle undefined; locate the block that calls
response.text() in frontend/src/lib/api/fetchClient.ts (the exported fetch
helper function that wraps fetch/response handling), change its return type to
include undefined (or implement a separate fetchVoid helper that returns
Promise<void | undefined>), and update related type annotations and call sites
to handle the new union result.
docs/Backend/03. jwt-oauth2-test-scenarios.md (1)

28-35: ⚠️ Potential issue | 🟡 Minor

A004 설명에서 만료를 빼야 합니다.

같은 문서의 AUTH-005/AUTH-009와 구현 흐름상 Refresh Token 만료는 A002로 처리되고, A004는 쿠키 부재·DB 미존재·재사용 초과 케이스입니다. 지금 표대로면 QA가 401 원인을 잘못 기대하게 됩니다.

📝 제안 수정
-| A004 | 401 | Refresh Token이 없습니다 | AuthController, AuthServiceImpl (토큰 미존재/만료/재사용 초과) |
+| A004 | 401 | Refresh Token이 없습니다 | AuthController, AuthServiceImpl (쿠키 부재/DB 미존재/재사용 초과) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/Backend/03`. jwt-oauth2-test-scenarios.md around lines 28 - 35, Update
the A004 row to remove "만료" from its message/description and clarify its scope
to only cover missing refresh token scenarios (cookie absent, DB record not
found, or refresh-token reuse exceeded) so it matches the implemented flow in
AuthController/AuthServiceImpl; ensure the table entry for A004 no longer
mentions expiration and add a short parenthetical or list that explicitly
references "쿠키 부재·DB 미존재·재사용 초과" and confirm that refresh-token expiration
remains described by A002 (JwtAuthenticationFilter / ExpiredJwtException) to
avoid QA confusion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/Backend/02`. jwt-oauth2-architecture.md:
- Around line 893-897: 문서에 표기된 리소스 경로가 레포 루트 기준으로는 열 수 없어 혼동을 줄 수 있으니, 현재 나열된
resources/mapper/refresh-token-mapper.xml,
resources/sql/V001__create_refresh_token.sql,
resources/application-{profile}.properties 항목들을 실제 저장소 경로인
src/main/resources/mapper/refresh-token-mapper.xml,
src/main/resources/sql/V001__create_refresh_token.sql,
src/main/resources/application-{profile}.properties 형태로 수정해 문서 내 모든 경로를 레포 루트에서
바로 열 수 있는 경로로 통일하세요.
- Around line 275-278: The Markdown sections around "401 감지 (`response.status
=== 401`)" and "AT 존재 조건 (`&& accessToken`)" (and the other listed ranges)
violate lint rules MD009, MD031/MD040 and MD022; run markdownlint or your
configured linter, remove trailing spaces, ensure a single blank line before and
after headings, and add/adjust blank lines around fenced code blocks (and
include language tags if required) so those rules pass—specifically edit the
heading blocks and fenced/code-adjacent lines referenced by the strings
"response.status === 401" and "&& accessToken" to eliminate trailing spaces and
restore required blank lines.

In `@docs/Backend/03`. jwt-oauth2-test-scenarios.md:
- Around line 365-368: 문서 내 Grace Period 허용 횟수가 서로 달라 혼란스러우니 S4-02, AUTH-003,
AUTH-011(및 동일 내용이 반복된 블록들: 해당 문서의 574-584, 706-711 범위)을 하나의 규칙으로 통일하세요; 예를 들어
"10초 동안 동일한 RT-B를 무제한 멱등 응답" 또는 "유예기간 동안 최대 N회(예: 2회) 재시도 허용" 중 하나를 결정하고, 선택한
규칙으로 S4-02의 설명과 AUTH-003, AUTH-011의 실패/성공 시나리오 문구를 모두 동일하게 바꿔 일관성 있게 업데이트하며 관련
테스트 기대값(성공/실패 시점)도 함께 맞춰 수정하세요.
- Around line 54-55: The document uses two different .http filenames
(src/test/api-test/auth-test.http vs jwt-oauth2-e2e.http); pick the correct test
file name (use jwt-oauth2-e2e.http) and update every mention in docs/Backend/03.
jwt-oauth2-test-scenarios.md so all references are consistent, including the
header link and the later occurrence around the other reference; ensure the
displayed link text and the target filename match exactly.

In `@frontend/src/app/auth/callback/page.tsx`:
- Around line 7-11: 주석과 실제 동작이 불일치합니다: 현재 callback page 내에서 initialize()를 직접
호출하고 있으므로 주석에서 "initialize()는 AuthProvider가 단독 호출"이라고 적혀 있는 부분을 삭제하거나 수정해 실제 동작을
반영하세요; 구체적으로 page.tsx의 설명 문구를 "이 페이지에서 initialize()를 호출하여 인증 결과를 처리" 또는
"AuthProvider가 아닌 이 컴포넌트에서 initialize()를 호출" 등으로 바꾸어 initialize(), AuthProvider,
그리고 인증 성공/실패 분기 로직을 정확히 설명하도록 업데이트하세요.

In `@frontend/src/app/my-links/page.tsx`:
- Around line 14-16: The page currently skips loading/empty states when memberId
is not yet initialized because useFolders is only called with memberId and there
is no branch for the "auth initializing" case; update the component to detect
auth initialization (e.g., read an auth init flag from useAuthStore such as
isInitializing or isAuthReady) and when memberId is null/undefined while auth is
initializing render a loading/skeleton UI instead of the empty folder area, or
alternatively ensure useFolders gracefully handles undefined memberId and
exposes a distinct "loading due to auth" state; locate and update the code
around memberId, useAuthStore, useFolders, and the existing isLoading/error
handling to add this early-auth-init branch.

In `@frontend/src/components/providers/QueryProvider.tsx`:
- Around line 15-25: Replace the blocking alert in the QueryProvider's
mutations.onError with a non-blocking toast: import and use your chosen toast
API (e.g., react-hot-toast's toast.error or sonner's toast) inside the onError
handler instead of alert, keep the existing development-only console.error
behavior, ensure the global toast container/toaster component is mounted in the
app root (e.g., Toaster or ToastContainer), and update any types/imports in
QueryProvider.tsx accordingly so the handler calls toast.error with a
user-friendly message.

In `@src/main/java/com/web/SearchWeb/auth/controller/AuthController.java`:
- Around line 91-99: The logout flow currently calls
authService.logout(refreshToken) and only then returns the cleared cookie; make
the handler idempotent by catching and ignoring expected auth/validation
exceptions from authService.logout (e.g., token expired, token not found,
invalid token) so the method always proceeds to createRefreshTokenCookie("", 0)
and set the "Set-Cookie" header regardless of logout outcome; keep unexpected
exceptions propagated but ensure known authentication exceptions are swallowed
or logged at debug level before building the ResponseEntity in AuthController so
browsers never retain a stale refresh cookie.

In `@src/main/java/com/web/SearchWeb/auth/error/AuthException.java`:
- Around line 13-18: Change the AuthException constructor and factory to accept
AuthErrorCode instead of the generic ErrorCode to preserve type safety: update
the private AuthException(ErrorCode errorCode) and public static AuthException
of(ErrorCode errorCode) signatures to use AuthErrorCode, adjust any internal
storage/use to match the narrower type, and update callers to pass an
AuthErrorCode (or map/convert other ErrorCode values where appropriate); ensure
imports/reference to AuthErrorCode are added where needed.

In `@src/main/java/com/web/SearchWeb/auth/service/AuthServiceImpl.java`:
- Around line 47-50: The current issueRefreshToken(Long memberId) flow can
produce two active refresh tokens under concurrent OAuth callbacks because
deleteByMemberId() and the subsequent insert happen in separate transactions; to
fix, serialize concurrent executions for the same member by acquiring a DB lock
or using a uniqueness constraint with retry: either (A) obtain a FOR UPDATE lock
on the member row before calling refreshTokenDao.deleteByMemberId() (e.g., add a
MemberRepository method like lockByIdForUpdate(memberId) and call it at the
start of issueRefreshToken), or (B) add a unique constraint on (member_id,
active) or similar so only one active token can exist and wrap
createAndSaveNewSessionRefreshToken(memberId) in a retry loop that catches the
duplicate-key exception and retries the delete+insert; pick one approach and
implement the corresponding repository/constraint and retry/error handling
around issueRefreshToken.
- Around line 64-72: The validateToken call in AuthServiceImpl currently only
catches ExpiredJwtException and JwtException, so IllegalArgumentException thrown
by jwtUtils.validateToken can bubble up; add a catch for
IllegalArgumentException in the same try/catch block and convert it to an
AuthException via AuthException.of(...) (map it to
AuthErrorCode.AUTH_INVALID_TOKEN or the appropriate code) while logging
similarly to the JwtException branch so token parsing errors return a proper
auth error instead of a 500.

In
`@src/main/java/com/web/SearchWeb/auth/service/RefreshTokenCleanupScheduler.java`:
- Around line 23-27: In RefreshTokenCleanupScheduler.cleanupExpiredTokens, wrap
the call to refreshTokenDao.deleteExpired() in try/catch to prevent exceptions
from propagating to the scheduler thread; update refreshTokenDao.deleteExpired()
to return the number of deleted rows (int) and after a successful call log that
deleted count (e.g. "[Scheduler] 만료된 Refresh Token 정리 완료, deletedCount=..."); in
the catch block log the exception with an error level and a clear message
(including the exception) so failures (DB connection/query errors) are
observable but do not crash the scheduler.

In `@src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java`:
- Around line 5-6: Remove the commented-out legacy API declarations from the
BookmarkService interface: delete the commented import lines for
BoardBookmarkCheckDto and BookmarkDto and any commented method signatures (the
commented block around lines 39-53) so the interface contains only active
contracts; if you need to preserve them, move the old signatures and DTO
references into a docs/backup file or an archival directory, but do not leave
stale commented code inside the BookmarkService interface.

In `@src/main/java/com/web/SearchWeb/config/jwt/JwtAuthenticationEntryPoint.java`:
- Around line 35-40: In JwtAuthenticationEntryPoint, avoid directly casting
request.getAttribute("exception") to ErrorCode; first check the attribute with
instanceof and only cast when it's an instance of ErrorCode, otherwise fall back
to AuthErrorCode.AUTH_UNAUTHORIZED; also replace the literal "exception" key
with a shared constant (e.g., EXCEPTION_ATTRIBUTE) used by the filter that sets
the attribute to prevent cross-component key collisions.

In `@src/main/java/com/web/SearchWeb/config/jwt/JwtUtils.java`:
- Around line 73-75: The code currently parses the same JWT twice because
validateToken() calls parseClaims() and parseAccessToken() also calls
parseClaims(); update the API so parsing+validation happens only once: either
change validateToken() to return the Claims (e.g., validateAndGetClaims or have
validateToken return Optional<Claims>/Claims) or add a new method
parseAndValidateClaims(String token) that both the authentication filter and
parseAccessToken() use; then modify parseAccessToken(String token) to accept a
Claims parameter (or call the single parseAndValidateClaims method) so signature
references JwtUtils.parseAccessToken, JwtUtils.validateToken,
JwtUtils.parseClaims (or new parseAndValidateClaims) are updated consistently to
avoid duplicate parsing.

In `@src/main/java/com/web/SearchWeb/config/jwt/OAuth2SuccessHandler.java`:
- Around line 95-110: The handler currently returns a relative redirect_uri
directly (in determineTargetUrl), which causes sendRedirect to resolve against
the backend origin; update determineTargetUrl so that when a redirect URI is
present and is a relative path, you first resolve it against the origin of
oauth2RedirectUri (e.g., build an absolute URI by applying the path/query to the
oauth2RedirectUri base), then run the existing isAuthorizedRedirectUri
validation on that absolute URL and return the normalized absolute URL; ensure
this logic references
HttpCookieOAuth2AuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME,
CookieUtils, determineTargetUrl, isAuthorizedRedirectUri and oauth2RedirectUri
so both the main branch (lines ~95-110) and the similar block at ~116-124 are
changed.

In `@src/main/java/com/web/SearchWeb/config/security/SecurityConfig.java`:
- Around line 84-87: SecurityConfig currently sets
authenticationEntryPoint(jwtAuthenticationEntryPoint) for 401 responses but
lacks an accessDeniedHandler for 403s; add an AccessDeniedHandler that returns
the same ApiResponse JSON shape and register it in the HttpSecurity
exceptionHandling chain (e.g., exceptionHandling(ex ->
ex.authenticationEntryPoint(jwtAuthenticationEntryPoint).accessDeniedHandler(yourAccessDeniedHandler))).
Implement or reuse a class implementing
org.springframework.security.web.access.AccessDeniedHandler (e.g.,
JwtAccessDeniedHandler or CustomAccessDeniedHandler) to produce the ApiResponse
JSON and ensure it is declared as a bean and referenced from SecurityConfig.

In `@src/main/java/com/web/SearchWeb/config/security/SecurityUtils.java`:
- Around line 64-66: The hashToken method currently throws
AuthException.of(AuthErrorCode.AUTH_INTERNAL_ERROR) for null or blank input,
which misclassifies client-side missing/invalid tokens as server errors; change
that branch to throw an appropriate 4xx auth error (e.g.,
AuthErrorCode.MISSING_TOKEN or INVALID_TOKEN) by creating/throwing
AuthException.of(...) with the correct error code so null/blank inputs from
hashToken are reported as client authentication errors rather than
AUTH_INTERNAL_ERROR; update any callers/tests if they rely on the old behavior.

In `@src/main/java/com/web/SearchWeb/member/dto/CustomOAuth2Member.java`:
- Around line 54-60: getAuthorities() in CustomOAuth2Member currently builds an
ArrayList and adds a lambda; simplify it to return a single-element immutable
collection instead (e.g., return Collections.singleton((GrantedAuthority) () ->
role) or preferably return List.of(new SimpleGrantedAuthority(role))) by
replacing the current body in CustomOAuth2Member.getAuthorities; if using
SimpleGrantedAuthority, add the import for
org.springframework.security.core.authority.SimpleGrantedAuthority.

In
`@src/main/java/com/web/SearchWeb/member/service/CustomOAuth2MemberService.java`:
- Around line 64-65: The code in CustomOAuth2MemberService sets
socialName/socialEmail to defaults and unconditionally writes them back, which
overwrites existing profile data when providers omit fields; change the update
logic so that when an existing Member is being synchronized you only overwrite
memberName/email if oAuth2Response.getName()/getEmail() are non-null and
non-empty (use the socialName/socialEmail variables only when creating a new
Member), and apply the same conditional-update rule to the other affected blocks
(lines around 97-99 and 112-120) so only provider-returned fields are used to
update existing Member records.

In `@src/main/resources/db/init_postgres.sql`:
- Around line 79-93: Add a unique constraint on (session_id, version) in the
"refresh_token" table to prevent duplicate versions per session which breaks
findBySessionIdAndVersion and grace-period successor logic; update the CREATE
TABLE "refresh_token" statement to include a UNIQUE constraint (e.g.
uq_refresh_token_session_version) on ("session_id", "version") or add an ALTER
TABLE ... ADD CONSTRAINT ... UNIQUE clause targeting the refresh_token table so
the DB enforces one version per session.
- Line 564: 인덱스 중복 문제: CREATE INDEX IF NOT EXISTS idx_refresh_token_hash ON
"refresh_token" ("token_hash"); 는 이미 동일 컬럼에 대해 존재하는 제약/인덱스인
uq_refresh_token_hash와 중복되므로 삭제하세요 — 즉, 제거하거나 주석 처리하여 중복 인덱스 생성을 막고 refresh 토큰
회전/삭제 시 불필요한 쓰기 비용을 없애십시오 (참조 대상: idx_refresh_token_hash, uq_refresh_token_hash,
테이블 refresh_token, 컬럼 token_hash).

In `@src/test/api-test/auth-test.http`:
- Around line 19-20: The committed file contains a real refresh token in the
variable `@refreshTokenCookie`; remove the secret immediately, replace it with a
non-sensitive placeholder (e.g., EMPTY_REFRESH_TOKEN or a note to use a local
env var), and update tests to read the token from a local-only environment
variable or secure secret store instead of the hardcoded value; also
revoke/rotate the exposed token before merging to remove live credentials from
use.
- Around line 49-57: The test currently sets `@accessToken` from the sync response
but never updates `@refreshTokenCookie`, so subsequent requests keep using the old
cookie; update the POST {{baseUrl}}/api/auth/refresh "sync" request to parse the
Set-Cookie response header and assign the new refreshToken value back into the
`@refreshTokenCookie` variable (i.e., add a response script that extracts
refreshToken from Set-Cookie and updates the variable), or alternatively remove
the explicit "Cookie: refreshToken={{refreshTokenCookie}}" header to let the
HTTP client cookie jar handle rotation automatically; modify the "sync" request
in src/test/api-test/auth-test.http accordingly.

---

Outside diff comments:
In `@frontend/src/components/dialogs/CreateFolderDialog.tsx`:
- Around line 31-42: The handler currently returns silently when memberId is
missing (in handleCreateFolder using memberId from useAuthStore), causing no UI
feedback; update handleCreateFolder to surface the condition by either showing a
login prompt/toast/modal (call your existing openLoginModal or dispatch a
login-needed toast) before returning, and also update the Create button's
disabled/tooltip state to depend on memberId (tie disabled prop to !memberId and
show a message like "Please log in to create a folder"); ensure
createFolderMutation and folderName validation remain intact so you only add
explicit user feedback when memberId is null.

In `@frontend/src/components/dialogs/SaveLinkDialog.tsx`:
- Around line 236-257: The handleSave function silently returns when memberId is
missing; change it to show user feedback before returning by invoking the app's
UI notification (e.g., toast.error or setError) with a clear message like
"Authentication required to save bookmarks" and then exit; update the guard in
handleSave (the `if (!memberId) return;` branch) to display the notification and
abort, and ensure this covers both the createFolderMutation and saveBookmark
paths so users know why save failed.

In `@src/main/java/com/web/SearchWeb/aop/OwnerCheckAspect.java`:
- Around line 82-88: OwnerCheckAspect currently only handles "memberService" in
its switch and throws on other values, but BoardController still uses
`@OwnerCheck`(service = "boardService"), causing runtime 500s; fix by restoring
the original "boardService" branch in OwnerCheckAspect to resolve the board
owner lookup (call the same lookup used before, e.g., invoke
boardService.findMemberIdByBoardId(targetId)) or alternatively update
BoardController to remove/replace the `@OwnerCheck`(service = "boardService")
annotations—locate OwnerCheckAspect and the switch over service, and either
re-add the case for "boardService" (and similarly "commentService" if needed,
referencing commentService.findMemberIdByCommentId) or remove the annotation
usages in BoardController to keep behavior consistent.

In
`@src/main/java/com/web/SearchWeb/folder/controller/MemberFolderController.java`:
- Around line 53-73: Both listRoot and listChildren expose a redundant
ownerMemberId path variable while authorization forces the current authenticated
user (via `@CurrentMemberId`) to be the effective owner; remove ownerMemberId from
the URL mappings and method signatures in MemberFolderController (update the
`@GetMapping` values for listRoot and listChildren to no longer include
/owners/{ownerMemberId} and remove the `@PathVariable` Long ownerMemberId
parameter), and call memberFolderService.listRootFolders(memberId, memberId) or
memberFolderService.listChildren(memberId, memberId, parentFolderId) (i.e., pass
the current memberId as the owner) so the service receives the authenticated
user only and the route reflects that.

---

Duplicate comments:
In @.gitignore:
- Around line 54-58: The .gitignore entries "status.txt", "status_utf8.txt" and
"docs/Backend/memo/" are unanchored and will ignore matching names in
subdirectories; update these patterns to be root-anchored by prefixing them with
a slash (e.g., change "status.txt" -> "/status.txt", "status_utf8.txt" ->
"/status_utf8.txt", and "docs/Backend/memo/" -> "/docs/Backend/memo/") so only
the repository root artifacts are ignored; apply the same anchoring to any other
intended root-only patterns (such as ".gradle-user/" if needed).

In `@docs/Backend/03`. jwt-oauth2-test-scenarios.md:
- Around line 28-35: Update the A004 row to remove "만료" from its
message/description and clarify its scope to only cover missing refresh token
scenarios (cookie absent, DB record not found, or refresh-token reuse exceeded)
so it matches the implemented flow in AuthController/AuthServiceImpl; ensure the
table entry for A004 no longer mentions expiration and add a short parenthetical
or list that explicitly references "쿠키 부재·DB 미존재·재사용 초과" and confirm that
refresh-token expiration remains described by A002 (JwtAuthenticationFilter /
ExpiredJwtException) to avoid QA confusion.

In `@frontend/src/components/layout/Sidebar.tsx`:
- Around line 52-58: In the Sidebar component's onClick handler, keep the
existing try/finally around logout() but replace router.push('/login') with
router.replace('/login') so the navigation replaces history entry (preventing
back-button re-entry to the authenticated view); locate the anonymous async
onClick that calls logout() and router.push and change the push call to replace
while preserving the finally block.

In `@frontend/src/lib/api/fetchClient.ts`:
- Around line 104-108: The code path that returns undefined on empty/204
responses breaks the function's declared contract of returning T; update the
function signature to return Promise<T | undefined> (or create a dedicated
void-return helper) and adjust all callers/types accordingly so they handle
undefined; locate the block that calls response.text() in
frontend/src/lib/api/fetchClient.ts (the exported fetch helper function that
wraps fetch/response handling), change its return type to include undefined (or
implement a separate fetchVoid helper that returns Promise<void | undefined>),
and update related type annotations and call sites to handle the new union
result.

In `@src/main/java/com/web/SearchWeb/board/controller/BoardController.java`:
- Around line 1-190: Remove the entire legacy, commented-out BoardController
class (named BoardController) from the repository instead of leaving it
commented; delete this file and ensure any references to its beans or imports
(BoardService, MemberService, LikesService, BookmarkService, JwtMemberPrincipal,
OwnerCheck annotation usages) are not required elsewhere or are resolved by
updating configuration/test code accordingly so the project still builds and
tests pass.

In `@src/main/java/com/web/SearchWeb/board/dao/MybatisBoardDao.java`:
- Around line 1-139: Remove the dead, fully-commented DAO class MybatisBoardDao
from the working tree (or move it to an archive folder) to avoid clutter and
unused code; this affects the class MybatisBoardDao and its methods
(insertBoard, selectBoardPage, countBoardList, selectBoardListByMemberId,
selectBoard, updateBoard, updateBoardProfile, deleteBoard, updateBookmarkCount,
incrementViewCount, incrementLikeCount, decrementLikeCount,
incrementCommentCount, decrementCommentCount) — keep history in Git if you need
to restore it, and ensure no other code references MybatisBoardDao before
deleting.

In `@src/main/java/com/web/SearchWeb/config/jwt/JwtMemberPrincipal.java`:
- Around line 11-20: JwtMemberPrincipal currently allows memberId to be null
(String.valueOf(memberId) yields "null"); add null checks in the record's
constructor so the principal cannot be created with a null memberId (and
optionally null role). Implement a compact/canonical constructor for
JwtMemberPrincipal that uses Objects.requireNonNull(memberId, "memberId must not
be null") (and requireNonNull for role if desired) or throws
IllegalArgumentException, so getName() can safely return
String.valueOf(memberId) without producing "null".

In
`@src/main/java/com/web/SearchWeb/config/security/HttpCookieOAuth2AuthorizationRequestRepository.java`:
- Around line 43-46: Validate the redirect_uri before storing it in the cookie:
in HttpCookieOAuth2AuthorizationRequestRepository, before calling
CookieUtils.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME, ...), run the
same whitelist/validation logic used in OAuth2SuccessHandler (extract that logic
into a shared utility method, e.g., isValidRedirectUri(String uri) in a common
class) and only add the cookie when isValidRedirectUri(redirectUriAfterLogin)
returns true; reference REDIRECT_URI_PARAM_COOKIE_NAME, COOKIE_EXPIRE_SECONDS
and cookieSecure so the cookie creation remains unchanged when validation
passes.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 31f701a8-9bdf-4fff-8fb3-7d3385cee145

📥 Commits

Reviewing files that changed from the base of the PR and between 59f22b9 and 9d6f62b.

📒 Files selected for processing (108)
  • .gitignore
  • build.gradle
  • docs/Backend/02. jwt-oauth2-architecture.md
  • docs/Backend/03. jwt-oauth2-test-scenarios.md
  • frontend/next.config.ts
  • frontend/src/app/auth/callback/page.tsx
  • frontend/src/app/layout.tsx
  • frontend/src/app/login/page.tsx
  • frontend/src/app/my-links/page.tsx
  • frontend/src/components/dialogs/CreateFolderDialog.tsx
  • frontend/src/components/dialogs/SaveLinkDialog.tsx
  • frontend/src/components/layout/Sidebar.tsx
  • frontend/src/components/my-links/RightPanel.tsx
  • frontend/src/components/providers/AuthProvider.tsx
  • frontend/src/components/providers/QueryProvider.tsx
  • frontend/src/lib/api/fetchClient.ts
  • frontend/src/lib/api/folderApi.ts
  • frontend/src/lib/api/tagApi.ts
  • frontend/src/lib/auth/currentUser.ts
  • frontend/src/lib/config/backend.ts
  • frontend/src/lib/store/authStore.ts
  • src/main/java/com/web/SearchWeb/SearchWebApplication.java
  • src/main/java/com/web/SearchWeb/aop/OwnerCheckAspect.java
  • src/main/java/com/web/SearchWeb/auth/controller/AuthController.java
  • src/main/java/com/web/SearchWeb/auth/controller/dto/AuthResponses.java
  • src/main/java/com/web/SearchWeb/auth/dao/MybatisRefreshTokenDao.java
  • src/main/java/com/web/SearchWeb/auth/dao/RefreshTokenDao.java
  • src/main/java/com/web/SearchWeb/auth/domain/RefreshToken.java
  • src/main/java/com/web/SearchWeb/auth/error/AuthErrorCode.java
  • src/main/java/com/web/SearchWeb/auth/error/AuthException.java
  • src/main/java/com/web/SearchWeb/auth/service/AuthService.java
  • src/main/java/com/web/SearchWeb/auth/service/AuthServiceImpl.java
  • src/main/java/com/web/SearchWeb/auth/service/RefreshTokenCleanupScheduler.java
  • src/main/java/com/web/SearchWeb/board/controller/BoardController.java
  • src/main/java/com/web/SearchWeb/board/dao/BoardDao.java
  • src/main/java/com/web/SearchWeb/board/dao/MybatisBoardDao.java
  • src/main/java/com/web/SearchWeb/board/domain/Board.java
  • src/main/java/com/web/SearchWeb/board/dto/BoardDto.java
  • src/main/java/com/web/SearchWeb/board/service/BoardService.java
  • src/main/java/com/web/SearchWeb/bookmark/controller/BookmarkApiController.java
  • src/main/java/com/web/SearchWeb/bookmark/service/BookmarkService.java
  • src/main/java/com/web/SearchWeb/bookmark/service/BookmarkServiceImpl.java
  • src/main/java/com/web/SearchWeb/comment/controller/CommentApiController.java
  • src/main/java/com/web/SearchWeb/comment/dao/CommentDao.java
  • src/main/java/com/web/SearchWeb/comment/dao/MybatisCommentDao.java
  • src/main/java/com/web/SearchWeb/comment/domain/Comment.java
  • src/main/java/com/web/SearchWeb/comment/dto/CommentDto.java
  • src/main/java/com/web/SearchWeb/comment/dto/UpdateUserProfileCommentDto.java
  • src/main/java/com/web/SearchWeb/comment/service/CommentService.java
  • src/main/java/com/web/SearchWeb/config/jwt/JwtAuthenticationEntryPoint.java
  • src/main/java/com/web/SearchWeb/config/jwt/JwtAuthenticationFilter.java
  • src/main/java/com/web/SearchWeb/config/jwt/JwtMemberPrincipal.java
  • src/main/java/com/web/SearchWeb/config/jwt/JwtProperties.java
  • src/main/java/com/web/SearchWeb/config/jwt/JwtUtils.java
  • src/main/java/com/web/SearchWeb/config/jwt/OAuth2FailureHandler.java
  • src/main/java/com/web/SearchWeb/config/jwt/OAuth2SuccessHandler.java
  • src/main/java/com/web/SearchWeb/config/security/CookieUtils.java
  • src/main/java/com/web/SearchWeb/config/security/CurrentMemberId.java
  • src/main/java/com/web/SearchWeb/config/security/CurrentMemberIdArgumentResolver.java
  • src/main/java/com/web/SearchWeb/config/security/CustomAuthenticationFailureHandler.java
  • src/main/java/com/web/SearchWeb/config/security/CustomAuthenticationSuccessHandler.java
  • src/main/java/com/web/SearchWeb/config/security/HttpCookieOAuth2AuthorizationRequestRepository.java
  • src/main/java/com/web/SearchWeb/config/security/SecurityConfig.java
  • src/main/java/com/web/SearchWeb/config/security/SecurityUtils.java
  • src/main/java/com/web/SearchWeb/config/security/WebMvcConfig.java
  • src/main/java/com/web/SearchWeb/folder/controller/MemberFolderController.java
  • src/main/java/com/web/SearchWeb/folder/dao/MemberFolderJpaDao.java
  • src/main/java/com/web/SearchWeb/folder/service/MemberFolderService.java
  • src/main/java/com/web/SearchWeb/folder/service/MemberFolderServiceImpl.java
  • src/main/java/com/web/SearchWeb/likes/dao/LikesDao.java
  • src/main/java/com/web/SearchWeb/likes/dao/MybatisLikesDao.java
  • src/main/java/com/web/SearchWeb/likes/service/LikesService.java
  • src/main/java/com/web/SearchWeb/linkanalysis/controller/LinkAnalysisController.java
  • src/main/java/com/web/SearchWeb/main/controller/MainController.java
  • src/main/java/com/web/SearchWeb/main/dao/MainDao.java
  • src/main/java/com/web/SearchWeb/main/dao/MybatisMainDao.java
  • src/main/java/com/web/SearchWeb/main/domain/Website.java
  • src/main/java/com/web/SearchWeb/main/service/MainService.java
  • src/main/java/com/web/SearchWeb/main/service/MainServiceImpl.java
  • src/main/java/com/web/SearchWeb/member/controller/MemberController.java
  • src/main/java/com/web/SearchWeb/member/dao/MemberDao.java
  • src/main/java/com/web/SearchWeb/member/dao/MybatisMemberDao.java
  • src/main/java/com/web/SearchWeb/member/domain/Member.java
  • src/main/java/com/web/SearchWeb/member/dto/CustomOAuth2Member.java
  • src/main/java/com/web/SearchWeb/member/dto/CustomUserDetails.java
  • src/main/java/com/web/SearchWeb/member/dto/MemberDto.java
  • src/main/java/com/web/SearchWeb/member/dto/Response/OAuth2Response.java
  • src/main/java/com/web/SearchWeb/member/error/MemberErrorCode.java
  • src/main/java/com/web/SearchWeb/member/error/MemberException.java
  • src/main/java/com/web/SearchWeb/member/service/CustomOAuth2MemberService.java
  • src/main/java/com/web/SearchWeb/member/service/CustomOAuth2UserService.java
  • src/main/java/com/web/SearchWeb/member/service/CustomerUserDetailService.java
  • src/main/java/com/web/SearchWeb/member/service/MemberService.java
  • src/main/java/com/web/SearchWeb/member/service/MemberServiceImpl.java
  • src/main/resources/application-dev.properties
  • src/main/resources/application-local.properties
  • src/main/resources/application-prod.properties
  • src/main/resources/application.properties
  • src/main/resources/db/init_postgres.sql
  • src/main/resources/mapper/board-mapper.xml
  • src/main/resources/mapper/comment-mapper.xml
  • src/main/resources/mapper/likes-mapper.xml
  • src/main/resources/mapper/main-mapper.xml
  • src/main/resources/mapper/member-mapper.xml
  • src/main/resources/mapper/refresh-token-mapper.xml
  • src/main/resources/templates/member/join.html
  • src/main/resources/templates/member/login.html
  • src/test/api-test/auth-test.http
💤 Files with no reviewable changes (31)
  • src/main/java/com/web/SearchWeb/main/dao/MainDao.java
  • src/main/java/com/web/SearchWeb/config/security/CustomAuthenticationSuccessHandler.java
  • src/main/java/com/web/SearchWeb/comment/dto/UpdateUserProfileCommentDto.java
  • src/main/java/com/web/SearchWeb/comment/dao/CommentDao.java
  • src/main/java/com/web/SearchWeb/main/service/MainService.java
  • src/main/java/com/web/SearchWeb/config/security/CustomAuthenticationFailureHandler.java
  • src/main/java/com/web/SearchWeb/comment/dto/CommentDto.java
  • src/main/java/com/web/SearchWeb/likes/service/LikesService.java
  • src/main/java/com/web/SearchWeb/member/dto/MemberDto.java
  • src/main/java/com/web/SearchWeb/main/service/MainServiceImpl.java
  • src/main/resources/mapper/main-mapper.xml
  • src/main/java/com/web/SearchWeb/member/service/CustomOAuth2UserService.java
  • src/main/java/com/web/SearchWeb/comment/dao/MybatisCommentDao.java
  • src/main/java/com/web/SearchWeb/main/domain/Website.java
  • src/main/java/com/web/SearchWeb/main/dao/MybatisMainDao.java
  • src/main/resources/mapper/comment-mapper.xml
  • src/main/resources/mapper/board-mapper.xml
  • src/main/java/com/web/SearchWeb/likes/dao/LikesDao.java
  • src/main/java/com/web/SearchWeb/member/service/MemberService.java
  • src/main/java/com/web/SearchWeb/member/service/CustomerUserDetailService.java
  • src/main/resources/templates/member/join.html
  • src/main/java/com/web/SearchWeb/likes/dao/MybatisLikesDao.java
  • frontend/src/lib/auth/currentUser.ts
  • src/main/java/com/web/SearchWeb/main/controller/MainController.java
  • src/main/resources/templates/member/login.html
  • src/main/java/com/web/SearchWeb/member/controller/MemberController.java
  • src/main/resources/mapper/likes-mapper.xml
  • src/main/java/com/web/SearchWeb/comment/domain/Comment.java
  • src/main/java/com/web/SearchWeb/member/dto/CustomUserDetails.java
  • src/main/java/com/web/SearchWeb/comment/service/CommentService.java
  • src/main/java/com/web/SearchWeb/comment/controller/CommentApiController.java

Comment on lines +275 to +278
> 1. **401 감지 (`response.status === 401`)**:
> - 모든 API 요청은 공통 함수인 `fetchClient`를 통과합니다. 여기서 응답 코드가 401인지를 실시간으로 체크합니다.
> 2. **AT 존재 조건 (`&& accessToken`)**:
> - "이미 AT를 가지고 있던 사용자(로그인했던 사용자)"인 경우에만 갱신을 시도합니다.
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

문서 lint 경고가 꽤 많이 남아 있습니다.

정적 분석에서 이미 잡힌 것처럼 이 구간들에는 MD009(trailing space), MD031/MD040(fence 주변 blank line/언어 태그), MD022(heading 주변 blank line) 경고가 같이 남아 있습니다. 문서 PR이라도 lint를 깨끗하게 맞춰두는 편이 이후 수정 때 잡음이 적습니다.

Also applies to: 402-409, 465-465, 483-560, 673-710, 727-733, 798-804, 851-898

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 275-275: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)


[warning] 278-278: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)

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

In `@docs/Backend/02`. jwt-oauth2-architecture.md around lines 275 - 278, The
Markdown sections around "401 감지 (`response.status === 401`)" and "AT 존재 조건 (`&&
accessToken`)" (and the other listed ranges) violate lint rules MD009,
MD031/MD040 and MD022; run markdownlint or your configured linter, remove
trailing spaces, ensure a single blank line before and after headings, and
add/adjust blank lines around fenced code blocks (and include language tags if
required) so those rules pass—specifically edit the heading blocks and
fenced/code-adjacent lines referenced by the strings "response.status === 401"
and "&& accessToken" to eliminate trailing spaces and restore required blank
lines.

Comment on lines +893 to +897
**6단계: DB/설정**
```
resources/mapper/refresh-token-mapper.xml ← MyBatis SQL (CRUD + FOR UPDATE)
resources/sql/V001__create_refresh_token.sql ← Flyway 마이그레이션
resources/application-{profile}.properties ← 프로필별 설정
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

리소스 파일 경로는 실제 저장소 경로로 적는 편이 안전합니다.

여기 적힌 resources/... 경로는 repo root 기준으로는 그대로 열 수 없습니다. 이 섹션은 src/main/resources/...로 맞춰야 새로 합류한 사람이 문서만 보고 바로 파일을 찾을 수 있습니다.

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 894-894: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 894-894: 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 `@docs/Backend/02`. jwt-oauth2-architecture.md around lines 893 - 897, 문서에 표기된
리소스 경로가 레포 루트 기준으로는 열 수 없어 혼동을 줄 수 있으니, 현재 나열된
resources/mapper/refresh-token-mapper.xml,
resources/sql/V001__create_refresh_token.sql,
resources/application-{profile}.properties 항목들을 실제 저장소 경로인
src/main/resources/mapper/refresh-token-mapper.xml,
src/main/resources/sql/V001__create_refresh_token.sql,
src/main/resources/application-{profile}.properties 형태로 수정해 문서 내 모든 경로를 레포 루트에서
바로 열 수 있는 경로로 통일하세요.

Comment on lines +54 to +55
👉 **테스트 파일 위치:** [`src/test/api-test/auth-test.http`](../../src/test/api-test/auth-test.http)

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

실행 가이드의 .http 파일명이 앞뒤로 다릅니다.

상단은 src/test/api-test/auth-test.http를 안내하는데 여기서는 jwt-oauth2-e2e.http를 가리킵니다. 문서 전체에서 파일명을 하나로 통일하지 않으면 독자가 잘못된 파일을 찾게 됩니다.

Also applies to: 133-135

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

In `@docs/Backend/03`. jwt-oauth2-test-scenarios.md around lines 54 - 55, The
document uses two different .http filenames (src/test/api-test/auth-test.http vs
jwt-oauth2-e2e.http); pick the correct test file name (use jwt-oauth2-e2e.http)
and update every mention in docs/Backend/03. jwt-oauth2-test-scenarios.md so all
references are consistent, including the header link and the later occurrence
around the other reference; ensure the displayed link text and the target
filename match exactly.

Comment on lines +365 to +368
**핵심 검증:**
- Step 2에서 새로운 토큰을 만들지 않고 **이미 발급된 RT-B를 그대로 반환**함 (결정적 생성 및 DB 조회)
- 10초 이내라면 몇 번을 요청해도 동일한 RT-B를 응답 (멱등성)
- 유예 기간이 지나면 폐기된 토큰으로 간주하여 거부
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

Grace Period 허용 횟수 설명을 하나로 고정해 주세요.

S4-02는 10초 안이면 여러 번 같은 RT-B를 돌려준다고 쓰고, AUTH-003는 3번째 사용에서 실패한다고 적었으며, AUTH-011 상세는 또 한 요청이 실패한다고 설명합니다. 허용 횟수가 1회인지, 10초 동안 멱등하게 재응답하는지 한 규칙으로 통일해야 QA 시나리오가 맞습니다.

Also applies to: 574-584, 706-711

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

In `@docs/Backend/03`. jwt-oauth2-test-scenarios.md around lines 365 - 368, 문서 내
Grace Period 허용 횟수가 서로 달라 혼란스러우니 S4-02, AUTH-003, AUTH-011(및 동일 내용이 반복된 블록들: 해당
문서의 574-584, 706-711 범위)을 하나의 규칙으로 통일하세요; 예를 들어 "10초 동안 동일한 RT-B를 무제한 멱등 응답" 또는
"유예기간 동안 최대 N회(예: 2회) 재시도 허용" 중 하나를 결정하고, 선택한 규칙으로 S4-02의 설명과 AUTH-003,
AUTH-011의 실패/성공 시나리오 문구를 모두 동일하게 바꿔 일관성 있게 업데이트하며 관련 테스트 기대값(성공/실패 시점)도 함께 맞춰
수정하세요.

Comment on lines +7 to +11
/**
* OAuth 로그인 완료 후 백엔드가 리디렉션하는 콜백 페이지
* - initialize()는 AuthProvider가 단독 호출 — 여기서는 결과만 구독
* - 초기화 완료 후 인증 성공 여부에 따라 분기
*/
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

주석과 실제 코드 동작이 불일치합니다.

Line 9의 주석에서 "initialize()는 AuthProvider가 단독 호출"이라고 설명하지만, 실제로는 Line 23에서 initialize()를 직접 호출하고 있습니다. 주석을 실제 동작에 맞게 수정해야 합니다.

📝 주석 수정 제안
 /**
  * OAuth 로그인 완료 후 백엔드가 리디렉션하는 콜백 페이지
- * - initialize()는 AuthProvider가 단독 호출 — 여기서는 결과만 구독
+ * - 마운트 시 initialize() 호출 (authStore 내부에서 중복 호출 방지)
  * - 초기화 완료 후 인증 성공 여부에 따라 분기
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/auth/callback/page.tsx` around lines 7 - 11, 주석과 실제 동작이
불일치합니다: 현재 callback page 내에서 initialize()를 직접 호출하고 있으므로 주석에서 "initialize()는
AuthProvider가 단독 호출"이라고 적혀 있는 부분을 삭제하거나 수정해 실제 동작을 반영하세요; 구체적으로 page.tsx의 설명 문구를
"이 페이지에서 initialize()를 호출하여 인증 결과를 처리" 또는 "AuthProvider가 아닌 이 컴포넌트에서
initialize()를 호출" 등으로 바꾸어 initialize(), AuthProvider, 그리고 인증 성공/실패 분기 로직을 정확히
설명하도록 업데이트하세요.

Comment on lines +64 to +65
String socialName = oAuth2Response.getName() != null ? oAuth2Response.getName() : "Unknown";
String socialEmail = oAuth2Response.getEmail();
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

nullable 소셜 필드로 기존 프로필을 지우게 됩니다.

Kakao처럼 name/email을 생략하는 공급자에서는 현재 코드가 null 또는 "Unknown"을 변경값으로 보고 기존 memberName/email을 덮어씁니다. 기본값은 신규 가입 시에만 쓰고, 기존 회원 동기화는 provider가 실제 값을 준 필드만 갱신해야 합니다.

Also applies to: 97-99, 112-120

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

In
`@src/main/java/com/web/SearchWeb/member/service/CustomOAuth2MemberService.java`
around lines 64 - 65, The code in CustomOAuth2MemberService sets
socialName/socialEmail to defaults and unconditionally writes them back, which
overwrites existing profile data when providers omit fields; change the update
logic so that when an existing Member is being synchronized you only overwrite
memberName/email if oAuth2Response.getName()/getEmail() are non-null and
non-empty (use the socialName/socialEmail variables only when creating a new
Member), and apply the same conditional-update rule to the other affected blocks
(lines around 97-99 and 112-120) so only provider-returned fields are used to
update existing Member records.

Comment on lines +79 to +93
CREATE TABLE IF NOT EXISTS "refresh_token" (
"id" bigint GENERATED ALWAYS AS IDENTITY NOT NULL,
"member_id" bigint NOT NULL,
"session_id" varchar(36) NOT NULL,
"version" integer NOT NULL DEFAULT 1,
"token_hash" varchar(64) NOT NULL,
"expires_at" timestamptz NOT NULL,
"created_at" timestamptz DEFAULT now() NOT NULL,
"rotated_at" timestamptz,
"replaced_by_version" integer,
"grace_until" timestamptz,
CONSTRAINT pk_refresh_token PRIMARY KEY ("id"),
CONSTRAINT uq_refresh_token_hash UNIQUE ("token_hash"),
CONSTRAINT fk_refresh_token_member_id FOREIGN KEY ("member_id") REFERENCES "member"("member_id") ON DELETE CASCADE
);
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

(session_id, version)는 유니크로 강제해야 합니다.

findBySessionIdAndVersion와 grace-period successor lookup은 세션당 버전이 1개라는 가정에 의존합니다. 지금처럼 일반 인덱스만 두면 중복 버전 row가 들어와도 DB가 막지 못해 재발급 체인이 비결정적으로 깨집니다.

🧩 제안 패치
 CREATE TABLE IF NOT EXISTS "refresh_token" (
   "id" bigint GENERATED ALWAYS AS IDENTITY NOT NULL,
   "member_id" bigint NOT NULL,
   "session_id" varchar(36) NOT NULL,
   "version" integer NOT NULL DEFAULT 1,
   "token_hash" varchar(64) NOT NULL,
   "expires_at" timestamptz NOT NULL,
   "created_at" timestamptz DEFAULT now() NOT NULL,
   "rotated_at" timestamptz,
   "replaced_by_version" integer,
   "grace_until" timestamptz,
   CONSTRAINT pk_refresh_token PRIMARY KEY ("id"),
   CONSTRAINT uq_refresh_token_hash UNIQUE ("token_hash"),
+  CONSTRAINT uq_refresh_token_session_version UNIQUE ("session_id", "version"),
   CONSTRAINT fk_refresh_token_member_id FOREIGN KEY ("member_id") REFERENCES "member"("member_id") ON DELETE CASCADE
 );
@@
-CREATE INDEX IF NOT EXISTS idx_refresh_token_session_version ON "refresh_token" ("session_id", "version");
+-- covered by uq_refresh_token_session_version

Also applies to: 563-563

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

In `@src/main/resources/db/init_postgres.sql` around lines 79 - 93, Add a unique
constraint on (session_id, version) in the "refresh_token" table to prevent
duplicate versions per session which breaks findBySessionIdAndVersion and
grace-period successor logic; update the CREATE TABLE "refresh_token" statement
to include a UNIQUE constraint (e.g. uq_refresh_token_session_version) on
("session_id", "version") or add an ALTER TABLE ... ADD CONSTRAINT ... UNIQUE
clause targeting the refresh_token table so the DB enforces one version per
session.


CREATE INDEX IF NOT EXISTS idx_refresh_token_member ON "refresh_token" ("member_id");
CREATE INDEX IF NOT EXISTS idx_refresh_token_session_version ON "refresh_token" ("session_id", "version");
CREATE INDEX IF NOT EXISTS idx_refresh_token_hash ON "refresh_token" ("token_hash");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

idx_refresh_token_hash는 중복 인덱스입니다.

uq_refresh_token_hash가 이미 동일 컬럼에 인덱스를 만드니, 이 인덱스는 refresh 회전/삭제 때 쓰기 비용만 추가합니다.

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

In `@src/main/resources/db/init_postgres.sql` at line 564, 인덱스 중복 문제: CREATE INDEX
IF NOT EXISTS idx_refresh_token_hash ON "refresh_token" ("token_hash"); 는 이미 동일
컬럼에 대해 존재하는 제약/인덱스인 uq_refresh_token_hash와 중복되므로 삭제하세요 — 즉, 제거하거나 주석 처리하여 중복 인덱스
생성을 막고 refresh 토큰 회전/삭제 시 불필요한 쓰기 비용을 없애십시오 (참조 대상: idx_refresh_token_hash,
uq_refresh_token_hash, 테이블 refresh_token, 컬럼 token_hash).

Comment on lines +19 to +20
# ★ [공통 필수 단계] Step 2를 수행하여 브라우저에서 복사한 쿠키를 여기에 붙여넣으세요.
@refreshTokenCookie = eyJhbGciOiJIUzM4NCJ9.eyJqdGkiOiIxMTBiOGJlYi00NDFlLTQ4ZDQtODgyYi0yN2VhYjlkNmNjM2Y6MiIsInN1YiI6IjciLCJzaWQiOiIxMTBiOGJlYi00NDFlLTQ4ZDQtODgyYi0yN2VhYjlkNmNjM2YiLCJ2ZXIiOjIsImlhdCI6MTc3NTA1NDg0NiwiZXhwIjoxNzc2MjY0NDQ2fQ.0ppPiXZSsumHjs2a1N2h157P3guZHpSGOBsIDjutGTRfssS6nNBB5kxYun-Q_sGL
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

실제 refresh token을 저장소에 커밋하면 안 됩니다.

이 값은 그대로 인증 자격 증명입니다. 병합 전 토큰을 즉시 폐기하고, 파일에는 빈 placeholder나 로컬 전용 환경 변수만 남겨주세요. 이미 커밋된 값은 히스토리에도 남습니다.

🔒 안전한 기본값 예시
-@refreshTokenCookie = <real-refresh-token>
+@refreshTokenCookie =
📝 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.

Suggested change
# ★ [공통 필수 단계] Step 2를 수행하여 브라우저에서 복사한 쿠키를 여기에 붙여넣으세요.
@refreshTokenCookie = eyJhbGciOiJIUzM4NCJ9.eyJqdGkiOiIxMTBiOGJlYi00NDFlLTQ4ZDQtODgyYi0yN2VhYjlkNmNjM2Y6MiIsInN1YiI6IjciLCJzaWQiOiIxMTBiOGJlYi00NDFlLTQ4ZDQtODgyYi0yN2VhYjlkNmNjM2YiLCJ2ZXIiOjIsImlhdCI6MTc3NTA1NDg0NiwiZXhwIjoxNzc2MjY0NDQ2fQ.0ppPiXZSsumHjs2a1N2h157P3guZHpSGOBsIDjutGTRfssS6nNBB5kxYun-Q_sGL
# ★ [공통 필수 단계] Step 2를 수행하여 브라우저에서 복사한 쿠키를 여기에 붙여넣으세요.
`@refreshTokenCookie` =
🧰 Tools
🪛 Betterleaks (1.1.1)

[high] 20-20: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

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

In `@src/test/api-test/auth-test.http` around lines 19 - 20, The committed file
contains a real refresh token in the variable `@refreshTokenCookie`; remove the
secret immediately, replace it with a non-sensitive placeholder (e.g.,
EMPTY_REFRESH_TOKEN or a note to use a local env var), and update tests to read
the token from a local-only environment variable or secure secret store instead
of the hardcoded value; also revoke/rotate the exposed token before merging to
remove live credentials from use.

Comment on lines +49 to +57
### [S1-Step 5] .http 세션 연동 (RT2 -> RT3 갱신)
# @name sync
# - (테스트 목적) 실제 프론트엔드는 RT2에서 멈추지만, 본 도구와의 세션 연동을 위해 갱신 API를 호출합니다.
# - (동작) 서버는 RT2를 확인하고 이 도구 전용 새 열쇠(RT3)를 발급합니다.
# - 실행 후에는 자동으로 변수가 업데이트되므로 즉시 다음 단계로 넘어가면 됩니다.
POST {{baseUrl}}/api/auth/refresh
Cookie: refreshToken={{refreshTokenCookie}}

@accessToken = {{sync.response.body.data.accessToken}}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Apr 2, 2026

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

🌐 Web query:

IntelliJ HTTP Client와 VS Code REST Client는 Set-Cookie응답으로부터 사용자 정의.http 변수(@refreshTokenCookie)를 자동 갱신하나요, 아니면 쿠키 jar/session만 유지하나요?

💡 Result:

아니요, IntelliJ HTTP Client와 VS Code REST Client는 Set-Cookie 응답으로부터 사용자 정의 .http 변수(@refreshTokenCookie)를 자동으로 갱신하지 않습니다. 둘 다 쿠키 jar/session을 유지하여 후속 요청에 쿠키를 자동으로 포함하지만, 사용자 정의 변수(예: @refreshTokenCookie 또는 {{refreshTokenCookie}})는 수동으로 스크립트나 할당을 통해 갱신해야 합니다.

Citations:


🏁 Script executed:

cat -n src/test/api-test/auth-test.http | sed -n '45,65p'

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 862


🏁 Script executed:

grep -n -A 5 "@name sync" src/test/api-test/auth-test.http | head -30

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 344


🏁 Script executed:

grep -n ">" src/test/api-test/auth-test.http | head -20

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 153


🏁 Script executed:

grep -n "{" src/test/api-test/auth-test.http | head -20

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 1050


🏁 Script executed:

cat src/test/api-test/auth-test.http | grep -E "^>|^<|script|response|client" -i

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 787


🏁 Script executed:

cat src/test/api-test/auth-test.http | wc -l

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 75


🏁 Script executed:

cat -n src/test/api-test/auth-test.http | grep -E "^\s*[0-9]+\s+>"

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 54


🏁 Script executed:

cat -n src/test/api-test/auth-test.http

Repository: Searchweb-Dev/Searchweb-Back

Length of output: 6860


응답 스크립트 추가로 @refreshTokenCookie 자동 갱신 구현 필요

파일의 헤더(9-10줄)는 @refreshTokenCookie가 자동으로 업데이트된다고 명시하지만, 실제로는 Set-Cookie 응답으로부터 새 값을 추출하는 응답 스크립트가 없습니다. 현재 57줄의 @accessToken = {{sync.response.body.data.accessToken}}만 존재하고, refreshToken은 추출되지 않습니다.

IntelliJ HTTP Client와 VS Code REST Client 모두 사용자 정의 변수(예: @refreshTokenCookie)를 자동 갱신하지 않으므로, 첫 번째 rotation 이후 모든 후속 refresh/logout 요청이 만료된 쿠키를 계속 사용하게 됩니다.

다음 중 하나로 해결하세요:

  1. 응답 스크립트를 추가하여 Set-Cookie 헤더에서 refreshTokenCookie 값을 추출해 변수 갱신
  2. 명시적 Cookie: 헤더를 제거하고 클라이언트의 자동 쿠키 jar 메커니즘에 의존
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/api-test/auth-test.http` around lines 49 - 57, The test currently
sets `@accessToken` from the sync response but never updates `@refreshTokenCookie`,
so subsequent requests keep using the old cookie; update the POST
{{baseUrl}}/api/auth/refresh "sync" request to parse the Set-Cookie response
header and assign the new refreshToken value back into the `@refreshTokenCookie`
variable (i.e., add a response script that extracts refreshToken from Set-Cookie
and updates the variable), or alternatively remove the explicit "Cookie:
refreshToken={{refreshTokenCookie}}" header to let the HTTP client cookie jar
handle rotation automatically; modify the "sync" request in
src/test/api-test/auth-test.http accordingly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

지금 vscode에서만 갱신돼? 둘다안돼?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@jin2304 둘 다 안 됩니다.

IntelliJ HTTP ClientVS Code REST Client 모두 Set-Cookie 응답 헤더로부터 사용자 정의 변수(@refreshTokenCookie)를 자동으로 갱신하지 않습니다.

동작 IntelliJ HTTP Client VS Code REST Client
내부 쿠키 jar 유지 (자동)
Set-Cookie@변수 자동 갱신

두 도구 모두 쿠키 jar를 내부적으로 유지해서 후속 요청에는 자동으로 쿠키를 첨부하지만, @refreshTokenCookie 같은 사용자 정의 변수는 수동 스크립트 없이는 갱신되지 않습니다.

따라서 파일 헤더의 "자동으로 전역 변수에 저장하는 스크립트를 포함하고 있습니다"라는 설명이 현재 코드와 맞지 않습니다. 아래 두 가지 방법 중 하나로 해결할 수 있습니다:

방법 1: IntelliJ 응답 스크립트로 추출

### [S1-Step 5] .http 세션 연동 (RT2 -> RT3 갱신)
# `@name` sync
POST {{baseUrl}}/api/auth/refresh
Cookie: refreshToken={{refreshTokenCookie}}

> {%
  var setCookie = response.headers.valueOf("Set-Cookie");
  if (setCookie) {
    var match = setCookie.match(/refreshToken=([^;]+)/);
    if (match) client.global.set("refreshTokenCookie", match[1]);
  }
  client.global.set("accessToken", response.body.data.accessToken);
%}

방법 2: 명시적 Cookie: 헤더 제거 → 쿠키 jar에 위임

### [S1-Step 5] .http 세션 연동
# `@name` sync
POST {{baseUrl}}/api/auth/refresh
# Cookie 헤더 제거 → 클라이언트 쿠키 jar가 자동 처리

`@accessToken` = {{sync.response.body.data.accessToken}}

방법 2가 더 간단하지만, 방법 1은 IntelliJ 전용이고 VS Code REST Client는 > {% %} 스크립트 블록을 지원하지 않으므로 두 도구 모두 지원하려면 방법 2를 권장합니다.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

VS Code REST Client에서도 응답 쿠키값(리프레시 토큰) 자동세팅 가능해?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

@jin2304 결론부터 말씀드리면, "자동 전송"은 가능하지만, @refreshTokenCookie 변수 자동 갱신은 불가능합니다.

두 기능을 명확하게 구분해서 설명드립니다:


기능 비교표

기능 VS Code REST Client IntelliJ HTTP Client
Set-Cookie 쿠키를 내부 쿠키 jar에 저장 후 다음 요청에 자동 전송 ✅ (rememberCookiesForSubsequentRequests: true, 기본값)
Set-Cookie 값을 @refreshTokenCookie 같은 사용자 정의 변수로 자동 갱신 ❌ (미지원) ✅ (> {% client.global.set(...) %} 스크립트로 가능)
> {% %} 응답 스크립트 블록 지원 ❌ (미지원)

실질적인 의미

rest-client.rememberCookiesForSubsequentRequests는 응답의 Set-Cookie 헤더에서 쿠키를 저장하고 이후 요청에 자동으로 사용합니다. 즉, 명시적인 Cookie: refreshToken={{refreshTokenCookie}} 헤더를 제거하면 VS Code REST Client가 쿠키 jar를 통해 자동으로 쿠키를 전송합니다.

그러나 쿠키의 일부를 추출해서 다음 요청에 변수로 사용하는 방법은 없습니다.


권장 해결책: 명시적 Cookie: 헤더 제거 (VS Code + IntelliJ 공통)

### [S1-Step 5] .http 세션 연동 (RT2 -> RT3 갱신)
# `@name` sync
POST {{baseUrl}}/api/auth/refresh
# ← Cookie 헤더를 제거하면 쿠키 jar가 자동으로 refreshToken 쿠키를 전송

`@accessToken` = {{sync.response.body.data.accessToken}}

이 방식은 두 도구 모두에서 동작하며, @refreshTokenCookie 변수 관리 자체가 필요 없어집니다. 단, 파일 상단의 @refreshTokenCookie 변수 및 헤더 주석도 함께 정리해야 현재 헤더의 "자동 갱신" 설명과 실제 동작이 일치하게 됩니다.


🌐 Web query:

VS Code REST Client extension response scripting Set-Cookie variable auto update 2024

💡 Result:

The VS Code REST Client extension (Huachao/vscode-restclient) still doesn’t support “response scripting” (i.e., running JS against a response to parse Set-Cookie and programmatically update variables). That capability is tracked as an open feature request (“Support Response Scripting”) since Aug 17, 2018. [1]

What it does support (and what you usually want for Set-Cookie) is an automatic cookie jar:

  • Keep rest-client.rememberCookiesForSubsequentRequests enabled (it’s true by default). The extension will save cookies from Set-Cookie response headers and send them on subsequent requests automatically. [2]

If you were hoping to extract Set-Cookie into a variable and “auto-update” it:

  • You can reference response headers via named-request variables (e.g. {{login.response.headers.Header-Name}}). [2]
  • But without response scripting, there’s no built-in way to parse Set-Cookie (strip attributes, handle multiple cookies, etc.) into a clean Cookie: name=value header value automatically—so the cookie-jar feature is the intended solution. [2]

Sources

  1. Huachao/vscode-restclient Issue #238 (“Support Response Scripting”). [1]
  2. Huachao/vscode-restclient README (request variables + rememberCookiesForSubsequentRequests). [2]

🌐 Web query:

humao vscode-restclient response handler script cookie variable

💡 Result:

humao.rest-client (vscode-restclient) does not support “response handler scripts” (the JetBrains-style JavaScript blocks with client.global.set(...)), so you can’t take a Set-Cookie header and run script logic to parse out just the cookie value into a new variable. JetBrains’ HTTP Client does support that scripting model, but that’s a different product. [2]

What you can do in vscode-restclient:

  1. Let the extension manage cookies automatically (recommended)
    The extension has a built-in cookie jar: it can remember cookies from Set-Cookie and send them on subsequent requests via the rest-client.rememberCookiesForSubsequentRequests setting (default true). You can disable per-request with # @no-cookie-jar``. [1]

  2. Capture the raw header value into a variable (but no parsing)
    You can reference response headers like:

# `@name` getToken
GET http://{{host}}/api/csrf_token

`@set_cookie` = {{getToken.response.headers.Set-Cookie}}

This works because request variables support {{requestName.response.headers.<Header-Name>}} (header names are case-insensitive). But it will give you the entire Set-Cookie string (including attributes), and vscode-restclient doesn’t provide a built-in string-splitting/expression language to extract only csrftoken=.... [1]

If you truly need “parse cookie → store only value”, you’ll need to either:

  • change the API to also return the token in JSON (then use JSONPath: {{req.response.body.$.token}}), or
  • use a client that supports response scripting (e.g., JetBrains HTTP Client), or another VS Code extension that supports extracting cookies into env vars. [2] [3]

Sources:
[1] Huachao/vscode-restclient README (request-variable syntax; cookie-jar setting; @no-cookie-jar). (github.com)
[2] JetBrains blog (REST client “response handler scripts”; client.global variables). (blog.jetbrains.com)
[3] Thunder Client docs (can set env var from response cookie via cookie.cookieName). (github.com)

Citations:


@jin2304 불가능합니다. 다만 한 가지 부분적인 방법과 대안이 있습니다.


VS Code REST Client의 실제 지원 범위

VS Code REST Client는 "응답 스크립팅(Response Scripting)"을 지원하지 않습니다. 이 기능은 2018년부터 오픈된 기능 요청(Issue #238)으로, 아직까지 구현되지 않았습니다.

기능 지원 여부
쿠키 jar 자동 저장 및 전송 ✅ (rememberCookiesForSubsequentRequests, 기본값 true)
Set-Cookie 전체 헤더 값을 변수에 저장 ⚠️ 가능하지만 속성 포함된 전체 문자열
Set-Cookie에서 값만 파싱해 변수 갱신 ❌ 불가능
> {% client.global.set() %} 스크립트 ❌ 불가능 (IntelliJ 전용)

부분적으로 가능한 방법 (비추천)

{{requestName.response.headers.Set-Cookie}} 문법으로 Set-Cookie 헤더 전체를 변수에 담을 수는 있습니다. 그러나 Path, HttpOnly, Max-Age 등 속성이 포함된 전체 문자열이 저장되므로, 토큰 값만 추출하는 내장 파싱 기능이 없어 실용적이지 않습니다.


권장 해결책

① 쿠키 jar에 위임 (명시적 Cookie: 헤더 제거)

rest-client.rememberCookiesForSubsequentRequests가 기본값 true이므로, 명시적 Cookie: 헤더를 제거하면 VS Code REST Client가 Set-Cookie 응답을 자동으로 저장하고 이후 요청에 전송합니다.

### [S1-Step 5] .http 세션 연동
# `@name` sync
POST {{baseUrl}}/api/auth/refresh
# ← Cookie 헤더 제거, 쿠키 jar가 자동으로 refreshToken 전송

`@accessToken` = {{sync.response.body.data.accessToken}}

② Thunder Client 확장으로 전환 (완전한 자동화가 필요한 경우)

Thunder Client는 cookie.cookieName 문법으로 응답 쿠키 값을 환경 변수로 자동 설정하는 기능을 지원합니다. 완전한 자동화가 필요하다면 Thunder Client가 현재 요구사항에 더 적합합니다.


결론적으로 현재 .http 파일의 헤더 주석("자동으로 전역 변수에 저장하는 스크립트를 포함")은 VS Code REST Client 기준으로 사실과 다릅니다. 방법 ①(쿠키 jar 위임)으로 수정하고 해당 주석을 업데이트하는 것을 권장합니다.

jin2304 added 3 commits April 4, 2026 18:15
- DB Schema (init_postgres.sql): 세션 버전 관리(version) 및 토큰 메타데이터 저장을 위한 테이블 스키마 업데이트
- DAO (MemberDao, MybatisMemberDao, RefreshTokenDao, MybatisRefreshTokenDao): 토큰 기반 인증 및 세션 추적을 위한 데이터 접근 인터페이스 업데이트
  - 동시 갱신 요청(Race Condition) 대비 FOR UPDATE 비관적 잠금을 적용하여 데이터 무결성 보장
- SQL Mapper (member-mapper.xml): 세션 및 토큰 관리를 위한 SQL 쿼리 최적화 및 행 단위 잠금 적용
- JwtUtils: 보안이 강화된 토큰 생성 및 유효성 검증 로직 고도화
- AuthServiceImpl: RTR(Refresh Token Rotation) 핵심 비즈니스 로직 구현
  - 이전 Refresh Token 즉시 무효화 및 새 토큰 발급으로 보안성 강화
  - 세션 버전(version) 업데이트를 통한 재사용 토큰 감지 및 차단
- AuthController: 쿠키 기반 로그아웃 처리 및 토큰 갱신 API 엔드포인트 보안 강화
- AuthException: 인증 실패, 토큰 만료, 권한 부족 등 예외 처리 표준화
- RefreshTokenCleanupScheduler: 만료 토큰 자동 정리 스케줄러 추가
- SecurityConfig: 상용 환경 대비 CORS/CSRF 정책 강화 및 API 접근 권한 설정 최적화
- SecurityUtils: SecurityContextHolder를 통한 인증된 유저 정보 조회 유틸리티 고도화
- JwtAuthenticationFilter: 헤더 및 쿠키에서 토큰을 추출하고 검증하는 로직 개선
- JwtAuthenticationEntryPoint: 401 Unauthorized 에러 발생 시 공통 ApiResponse 형식 JSON 응답 처리
- JwtAccessDeniedHandler: 403 Forbidden 에러 발생 시 공통 ApiResponse 형식 JSON 응답 처리 (신규)
- OAuth2SuccessHandler: 소셜 로그인 성공 후 프론트엔드 안전한 리다이렉션 및 토큰 발급 로직 연동
jin2304 added 2 commits April 4, 2026 18:25
- next.config.ts: 백엔드 도메인과의 쿠키 공유를 위한 도메인 설정 및 API 프록시 경로 조정
- page.tsx (callback): 소셜 로그인 성공 후 서버로부터 전달받은 인증 상태를 처리하고 대시보드로 이동시키는 로직 구현
- page.tsx (my-links): 클라이언트 사이드 로그인 상태 체크 및 인증된 사용자 전용 데이터 렌더링 최적화
- 02. jwt-oauth2-architecture.md: RTR 시퀀스 및 세션 관리 전략 상세 설명 및 다이어그램 업데이트
  - FOR UPDATE 비관적 잠금을 통한 동시성 제어 흐름 문서화
  - 세션 버전 기반 토큰 재사용 탐지 시나리오 추가
- auth-test.http: 복합적 인증 시나리오 자동화 테스트 스크립트 완성
  - 정상 로그인, 토큰 로테이션, 동시 접근 상황 테스트 케이스 포함
  - 쿠키 기반 RefreshToken 자동 갱신 흐름 검증 스크립트 구성
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@jin2304 jin2304 merged commit 4cc6396 into dev Apr 4, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 새로운 기능을 추가 ♻️ refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant