diff --git a/docs/code-improvements.md b/docs/code-improvements.md index 3b85c15..a401b5f 100644 --- a/docs/code-improvements.md +++ b/docs/code-improvements.md @@ -640,12 +640,14 @@ closeConfirm: () => ..., --- -## 3. 중간 우선순위 (Medium) +## 3. 중간 우선순위 (Medium) ✅ 완료 -### 3.1 API 에러 처리 불일치 +### 3.1 API 에러 처리 불일치 ✅ **파일**: `api/client.js` +**상태**: ✅ 완료 - `fetchFormData`에 `requireAuth` 옵션 추가 + **문제**: ```javascript // fetchAuthApi - 토큰 없으면 에러 발생 @@ -688,10 +690,12 @@ export async function fetchFormData(endpoint, formData, method = 'POST', { requi --- -### 3.2 HTTP 헬퍼 함수 중복 +### 3.2 HTTP 헬퍼 함수 중복 ✅ **파일**: `api/client.js` +**상태**: ✅ 완료 - `createMethodHelpers` 팩토리 함수로 통합, `api`/`authApi` 객체 export + **문제**: ```javascript // 8개의 유사한 헬퍼 함수 @@ -736,10 +740,12 @@ export const authApi = createMethodHelpers(fetchAuthApi); --- -### 3.3 useMediaQuery 리스너 메모이제이션 +### 3.3 useMediaQuery 리스너 메모이제이션 ✅ **파일**: `hooks/useMediaQuery.js` +**상태**: ✅ 완료 - `useCallback`으로 핸들러 메모이제이션 + **문제**: ```javascript useEffect(() => { @@ -780,10 +786,12 @@ export function useMediaQuery(query) { --- -### 3.4 useCalendar 반환 객체 메모이제이션 +### 3.4 useCalendar 반환 객체 메모이제이션 ✅ **파일**: `hooks/useCalendar.js` +**상태**: ✅ 완료 - `useMemo`로 반환 객체 메모이제이션 + **문제**: ```javascript return { @@ -825,10 +833,12 @@ return useMemo(() => ({ --- -### 3.5 날짜/시간 추출 함수 중복 +### 3.5 날짜/시간 추출 함수 중복 ✅ **파일**: `utils/date.js`, `utils/schedule.js` +**상태**: ✅ 완료 - `schedule.js`에서 `date.js`의 `extractDate`, `extractTime` 재사용 + **중복 함수**: ```javascript // date.js @@ -858,10 +868,12 @@ export function getScheduleTime(schedule) { --- -### 3.6 decodeHtmlEntities DOM 조작 +### 3.6 decodeHtmlEntities DOM 조작 ✅ **파일**: `utils/format.js` +**상태**: ✅ 완료 - 순수 함수로 변경 (정규식 + 매핑 객체), SSR 호환 + **문제**: ```javascript export function decodeHtmlEntities(text) { diff --git a/frontend-temp/src/api/client.js b/frontend-temp/src/api/client.js index 39e7d8d..4110687 100644 --- a/frontend-temp/src/api/client.js +++ b/frontend-temp/src/api/client.js @@ -87,11 +87,17 @@ export async function fetchAuthApi(endpoint, options = {}) { * @param {string} endpoint - API 엔드포인트 * @param {FormData} formData - 전송할 FormData * @param {string} method - HTTP 메서드 (기본: POST) + * @param {Object} options - 추가 옵션 + * @param {boolean} options.requireAuth - 인증 필수 여부 (기본: true) */ -export async function fetchFormData(endpoint, formData, method = 'POST') { +export async function fetchFormData(endpoint, formData, method = 'POST', { requireAuth = true } = {}) { const token = useAuthStore.getState().token; const url = endpoint.startsWith('/api') ? endpoint : `${API_BASE}${endpoint}`; + if (requireAuth && !token) { + throw new ApiError('인증이 필요합니다.', 401); + } + const headers = {}; if (token) { headers.Authorization = `Bearer ${token}`; @@ -107,46 +113,43 @@ export async function fetchFormData(endpoint, formData, method = 'POST') { } /** - * GET 요청 헬퍼 + * HTTP 메서드 헬퍼 생성기 */ -export const get = (endpoint) => fetchApi(endpoint); -export const authGet = (endpoint) => fetchAuthApi(endpoint); +function createMethodHelpers(baseFetch) { + return { + get: (endpoint) => baseFetch(endpoint), + post: (endpoint, data) => + baseFetch(endpoint, { + method: 'POST', + body: JSON.stringify(data), + }), + put: (endpoint, data) => + baseFetch(endpoint, { + method: 'PUT', + body: JSON.stringify(data), + }), + del: (endpoint) => baseFetch(endpoint, { method: 'DELETE' }), + }; +} /** - * POST 요청 헬퍼 + * 공개 API 헬퍼 + * @example api.get('/albums'), api.post('/albums', data) */ -export const post = (endpoint, data) => - fetchApi(endpoint, { - method: 'POST', - body: JSON.stringify(data), - }); - -export const authPost = (endpoint, data) => - fetchAuthApi(endpoint, { - method: 'POST', - body: JSON.stringify(data), - }); +export const api = createMethodHelpers(fetchApi); /** - * PUT 요청 헬퍼 + * 인증 API 헬퍼 + * @example authApi.get('/admin/stats'), authApi.post('/admin/schedules', data) */ -export const put = (endpoint, data) => - fetchApi(endpoint, { - method: 'PUT', - body: JSON.stringify(data), - }); +export const authApi = createMethodHelpers(fetchAuthApi); -export const authPut = (endpoint, data) => - fetchAuthApi(endpoint, { - method: 'PUT', - body: JSON.stringify(data), - }); - -/** - * DELETE 요청 헬퍼 - */ -export const del = (endpoint) => - fetchApi(endpoint, { method: 'DELETE' }); - -export const authDel = (endpoint) => - fetchAuthApi(endpoint, { method: 'DELETE' }); +// 기존 호환성을 위한 개별 export (점진적 마이그레이션 후 삭제 예정) +export const get = api.get; +export const post = api.post; +export const put = api.put; +export const del = api.del; +export const authGet = authApi.get; +export const authPost = authApi.post; +export const authPut = authApi.put; +export const authDel = authApi.del; diff --git a/frontend-temp/src/hooks/useCalendar.js b/frontend-temp/src/hooks/useCalendar.js index 8a2e190..d645304 100644 --- a/frontend-temp/src/hooks/useCalendar.js +++ b/frontend-temp/src/hooks/useCalendar.js @@ -89,16 +89,30 @@ export function useCalendar(initialDate = new Date()) { [year, month] ); - return { - ...calendarData, - currentDate, - selectedDate, - canGoPrevMonth, - goToPrevMonth, - goToNextMonth, - goToMonth, - goToToday, - selectDate, - setSelectedDate, - }; + // 반환 객체 메모이제이션 + return useMemo( + () => ({ + ...calendarData, + currentDate, + selectedDate, + canGoPrevMonth, + goToPrevMonth, + goToNextMonth, + goToMonth, + goToToday, + selectDate, + setSelectedDate, + }), + [ + calendarData, + currentDate, + selectedDate, + canGoPrevMonth, + goToPrevMonth, + goToNextMonth, + goToMonth, + goToToday, + selectDate, + ] + ); } diff --git a/frontend-temp/src/hooks/useMediaQuery.js b/frontend-temp/src/hooks/useMediaQuery.js index 09a4986..5e6f390 100644 --- a/frontend-temp/src/hooks/useMediaQuery.js +++ b/frontend-temp/src/hooks/useMediaQuery.js @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; /** * 미디어 쿼리 훅 @@ -13,14 +13,19 @@ export function useMediaQuery(query) { return false; }); - // DOM 이벤트 리스너이므로 useEffect 사용 허용 + // 핸들러 메모이제이션 + const handler = useCallback((e) => { + setMatches(e.matches); + }, []); + + // DOM 이벤트 리스너 useEffect(() => { const mediaQuery = window.matchMedia(query); - const handler = (e) => setMatches(e.matches); + setMatches(mediaQuery.matches); mediaQuery.addEventListener('change', handler); return () => mediaQuery.removeEventListener('change', handler); - }, [query]); + }, [query, handler]); return matches; } diff --git a/frontend-temp/src/utils/format.js b/frontend-temp/src/utils/format.js index 031600a..48f46ff 100644 --- a/frontend-temp/src/utils/format.js +++ b/frontend-temp/src/utils/format.js @@ -3,15 +3,30 @@ */ /** - * HTML 엔티티 디코딩 + * HTML 엔티티 매핑 + */ +const HTML_ENTITIES = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", + ''': "'", + ''': "'", + ' ': ' ', +}; + +/** + * HTML 엔티티 디코딩 (순수 함수 - SSR 호환) * @param {string} text - HTML 엔티티가 포함된 텍스트 * @returns {string} 디코딩된 텍스트 */ export const decodeHtmlEntities = (text) => { if (!text) return ''; - const textarea = document.createElement('textarea'); - textarea.innerHTML = text; - return textarea.value; + return text.replace( + /&(?:amp|lt|gt|quot|#39|apos|#x27|nbsp);/g, + (match) => HTML_ENTITIES[match] || match + ); }; /** diff --git a/frontend-temp/src/utils/schedule.js b/frontend-temp/src/utils/schedule.js index 5aca8d1..d974f6b 100644 --- a/frontend-temp/src/utils/schedule.js +++ b/frontend-temp/src/utils/schedule.js @@ -1,6 +1,7 @@ /** * 스케줄 관련 유틸리티 함수 */ +import { extractDate, extractTime } from './date'; /** * 스케줄에서 카테고리 ID 추출 @@ -32,9 +33,7 @@ export function getCategoryInfo(schedule) { * @returns {string} YYYY-MM-DD 형식 날짜 */ export function getScheduleDate(schedule) { - const datetime = schedule.datetime || schedule.date; - if (!datetime) return ''; - return datetime.split(' ')[0].split('T')[0]; + return schedule.date || extractDate(schedule.datetime); } /** @@ -46,14 +45,7 @@ export function getScheduleTime(schedule) { if (schedule.time) { return schedule.time.slice(0, 5); } - const datetime = schedule.datetime; - if (datetime && (datetime.includes(' ') || datetime.includes('T'))) { - const timePart = datetime.includes(' ') - ? datetime.split(' ')[1] - : datetime.split('T')[1]; - return timePart?.slice(0, 5) || null; - } - return null; + return extractTime(schedule.datetime); } /**