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:
parent
b49e5e277d
commit
9d1f54c68a
20 changed files with 373 additions and 301 deletions
|
|
@ -1547,18 +1547,54 @@ rm -rf frontend-backup
|
|||
|
||||
| Phase | 작업 | 상태 |
|
||||
|-------|------|------|
|
||||
| 1 | 프로젝트 기반 설정 | ⬜ 대기 |
|
||||
| 2 | 유틸리티 및 상수 | ⬜ 대기 |
|
||||
| 3 | Stores 구축 | ⬜ 대기 |
|
||||
| 4 | API 계층 | ⬜ 대기 |
|
||||
| 5 | 커스텀 훅 | ⬜ 대기 |
|
||||
| 6 | 공통 컴포넌트 | ⬜ 대기 |
|
||||
| 7 | Schedule 페이지 | ⬜ 대기 |
|
||||
| 8 | Album 페이지 | ⬜ 대기 |
|
||||
| 9 | 기타 Public 페이지 | ⬜ 대기 |
|
||||
| 1 | 프로젝트 기반 설정 | ✅ 완료 |
|
||||
| 2 | 유틸리티 및 상수 | ✅ 완료 |
|
||||
| 3 | Stores 구축 | ✅ 완료 |
|
||||
| 4 | API 계층 (공개 API) | ✅ 완료 |
|
||||
| 5 | 커스텀 훅 | ✅ 완료 |
|
||||
| 6 | 공통 컴포넌트 | ✅ 완료 |
|
||||
| 7 | Schedule 페이지 | ✅ 완료 |
|
||||
| 8 | Album 페이지 | ✅ 완료 |
|
||||
| 9 | 기타 Public 페이지 (Home, Members) | ✅ 완료 |
|
||||
| 9-1 | NotFound 페이지 | ⬜ 대기 |
|
||||
| 10 | Admin 페이지 | ⬜ 대기 |
|
||||
| 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. 참고 사항
|
||||
|
|
|
|||
|
|
@ -233,35 +233,56 @@ function App() {
|
|||
- [x] useMemberData.js
|
||||
- [x] useScheduleData.js
|
||||
- [x] useScheduleSearch.js
|
||||
- [x] useScheduleFiltering.js
|
||||
- [x] useCalendar.js
|
||||
- [x] useMediaQuery.js
|
||||
- [x] useAdminAuth.js
|
||||
- [ ] useToast.js
|
||||
- [ ] useToast.js (Toast 컴포넌트는 있으나 훅은 미구현)
|
||||
|
||||
### 스토어 (stores/)
|
||||
- [x] useScheduleStore.js
|
||||
- [x] useAuthStore.js
|
||||
- [x] useUIStore.js
|
||||
|
||||
### 상수 (constants/)
|
||||
- [x] index.js (CATEGORY_ID, MIN_YEAR, SEARCH_LIMIT 등)
|
||||
|
||||
### 유틸리티 (utils/)
|
||||
- [x] index.js
|
||||
- [x] date.js
|
||||
- [x] format.js
|
||||
- [x] cn.js (className 유틸)
|
||||
- [x] schedule.js (일정 관련 유틸)
|
||||
|
||||
### 공통 컴포넌트 (components/common/)
|
||||
- [x] Loading.jsx
|
||||
- [x] ErrorBoundary.jsx
|
||||
- [x] Toast.jsx
|
||||
- [x] Lightbox.jsx
|
||||
- [ ] LightboxIndicator.jsx
|
||||
- [ ] Tooltip.jsx
|
||||
- [ ] ScrollToTop.jsx
|
||||
- [x] LightboxIndicator.jsx
|
||||
- [x] Tooltip.jsx
|
||||
- [x] ScrollToTop.jsx
|
||||
|
||||
### PC 레이아웃 컴포넌트 (components/pc/)
|
||||
- [ ] Layout.jsx (Outlet 사용)
|
||||
- [ ] Header.jsx
|
||||
- [ ] Footer.jsx
|
||||
### PC 컴포넌트 (components/pc/)
|
||||
- [x] Layout.jsx
|
||||
- [x] Header.jsx
|
||||
- [x] Footer.jsx
|
||||
- [x] Calendar.jsx
|
||||
- [x] ScheduleCard.jsx
|
||||
- [x] BirthdayCard.jsx
|
||||
- [x] CategoryFilter.jsx
|
||||
|
||||
### Mobile 레이아웃 컴포넌트 (components/mobile/)
|
||||
- [ ] Layout.jsx (Outlet 사용)
|
||||
- [ ] MobileNav.jsx
|
||||
### Mobile 컴포넌트 (components/mobile/)
|
||||
- [x] Layout.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/)
|
||||
- [ ] AdminLayout.jsx
|
||||
|
|
@ -272,32 +293,35 @@ function App() {
|
|||
- [ ] NumberPicker.jsx
|
||||
|
||||
### 페이지 - Home (pages/home/)
|
||||
- [ ] pc/Home.jsx
|
||||
- [ ] mobile/Home.jsx
|
||||
- [x] pc/Home.jsx
|
||||
- [x] mobile/Home.jsx
|
||||
|
||||
### 페이지 - Members (pages/members/)
|
||||
- [ ] pc/Members.jsx
|
||||
- [ ] mobile/Members.jsx
|
||||
- [x] pc/Members.jsx
|
||||
- [x] mobile/Members.jsx
|
||||
|
||||
### 페이지 - Album (pages/album/)
|
||||
- [ ] pc/Album.jsx
|
||||
- [ ] pc/AlbumDetail.jsx
|
||||
- [ ] pc/AlbumGallery.jsx
|
||||
- [ ] pc/TrackDetail.jsx
|
||||
- [ ] mobile/Album.jsx
|
||||
- [ ] mobile/AlbumDetail.jsx
|
||||
- [ ] mobile/AlbumGallery.jsx
|
||||
- [ ] mobile/TrackDetail.jsx
|
||||
- [x] pc/Album.jsx
|
||||
- [x] pc/AlbumDetail.jsx
|
||||
- [x] pc/AlbumGallery.jsx
|
||||
- [x] pc/TrackDetail.jsx
|
||||
- [x] mobile/Album.jsx
|
||||
- [x] mobile/AlbumDetail.jsx
|
||||
- [x] mobile/AlbumGallery.jsx
|
||||
- [x] mobile/TrackDetail.jsx
|
||||
|
||||
### 페이지 - Schedule (pages/schedule/)
|
||||
- [ ] sections/DefaultSection.jsx
|
||||
- [ ] sections/XSection.jsx
|
||||
- [ ] sections/YoutubeSection.jsx
|
||||
- [ ] pc/Schedule.jsx
|
||||
- [ ] pc/ScheduleDetail.jsx
|
||||
- [ ] pc/Birthday.jsx
|
||||
- [ ] mobile/Schedule.jsx
|
||||
- [ ] mobile/ScheduleDetail.jsx
|
||||
- [x] sections/DefaultSection.jsx
|
||||
- [x] sections/XSection.jsx
|
||||
- [x] sections/YoutubeSection.jsx
|
||||
- [x] sections/utils.js
|
||||
- [x] sections/index.js
|
||||
- [x] pc/Schedule.jsx
|
||||
- [x] pc/ScheduleDetail.jsx
|
||||
- [x] pc/Birthday.jsx
|
||||
- [x] mobile/Schedule.jsx
|
||||
- [x] mobile/ScheduleDetail.jsx
|
||||
- [x] mobile/Birthday.jsx
|
||||
|
||||
### 페이지 - Common (pages/common/)
|
||||
- [ ] pc/NotFound.jsx
|
||||
|
|
@ -321,18 +345,18 @@ function App() {
|
|||
- [ ] dict/Manager.jsx
|
||||
|
||||
### 기타
|
||||
- [ ] App.jsx (BrowserView/MobileView 라우팅)
|
||||
- [ ] main.jsx
|
||||
- [x] App.jsx (BrowserView/MobileView 라우팅)
|
||||
- [x] main.jsx
|
||||
|
||||
### CSS 파일
|
||||
- [x] index.css
|
||||
- [ ] mobile.css (모바일 전용 스타일, 달력 등)
|
||||
- [ ] pc.css (PC 전용 스타일)
|
||||
- [x] mobile.css (모바일 전용 스타일, 달력 등)
|
||||
- [x] pc.css (PC 전용 스타일)
|
||||
|
||||
### 기타 파일
|
||||
- [ ] data/dummy.js (개발용 더미 데이터)
|
||||
- [ ] .env (VITE_KAKAO_JS_KEY - 콘서트 장소 검색용, 미구현)
|
||||
- [ ] public/favicon.ico
|
||||
- [ ] data/dummy.js (개발용 더미 데이터, 필요 시 추가)
|
||||
- [ ] .env (VITE_KAKAO_JS_KEY - 콘서트 장소 검색용, 관리자 기능에서 사용)
|
||||
- [ ] public/favicon.ico (필요 시 추가)
|
||||
|
||||
## 사용 라이브러리 (package.json)
|
||||
|
||||
|
|
@ -396,27 +420,29 @@ import 'swiper/css';
|
|||
|
||||
## 마이그레이션 진행 상황
|
||||
|
||||
### 완료된 작업 (재검토 필요)
|
||||
### 완료된 작업
|
||||
- [x] Phase 1: 프로젝트 셋업
|
||||
- [x] Phase 2: 유틸리티 및 상수
|
||||
- [x] Phase 3: Zustand 스토어
|
||||
- [x] Phase 4: API 계층 (공개 API만)
|
||||
- [x] Phase 5: 커스텀 훅 (일부)
|
||||
- [x] Phase 6: 공통 컴포넌트 (일부)
|
||||
|
||||
### 구조 리팩토링 필요
|
||||
- [ ] 기존 코드를 새 폴더 구조로 재배치
|
||||
- [ ] App.jsx를 BrowserView/MobileView 구조로 변경
|
||||
- [ ] 레이아웃 컴포넌트 분리 (components/pc, components/mobile)
|
||||
- [x] Phase 4: API 계층 (공개 API)
|
||||
- [x] Phase 5: 커스텀 훅
|
||||
- [x] Phase 6: 공통 컴포넌트
|
||||
- [x] Phase 7: PC/Mobile 레이아웃 컴포넌트
|
||||
- [x] Phase 8: App.jsx (BrowserView/MobileView 라우팅)
|
||||
- [x] Phase 9: 공개 페이지 전체
|
||||
- Home, Members, Album, AlbumDetail, AlbumGallery, TrackDetail
|
||||
- Schedule, ScheduleDetail, Birthday
|
||||
|
||||
### 미완료 작업
|
||||
- [ ] 관리자 API 전체
|
||||
- [ ] 관리자 컴포넌트 전체
|
||||
- [ ] 관리자 페이지 전체
|
||||
- [ ] 누락된 공통 컴포넌트 (LightboxIndicator, Tooltip, ScrollToTop)
|
||||
- [ ] 누락된 페이지 (AlbumDetail, AlbumGallery, TrackDetail, ScheduleDetail, Birthday)
|
||||
- [ ] 누락된 훅 (useToast)
|
||||
- [ ] PC/Mobile 페이지 분리
|
||||
|
||||
#### 공개 영역
|
||||
- [ ] NotFound 페이지 (pc/mobile)
|
||||
- [ ] useToast 훅
|
||||
|
||||
#### 관리자 영역 (별도 요청 시 진행)
|
||||
- [ ] 관리자 API 전체 (api/admin/)
|
||||
- [ ] 관리자 컴포넌트 전체 (components/admin/)
|
||||
- [ ] 관리자 페이지 전체 (pages/admin/)
|
||||
|
||||
### 최종 검증
|
||||
- [ ] 모든 라우트 동작 확인
|
||||
|
|
|
|||
84
frontend-temp/src/components/mobile/BirthdayCard.jsx
Normal file
84
frontend-temp/src/components/mobile/BirthdayCard.jsx
Normal 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;
|
||||
|
|
@ -5,7 +5,7 @@ import { getCategoryInfo } from '@/utils';
|
|||
import { MIN_YEAR, WEEKDAYS } from '@/constants';
|
||||
|
||||
/**
|
||||
* 모바일 달력 컴포넌트 (팝업형)
|
||||
* Mobile용 달력 컴포넌트 (팝업형)
|
||||
* @param {Date} selectedDate - 선택된 날짜
|
||||
* @param {Array} schedules - 일정 목록 (점 표시용)
|
||||
* @param {function} onSelectDate - 날짜 선택 핸들러
|
||||
|
|
@ -15,7 +15,7 @@ import { MIN_YEAR, WEEKDAYS } from '@/constants';
|
|||
* @param {boolean} externalShowYearMonth - 외부에서 제어하는 년월 선택 모드
|
||||
* @param {function} onShowYearMonthChange - 년월 선택 모드 변경 콜백
|
||||
*/
|
||||
function MobileCalendar({
|
||||
function Calendar({
|
||||
selectedDate,
|
||||
schedules = [],
|
||||
onSelectDate,
|
||||
|
|
@ -380,4 +380,4 @@ function MobileCalendar({
|
|||
);
|
||||
}
|
||||
|
||||
export default MobileCalendar;
|
||||
export default Calendar;
|
||||
|
|
@ -2,11 +2,11 @@ import { Clock, Tag, Link2 } from 'lucide-react';
|
|||
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 today = new Date();
|
||||
const currentYear = today.getFullYear();
|
||||
|
|
@ -96,4 +96,4 @@ function MobileScheduleCard({ schedule, onClick, className = '' }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default MobileScheduleCard;
|
||||
export default ScheduleCard;
|
||||
|
|
@ -3,11 +3,11 @@ import { Clock, Link2 } from 'lucide-react';
|
|||
import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils';
|
||||
|
||||
/**
|
||||
* Mobile 일정 리스트 카드 컴포넌트 (타임라인용)
|
||||
* Mobile용 일정 리스트 카드 컴포넌트 (타임라인용)
|
||||
* 스케줄 페이지에서 날짜별 일정 목록에 사용
|
||||
* 날짜가 이미 헤더에 표시되므로 날짜 없이 표시
|
||||
*/
|
||||
function MobileScheduleListCard({
|
||||
function ScheduleListCard({
|
||||
schedule,
|
||||
onClick,
|
||||
delay = 0,
|
||||
|
|
@ -83,4 +83,4 @@ function MobileScheduleListCard({
|
|||
);
|
||||
}
|
||||
|
||||
export default MobileScheduleListCard;
|
||||
export default ScheduleListCard;
|
||||
|
|
@ -3,11 +3,11 @@ import { Clock, Link2 } from 'lucide-react';
|
|||
import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils';
|
||||
|
||||
/**
|
||||
* Mobile 일정 검색 카드 컴포넌트
|
||||
* Mobile용 일정 검색 카드 컴포넌트
|
||||
* 스케줄 페이지의 검색 결과에서 사용
|
||||
* 날짜를 왼쪽에 표시하는 레이아웃
|
||||
*/
|
||||
function MobileScheduleSearchCard({
|
||||
function ScheduleSearchCard({
|
||||
schedule,
|
||||
onClick,
|
||||
delay = 0,
|
||||
|
|
@ -112,4 +112,4 @@ function MobileScheduleSearchCard({
|
|||
);
|
||||
}
|
||||
|
||||
export default MobileScheduleSearchCard;
|
||||
export default ScheduleSearchCard;
|
||||
|
|
@ -1 +1,9 @@
|
|||
// 레이아웃
|
||||
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';
|
||||
|
|
|
|||
60
frontend-temp/src/components/pc/BirthdayCard.jsx
Normal file
60
frontend-temp/src/components/pc/BirthdayCard.jsx
Normal 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;
|
||||
|
|
@ -7,7 +7,7 @@ import { MIN_YEAR, WEEKDAYS, MONTH_NAMES } from '@/constants';
|
|||
const MONTHS = MONTH_NAMES;
|
||||
|
||||
/**
|
||||
* 달력 컴포넌트
|
||||
* PC용 달력 컴포넌트
|
||||
* @param {Date} currentDate - 현재 표시 중인 년/월
|
||||
* @param {function} onDateChange - 년/월 변경 핸들러
|
||||
* @param {string} selectedDate - 선택된 날짜 (YYYY-MM-DD)
|
||||
|
|
@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
|||
import { motion } from 'framer-motion';
|
||||
|
||||
/**
|
||||
* 카테고리 필터 컴포넌트
|
||||
* PC용 카테고리 필터 컴포넌트
|
||||
* @param {Array} categories - 카테고리 목록
|
||||
* @param {Array} selectedCategories - 선택된 카테고리 ID 목록
|
||||
* @param {function} onToggle - 카테고리 토글 핸들러
|
||||
|
|
@ -2,7 +2,7 @@ import { Clock, Tag, Link2 } from 'lucide-react';
|
|||
import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils';
|
||||
|
||||
/**
|
||||
* PC 일정 카드 컴포넌트 (일반용)
|
||||
* PC용 일정 카드 컴포넌트
|
||||
* 홈, 스케줄 페이지에서 공통으로 사용
|
||||
*/
|
||||
function ScheduleCard({ schedule, onClick, className = '' }) {
|
||||
|
|
@ -1,3 +1,10 @@
|
|||
// 레이아웃
|
||||
export { default as Layout } from './Layout';
|
||||
export { default as Header } from './Header';
|
||||
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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
60
frontend-temp/src/components/schedule/confetti.js
Normal file
60
frontend-temp/src/components/schedule/confetti.js
Normal 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,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,14 +1,5 @@
|
|||
// PC 컴포넌트
|
||||
export { default as ScheduleCard } from './ScheduleCard';
|
||||
// 공용 함수
|
||||
export { fireBirthdayConfetti } from './confetti';
|
||||
|
||||
// 관리자 컴포넌트
|
||||
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';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { motion } from 'framer-motion';
|
|||
import { ChevronRight } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useMembers, useAlbums, useUpcomingSchedules } from '@/hooks';
|
||||
import { MobileScheduleCard } from '@/components';
|
||||
import { ScheduleCard as MobileScheduleCard } from '@/components/mobile';
|
||||
|
||||
/**
|
||||
* Mobile 홈 페이지
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ 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';
|
||||
import { ScheduleCard } from '@/components/pc';
|
||||
|
||||
/**
|
||||
* PC 홈 페이지
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ import { getSchedules, searchSchedules } from '@/api/schedules';
|
|||
import { useScheduleStore } from '@/stores';
|
||||
import { MIN_YEAR, SEARCH_LIMIT } from '@/constants';
|
||||
import {
|
||||
MobileCalendar,
|
||||
MobileScheduleListCard,
|
||||
MobileScheduleSearchCard,
|
||||
MobileBirthdayCard,
|
||||
fireBirthdayConfetti,
|
||||
} from '@/components/schedule';
|
||||
Calendar as MobileCalendar,
|
||||
ScheduleListCard as MobileScheduleListCard,
|
||||
ScheduleSearchCard as MobileScheduleSearchCard,
|
||||
BirthdayCard as MobileBirthdayCard,
|
||||
} from '@/components/mobile';
|
||||
import { fireBirthdayConfetti } from '@/components/schedule';
|
||||
|
||||
/**
|
||||
* 모바일 일정 페이지
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import {
|
|||
CategoryFilter,
|
||||
ScheduleCard,
|
||||
BirthdayCard,
|
||||
fireBirthdayConfetti,
|
||||
} from '@/components/schedule';
|
||||
} from '@/components/pc';
|
||||
import { fireBirthdayConfetti } from '@/components/schedule';
|
||||
import { getSchedules, searchSchedules } from '@/api/schedules';
|
||||
import { useScheduleStore } from '@/stores';
|
||||
import { getTodayKST } from '@/utils';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue