From 97453b1d818730077982a62d6ce98b765dcbdc66 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Thu, 12 Mar 2026 14:37:05 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EA=B8=B0=EB=B0=98=20=EC=A0=84=EC=B2=B4=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=20=E2=80=94=20=EB=B3=B4=EC=95=88,=20=ED=92=88=EC=A7=88,=20UX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - refreshToken 중복 로직 일원화 (동시 호출 방지 포함) - 파일 업로드 401 시 토큰 갱신 후 재시도 추가 - XHR JSON.parse 에러 보호 - index.html lang="ko", title "One of the plans" 변경 - Vite 기본 에셋(vite.svg, react.svg) 및 빈 App.css 제거 - 공지 CRUD API 레이어 분리 (AnnouncementAdmin → announcements.js) - load 함수 useCallback 적용 및 useEffect 의존성 정상화 - 로딩/빈 목록 상태 표시 추가 (AnnouncementBoard, UserAdmin) - 누락 CSS 정의 추가 (announcement-error, announcement-empty) - 로그인/회원가입 빈 필드 클라이언트 검증 추가 - 공지 등록 시 빈 제목/내용 에러 피드백 추가 Co-Authored-By: Claude Opus 4.6 --- index.html | 5 +- public/vite.svg | 1 - src/App.css | 1 - src/api/announcements.js | 18 +++++ src/api/auth.js | 23 +----- src/api/client.js | 4 +- src/assets/react.svg | 1 - src/components/AnnouncementBoard.css | 12 +++ src/components/AnnouncementBoard.jsx | 8 +- src/components/DownloadSection.jsx | 14 ++-- src/components/admin/AdminCommon.css | 7 ++ src/components/admin/AnnouncementAdmin.jsx | 28 +++---- src/components/admin/DownloadAdmin.jsx | 93 ++++++++++++++++------ src/components/admin/UserAdmin.jsx | 14 +++- src/context/AuthContext.jsx | 2 +- src/pages/LoginPage.jsx | 4 + src/pages/RegisterPage.jsx | 4 + 17 files changed, 162 insertions(+), 77 deletions(-) delete mode 100644 public/vite.svg delete mode 100644 src/App.css delete mode 100644 src/assets/react.svg diff --git a/index.html b/index.html index 9afc5b0..baaaf83 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,9 @@ - + - - a301_client + One of the plans
diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 2acca77..0000000 --- a/src/App.css +++ /dev/null @@ -1 +0,0 @@ -/* Global app styles - kept minimal, page-level styles in pages/ */ diff --git a/src/api/announcements.js b/src/api/announcements.js index 6eae046..52fd871 100644 --- a/src/api/announcements.js +++ b/src/api/announcements.js @@ -3,3 +3,21 @@ import { apiFetch } from './client'; export async function getAnnouncements() { return apiFetch('/api/announcements'); } + +export async function createAnnouncement(title, content) { + return apiFetch('/api/announcements', { + method: 'POST', + body: JSON.stringify({ title, content }), + }); +} + +export async function updateAnnouncement(id, title, content) { + return apiFetch(`/api/announcements/${id}`, { + method: 'PUT', + body: JSON.stringify({ title, content }), + }); +} + +export async function deleteAnnouncement(id) { + return apiFetch(`/api/announcements/${id}`, { method: 'DELETE' }); +} diff --git a/src/api/auth.js b/src/api/auth.js index 1206461..fe9d6ed 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -29,25 +29,6 @@ export async function ssafyCallback(code) { }); } -// 토큰을 리프레시하고 새 access token을 반환 -export async function refreshToken() { - const rt = localStorage.getItem('refreshToken'); - if (!rt) throw new Error('no_refresh_token'); - - const res = await fetch( - (import.meta.env.VITE_API_BASE_URL || '') + '/api/auth/refresh', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ refreshToken: rt }), - } - ); - - if (!res.ok) throw new Error('refresh_failed'); - - const data = await res.json(); - localStorage.setItem('token', data.token); - localStorage.setItem('refreshToken', data.refreshToken); - return data.token; -} +// 토큰을 리프레시하고 새 access token을 반환 (동시 호출 방지 포함) +export { tryRefresh as refreshToken } from './client'; diff --git a/src/api/client.js b/src/api/client.js index 2b46a88..2b6073e 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -3,7 +3,7 @@ const BASE = import.meta.env.VITE_API_BASE_URL || ''; // 동시 401 발생 시 refresh를 한 번만 실행하기 위한 Promise 공유 let refreshingPromise = null; -async function tryRefresh() { +export async function tryRefresh() { if (refreshingPromise) return refreshingPromise; refreshingPromise = (async () => { @@ -40,7 +40,7 @@ async function parseError(res) { try { const body = await res.json(); if (body.error) message = body.error; - } catch {} + } catch { /* 응답 바디 파싱 실패 시 statusText 사용 */ } const err = new Error(message); err.status = res.status; return err; diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/AnnouncementBoard.css b/src/components/AnnouncementBoard.css index 30eb071..08c934c 100644 --- a/src/components/AnnouncementBoard.css +++ b/src/components/AnnouncementBoard.css @@ -59,3 +59,15 @@ color: rgba(255, 255, 255, 0.6); line-height: 1.6; } + +.announcement-error { + font-size: 0.9rem; + color: #e57373; + padding: 12px 8px; +} + +.announcement-empty { + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.35); + padding: 12px 8px; +} diff --git a/src/components/AnnouncementBoard.jsx b/src/components/AnnouncementBoard.jsx index beeab97..214a75a 100644 --- a/src/components/AnnouncementBoard.jsx +++ b/src/components/AnnouncementBoard.jsx @@ -5,18 +5,24 @@ import './AnnouncementBoard.css'; export default function AnnouncementBoard() { const [list, setList] = useState([]); const [expanded, setExpanded] = useState(null); + const [loading, setLoading] = useState(true); const [error, setError] = useState(false); useEffect(() => { getAnnouncements() .then(setList) - .catch(() => setError(true)); + .catch(() => setError(true)) + .finally(() => setLoading(false)); }, []); return (

공지사항

+ {loading &&

불러오는 중...

} {error &&

공지사항을 불러오지 못했습니다.

} + {!loading && !error && list.length === 0 && ( +

등록된 공지사항이 없습니다.

+ )}