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

- constants에 GROUP_INFO 상수 추가 (데뷔일, 팬덤명)
- PC Home에서 멤버 수 동적 계산 (API 기반)
- mobile/Layout.jsx 컴포넌트 분리 (Header.jsx, BottomNav.jsx)
- 미사용 유틸리티 함수는 관리자 페이지용으로 유지

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-22 16:30:39 +09:00
parent 116d41ff07
commit a0fc67adae
8 changed files with 122 additions and 82 deletions

View file

@ -943,12 +943,14 @@ export function decodeHtmlEntities(text) {
---
## 4. 낮은 우선순위 (Low)
## 4. 낮은 우선순위 (Low) ✅ 완료
### 4.1 하드코딩된 값
### 4.1 하드코딩된 값
**파일**: `pages/home/pc/Home.jsx`, `pages/home/mobile/Home.jsx`
**상태**: ✅ 완료 - `GROUP_INFO` 상수 추가, 멤버 수는 API에서 동적 계산
```javascript
// 하드코딩된 데뷔일
const debutDate = new Date('2018-01-24');
@ -959,51 +961,57 @@ const debutDate = new Date('2018-01-24');
{ value: '2018.01.24', label: '데뷔일' },
```
**해결방법**: 상수 또는 API로 이동
**해결방법**: 상수 + 동적 계산
```javascript
// constants/index.js에 추가
export const GROUP_INFO = {
NAME: 'fromis_9',
NAME_KR: '프로미스나인',
DEBUT_DATE: '2018-01-24',
DEBUT_DATE_DISPLAY: '2018.01.24',
FANDOM_NAME: 'flover',
};
// 또는 멤버 수는 API에서 동적으로 계산
const activeMembers = members.filter(m => !m.is_former);
const memberCount = activeMembers.length;
// 멤버 수는 API에서 동적으로 계산
const activeMemberCount = members.filter(m => !m.is_former).length;
```
---
### 4.2 미사용 유틸리티 함수
### 4.2 미사용 유틸리티 함수 ✅ (유지)
**파일**: `utils/format.js`, `utils/schedule.js`
**상태**: ✅ 검토 완료 - 관리자 페이지 마이그레이션에서 사용 예정으로 유지
**미사용 함수 목록**:
- `formatViewCount()` - 검색 결과 0건
- `formatFileSize()` - 검색 결과 0건
- `groupSchedulesByDate()` - 검색 결과 0건 (정의부만)
- `countByCategory()` - 검색 결과 0건 (정의부만)
**권장**: 관리자 페이지 마이그레이션 후 재검토, 여전히 미사용이면 삭제
**결정**: 관리자 페이지 마이그레이션 후 재검토, 여전히 미사용이면 삭제
---
### 4.3 mobile/Layout.jsx 컴포넌트 분리
### 4.3 mobile/Layout.jsx 컴포넌트 분리
**파일**: `components/mobile/Layout.jsx`
**상태**: ✅ 완료 - Header.jsx, BottomNav.jsx로 분리
**문제**: 한 파일에 3개 컴포넌트 존재
- `MobileHeader`
- `MobileBottomNav`
- `Layout`
**권장**: 별도 파일로 분리
**해결**: 별도 파일로 분리
```
components/mobile/
├── Layout.jsx
├── Layout.jsx # 분리된 컴포넌트 import
├── Header.jsx # MobileHeader
├── BottomNav.jsx # MobileBottomNav
└── index.js
└── index.js # Header, BottomNav export 추가
```
---

View file

@ -277,6 +277,8 @@ function App() {
### Mobile 컴포넌트 (components/mobile/)
- [x] Layout.jsx
- [x] Header.jsx (MobileHeader)
- [x] BottomNav.jsx (MobileBottomNav)
- [x] Calendar.jsx
- [x] ScheduleCard.jsx
- [x] ScheduleListCard.jsx

View file

@ -0,0 +1,45 @@
import { NavLink, useLocation } from 'react-router-dom';
import { Home, Users, Disc3, Calendar } from 'lucide-react';
/**
* 모바일 하단 네비게이션
*/
function MobileBottomNav() {
const location = useLocation();
const navItems = [
{ path: '/', label: '홈', icon: Home },
{ path: '/members', label: '멤버', icon: Users },
{ path: '/album', label: '앨범', icon: Disc3 },
{ path: '/schedule', label: '일정', icon: Calendar },
];
return (
<nav className="flex-shrink-0 bg-white border-t border-gray-200 z-50 safe-area-bottom">
<div className="flex items-center justify-around h-16">
{navItems.map((item) => {
const Icon = item.icon;
const isActive =
location.pathname === item.path ||
(item.path !== '/' && location.pathname.startsWith(item.path));
return (
<NavLink
key={item.path}
to={item.path}
onClick={() => window.scrollTo(0, 0)}
className={`flex flex-col items-center justify-center gap-1 w-full h-full transition-colors ${
isActive ? 'text-primary' : 'text-gray-400'
}`}
>
<Icon size={22} strokeWidth={isActive ? 2.5 : 2} />
<span className="text-xs font-medium">{item.label}</span>
</NavLink>
);
})}
</div>
</nav>
);
}
export default MobileBottomNav;

View file

@ -0,0 +1,26 @@
import { NavLink } from 'react-router-dom';
/**
* 모바일 헤더 컴포넌트
* @param {string} title - 페이지 제목 (없으면 fromis_9)
* @param {boolean} noShadow - 그림자 숨김 여부
*/
function MobileHeader({ title, noShadow = false }) {
return (
<header
className={`bg-white sticky top-0 z-50 ${noShadow ? '' : 'shadow-sm'}`}
>
<div className="flex items-center justify-center h-14 px-4">
{title ? (
<span className="text-xl font-bold text-primary">{title}</span>
) : (
<NavLink to="/" className="text-xl font-bold text-primary">
fromis_9
</NavLink>
)}
</div>
</header>
);
}
export default MobileHeader;

View file

@ -1,70 +1,8 @@
import { NavLink, useLocation } from 'react-router-dom';
import { Home, Users, Disc3, Calendar } from 'lucide-react';
import { useEffect } from 'react';
import MobileHeader from './Header';
import MobileBottomNav from './BottomNav';
import '@/mobile.css';
/**
* 모바일 헤더 컴포넌트
*/
function MobileHeader({ title, noShadow = false }) {
return (
<header
className={`bg-white sticky top-0 z-50 ${noShadow ? '' : 'shadow-sm'}`}
>
<div className="flex items-center justify-center h-14 px-4">
{title ? (
<span className="text-xl font-bold text-primary">{title}</span>
) : (
<NavLink to="/" className="text-xl font-bold text-primary">
fromis_9
</NavLink>
)}
</div>
</header>
);
}
/**
* 모바일 하단 네비게이션
*/
function MobileBottomNav() {
const location = useLocation();
const navItems = [
{ path: '/', label: '홈', icon: Home },
{ path: '/members', label: '멤버', icon: Users },
{ path: '/album', label: '앨범', icon: Disc3 },
{ path: '/schedule', label: '일정', icon: Calendar },
];
return (
<nav className="flex-shrink-0 bg-white border-t border-gray-200 z-50 safe-area-bottom">
<div className="flex items-center justify-around h-16">
{navItems.map((item) => {
const Icon = item.icon;
const isActive =
location.pathname === item.path ||
(item.path !== '/' && location.pathname.startsWith(item.path));
return (
<NavLink
key={item.path}
to={item.path}
onClick={() => window.scrollTo(0, 0)}
className={`flex flex-col items-center justify-center gap-1 w-full h-full transition-colors ${
isActive ? 'text-primary' : 'text-gray-400'
}`}
>
<Icon size={22} strokeWidth={isActive ? 2.5 : 2} />
<span className="text-xs font-medium">{item.label}</span>
</NavLink>
);
})}
</div>
</nav>
);
}
/**
* 모바일 레이아웃 컴포넌트
* @param {React.ReactNode} children - 페이지 컨텐츠

View file

@ -1,5 +1,7 @@
// 레이아웃
export { default as Layout } from './Layout';
export { default as Header } from './Header';
export { default as BottomNav } from './BottomNav';
// 일정 컴포넌트
export { default as Calendar } from './Calendar';

View file

@ -57,3 +57,12 @@ export const MONTH_NAMES = [
'1월', '2월', '3월', '4월', '5월', '6월',
'7월', '8월', '9월', '10월', '11월', '12월',
];
/** 그룹 정보 */
export const GROUP_INFO = {
NAME: 'fromis_9',
NAME_KR: '프로미스나인',
DEBUT_DATE: '2018-01-24',
DEBUT_DATE_DISPLAY: '2018.01.24',
FANDOM_NAME: 'flover',
};

View file

@ -1,8 +1,10 @@
import { useMemo } from 'react';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
import { Calendar, ArrowRight, Music } from 'lucide-react';
import { useMembers, useAlbums, useUpcomingSchedules } from '@/hooks';
import { ScheduleCard } from '@/components/pc';
import { GROUP_INFO } from '@/constants';
/**
* PC 페이지
@ -18,10 +20,18 @@ function Home() {
// (3)
const { data: upcomingSchedules = [] } = useUpcomingSchedules(3);
//
const activeMemberCount = useMemo(
() => members.filter((m) => !m.is_former).length,
[members]
);
// D+Day
const debutDate = new Date('2018-01-24');
const today = new Date();
const dDay = Math.floor((today - debutDate) / (1000 * 60 * 60 * 24)) + 1;
const dDay = useMemo(() => {
const debutDate = new Date(GROUP_INFO.DEBUT_DATE);
const today = new Date();
return Math.floor((today - debutDate) / (1000 * 60 * 60 * 24)) + 1;
}, []);
return (
<div>
@ -67,10 +77,10 @@ function Home() {
}}
>
{[
{ value: '2018.01.24', label: '데뷔일' },
{ value: GROUP_INFO.DEBUT_DATE_DISPLAY, label: '데뷔일' },
{ value: `D+${dDay.toLocaleString()}`, label: 'D+Day' },
{ value: '5', label: '멤버 수' },
{ value: 'flover', label: '팬덤명' },
{ value: String(activeMemberCount || '-'), label: '멤버 수' },
{ value: GROUP_INFO.FANDOM_NAME, label: '팬덤명' },
].map((stat, index) => (
<motion.div
key={index}