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:
parent
21639171e1
commit
116d41ff07
6 changed files with 115 additions and 74 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue