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:
parent
116d41ff07
commit
a0fc67adae
8 changed files with 122 additions and 82 deletions
|
|
@ -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 추가
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
45
frontend-temp/src/components/mobile/BottomNav.jsx
Normal file
45
frontend-temp/src/components/mobile/BottomNav.jsx
Normal 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;
|
||||
26
frontend-temp/src/components/mobile/Header.jsx
Normal file
26
frontend-temp/src/components/mobile/Header.jsx
Normal 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;
|
||||
|
|
@ -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 - 페이지 컨텐츠
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 dDay = useMemo(() => {
|
||||
const debutDate = new Date(GROUP_INFO.DEBUT_DATE);
|
||||
const today = new Date();
|
||||
const dDay = Math.floor((today - debutDate) / (1000 * 60 * 60 * 24)) + 1;
|
||||
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}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue