refactor: 컴포넌트 폴더 구조 정리

- PC 컴포넌트를 components/pc/로 이동 (Calendar, ScheduleCard, BirthdayCard, CategoryFilter)
- Mobile 컴포넌트를 components/mobile/로 이동 (Mobile 접두사 제거)
- components/schedule/에는 공용 코드만 유지 (confetti.js, AdminScheduleCard)
- Schedule, Home 페이지의 import 경로 업데이트
- 관련 문서 업데이트

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-22 12:08:47 +09:00
parent b49e5e277d
commit 9d1f54c68a
20 changed files with 373 additions and 301 deletions

View file

@ -1547,18 +1547,54 @@ rm -rf frontend-backup
| Phase | 작업 | 상태 | | Phase | 작업 | 상태 |
|-------|------|------| |-------|------|------|
| 1 | 프로젝트 기반 설정 | ⬜ 대기 | | 1 | 프로젝트 기반 설정 | ✅ 완료 |
| 2 | 유틸리티 및 상수 | ⬜ 대기 | | 2 | 유틸리티 및 상수 | ✅ 완료 |
| 3 | Stores 구축 | ⬜ 대기 | | 3 | Stores 구축 | ✅ 완료 |
| 4 | API 계층 | ⬜ 대기 | | 4 | API 계층 (공개 API) | ✅ 완료 |
| 5 | 커스텀 훅 | ⬜ 대기 | | 5 | 커스텀 훅 | ✅ 완료 |
| 6 | 공통 컴포넌트 | ⬜ 대기 | | 6 | 공통 컴포넌트 | ✅ 완료 |
| 7 | Schedule 페이지 | ⬜ 대기 | | 7 | Schedule 페이지 | ✅ 완료 |
| 8 | Album 페이지 | ⬜ 대기 | | 8 | Album 페이지 | ✅ 완료 |
| 9 | 기타 Public 페이지 | ⬜ 대기 | | 9 | 기타 Public 페이지 (Home, Members) | ✅ 완료 |
| 9-1 | NotFound 페이지 | ⬜ 대기 |
| 10 | Admin 페이지 | ⬜ 대기 | | 10 | Admin 페이지 | ⬜ 대기 |
| 11 | 최종 검증 및 교체 | ⬜ 대기 | | 11 | 최종 검증 및 교체 | ⬜ 대기 |
### 세부 완료 현황
**레이아웃 컴포넌트**
- ✅ PC: Layout, Header, Footer
- ✅ Mobile: Layout
**공개 페이지**
- ✅ Home (PC/Mobile)
- ✅ Members (PC/Mobile)
- ✅ Album, AlbumDetail, AlbumGallery, TrackDetail (PC/Mobile)
- ✅ Schedule, ScheduleDetail, Birthday (PC/Mobile)
- ⬜ NotFound (PC/Mobile)
**일정 컴포넌트**
- ✅ Calendar, MobileCalendar
- ✅ ScheduleCard, MobileScheduleCard, MobileScheduleListCard, MobileScheduleSearchCard
- ✅ BirthdayCard, CategoryFilter, AdminScheduleCard
**공통 컴포넌트**
- ✅ Loading, ErrorBoundary, Toast, Lightbox
- ✅ LightboxIndicator, Tooltip, ScrollToTop
**훅**
- ✅ useAlbumData, useMemberData, useScheduleData, useScheduleSearch
- ✅ useScheduleFiltering, useCalendar, useMediaQuery, useAdminAuth
- ⬜ useToast
**스토어**
- ✅ useAuthStore, useScheduleStore, useUIStore
**관리자 (별도 요청 시 진행)**
- ⬜ 관리자 API 전체
- ⬜ 관리자 컴포넌트 전체
- ⬜ 관리자 페이지 전체
--- ---
## 7. 참고 사항 ## 7. 참고 사항

View file

