refactor: Medium 우선순위 코드 품질 개선

- API client: fetchFormData에 requireAuth 옵션 추가
- API client: HTTP 헬퍼 함수를 팩토리 함수로 통합 (api, authApi)
- useMediaQuery: 리스너 핸들러 useCallback 메모이제이션
- useCalendar: 반환 객체 useMemo 메모이제이션
- schedule.js: date.js의 extractDate/extractTime 재사용
- format.js: decodeHtmlEntities 순수 함수화 (SSR 호환)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-22 14:25:20 +09:00
parent 21639171e1
commit 116d41ff07
6 changed files with 115 additions and 74 deletions

View file

@ -640,12 +640,14 @@ closeConfirm: () => ...,
--- ---
## 3. 중간 우선순위 (Medium) ## 3. 중간 우선순위 (Medium) ✅ 완료
### 3.1 API 에러 처리 불일치 ### 3.1 API 에러 처리 불일치
**파일**: `api/client.js` **파일**: `api/client.js`
**상태**: ✅ 완료 - `fetchFormData``requireAuth` 옵션 추가
**문제**: **문제**:
```javascript ```javascript
// fetchAuthApi - 토큰 없으면 에러 발생 // fetchAuthApi - 토큰 없으면 에러 발생
@ -688,10 +690,12 @@ export async function fetchFormData(endpoint, formData, method = 'POST', { requi
--- ---
### 3.2 HTTP 헬퍼 함수 중복 ### 3.2 HTTP 헬퍼 함수 중복
**파일**: `api/client.js` **파일**: `api/client.js`
**상태**: ✅ 완료 - `createMethodHelpers` 팩토리 함수로 통합, `api`/`authApi` 객체 export
**문제**: **문제**:
```javascript ```javascript
// 8개의 유사한 헬퍼 함수 // 8개의 유사한 헬퍼 함수
@ -736,10 +740,12 @@ export const authApi = createMethodHelpers(fetchAuthApi);
--- ---
### 3.3 useMediaQuery 리스너 메모이제이션 ### 3.3 useMediaQuery 리스너 메모이제이션
**파일**: `hooks/useMediaQuery.js` **파일**: `hooks/useMediaQuery.js`
**상태**: ✅ 완료 - `useCallback`으로 핸들러 메모이제이션
**문제**: **문제**:
```javascript ```javascript
useEffect(() => { useEffect(() => {
@ -780,10 +786,12 @@ export function useMediaQuery(query) {
--- ---
### 3.4 useCalendar 반환 객체 메모이제이션 ### 3.4 useCalendar 반환 객체 메모이제이션
**파일**: `hooks/useCalendar.js` **파일**: `hooks/useCalendar.js`
**상태**: ✅ 완료 - `useMemo`로 반환 객체 메모이제이션
**문제**: **문제**:
```javascript ```javascript
return { return {
@ -825,10 +833,12 @@ return useMemo(() => ({
--- ---
### 3.5 날짜/시간 추출 함수 중복 ### 3.5 날짜/시간 추출 함수 중복
**파일**: `utils/date.js`, `utils/schedule.js` **파일**: `utils/date.js`, `utils/schedule.js`
**상태**: ✅ 완료 - `schedule.js`에서 `date.js``extractDate`, `extractTime` 재사용
**중복 함수**: **중복 함수**:
```javascript ```javascript
// date.js // date.js
@ -858,10 +868,12 @@ export function getScheduleTime(schedule) {
--- ---
### 3.6 decodeHtmlEntities DOM 조작 ### 3.6 decodeHtmlEntities DOM 조작
**파일**: `utils/format.js` **파일**: `utils/format.js`
**상태**: ✅ 완료 - 순수 함수로 변경 (정규식 + 매핑 객체), SSR 호환
**문제**: **문제**:
```javascript ```javascript
export function decodeHtmlEntities(text) { export function decodeHtmlEntities(text) {

View file

@ -87,11 +87,17 @@ export async function fetchAuthApi(endpoint, options = {}) {
* @param {string} endpoint - API 엔드포인트 * @param {string} endpoint - API 엔드포인트
* @param {FormData} formData - 전송할 FormData * @param {FormData} formData - 전송할 FormData
* @param {string} method - HTTP 메서드 (기본: POST) * @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 token = useAuthStore.getState().token;
const url = endpoint.startsWith('/api') ? endpoint : `${API_BASE}${endpoint}`; const url = endpoint.startsWith('/api') ? endpoint : `${API_BASE}${endpoint}`;
if (requireAuth && !token) {
throw new ApiError('인증이 필요합니다.', 401);
}
const headers = {}; const headers = {};
if (token) { if (token) {
headers.Authorization = `Bearer ${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); function createMethodHelpers(baseFetch) {
export const authGet = (endpoint) => fetchAuthApi(endpoint); return {
get: (endpoint) => baseFetch(endpoint),
/** post: (endpoint, data) =>
* POST 요청 헬퍼 baseFetch(endpoint, {
*/
export const post = (endpoint, data) =>
fetchApi(endpoint, {
method: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(data),
}); }),
put: (endpoint, data) =>
export const authPost = (endpoint, data) => baseFetch(endpoint, {
fetchAuthApi(endpoint, {
method: 'POST',
body: JSON.stringify(data),
});
/**
* PUT 요청 헬퍼
*/
export const put = (endpoint, data) =>
fetchApi(endpoint, {
method: 'PUT', method: 'PUT',
body: JSON.stringify(data), body: JSON.stringify(data),
}); }),
del: (endpoint) => baseFetch(endpoint, { method: 'DELETE' }),
export const authPut = (endpoint, data) => };
fetchAuthApi(endpoint, { }
method: 'PUT',
body: JSON.stringify(data),
});
/** /**
* DELETE 요청 헬퍼 * 공개 API 헬퍼
* @example api.get('/albums'), api.post('/albums', data)
*/ */
export const del = (endpoint) => export const api = createMethodHelpers(fetchApi);
fetchApi(endpoint, { method: 'DELETE' });
export const authDel = (endpoint) => /**
fetchAuthApi(endpoint, { method: 'DELETE' }); * 인증 API 헬퍼
* @example authApi.get('/admin/stats'), authApi.post('/admin/schedules', data)
*/
export const authApi = createMethodHelpers(fetchAuthApi);
// 기존 호환성을 위한 개별 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;

View file

@ -89,7 +89,9 @@ export function useCalendar(initialDate = new Date()) {
[year, month] [year, month]
); );
return { // 반환 객체 메모이제이션
return useMemo(
() => ({
...calendarData, ...calendarData,
currentDate, currentDate,
selectedDate, selectedDate,
@ -100,5 +102,17 @@ export function useCalendar(initialDate = new Date()) {
goToToday, goToToday,
selectDate, selectDate,
setSelectedDate, setSelectedDate,
}; }),
[
calendarData,
currentDate,
selectedDate,
canGoPrevMonth,
goToPrevMonth,
goToNextMonth,
goToMonth,
goToToday,
selectDate,
]
);
} }

View file

@ -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; return false;
}); });
// DOM 이벤트 리스너이므로 useEffect 사용 허용 // 핸들러 메모이제이션
const handler = useCallback((e) => {
setMatches(e.matches);
}, []);
// DOM 이벤트 리스너
useEffect(() => { useEffect(() => {
const mediaQuery = window.matchMedia(query); const mediaQuery = window.matchMedia(query);
const handler = (e) => setMatches(e.matches); setMatches(mediaQuery.matches);
mediaQuery.addEventListener('change', handler); mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler); return () => mediaQuery.removeEventListener('change', handler);
}, [query]); }, [query, handler]);
return matches; return matches;
} }

View file

@ -3,15 +3,30 @@
*/ */
/** /**
* HTML 엔티티 디코딩 * HTML 엔티티 매핑
*/
const HTML_ENTITIES = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&#39;': "'",
'&apos;': "'",
'&#x27;': "'",
'&nbsp;': ' ',
};
/**
* HTML 엔티티 디코딩 (순수 함수 - SSR 호환)
* @param {string} text - HTML 엔티티가 포함된 텍스트 * @param {string} text - HTML 엔티티가 포함된 텍스트
* @returns {string} 디코딩된 텍스트 * @returns {string} 디코딩된 텍스트
*/ */
export const decodeHtmlEntities = (text) => { export const decodeHtmlEntities = (text) => {
if (!text) return ''; if (!text) return '';
const textarea = document.createElement('textarea'); return text.replace(
textarea.innerHTML = text; /&(?:amp|lt|gt|quot|#39|apos|#x27|nbsp);/g,
return textarea.value; (match) => HTML_ENTITIES[match] || match
);
}; };
/** /**

View file

@ -1,6 +1,7 @@
/** /**
* 스케줄 관련 유틸리티 함수 * 스케줄 관련 유틸리티 함수
*/ */
import { extractDate, extractTime } from './date';
/** /**
* 스케줄에서 카테고리 ID 추출 * 스케줄에서 카테고리 ID 추출
@ -32,9 +33,7 @@ export function getCategoryInfo(schedule) {
* @returns {string} YYYY-MM-DD 형식 날짜 * @returns {string} YYYY-MM-DD 형식 날짜
*/ */
export function getScheduleDate(schedule) { export function getScheduleDate(schedule) {
const datetime = schedule.datetime || schedule.date; return schedule.date || extractDate(schedule.datetime);
if (!datetime) return '';
return datetime.split(' ')[0].split('T')[0];
} }
/** /**
@ -46,14 +45,7 @@ export function getScheduleTime(schedule) {
if (schedule.time) { if (schedule.time) {
return schedule.time.slice(0, 5); return schedule.time.slice(0, 5);
} }
const datetime = schedule.datetime; return extractTime(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;
} }
/** /**