1- 'use client'
2-
3- import { useEffect , useState } from 'react' ;
4- import { useRouter } from 'next/navigation' ;
1+ 'use client' ;
52
3+ import { useEffect , useMemo , useRef , useState } from 'react' ;
4+ import { usePathname , useRouter , useSearchParams } from 'next/navigation' ;
65import { useAuthenticatedApi } from '@/hooks/useAuthenticatedApi' ;
76import Loader from '@/components/ui/common/Loader' ;
87
9- export default function ApiCodeGuard ( { children } ) {
10- const router = useRouter ( ) ;
11- const { apiClient } = useAuthenticatedApi ( ) ;
12-
13- const [ checking , setChecking ] = useState ( true ) ;
14- const [ allowed , setAllowed ] = useState ( false ) ;
15-
16- useEffect ( ( ) => {
17- let cancelled = false ;
18-
19- const verifyAccess = async ( ) => {
20- try {
21- const res = await apiClient . get ( '/recruit/members' , {
22- params : { page : 0 , size : 20 , sort : 'createdAt' , dir : 'DESC' } ,
23- } ) ;
24-
25- const code = res ?. data ?. code ;
26- if ( ! cancelled ) {
27- if ( code === 200 ) {
28- setAllowed ( true ) ;
29- } else {
30- router . replace ( '/auth/signin' ) ;
31- }
32- }
33- } catch ( error ) {
34- if ( ! cancelled ) {
35- // 인터셉터에서 401/403 처리로 리다이렉트가 발생할 수 있으므로, 보조적으로 차단
36- router . replace ( '/auth/signin' ) ;
37- }
38- } finally {
39- if ( ! cancelled ) {
40- setChecking ( false ) ;
8+ /**
9+ * ApiCodeGuard
10+ * - /auth/{role}?next=<...> 를 호출해 200(또는 body.code=200)이면 통과
11+ * - 아니면 로그인(/auth/signin?next=...)으로 보냄
12+ *
13+ * props:
14+ * - requiredRole: 'GUEST'|'MEMBER'|'CORE'|'LEAD'|'ORGANIZER'|'ADMIN' (백엔드 enum과 동일 문자열)
15+ * - nextOverride?: string // 지정 시 이 URL을 next로 사용, 없으면 현재 경로 기준 자동 계산
16+ * - children: ReactNode
17+ */
18+ export default function ApiCodeGuard ( { requiredRole, nextOverride, children } ) {
19+ const router = useRouter ( ) ;
20+ const pathname = usePathname ( ) ;
21+ const searchParams = useSearchParams ( ) ;
22+ const { apiClient } = useAuthenticatedApi ( ) ;
23+
24+ const [ checking , setChecking ] = useState ( true ) ;
25+ const [ allowed , setAllowed ] = useState ( false ) ;
26+
27+ // next URL 계산 (override > 현재 경로)
28+ const nextUrl = useMemo ( ( ) => {
29+ if ( nextOverride ) return encodeURIComponent ( nextOverride ) ;
30+ const q = searchParams ?. toString ( ) ;
31+ return encodeURIComponent ( `${ pathname } ${ q ? `?${ q } ` : '' } ` ) ;
32+ } , [ nextOverride , pathname , searchParams ] ) ;
33+
34+ const cancelledRef = useRef ( false ) ;
35+
36+ useEffect ( ( ) => {
37+ if ( ! requiredRole ) {
38+ // 역할이 없으면 바로 차단
39+ router . replace ( `/auth/signin?next=${ nextUrl } ` ) ;
40+ return ;
4141 }
42- }
43- } ;
4442
45- verifyAccess ( ) ;
43+ cancelledRef . current = false ;
44+
45+ const verify = async ( ) => {
46+ try {
47+ // ✅ 권한 체크: /auth/{role}?next=...
48+ const res = await apiClient . get ( `/auth/${ requiredRole } ` , {
49+ params : { next : decodeURIComponent ( nextUrl ) } , // 서버가 raw URL 원하면 decode해서 전달
50+ } ) ;
51+
52+ if ( cancelledRef . current ) return ;
53+
54+ const okHttp = res ?. status === 200 || res ?. status === 204 ;
55+ const okBody = ( res ?. data ?. code ?? 200 ) === 200 ;
56+
57+ if ( okHttp && okBody ) {
58+ setAllowed ( true ) ;
59+ } else {
60+ router . replace ( `/auth/signin?next=${ nextUrl } ` ) ;
61+ }
62+ } catch {
63+ if ( ! cancelledRef . current ) {
64+ router . replace ( `/auth/signin?next=${ nextUrl } ` ) ;
65+ }
66+ } finally {
67+ if ( ! cancelledRef . current ) setChecking ( false ) ;
68+ }
69+ } ;
4670
47- return ( ) => {
48- cancelled = true ;
49- } ;
50- } , [ apiClient , router ] ) ;
71+ void verify ( ) ;
72+ return ( ) => {
73+ cancelledRef . current = true ;
74+ } ;
75+ } , [ apiClient , requiredRole , nextUrl , router ] ) ;
5176
52- if ( checking ) return < Loader isLoading = { true } /> ;
53- if ( ! allowed ) return null ;
54- return children ;
55- }
77+ if ( checking ) return < Loader isLoading /> ;
78+ if ( ! allowed ) return null ;
79+ return < > { children } </ > ;
80+ }
0 commit comments