@ -233,35 +233,56 @@ function App() {
- [x] useMemberData.js - [x] useMemberData.js
- [x] useScheduleData.js - [x] useScheduleData.js
- [x] useScheduleSearch.js - [x] useScheduleSearch.js
- [x] useScheduleFiltering.js
- [x] useCalendar.js - [x] useCalendar.js
- [x] useMediaQuery.js
- [x] useAdminAuth.js - [x] useAdminAuth.js
- [ ] useToast.js - [ ] useToast.js (Toast 컴포넌트는 있으나 훅은 미구현)
### 스토어 (stores/) ### 스토어 (stores/)
- [x] useScheduleStore.js - [x] useScheduleStore.js
- [x] useAuthStore.js - [x] useAuthStore.js
- [x] useUIStore.js
### 상수 (constants/)
- [x] index.js (CATEGORY_ID, MIN_YEAR, SEARCH_LIMIT 등)
### 유틸리티 (utils/) ### 유틸리티 (utils/)
- [x] index.js
- [x] date.js - [x] date.js
- [x] format.js - [x] format.js
- [x] cn.js (className 유틸)
- [x] schedule.js (일정 관련 유틸)
### 공통 컴포넌트 (components/common/) ### 공통 컴포넌트 (components/common/)
- [x] Loading.jsx - [x] Loading.jsx
- [x] ErrorBoundary.jsx - [x] ErrorBoundary.jsx
- [x] Toast.jsx - [x] Toast.jsx
- [x] Lightbox.jsx - [x] Lightbox.jsx
- [ ] LightboxIndicator.jsx - [x] LightboxIndicator.jsx
- [ ] Tooltip.jsx - [x] Tooltip.jsx
- [ ] ScrollToTop.jsx - [x] ScrollToTop.jsx
### PC 레이아웃 컴포넌트 (components/pc/) ### PC 컴포넌트 (components/pc/)
- [ ] Layout.jsx (Outlet 사용) - [x] Layout.jsx
- [ ] Header.jsx - [x] Header.jsx
- [ ] Footer.jsx - [x] Footer.jsx
- [x] Calendar.jsx
- [x] ScheduleCard.jsx
- [x] BirthdayCard.jsx
- [x] CategoryFilter.jsx
### Mobile 레이아웃 컴포넌트 (components/mobile/) ### Mobile 컴포넌트 (components/mobile/)
- [ ] Layout.jsx (Outlet 사용) - [x] Layout.jsx
- [ ] MobileNav.jsx - [x] Calendar.jsx
- [x] ScheduleCard.jsx
- [x] ScheduleListCard.jsx
- [x] ScheduleSearchCard.jsx
- [x] BirthdayCard.jsx
### 공용 일정 컴포넌트 (components/schedule/)
- [x] confetti.js (fireBirthdayConfetti)
- [x] AdminScheduleCard.jsx
### 관리자 컴포넌트 (components/admin/) ### 관리자 컴포넌트 (components/admin/)
- [ ] AdminLayout.jsx - [ ] AdminLayout.jsx
@ -272,32 +293,35 @@ function App() {
- [ ] NumberPicker.jsx - [ ] NumberPicker.jsx
### 페이지 - Home (pages/home/) ### 페이지 - Home (pages/home/)
- [ ] pc/Home.jsx - [x] pc/Home.jsx
- [ ] mobile/Home.jsx - [x] mobile/Home.jsx
### 페이지 - Members (pages/members/) ### 페이지 - Members (pages/members/)
- [ ] pc/Members.jsx - [x] pc/Members.jsx
- [ ] mobile/Members.jsx - [x] mobile/Members.jsx
### 페이지 - Album (pages/album/) ### 페이지 - Album (pages/album/)
- [ ] pc/Album.jsx - [x] pc/Album.jsx
- [ ] pc/AlbumDetail.jsx - [x] pc/AlbumDetail.jsx
- [ ] pc/AlbumGallery.jsx - [x] pc/AlbumGallery.jsx
- [ ] pc/TrackDetail.jsx - [x] pc/TrackDetail.jsx
- [ ] mobile/Album.jsx - [x] mobile/Album.jsx
- [ ] mobile/AlbumDetail.jsx - [x] mobile/AlbumDetail.jsx
- [ ] mobile/AlbumGallery.jsx - [x] mobile/AlbumGallery.jsx
- [ ] mobile/TrackDetail.jsx - [x] mobile/TrackDetail.jsx
### 페이지 - Schedule (pages/schedule/) ### 페이지 - Schedule (pages/schedule/)
- [ ] sections/DefaultSection.jsx - [x] sections/DefaultSection.jsx
- [ ] sections/XSection.jsx - [x] sections/XSection.jsx
- [ ] sections/YoutubeSection.jsx - [x] sections/YoutubeSection.jsx
- [ ] pc/Schedule.jsx - [x] sections/utils.js
- [ ] pc/ScheduleDetail.jsx - [x] sections/index.js
- [ ] pc/Birthday.jsx - [x] pc/Schedule.jsx
- [ ] mobile/Schedule.jsx - [x] pc/ScheduleDetail.jsx
- [ ] mobile/ScheduleDetail.jsx - [x] pc/Birthday.jsx
- [x] mobile/Schedule.jsx
- [x] mobile/ScheduleDetail.jsx
- [x] mobile/Birthday.jsx
### 페이지 - Common (pages/common/) ### 페이지 - Common (pages/common/)
- [ ] pc/NotFound.jsx - [ ] pc/NotFound.jsx
@ -321,18 +345,18 @@ function App() {
- [ ] dict/Manager.jsx - [ ] dict/Manager.jsx
### 기타 ### 기타
- [ ] App.jsx (BrowserView/MobileView 라우팅) - [x] App.jsx (BrowserView/MobileView 라우팅)
- [ ] main.jsx - [x] main.jsx
### CSS 파일 ### CSS 파일
- [x] index.css - [x] index.css
- [ ] mobile.css (모바일 전용 스타일, 달력 등) - [x] mobile.css (모바일 전용 스타일, 달력 등)
- [ ] pc.css (PC 전용 스타일) - [x] pc.css (PC 전용 스타일)
### 기타 파일 ### 기타 파일
- [ ] data/dummy.js (개발용 더미 데이터) - [ ] data/dummy.js (개발용 더미 데이터, 필요 시 추가)
- [ ] .env (VITE_KAKAO_JS_KEY - 콘서트 장소 검색용, 미구현) - [ ] .env (VITE_KAKAO_JS_KEY - 콘서트 장소 검색용, 관리자 기능에서 사용)
- [ ] public/favicon.ico - [ ] public/favicon.ico (필요 시 추가)
## 사용 라이브러리 (package.json) ## 사용 라이브러리 (package.json)
@ -396,27 +420,29 @@ import 'swiper/css';
## 마이그레이션 진행 상황 ## 마이그레이션 진행 상황
### 완료된 작업 (재검토 필요) ### 완료된 작업
- [x] Phase 1: 프로젝트 셋업 - [x] Phase 1: 프로젝트 셋업
- [x] Phase 2: 유틸리티 및 상수 - [x] Phase 2: 유틸리티 및 상수
- [x] Phase 3: Zustand 스토어 - [x] Phase 3: Zustand 스토어
- [x] Phase 4: API 계층 (공개 API) - [x] Phase 4: API 계층 (공개 API)
- [x] Phase 5: 커스텀 훅 (일부) - [x] Phase 5: 커스텀 훅
- [x] Phase 6: 공통 컴포넌트 (일부) - [x] Phase 6: 공통 컴포넌트
- [x] Phase 7: PC/Mobile 레이아웃 컴포넌트
### 구조 리팩토링 필요 - [x] Phase 8: App.jsx (BrowserView/MobileView 라우팅)
- [ ] 기존 코드를 새 폴더 구조로 재배치 - [x] Phase 9: 공개 페이지 전체
- [ ] App.jsx를 BrowserView/MobileView 구조로 변경 - Home, Members, Album, AlbumDetail, AlbumGallery, TrackDetail
- [ ] 레이아웃 컴포넌트 분리 (components/pc, components/mobile) - Schedule, ScheduleDetail, Birthday
### 미완료 작업 ### 미완료 작업
- [ ] 관리자 API 전체
- [ ] 관리자 컴포넌트 전체 #### 공개 영역
- [ ] 관리자 페이지 전체 - [ ] NotFound 페이지 (pc/mobile)
- [ ] 누락된 공통 컴포넌트 (LightboxIndicator, Tooltip, ScrollToTop) - [ ] useToast 훅
- [ ] 누락된 페이지 (AlbumDetail, AlbumGallery, TrackDetail, ScheduleDetail, Birthday)
- [ ] 누락된 훅 (useToast) #### 관리자 영역 (별도 요청 시 진행)
- [ ] PC/Mobile 페이지 분리 - [ ] 관리자 API 전체 (api/admin/)
- [ ] 관리자 컴포넌트 전체 (components/admin/)
- [ ] 관리자 페이지 전체 (pages/admin/)
### 최종 검증 ### 최종 검증
- [ ] 모든 라우트 동작 확인 - [ ] 모든 라우트 동작 확인

View file

@ -0,0 +1,84 @@
import { motion } from 'framer-motion';
import { dayjs, decodeHtmlEntities } from '@/utils';
/**
* Mobile용 생일 카드 컴포넌트
* @param {Object} schedule - 일정 데이터
* @param {boolean} showYear - 년도 표시 여부
* @param {number} delay - 애니메이션 딜레이 ()
* @param {function} onClick - 클릭 핸들러
*/
function BirthdayCard({ schedule, showYear = false, delay = 0, onClick }) {
const scheduleDate = dayjs(schedule.date);
const formatted = {
year: scheduleDate.year(),
month: scheduleDate.month() + 1,
day: scheduleDate.date(),
};
const CardContent = (
<div className="relative overflow-hidden bg-gradient-to-r from-pink-400 via-purple-400 to-indigo-400 rounded-xl shadow-md hover:shadow-lg transition-shadow cursor-pointer">
{/* 배경 장식 */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-3 -right-3 w-16 h-16 bg-white/10 rounded-full" />
<div className="absolute -bottom-4 -left-4 w-20 h-20 bg-white/10 rounded-full" />
<div className="absolute bottom-3 left-8 text-sm">🎉</div>
</div>
<div className="relative flex items-center p-4 gap-3">
{/* 멤버 사진 */}
{schedule.member_image && (
<div className="flex-shrink-0">
<div className="w-14 h-14 rounded-full border-2 border-white/50 shadow-md overflow-hidden bg-white">
<img
src={schedule.member_image}
alt={schedule.member_names}
className="w-full h-full object-cover"
/>
</div>
</div>
)}
{/* 내용 */}
<div className="flex-1 text-white flex items-center gap-2 min-w-0">
<span className="text-2xl flex-shrink-0">🎂</span>
<h3 className="font-bold text-base tracking-wide truncate">
{decodeHtmlEntities(schedule.title)}
</h3>
</div>
{/* 날짜 뱃지 (showYear가 true일 때만 표시) */}
{showYear && (
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-lg px-3 py-1.5 text-center">
<div className="text-white/70 text-[10px] font-medium">{formatted.year}</div>
<div className="text-white/70 text-[10px] font-medium">{formatted.month}</div>
<div className="text-white text-xl font-bold">{formatted.day}</div>
</div>
)}
</div>
</div>
);
// delay motion
if (delay > 0) {
return (
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay, type: 'spring', stiffness: 300, damping: 30 }}
onClick={onClick}
className="cursor-pointer"
>
{CardContent}
</motion.div>
);
}
return (
<div onClick={onClick} className="cursor-pointer">
{CardContent}
</div>
);
}
export default BirthdayCard;

View file

@ -5,7 +5,7 @@ import { getCategoryInfo } from '@/utils';
import { MIN_YEAR, WEEKDAYS } from '@/constants'; import { MIN_YEAR, WEEKDAYS } from '@/constants';
/** /**
* 모바일 달력 컴포넌트 (팝업형) * Mobile용 달력 컴포넌트 (팝업형)
* @param {Date} selectedDate - 선택된 날짜 * @param {Date} selectedDate - 선택된 날짜
* @param {Array} schedules - 일정 목록 ( 표시용) * @param {Array} schedules - 일정 목록 ( 표시용)
* @param {function} onSelectDate - 날짜 선택 핸들러 * @param {function} onSelectDate - 날짜 선택 핸들러
@ -15,7 +15,7 @@ import { MIN_YEAR, WEEKDAYS } from '@/constants';
* @param {boolean} externalShowYearMonth - 외부에서 제어하는 년월 선택 모드 * @param {boolean} externalShowYearMonth - 외부에서 제어하는 년월 선택 모드
* @param {function} onShowYearMonthChange - 년월 선택 모드 변경 콜백 * @param {function} onShowYearMonthChange - 년월 선택 모드 변경 콜백
*/ */
function MobileCalendar({ function Calendar({
selectedDate, selectedDate,
schedules = [], schedules = [],
onSelectDate, onSelectDate,
@ -380,4 +380,4 @@ function MobileCalendar({
); );
} }
export default MobileCalendar; export default Calendar;

View file

@ -2,11 +2,11 @@ import { Clock, Tag, Link2 } from 'lucide-react';
import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils'; import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils';
/** /**
* Mobile 일정 카드 컴포넌트 (홈용) * Mobile 일정 카드 컴포넌트 (홈용)
* 페이지의 다가오는 일정 섹션에서 사용 * 페이지의 다가오는 일정 섹션에서 사용
* 간결한 레이아웃 * 간결한 레이아웃
*/ */
function MobileScheduleCard({ schedule, onClick, className = '' }) { function ScheduleCard({ schedule, onClick, className = '' }) {
const scheduleDate = new Date(schedule.date); const scheduleDate = new Date(schedule.date);
const today = new Date(); const today = new Date();
const currentYear = today.getFullYear(); const currentYear = today.getFullYear();
@ -96,4 +96,4 @@ function MobileScheduleCard({ schedule, onClick, className = '' }) {
); );
} }
export default MobileScheduleCard; export default ScheduleCard;

View file

@ -3,11 +3,11 @@ import { Clock, Link2 } from 'lucide-react';
import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils'; import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils';
/** /**
* Mobile 일정 리스트 카드 컴포넌트 (타임라인용) * Mobile 일정 리스트 카드 컴포넌트 (타임라인용)
* 스케줄 페이지에서 날짜별 일정 목록에 사용 * 스케줄 페이지에서 날짜별 일정 목록에 사용
* 날짜가 이미 헤더에 표시되므로 날짜 없이 표시 * 날짜가 이미 헤더에 표시되므로 날짜 없이 표시
*/ */
function MobileScheduleListCard({ function ScheduleListCard({
schedule, schedule,
onClick, onClick,
delay = 0, delay = 0,
@ -83,4 +83,4 @@ function MobileScheduleListCard({
); );
} }
export default MobileScheduleListCard; export default ScheduleListCard;

View file

@ -3,11 +3,11 @@ import { Clock, Link2 } from 'lucide-react';
import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils'; import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils';
/** /**
* Mobile 일정 검색 카드 컴포넌트 * Mobile 일정 검색 카드 컴포넌트
* 스케줄 페이지의 검색 결과에서 사용 * 스케줄 페이지의 검색 결과에서 사용
* 날짜를 왼쪽에 표시하는 레이아웃 * 날짜를 왼쪽에 표시하는 레이아웃
*/ */
function MobileScheduleSearchCard({ function ScheduleSearchCard({
schedule, schedule,
onClick, onClick,
delay = 0, delay = 0,
@ -112,4 +112,4 @@ function MobileScheduleSearchCard({
); );
} }
export default MobileScheduleSearchCard; export default ScheduleSearchCard;

View file

@ -1 +1,9 @@
// 레이아웃
export { default as Layout } from './Layout'; export { default as Layout } from './Layout';
// 일정 컴포넌트
export { default as Calendar } from './Calendar';
export { default as ScheduleCard } from './ScheduleCard';
export { default as ScheduleListCard } from './ScheduleListCard';
export { default as ScheduleSearchCard } from './ScheduleSearchCard';
export { default as BirthdayCard } from './BirthdayCard';

View file

@ -0,0 +1,60 @@
import { dayjs } from '@/utils';
/**
* PC용 생일 카드 컴포넌트
*/
function BirthdayCard({ schedule, showYear = false, onClick }) {
const scheduleDate = dayjs(schedule.date);
const formatted = {
year: scheduleDate.year(),
month: scheduleDate.month() + 1,
day: scheduleDate.date(),
};
return (
<div
onClick={onClick}
className="relative overflow-hidden bg-gradient-to-r from-pink-400 via-purple-400 to-indigo-400 rounded-2xl shadow-lg hover:shadow-xl transition-shadow cursor-pointer"
>
{/* 배경 장식 */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-4 -right-4 w-24 h-24 bg-white/10 rounded-full" />
<div className="absolute -bottom-6 -left-6 w-32 h-32 bg-white/10 rounded-full" />
<div className="absolute top-1/2 right-1/4 w-16 h-16 bg-white/5 rounded-full" />
<div className="absolute bottom-4 left-12 text-xl animate-pulse">🎉</div>
</div>
<div className="relative flex items-center p-4 gap-4">
{/* 멤버 사진 */}
{schedule.member_image && (
<div className="flex-shrink-0">
<div className="w-20 h-20 rounded-full border-4 border-white/50 shadow-lg overflow-hidden bg-white">
<img
src={schedule.member_image}
alt={schedule.member_names}
className="w-full h-full object-cover"
/>
</div>
</div>
)}
{/* 내용 */}
<div className="flex-1 text-white flex items-center gap-3">
<span className="text-4xl">🎂</span>
<h3 className="font-bold text-2xl tracking-wide">{schedule.title}</h3>
</div>
{/* 날짜 뱃지 */}
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-xl px-4 py-2 text-center">
{showYear && (
<div className="text-white/70 text-xs font-medium">{formatted.year}</div>
)}
<div className="text-white/70 text-xs font-medium">{formatted.month}</div>
<div className="text-white text-2xl font-bold">{formatted.day}</div>
</div>
</div>
</div>
);
}
export default BirthdayCard;

View file

@ -7,7 +7,7 @@ import { MIN_YEAR, WEEKDAYS, MONTH_NAMES } from '@/constants';
const MONTHS = MONTH_NAMES; const MONTHS = MONTH_NAMES;
/** /**
* 달력 컴포넌트 * PC용 달력 컴포넌트
* @param {Date} currentDate - 현재 표시 중인 / * @param {Date} currentDate - 현재 표시 중인 /
* @param {function} onDateChange - / 변경 핸들러 * @param {function} onDateChange - / 변경 핸들러
* @param {string} selectedDate - 선택된 날짜 (YYYY-MM-DD) * @param {string} selectedDate - 선택된 날짜 (YYYY-MM-DD)

View file

@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
/** /**
* 카테고리 필터 컴포넌트 * PC용 카테고리 필터 컴포넌트
* @param {Array} categories - 카테고리 목록 * @param {Array} categories - 카테고리 목록
* @param {Array} selectedCategories - 선택된 카테고리 ID 목록 * @param {Array} selectedCategories - 선택된 카테고리 ID 목록
* @param {function} onToggle - 카테고리 토글 핸들러 * @param {function} onToggle - 카테고리 토글 핸들러

View file

@ -2,7 +2,7 @@ import { Clock, Tag, Link2 } from 'lucide-react';
import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils'; import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils';
/** /**
* PC 일정 카드 컴포넌트 (일반용) * PC 일정 카드 컴포넌트
* , 스케줄 페이지에서 공통으로 사용 * , 스케줄 페이지에서 공통으로 사용
*/ */
function ScheduleCard({ schedule, onClick, className = '' }) { function ScheduleCard({ schedule, onClick, className = '' }) {

View file

@ -1,3 +1,10 @@
// 레이아웃
export { default as Layout } from './Layout'; export { default as Layout } from './Layout';
export { default as Header } from './Header'; export { default as Header } from './Header';
export { default as Footer } from './Footer'; export { default as Footer } from './Footer';
// 일정 컴포넌트
export { default as Calendar } from './Calendar';
export { default as ScheduleCard } from './ScheduleCard';
export { default as BirthdayCard } from './BirthdayCard';
export { default as CategoryFilter } from './CategoryFilter';

View file

@ -1,200 +0,0 @@
import { motion } from 'framer-motion';
import confetti from 'canvas-confetti';
import { dayjs, decodeHtmlEntities } from '@/utils';
/**
* 생일 폭죽 애니메이션
*/
export function fireBirthdayConfetti() {
const duration = 3000;
const animationEnd = Date.now() + duration;
const colors = ['#ff69b4', '#ff1493', '#da70d6', '#ba55d3', '#9370db', '#8a2be2', '#ffd700', '#ff6347'];
const randomInRange = (min, max) => Math.random() * (max - min) + min;
const interval = setInterval(() => {
const timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
clearInterval(interval);
return;
}
const particleCount = 50 * (timeLeft / duration);
//
confetti({
particleCount: Math.floor(particleCount),
startVelocity: 30,
spread: 60,
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
colors,
shapes: ['circle', 'square'],
gravity: 1.2,
scalar: randomInRange(0.8, 1.2),
drift: randomInRange(-0.5, 0.5),
});
//
confetti({
particleCount: Math.floor(particleCount),
startVelocity: 30,
spread: 60,
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
colors,
shapes: ['circle', 'square'],
gravity: 1.2,
scalar: randomInRange(0.8, 1.2),
drift: randomInRange(-0.5, 0.5),
});
}, 250);
//
confetti({
particleCount: 100,
spread: 100,
origin: { x: 0.5, y: 0.6 },
colors,
shapes: ['circle', 'square'],
startVelocity: 45,
});
}
/**
* PC용 생일 카드 컴포넌트
*/
function BirthdayCard({ schedule, showYear = false, onClick }) {
const scheduleDate = dayjs(schedule.date);
const formatted = {
year: scheduleDate.year(),
month: scheduleDate.month() + 1,
day: scheduleDate.date(),
};
return (
<div
onClick={onClick}
className="relative overflow-hidden bg-gradient-to-r from-pink-400 via-purple-400 to-indigo-400 rounded-2xl shadow-lg hover:shadow-xl transition-shadow cursor-pointer"
>
{/* 배경 장식 */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-4 -right-4 w-24 h-24 bg-white/10 rounded-full" />
<div className="absolute -bottom-6 -left-6 w-32 h-32 bg-white/10 rounded-full" />
<div className="absolute top-1/2 right-1/4 w-16 h-16 bg-white/5 rounded-full" />
<div className="absolute bottom-4 left-12 text-xl animate-pulse">🎉</div>
</div>
<div className="relative flex items-center p-4 gap-4">
{/* 멤버 사진 */}
{schedule.member_image && (
<div className="flex-shrink-0">
<div className="w-20 h-20 rounded-full border-4 border-white/50 shadow-lg overflow-hidden bg-white">
<img
src={schedule.member_image}
alt={schedule.member_names}
className="w-full h-full object-cover"
/>
</div>
</div>
)}
{/* 내용 */}
<div className="flex-1 text-white flex items-center gap-3">
<span className="text-4xl">🎂</span>
<h3 className="font-bold text-2xl tracking-wide">{schedule.title}</h3>
</div>
{/* 날짜 뱃지 */}
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-xl px-4 py-2 text-center">
{showYear && (
<div className="text-white/70 text-xs font-medium">{formatted.year}</div>
)}
<div className="text-white/70 text-xs font-medium">{formatted.month}</div>
<div className="text-white text-2xl font-bold">{formatted.day}</div>
</div>
</div>
</div>
);
}
/**
* Mobile용 생일 카드 컴포넌트
* @param {Object} schedule - 일정 데이터
* @param {boolean} showYear - 년도 표시 여부
* @param {number} delay - 애니메이션 딜레이 ()
* @param {function} onClick - 클릭 핸들러
*/
export function MobileBirthdayCard({ schedule, showYear = false, delay = 0, onClick }) {
const scheduleDate = dayjs(schedule.date);
const formatted = {
year: scheduleDate.year(),
month: scheduleDate.month() + 1,
day: scheduleDate.date(),
};
const CardContent = (
<div className="relative overflow-hidden bg-gradient-to-r from-pink-400 via-purple-400 to-indigo-400 rounded-xl shadow-md hover:shadow-lg transition-shadow cursor-pointer">
{/* 배경 장식 */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-3 -right-3 w-16 h-16 bg-white/10 rounded-full" />
<div className="absolute -bottom-4 -left-4 w-20 h-20 bg-white/10 rounded-full" />
<div className="absolute bottom-3 left-8 text-sm">🎉</div>
</div>
<div className="relative flex items-center p-4 gap-3">
{/* 멤버 사진 */}
{schedule.member_image && (
<div className="flex-shrink-0">
<div className="w-14 h-14 rounded-full border-2 border-white/50 shadow-md overflow-hidden bg-white">
<img
src={schedule.member_image}
alt={schedule.member_names}
className="w-full h-full object-cover"
/>
</div>
</div>
)}
{/* 내용 */}
<div className="flex-1 text-white flex items-center gap-2 min-w-0">
<span className="text-2xl flex-shrink-0">🎂</span>
<h3 className="font-bold text-base tracking-wide truncate">
{decodeHtmlEntities(schedule.title)}
</h3>
</div>
{/* 날짜 뱃지 (showYear가 true일 때만 표시) */}
{showYear && (
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-lg px-3 py-1.5 text-center">
<div className="text-white/70 text-[10px] font-medium">{formatted.year}</div>
<div className="text-white/70 text-[10px] font-medium">{formatted.month}</div>
<div className="text-white text-xl font-bold">{formatted.day}</div>
</div>
)}
</div>
</div>
);
// delay motion
if (delay > 0) {
return (
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay, type: 'spring', stiffness: 300, damping: 30 }}
onClick={onClick}
className="cursor-pointer"
>
{CardContent}
</motion.div>
);
}
return (
<div onClick={onClick} className="cursor-pointer">
{CardContent}
</div>
);
}
export default BirthdayCard;

View file

@ -0,0 +1,60 @@
import confetti from 'canvas-confetti';
/**
* 생일 폭죽 애니메이션
* PC/Mobile 공용
*/
export function fireBirthdayConfetti() {
const duration = 3000;
const animationEnd = Date.now() + duration;
const colors = ['#ff69b4', '#ff1493', '#da70d6', '#ba55d3', '#9370db', '#8a2be2', '#ffd700', '#ff6347'];
const randomInRange = (min, max) => Math.random() * (max - min) + min;
const interval = setInterval(() => {
const timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
clearInterval(interval);
return;
}
const particleCount = 50 * (timeLeft / duration);
// 왼쪽에서 발사
confetti({
particleCount: Math.floor(particleCount),
startVelocity: 30,
spread: 60,
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
colors,
shapes: ['circle', 'square'],
gravity: 1.2,
scalar: randomInRange(0.8, 1.2),
drift: randomInRange(-0.5, 0.5),
});
// 오른쪽에서 발사
confetti({
particleCount: Math.floor(particleCount),
startVelocity: 30,
spread: 60,
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
colors,
shapes: ['circle', 'square'],
gravity: 1.2,
scalar: randomInRange(0.8, 1.2),
drift: randomInRange(-0.5, 0.5),
});
}, 250);
// 초기 대형 폭죽
confetti({
particleCount: 100,
spread: 100,
origin: { x: 0.5, y: 0.6 },
colors,
shapes: ['circle', 'square'],
startVelocity: 45,
});
}

View file

@ -1,14 +1,5 @@
// PC 컴포넌트 // 공용 함수
export { default as ScheduleCard } from './ScheduleCard'; export { fireBirthdayConfetti } from './confetti';
// 관리자 컴포넌트
export { default as AdminScheduleCard } from './AdminScheduleCard'; export { default as AdminScheduleCard } from './AdminScheduleCard';
export { default as Calendar } from './Calendar';
// Mobile 컴포넌트
export { default as MobileScheduleCard } from './MobileScheduleCard';
export { default as MobileScheduleListCard } from './MobileScheduleListCard';
export { default as MobileScheduleSearchCard } from './MobileScheduleSearchCard';
export { default as MobileCalendar } from './MobileCalendar';
// 공통 컴포넌트
export { default as CategoryFilter } from './CategoryFilter';
export { default as BirthdayCard, MobileBirthdayCard, fireBirthdayConfetti } from './BirthdayCard';

View file

@ -2,7 +2,7 @@ import { motion } from 'framer-motion';
import { ChevronRight } from 'lucide-react'; import { ChevronRight } from 'lucide-react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useMembers, useAlbums, useUpcomingSchedules } from '@/hooks'; import { useMembers, useAlbums, useUpcomingSchedules } from '@/hooks';
import { MobileScheduleCard } from '@/components'; import { ScheduleCard as MobileScheduleCard } from '@/components/mobile';
/** /**
* Mobile 페이지 * Mobile 페이지

View file

@ -2,7 +2,7 @@ import { motion } from 'framer-motion';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Calendar, ArrowRight, Music } from 'lucide-react'; import { Calendar, ArrowRight, Music } from 'lucide-react';
import { useMembers, useAlbums, useUpcomingSchedules } from '@/hooks'; import { useMembers, useAlbums, useUpcomingSchedules } from '@/hooks';
import { ScheduleCard } from '@/components'; import { ScheduleCard } from '@/components/pc';
/** /**
* PC 페이지 * PC 페이지

View file

@ -11,12 +11,12 @@ import { getSchedules, searchSchedules } from '@/api/schedules';
import { useScheduleStore } from '@/stores'; import { useScheduleStore } from '@/stores';
import { MIN_YEAR, SEARCH_LIMIT } from '@/constants'; import { MIN_YEAR, SEARCH_LIMIT } from '@/constants';
import { import {
MobileCalendar, Calendar as MobileCalendar,
MobileScheduleListCard, ScheduleListCard as MobileScheduleListCard,
MobileScheduleSearchCard, ScheduleSearchCard as MobileScheduleSearchCard,
MobileBirthdayCard, BirthdayCard as MobileBirthdayCard,
fireBirthdayConfetti, } from '@/components/mobile';
} from '@/components/schedule'; import { fireBirthdayConfetti } from '@/components/schedule';
/** /**
* 모바일 일정 페이지 * 모바일 일정 페이지

View file

@ -11,8 +11,8 @@ import {
CategoryFilter, CategoryFilter,
ScheduleCard, ScheduleCard,
BirthdayCard, BirthdayCard,
fireBirthdayConfetti, } from '@/components/pc';
} from '@/components/schedule'; import { fireBirthdayConfetti } from '@/components/schedule';
import { getSchedules, searchSchedules } from '@/api/schedules'; import { getSchedules, searchSchedules } from '@/api/schedules';
import { useScheduleStore } from '@/stores'; import { useScheduleStore } from '@/stores';
import { getTodayKST } from '@/utils'; import { getTodayKST } from '@/utils';