refactor: components/ 정리
- ConfirmProvider useMemo 불필요한 래핑 제거 - DownloadAdmin useCallback 적용, toast 중복 제거, eslint-disable 정리 - UserAdmin useCallback 적용, PAGE_SIZE 컴포넌트 밖으로 이동, 페이지네이션 버튼 가독성 개선 - UploadForm 에러 처리 fail 헬퍼로 중복 제거 - DownloadSection 후행 빈 줄 제거 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -114,4 +114,4 @@ export default function DownloadSection() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,33 +1,24 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { getDownloadInfo } from '../../api/download';
|
import { getDownloadInfo } from '../../api/download';
|
||||||
import { useToast } from '../toast/useToast';
|
|
||||||
import UploadForm from './UploadForm';
|
import UploadForm from './UploadForm';
|
||||||
import s from './AdminCommon.module.css';
|
import s from './AdminCommon.module.css';
|
||||||
|
|
||||||
export default function DownloadAdmin() {
|
export default function DownloadAdmin() {
|
||||||
const toast = useToast();
|
|
||||||
const [info, setInfo] = useState(null);
|
const [info, setInfo] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [loadError, setLoadError] = useState('');
|
const [loadError, setLoadError] = useState('');
|
||||||
|
|
||||||
const load = () => {
|
const load = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setLoadError('');
|
setLoadError('');
|
||||||
getDownloadInfo()
|
getDownloadInfo()
|
||||||
.then((data) => {
|
.then((data) => setInfo(data))
|
||||||
setInfo(data);
|
.catch(() => setLoadError('배포 정보를 불러올 수 없습니다.'))
|
||||||
setLoadError('');
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error('다운로드 정보 로드 실패:', err);
|
|
||||||
setLoadError('배포 정보를 불러올 수 없습니다.');
|
|
||||||
toast.error('배포 정보를 불러올 수 없습니다.');
|
|
||||||
})
|
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect, react-hooks/exhaustive-deps -- initial data fetch on mount
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
useEffect(() => { load(); }, []);
|
useEffect(() => { load(); }, [load]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ export default function UploadForm({ title, hint, accept, endpoint, onSuccess })
|
|||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const path = `${endpoint}?filename=${encodeURIComponent(file.name)}`;
|
const path = `${endpoint}?filename=${encodeURIComponent(file.name)}`;
|
||||||
|
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
|
const fail = (msg) => { setError(msg); toast.error(msg); setProgress(0); };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { status, body } = await apiUpload(path, file, (p) => setProgress(p));
|
const { status, body } = await apiUpload(path, file, (p) => setProgress(p));
|
||||||
if (status >= 200 && status < 300) {
|
if (status >= 200 && status < 300) {
|
||||||
@@ -35,28 +36,16 @@ export default function UploadForm({ title, hint, accept, endpoint, onSuccess })
|
|||||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
} else if (status === 413) {
|
} else if (status === 413) {
|
||||||
const msg = '파일 크기가 너무 큽니다. 더 작은 파일을 선택해주세요.';
|
fail('파일 크기가 너무 큽니다. 더 작은 파일을 선택해주세요.');
|
||||||
setError(msg);
|
|
||||||
toast.error(msg);
|
|
||||||
} else if (status === 409) {
|
} else if (status === 409) {
|
||||||
const msg = '동일한 파일이 이미 존재합니다.';
|
fail('동일한 파일이 이미 존재합니다.');
|
||||||
setError(msg);
|
|
||||||
toast.error(msg);
|
|
||||||
} else if (status >= 500) {
|
} else if (status >= 500) {
|
||||||
const msg = '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';
|
fail('서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.');
|
||||||
setError(msg);
|
|
||||||
toast.error(msg);
|
|
||||||
} else {
|
} else {
|
||||||
const msg = body.error || '업로드에 실패했습니다.';
|
fail(body.error || '업로드에 실패했습니다.');
|
||||||
setError(msg);
|
|
||||||
toast.error(msg);
|
|
||||||
setProgress(0);
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
const msg = '네트워크 오류가 발생했습니다.';
|
fail('네트워크 오류가 발생했습니다.');
|
||||||
setError(msg);
|
|
||||||
toast.error(msg);
|
|
||||||
setProgress(0);
|
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
}
|
}
|
||||||
@@ -93,4 +82,4 @@ export default function UploadForm({ title, hint, accept, endpoint, onSuccess })
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { getUsers, updateUserRole, deleteUser } from '../../api/users';
|
import { getUsers, updateUserRole, deleteUser } from '../../api/users';
|
||||||
import { useAuth } from '../../context/useAuth';
|
import { useAuth } from '../../context/useAuth';
|
||||||
import { useToast } from '../toast/useToast';
|
import { useToast } from '../toast/useToast';
|
||||||
import { useConfirm } from '../confirm/useConfirm';
|
import { useConfirm } from '../confirm/useConfirm';
|
||||||
import s from './AdminCommon.module.css';
|
import s from './AdminCommon.module.css';
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
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);
|
||||||
@@ -16,7 +17,7 @@ export default function UserAdmin() {
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
|
||||||
const load = (off = offset) => {
|
const load = useCallback((off = 0) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setFetchError(false);
|
setFetchError(false);
|
||||||
getUsers(off, PAGE_SIZE)
|
getUsers(off, PAGE_SIZE)
|
||||||
@@ -24,14 +25,12 @@ export default function UserAdmin() {
|
|||||||
setUsers(data);
|
setUsers(data);
|
||||||
setHasMore(data.length === PAGE_SIZE);
|
setHasMore(data.length === PAGE_SIZE);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(() => setFetchError(true))
|
||||||
console.error('유저 목록 로드 실패:', err);
|
|
||||||
setFetchError(true);
|
|
||||||
})
|
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
}, []);
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect, react-hooks/exhaustive-deps -- initial data fetch on mount
|
|
||||||
useEffect(() => { load(0); }, []);
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
|
useEffect(() => { load(0); }, [load]);
|
||||||
|
|
||||||
const handleRoleToggle = async (u) => {
|
const handleRoleToggle = async (u) => {
|
||||||
const newRole = u.role === 'admin' ? 'user' : 'admin';
|
const newRole = u.role === 'admin' ? 'user' : 'admin';
|
||||||
@@ -86,10 +85,26 @@ export default function UserAdmin() {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<div className={s.listActions} style={{ justifyContent: 'center', marginTop: '1rem' }}>
|
<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
|
||||||
|
className={s.btnSecondary}
|
||||||
|
disabled={offset === 0}
|
||||||
|
onClick={() => {
|
||||||
|
const prev = Math.max(0, offset - PAGE_SIZE);
|
||||||
|
setOffset(prev);
|
||||||
|
load(prev);
|
||||||
|
}}
|
||||||
|
>
|
||||||
이전
|
이전
|
||||||
</button>
|
</button>
|
||||||
<button className={s.btnSecondary} disabled={!hasMore} onClick={() => { const next = offset + PAGE_SIZE; setOffset(next); load(next); }}>
|
<button
|
||||||
|
className={s.btnSecondary}
|
||||||
|
disabled={!hasMore}
|
||||||
|
onClick={() => {
|
||||||
|
const next = offset + PAGE_SIZE;
|
||||||
|
setOffset(next);
|
||||||
|
load(next);
|
||||||
|
}}
|
||||||
|
>
|
||||||
다음
|
다음
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useCallback, useMemo, useRef } from 'react';
|
import { useState, useCallback, useRef } from 'react';
|
||||||
import { ConfirmContext } from './confirmContextValue';
|
import { ConfirmContext } from './confirmContextValue';
|
||||||
import './Confirm.css';
|
import './Confirm.css';
|
||||||
|
|
||||||
@@ -25,10 +25,8 @@ export function ConfirmProvider({ children }) {
|
|||||||
setDialog(null);
|
setDialog(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const value = useMemo(() => confirm, [confirm]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmContext.Provider value={value}>
|
<ConfirmContext.Provider value={confirm}>
|
||||||
{children}
|
{children}
|
||||||
{dialog && (
|
{dialog && (
|
||||||
<div className="confirm-overlay" onClick={handleCancel}>
|
<div className="confirm-overlay" onClick={handleCancel}>
|
||||||
|
|||||||
Reference in New Issue
Block a user