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');
|
||||
}
|
||||
|
||||
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 }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
</li>
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user