fix: 코드 리뷰 기반 전체 개선 — 보안, 품질, UX
All checks were successful
Client CI/CD / deploy (push) Successful in 30s

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 14:37:05 +09:00
parent c2e3be491d
commit 97453b1d81
17 changed files with 162 additions and 77 deletions

View File

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

View File

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

View File

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