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:
2026-04-12 19:17:17 +09:00
parent 9a8102fb19
commit 4e0716c1cb
5 changed files with 45 additions and 52 deletions

View File

@@ -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 (

View File

@@ -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);
} }

View File

@@ -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>

View File

@@ -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}>