diff --git a/src/api/auth.js b/src/api/auth.js index db09468..558beff 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -22,10 +22,10 @@ export async function getSSAFYLoginURL() { return apiFetch('/api/auth/ssafy/login'); } -export async function ssafyCallback(code) { +export async function ssafyCallback(code, state) { return apiFetch('/api/auth/ssafy/callback', { method: 'POST', - body: JSON.stringify({ code }), + body: JSON.stringify({ code, state }), }); } diff --git a/src/api/client.js b/src/api/client.js index b3a60a1..40dacd8 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -76,11 +76,14 @@ export async function apiFetch(path, options = {}, _retryCount = 0) { const token = localStorage.getItem('token'); let res; + const method = (options.method || 'GET').toUpperCase(); + const isIdempotent = method === 'GET' || method === 'HEAD'; + try { res = await doFetch(path, options, token); } catch (e) { - // 네트워크 에러 (오프라인 등) — 재시도 - if (_retryCount < 2) { + // 네트워크 에러 (오프라인 등) — 멱등 요청만 재시도 + if (isIdempotent && _retryCount < 2) { await delay(1000 * (_retryCount + 1)); return apiFetch(path, options, _retryCount + 1); } @@ -111,8 +114,8 @@ export async function apiFetch(path, options = {}, _retryCount = 0) { } } - // 5xx 서버 에러 — 최대 2회 재시도 (exponential backoff) - if (res.status >= 500 && _retryCount < 2) { + // 5xx 서버 에러 — 멱등 요청만 최대 2회 재시도 (exponential backoff) + if (res.status >= 500 && isIdempotent && _retryCount < 2) { await delay(1000 * (_retryCount + 1)); return apiFetch(path, options, _retryCount + 1); } diff --git a/src/api/users.js b/src/api/users.js index c0eca5f..34b57c1 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -1,7 +1,7 @@ import { apiFetch } from './client'; -export function getUsers() { - return apiFetch('/api/users'); +export function getUsers(offset = 0, limit = 20) { + return apiFetch(`/api/users?offset=${offset}&limit=${limit}`); } export function updateUserRole(id, role) { diff --git a/src/components/admin/UserAdmin.jsx b/src/components/admin/UserAdmin.jsx index 278530e..7362c28 100644 --- a/src/components/admin/UserAdmin.jsx +++ b/src/components/admin/UserAdmin.jsx @@ -6,18 +6,24 @@ import { useConfirm } from '../confirm/useConfirm'; import s from './AdminCommon.module.css'; export default function UserAdmin() { + const PAGE_SIZE = 20; const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [fetchError, setFetchError] = useState(false); + const [offset, setOffset] = useState(0); + const [hasMore, setHasMore] = useState(false); const { user: me } = useAuth(); const toast = useToast(); const confirm = useConfirm(); - const load = () => { + const load = (off = offset) => { setLoading(true); setFetchError(false); - getUsers() - .then(setUsers) + getUsers(off, PAGE_SIZE) + .then((data) => { + setUsers(data); + setHasMore(data.length === PAGE_SIZE); + }) .catch((err) => { console.error('유저 목록 로드 실패:', err); setFetchError(true); @@ -25,7 +31,7 @@ export default function UserAdmin() { .finally(() => setLoading(false)); }; // eslint-disable-next-line react-hooks/set-state-in-effect -- initial data fetch on mount - useEffect(() => { load(); }, []); + useEffect(() => { load(0); }, []); const handleRoleToggle = async (u) => { const newRole = u.role === 'admin' ? 'user' : 'admin'; @@ -79,6 +85,14 @@ export default function UserAdmin() { ))} +
+ + +
); } diff --git a/src/pages/SSAFYCallbackPage.jsx b/src/pages/SSAFYCallbackPage.jsx index 7b01215..f697dae 100644 --- a/src/pages/SSAFYCallbackPage.jsx +++ b/src/pages/SSAFYCallbackPage.jsx @@ -19,12 +19,13 @@ export default function SSAFYCallbackPage() { called.current = true; const code = searchParams.get('code'); + const state = searchParams.get('state'); if (!code) { setError('인가 코드가 없습니다.'); // eslint-disable-line react-hooks/set-state-in-effect -- error state from URL param check return; } - ssafyCallback(code) + ssafyCallback(code, state) .then((data) => { setUserFromSSAFY(data); navigate('/', { replace: true });