fix: SSAFY OAuth 로그인 수정 및 API 클라이언트 안정성 개선 (8건)
- SSAFY OAuth state 파라미터 전달 추가 (로그인 불능 해결) - POST/PUT/DELETE 5xx 및 네트워크 에러 재시도 방지 (멱등 요청만 재시도) - UserAdmin 페이지네이션 추가 (offset/limit) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,10 +22,10 @@ export async function getSSAFYLoginURL() {
|
|||||||
return apiFetch('/api/auth/ssafy/login');
|
return apiFetch('/api/auth/ssafy/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ssafyCallback(code) {
|
export async function ssafyCallback(code, state) {
|
||||||
return apiFetch('/api/auth/ssafy/callback', {
|
return apiFetch('/api/auth/ssafy/callback', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ code }),
|
body: JSON.stringify({ code, state }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,11 +76,14 @@ export async function apiFetch(path, options = {}, _retryCount = 0) {
|
|||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
let res;
|
let res;
|
||||||
|
|
||||||
|
const method = (options.method || 'GET').toUpperCase();
|
||||||
|
const isIdempotent = method === 'GET' || method === 'HEAD';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
res = await doFetch(path, options, token);
|
res = await doFetch(path, options, token);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 네트워크 에러 (오프라인 등) — 재시도
|
// 네트워크 에러 (오프라인 등) — 멱등 요청만 재시도
|
||||||
if (_retryCount < 2) {
|
if (isIdempotent && _retryCount < 2) {
|
||||||
await delay(1000 * (_retryCount + 1));
|
await delay(1000 * (_retryCount + 1));
|
||||||
return apiFetch(path, options, _retryCount + 1);
|
return apiFetch(path, options, _retryCount + 1);
|
||||||
}
|
}
|
||||||
@@ -111,8 +114,8 @@ export async function apiFetch(path, options = {}, _retryCount = 0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5xx 서버 에러 — 최대 2회 재시도 (exponential backoff)
|
// 5xx 서버 에러 — 멱등 요청만 최대 2회 재시도 (exponential backoff)
|
||||||
if (res.status >= 500 && _retryCount < 2) {
|
if (res.status >= 500 && isIdempotent && _retryCount < 2) {
|
||||||
await delay(1000 * (_retryCount + 1));
|
await delay(1000 * (_retryCount + 1));
|
||||||
return apiFetch(path, options, _retryCount + 1);
|
return apiFetch(path, options, _retryCount + 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { apiFetch } from './client';
|
import { apiFetch } from './client';
|
||||||
|
|
||||||
export function getUsers() {
|
export function getUsers(offset = 0, limit = 20) {
|
||||||
return apiFetch('/api/users');
|
return apiFetch(`/api/users?offset=${offset}&limit=${limit}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateUserRole(id, role) {
|
export function updateUserRole(id, role) {
|
||||||
|
|||||||
@@ -6,18 +6,24 @@ import { useConfirm } from '../confirm/useConfirm';
|
|||||||
import s from './AdminCommon.module.css';
|
import s from './AdminCommon.module.css';
|
||||||
|
|
||||||
export default function UserAdmin() {
|
export default function UserAdmin() {
|
||||||
|
const PAGE_SIZE = 20;
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [fetchError, setFetchError] = useState(false);
|
const [fetchError, setFetchError] = useState(false);
|
||||||
|
const [offset, setOffset] = useState(0);
|
||||||
|
const [hasMore, setHasMore] = useState(false);
|
||||||
const { user: me } = useAuth();
|
const { user: me } = useAuth();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
|
||||||
const load = () => {
|
const load = (off = offset) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setFetchError(false);
|
setFetchError(false);
|
||||||
getUsers()
|
getUsers(off, PAGE_SIZE)
|
||||||
.then(setUsers)
|
.then((data) => {
|
||||||
|
setUsers(data);
|
||||||
|
setHasMore(data.length === PAGE_SIZE);
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('유저 목록 로드 실패:', err);
|
console.error('유저 목록 로드 실패:', err);
|
||||||
setFetchError(true);
|
setFetchError(true);
|
||||||
@@ -25,7 +31,7 @@ export default function UserAdmin() {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- initial data fetch on mount
|
// 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 handleRoleToggle = async (u) => {
|
||||||
const newRole = u.role === 'admin' ? 'user' : 'admin';
|
const newRole = u.role === 'admin' ? 'user' : 'admin';
|
||||||
@@ -79,6 +85,14 @@ export default function UserAdmin() {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
<div className={s.listActions} style={{ justifyContent: 'center', marginTop: '1rem' }}>
|
||||||
|
<button className={s.btnSecondary} disabled={offset === 0} onClick={() => { const prev = Math.max(0, offset - PAGE_SIZE); setOffset(prev); load(prev); }}>
|
||||||
|
이전
|
||||||
|
</button>
|
||||||
|
<button className={s.btnSecondary} disabled={!hasMore} onClick={() => { const next = offset + PAGE_SIZE; setOffset(next); load(next); }}>
|
||||||
|
다음
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,13 @@ export default function SSAFYCallbackPage() {
|
|||||||
called.current = true;
|
called.current = true;
|
||||||
|
|
||||||
const code = searchParams.get('code');
|
const code = searchParams.get('code');
|
||||||
|
const state = searchParams.get('state');
|
||||||
if (!code) {
|
if (!code) {
|
||||||
setError('인가 코드가 없습니다.'); // eslint-disable-line react-hooks/set-state-in-effect -- error state from URL param check
|
setError('인가 코드가 없습니다.'); // eslint-disable-line react-hooks/set-state-in-effect -- error state from URL param check
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ssafyCallback(code)
|
ssafyCallback(code, state)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setUserFromSSAFY(data);
|
setUserFromSSAFY(data);
|
||||||
navigate('/', { replace: true });
|
navigate('/', { replace: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user