Repository navigation
Conversation
refactor: 관리자 페이지 권한 체크 및 라우트 구조 개선
[fx] 스크롤 잠금 버그 수정
📝 WalkthroughWalkthroughThe changes refactor login modal rendering into a centralized Changes
Sequence DiagramsequenceDiagram
participant Client as Client / Browser
participant AdminLayout as Admin Layout<br/>(useEffect)
participant API as Backend API<br/>/members/me/login-status
participant Router as Next.js Router
participant AuthRoutes as Auth Routes<br/>(/login, /signup)
Client->>AdminLayout: Navigate to /admin/*
rect rgba(100, 150, 200, 0.5)
Note over AdminLayout: Check if route is auth-exempt
alt Is /login or /signup
AdminLayout->>AdminLayout: Set authorized = true
AdminLayout->>Client: Render children
else Is protected route
AdminLayout->>AdminLayout: Show loading state
AdminLayout->>API: GET /members/me/login-status<br/>(credentials: include)
API-->>AdminLayout: Response { isSuccess, result.admin }
rect rgba(200, 100, 100, 0.5)
alt Success && admin === true
AdminLayout->>AdminLayout: Set authorized = true
AdminLayout->>Client: Render children
else Failure or !admin
AdminLayout->>Router: router.push(/admin/login)
Router-->>Client: Redirect to /admin/login
end
end
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request centralizes the login modal into a global component and introduces a useScrollLock hook with a counter to manage body scrolling across nested modals. It also implements admin authorization checks within the admin layout. Key feedback includes a critical concern regarding the removal of the admin login form, which prevents users from logging in. Additionally, the reviewer suggests using server-side redirects for the admin index, correcting path matching logic in the admin layout, and utilizing getComputedStyle to ensure accurate restoration of the body's overflow state.
| <div className="mt-[80px] text-black subhead_1"> | ||
| 관리자 기능은 로그인이 필요합니다! | ||
| </div> |
| useEffect(() => { | ||
| router.replace("/admin/users"); | ||
| }, [router]); |
| const isAuthPage = | ||
| pathname.startsWith("/login") || | ||
| pathname.startsWith("/signup"); |
There was a problem hiding this comment.
isAuthPage 판별 시 pathname.startsWith("/login")을 사용하고 있으나, 현재 파일 위치와 경로 구조상 관리자 로그인 경로는 /admin/login일 가능성이 높습니다. 이 경우 조건문이 항상 false를 반환하게 됩니다. 실제 경로에 맞춰 /admin/login 등으로 수정하거나, 이 레이아웃이 해당 페이지를 포함하지 않는다면 불필요한 로직을 정리하는 것이 좋습니다.
| const isAuthPage = | |
| pathname.startsWith("/login") || | |
| pathname.startsWith("/signup"); | |
| const isAuthPage = | |
| pathname.startsWith("/admin/login") || | |
| pathname.startsWith("/admin/signup"); |
|
|
||
| if (lockCount === 0) { | ||
| // 최초 1회 잠금 시 원래의 인라인 스타일 저장 | ||
| originalOverflow = document.body.style.overflow; |
There was a problem hiding this comment.
document.body.style.overflow는 인라인 스타일만 가져옵니다. 만약 CSS 클래스 등을 통해 body에 기본 overflow 스타일이 지정되어 있다면 이를 정확히 캡처하지 못할 수 있습니다. window.getComputedStyle(document.body).overflow를 사용하여 현재 계산된 스타일을 저장하는 것이 더 안전합니다.
| originalOverflow = document.body.style.overflow; | |
| originalOverflow = window.getComputedStyle(document.body).overflow; |
There was a problem hiding this comment.
스타일 지정을 따로 하지 않았습니다.
해당 부분은 문제가 없습니다. 추후에 스타일이 지정되면 스타일을 넣겠습니다.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/`(admin)/admin/(app)/layout.tsx:
- Around line 7-12: The current LoginStatusResponse type and the admin check
consume /members/me/login-status as { result: { admin: boolean } } which
mismatches the shared member/login-status contract and can misclassify valid
sessions; update the code to use the shared member/login-status type/service (or
change the backend contract to include a permission-aware response) and replace
the inline LoginStatusResponse with the canonical shared type, then update the
admin check where you reference the response (the check that inspects
result.admin) to read the correct property shape provided by the shared
service/endpoint (or call the permission-capable endpoint instead) so the admin
redirect logic uses the correct field names and types.
- Around line 26-28: The isAuthPage check currently uses
pathname.startsWith("/login") and "/signup", which doesn't match the actual
route (/admin/login); update the isAuthPage logic in layout.tsx (the const
isAuthPage and the duplicate check near the redirect logic) to only test
pathname.startsWith("/admin/login") so the auth bypass correctly recognizes the
admin login page and avoids running the auth guard on it.
In `@src/app/`(admin)/admin/(auth)/login/page.tsx:
- Around line 7-33: The page currently only renders a notice and must provide a
real login action: add a visible call-to-action (e.g., a "Sign in" button and a
"Forgot password" link) inside the rendered section so users can authenticate or
recover access; the button should either invoke the app's existing global login
modal opener (e.g., call openAuthModal(), showAuthModal(), or dispatch an
auth-modal action) or navigate to the canonical auth route that triggers the
modal (for example /login?modal=1 or /api/auth/signin), and the forgot-password
link should point to the password-recovery route; place these controls next to
the existing Image and the div with the text so the admin flow is not a
dead-end.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3bc196e6-635d-431a-aab1-273041ef19b1
📒 Files selected for processing (21)
src/app/(admin)/admin/(app)/layout.tsxsrc/app/(admin)/admin/(app)/page.tsxsrc/app/(admin)/admin/(app)/stories/[id]/page.tsxsrc/app/(admin)/admin/(auth)/login/page.tsxsrc/app/(main)/page.tsxsrc/app/(main)/stories/[id]/page.tsxsrc/app/(main)/stories/page.tsxsrc/app/groups/page.tsxsrc/app/layout.tsxsrc/components/base-ui/Admin/ConfirmModal.tsxsrc/components/base-ui/Bookcase/bookid/BookshelfDeleteConfirmModal.tsxsrc/components/base-ui/Login/GlobalLoginModal.tsxsrc/components/base-ui/Login/LoginModal.tsxsrc/components/base-ui/Login/hooks/useScrollLock.tssrc/components/common/ConfirmModal.tsxsrc/components/common/ReportModal.tsxsrc/components/layout/BookSelectModal.tsxsrc/components/layout/BottomNav.tsxsrc/components/layout/Header.tsxsrc/components/layout/SearchModal.tsxsrc/hooks/useScrollLock.ts
💤 Files with no reviewable changes (1)
- src/components/base-ui/Login/hooks/useScrollLock.ts
| type LoginStatusResponse = { | ||
| isSuccess: boolean; | ||
| result?: { | ||
| admin: boolean; | ||
| }; | ||
| }; |
There was a problem hiding this comment.
Authorization check is using a mismatched API shape.
/members/me/login-status is being parsed as { result: { admin: boolean } } and checked at Line 55, but the shared member/login-status contract in this codebase is not shaped that way. This can make valid sessions fail admin checks and redirect incorrectly.
Use a permission-capable endpoint (or align backend contract) and shared types/service to avoid contract drift.
Also applies to: 53-59
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`(admin)/admin/(app)/layout.tsx around lines 7 - 12, The current
LoginStatusResponse type and the admin check consume /members/me/login-status as
{ result: { admin: boolean } } which mismatches the shared member/login-status
contract and can misclassify valid sessions; update the code to use the shared
member/login-status type/service (or change the backend contract to include a
permission-aware response) and replace the inline LoginStatusResponse with the
canonical shared type, then update the admin check where you reference the
response (the check that inspects result.admin) to read the correct property
shape provided by the shared service/endpoint (or call the permission-capable
endpoint instead) so the admin redirect logic uses the correct field names and
types.
| const isAuthPage = | ||
| pathname.startsWith("/login") || | ||
| pathname.startsWith("/signup"); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 1) Locate admin login/signup pages and admin layouts
fd -t f 'page.tsx|layout.tsx' src/app | rg -n '/\(admin\)/admin/'
# 2) Show current bypass matcher and redirect lines in this layout
rg -n -C2 'startsWith\\("/login"\\)|startsWith\\("/signup"\\)|router\\.replace\\("/admin/login"\\)' src/appRepository: checkmo2025/FE
Length of output: 733
🏁 Script executed:
head -60 src/app/\(admin\)/admin/\(app\)/layout.tsxRepository: checkmo2025/FE
Length of output: 1404
🏁 Script executed:
fd -t f 'signup.*page.tsx' src/appRepository: checkmo2025/FE
Length of output: 40
Auth-page bypass matcher uses incorrect paths and will not work for the actual login route.
Bypass checks pathname.startsWith("/login") and pathname.startsWith("/signup"), but the actual login page is at /admin/login (under the (auth) route group). When users navigate to /admin/login, the pathname will be /admin/login, which does not match the bypass condition. This causes the auth check to run on the login page itself, leading to a redirect loop or authentication failures.
Update lines 26–28 to match the actual route paths:
const isAuthPage =
pathname.startsWith("/admin/login");
(No /admin/signup page exists in the codebase, so only the login path needs to be checked.)
Also applies to: 49-49
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`(admin)/admin/(app)/layout.tsx around lines 26 - 28, The isAuthPage
check currently uses pathname.startsWith("/login") and "/signup", which doesn't
match the actual route (/admin/login); update the isAuthPage logic in layout.tsx
(the const isAuthPage and the duplicate check near the redirect logic) to only
test pathname.startsWith("/admin/login") so the auth bypass correctly recognizes
the admin login page and avoids running the auth guard on it.
| <main | ||
| className=" | ||
| min-h-screen | ||
| flex items-center justify-center px-4 | ||
| bg-[#826656] | ||
| " | ||
| > | ||
| <section | ||
| className=" | ||
| w-full max-w-[456px] h-[276px] | ||
| rounded-[8px] | ||
| bg-white | ||
| flex flex-col items-center justify-center | ||
| text-center | ||
| " | ||
| > | ||
| <Image | ||
| src="/Vector.svg" | ||
| alt="CheckMo Logo" | ||
| width={76} | ||
| height={46} | ||
| className="w-[76.607px] h-[46.815px] shrink-0" | ||
| /> | ||
|
|
||
| <div className="mt-[80px] text-black subhead_1"> | ||
| 관리자 기능은 로그인이 필요합니다! | ||
| </div> |
There was a problem hiding this comment.
Add a real login action here.
Users redirected to /admin/login now only see a notice, but this page no longer gives them any way to authenticate or recover to a route that can open the global login modal. That makes the admin flow dead-end for logged-out users.
💡 Minimal fix
+"use client";
+import { useAuthStore } from "@/store/useAuthStore";
import Image from "next/image";
export default function AdminLoginPage() {
+ const { openLoginModal } = useAuthStore();
+
return (
<main
className="
min-h-screen
flex items-center justify-center px-4
@@
<div className="mt-[80px] text-black subhead_1">
관리자 기능은 로그인이 필요합니다!
</div>
+
+ <button
+ type="button"
+ onClick={openLoginModal}
+ className="mt-4 rounded-[8px] bg-primary-3 px-4 py-2 text-white"
+ >
+ 로그인하기
+ </button>
</section>
</main>
);
}📝 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.
| <main | |
| className=" | |
| min-h-screen | |
| flex items-center justify-center px-4 | |
| bg-[#826656] | |
| " | |
| > | |
| <section | |
| className=" | |
| w-full max-w-[456px] h-[276px] | |
| rounded-[8px] | |
| bg-white | |
| flex flex-col items-center justify-center | |
| text-center | |
| " | |
| > | |
| <Image | |
| src="/Vector.svg" | |
| alt="CheckMo Logo" | |
| width={76} | |
| height={46} | |
| className="w-[76.607px] h-[46.815px] shrink-0" | |
| /> | |
| <div className="mt-[80px] text-black subhead_1"> | |
| 관리자 기능은 로그인이 필요합니다! | |
| </div> | |
| "use client"; | |
| import { useAuthStore } from "@/store/useAuthStore"; | |
| import Image from "next/image"; | |
| export default function AdminLoginPage() { | |
| const { openLoginModal } = useAuthStore(); | |
| return ( | |
| <main | |
| className=" | |
| min-h-screen | |
| flex items-center justify-center px-4 | |
| bg-[`#826656`] | |
| " | |
| > | |
| <section | |
| className=" | |
| w-full max-w-[456px] h-[276px] | |
| rounded-[8px] | |
| bg-white | |
| flex flex-col items-center justify-center | |
| text-center | |
| " | |
| > | |
| <Image | |
| src="/Vector.svg" | |
| alt="CheckMo Logo" | |
| width={76} | |
| height={46} | |
| className="w-[76.607px] h-[46.815px] shrink-0" | |
| /> | |
| <div className="mt-[80px] text-black subhead_1"> | |
| 관리자 기능은 로그인이 필요합니다! | |
| </div> | |
| <button | |
| type="button" | |
| onClick={openLoginModal} | |
| className="mt-4 rounded-[8px] bg-primary-3 px-4 py-2 text-white" | |
| > | |
| 로그인하기 | |
| </button> | |
| </section> | |
| </main> | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`(admin)/admin/(auth)/login/page.tsx around lines 7 - 33, The page
currently only renders a notice and must provide a real login action: add a
visible call-to-action (e.g., a "Sign in" button and a "Forgot password" link)
inside the rendered section so users can authenticate or recover access; the
button should either invoke the app's existing global login modal opener (e.g.,
call openAuthModal(), showAuthModal(), or dispatch an auth-modal action) or
navigate to the canonical auth route that triggers the modal (for example
/login?modal=1 or /api/auth/signin), and the forgot-password link should point
to the password-recovery route; place these controls next to the existing Image
and the div with the text so the admin flow is not a dead-end.
📌 개요 (Summary)
🛠️ 변경 사항 (Changes)
📸 스크린샷 (Screenshots)
(UI 변경 사항이 있다면 첨부해주세요)
✅ 체크리스트 (Checklist)
pnpm build)pnpm lint)Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Refactor