Skip to content

Commit b8191fa

Browse files
authored
Merge pull request #175 from CSE-Shaco/develop
Feat: 권한별 접근 제어를 위한 ApiCodeGuard 적용 및 레이아웃 업데이트
2 parents 55bd1eb + c86f10a commit b8191fa

File tree

5 files changed

+92
-82
lines changed

5 files changed

+92
-82
lines changed

src/app/admin/layout.js

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
1-
//import { Suspense } from "react";
2-
3-
41
import MenuHeader from '@/components/ui/common/MenuHeader';
52
import ApiCodeGuard from '@/components/auth/ApiCodeGuard.jsx';
63

74
export const metadata = {
8-
title: "Admin",
9-
description: "Admin management and participation platform",
5+
title: "Admin", description: "Admin management and participation platform",
106
};
117

12-
export default function AdminLayout({ children }) {
13-
return (
14-
<ApiCodeGuard>
8+
export default function AdminLayout({children}) {
9+
return (<ApiCodeGuard requiredRole="ADMIN" nextOverride="/admin">
1510
<>
16-
<MenuHeader />
11+
<MenuHeader/>
1712
{children}
1813
</>
19-
</ApiCodeGuard>
20-
);
14+
</ApiCodeGuard>);
2115
}

src/app/core-attendance/layout.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const metadata = {
66
};
77

88
export default function CoreAttendanceLayout({children}) {
9-
return (<ApiCodeGuard>
9+
return (<ApiCodeGuard requiredRole="LEAD" nextOverride="/core-attendance">
1010
<>
1111
<MenuHeader/>
1212
{children}

src/app/coreadmin/layout.js

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,14 @@ import MenuHeader from '@/components/ui/common/MenuHeader';
22
import ApiCodeGuard from '@/components/auth/ApiCodeGuard.jsx';
33

44
export const metadata = {
5-
title: 'CoreAdmin',
6-
description: 'Core member application management',
5+
title: 'CoreAdmin', description: 'Core member application management',
76
};
87

9-
export default function CoreAdminLayout({ children }) {
10-
return (
11-
<ApiCodeGuard>
12-
<>
13-
<MenuHeader />
14-
{children}
15-
</>
16-
</ApiCodeGuard>
17-
);
18-
}
19-
20-
8+
export default function CoreAdminLayout({children}) {
9+
return (<ApiCodeGuard requiredRole="ORGANIZER" nextOverride="/coreadmin">
10+
<>
11+
<MenuHeader/>
12+
{children}
13+
</>
14+
</ApiCodeGuard>);
15+
}

src/app/main/layout.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
1-
//import { Suspense } from "react";
21
import MenuHeader from "@/components/ui/common/MenuHeader";
32
import ApiCodeGuard from '@/components/auth/ApiCodeGuard.jsx';
43

54
export const metadata = {
6-
title: "Home",
7-
description: "Home management and participation platform",
5+
title: "Home", description: "Home management and participation platform",
86
};
97

10-
export default function HomeLayout({ children }) {
11-
return (
12-
<ApiCodeGuard>
8+
export default function HomeLayout({children}) {
9+
return (<ApiCodeGuard requiredRole="CORE" nextOverride="/main">
1310
<>
14-
<MenuHeader />
11+
<MenuHeader/>
1512
{children}
1613
</>
17-
</ApiCodeGuard>
18-
);
14+
</ApiCodeGuard>);
1915
}
Lines changed: 72 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,80 @@
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';
65
import { useAuthenticatedApi } from '@/hooks/useAuthenticatedApi';
76
import 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

Comments
 (0)