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