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

@@ -114,4 +114,4 @@ export default function DownloadSection() {
</div>
</section>
);
}
}

View File

@@ -1,33 +1,24 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { getDownloadInfo } from '../../api/download';
import { useToast } from '../toast/useToast';
import UploadForm from './UploadForm';
import s from './AdminCommon.module.css';
export default function DownloadAdmin() {
const toast = useToast();
const [info, setInfo] = useState(null);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState('');
const load = () => {
const load = useCallback(() => {
setLoading(true);
setLoadError('');
getDownloadInfo()
.then((data) => {
setInfo(data);
setLoadError('');
})
.catch((err) => {
console.error('다운로드 정보 로드 실패:', err);
setLoadError('배포 정보를 불러올 수 없습니다.');
toast.error('배포 정보를 불러올 수 없습니다.');
})
.then((data) => setInfo(data))
.catch(() => setLoadError('배포 정보를 불러올 수 없습니다.'))
.finally(() => setLoading(false));
};
}, []);
// eslint-disable-next-line react-hooks/set-state-in-effect, react-hooks/exhaustive-deps -- initial data fetch on mount
useEffect(() => { load(); }, []);
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => { load(); }, [load]);
if (loading) {
return (

View File

@@ -22,10 +22,11 @@ export default function UploadForm({ title, hint, accept, endpoint, onSuccess })
if (!file) return;
const path = `${endpoint}?filename=${encodeURIComponent(file.name)}`;
setUploading(true);
setError('');
const fail = (msg) => { setError(msg); toast.error(msg); setProgress(0); };
try {
const { status, body } = await apiUpload(path, file, (p) => setProgress(p));
if (status >= 200 && status < 300) {
@@ -35,28 +36,16 @@ export default function UploadForm({ title, hint, accept, endpoint, onSuccess })
if (fileInputRef.current) fileInputRef.current.value = '';
setProgress(0);
} else if (status === 413) {
const msg = '파일 크기가 너무 큽니다. 더 작은 파일을 선택해주세요.';
setError(msg);
toast.error(msg);
fail('파일 크기가 너무 큽니다. 더 작은 파일을 선택해주세요.');
} else if (status === 409) {
const msg = '동일한 파일이 이미 존재합니다.';
setError(msg);
toast.error(msg);
fail('동일한 파일이 이미 존재합니다.');
} else if (status >= 500) {
const msg = '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';
setError(msg);
toast.error(msg);
fail('서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.');
} else {
const msg = body.error || '업로드에 실패했습니다.';
setError(msg);
toast.error(msg);
setProgress(0);
fail(body.error || '업로드에 실패했습니다.');
}
} catch {
const msg = '네트워크 오류가 발생했습니다.';
setError(msg);
toast.error(msg);
setProgress(0);
fail('네트워크 오류가 발생했습니다.');
} finally {
setUploading(false);
}
@@ -93,4 +82,4 @@ export default function UploadForm({ title, hint, accept, endpoint, onSuccess })
</div>
</form>
);
}
}

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 { useAuth } from '../../context/useAuth';
import { useToast } from '../toast/useToast';
import { useConfirm } from '../confirm/useConfirm';
import s from './AdminCommon.module.css';
const PAGE_SIZE = 20;
export default function UserAdmin() {
const PAGE_SIZE = 20;
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [fetchError, setFetchError] = useState(false);
@@ -16,7 +17,7 @@ export default function UserAdmin() {
const toast = useToast();
const confirm = useConfirm();
const load = (off = offset) => {
const load = useCallback((off = 0) => {
setLoading(true);
setFetchError(false);
getUsers(off, PAGE_SIZE)
@@ -24,14 +25,12 @@ export default function UserAdmin() {
setUsers(data);
setHasMore(data.length === PAGE_SIZE);
})
.catch((err) => {
console.error('유저 목록 로드 실패:', err);
setFetchError(true);
})
.catch(() => setFetchError(true))
.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 newRole = u.role === 'admin' ? 'user' : 'admin';
@@ -86,10 +85,26 @@ export default function UserAdmin() {
))}
</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
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
className={s.btnSecondary}
disabled={!hasMore}
onClick={() => {
const next = offset + PAGE_SIZE;
setOffset(next);
load(next);
}}
>
다음
</button>
</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 './Confirm.css';
@@ -25,10 +25,8 @@ export function ConfirmProvider({ children }) {
setDialog(null);
}, []);
const value = useMemo(() => confirm, [confirm]);
return (
<ConfirmContext.Provider value={value}>
<ConfirmContext.Provider value={confirm}>
{children}
{dialog && (
<div className="confirm-overlay" onClick={handleCancel}>