diff --git a/docker-compose.yml b/docker-compose.yml index 1b52ce1..95c2840 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,19 +12,6 @@ services: - app restart: unless-stopped - fromis9-frontend-dev: - build: ./frontend-temp - container_name: fromis9-frontend-dev - labels: - - "com.centurylinklabs.watchtower.enable=false" - volumes: - - ./frontend-temp:/app - depends_on: - - fromis9-backend - networks: - - app - restart: unless-stopped - fromis9-backend: build: ./backend container_name: fromis9-backend diff --git a/docs/architecture.md b/docs/architecture.md index f216854..30ff31d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,7 +4,7 @@ ``` fromis_9/ -├── backend/ # Fastify 백엔드 (현재 사용) +├── backend/ # Fastify 백엔드 │ ├── src/ │ │ ├── config/ │ │ │ ├── index.js # 환경변수 통합 관리 @@ -43,28 +43,15 @@ fromis_9/ │ ├── Dockerfile # 백엔드 컨테이너 │ └── package.json │ -├── backend-backup/ # Express 백엔드 (참조용, 마이그레이션 원본) -│ -├── frontend/ # React 프론트엔드 (레거시, frontend-temp로 대체 예정) +├── frontend/ # React 프론트엔드 │ ├── src/ -│ │ ├── api/ -│ │ │ ├── public/ # 공개 API -│ │ │ └── admin/ # 어드민 API -│ │ ├── pages/ -│ │ │ ├── pc/ # PC 페이지 -│ │ │ └── mobile/ # 모바일 페이지 -│ │ └── ... -│ └── package.json -│ -├── frontend-temp/ # React 프론트엔드 (신규, Strangler Fig 마이그레이션) -│ ├── src/ -│ │ ├── api/ # API 클라이언트 (공유) +│ │ ├── api/ # API 클라이언트 │ │ │ ├── index.js │ │ │ ├── client.js # fetchApi, fetchAuthApi -│ │ │ ├── albums.js -│ │ │ ├── members.js -│ │ │ ├── schedules.js -│ │ │ ├── auth.js +│ │ │ ├── public/ # 공개 API +│ │ │ │ ├── albums.js +│ │ │ │ ├── members.js +│ │ │ │ └── schedules.js │ │ │ └── admin/ # 관리자 API │ │ │ ├── albums.js │ │ │ ├── members.js @@ -72,131 +59,164 @@ fromis_9/ │ │ │ ├── categories.js │ │ │ ├── stats.js │ │ │ ├── bots.js +│ │ │ ├── auth.js │ │ │ └── suggestions.js │ │ │ -│ │ ├── hooks/ # 커스텀 훅 (공유) +│ │ ├── hooks/ # 커스텀 훅 │ │ │ ├── index.js -│ │ │ ├── useAlbumData.js -│ │ │ ├── useMemberData.js -│ │ │ ├── useScheduleData.js -│ │ │ ├── useScheduleSearch.js -│ │ │ ├── useCalendar.js -│ │ │ ├── useToast.js -│ │ │ └── useAdminAuth.js +│ │ │ ├── common/ # 공통 훅 +│ │ │ │ └── useToast.js +│ │ │ └── pc/ +│ │ │ └── admin/ # 관리자 훅 +│ │ │ ├── useAdminAuth.js +│ │ │ └── useScheduleSearch.js │ │ │ -│ │ ├── stores/ # Zustand 스토어 (공유) +│ │ ├── stores/ # Zustand 스토어 │ │ │ ├── index.js │ │ │ ├── useScheduleStore.js │ │ │ └── useAuthStore.js │ │ │ -│ │ ├── utils/ # 유틸리티 (공유) +│ │ ├── utils/ # 유틸리티 │ │ │ ├── index.js -│ │ │ ├── date.js -│ │ │ └── format.js +│ │ │ ├── cn.js # className 병합 +│ │ │ ├── color.js # 색상 상수/유틸 +│ │ │ ├── confetti.js # 생일 축하 효과 +│ │ │ ├── date.js # 날짜 포맷 +│ │ │ ├── format.js # 문자열 포맷 +│ │ │ ├── schedule.js # 일정 관련 유틸 +│ │ │ └── youtube.js # YouTube URL 파싱 │ │ │ │ │ ├── constants/ -│ │ │ └── index.js +│ │ │ └── index.js # 상수 정의 │ │ │ │ │ ├── components/ │ │ │ ├── index.js -│ │ │ ├── common/ # 디바이스 무관 공통 컴포넌트 +│ │ │ ├── common/ # 공통 컴포넌트 │ │ │ │ ├── Loading.jsx │ │ │ │ ├── ErrorBoundary.jsx +│ │ │ │ ├── ErrorMessage.jsx │ │ │ │ ├── Toast.jsx -│ │ │ │ ├── Lightbox.jsx -│ │ │ │ ├── LightboxIndicator.jsx │ │ │ │ ├── Tooltip.jsx +│ │ │ │ ├── Lightbox.jsx +│ │ │ │ ├── MobileLightbox.jsx +│ │ │ │ ├── LightboxIndicator.jsx +│ │ │ │ ├── AnimatedNumber.jsx │ │ │ │ └── ScrollToTop.jsx -│ │ │ ├── pc/ # PC 레이아웃 컴포넌트 -│ │ │ │ ├── Layout.jsx -│ │ │ │ ├── Header.jsx -│ │ │ │ └── Footer.jsx -│ │ │ ├── mobile/ # Mobile 레이아웃 컴포넌트 -│ │ │ │ ├── Layout.jsx -│ │ │ │ └── MobileNav.jsx -│ │ │ └── admin/ # 관리자 컴포넌트 -│ │ │ ├── AdminLayout.jsx -│ │ │ ├── AdminHeader.jsx -│ │ │ ├── ConfirmDialog.jsx -│ │ │ ├── CustomDatePicker.jsx -│ │ │ ├── CustomTimePicker.jsx -│ │ │ └── NumberPicker.jsx +│ │ │ │ +│ │ │ ├── pc/ +│ │ │ │ ├── public/ # PC 공개 컴포넌트 +│ │ │ │ │ ├── layout/ +│ │ │ │ │ │ ├── Layout.jsx +│ │ │ │ │ │ ├── Header.jsx +│ │ │ │ │ │ └── Footer.jsx +│ │ │ │ │ └── schedule/ +│ │ │ │ │ ├── Calendar.jsx +│ │ │ │ │ ├── ScheduleCard.jsx +│ │ │ │ │ ├── BirthdayCard.jsx +│ │ │ │ │ └── CategoryFilter.jsx +│ │ │ │ │ +│ │ │ │ └── admin/ # PC 관리자 컴포넌트 +│ │ │ │ ├── layout/ +│ │ │ │ │ ├── Layout.jsx +│ │ │ │ │ └── Header.jsx +│ │ │ │ ├── common/ +│ │ │ │ │ ├── ConfirmDialog.jsx +│ │ │ │ │ ├── DatePicker.jsx +│ │ │ │ │ ├── TimePicker.jsx +│ │ │ │ │ ├── NumberPicker.jsx +│ │ │ │ │ └── CustomSelect.jsx +│ │ │ │ ├── schedule/ +│ │ │ │ │ ├── AdminScheduleCard.jsx +│ │ │ │ │ ├── ScheduleItem.jsx +│ │ │ │ │ ├── CategorySelector.jsx +│ │ │ │ │ ├── CategoryFormModal.jsx +│ │ │ │ │ ├── MemberSelector.jsx +│ │ │ │ │ ├── ImageUploader.jsx +│ │ │ │ │ ├── LocationSearchDialog.jsx +│ │ │ │ │ └── WordItem.jsx +│ │ │ │ ├── album/ +│ │ │ │ │ ├── TrackItem.jsx +│ │ │ │ │ ├── PhotoGrid.jsx +│ │ │ │ │ ├── PhotoPreviewModal.jsx +│ │ │ │ │ ├── PendingFileItem.jsx +│ │ │ │ │ └── BulkEditPanel.jsx +│ │ │ │ └── bot/ +│ │ │ │ └── BotCard.jsx +│ │ │ │ +│ │ │ └── mobile/ # 모바일 컴포넌트 +│ │ │ ├── layout/ +│ │ │ │ ├── Layout.jsx +│ │ │ │ ├── Header.jsx +│ │ │ │ └── BottomNav.jsx +│ │ │ └── schedule/ +│ │ │ ├── Calendar.jsx +│ │ │ ├── ScheduleCard.jsx +│ │ │ ├── ScheduleListCard.jsx +│ │ │ ├── ScheduleSearchCard.jsx +│ │ │ └── BirthdayCard.jsx │ │ │ │ │ ├── pages/ -│ │ │ ├── index.js +│ │ │ ├── pc/ +│ │ │ │ ├── public/ # PC 공개 페이지 +│ │ │ │ │ ├── home/ +│ │ │ │ │ │ └── Home.jsx +│ │ │ │ │ ├── members/ +│ │ │ │ │ │ └── Members.jsx +│ │ │ │ │ ├── album/ +│ │ │ │ │ │ ├── Album.jsx +│ │ │ │ │ │ ├── AlbumDetail.jsx +│ │ │ │ │ │ ├── AlbumGallery.jsx +│ │ │ │ │ │ └── TrackDetail.jsx +│ │ │ │ │ ├── schedule/ +│ │ │ │ │ │ ├── Schedule.jsx +│ │ │ │ │ │ ├── ScheduleDetail.jsx +│ │ │ │ │ │ ├── Birthday.jsx +│ │ │ │ │ │ └── sections/ +│ │ │ │ │ │ ├── DefaultSection.jsx +│ │ │ │ │ │ ├── YoutubeSection.jsx +│ │ │ │ │ │ └── XSection.jsx +│ │ │ │ │ └── common/ +│ │ │ │ │ └── NotFound.jsx +│ │ │ │ │ +│ │ │ │ └── admin/ # PC 관리자 페이지 +│ │ │ │ ├── Login.jsx +│ │ │ │ ├── Dashboard.jsx +│ │ │ │ ├── members/ +│ │ │ │ │ ├── Members.jsx +│ │ │ │ │ └── MemberEdit.jsx +│ │ │ │ ├── albums/ +│ │ │ │ │ ├── Albums.jsx +│ │ │ │ │ ├── AlbumForm.jsx +│ │ │ │ │ └── AlbumPhotos.jsx +│ │ │ │ └── schedules/ +│ │ │ │ ├── Schedules.jsx +│ │ │ │ ├── ScheduleForm.jsx +│ │ │ │ ├── ScheduleDict.jsx +│ │ │ │ ├── ScheduleBots.jsx +│ │ │ │ ├── ScheduleCategory.jsx +│ │ │ │ ├── form/ +│ │ │ │ │ ├── YouTubeForm.jsx +│ │ │ │ │ └── XForm.jsx +│ │ │ │ └── edit/ +│ │ │ │ └── YouTubeEdit.jsx │ │ │ │ -│ │ │ ├── home/ -│ │ │ │ ├── index.js # export { PCHome, MobileHome } -│ │ │ │ ├── pc/ -│ │ │ │ │ └── Home.jsx -│ │ │ │ └── mobile/ -│ │ │ │ └── Home.jsx -│ │ │ │ -│ │ │ ├── members/ -│ │ │ │ ├── index.js -│ │ │ │ ├── pc/ -│ │ │ │ │ └── Members.jsx -│ │ │ │ └── mobile/ -│ │ │ │ └── Members.jsx -│ │ │ │ -│ │ │ ├── album/ -│ │ │ │ ├── index.js -│ │ │ │ ├── pc/ -│ │ │ │ │ ├── Album.jsx -│ │ │ │ │ ├── AlbumDetail.jsx -│ │ │ │ │ ├── AlbumGallery.jsx -│ │ │ │ │ └── TrackDetail.jsx -│ │ │ │ └── mobile/ -│ │ │ │ ├── Album.jsx -│ │ │ │ ├── AlbumDetail.jsx -│ │ │ │ ├── AlbumGallery.jsx -│ │ │ │ └── TrackDetail.jsx -│ │ │ │ -│ │ │ ├── schedule/ -│ │ │ │ ├── index.js -│ │ │ │ ├── sections/ # 일정 상세 섹션 (PC 전용) -│ │ │ │ │ ├── DefaultSection.jsx -│ │ │ │ │ ├── XSection.jsx -│ │ │ │ │ └── YoutubeSection.jsx -│ │ │ │ ├── pc/ -│ │ │ │ │ ├── Schedule.jsx -│ │ │ │ │ ├── ScheduleDetail.jsx -│ │ │ │ │ └── Birthday.jsx -│ │ │ │ └── mobile/ -│ │ │ │ ├── Schedule.jsx -│ │ │ │ └── ScheduleDetail.jsx -│ │ │ │ -│ │ │ ├── common/ -│ │ │ │ ├── pc/ -│ │ │ │ │ └── NotFound.jsx -│ │ │ │ └── mobile/ -│ │ │ │ └── NotFound.jsx -│ │ │ │ -│ │ │ └── admin/ # 관리자 페이지 (PC 전용) -│ │ │ ├── index.js -│ │ │ ├── Login.jsx -│ │ │ ├── Dashboard.jsx +│ │ │ └── mobile/ # 모바일 페이지 +│ │ │ ├── home/ +│ │ │ │ └── Home.jsx │ │ │ ├── members/ -│ │ │ │ ├── List.jsx -│ │ │ │ └── Edit.jsx -│ │ │ ├── albums/ -│ │ │ │ ├── List.jsx -│ │ │ │ ├── Form.jsx -│ │ │ │ └── Photos.jsx -│ │ │ ├── schedules/ -│ │ │ │ ├── List.jsx -│ │ │ │ ├── Form.jsx -│ │ │ │ ├── YouTubeForm.jsx -│ │ │ │ ├── XForm.jsx -│ │ │ │ └── YouTubeEditForm.jsx -│ │ │ ├── categories/ -│ │ │ │ └── List.jsx -│ │ │ ├── bots/ -│ │ │ │ └── Manager.jsx -│ │ │ └── dict/ -│ │ │ └── Manager.jsx +│ │ │ │ └── Members.jsx +│ │ │ ├── album/ +│ │ │ │ ├── Album.jsx +│ │ │ │ ├── AlbumDetail.jsx +│ │ │ │ ├── AlbumGallery.jsx +│ │ │ │ └── TrackDetail.jsx +│ │ │ ├── schedule/ +│ │ │ │ ├── Schedule.jsx +│ │ │ │ └── ScheduleDetail.jsx +│ │ │ └── common/ +│ │ │ └── NotFound.jsx │ │ │ -│ │ ├── App.jsx # BrowserView/MobileView 라우팅 +│ │ ├── App.jsx # 라우팅 (PC/모바일 분기) │ │ └── main.jsx │ │ │ ├── vite.config.js diff --git a/docs/development.md b/docs/development.md index bed1934..7aa6469 100644 --- a/docs/development.md +++ b/docs/development.md @@ -170,7 +170,8 @@ docker exec caddy caddy reload --config /etc/caddy/Caddyfile ``` src/api/ -├── index.js # fetchApi 유틸 (에러 처리, 토큰 주입) +├── index.js # 전체 export +├── client.js # fetchApi, fetchAuthApi (에러 처리, 토큰 주입) ├── public/ # 공개 API (인증 불필요) │ ├── albums.js # getAlbums, getAlbumByName, getTrack │ ├── members.js # getMembers @@ -179,7 +180,7 @@ src/api/ ├── auth.js # login, verifyToken ├── albums.js # createAlbum, updateAlbum, deleteAlbum, ... ├── bots.js # getBots, startBot, stopBot, syncBot - ├── categories.js # getCategories + ├── categories.js # getCategories, createCategory, updateCategory, ... ├── members.js # updateMember ├── schedules.js # getYoutubeInfo, saveYoutube, getXInfo, saveX, ... ├── stats.js # getStats @@ -192,7 +193,7 @@ src/api/ import { getSchedules, getSchedule } from '@/api/public/schedules'; // 관리자 API -import { getBots, startBot } from '@/api/admin/bots'; +import * as botsApi from '@/api/admin/bots'; ``` ### React Query 사용 (데이터 페칭) diff --git a/docs/frontend-improvement.md b/docs/frontend-improvement.md deleted file mode 100644 index 69318b1..0000000 --- a/docs/frontend-improvement.md +++ /dev/null @@ -1,273 +0,0 @@ -# 일정 관리 페이지 개선 계획 - -## 대상 파일 - -| 파일 | 라인 수 | 역할 | -|------|---------|------| -| Schedules.jsx | 1159 | 일정 목록/검색 | -| ScheduleForm.jsx | 765 | 일정 추가/수정 폼 | -| ScheduleDict.jsx | 572 | 사전 관리 | -| ScheduleBots.jsx | 570 | 봇 관리 | -| ScheduleCategory.jsx | 466 | 카테고리 관리 | - ---- - -## 1. 공통 코드 중복 문제 - -### 1.1 colorMap / getColorStyle 중복 - -**현황:** 3개 파일에서 동일한 코드 반복 - -```javascript -// Schedules.jsx:206-224 -// ScheduleForm.jsx:97-117 -// ScheduleCategory.jsx:24-36 -const colorMap = { - blue: 'bg-blue-500', - green: 'bg-green-500', - // ... -}; - -const getColorStyle = (color) => { - if (!color) return { className: 'bg-gray-500' }; - if (color.startsWith('#')) { - return { style: { backgroundColor: color } }; - } - return { className: colorMap[color] || 'bg-gray-500' }; -}; -``` - -**개선안:** -``` -utils/color.js 생성 -├── COLOR_MAP (상수) -├── COLOR_OPTIONS (ScheduleCategory에서 사용하는 색상 옵션) -└── getColorStyle(color) (함수) -``` - -### 1.2 colorOptions 상수 - -**현황:** ScheduleCategory.jsx에만 있지만 확장성 고려 - -```javascript -// ScheduleCategory.jsx:13-22 -const colorOptions = [ - { id: 'blue', name: '파란색', bg: 'bg-blue-500', hex: '#3b82f6' }, - // ... -]; -``` - -**개선안:** `constants/colors.js` 또는 `utils/color.js`에 통합 - ---- - -## 2. 파일별 개선 사항 - -### 2.1 Schedules.jsx (1159줄) - -#### 검색 관련 상태/로직 복잡 - -**현황:** 검색 관련 상태가 10개 이상, useEffect 5개 - -```javascript -// 검색 관련 상태 (55-65줄) -const [showSuggestions, setShowSuggestions] = useState(false); -const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1); -const [originalSearchQuery, setOriginalSearchQuery] = useState(''); -const [suggestions, setSuggestions] = useState([]); -const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false); -``` - -**개선안:** `useScheduleSearch` 커스텀 훅 분리 -```javascript -// hooks/pc/admin/useScheduleSearch.js -export function useScheduleSearch() { - // 검색 상태 및 로직 캡슐화 - return { - searchInput, setSearchInput, - suggestions, isLoadingSuggestions, - handleSearch, handleSuggestionSelect, - // ... - }; -} -``` - -#### 달력 로직 분리 가능 - -**현황:** 달력 관련 계산이 컴포넌트 내부에 산재 - -```javascript -// 161-181줄 -const year = currentDate.getFullYear(); -const month = currentDate.getMonth(); -const firstDay = new Date(year, month, 1).getDay(); -// ... -``` - -**개선안:** 기존 `utils/date.js`에 달력 헬퍼 함수 추가 또는 `useCalendar` 훅 생성 - ---- - -### 2.2 ScheduleForm.jsx (765줄) - -#### fetchSchedule 함수 중복 설정 - -**현황:** formData를 두 번 설정 (비효율) - -```javascript -// 140-158줄: 첫 번째 setFormData -setFormData({ - title: data.title || '', - startDate: data.date ? formatDate(data.date) : '', - // ... -}); - -// 163-184줄: 두 번째 setFormData (기존 이미지 처리 시) -if (data.images && data.images.length > 0) { - setFormData((prev) => ({ - ...prev, - title: data.title || '', // 중복! - // ... - })); -} -``` - -**개선안:** 하나의 setFormData로 통합 - -```javascript -const initialFormData = { - title: data.title || '', - startDate: data.date ? formatDate(data.date) : '', - // ... - images: data.images?.map((img) => ({ id: img.id, url: img.image_url })) || [], -}; -setFormData(initialFormData); -``` - ---- - -### 2.3 ScheduleDict.jsx (572줄) - -#### generateId 일관성 - -**현황:** `generateId`를 useCallback으로 정의했지만, `parseDict` 내부에서는 인라인으로 같은 로직 사용 - -```javascript -// 113-116줄 -const generateId = useCallback( - () => `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, - [] -); - -// 128줄 (parseDict 내부) -id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, -``` - -**개선안:** `generateId`를 외부 유틸 함수로 분리하거나, parseDict에서 generateId 참조 - ---- - -### 2.4 ScheduleBots.jsx (570줄) - -#### 인라인 컴포넌트 분리 - -**현황:** AnimatedNumber, XIcon, MeilisearchIcon이 파일 내부에 정의 - -```javascript -// 42-65줄 -function AnimatedNumber({ value, className = '' }) { ... } - -// 68-72줄 -const XIcon = ({ size = 20, fill = 'currentColor' }) => ( ... ); - -// 75-128줄 -const MeilisearchIcon = ({ size = 20 }) => ( ... ); -``` - -**개선안:** -``` -components/common/ -├── AnimatedNumber.jsx (재사용 가능한 애니메이션 숫자) -└── icons/ - ├── XIcon.jsx - └── MeilisearchIcon.jsx -``` - -#### 봇 카드 컴포넌트 분리 - -**현황:** 봇 카드 렌더링이 414-559줄로 약 145줄 - -**개선안:** -```javascript -// components/pc/admin/bot/BotCard.jsx -function BotCard({ bot, onToggle, onSync, syncing }) { - // ... -} -``` - ---- - -### 2.5 ScheduleCategory.jsx (466줄) - -#### 모달 컴포넌트 인라인 - -**현황:** 카테고리 추가/수정 모달이 284-445줄로 약 160줄 - -**개선안:** -```javascript -// components/pc/admin/schedule/CategoryFormModal.jsx -function CategoryFormModal({ isOpen, onClose, category, onSave }) { - // 색상 선택, 폼 로직 포함 -} -``` - ---- - -## 3. 개선 우선순위 - -### Phase 1: 중복 코드 제거 (빠른 효과) ✅ 완료 -1. [x] `utils/color.js` 생성 - COLOR_MAP, COLOR_OPTIONS, getColorStyle 통합 -2. [x] 3개 파일에서 import로 교체 - - Schedules.jsx: 1159줄 → 1139줄 (20줄 감소) - - ScheduleForm.jsx: 765줄 → 743줄 (22줄 감소) - - ScheduleCategory.jsx: 466줄 → 441줄 (25줄 감소) - -### Phase 2: 커스텀 훅 분리 (복잡도 감소) ✅ 완료 -1. [x] `useScheduleSearch` 훅 생성 - Schedules.jsx 검색 로직 분리 - - 검색어 자동완성, 무한 스크롤, 키보드 네비게이션 캡슐화 - - Schedules.jsx: 1139줄 → 1009줄 (130줄 감소) -2. [ ] 달력 관련 로직 정리 (선택사항, 현재 규모 적절) - -### Phase 3: 컴포넌트 분리 (재사용성) ✅ 완료 -1. [x] `AnimatedNumber` 공통 컴포넌트화 → components/common/AnimatedNumber.jsx (32줄) -2. [x] `BotCard` 컴포넌트 분리 → components/pc/admin/bot/BotCard.jsx (233줄) -3. [x] `CategoryFormModal` 컴포넌트 분리 → components/pc/admin/schedule/CategoryFormModal.jsx (195줄) -4. [x] SVG 아이콘 분리 (XIcon, MeilisearchIcon) → BotCard.jsx에 포함 - - ScheduleBots.jsx: 570줄 → 339줄 (231줄 감소) - - ScheduleCategory.jsx: 441줄 → 289줄 (152줄 감소) - -### Phase 4: 코드 정리 -1. [ ] ScheduleForm.jsx - fetchSchedule 중복 제거 -2. [ ] ScheduleDict.jsx - generateId 일관성 - ---- - -## 4. 개선 결과 - -| 파일 | 개선 전 | 개선 후 | 감소 | -|------|---------|---------|------| -| Schedules.jsx | 1159줄 | 1009줄 | 150줄 | -| ScheduleForm.jsx | 765줄 | 743줄 | 22줄 | -| ScheduleDict.jsx | 572줄 | 572줄 | - | -| ScheduleBots.jsx | 570줄 | 339줄 | 231줄 | -| ScheduleCategory.jsx | 466줄 | 289줄 | 177줄 | -| **합계** | **3532줄** | **2952줄** | **580줄** | - -### 새로 생성된 파일 -| 파일 | 라인 수 | 역할 | -|------|---------|------| -| utils/color.js | 35줄 | 색상 상수/유틸 | -| hooks/pc/admin/useScheduleSearch.js | 217줄 | 검색 로직 훅 | -| components/common/AnimatedNumber.jsx | 32줄 | 숫자 애니메이션 | -| components/pc/admin/bot/BotCard.jsx | 233줄 | 봇 카드 | -| components/pc/admin/schedule/CategoryFormModal.jsx | 195줄 | 카테고리 폼 모달 | diff --git a/frontend-temp/Dockerfile b/frontend-temp/Dockerfile deleted file mode 100644 index 2f915b7..0000000 --- a/frontend-temp/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -# 개발 모드 -FROM node:20-alpine -WORKDIR /app -CMD ["sh", "-c", "npm install --include=dev && npm run dev -- --host 0.0.0.0"] diff --git a/frontend-temp/index.html b/frontend-temp/index.html deleted file mode 100644 index 31e26d1..0000000 --- a/frontend-temp/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - fromis_9 - 프로미스나인 - - - -
- - - diff --git a/frontend-temp/package-lock.json b/frontend-temp/package-lock.json deleted file mode 100644 index 04fc9a0..0000000 --- a/frontend-temp/package-lock.json +++ /dev/null @@ -1,2106 +0,0 @@ -{ - "name": "fromis9-frontend", - "version": "2.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "fromis9-frontend", - "version": "2.0.0", - "dependencies": { - "@babel/runtime": "^7.28.6", - "@tanstack/react-query": "^5.90.16", - "@tanstack/react-virtual": "^3.13.18", - "canvas-confetti": "^1.9.4", - "clsx": "^2.1.1", - "dayjs": "^1.11.19", - "framer-motion": "^11.0.8", - "lucide-react": "^0.344.0", - "react": "^18.2.0", - "react-calendar": "^6.0.0", - "react-colorful": "^5.6.1", - "react-device-detect": "^2.2.3", - "react-dom": "^18.2.0", - "react-infinite-scroll-component": "^6.1.1", - "react-intersection-observer": "^10.0.0", - "react-ios-time-picker": "^0.2.2", - "react-linkify": "^1.0.0-alpha", - "react-photo-album": "^3.4.0", - "react-router-dom": "^6.22.3", - "react-window": "^2.2.3", - "swiper": "^12.0.3", - "zustand": "^5.0.9" - }, - "devDependencies": { - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "autoprefixer": "^10.4.22", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.18", - "vite": "^5.4.1" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.6" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.6", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.6", - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@remix-run/router": { - "version": "1.23.2", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.55.3", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@tanstack/query-core": { - "version": "5.90.19", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.90.19", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.90.19" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/@tanstack/react-virtual": { - "version": "3.13.18", - "license": "MIT", - "dependencies": { - "@tanstack/virtual-core": "3.13.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@tanstack/virtual-core": { - "version": "3.13.18", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.27", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@wojtekmaj/date-utils": { - "version": "2.0.2", - "license": "MIT", - "funding": { - "url": "https://github.com/wojtekmaj/date-utils?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/autoprefixer": { - "version": "10.4.23", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.16", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001765", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/canvas-confetti": { - "version": "1.9.4", - "license": "ISC", - "funding": { - "type": "donate", - "url": "https://www.paypal.me/kirilvatev" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "devOptional": true, - "license": "MIT" - }, - "node_modules/dayjs": { - "version": "1.11.19", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.267", - "dev": true, - "license": "ISC" - }, - "node_modules/esbuild": { - "version": "0.21.5", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fastq": { - "version": "1.20.1", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/framer-motion": { - "version": "11.18.2", - "license": "MIT", - "dependencies": { - "motion-dom": "^11.18.1", - "motion-utils": "^11.18.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-user-locale": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "memoize": "^10.0.0" - }, - "funding": { - "url": "https://github.com/wojtekmaj/get-user-locale?sponsor=1" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "dev": true, - "license": "MIT" - }, - "node_modules/linkify-it": { - "version": "2.2.0", - "license": "MIT", - "dependencies": { - "uc.micro": "^1.0.1" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.344.0", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/memoize": { - "version": "10.2.0", - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/memoize?sponsor=1" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/motion-dom": { - "version": "11.18.1", - "license": "MIT", - "dependencies": { - "motion-utils": "^11.18.1" - } - }, - "node_modules/motion-utils": { - "version": "11.18.1", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.27", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/prop-types": { - "version": "15.8.1", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/react": { - "version": "18.3.1", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-calendar": { - "version": "6.0.0", - "license": "MIT", - "dependencies": { - "@wojtekmaj/date-utils": "^2.0.2", - "clsx": "^2.0.0", - "get-user-locale": "^3.0.0", - "warning": "^4.0.0" - }, - "funding": { - "url": "https://github.com/wojtekmaj/react-calendar?sponsor=1" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-colorful": { - "version": "5.6.1", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/react-device-detect": { - "version": "2.2.3", - "license": "MIT", - "dependencies": { - "ua-parser-js": "^1.0.33" - }, - "peerDependencies": { - "react": ">= 0.14.0", - "react-dom": ">= 0.14.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-infinite-scroll-component": { - "version": "6.1.1", - "license": "MIT", - "dependencies": { - "throttle-debounce": "^2.1.0" - }, - "peerDependencies": { - "react": ">=16.0.0" - } - }, - "node_modules/react-intersection-observer": { - "version": "10.0.2", - "license": "MIT", - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-ios-time-picker": { - "version": "0.2.2", - "license": "MIT", - "dependencies": { - "react-portal": "^4.2.2" - }, - "peerDependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "license": "MIT" - }, - "node_modules/react-linkify": { - "version": "1.0.0-alpha", - "license": "MIT", - "dependencies": { - "linkify-it": "^2.0.3", - "tlds": "^1.199.0" - } - }, - "node_modules/react-photo-album": { - "version": "3.4.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/igordanchenko" - }, - "peerDependencies": { - "@types/react": "^18 || ^19", - "react": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-portal": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "prop-types": "^15.5.8" - }, - "peerDependencies": { - "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0", - "react-dom": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "6.30.3", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.30.3", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.2", - "react-router": "6.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-window": { - "version": "2.2.5", - "license": "MIT", - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.11", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.55.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.55.3", - "@rollup/rollup-android-arm64": "4.55.3", - "@rollup/rollup-darwin-arm64": "4.55.3", - "@rollup/rollup-darwin-x64": "4.55.3", - "@rollup/rollup-freebsd-arm64": "4.55.3", - "@rollup/rollup-freebsd-x64": "4.55.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.55.3", - "@rollup/rollup-linux-arm-musleabihf": "4.55.3", - "@rollup/rollup-linux-arm64-gnu": "4.55.3", - "@rollup/rollup-linux-arm64-musl": "4.55.3", - "@rollup/rollup-linux-loong64-gnu": "4.55.3", - "@rollup/rollup-linux-loong64-musl": "4.55.3", - "@rollup/rollup-linux-ppc64-gnu": "4.55.3", - "@rollup/rollup-linux-ppc64-musl": "4.55.3", - "@rollup/rollup-linux-riscv64-gnu": "4.55.3", - "@rollup/rollup-linux-riscv64-musl": "4.55.3", - "@rollup/rollup-linux-s390x-gnu": "4.55.3", - "@rollup/rollup-linux-x64-gnu": "4.55.3", - "@rollup/rollup-linux-x64-musl": "4.55.3", - "@rollup/rollup-openbsd-x64": "4.55.3", - "@rollup/rollup-openharmony-arm64": "4.55.3", - "@rollup/rollup-win32-arm64-msvc": "4.55.3", - "@rollup/rollup-win32-ia32-msvc": "4.55.3", - "@rollup/rollup-win32-x64-gnu": "4.55.3", - "@rollup/rollup-win32-x64-msvc": "4.55.3", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sucrase": { - "version": "3.35.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/swiper": { - "version": "12.0.3", - "funding": [ - { - "type": "patreon", - "url": "https://www.patreon.com/swiperjs" - }, - { - "type": "open_collective", - "url": "http://opencollective.com/swiper" - } - ], - "license": "MIT", - "engines": { - "node": ">= 4.7.0" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.19", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/throttle-debounce": { - "version": "2.3.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tlds": { - "version": "1.261.0", - "license": "MIT", - "bin": { - "tlds": "bin.js" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tslib": { - "version": "2.8.1", - "license": "0BSD" - }, - "node_modules/ua-parser-js": { - "version": "1.0.41", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "license": "MIT", - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/uc.micro": { - "version": "1.0.6", - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "5.4.21", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/warning": { - "version": "4.0.3", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "dev": true, - "license": "ISC" - }, - "node_modules/zustand": { - "version": "5.0.10", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - } - } -} diff --git a/frontend-temp/package.json b/frontend-temp/package.json deleted file mode 100644 index 3ae2e24..0000000 --- a/frontend-temp/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "fromis9-frontend", - "private": true, - "version": "2.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "@babel/runtime": "^7.28.6", - "@tanstack/react-query": "^5.90.16", - "@tanstack/react-virtual": "^3.13.18", - "canvas-confetti": "^1.9.4", - "clsx": "^2.1.1", - "dayjs": "^1.11.19", - "framer-motion": "^11.0.8", - "lucide-react": "^0.344.0", - "react": "^18.2.0", - "react-calendar": "^6.0.0", - "react-colorful": "^5.6.1", - "react-device-detect": "^2.2.3", - "react-dom": "^18.2.0", - "react-infinite-scroll-component": "^6.1.1", - "react-intersection-observer": "^10.0.0", - "react-ios-time-picker": "^0.2.2", - "react-linkify": "^1.0.0-alpha", - "react-photo-album": "^3.4.0", - "react-router-dom": "^6.22.3", - "react-window": "^2.2.3", - "swiper": "^12.0.3", - "zustand": "^5.0.9" - }, - "devDependencies": { - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "autoprefixer": "^10.4.22", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.18", - "vite": "^5.4.1" - } -} diff --git a/frontend-temp/postcss.config.js b/frontend-temp/postcss.config.js deleted file mode 100644 index 2aa7205..0000000 --- a/frontend-temp/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/frontend-temp/public/favicon.ico b/frontend-temp/public/favicon.ico deleted file mode 100644 index 4d51a28..0000000 Binary files a/frontend-temp/public/favicon.ico and /dev/null differ diff --git a/frontend-temp/src/App.jsx b/frontend-temp/src/App.jsx deleted file mode 100644 index 51bc0e1..0000000 --- a/frontend-temp/src/App.jsx +++ /dev/null @@ -1,193 +0,0 @@ -import { useEffect } from 'react'; -import { BrowserRouter, Routes, Route } from 'react-router-dom'; -import { BrowserView, MobileView } from 'react-device-detect'; - -// 공통 컴포넌트 -import { ScrollToTop } from '@/components/common'; - -// PC 레이아웃 -import { Layout as PCLayout } from '@/components/pc/public'; - -// Mobile 레이아웃 -import { Layout as MobileLayout } from '@/components/mobile'; - -// PC 공개 페이지 -import PCHome from '@/pages/pc/public/home/Home'; -import PCMembers from '@/pages/pc/public/members/Members'; -import PCSchedule from '@/pages/pc/public/schedule/Schedule'; -import PCScheduleDetail from '@/pages/pc/public/schedule/ScheduleDetail'; -import PCBirthday from '@/pages/pc/public/schedule/Birthday'; -import PCAlbum from '@/pages/pc/public/album/Album'; -import PCAlbumDetail from '@/pages/pc/public/album/AlbumDetail'; -import PCTrackDetail from '@/pages/pc/public/album/TrackDetail'; -import PCAlbumGallery from '@/pages/pc/public/album/AlbumGallery'; -import PCNotFound from '@/pages/pc/public/common/NotFound'; - -// PC 관리자 페이지 -import AdminLogin from '@/pages/pc/admin/login/Login'; -import AdminDashboard from '@/pages/pc/admin/dashboard/Dashboard'; -import AdminMembers from '@/pages/pc/admin/members/Members'; -import AdminMemberEdit from '@/pages/pc/admin/members/MemberEdit'; -import AdminAlbums from '@/pages/pc/admin/albums/Albums'; -import AdminAlbumForm from '@/pages/pc/admin/albums/AlbumForm'; -import AdminAlbumPhotos from '@/pages/pc/admin/albums/AlbumPhotos'; -import AdminSchedules from '@/pages/pc/admin/schedules/Schedules'; -import AdminScheduleForm from '@/pages/pc/admin/schedules/ScheduleForm'; -import AdminScheduleFormPage from '@/pages/pc/admin/schedules/form'; -import AdminYouTubeEditForm from '@/pages/pc/admin/schedules/edit/YouTubeEditForm'; -import AdminScheduleCategory from '@/pages/pc/admin/schedules/ScheduleCategory'; -import AdminScheduleDict from '@/pages/pc/admin/schedules/ScheduleDict'; -import AdminScheduleBots from '@/pages/pc/admin/schedules/ScheduleBots'; -import AdminNotFound from '@/pages/pc/admin/common/NotFound'; - -// Mobile 페이지 -import MobileHome from '@/pages/mobile/home/Home'; -import MobileMembers from '@/pages/mobile/members/Members'; -import MobileSchedule from '@/pages/mobile/schedule/Schedule'; -import MobileScheduleDetail from '@/pages/mobile/schedule/ScheduleDetail'; -import MobileBirthday from '@/pages/mobile/schedule/Birthday'; -import MobileAlbum from '@/pages/mobile/album/Album'; -import MobileAlbumDetail from '@/pages/mobile/album/AlbumDetail'; -import MobileTrackDetail from '@/pages/mobile/album/TrackDetail'; -import MobileAlbumGallery from '@/pages/mobile/album/AlbumGallery'; -import MobileNotFound from '@/pages/mobile/common/NotFound'; - -/** - * PC 환경에서 body에 클래스 추가하는 래퍼 - */ -function PCWrapper({ children }) { - useEffect(() => { - document.body.classList.add('is-pc'); - return () => document.body.classList.remove('is-pc'); - }, []); - return children; -} - -/** - * 프로미스나인 팬사이트 메인 앱 - * react-device-detect를 사용한 PC/Mobile 분리 - */ -function App() { - return ( - - - - {/* PC 뷰 */} - - - - {/* 관리자 페이지 (자체 레이아웃 사용) */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* 관리자 404 페이지 */} - } /> - - {/* 일반 페이지 (레이아웃 포함) */} - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* 404 페이지 */} - } /> - - - } - /> - - - - - {/* Mobile 뷰 */} - - - - - - } - /> - - - - } - /> - - - - } - /> - } /> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - {/* 404 페이지 */} - } /> - - - - ); -} - -export default App; diff --git a/frontend-temp/src/api/admin/albums.js b/frontend-temp/src/api/admin/albums.js deleted file mode 100644 index 582e291..0000000 --- a/frontend-temp/src/api/admin/albums.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * 관리자 앨범 API - */ -import { fetchAuthApi, fetchFormData } from '@/api/client'; - -/** - * 앨범 목록 조회 - * @returns {Promise} - */ -export async function getAlbums() { - return fetchAuthApi('/albums'); -} - -/** - * 앨범 상세 조회 - * @param {number} id - 앨범 ID - * @returns {Promise} - */ -export async function getAlbum(id) { - return fetchAuthApi(`/albums/${id}`); -} - -/** - * 앨범 생성 - * @param {FormData} formData - 앨범 데이터 - * @returns {Promise} - */ -export async function createAlbum(formData) { - return fetchFormData('/albums', formData, 'POST'); -} - -/** - * 앨범 수정 - * @param {number} id - 앨범 ID - * @param {FormData} formData - 앨범 데이터 - * @returns {Promise} - */ -export async function updateAlbum(id, formData) { - return fetchFormData(`/albums/${id}`, formData, 'PUT'); -} - -/** - * 앨범 삭제 - * @param {number} id - 앨범 ID - * @returns {Promise} - */ -export async function deleteAlbum(id) { - return fetchAuthApi(`/albums/${id}`, { method: 'DELETE' }); -} - -/** - * 앨범 사진 목록 조회 - * @param {number} albumId - 앨범 ID - * @returns {Promise} - */ -export async function getAlbumPhotos(albumId) { - return fetchAuthApi(`/albums/${albumId}/photos`); -} - -/** - * 앨범 사진 업로드 - * @param {number} albumId - 앨범 ID - * @param {FormData} formData - 사진 데이터 - * @returns {Promise} - */ -export async function uploadAlbumPhotos(albumId, formData) { - return fetchFormData(`/albums/${albumId}/photos`, formData, 'POST'); -} - -/** - * 앨범 사진 삭제 - * @param {number} albumId - 앨범 ID - * @param {number} photoId - 사진 ID - * @returns {Promise} - */ -export async function deleteAlbumPhoto(albumId, photoId) { - return fetchAuthApi(`/albums/${albumId}/photos/${photoId}`, { method: 'DELETE' }); -} - -/** - * 앨범 티저 목록 조회 - * @param {number} albumId - 앨범 ID - * @returns {Promise} - */ -export async function getAlbumTeasers(albumId) { - return fetchAuthApi(`/albums/${albumId}/teasers`); -} - -/** - * 앨범 티저 삭제 - * @param {number} albumId - 앨범 ID - * @param {number} teaserId - 티저 ID - * @returns {Promise} - */ -export async function deleteAlbumTeaser(albumId, teaserId) { - return fetchAuthApi(`/albums/${albumId}/teasers/${teaserId}`, { method: 'DELETE' }); -} diff --git a/frontend-temp/src/api/admin/auth.js b/frontend-temp/src/api/admin/auth.js deleted file mode 100644 index 98c227b..0000000 --- a/frontend-temp/src/api/admin/auth.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * 관리자 인증 API - */ -import { fetchApi, fetchAuthApi } from '@/api/client'; - -/** - * 로그인 - * @param {string} username - 사용자명 - * @param {string} password - 비밀번호 - * @returns {Promise<{token: string, user: object}>} - */ -export async function login(username, password) { - return fetchApi('/auth/login', { - method: 'POST', - body: JSON.stringify({ username, password }), - }); -} - -/** - * 토큰 검증 - * @returns {Promise<{valid: boolean, user: object}>} - */ -export async function verifyToken() { - return fetchAuthApi('/auth/verify'); -} - -/** - * 비밀번호 변경 - * @param {string} currentPassword - 현재 비밀번호 - * @param {string} newPassword - 새 비밀번호 - */ -export async function changePassword(currentPassword, newPassword) { - return fetchAuthApi('/auth/change-password', { - method: 'POST', - body: JSON.stringify({ currentPassword, newPassword }), - }); -} diff --git a/frontend-temp/src/api/admin/bots.js b/frontend-temp/src/api/admin/bots.js deleted file mode 100644 index 1aff9cb..0000000 --- a/frontend-temp/src/api/admin/bots.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * 관리자 봇 관리 API - */ -import { fetchAuthApi } from '@/api/client'; - -/** - * 봇 목록 조회 - * @returns {Promise} - */ -export async function getBots() { - return fetchAuthApi('/admin/bots'); -} - -/** - * 봇 시작 - * @param {string} id - 봇 ID - * @returns {Promise} - */ -export async function startBot(id) { - return fetchAuthApi(`/admin/bots/${id}/start`, { method: 'POST' }); -} - -/** - * 봇 정지 - * @param {string} id - 봇 ID - * @returns {Promise} - */ -export async function stopBot(id) { - return fetchAuthApi(`/admin/bots/${id}/stop`, { method: 'POST' }); -} - -/** - * 봇 전체 동기화 - * @param {string} id - 봇 ID - * @returns {Promise} - */ -export async function syncAllVideos(id) { - return fetchAuthApi(`/admin/bots/${id}/sync-all`, { method: 'POST' }); -} - -/** - * 할당량 경고 조회 - * @returns {Promise<{warning: boolean, message: string}>} - */ -export async function getQuotaWarning() { - return fetchAuthApi('/admin/bots/quota-warning'); -} - -/** - * 할당량 경고 해제 - * @returns {Promise} - */ -export async function dismissQuotaWarning() { - return fetchAuthApi('/admin/bots/quota-warning', { method: 'DELETE' }); -} diff --git a/frontend-temp/src/api/admin/categories.js b/frontend-temp/src/api/admin/categories.js deleted file mode 100644 index 08e573e..0000000 --- a/frontend-temp/src/api/admin/categories.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * 관리자 카테고리 API - */ -import { fetchAuthApi } from '@/api/client'; - -/** - * 카테고리 목록 조회 - * @returns {Promise} - */ -export async function getCategories() { - return fetchAuthApi('/schedules/categories'); -} - -/** - * 카테고리 생성 - * @param {object} data - 카테고리 데이터 - * @param {string} data.name - 카테고리 이름 - * @param {string} data.color - 색상 코드 - * @returns {Promise} - */ -export async function createCategory(data) { - return fetchAuthApi('/admin/schedule-categories', { - method: 'POST', - body: JSON.stringify(data), - }); -} - -/** - * 카테고리 수정 - * @param {number} id - 카테고리 ID - * @param {object} data - 카테고리 데이터 - * @returns {Promise} - */ -export async function updateCategory(id, data) { - return fetchAuthApi(`/admin/schedule-categories/${id}`, { - method: 'PUT', - body: JSON.stringify(data), - }); -} - -/** - * 카테고리 삭제 - * @param {number} id - 카테고리 ID - * @returns {Promise} - */ -export async function deleteCategory(id) { - return fetchAuthApi(`/admin/schedule-categories/${id}`, { method: 'DELETE' }); -} - -/** - * 카테고리 순서 변경 - * @param {Array<{id: number, sort_order: number}>} orders - 순서 데이터 - * @returns {Promise} - */ -export async function reorderCategories(orders) { - return fetchAuthApi('/admin/schedule-categories-order', { - method: 'PUT', - body: JSON.stringify({ orders }), - }); -} diff --git a/frontend-temp/src/api/admin/members.js b/frontend-temp/src/api/admin/members.js deleted file mode 100644 index 7a83ad6..0000000 --- a/frontend-temp/src/api/admin/members.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * 관리자 멤버 API - */ -import { fetchAuthApi, fetchFormData } from '@/api/client'; - -/** - * 멤버 목록 조회 - * @returns {Promise} - */ -export async function getMembers() { - return fetchAuthApi('/members'); -} - -/** - * 멤버 상세 조회 - * @param {number} id - 멤버 ID - * @returns {Promise} - */ -export async function getMember(id) { - return fetchAuthApi(`/members/${id}`); -} - -/** - * 멤버 수정 - * @param {number} id - 멤버 ID - * @param {FormData} formData - 멤버 데이터 - * @returns {Promise} - */ -export async function updateMember(id, formData) { - return fetchFormData(`/members/${id}`, formData, 'PUT'); -} diff --git a/frontend-temp/src/api/admin/schedules.js b/frontend-temp/src/api/admin/schedules.js deleted file mode 100644 index d7855c8..0000000 --- a/frontend-temp/src/api/admin/schedules.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * 관리자 일정 API - */ -import { fetchAuthApi, fetchFormData } from '@/api/client'; - -/** - * API 응답을 프론트엔드 형식으로 변환 - * - datetime → date, time 분리 - * - category 객체 → category_id, category_name, category_color 플랫화 - * - members 배열 → member_names 문자열 - */ -function transformSchedule(schedule) { - const category = schedule.category || {}; - - // datetime에서 date와 time 분리 - let date = ''; - let time = null; - if (schedule.datetime) { - const parts = schedule.datetime.split('T'); - date = parts[0]; - time = parts[1] || null; - } - - // members 배열을 문자열로 (기존 코드 호환성) - const memberNames = Array.isArray(schedule.members) ? schedule.members.join(',') : ''; - - return { - ...schedule, - date, - time, - category_id: category.id, - category_name: category.name, - category_color: category.color, - member_names: memberNames, - }; -} - -/** - * 일정 목록 조회 (월별) - * @param {number} year - 년도 - * @param {number} month - 월 - * @returns {Promise} - */ -export async function getSchedules(year, month) { - const data = await fetchAuthApi(`/schedules?year=${year}&month=${month}`); - return (data.schedules || []).map(transformSchedule); -} - -/** - * 일정 검색 (Meilisearch) - * @param {string} query - 검색어 - * @param {object} options - 페이지네이션 옵션 - * @param {number} options.offset - 시작 위치 - * @param {number} options.limit - 조회 개수 - * @returns {Promise<{schedules: Array, total: number}>} - */ -export async function searchSchedules(query, { offset = 0, limit = 20 } = {}) { - const data = await fetchAuthApi( - `/schedules?search=${encodeURIComponent(query)}&offset=${offset}&limit=${limit}` - ); - return { - ...data, - schedules: (data.schedules || []).map(transformSchedule), - }; -} - -/** - * 일정 상세 조회 - * @param {number} id - 일정 ID - * @returns {Promise} - */ -export async function getSchedule(id) { - return fetchAuthApi(`/schedules/${id}`); -} - -/** - * 일정 생성 - * @param {FormData} formData - 일정 데이터 - * @returns {Promise} - */ -export async function createSchedule(formData) { - return fetchFormData('/admin/schedules', formData, 'POST'); -} - -/** - * 일정 수정 - * @param {number} id - 일정 ID - * @param {FormData} formData - 일정 데이터 - * @returns {Promise} - */ -export async function updateSchedule(id, formData) { - return fetchFormData(`/admin/schedules/${id}`, formData, 'PUT'); -} - -/** - * 일정 삭제 - * @param {number} id - 일정 ID - * @returns {Promise} - */ -export async function deleteSchedule(id) { - return fetchAuthApi(`/schedules/${id}`, { method: 'DELETE' }); -} diff --git a/frontend-temp/src/api/admin/stats.js b/frontend-temp/src/api/admin/stats.js deleted file mode 100644 index c67306b..0000000 --- a/frontend-temp/src/api/admin/stats.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * 관리자 통계 API - */ -import { fetchAuthApi } from '@/api/client'; - -/** - * 대시보드 통계 조회 - * @returns {Promise} - */ -export async function getStats() { - return fetchAuthApi('/stats'); -} diff --git a/frontend-temp/src/api/admin/suggestions.js b/frontend-temp/src/api/admin/suggestions.js deleted file mode 100644 index 746eb27..0000000 --- a/frontend-temp/src/api/admin/suggestions.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * 관리자 추천 검색어 API - */ -import { fetchAuthApi } from '@/api/client'; - -/** - * 사전 내용 조회 - * @returns {Promise<{content: string}>} - */ -export async function getDict() { - return fetchAuthApi('/schedules/suggestions/dict'); -} - -/** - * 사전 저장 - * @param {string} content - 사전 내용 - * @returns {Promise} - */ -export async function saveDict(content) { - return fetchAuthApi('/schedules/suggestions/dict', { - method: 'PUT', - body: JSON.stringify({ content }), - }); -} diff --git a/frontend-temp/src/api/index.js b/frontend-temp/src/api/index.js deleted file mode 100644 index 59ca0b4..0000000 --- a/frontend-temp/src/api/index.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * API 통합 export - */ - -// 공통 유틸리티 -export * from './client'; - -// 공개 API -export * from './public'; -export * as scheduleApi from './public/schedules'; -export * as albumApi from './public/albums'; -export * as memberApi from './public/members'; - -// 관리자 API -export * from './admin'; -export * as authApi from './admin/auth'; diff --git a/frontend-temp/src/api/public/albums.js b/frontend-temp/src/api/public/albums.js deleted file mode 100644 index 010c069..0000000 --- a/frontend-temp/src/api/public/albums.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * 앨범 API - */ -import { fetchApi, fetchAuthApi, fetchFormData } from '@/api/client'; - -// ==================== 공개 API ==================== - -/** - * 앨범 목록 조회 - */ -export async function getAlbums() { - return fetchApi('/albums'); -} - -/** - * 앨범 상세 조회 (ID) - */ -export async function getAlbum(id) { - return fetchApi(`/albums/${id}`); -} - -/** - * 앨범 상세 조회 (이름) - */ -export async function getAlbumByName(name) { - return fetchApi(`/albums/by-name/${encodeURIComponent(name)}`); -} - -/** - * 앨범 사진 조회 - */ -export async function getAlbumPhotos(albumId) { - return fetchApi(`/albums/${albumId}/photos`); -} - -/** - * 앨범 트랙 조회 - */ -export async function getAlbumTracks(albumId) { - return fetchApi(`/albums/${albumId}/tracks`); -} - -/** - * 트랙 상세 조회 (앨범명, 트랙명으로) - */ -export async function getTrack(albumName, trackTitle) { - return fetchApi( - `/albums/by-name/${encodeURIComponent(albumName)}/track/${encodeURIComponent(trackTitle)}` - ); -} - -/** - * 앨범 티저 조회 - */ -export async function getAlbumTeasers(albumId) { - return fetchApi(`/albums/${albumId}/teasers`); -} - -// ==================== 어드민 API ==================== - -/** - * [Admin] 앨범 생성 - */ -export async function createAlbum(formData) { - return fetchFormData('/albums', formData, 'POST'); -} - -/** - * [Admin] 앨범 수정 - */ -export async function updateAlbum(id, formData) { - return fetchFormData(`/albums/${id}`, formData, 'PUT'); -} - -/** - * [Admin] 앨범 삭제 - */ -export async function deleteAlbum(id) { - return fetchAuthApi(`/albums/${id}`, { method: 'DELETE' }); -} - -/** - * [Admin] 앨범 사진 업로드 - */ -export async function uploadAlbumPhotos(albumId, formData) { - return fetchFormData(`/albums/${albumId}/photos`, formData, 'POST'); -} - -/** - * [Admin] 앨범 사진 삭제 - */ -export async function deleteAlbumPhoto(albumId, photoId) { - return fetchAuthApi(`/albums/${albumId}/photos/${photoId}`, { method: 'DELETE' }); -} - -/** - * [Admin] 앨범 티저 삭제 - */ -export async function deleteAlbumTeaser(albumId, teaserId) { - return fetchAuthApi(`/albums/${albumId}/teasers/${teaserId}`, { method: 'DELETE' }); -} diff --git a/frontend-temp/src/api/public/members.js b/frontend-temp/src/api/public/members.js deleted file mode 100644 index af55b14..0000000 --- a/frontend-temp/src/api/public/members.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * 멤버 API - */ -import { fetchApi, fetchAuthApi, fetchFormData } from '@/api/client'; - -// ==================== 공개 API ==================== - -/** - * 멤버 목록 조회 - */ -export async function getMembers() { - return fetchApi('/members'); -} - -/** - * 멤버 상세 조회 - */ -export async function getMember(id) { - return fetchApi(`/members/${id}`); -} - -// ==================== 어드민 API ==================== - -/** - * [Admin] 멤버 생성 - */ -export async function createMember(formData) { - return fetchFormData('/admin/members', formData, 'POST'); -} - -/** - * [Admin] 멤버 수정 - */ -export async function updateMember(id, formData) { - return fetchFormData(`/admin/members/${id}`, formData, 'PUT'); -} - -/** - * [Admin] 멤버 삭제 - */ -export async function deleteMember(id) { - return fetchAuthApi(`/admin/members/${id}`, { method: 'DELETE' }); -} diff --git a/frontend-temp/src/api/public/schedules.js b/frontend-temp/src/api/public/schedules.js deleted file mode 100644 index 18a9739..0000000 --- a/frontend-temp/src/api/public/schedules.js +++ /dev/null @@ -1,169 +0,0 @@ -/** - * 스케줄 API - */ -import { fetchApi, fetchAuthApi, fetchFormData } from '@/api/client'; -import { getTodayKST, dayjs } from '@/utils'; - -/** - * API 응답을 프론트엔드 형식으로 변환 - * - datetime → date, time 분리 - * - category 객체 → category_id, category_name, category_color 플랫화 - * - members 배열 → member_names 문자열 - */ -function transformSchedule(schedule) { - const category = schedule.category || {}; - - // datetime에서 date와 time 분리 - let date = ''; - let time = null; - if (schedule.datetime) { - const dt = dayjs(schedule.datetime); - date = dt.format('YYYY-MM-DD'); - // datetime에 T가 포함되어 있으면 시간이 있는 것 - time = schedule.datetime.includes('T') ? dt.format('HH:mm:ss') : null; - } - - // members 배열을 문자열로 (기존 코드 호환성) - const memberNames = Array.isArray(schedule.members) - ? schedule.members.join(',') - : ''; - - return { - ...schedule, - date, - time, - category_id: category.id, - category_name: category.name, - category_color: category.color, - member_names: memberNames, - }; -} - -// ==================== 공개 API ==================== - -/** - * 스케줄 목록 조회 (월별) - */ -export async function getSchedules(year, month) { - const data = await fetchApi(`/schedules?year=${year}&month=${month}`); - return (data.schedules || []).map(transformSchedule); -} - -/** - * 다가오는 스케줄 조회 - */ -export async function getUpcomingSchedules(limit = 3) { - const today = getTodayKST(); - const data = await fetchApi(`/schedules?startDate=${today}&limit=${limit}`); - return (data.schedules || []).map(transformSchedule); -} - -/** - * 스케줄 검색 (Meilisearch) - */ -export async function searchSchedules(query, { offset = 0, limit = 20 } = {}) { - const data = await fetchApi( - `/schedules?search=${encodeURIComponent(query)}&offset=${offset}&limit=${limit}` - ); - return { - ...data, - schedules: (data.schedules || []).map(transformSchedule), - }; -} - -/** - * 스케줄 상세 조회 - */ -export async function getSchedule(id) { - return fetchApi(`/schedules/${id}`); -} - -/** - * X 프로필 정보 조회 - */ -export async function getXProfile(username) { - return fetchApi(`/schedules/x-profile/${encodeURIComponent(username)}`); -} - -/** - * 카테고리 목록 조회 - */ -export async function getCategories() { - return fetchApi('/schedules/categories'); -} - -// ==================== 어드민 API ==================== - -/** - * [Admin] 스케줄 검색 - */ -export async function adminSearchSchedules(query) { - return fetchAuthApi(`/admin/schedules/search?q=${encodeURIComponent(query)}`); -} - -/** - * [Admin] 스케줄 상세 조회 - */ -export async function adminGetSchedule(id) { - return fetchAuthApi(`/admin/schedules/${id}`); -} - -/** - * [Admin] 스케줄 생성 - */ -export async function createSchedule(formData) { - return fetchFormData('/admin/schedules', formData, 'POST'); -} - -/** - * [Admin] 스케줄 수정 - */ -export async function updateSchedule(id, formData) { - return fetchFormData(`/admin/schedules/${id}`, formData, 'PUT'); -} - -/** - * [Admin] 스케줄 삭제 - */ -export async function deleteSchedule(id) { - return fetchAuthApi(`/schedules/${id}`, { method: 'DELETE' }); -} - -// ==================== 카테고리 어드민 API ==================== - -/** - * [Admin] 카테고리 생성 - */ -export async function createCategory(data) { - return fetchAuthApi('/admin/schedule-categories', { - method: 'POST', - body: JSON.stringify(data), - }); -} - -/** - * [Admin] 카테고리 수정 - */ -export async function updateCategory(id, data) { - return fetchAuthApi(`/admin/schedule-categories/${id}`, { - method: 'PUT', - body: JSON.stringify(data), - }); -} - -/** - * [Admin] 카테고리 삭제 - */ -export async function deleteCategory(id) { - return fetchAuthApi(`/admin/schedule-categories/${id}`, { method: 'DELETE' }); -} - -/** - * [Admin] 카테고리 순서 변경 - */ -export async function reorderCategories(orders) { - return fetchAuthApi('/admin/schedule-categories-order', { - method: 'PUT', - body: JSON.stringify({ orders }), - }); -} diff --git a/frontend-temp/src/components/common/Lightbox.jsx b/frontend-temp/src/components/common/Lightbox.jsx deleted file mode 100644 index 9d794a8..0000000 --- a/frontend-temp/src/components/common/Lightbox.jsx +++ /dev/null @@ -1,290 +0,0 @@ -import { useState, useEffect, useCallback, memo } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { X, ChevronLeft, ChevronRight, Download } from 'lucide-react'; -import LightboxIndicator from './LightboxIndicator'; - -/** - * 라이트박스 공통 컴포넌트 - * 이미지/비디오 갤러리를 전체 화면으로 표시 - * - * @param {string[]} images - 이미지/비디오 URL 배열 - * @param {Object[]} photos - 메타데이터 포함 사진 배열 (선택적) - * @param {string} photos[].title - 컨셉 이름 - * @param {string} photos[].members - 멤버 이름 (쉼표 구분) - * @param {Object[]} teasers - 티저 정보 배열 (비디오 여부 확인용) - * @param {string} teasers[].media_type - 'video' 또는 'image' - * @param {number} currentIndex - 현재 인덱스 - * @param {boolean} isOpen - 열림 상태 - * @param {function} onClose - 닫기 콜백 - * @param {function} onIndexChange - 인덱스 변경 콜백 - * @param {boolean} showCounter - 카운터 표시 여부 (기본: true) - * @param {boolean} showDownload - 다운로드 버튼 표시 여부 (기본: true) - */ -function Lightbox({ - images, - photos, - teasers, - currentIndex, - isOpen, - onClose, - onIndexChange, - showCounter = true, - showDownload = true, -}) { - const [imageLoaded, setImageLoaded] = useState(false); - const [slideDirection, setSlideDirection] = useState(0); - - // 이전/다음 네비게이션 - const goToPrev = useCallback(() => { - if (images.length <= 1) return; - setImageLoaded(false); - setSlideDirection(-1); - onIndexChange((currentIndex - 1 + images.length) % images.length); - }, [images.length, currentIndex, onIndexChange]); - - const goToNext = useCallback(() => { - if (images.length <= 1) return; - setImageLoaded(false); - setSlideDirection(1); - onIndexChange((currentIndex + 1) % images.length); - }, [images.length, currentIndex, onIndexChange]); - - const goToIndex = useCallback( - (index) => { - if (index === currentIndex) return; - setImageLoaded(false); - setSlideDirection(index > currentIndex ? 1 : -1); - onIndexChange(index); - }, - [currentIndex, onIndexChange] - ); - - // 이미지 다운로드 - const downloadImage = useCallback(async () => { - const imageUrl = images[currentIndex]; - if (!imageUrl) return; - - try { - const response = await fetch(imageUrl); - const blob = await response.blob(); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `image_${currentIndex + 1}.jpg`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - } catch (error) { - console.error('이미지 다운로드 실패:', error); - } - }, [images, currentIndex]); - - // 라이트박스 열릴 때 body 스크롤 숨기기 - useEffect(() => { - if (isOpen) { - document.documentElement.style.overflow = 'hidden'; - document.body.style.overflow = 'hidden'; - } else { - document.documentElement.style.overflow = ''; - document.body.style.overflow = ''; - } - return () => { - document.documentElement.style.overflow = ''; - document.body.style.overflow = ''; - }; - }, [isOpen]); - - // 키보드 이벤트 핸들러 - useEffect(() => { - if (!isOpen) return; - - const handleKeyDown = (e) => { - switch (e.key) { - case 'ArrowLeft': - goToPrev(); - break; - case 'ArrowRight': - goToNext(); - break; - case 'Escape': - onClose(); - break; - default: - break; - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [isOpen, goToPrev, goToNext, onClose]); - - // 이미지가 바뀔 때 로딩 상태 리셋 - useEffect(() => { - setImageLoaded(false); - }, [currentIndex]); - - // 현재 사진의 메타데이터 - const currentPhoto = photos?.[currentIndex]; - const photoTitle = currentPhoto?.title; - const hasValidTitle = photoTitle && photoTitle.trim() && photoTitle !== 'Default'; - const photoMembers = currentPhoto?.members; - const hasMembers = photoMembers && String(photoMembers).trim(); - - return ( - - {isOpen && images.length > 0 && ( - - {/* 내부 컨테이너 */} -
- {/* 카운터 */} - {showCounter && images.length > 1 && ( -
- {currentIndex + 1} / {images.length} -
- )} - - {/* 상단 버튼들 */} -
- {showDownload && ( - - )} - -
- - {/* 이전 버튼 */} - {images.length > 1 && ( - - )} - - {/* 로딩 스피너 */} - {!imageLoaded && ( -
-
-
- )} - - {/* 이미지/비디오 + 메타데이터 */} -
- {teasers?.[currentIndex]?.media_type === 'video' ? ( - e.stopPropagation()} - onCanPlay={() => setImageLoaded(true)} - initial={{ x: slideDirection * 100 }} - animate={{ x: 0 }} - transition={{ duration: 0.25, ease: 'easeOut' }} - controls - autoPlay - /> - ) : ( - e.stopPropagation()} - onLoad={() => setImageLoaded(true)} - initial={{ x: slideDirection * 100 }} - animate={{ x: 0 }} - transition={{ duration: 0.25, ease: 'easeOut' }} - /> - )} - - {/* 컨셉/멤버 정보 */} - {imageLoaded && (hasValidTitle || hasMembers) && ( -
- {hasValidTitle && ( - - {photoTitle} - - )} - {hasMembers && ( -
- {String(photoMembers) - .split(',') - .map((member, idx) => ( - - {member.trim()} - - ))} -
- )} -
- )} -
- - {/* 다음 버튼 */} - {images.length > 1 && ( - - )} - - {/* 인디케이터 */} - {images.length > 1 && ( - - )} -
-
- )} -
- ); -} - -export default Lightbox; diff --git a/frontend-temp/src/components/common/LightboxIndicator.jsx b/frontend-temp/src/components/common/LightboxIndicator.jsx deleted file mode 100644 index ef025f4..0000000 --- a/frontend-temp/src/components/common/LightboxIndicator.jsx +++ /dev/null @@ -1,57 +0,0 @@ -import { memo } from 'react'; - -/** - * 라이트박스 인디케이터 컴포넌트 - * 이미지 갤러리에서 현재 위치를 표시하는 슬라이딩 점 인디케이터 - * CSS transition 사용으로 GPU 가속 - */ -const LightboxIndicator = memo(function LightboxIndicator({ - count, - currentIndex, - goToIndex, - width = 200, -}) { - const halfWidth = width / 2; - const translateX = -(currentIndex * 18) + halfWidth - 6; - - return ( -
- {/* 양옆 페이드 그라데이션 */} -
- {/* 슬라이딩 컨테이너 - CSS transition으로 GPU 가속 */} -
- {Array.from({ length: count }).map((_, i) => ( -
-
- ); -}); - -export default LightboxIndicator; diff --git a/frontend-temp/src/index.css b/frontend-temp/src/index.css deleted file mode 100644 index 8e58216..0000000 --- a/frontend-temp/src/index.css +++ /dev/null @@ -1,85 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -/* 기본 스타일 */ -body { - background-color: #fafafa; - color: #1a1a1a; -} - -/* 스크롤바 스타일 */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: #f1f1f1; -} - -::-webkit-scrollbar-thumb { - background: #548360; - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: #456e50; -} - -/* View Transitions API - 앨범 커버 이미지 부드러운 전환 */ -::view-transition-old(root), -::view-transition-new(root) { - animation-duration: 0.3s; -} - -/* 앨범 커버 트랜지션 */ -::view-transition-group(*) { - animation-duration: 0.4s; - animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); -} - -::view-transition-old(*) { - animation: fade-out 0.3s ease-out both; -} - -::view-transition-new(*) { - animation: fade-in 0.3s ease-in both; -} - -@keyframes fade-out { - from { - opacity: 1; - } - to { - opacity: 0; - } -} - -@keyframes fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -/* 라이트박스 스크롤바 숨기기 */ -.lightbox-no-scrollbar::-webkit-scrollbar { - display: none; -} - -/* 스크롤바 숨기기 유틸리티 */ -.scrollbar-hide::-webkit-scrollbar { - display: none; -} -.scrollbar-hide { - -ms-overflow-style: none; - scrollbar-width: none; -} - -/* Swiper autoHeight 지원 */ -.swiper-slide { - height: auto !important; -} diff --git a/frontend-temp/src/main.jsx b/frontend-temp/src/main.jsx deleted file mode 100644 index 975ee56..0000000 --- a/frontend-temp/src/main.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import App from "./App"; -import "./index.css"; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 1000 * 60 * 5, // 5분 - retry: 1, - refetchOnWindowFocus: false, - }, - }, -}); - -ReactDOM.createRoot(document.getElementById("root")).render( - - - - - -); diff --git a/frontend-temp/src/mobile.css b/frontend-temp/src/mobile.css deleted file mode 100644 index 01ee413..0000000 --- a/frontend-temp/src/mobile.css +++ /dev/null @@ -1,156 +0,0 @@ -/* 모바일 전용 스타일 */ - -/* 모바일 html,body 스크롤 방지 */ -html.mobile-layout, -html.mobile-layout body { - margin: 0; - padding: 0; - height: 100%; - overflow: hidden; -} - -/* 모바일 레이아웃 컨테이너 */ -.mobile-layout-container { - display: flex; - flex-direction: column; - height: 100dvh; - overflow: hidden; -} - -/* 모바일 툴바 (기본 56px) */ -.mobile-toolbar { - flex-shrink: 0; - background-color: #ffffff; -} - -/* 일정 페이지 툴바 (헤더 + 날짜 선택기) */ -.mobile-toolbar-schedule { - flex-shrink: 0; - background-color: #ffffff; -} - -/* 하단 네비게이션 */ -.mobile-bottom-nav { - flex-shrink: 0; -} - -/* 컨텐츠 영역 - 스크롤 가능, 스크롤바 숨김 */ -.mobile-content { - flex: 1; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - overscroll-behavior: contain; - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE/Edge */ -} - -.mobile-content::-webkit-scrollbar { - display: none; /* Chrome, Safari, Opera */ -} - -/* 모바일 safe-area 지원 (노치, 홈 인디케이터) */ -.safe-area-bottom { - padding-bottom: env(safe-area-inset-bottom, 0); -} - -.safe-area-top { - padding-top: env(safe-area-inset-top, 0); -} - -/* 모바일 달력 스타일 */ -.mobile-calendar-wrapper .react-calendar__navigation button:hover { - background-color: #f3f4f6; - border-radius: 0.5rem; -} - -.mobile-calendar-wrapper .react-calendar__navigation__label { - font-weight: 600; - font-size: 0.875rem; - color: #374151; -} - -.mobile-calendar-wrapper .react-calendar__month-view__weekdays { - text-align: center; - font-size: 0.75rem; - font-weight: 500; - color: #6b7280; -} - -.mobile-calendar-wrapper .react-calendar__month-view__weekdays__weekday { - padding: 0.5rem 0; -} - -.mobile-calendar-wrapper .react-calendar__month-view__weekdays__weekday abbr { - text-decoration: none; -} - -/* 일요일 (빨간색) */ -.mobile-calendar-wrapper - .react-calendar__month-view__weekdays__weekday:first-child { - color: #f87171; -} - -/* 토요일 (파란색) */ -.mobile-calendar-wrapper - .react-calendar__month-view__weekdays__weekday:last-child { - color: #60a5fa; -} - -.mobile-calendar-wrapper .react-calendar__tile { - display: flex; - flex-direction: column; - align-items: center; - padding: 0.25rem; - background: none; - border: none; - font-size: 0.75rem; - color: #374151; -} - -.mobile-calendar-wrapper .react-calendar__tile:hover { - background-color: #f3f4f6; - border-radius: 9999px; -} - -.mobile-calendar-wrapper .react-calendar__tile abbr { - display: flex; - align-items: center; - justify-content: center; - width: 1.75rem; - height: 1.75rem; - border-radius: 9999px; -} - -/* 이웃 달 날짜 (흐리게) */ -.mobile-calendar-wrapper - .react-calendar__month-view__days__day--neighboringMonth { - color: #d1d5db; -} - -/* 일요일 */ -.mobile-calendar-wrapper .react-calendar__tile.sunday abbr { - color: #ef4444; -} - -/* 토요일 */ -.mobile-calendar-wrapper .react-calendar__tile.saturday abbr { - color: #3b82f6; -} - -/* 오늘 */ -.mobile-calendar-wrapper .react-calendar__tile--now abbr { - background-color: #548360; - color: white; - font-weight: 700; -} - -/* 선택된 날짜 */ -.mobile-calendar-wrapper .react-calendar__tile--active abbr { - background-color: #548360; - color: white; -} - -.mobile-calendar-wrapper .react-calendar__tile--active:enabled:hover abbr, -.mobile-calendar-wrapper .react-calendar__tile--active:enabled:focus abbr { - background-color: #456e50; -} diff --git a/frontend-temp/src/pc.css b/frontend-temp/src/pc.css deleted file mode 100644 index 3d217c9..0000000 --- a/frontend-temp/src/pc.css +++ /dev/null @@ -1,14 +0,0 @@ -/* PC 전용 스타일 - body.is-pc 클래스가 있을 때만 적용 */ - -/* PC에서는 body 스크롤 숨기고 내부 영역에서만 스크롤 */ -html.is-pc, -body.is-pc { - height: 100%; - overflow: hidden; -} - -/* PC 최소 너비 설정 */ -body.is-pc #root { - min-width: 1440px; - height: 100%; -} diff --git a/frontend-temp/src/stores/useScheduleStore.js b/frontend-temp/src/stores/useScheduleStore.js deleted file mode 100644 index 076c060..0000000 --- a/frontend-temp/src/stores/useScheduleStore.js +++ /dev/null @@ -1,107 +0,0 @@ -import { create } from 'zustand'; - -/** - * 스케줄 페이지 상태 스토어 - * 메모리 기반 - SPA 내 페이지 이동 시 유지, 새로고침 시 초기화 - */ -const useScheduleStore = create((set, get) => ({ - // ===== 검색 관련 ===== - searchInput: '', - searchTerm: '', - isSearchMode: false, - - // ===== 필터 관련 ===== - selectedCategories: [], - selectedMembers: [], - - // ===== 날짜 관련 ===== - selectedDate: undefined, // undefined: 오늘, null: 전체, Date: 특정 날짜 - currentDate: new Date(), - - // ===== 뷰 관련 ===== - viewMode: 'list', // 'list' | 'calendar' - scrollPosition: 0, - - // ===== 검색 액션 ===== - setSearchInput: (value) => set({ searchInput: value }), - setSearchTerm: (value) => set({ searchTerm: value }), - setIsSearchMode: (value) => set({ isSearchMode: value }), - - startSearch: (term) => { - set({ - searchTerm: term, - isSearchMode: true, - selectedDate: null, // 검색 시 날짜 필터 해제 - }); - }, - - clearSearch: () => { - set({ - searchInput: '', - searchTerm: '', - isSearchMode: false, - }); - }, - - // ===== 필터 액션 ===== - setSelectedCategories: (value) => set({ selectedCategories: value }), - setSelectedMembers: (value) => set({ selectedMembers: value }), - - toggleCategory: (categoryId) => { - const { selectedCategories } = get(); - const isSelected = selectedCategories.includes(categoryId); - set({ - selectedCategories: isSelected - ? selectedCategories.filter((id) => id !== categoryId) - : [...selectedCategories, categoryId], - }); - }, - - toggleMember: (memberId) => { - const { selectedMembers } = get(); - const isSelected = selectedMembers.includes(memberId); - set({ - selectedMembers: isSelected - ? selectedMembers.filter((id) => id !== memberId) - : [...selectedMembers, memberId], - }); - }, - - clearFilters: () => { - set({ - selectedCategories: [], - selectedMembers: [], - }); - }, - - // ===== 날짜 액션 ===== - setSelectedDate: (value) => set({ selectedDate: value }), - setCurrentDate: (value) => set({ currentDate: value }), - - goToToday: () => { - set({ - selectedDate: undefined, - currentDate: new Date(), - }); - }, - - // ===== 뷰 액션 ===== - setViewMode: (mode) => set({ viewMode: mode }), - setScrollPosition: (value) => set({ scrollPosition: value }), - - // ===== 전체 초기화 ===== - reset: () => - set({ - searchInput: '', - searchTerm: '', - isSearchMode: false, - selectedCategories: [], - selectedMembers: [], - selectedDate: undefined, - currentDate: new Date(), - viewMode: 'list', - scrollPosition: 0, - }), -})); - -export default useScheduleStore; diff --git a/frontend-temp/src/utils/date.js b/frontend-temp/src/utils/date.js deleted file mode 100644 index d8dfa56..0000000 --- a/frontend-temp/src/utils/date.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * 날짜 관련 유틸리티 함수 - * dayjs를 사용하여 KST(한국 표준시) 기준으로 날짜 처리 - */ -import dayjs from 'dayjs'; -import utc from 'dayjs/plugin/utc'; -import timezone from 'dayjs/plugin/timezone'; -import { TIMEZONE, WEEKDAYS } from '@/constants'; - -// 플러그인 확장 -dayjs.extend(utc); -dayjs.extend(timezone); - -/** - * KST 기준 오늘 날짜 (YYYY-MM-DD) - * @returns {string} 오늘 날짜 문자열 - */ -export const getTodayKST = () => { - return dayjs().tz(TIMEZONE).format('YYYY-MM-DD'); -}; - -/** - * 날짜 문자열 포맷팅 - * @param {string|Date} date - 날짜 - * @param {string} format - 포맷 (기본: 'YYYY-MM-DD') - * @returns {string} 포맷된 날짜 문자열 - */ -export const formatDate = (date, format = 'YYYY-MM-DD') => { - if (!date) return ''; - return dayjs(date).tz(TIMEZONE).format(format); -}; - -/** - * 두 날짜 비교 (같은 날인지) - * @param {string|Date} date1 - * @param {string|Date} date2 - * @returns {boolean} - */ -export const isSameDay = (date1, date2) => { - return ( - dayjs(date1).tz(TIMEZONE).format('YYYY-MM-DD') === - dayjs(date2).tz(TIMEZONE).format('YYYY-MM-DD') - ); -}; - -/** - * 날짜가 오늘인지 확인 - * @param {string|Date} date - * @returns {boolean} - */ -export const isToday = (date) => { - return isSameDay(date, dayjs()); -}; - -/** - * 전체 날짜 포맷 (YYYY. M. D. (요일)) - * @param {string|Date} date - 날짜 - * @returns {string} 포맷된 문자열 - */ -export const formatFullDate = (date) => { - if (!date) return ''; - const d = dayjs(date).tz(TIMEZONE); - return `${d.year()}. ${d.month() + 1}. ${d.date()}. (${WEEKDAYS[d.day()]})`; -}; - -/** - * X(트위터) 스타일 날짜/시간 포맷팅 - * @param {string} datetime - datetime 문자열 (YYYY-MM-DDTHH:mm:ss 또는 YYYY-MM-DD) - * @returns {string} "오후 7:00 · 2026년 1월 18일" 또는 "2026년 1월 18일" - */ -export const formatXDateTime = (datetime) => { - if (!datetime) return ''; - - const d = dayjs(datetime).tz(TIMEZONE); - const datePart = `${d.year()}년 ${d.month() + 1}월 ${d.date()}일`; - - // datetime에 T가 포함되고 시간이 00:00:00이 아니면 시간 표시 - if (datetime.includes('T') && !datetime.endsWith('T00:00:00')) { - const hours = d.hour(); - const minutes = d.minute(); - // 00:00인 경우 시간 표시 안함 - if (hours !== 0 || minutes !== 0) { - const period = hours < 12 ? '오전' : '오후'; - const hour12 = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours; - return `${period} ${hour12}:${String(minutes).padStart(2, '0')} · ${datePart}`; - } - } - - return datePart; -}; - -/** - * datetime 문자열에서 date 추출 - * @param {string} datetime - "YYYY-MM-DD HH:mm" 또는 "YYYY-MM-DD" - * @returns {string} "YYYY-MM-DD" - */ -export const extractDate = (datetime) => { - if (!datetime) return ''; - return datetime.split(' ')[0].split('T')[0]; -}; - -/** - * datetime 문자열에서 time 추출 - * @param {string} datetime - "YYYY-MM-DD HH:mm" 또는 "YYYY-MM-DDTHH:mm" - * @returns {string|null} "HH:mm" 또는 null - */ -export const extractTime = (datetime) => { - if (!datetime) return null; - if (datetime.includes(' ')) { - return datetime.split(' ')[1]?.slice(0, 5) || null; - } - if (datetime.includes('T')) { - return datetime.split('T')[1]?.slice(0, 5) || null; - } - return null; -}; - -// dayjs 인스턴스도 export (고급 사용용) -export { dayjs }; diff --git a/frontend-temp/tailwind.config.js b/frontend-temp/tailwind.config.js deleted file mode 100644 index 210016d..0000000 --- a/frontend-temp/tailwind.config.js +++ /dev/null @@ -1,21 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], - theme: { - extend: { - colors: { - primary: { - DEFAULT: "#548360", - dark: "#456E50", - light: "#6A9A75", - }, - secondary: "#F5F5F5", - accent: "#FFD700", - }, - fontFamily: { - sans: ["Pretendard", "Inter", "sans-serif"], - }, - }, - }, - plugins: [], -}; diff --git a/frontend-temp/vite.config.js b/frontend-temp/vite.config.js deleted file mode 100644 index b73e439..0000000 --- a/frontend-temp/vite.config.js +++ /dev/null @@ -1,27 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; -import path from "path"; - -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, - server: { - host: true, - port: 80, - allowedHosts: true, - proxy: { - "/api": { - target: "http://fromis9-backend:80", - changeOrigin: true, - }, - "/docs": { - target: "http://fromis9-backend:80", - changeOrigin: true, - }, - }, - }, -}); diff --git a/frontend/.env b/frontend/.env deleted file mode 100644 index 6cff0c1..0000000 --- a/frontend/.env +++ /dev/null @@ -1 +0,0 @@ -VITE_KAKAO_JS_KEY=84b3c657c3de7d1ca89e1fa33455b8da diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 4ef7ba7..2f915b7 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -2,16 +2,3 @@ FROM node:20-alpine WORKDIR /app CMD ["sh", "-c", "npm install --include=dev && npm run dev -- --host 0.0.0.0"] - -# 배포 모드 (사용 시 위 개발 모드를 주석처리) -# FROM node:20-alpine AS builder -# WORKDIR /app -# COPY package*.json ./ -# RUN npm install -# COPY . . -# RUN npm run build -# -# FROM nginx:alpine -# COPY --from=builder /app/dist /usr/share/nginx/html -# EXPOSE 80 -# CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 33ef6c0..04fc9a0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,3214 +1,2106 @@ { - "name": "fromis9-frontend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "fromis9-frontend", - "version": "1.0.0", - "dependencies": { - "@babel/runtime": "^7.28.6", - "@tanstack/react-query": "^5.90.16", - "@tanstack/react-virtual": "^3.13.18", - "canvas-confetti": "^1.9.4", - "dayjs": "^1.11.19", - "framer-motion": "^11.0.8", - "lucide-react": "^0.344.0", - "react": "^18.2.0", - "react-calendar": "^6.0.0", - "react-colorful": "^5.6.1", - "react-device-detect": "^2.2.3", - "react-dom": "^18.2.0", - "react-infinite-scroll-component": "^6.1.1", - "react-intersection-observer": "^10.0.0", - "react-ios-time-picker": "^0.2.2", - "react-linkify": "^1.0.0-alpha", - "react-photo-album": "^3.4.0", - "react-router-dom": "^6.22.3", - "react-window": "^2.2.3", - "swiper": "^12.0.3", - "zustand": "^5.0.9" - }, - "devDependencies": { - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "autoprefixer": "^10.4.22", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.18", - "vite": "^5.4.1" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", - "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", - "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", - "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.6" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", - "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.6", - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", - "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emotion/memoize": "^0.9.0" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@remix-run/router": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", - "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", - "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", - "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", - "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", - "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", - "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", - "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", - "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", - "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", - "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", - "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", - "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", - "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", - "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", - "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", - "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", - "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", - "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", - "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", - "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", - "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", - "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", - "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", - "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", - "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", - "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@tanstack/query-core": { - "version": "5.90.19", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.19.tgz", - "integrity": "sha512-GLW5sjPVIvH491VV1ufddnfldyVB+teCnpPIvweEfkpRx7CfUmUGhoh9cdcUKBh/KwVxk22aNEDxeTsvmyB/WA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.90.19", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.19.tgz", - "integrity": "sha512-qTZRZ4QyTzQc+M0IzrbKHxSeISUmRB3RPGmao5bT+sI6ayxSRhn0FXEnT5Hg3as8SBFcRosrXXRFB+yAcxVxJQ==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.90.19" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/@tanstack/react-virtual": { - "version": "3.13.18", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.18.tgz", - "integrity": "sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A==", - "license": "MIT", - "dependencies": { - "@tanstack/virtual-core": "3.13.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@tanstack/virtual-core": { - "version": "3.13.18", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.18.tgz", - "integrity": "sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.0.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz", - "integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.27", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", - "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@wojtekmaj/date-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-2.0.2.tgz", - "integrity": "sha512-Do66mSlSNifFFuo3l9gNKfRMSFi26CRuQMsDJuuKO/ekrDWuTTtE4ZQxoFCUOG+NgxnpSeBq/k5TY8ZseEzLpA==", - "license": "MIT", - "funding": { - "url": "https://github.com/wojtekmaj/date-utils?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/autoprefixer": { - "version": "10.4.23", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", - "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.15", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", - "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001764", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", - "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/canvas-confetti": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", - "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==", - "license": "ISC", - "funding": { - "type": "donate", - "url": "https://www.paypal.me/kirilvatev" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", - "dev": true, - "license": "ISC" - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/framer-motion": { - "version": "11.18.2", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", - "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", - "license": "MIT", - "dependencies": { - "motion-dom": "^11.18.1", - "motion-utils": "^11.18.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-user-locale": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-user-locale/-/get-user-locale-3.0.0.tgz", - "integrity": "sha512-iJfHSmdYV39UUBw7Jq6GJzeJxUr4U+S03qdhVuDsR9gCEnfbqLy9gYDJFBJQL1riqolFUKQvx36mEkp2iGgJ3g==", - "license": "MIT", - "dependencies": { - "memoize": "^10.0.0" - }, - "funding": { - "url": "https://github.com/wojtekmaj/get-user-locale?sponsor=1" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/linkify-it": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", - "license": "MIT", - "dependencies": { - "uc.micro": "^1.0.1" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.344.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.344.0.tgz", - "integrity": "sha512-6YyBnn91GB45VuVT96bYCOKElbJzUHqp65vX8cDcu55MQL9T969v4dhGClpljamuI/+KMO9P6w9Acq1CVQGvIQ==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/memoize": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.2.0.tgz", - "integrity": "sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA==", - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/memoize?sponsor=1" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/motion-dom": { - "version": "11.18.1", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", - "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", - "license": "MIT", - "dependencies": { - "motion-utils": "^11.18.1" - } - }, - "node_modules/motion-utils": { - "version": "11.18.1", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", - "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-calendar": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-6.0.0.tgz", - "integrity": "sha512-6wqaki3Us0DNDjZDr0DYIzhSFprNoy4FdPT9Pjy5aD2hJJVjtJwmdMT9VmrTUo949nlk35BOxehThxX62RkuRQ==", - "license": "MIT", - "dependencies": { - "@wojtekmaj/date-utils": "^2.0.2", - "clsx": "^2.0.0", - "get-user-locale": "^3.0.0", - "warning": "^4.0.0" - }, - "funding": { - "url": "https://github.com/wojtekmaj/react-calendar?sponsor=1" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-colorful": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", - "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/react-device-detect": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-device-detect/-/react-device-detect-2.2.3.tgz", - "integrity": "sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==", - "license": "MIT", - "dependencies": { - "ua-parser-js": "^1.0.33" - }, - "peerDependencies": { - "react": ">= 0.14.0", - "react-dom": ">= 0.14.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-infinite-scroll-component": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.1.tgz", - "integrity": "sha512-R8YoOyiNDynSWmfVme5LHslsKrP+/xcRUWR2ies8UgUab9dtyw5ECnMCVPPmnmjjF4MWQmfVdRwRWcWaDgeyMA==", - "license": "MIT", - "dependencies": { - "throttle-debounce": "^2.1.0" - }, - "peerDependencies": { - "react": ">=16.0.0" - } - }, - "node_modules/react-intersection-observer": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-10.0.2.tgz", - "integrity": "sha512-lAMzxVWrBko6SLd1jx6l84fVrzJu91hpxHlvD2as2Wec9mDCjdYXwc5xNOFBchpeBir0Y7AGBW+C/AYMa7CSFg==", - "license": "MIT", - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-ios-time-picker": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/react-ios-time-picker/-/react-ios-time-picker-0.2.2.tgz", - "integrity": "sha512-bi+K23lK6Pf2xDXmhAlz+RJuy9/onWYi7Ye+ODVhIkis9AVFECOza2ckkZl/4vUypj2+TdTsHn+VZrTNdGIwDQ==", - "license": "MIT", - "dependencies": { - "react-portal": "^4.2.2" - }, - "peerDependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/react-linkify": { - "version": "1.0.0-alpha", - "resolved": "https://registry.npmjs.org/react-linkify/-/react-linkify-1.0.0-alpha.tgz", - "integrity": "sha512-7gcIUvJkAXXttt1fmBK9cwn+1jTa4hbKLGCZ9J1U6EOkyb2/+LKL1Z28d9rtDLMnpvImlNlLPdTPooorl5cpmg==", - "license": "MIT", - "dependencies": { - "linkify-it": "^2.0.3", - "tlds": "^1.199.0" - } - }, - "node_modules/react-photo-album": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/react-photo-album/-/react-photo-album-3.4.0.tgz", - "integrity": "sha512-pPaCoxEfVDhowpqxECq4SOD5kzP1Uao8PEN11Jasxayv4cZjad3Fy8SlKt6wvtLnVJRtOjsQDU/ZnnUuberwMg==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/igordanchenko" - }, - "peerDependencies": { - "@types/react": "^18 || ^19", - "react": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-portal": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/react-portal/-/react-portal-4.3.0.tgz", - "integrity": "sha512-qs/2uKq1ifB3J1+K8ExfgUvCDZqlqCkfOEhqTELEDTfosloKiuzOzc7hl7IQ/7nohiFZD41BUYU0boAsIsGYHw==", - "license": "MIT", - "dependencies": { - "prop-types": "^15.5.8" - }, - "peerDependencies": { - "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0", - "react-dom": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", - "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", - "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.2", - "react-router": "6.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-window": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.5.tgz", - "integrity": "sha512-6viWvPSZvVuMIe9hrl4IIZoVfO/npiqOb03m4Z9w+VihmVzBbiudUrtUqDpsWdKvd/Ai31TCR25CBcFFAUm28w==", - "license": "MIT", - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", - "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.55.1", - "@rollup/rollup-android-arm64": "4.55.1", - "@rollup/rollup-darwin-arm64": "4.55.1", - "@rollup/rollup-darwin-x64": "4.55.1", - "@rollup/rollup-freebsd-arm64": "4.55.1", - "@rollup/rollup-freebsd-x64": "4.55.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", - "@rollup/rollup-linux-arm-musleabihf": "4.55.1", - "@rollup/rollup-linux-arm64-gnu": "4.55.1", - "@rollup/rollup-linux-arm64-musl": "4.55.1", - "@rollup/rollup-linux-loong64-gnu": "4.55.1", - "@rollup/rollup-linux-loong64-musl": "4.55.1", - "@rollup/rollup-linux-ppc64-gnu": "4.55.1", - "@rollup/rollup-linux-ppc64-musl": "4.55.1", - "@rollup/rollup-linux-riscv64-gnu": "4.55.1", - "@rollup/rollup-linux-riscv64-musl": "4.55.1", - "@rollup/rollup-linux-s390x-gnu": "4.55.1", - "@rollup/rollup-linux-x64-gnu": "4.55.1", - "@rollup/rollup-linux-x64-musl": "4.55.1", - "@rollup/rollup-openbsd-x64": "4.55.1", - "@rollup/rollup-openharmony-arm64": "4.55.1", - "@rollup/rollup-win32-arm64-msvc": "4.55.1", - "@rollup/rollup-win32-ia32-msvc": "4.55.1", - "@rollup/rollup-win32-x64-gnu": "4.55.1", - "@rollup/rollup-win32-x64-msvc": "4.55.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/swiper": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/swiper/-/swiper-12.0.3.tgz", - "integrity": "sha512-BHd6U1VPEIksrXlyXjMmRWO0onmdNPaTAFduzqR3pgjvi7KfmUCAm/0cj49u2D7B0zNjMw02TSeXfinC1hDCXg==", - "funding": [ - { - "type": "patreon", - "url": "https://www.patreon.com/swiperjs" - }, - { - "type": "open_collective", - "url": "http://opencollective.com/swiper" - } - ], - "license": "MIT", - "engines": { - "node": ">= 4.7.0" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", - "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/throttle-debounce": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", - "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tlds": { - "version": "1.261.0", - "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", - "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", - "license": "MIT", - "bin": { - "tlds": "bin.js" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/ua-parser-js": { - "version": "1.0.41", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", - "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "license": "MIT", - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/zustand": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz", - "integrity": "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } + "name": "fromis9-frontend", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fromis9-frontend", + "version": "2.0.0", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@tanstack/react-query": "^5.90.16", + "@tanstack/react-virtual": "^3.13.18", + "canvas-confetti": "^1.9.4", + "clsx": "^2.1.1", + "dayjs": "^1.11.19", + "framer-motion": "^11.0.8", + "lucide-react": "^0.344.0", + "react": "^18.2.0", + "react-calendar": "^6.0.0", + "react-colorful": "^5.6.1", + "react-device-detect": "^2.2.3", + "react-dom": "^18.2.0", + "react-infinite-scroll-component": "^6.1.1", + "react-intersection-observer": "^10.0.0", + "react-ios-time-picker": "^0.2.2", + "react-linkify": "^1.0.0-alpha", + "react-photo-album": "^3.4.0", + "react-router-dom": "^6.22.3", + "react-window": "^2.2.3", + "swiper": "^12.0.3", + "zustand": "^5.0.9" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.22", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.18", + "vite": "^5.4.1" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.3", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.19", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.19", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.18", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.18", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@wojtekmaj/date-utils": { + "version": "2.0.2", + "license": "MIT", + "funding": { + "url": "https://github.com/wojtekmaj/date-utils?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.16", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001765", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvas-confetti": { + "version": "1.9.4", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "devOptional": true, + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.19", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "11.18.2", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-user-locale": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "memoize": "^10.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/get-user-locale?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.344.0", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/memoize": { + "version": "10.2.0", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/memoize?sponsor=1" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/motion-dom": { + "version": "11.18.1", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-calendar": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "@wojtekmaj/date-utils": "^2.0.2", + "clsx": "^2.0.0", + "get-user-locale": "^3.0.0", + "warning": "^4.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-calendar?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-colorful": { + "version": "5.6.1", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/react-device-detect": { + "version": "2.2.3", + "license": "MIT", + "dependencies": { + "ua-parser-js": "^1.0.33" + }, + "peerDependencies": { + "react": ">= 0.14.0", + "react-dom": ">= 0.14.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-infinite-scroll-component": { + "version": "6.1.1", + "license": "MIT", + "dependencies": { + "throttle-debounce": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, + "node_modules/react-intersection-observer": { + "version": "10.0.2", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-ios-time-picker": { + "version": "0.2.2", + "license": "MIT", + "dependencies": { + "react-portal": "^4.2.2" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "license": "MIT" + }, + "node_modules/react-linkify": { + "version": "1.0.0-alpha", + "license": "MIT", + "dependencies": { + "linkify-it": "^2.0.3", + "tlds": "^1.199.0" + } + }, + "node_modules/react-photo-album": { + "version": "3.4.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/igordanchenko" + }, + "peerDependencies": { + "@types/react": "^18 || ^19", + "react": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-portal": { + "version": "4.3.0", + "license": "MIT", + "dependencies": { + "prop-types": "^15.5.8" + }, + "peerDependencies": { + "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0", + "react-dom": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-window": { + "version": "2.2.5", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.55.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.3", + "@rollup/rollup-android-arm64": "4.55.3", + "@rollup/rollup-darwin-arm64": "4.55.3", + "@rollup/rollup-darwin-x64": "4.55.3", + "@rollup/rollup-freebsd-arm64": "4.55.3", + "@rollup/rollup-freebsd-x64": "4.55.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.3", + "@rollup/rollup-linux-arm-musleabihf": "4.55.3", + "@rollup/rollup-linux-arm64-gnu": "4.55.3", + "@rollup/rollup-linux-arm64-musl": "4.55.3", + "@rollup/rollup-linux-loong64-gnu": "4.55.3", + "@rollup/rollup-linux-loong64-musl": "4.55.3", + "@rollup/rollup-linux-ppc64-gnu": "4.55.3", + "@rollup/rollup-linux-ppc64-musl": "4.55.3", + "@rollup/rollup-linux-riscv64-gnu": "4.55.3", + "@rollup/rollup-linux-riscv64-musl": "4.55.3", + "@rollup/rollup-linux-s390x-gnu": "4.55.3", + "@rollup/rollup-linux-x64-gnu": "4.55.3", + "@rollup/rollup-linux-x64-musl": "4.55.3", + "@rollup/rollup-openbsd-x64": "4.55.3", + "@rollup/rollup-openharmony-arm64": "4.55.3", + "@rollup/rollup-win32-arm64-msvc": "4.55.3", + "@rollup/rollup-win32-ia32-msvc": "4.55.3", + "@rollup/rollup-win32-x64-gnu": "4.55.3", + "@rollup/rollup-win32-x64-msvc": "4.55.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swiper": { + "version": "12.0.3", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "license": "MIT", + "engines": { + "node": ">= 4.7.0" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throttle-debounce": { + "version": "2.3.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tlds": { + "version": "1.261.0", + "license": "MIT", + "bin": { + "tlds": "bin.js" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "node_modules/ua-parser-js": { + "version": "1.0.41", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/warning": { + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/zustand": { + "version": "5.0.10", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } + } } diff --git a/frontend/package.json b/frontend/package.json index e891306..3ae2e24 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,43 +1,44 @@ { - "name": "fromis9-frontend", - "private": true, - "version": "1.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "@babel/runtime": "^7.28.6", - "@tanstack/react-query": "^5.90.16", - "@tanstack/react-virtual": "^3.13.18", - "canvas-confetti": "^1.9.4", - "dayjs": "^1.11.19", - "framer-motion": "^11.0.8", - "lucide-react": "^0.344.0", - "react": "^18.2.0", - "react-calendar": "^6.0.0", - "react-colorful": "^5.6.1", - "react-device-detect": "^2.2.3", - "react-dom": "^18.2.0", - "react-infinite-scroll-component": "^6.1.1", - "react-intersection-observer": "^10.0.0", - "react-ios-time-picker": "^0.2.2", - "react-linkify": "^1.0.0-alpha", - "react-photo-album": "^3.4.0", - "react-router-dom": "^6.22.3", - "react-window": "^2.2.3", - "swiper": "^12.0.3", - "zustand": "^5.0.9" - }, - "devDependencies": { - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "autoprefixer": "^10.4.22", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.18", - "vite": "^5.4.1" - } + "name": "fromis9-frontend", + "private": true, + "version": "2.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@babel/runtime": "^7.28.6", + "@tanstack/react-query": "^5.90.16", + "@tanstack/react-virtual": "^3.13.18", + "canvas-confetti": "^1.9.4", + "clsx": "^2.1.1", + "dayjs": "^1.11.19", + "framer-motion": "^11.0.8", + "lucide-react": "^0.344.0", + "react": "^18.2.0", + "react-calendar": "^6.0.0", + "react-colorful": "^5.6.1", + "react-device-detect": "^2.2.3", + "react-dom": "^18.2.0", + "react-infinite-scroll-component": "^6.1.1", + "react-intersection-observer": "^10.0.0", + "react-ios-time-picker": "^0.2.2", + "react-linkify": "^1.0.0-alpha", + "react-photo-album": "^3.4.0", + "react-router-dom": "^6.22.3", + "react-window": "^2.2.3", + "swiper": "^12.0.3", + "zustand": "^5.0.9" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.22", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.18", + "vite": "^5.4.1" + } } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 6d1df21..51bc0e1 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,121 +3,191 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { BrowserView, MobileView } from 'react-device-detect'; // 공통 컴포넌트 -import ScrollToTop from './components/ScrollToTop'; +import { ScrollToTop } from '@/components/common'; -// PC 페이지 -import PCHome from './pages/pc/public/Home'; -import PCMembers from './pages/pc/public/Members'; -import PCAlbum from './pages/pc/public/Album'; -import PCAlbumDetail from './pages/pc/public/AlbumDetail'; -import PCAlbumGallery from './pages/pc/public/AlbumGallery'; -import PCTrackDetail from './pages/pc/public/TrackDetail'; -import PCSchedule from './pages/pc/public/Schedule'; -import PCScheduleDetail from './pages/pc/public/ScheduleDetail'; -import PCBirthday from './pages/pc/public/Birthday'; -import PCNotFound from './pages/pc/public/NotFound'; +// PC 레이아웃 +import { Layout as PCLayout } from '@/components/pc/public'; -// 모바일 페이지 -import MobileHome from './pages/mobile/public/Home'; -import MobileMembers from './pages/mobile/public/Members'; -import MobileAlbum from './pages/mobile/public/Album'; -import MobileAlbumDetail from './pages/mobile/public/AlbumDetail'; -import MobileAlbumGallery from './pages/mobile/public/AlbumGallery'; -import MobileTrackDetail from './pages/mobile/public/TrackDetail'; -import MobileSchedule from './pages/mobile/public/Schedule'; -import MobileScheduleDetail from './pages/mobile/public/ScheduleDetail'; -import MobileNotFound from './pages/mobile/public/NotFound'; +// Mobile 레이아웃 +import { Layout as MobileLayout } from '@/components/mobile'; -// 관리자 페이지 -import AdminLogin from './pages/pc/admin/AdminLogin'; -import AdminDashboard from './pages/pc/admin/AdminDashboard'; -import AdminMembers from './pages/pc/admin/AdminMembers'; -import AdminMemberEdit from './pages/pc/admin/AdminMemberEdit'; -import AdminAlbums from './pages/pc/admin/AdminAlbums'; -import AdminAlbumForm from './pages/pc/admin/AdminAlbumForm'; -import AdminAlbumPhotos from './pages/pc/admin/AdminAlbumPhotos'; -import AdminSchedule from './pages/pc/admin/AdminSchedule'; -import AdminScheduleForm from './pages/pc/admin/AdminScheduleForm'; -import ScheduleFormPage from './pages/pc/admin/schedule/form'; -import AdminScheduleCategory from './pages/pc/admin/AdminScheduleCategory'; -import AdminScheduleBots from './pages/pc/admin/AdminScheduleBots'; -import AdminScheduleDict from './pages/pc/admin/AdminScheduleDict'; -import YouTubeEditForm from './pages/pc/admin/schedule/edit/YouTubeEditForm'; +// PC 공개 페이지 +import PCHome from '@/pages/pc/public/home/Home'; +import PCMembers from '@/pages/pc/public/members/Members'; +import PCSchedule from '@/pages/pc/public/schedule/Schedule'; +import PCScheduleDetail from '@/pages/pc/public/schedule/ScheduleDetail'; +import PCBirthday from '@/pages/pc/public/schedule/Birthday'; +import PCAlbum from '@/pages/pc/public/album/Album'; +import PCAlbumDetail from '@/pages/pc/public/album/AlbumDetail'; +import PCTrackDetail from '@/pages/pc/public/album/TrackDetail'; +import PCAlbumGallery from '@/pages/pc/public/album/AlbumGallery'; +import PCNotFound from '@/pages/pc/public/common/NotFound'; -// 레이아웃 -import PCLayout from './components/pc/Layout'; -import MobileLayout from './components/mobile/Layout'; +// PC 관리자 페이지 +import AdminLogin from '@/pages/pc/admin/login/Login'; +import AdminDashboard from '@/pages/pc/admin/dashboard/Dashboard'; +import AdminMembers from '@/pages/pc/admin/members/Members'; +import AdminMemberEdit from '@/pages/pc/admin/members/MemberEdit'; +import AdminAlbums from '@/pages/pc/admin/albums/Albums'; +import AdminAlbumForm from '@/pages/pc/admin/albums/AlbumForm'; +import AdminAlbumPhotos from '@/pages/pc/admin/albums/AlbumPhotos'; +import AdminSchedules from '@/pages/pc/admin/schedules/Schedules'; +import AdminScheduleForm from '@/pages/pc/admin/schedules/ScheduleForm'; +import AdminScheduleFormPage from '@/pages/pc/admin/schedules/form'; +import AdminYouTubeEditForm from '@/pages/pc/admin/schedules/edit/YouTubeEditForm'; +import AdminScheduleCategory from '@/pages/pc/admin/schedules/ScheduleCategory'; +import AdminScheduleDict from '@/pages/pc/admin/schedules/ScheduleDict'; +import AdminScheduleBots from '@/pages/pc/admin/schedules/ScheduleBots'; +import AdminNotFound from '@/pages/pc/admin/common/NotFound'; -// PC 환경에서 body에 클래스 추가하는 래퍼 +// Mobile 페이지 +import MobileHome from '@/pages/mobile/home/Home'; +import MobileMembers from '@/pages/mobile/members/Members'; +import MobileSchedule from '@/pages/mobile/schedule/Schedule'; +import MobileScheduleDetail from '@/pages/mobile/schedule/ScheduleDetail'; +import MobileBirthday from '@/pages/mobile/schedule/Birthday'; +import MobileAlbum from '@/pages/mobile/album/Album'; +import MobileAlbumDetail from '@/pages/mobile/album/AlbumDetail'; +import MobileTrackDetail from '@/pages/mobile/album/TrackDetail'; +import MobileAlbumGallery from '@/pages/mobile/album/AlbumGallery'; +import MobileNotFound from '@/pages/mobile/common/NotFound'; + +/** + * PC 환경에서 body에 클래스 추가하는 래퍼 + */ function PCWrapper({ children }) { - useEffect(() => { - document.body.classList.add('is-pc'); - return () => document.body.classList.remove('is-pc'); - }, []); - return children; + useEffect(() => { + document.body.classList.add('is-pc'); + return () => document.body.classList.remove('is-pc'); + }, []); + return children; } +/** + * 프로미스나인 팬사이트 메인 앱 + * react-device-detect를 사용한 PC/Mobile 분리 + */ function App() { - return ( - - - - - - {/* 관리자 페이지 (레이아웃 없음) */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + return ( + + - {/* 일반 페이지 (레이아웃 포함) */} - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - } /> - - - - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - - ); + {/* PC 뷰 */} + + + + {/* 관리자 페이지 (자체 레이아웃 사용) */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* 관리자 404 페이지 */} + } /> + + {/* 일반 페이지 (레이아웃 포함) */} + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* 404 페이지 */} + } /> + + + } + /> + + + + + {/* Mobile 뷰 */} + + + + + + } + /> + + + + } + /> + + + + } + /> + } /> + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + {/* 404 페이지 */} + } /> + + + + ); } export default App; - diff --git a/frontend/src/api/admin/albums.js b/frontend/src/api/admin/albums.js index 6c203c5..582e291 100644 --- a/frontend/src/api/admin/albums.js +++ b/frontend/src/api/admin/albums.js @@ -1,62 +1,97 @@ /** - * 어드민 앨범 관리 API + * 관리자 앨범 API */ -import { fetchAdminApi, fetchAdminFormData } from "../index"; +import { fetchAuthApi, fetchFormData } from '@/api/client'; -// 앨범 목록 조회 +/** + * 앨범 목록 조회 + * @returns {Promise} + */ export async function getAlbums() { - return fetchAdminApi("/api/albums"); + return fetchAuthApi('/albums'); } -// 앨범 상세 조회 +/** + * 앨범 상세 조회 + * @param {number} id - 앨범 ID + * @returns {Promise} + */ export async function getAlbum(id) { - return fetchAdminApi(`/api/albums/${id}`); + return fetchAuthApi(`/albums/${id}`); } -// 앨범 생성 +/** + * 앨범 생성 + * @param {FormData} formData - 앨범 데이터 + * @returns {Promise} + */ export async function createAlbum(formData) { - return fetchAdminFormData("/api/albums", formData, "POST"); + return fetchFormData('/albums', formData, 'POST'); } -// 앨범 수정 +/** + * 앨범 수정 + * @param {number} id - 앨범 ID + * @param {FormData} formData - 앨범 데이터 + * @returns {Promise} + */ export async function updateAlbum(id, formData) { - return fetchAdminFormData(`/api/albums/${id}`, formData, "PUT"); + return fetchFormData(`/albums/${id}`, formData, 'PUT'); } -// 앨범 삭제 +/** + * 앨범 삭제 + * @param {number} id - 앨범 ID + * @returns {Promise} + */ export async function deleteAlbum(id) { - return fetchAdminApi(`/api/albums/${id}`, { method: "DELETE" }); + return fetchAuthApi(`/albums/${id}`, { method: 'DELETE' }); } -// 앨범 사진 목록 조회 +/** + * 앨범 사진 목록 조회 + * @param {number} albumId - 앨범 ID + * @returns {Promise} + */ export async function getAlbumPhotos(albumId) { - return fetchAdminApi(`/api/albums/${albumId}/photos`); + return fetchAuthApi(`/albums/${albumId}/photos`); } -// 앨범 사진 업로드 +/** + * 앨범 사진 업로드 + * @param {number} albumId - 앨범 ID + * @param {FormData} formData - 사진 데이터 + * @returns {Promise} + */ export async function uploadAlbumPhotos(albumId, formData) { - return fetchAdminFormData( - `/api/albums/${albumId}/photos`, - formData, - "POST" - ); + return fetchFormData(`/albums/${albumId}/photos`, formData, 'POST'); } -// 앨범 사진 삭제 +/** + * 앨범 사진 삭제 + * @param {number} albumId - 앨범 ID + * @param {number} photoId - 사진 ID + * @returns {Promise} + */ export async function deleteAlbumPhoto(albumId, photoId) { - return fetchAdminApi(`/api/albums/${albumId}/photos/${photoId}`, { - method: "DELETE", - }); + return fetchAuthApi(`/albums/${albumId}/photos/${photoId}`, { method: 'DELETE' }); } -// 앨범 티저 목록 조회 +/** + * 앨범 티저 목록 조회 + * @param {number} albumId - 앨범 ID + * @returns {Promise} + */ export async function getAlbumTeasers(albumId) { - return fetchAdminApi(`/api/albums/${albumId}/teasers`); + return fetchAuthApi(`/albums/${albumId}/teasers`); } -// 앨범 티저 삭제 +/** + * 앨범 티저 삭제 + * @param {number} albumId - 앨범 ID + * @param {number} teaserId - 티저 ID + * @returns {Promise} + */ export async function deleteAlbumTeaser(albumId, teaserId) { - return fetchAdminApi(`/api/albums/${albumId}/teasers/${teaserId}`, { - method: "DELETE", - }); + return fetchAuthApi(`/albums/${albumId}/teasers/${teaserId}`, { method: 'DELETE' }); } diff --git a/frontend/src/api/admin/auth.js b/frontend/src/api/admin/auth.js index 968d2d2..98c227b 100644 --- a/frontend/src/api/admin/auth.js +++ b/frontend/src/api/admin/auth.js @@ -1,42 +1,37 @@ /** - * 어드민 인증 API + * 관리자 인증 API */ -import { fetchAdminApi } from "../index"; +import { fetchApi, fetchAuthApi } from '@/api/client'; -// 토큰 검증 -export async function verifyToken() { - return fetchAdminApi("/api/auth/verify"); -} - -// 로그인 +/** + * 로그인 + * @param {string} username - 사용자명 + * @param {string} password - 비밀번호 + * @returns {Promise<{token: string, user: object}>} + */ export async function login(username, password) { - const response = await fetch("/api/auth/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, + return fetchApi('/auth/login', { + method: 'POST', body: JSON.stringify({ username, password }), }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "로그인 실패"); - } - - return response.json(); } -// 로그아웃 (로컬 스토리지 정리) -export function logout() { - localStorage.removeItem("adminToken"); - localStorage.removeItem("adminUser"); +/** + * 토큰 검증 + * @returns {Promise<{valid: boolean, user: object}>} + */ +export async function verifyToken() { + return fetchAuthApi('/auth/verify'); } -// 현재 사용자 정보 가져오기 -export function getCurrentUser() { - const userData = localStorage.getItem("adminUser"); - return userData ? JSON.parse(userData) : null; -} - -// 토큰 존재 여부 확인 -export function hasToken() { - return !!localStorage.getItem("adminToken"); +/** + * 비밀번호 변경 + * @param {string} currentPassword - 현재 비밀번호 + * @param {string} newPassword - 새 비밀번호 + */ +export async function changePassword(currentPassword, newPassword) { + return fetchAuthApi('/auth/change-password', { + method: 'POST', + body: JSON.stringify({ currentPassword, newPassword }), + }); } diff --git a/frontend/src/api/admin/bots.js b/frontend/src/api/admin/bots.js index fab0d51..1aff9cb 100644 --- a/frontend/src/api/admin/bots.js +++ b/frontend/src/api/admin/bots.js @@ -1,34 +1,55 @@ /** - * 어드민 봇 관리 API + * 관리자 봇 관리 API */ -import { fetchAdminApi } from "../index"; +import { fetchAuthApi } from '@/api/client'; -// 봇 목록 조회 +/** + * 봇 목록 조회 + * @returns {Promise} + */ export async function getBots() { - return fetchAdminApi("/api/admin/bots"); + return fetchAuthApi('/admin/bots'); } -// 봇 시작 +/** + * 봇 시작 + * @param {string} id - 봇 ID + * @returns {Promise} + */ export async function startBot(id) { - return fetchAdminApi(`/api/admin/bots/${id}/start`, { method: "POST" }); + return fetchAuthApi(`/admin/bots/${id}/start`, { method: 'POST' }); } -// 봇 정지 +/** + * 봇 정지 + * @param {string} id - 봇 ID + * @returns {Promise} + */ export async function stopBot(id) { - return fetchAdminApi(`/api/admin/bots/${id}/stop`, { method: "POST" }); + return fetchAuthApi(`/admin/bots/${id}/stop`, { method: 'POST' }); } -// 봇 전체 동기화 +/** + * 봇 전체 동기화 + * @param {string} id - 봇 ID + * @returns {Promise} + */ export async function syncAllVideos(id) { - return fetchAdminApi(`/api/admin/bots/${id}/sync-all`, { method: "POST" }); + return fetchAuthApi(`/admin/bots/${id}/sync-all`, { method: 'POST' }); } -// 할당량 경고 조회 +/** + * 할당량 경고 조회 + * @returns {Promise<{warning: boolean, message: string}>} + */ export async function getQuotaWarning() { - return fetchAdminApi("/api/admin/bots/quota-warning"); + return fetchAuthApi('/admin/bots/quota-warning'); } -// 할당량 경고 해제 +/** + * 할당량 경고 해제 + * @returns {Promise} + */ export async function dismissQuotaWarning() { - return fetchAdminApi("/api/admin/bots/quota-warning", { method: "DELETE" }); + return fetchAuthApi('/admin/bots/quota-warning', { method: 'DELETE' }); } diff --git a/frontend/src/api/admin/categories.js b/frontend/src/api/admin/categories.js index fe2ab78..08e573e 100644 --- a/frontend/src/api/admin/categories.js +++ b/frontend/src/api/admin/categories.js @@ -1,40 +1,60 @@ /** - * 어드민 카테고리 관리 API + * 관리자 카테고리 API */ -import { fetchAdminApi } from "../index"; +import { fetchAuthApi } from '@/api/client'; -// 카테고리 목록 조회 +/** + * 카테고리 목록 조회 + * @returns {Promise} + */ export async function getCategories() { - return fetchAdminApi("/api/schedules/categories"); + return fetchAuthApi('/schedules/categories'); } -// 카테고리 생성 +/** + * 카테고리 생성 + * @param {object} data - 카테고리 데이터 + * @param {string} data.name - 카테고리 이름 + * @param {string} data.color - 색상 코드 + * @returns {Promise} + */ export async function createCategory(data) { - return fetchAdminApi("/api/admin/schedule-categories", { - method: "POST", + return fetchAuthApi('/admin/schedule-categories', { + method: 'POST', body: JSON.stringify(data), }); } -// 카테고리 수정 +/** + * 카테고리 수정 + * @param {number} id - 카테고리 ID + * @param {object} data - 카테고리 데이터 + * @returns {Promise} + */ export async function updateCategory(id, data) { - return fetchAdminApi(`/api/admin/schedule-categories/${id}`, { - method: "PUT", + return fetchAuthApi(`/admin/schedule-categories/${id}`, { + method: 'PUT', body: JSON.stringify(data), }); } -// 카테고리 삭제 +/** + * 카테고리 삭제 + * @param {number} id - 카테고리 ID + * @returns {Promise} + */ export async function deleteCategory(id) { - return fetchAdminApi(`/api/admin/schedule-categories/${id}`, { - method: "DELETE", - }); + return fetchAuthApi(`/admin/schedule-categories/${id}`, { method: 'DELETE' }); } -// 카테고리 순서 변경 +/** + * 카테고리 순서 변경 + * @param {Array<{id: number, sort_order: number}>} orders - 순서 데이터 + * @returns {Promise} + */ export async function reorderCategories(orders) { - return fetchAdminApi("/api/admin/schedule-categories-order", { - method: "PUT", + return fetchAuthApi('/admin/schedule-categories-order', { + method: 'PUT', body: JSON.stringify({ orders }), }); } diff --git a/frontend-temp/src/api/admin/index.js b/frontend/src/api/admin/index.js similarity index 100% rename from frontend-temp/src/api/admin/index.js rename to frontend/src/api/admin/index.js diff --git a/frontend/src/api/admin/members.js b/frontend/src/api/admin/members.js index 065bef2..7a83ad6 100644 --- a/frontend/src/api/admin/members.js +++ b/frontend/src/api/admin/members.js @@ -1,19 +1,31 @@ /** - * 어드민 멤버 관리 API + * 관리자 멤버 API */ -import { fetchAdminApi, fetchAdminFormData } from "../index"; +import { fetchAuthApi, fetchFormData } from '@/api/client'; -// 멤버 목록 조회 +/** + * 멤버 목록 조회 + * @returns {Promise} + */ export async function getMembers() { - return fetchAdminApi("/api/members"); + return fetchAuthApi('/members'); } -// 멤버 상세 조회 +/** + * 멤버 상세 조회 + * @param {number} id - 멤버 ID + * @returns {Promise} + */ export async function getMember(id) { - return fetchAdminApi(`/api/members/${id}`); + return fetchAuthApi(`/members/${id}`); } -// 멤버 수정 +/** + * 멤버 수정 + * @param {number} id - 멤버 ID + * @param {FormData} formData - 멤버 데이터 + * @returns {Promise} + */ export async function updateMember(id, formData) { - return fetchAdminFormData(`/api/members/${id}`, formData, "PUT"); + return fetchFormData(`/members/${id}`, formData, 'PUT'); } diff --git a/frontend/src/api/admin/schedules.js b/frontend/src/api/admin/schedules.js index 26fdbdc..d7855c8 100644 --- a/frontend/src/api/admin/schedules.js +++ b/frontend/src/api/admin/schedules.js @@ -1,7 +1,7 @@ /** - * 어드민 일정 관리 API + * 관리자 일정 API */ -import { fetchAdminApi, fetchAdminFormData } from "../index"; +import { fetchAuthApi, fetchFormData } from '@/api/client'; /** * API 응답을 프론트엔드 형식으로 변환 @@ -22,9 +22,7 @@ function transformSchedule(schedule) { } // members 배열을 문자열로 (기존 코드 호환성) - const memberNames = Array.isArray(schedule.members) - ? schedule.members.join(',') - : ''; + const memberNames = Array.isArray(schedule.members) ? schedule.members.join(',') : ''; return { ...schedule, @@ -37,16 +35,28 @@ function transformSchedule(schedule) { }; } -// 일정 목록 조회 (월별) +/** + * 일정 목록 조회 (월별) + * @param {number} year - 년도 + * @param {number} month - 월 + * @returns {Promise} + */ export async function getSchedules(year, month) { - const data = await fetchAdminApi(`/api/schedules?year=${year}&month=${month}`); + const data = await fetchAuthApi(`/schedules?year=${year}&month=${month}`); return (data.schedules || []).map(transformSchedule); } -// 일정 검색 (Meilisearch) +/** + * 일정 검색 (Meilisearch) + * @param {string} query - 검색어 + * @param {object} options - 페이지네이션 옵션 + * @param {number} options.offset - 시작 위치 + * @param {number} options.limit - 조회 개수 + * @returns {Promise<{schedules: Array, total: number}>} + */ export async function searchSchedules(query, { offset = 0, limit = 20 } = {}) { - const data = await fetchAdminApi( - `/api/schedules?search=${encodeURIComponent(query)}&offset=${offset}&limit=${limit}` + const data = await fetchAuthApi( + `/schedules?search=${encodeURIComponent(query)}&offset=${offset}&limit=${limit}` ); return { ...data, @@ -54,22 +64,39 @@ export async function searchSchedules(query, { offset = 0, limit = 20 } = {}) { }; } -// 일정 상세 조회 +/** + * 일정 상세 조회 + * @param {number} id - 일정 ID + * @returns {Promise} + */ export async function getSchedule(id) { - return fetchAdminApi(`/api/admin/schedules/${id}`); + return fetchAuthApi(`/schedules/${id}`); } -// 일정 생성 +/** + * 일정 생성 + * @param {FormData} formData - 일정 데이터 + * @returns {Promise} + */ export async function createSchedule(formData) { - return fetchAdminFormData("/api/admin/schedules", formData, "POST"); + return fetchFormData('/admin/schedules', formData, 'POST'); } -// 일정 수정 +/** + * 일정 수정 + * @param {number} id - 일정 ID + * @param {FormData} formData - 일정 데이터 + * @returns {Promise} + */ export async function updateSchedule(id, formData) { - return fetchAdminFormData(`/api/admin/schedules/${id}`, formData, "PUT"); + return fetchFormData(`/admin/schedules/${id}`, formData, 'PUT'); } -// 일정 삭제 +/** + * 일정 삭제 + * @param {number} id - 일정 ID + * @returns {Promise} + */ export async function deleteSchedule(id) { - return fetchAdminApi(`/api/schedules/${id}`, { method: "DELETE" }); + return fetchAuthApi(`/schedules/${id}`, { method: 'DELETE' }); } diff --git a/frontend/src/api/admin/stats.js b/frontend/src/api/admin/stats.js index 5f7577e..c67306b 100644 --- a/frontend/src/api/admin/stats.js +++ b/frontend/src/api/admin/stats.js @@ -1,9 +1,12 @@ /** - * 어드민 통계 API + * 관리자 통계 API */ -import { fetchAdminApi } from "../index"; +import { fetchAuthApi } from '@/api/client'; -// 대시보드 통계 조회 +/** + * 대시보드 통계 조회 + * @returns {Promise} + */ export async function getStats() { - return fetchAdminApi("/api/stats"); + return fetchAuthApi('/stats'); } diff --git a/frontend/src/api/admin/suggestions.js b/frontend/src/api/admin/suggestions.js index 6676697..746eb27 100644 --- a/frontend/src/api/admin/suggestions.js +++ b/frontend/src/api/admin/suggestions.js @@ -1,17 +1,24 @@ /** - * 어드민 추천 검색어 API + * 관리자 추천 검색어 API */ -import { fetchAdminApi } from "../index"; +import { fetchAuthApi } from '@/api/client'; -// 사전 내용 조회 +/** + * 사전 내용 조회 + * @returns {Promise<{content: string}>} + */ export async function getDict() { - return fetchAdminApi("/api/schedules/suggestions/dict"); + return fetchAuthApi('/schedules/suggestions/dict'); } -// 사전 저장 +/** + * 사전 저장 + * @param {string} content - 사전 내용 + * @returns {Promise} + */ export async function saveDict(content) { - return fetchAdminApi("/api/schedules/suggestions/dict", { - method: "PUT", + return fetchAuthApi('/schedules/suggestions/dict', { + method: 'PUT', body: JSON.stringify({ content }), }); } diff --git a/frontend-temp/src/api/client.js b/frontend/src/api/client.js similarity index 100% rename from frontend-temp/src/api/client.js rename to frontend/src/api/client.js diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 661c0b4..59ca0b4 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -1,64 +1,16 @@ /** - * 공통 API 유틸리티 - * 모든 API 호출에서 사용되는 기본 fetch 래퍼 + * API 통합 export */ -// 기본 fetch 래퍼 -export async function fetchApi(url, options = {}) { - const headers = { ...options.headers }; +// 공통 유틸리티 +export * from './client'; - // body가 있을 때만 Content-Type 설정 (DELETE 등 body 없는 요청 대응) - if (options.body) { - headers["Content-Type"] = "application/json"; - } +// 공개 API +export * from './public'; +export * as scheduleApi from './public/schedules'; +export * as albumApi from './public/albums'; +export * as memberApi from './public/members'; - const response = await fetch(url, { - ...options, - headers, - }); - - if (!response.ok) { - const error = await response.json().catch(() => ({ error: "요청 실패" })); - throw new Error(error.error || `HTTP ${response.status}`); - } - - return response.json(); -} - -// 어드민 토큰 가져오기 -export function getAdminToken() { - return localStorage.getItem("adminToken"); -} - -// 어드민 API용 fetch 래퍼 (토큰 자동 추가) -export async function fetchAdminApi(url, options = {}) { - const token = getAdminToken(); - - return fetchApi(url, { - ...options, - headers: { - ...options.headers, - Authorization: `Bearer ${token}`, - }, - }); -} - -// FormData 전송용 (이미지 업로드 등) -export async function fetchAdminFormData(url, formData, method = "POST") { - const token = getAdminToken(); - - const response = await fetch(url, { - method, - headers: { - Authorization: `Bearer ${token}`, - }, - body: formData, - }); - - if (!response.ok) { - const error = await response.json().catch(() => ({ error: "요청 실패" })); - throw new Error(error.error || `HTTP ${response.status}`); - } - - return response.json(); -} +// 관리자 API +export * from './admin'; +export * as authApi from './admin/auth'; diff --git a/frontend/src/api/public/albums.js b/frontend/src/api/public/albums.js index 7df0c2f..010c069 100644 --- a/frontend/src/api/public/albums.js +++ b/frontend/src/api/public/albums.js @@ -1,38 +1,101 @@ /** - * 앨범 관련 공개 API + * 앨범 API */ -import { fetchApi } from "../index"; +import { fetchApi, fetchAuthApi, fetchFormData } from '@/api/client'; -// 앨범 목록 조회 +// ==================== 공개 API ==================== + +/** + * 앨범 목록 조회 + */ export async function getAlbums() { - return fetchApi("/api/albums"); + return fetchApi('/albums'); } -// 앨범 상세 조회 (ID) +/** + * 앨범 상세 조회 (ID) + */ export async function getAlbum(id) { - return fetchApi(`/api/albums/${id}`); + return fetchApi(`/albums/${id}`); } -// 앨범 상세 조회 (이름) +/** + * 앨범 상세 조회 (이름) + */ export async function getAlbumByName(name) { - return fetchApi(`/api/albums/by-name/${name}`); + return fetchApi(`/albums/by-name/${encodeURIComponent(name)}`); } -// 앨범 사진 조회 +/** + * 앨범 사진 조회 + */ export async function getAlbumPhotos(albumId) { - return fetchApi(`/api/albums/${albumId}/photos`); + return fetchApi(`/albums/${albumId}/photos`); } -// 앨범 트랙 조회 +/** + * 앨범 트랙 조회 + */ export async function getAlbumTracks(albumId) { - return fetchApi(`/api/albums/${albumId}/tracks`); + return fetchApi(`/albums/${albumId}/tracks`); } -// 트랙 상세 조회 (앨범명, 트랙명으로) +/** + * 트랙 상세 조회 (앨범명, 트랙명으로) + */ export async function getTrack(albumName, trackTitle) { return fetchApi( - `/api/albums/by-name/${encodeURIComponent( - albumName - )}/track/${encodeURIComponent(trackTitle)}` + `/albums/by-name/${encodeURIComponent(albumName)}/track/${encodeURIComponent(trackTitle)}` ); } + +/** + * 앨범 티저 조회 + */ +export async function getAlbumTeasers(albumId) { + return fetchApi(`/albums/${albumId}/teasers`); +} + +// ==================== 어드민 API ==================== + +/** + * [Admin] 앨범 생성 + */ +export async function createAlbum(formData) { + return fetchFormData('/albums', formData, 'POST'); +} + +/** + * [Admin] 앨범 수정 + */ +export async function updateAlbum(id, formData) { + return fetchFormData(`/albums/${id}`, formData, 'PUT'); +} + +/** + * [Admin] 앨범 삭제 + */ +export async function deleteAlbum(id) { + return fetchAuthApi(`/albums/${id}`, { method: 'DELETE' }); +} + +/** + * [Admin] 앨범 사진 업로드 + */ +export async function uploadAlbumPhotos(albumId, formData) { + return fetchFormData(`/albums/${albumId}/photos`, formData, 'POST'); +} + +/** + * [Admin] 앨범 사진 삭제 + */ +export async function deleteAlbumPhoto(albumId, photoId) { + return fetchAuthApi(`/albums/${albumId}/photos/${photoId}`, { method: 'DELETE' }); +} + +/** + * [Admin] 앨범 티저 삭제 + */ +export async function deleteAlbumTeaser(albumId, teaserId) { + return fetchAuthApi(`/albums/${albumId}/teasers/${teaserId}`, { method: 'DELETE' }); +} diff --git a/frontend-temp/src/api/public/index.js b/frontend/src/api/public/index.js similarity index 100% rename from frontend-temp/src/api/public/index.js rename to frontend/src/api/public/index.js diff --git a/frontend/src/api/public/members.js b/frontend/src/api/public/members.js index 0cb1ace..af55b14 100644 --- a/frontend/src/api/public/members.js +++ b/frontend/src/api/public/members.js @@ -1,14 +1,43 @@ /** - * 멤버 관련 공개 API + * 멤버 API */ -import { fetchApi } from "../index"; +import { fetchApi, fetchAuthApi, fetchFormData } from '@/api/client'; -// 멤버 목록 조회 +// ==================== 공개 API ==================== + +/** + * 멤버 목록 조회 + */ export async function getMembers() { - return fetchApi("/api/members"); + return fetchApi('/members'); } -// 멤버 상세 조회 +/** + * 멤버 상세 조회 + */ export async function getMember(id) { - return fetchApi(`/api/members/${id}`); + return fetchApi(`/members/${id}`); +} + +// ==================== 어드민 API ==================== + +/** + * [Admin] 멤버 생성 + */ +export async function createMember(formData) { + return fetchFormData('/admin/members', formData, 'POST'); +} + +/** + * [Admin] 멤버 수정 + */ +export async function updateMember(id, formData) { + return fetchFormData(`/admin/members/${id}`, formData, 'PUT'); +} + +/** + * [Admin] 멤버 삭제 + */ +export async function deleteMember(id) { + return fetchAuthApi(`/admin/members/${id}`, { method: 'DELETE' }); } diff --git a/frontend/src/api/public/schedules.js b/frontend/src/api/public/schedules.js index 56fbbe5..18a9739 100644 --- a/frontend/src/api/public/schedules.js +++ b/frontend/src/api/public/schedules.js @@ -1,8 +1,8 @@ /** - * 일정 관련 공개 API + * 스케줄 API */ -import { fetchApi } from "../index"; -import { getTodayKST } from "../../utils/date"; +import { fetchApi, fetchAuthApi, fetchFormData } from '@/api/client'; +import { getTodayKST, dayjs } from '@/utils'; /** * API 응답을 프론트엔드 형식으로 변환 @@ -17,9 +17,10 @@ function transformSchedule(schedule) { let date = ''; let time = null; if (schedule.datetime) { - const parts = schedule.datetime.split('T'); - date = parts[0]; - time = parts[1] || null; + const dt = dayjs(schedule.datetime); + date = dt.format('YYYY-MM-DD'); + // datetime에 T가 포함되어 있으면 시간이 있는 것 + time = schedule.datetime.includes('T') ? dt.format('HH:mm:ss') : null; } // members 배열을 문자열로 (기존 코드 호환성) @@ -38,23 +39,31 @@ function transformSchedule(schedule) { }; } -// 일정 목록 조회 (월별) +// ==================== 공개 API ==================== + +/** + * 스케줄 목록 조회 (월별) + */ export async function getSchedules(year, month) { - const data = await fetchApi(`/api/schedules?year=${year}&month=${month}`); + const data = await fetchApi(`/schedules?year=${year}&month=${month}`); return (data.schedules || []).map(transformSchedule); } -// 다가오는 일정 조회 (오늘 이후) +/** + * 다가오는 스케줄 조회 + */ export async function getUpcomingSchedules(limit = 3) { - const todayStr = getTodayKST(); - const data = await fetchApi(`/api/schedules?startDate=${todayStr}&limit=${limit}`); + const today = getTodayKST(); + const data = await fetchApi(`/schedules?startDate=${today}&limit=${limit}`); return (data.schedules || []).map(transformSchedule); } -// 일정 검색 (Meilisearch) +/** + * 스케줄 검색 (Meilisearch) + */ export async function searchSchedules(query, { offset = 0, limit = 20 } = {}) { const data = await fetchApi( - `/api/schedules?search=${encodeURIComponent(query)}&offset=${offset}&limit=${limit}` + `/schedules?search=${encodeURIComponent(query)}&offset=${offset}&limit=${limit}` ); return { ...data, @@ -62,12 +71,99 @@ export async function searchSchedules(query, { offset = 0, limit = 20 } = {}) { }; } -// 일정 상세 조회 +/** + * 스케줄 상세 조회 + */ export async function getSchedule(id) { - return fetchApi(`/api/schedules/${id}`); + return fetchApi(`/schedules/${id}`); } -// X 프로필 정보 조회 +/** + * X 프로필 정보 조회 + */ export async function getXProfile(username) { - return fetchApi(`/api/schedules/x-profile/${encodeURIComponent(username)}`); + return fetchApi(`/schedules/x-profile/${encodeURIComponent(username)}`); +} + +/** + * 카테고리 목록 조회 + */ +export async function getCategories() { + return fetchApi('/schedules/categories'); +} + +// ==================== 어드민 API ==================== + +/** + * [Admin] 스케줄 검색 + */ +export async function adminSearchSchedules(query) { + return fetchAuthApi(`/admin/schedules/search?q=${encodeURIComponent(query)}`); +} + +/** + * [Admin] 스케줄 상세 조회 + */ +export async function adminGetSchedule(id) { + return fetchAuthApi(`/admin/schedules/${id}`); +} + +/** + * [Admin] 스케줄 생성 + */ +export async function createSchedule(formData) { + return fetchFormData('/admin/schedules', formData, 'POST'); +} + +/** + * [Admin] 스케줄 수정 + */ +export async function updateSchedule(id, formData) { + return fetchFormData(`/admin/schedules/${id}`, formData, 'PUT'); +} + +/** + * [Admin] 스케줄 삭제 + */ +export async function deleteSchedule(id) { + return fetchAuthApi(`/schedules/${id}`, { method: 'DELETE' }); +} + +// ==================== 카테고리 어드민 API ==================== + +/** + * [Admin] 카테고리 생성 + */ +export async function createCategory(data) { + return fetchAuthApi('/admin/schedule-categories', { + method: 'POST', + body: JSON.stringify(data), + }); +} + +/** + * [Admin] 카테고리 수정 + */ +export async function updateCategory(id, data) { + return fetchAuthApi(`/admin/schedule-categories/${id}`, { + method: 'PUT', + body: JSON.stringify(data), + }); +} + +/** + * [Admin] 카테고리 삭제 + */ +export async function deleteCategory(id) { + return fetchAuthApi(`/admin/schedule-categories/${id}`, { method: 'DELETE' }); +} + +/** + * [Admin] 카테고리 순서 변경 + */ +export async function reorderCategories(orders) { + return fetchAuthApi('/admin/schedule-categories-order', { + method: 'PUT', + body: JSON.stringify({ orders }), + }); } diff --git a/frontend/src/components/ScrollToTop.jsx b/frontend/src/components/ScrollToTop.jsx deleted file mode 100644 index d74e45f..0000000 --- a/frontend/src/components/ScrollToTop.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffect } from 'react'; -import { useLocation } from 'react-router-dom'; - -// 페이지 이동 시 스크롤을 맨 위로 이동시키는 컴포넌트 -function ScrollToTop() { - const { pathname } = useLocation(); - - useEffect(() => { - // window 스크롤 초기화 - window.scrollTo(0, 0); - - // 모바일 레이아웃 스크롤 컨테이너 초기화 - const mobileContent = document.querySelector('.mobile-content'); - if (mobileContent) { - mobileContent.scrollTop = 0; - } - }, [pathname]); - - return null; -} - -export default ScrollToTop; diff --git a/frontend/src/components/Toast.jsx b/frontend/src/components/Toast.jsx deleted file mode 100644 index 2b58b17..0000000 --- a/frontend/src/components/Toast.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { motion, AnimatePresence } from 'framer-motion'; - -/** - * Toast 컴포넌트 (Minecraft Web 스타일) - * - 하단 중앙에 표시 - * - type: 'success' | 'error' | 'warning' - */ -function Toast({ toast, onClose }) { - return ( - - {toast && ( - - {toast.message} - - )} - - ); -} - -export default Toast; diff --git a/frontend/src/components/Tooltip.jsx b/frontend/src/components/Tooltip.jsx deleted file mode 100644 index e2cefeb..0000000 --- a/frontend/src/components/Tooltip.jsx +++ /dev/null @@ -1,70 +0,0 @@ -import { useState, useRef } from 'react'; -import ReactDOM from 'react-dom'; -import { motion, AnimatePresence } from 'framer-motion'; - -/** - * 커스텀 툴팁 컴포넌트 - * 마우스 커서를 따라다니는 방식 - * @param {React.ReactNode} children - 툴팁을 표시할 요소 - * @param {string|React.ReactNode} text - 툴팁에 표시할 내용 (content prop과 호환) - * @param {string|React.ReactNode} content - 툴팁에 표시할 내용 (text prop과 호환) - */ -const Tooltip = ({ children, text, content, className = "" }) => { - const [isVisible, setIsVisible] = useState(false); - const [position, setPosition] = useState({ bottom: 0, left: 0 }); - const triggerRef = useRef(null); - - // text 또는 content prop 사용 (문자열 또는 React 노드) - const tooltipContent = text || content; - - const handleMouseEnter = (e) => { - // 마우스 커서 위치를 기준으로 툴팁 위치 설정 (커서 위로) - setPosition({ - bottom: window.innerHeight - e.clientY + 10, - left: e.clientX - }); - setIsVisible(true); - }; - - const handleMouseMove = (e) => { - // 마우스 이동 시 툴팁 위치 업데이트 - setPosition({ - bottom: window.innerHeight - e.clientY + 10, - left: e.clientX - }); - }; - - return ( - <> -
setIsVisible(false)} - > - {children} -
- {isVisible && tooltipContent && ReactDOM.createPortal( - - - {tooltipContent} - - , - document.body - )} - - ); -}; - -export default Tooltip; diff --git a/frontend/src/components/admin/AdminHeader.jsx b/frontend/src/components/admin/AdminHeader.jsx deleted file mode 100644 index f472c57..0000000 --- a/frontend/src/components/admin/AdminHeader.jsx +++ /dev/null @@ -1,46 +0,0 @@ -/** - * AdminHeader 컴포넌트 - * 모든 Admin 페이지에서 공통으로 사용하는 헤더 - * 로고, Admin 배지, 사용자 정보, 로그아웃 버튼 포함 - */ -import { useNavigate, Link } from 'react-router-dom'; -import { LogOut } from 'lucide-react'; - -function AdminHeader({ user }) { - const navigate = useNavigate(); - - const handleLogout = () => { - localStorage.removeItem('adminToken'); - localStorage.removeItem('adminUser'); - navigate('/admin'); - }; - - return ( -
-
-
- - fromis_9 - - - Admin - -
-
- - 안녕하세요, {user?.username}님 - - -
-
-
- ); -} - -export default AdminHeader; diff --git a/frontend/src/components/admin/AdminLayout.jsx b/frontend/src/components/admin/AdminLayout.jsx deleted file mode 100644 index 1659916..0000000 --- a/frontend/src/components/admin/AdminLayout.jsx +++ /dev/null @@ -1,25 +0,0 @@ -/** - * AdminLayout 컴포넌트 - * 모든 Admin 페이지에서 공통으로 사용하는 레이아웃 - * 헤더 고정 + 본문 스크롤 구조 - */ -import { useLocation } from 'react-router-dom'; -import AdminHeader from './AdminHeader'; - -function AdminLayout({ user, children }) { - const location = useLocation(); - - // 일정 관리 페이지는 내부 스크롤 처리 - const isSchedulePage = location.pathname.includes('/admin/schedules'); - - return ( -
- -
- {children} -
-
- ); -} - -export default AdminLayout; diff --git a/frontend/src/components/admin/ConfirmDialog.jsx b/frontend/src/components/admin/ConfirmDialog.jsx deleted file mode 100644 index 4c67983..0000000 --- a/frontend/src/components/admin/ConfirmDialog.jsx +++ /dev/null @@ -1,115 +0,0 @@ -/** - * ConfirmDialog 컴포넌트 - * 삭제 등 위험한 작업의 확인을 위한 공통 다이얼로그 - * - * Props: - * - isOpen: 다이얼로그 표시 여부 - * - onClose: 닫기 콜백 - * - onConfirm: 확인 콜백 - * - title: 제목 (예: "앨범 삭제") - * - message: 메시지 내용 (ReactNode 가능) - * - confirmText: 확인 버튼 텍스트 (기본: "삭제") - * - cancelText: 취소 버튼 텍스트 (기본: "취소") - * - loading: 로딩 상태 - * - loadingText: 로딩 중 텍스트 (기본: "삭제 중...") - * - variant: 버튼 색상 (기본: "danger", "primary" 가능) - */ -import { motion, AnimatePresence } from 'framer-motion'; -import { AlertTriangle, Trash2 } from 'lucide-react'; - -function ConfirmDialog({ - isOpen, - onClose, - onConfirm, - title, - message, - confirmText = '삭제', - cancelText = '취소', - loading = false, - loadingText = '삭제 중...', - variant = 'danger', - icon: Icon = AlertTriangle -}) { - // 버튼 색상 설정 - const buttonColors = { - danger: 'bg-red-500 hover:bg-red-600', - primary: 'bg-primary hover:bg-primary-dark' - }; - - const iconBgColors = { - danger: 'bg-red-100', - primary: 'bg-primary/10' - }; - - const iconColors = { - danger: 'text-red-500', - primary: 'text-primary' - }; - - return ( - - {isOpen && ( - !loading && onClose()} - > - e.stopPropagation()} - > - {/* 헤더 */} -
-
- -
-

{title}

-
- - {/* 메시지 */} -
- {message} -
- - {/* 버튼 */} -
- - -
-
-
- )} -
- ); -} - -export default ConfirmDialog; diff --git a/frontend/src/components/admin/CustomDatePicker.jsx b/frontend/src/components/admin/CustomDatePicker.jsx deleted file mode 100644 index e4524b7..0000000 --- a/frontend/src/components/admin/CustomDatePicker.jsx +++ /dev/null @@ -1,263 +0,0 @@ -/** - * 커스텀 데이트픽커 컴포넌트 - * 연/월/일 선택이 가능한 드롭다운 형태의 날짜 선택기 - */ -import { useState, useEffect, useRef } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { Calendar, ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react'; - -function CustomDatePicker({ value, onChange, placeholder = '날짜 선택', showDayOfWeek = false }) { - const [isOpen, setIsOpen] = useState(false); - const [viewMode, setViewMode] = useState('days'); - const [viewDate, setViewDate] = useState(() => { - if (value) return new Date(value); - return new Date(); - }); - const ref = useRef(null); - - useEffect(() => { - const handleClickOutside = (e) => { - if (ref.current && !ref.current.contains(e.target)) { - setIsOpen(false); - setViewMode('days'); - } - }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); - - const year = viewDate.getFullYear(); - const month = viewDate.getMonth(); - - const firstDay = new Date(year, month, 1).getDay(); - const daysInMonth = new Date(year, month + 1, 0).getDate(); - - const days = []; - for (let i = 0; i < firstDay; i++) { - days.push(null); - } - for (let i = 1; i <= daysInMonth; i++) { - days.push(i); - } - - const MIN_YEAR = 2025; - const startYear = Math.max(MIN_YEAR, Math.floor(year / 12) * 12 - 1); - const years = Array.from({ length: 12 }, (_, i) => startYear + i); - const canGoPrevYearRange = startYear > MIN_YEAR; - - const prevMonth = () => setViewDate(new Date(year, month - 1, 1)); - const nextMonth = () => setViewDate(new Date(year, month + 1, 1)); - const prevYearRange = () => canGoPrevYearRange && setViewDate(new Date(Math.max(MIN_YEAR, year - 12), month, 1)); - const nextYearRange = () => setViewDate(new Date(year + 12, month, 1)); - - const selectDate = (day) => { - const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; - onChange(dateStr); - setIsOpen(false); - setViewMode('days'); - }; - - const selectYear = (y) => { - setViewDate(new Date(y, month, 1)); - }; - - const selectMonth = (m) => { - setViewDate(new Date(year, m, 1)); - setViewMode('days'); - }; - - // 날짜 표시 포맷 (요일 포함 옵션) - const formatDisplayDate = (dateStr) => { - if (!dateStr) return ''; - const [y, m, d] = dateStr.split('-'); - if (showDayOfWeek) { - const dayNames = ['일', '월', '화', '수', '목', '금', '토']; - const date = new Date(parseInt(y), parseInt(m) - 1, parseInt(d)); - const dayOfWeek = dayNames[date.getDay()]; - return `${y}년 ${parseInt(m)}월 ${parseInt(d)}일 (${dayOfWeek})`; - } - return `${y}년 ${parseInt(m)}월 ${parseInt(d)}일`; - }; - - const isSelected = (day) => { - if (!value || !day) return false; - const [y, m, d] = value.split('-'); - return parseInt(y) === year && parseInt(m) === month + 1 && parseInt(d) === day; - }; - - const isToday = (day) => { - if (!day) return false; - const today = new Date(); - return today.getFullYear() === year && today.getMonth() === month && today.getDate() === day; - }; - - const isCurrentYear = (y) => new Date().getFullYear() === y; - const isCurrentMonth = (m) => { - const today = new Date(); - return today.getFullYear() === year && today.getMonth() === m; - }; - - const monthNames = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월']; - - return ( -
- - - - {isOpen && ( - -
- - - -
- - - {viewMode === 'years' && ( - -
년도
-
- {years.map((y) => ( - - ))} -
-
-
- {monthNames.map((m, i) => ( - - ))} -
-
- )} - - {viewMode === 'months' && ( - -
월 선택
-
- {monthNames.map((m, i) => ( - - ))} -
-
- )} - - {viewMode === 'days' && ( - -
- {['일', '월', '화', '수', '목', '금', '토'].map((d, i) => ( -
- {d} -
- ))} -
-
- {days.map((day, i) => { - const dayOfWeek = i % 7; - return ( - - ); - })} -
-
- )} -
-
- )} -
-
- ); -} - -export default CustomDatePicker; diff --git a/frontend/src/components/admin/CustomTimePicker.jsx b/frontend/src/components/admin/CustomTimePicker.jsx deleted file mode 100644 index 9d42a96..0000000 --- a/frontend/src/components/admin/CustomTimePicker.jsx +++ /dev/null @@ -1,178 +0,0 @@ -/** - * CustomTimePicker 컴포넌트 - * 오전/오후, 시간, 분을 선택할 수 있는 시간 피커 - * NumberPicker를 사용하여 스크롤 방식 선택 제공 - */ -import { useState, useEffect, useRef } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { Clock } from 'lucide-react'; -import NumberPicker from './NumberPicker'; - -function CustomTimePicker({ value, onChange, placeholder = "시간 선택" }) { - const [isOpen, setIsOpen] = useState(false); - const ref = useRef(null); - - // 현재 값 파싱 - const parseValue = () => { - if (!value) return { hour: "12", minute: "00", period: "오후" }; - const [h, m] = value.split(":"); - const hour = parseInt(h); - const isPM = hour >= 12; - const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; - return { - hour: String(hour12).padStart(2, "0"), - minute: m, - period: isPM ? "오후" : "오전", - }; - }; - - const parsed = parseValue(); - const [selectedHour, setSelectedHour] = useState(parsed.hour); - const [selectedMinute, setSelectedMinute] = useState(parsed.minute); - const [selectedPeriod, setSelectedPeriod] = useState(parsed.period); - - // 외부 클릭 시 닫기 - useEffect(() => { - const handleClickOutside = (e) => { - if (ref.current && !ref.current.contains(e.target)) { - setIsOpen(false); - } - }; - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, []); - - // 피커 열릴 때 현재 값으로 초기화 - useEffect(() => { - if (isOpen) { - const parsed = parseValue(); - setSelectedHour(parsed.hour); - setSelectedMinute(parsed.minute); - setSelectedPeriod(parsed.period); - } - }, [isOpen, value]); - - // 시간 확정 - const handleSave = () => { - let hour = parseInt(selectedHour); - if (selectedPeriod === "오후" && hour !== 12) hour += 12; - if (selectedPeriod === "오전" && hour === 12) hour = 0; - const timeStr = `${String(hour).padStart(2, "0")}:${selectedMinute}`; - onChange(timeStr); - setIsOpen(false); - }; - - // 취소 - const handleCancel = () => { - setIsOpen(false); - }; - - // 초기화 - const handleClear = () => { - onChange(""); - setIsOpen(false); - }; - - // 표시용 포맷 - const displayValue = () => { - if (!value) return placeholder; - const [h, m] = value.split(":"); - const hour = parseInt(h); - const isPM = hour >= 12; - const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; - return `${isPM ? "오후" : "오전"} ${hour12}:${m}`; - }; - - // 피커 아이템 데이터 - const periods = ["오전", "오후"]; - const hours = [ - "01", "02", "03", "04", "05", "06", - "07", "08", "09", "10", "11", "12", - ]; - const minutes = Array.from({ length: 60 }, (_, i) => - String(i).padStart(2, "0") - ); - - return ( -
- - - - {isOpen && ( - - {/* 피커 영역 */} -
- {/* 오전/오후 (맨 앞) */} - - - {/* 시간 */} - - - - : - - - {/* 분 */} - -
- - {/* 푸터 버튼 */} -
- -
- - -
-
-
- )} -
-
- ); -} - -export default CustomTimePicker; diff --git a/frontend/src/components/admin/NumberPicker.jsx b/frontend/src/components/admin/NumberPicker.jsx deleted file mode 100644 index 807d4a5..0000000 --- a/frontend/src/components/admin/NumberPicker.jsx +++ /dev/null @@ -1,192 +0,0 @@ -/** - * NumberPicker 컴포넌트 - * 스크롤 가능한 숫자/값 선택 피커 - * AdminScheduleForm의 시간 선택에서 사용 - */ -import { useState, useEffect, useRef } from 'react'; - -function NumberPicker({ items, value, onChange }) { - const ITEM_HEIGHT = 40; - const containerRef = useRef(null); - const [offset, setOffset] = useState(0); - const offsetRef = useRef(0); // 드래그용 ref - const touchStartY = useRef(0); - const startOffset = useRef(0); - const isScrolling = useRef(false); - - // offset 변경시 ref도 업데이트 - useEffect(() => { - offsetRef.current = offset; - }, [offset]); - - // 초기 위치 설정 - useEffect(() => { - if (value !== null && value !== undefined) { - const index = items.indexOf(value); - if (index !== -1) { - const newOffset = -index * ITEM_HEIGHT; - setOffset(newOffset); - offsetRef.current = newOffset; - } - } - }, []); - - // 값 변경시 위치 업데이트 - useEffect(() => { - const index = items.indexOf(value); - if (index !== -1) { - const targetOffset = -index * ITEM_HEIGHT; - if (Math.abs(offset - targetOffset) > 1) { - setOffset(targetOffset); - offsetRef.current = targetOffset; - } - } - }, [value, items]); - - const centerOffset = ITEM_HEIGHT; // 중앙 위치 오프셋 - - // 아이템이 중앙에 있는지 확인 - const isItemInCenter = (item) => { - const itemIndex = items.indexOf(item); - const itemPosition = -itemIndex * ITEM_HEIGHT; - const tolerance = ITEM_HEIGHT / 2; - return Math.abs(offset - itemPosition) < tolerance; - }; - - // 오프셋 업데이트 (경계 제한) - const updateOffset = (newOffset) => { - const maxOffset = 0; - const minOffset = -(items.length - 1) * ITEM_HEIGHT; - return Math.min(maxOffset, Math.max(minOffset, newOffset)); - }; - - // 중앙 아이템 업데이트 - const updateCenterItem = (currentOffset) => { - const centerIndex = Math.round(-currentOffset / ITEM_HEIGHT); - if (centerIndex >= 0 && centerIndex < items.length) { - const centerItem = items[centerIndex]; - if (value !== centerItem) { - onChange(centerItem); - } - } - }; - - // 가장 가까운 아이템에 스냅 - const snapToClosestItem = (currentOffset) => { - const targetOffset = Math.round(currentOffset / ITEM_HEIGHT) * ITEM_HEIGHT; - setOffset(targetOffset); - offsetRef.current = targetOffset; - updateCenterItem(targetOffset); - }; - - // 터치 시작 - const handleTouchStart = (e) => { - e.stopPropagation(); - touchStartY.current = e.touches[0].clientY; - startOffset.current = offsetRef.current; - }; - - // 터치 이동 - const handleTouchMove = (e) => { - e.stopPropagation(); - const touchY = e.touches[0].clientY; - const deltaY = touchY - touchStartY.current; - const newOffset = updateOffset(startOffset.current + deltaY); - setOffset(newOffset); - offsetRef.current = newOffset; - }; - - // 터치 종료 - const handleTouchEnd = (e) => { - e.stopPropagation(); - snapToClosestItem(offsetRef.current); - }; - - // 마우스 휠 - 바깥 스크롤 방지 - const handleWheel = (e) => { - e.preventDefault(); - e.stopPropagation(); - if (isScrolling.current) return; - isScrolling.current = true; - - const newOffset = updateOffset( - offsetRef.current - Math.sign(e.deltaY) * ITEM_HEIGHT - ); - setOffset(newOffset); - offsetRef.current = newOffset; - snapToClosestItem(newOffset); - - setTimeout(() => { - isScrolling.current = false; - }, 50); - }; - - // 마우스 드래그 - const handleMouseDown = (e) => { - e.preventDefault(); - e.stopPropagation(); - touchStartY.current = e.clientY; - startOffset.current = offsetRef.current; - - const handleMouseMove = (moveEvent) => { - moveEvent.preventDefault(); - const deltaY = moveEvent.clientY - touchStartY.current; - const newOffset = updateOffset(startOffset.current + deltaY); - setOffset(newOffset); - offsetRef.current = newOffset; - }; - - const handleMouseUp = () => { - snapToClosestItem(offsetRef.current); - document.removeEventListener("mousemove", handleMouseMove); - document.removeEventListener("mouseup", handleMouseUp); - }; - - document.addEventListener("mousemove", handleMouseMove); - document.addEventListener("mouseup", handleMouseUp); - }; - - // wheel 이벤트 passive false로 등록 - useEffect(() => { - const container = containerRef.current; - if (container) { - container.addEventListener("wheel", handleWheel, { passive: false }); - return () => container.removeEventListener("wheel", handleWheel); - } - }, []); - - return ( -
- {/* 중앙 선택 영역 */} -
- - {/* 피커 내부 */} -
- {items.map((item) => ( -
- {item} -
- ))} -
-
- ); -} - -export default NumberPicker; diff --git a/frontend-temp/src/components/common/AnimatedNumber.jsx b/frontend/src/components/common/AnimatedNumber.jsx similarity index 100% rename from frontend-temp/src/components/common/AnimatedNumber.jsx rename to frontend/src/components/common/AnimatedNumber.jsx diff --git a/frontend-temp/src/components/common/ErrorBoundary.jsx b/frontend/src/components/common/ErrorBoundary.jsx similarity index 100% rename from frontend-temp/src/components/common/ErrorBoundary.jsx rename to frontend/src/components/common/ErrorBoundary.jsx diff --git a/frontend-temp/src/components/common/ErrorMessage.jsx b/frontend/src/components/common/ErrorMessage.jsx similarity index 100% rename from frontend-temp/src/components/common/ErrorMessage.jsx rename to frontend/src/components/common/ErrorMessage.jsx diff --git a/frontend/src/components/common/Lightbox.jsx b/frontend/src/components/common/Lightbox.jsx index 1ebe851..9d794a8 100644 --- a/frontend/src/components/common/Lightbox.jsx +++ b/frontend/src/components/common/Lightbox.jsx @@ -1,196 +1,290 @@ import { useState, useEffect, useCallback, memo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { X, ChevronLeft, ChevronRight } from 'lucide-react'; +import { X, ChevronLeft, ChevronRight, Download } from 'lucide-react'; +import LightboxIndicator from './LightboxIndicator'; -// 인디케이터 컴포넌트 - CSS transition 사용으로 GPU 가속 -const LightboxIndicator = memo(function LightboxIndicator({ count, currentIndex, goToIndex }) { - const translateX = -(currentIndex * 18) + 100 - 6; - - return ( -
- {/* 양옆 페이드 그라데이션 */} -
- {/* 슬라이딩 컨테이너 */} -
- {Array.from({ length: count }).map((_, i) => ( -
-
- ); -}); +/** + * 라이트박스 공통 컴포넌트 + * 이미지/비디오 갤러리를 전체 화면으로 표시 + * + * @param {string[]} images - 이미지/비디오 URL 배열 + * @param {Object[]} photos - 메타데이터 포함 사진 배열 (선택적) + * @param {string} photos[].title - 컨셉 이름 + * @param {string} photos[].members - 멤버 이름 (쉼표 구분) + * @param {Object[]} teasers - 티저 정보 배열 (비디오 여부 확인용) + * @param {string} teasers[].media_type - 'video' 또는 'image' + * @param {number} currentIndex - 현재 인덱스 + * @param {boolean} isOpen - 열림 상태 + * @param {function} onClose - 닫기 콜백 + * @param {function} onIndexChange - 인덱스 변경 콜백 + * @param {boolean} showCounter - 카운터 표시 여부 (기본: true) + * @param {boolean} showDownload - 다운로드 버튼 표시 여부 (기본: true) + */ +function Lightbox({ + images, + photos, + teasers, + currentIndex, + isOpen, + onClose, + onIndexChange, + showCounter = true, + showDownload = true, +}) { + const [imageLoaded, setImageLoaded] = useState(false); + const [slideDirection, setSlideDirection] = useState(0); -// 라이트박스 공통 컴포넌트 -function Lightbox({ images, currentIndex, isOpen, onClose, onIndexChange }) { - const [imageLoaded, setImageLoaded] = useState(false); - const [slideDirection, setSlideDirection] = useState(0); - - // 이전/다음 네비게이션 - const goToPrev = useCallback(() => { - if (images.length <= 1) return; - setImageLoaded(false); - setSlideDirection(-1); - onIndexChange((currentIndex - 1 + images.length) % images.length); - }, [images.length, currentIndex, onIndexChange]); + // 이전/다음 네비게이션 + const goToPrev = useCallback(() => { + if (images.length <= 1) return; + setImageLoaded(false); + setSlideDirection(-1); + onIndexChange((currentIndex - 1 + images.length) % images.length); + }, [images.length, currentIndex, onIndexChange]); - const goToNext = useCallback(() => { - if (images.length <= 1) return; - setImageLoaded(false); - setSlideDirection(1); - onIndexChange((currentIndex + 1) % images.length); - }, [images.length, currentIndex, onIndexChange]); + const goToNext = useCallback(() => { + if (images.length <= 1) return; + setImageLoaded(false); + setSlideDirection(1); + onIndexChange((currentIndex + 1) % images.length); + }, [images.length, currentIndex, onIndexChange]); - const goToIndex = useCallback((index) => { - if (index === currentIndex) return; - setImageLoaded(false); - setSlideDirection(index > currentIndex ? 1 : -1); - onIndexChange(index); - }, [currentIndex, onIndexChange]); + const goToIndex = useCallback( + (index) => { + if (index === currentIndex) return; + setImageLoaded(false); + setSlideDirection(index > currentIndex ? 1 : -1); + onIndexChange(index); + }, + [currentIndex, onIndexChange] + ); - // 라이트박스 열릴 때 body 스크롤 숨기기 - useEffect(() => { - if (isOpen) { - document.documentElement.style.overflow = 'hidden'; - document.body.style.overflow = 'hidden'; - } else { - document.documentElement.style.overflow = ''; - document.body.style.overflow = ''; - } - return () => { - document.documentElement.style.overflow = ''; - document.body.style.overflow = ''; - }; - }, [isOpen]); + // 이미지 다운로드 + const downloadImage = useCallback(async () => { + const imageUrl = images[currentIndex]; + if (!imageUrl) return; - // 키보드 이벤트 핸들러 - useEffect(() => { - if (!isOpen) return; + try { + const response = await fetch(imageUrl); + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `image_${currentIndex + 1}.jpg`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } catch (error) { + console.error('이미지 다운로드 실패:', error); + } + }, [images, currentIndex]); - const handleKeyDown = (e) => { - switch (e.key) { - case 'ArrowLeft': - goToPrev(); - break; - case 'ArrowRight': - goToNext(); - break; - case 'Escape': - onClose(); - break; - default: - break; - } - }; + // 라이트박스 열릴 때 body 스크롤 숨기기 + useEffect(() => { + if (isOpen) { + document.documentElement.style.overflow = 'hidden'; + document.body.style.overflow = 'hidden'; + } else { + document.documentElement.style.overflow = ''; + document.body.style.overflow = ''; + } + return () => { + document.documentElement.style.overflow = ''; + document.body.style.overflow = ''; + }; + }, [isOpen]); - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [isOpen, goToPrev, goToNext, onClose]); + // 키보드 이벤트 핸들러 + useEffect(() => { + if (!isOpen) return; - // 이미지가 바뀔 때 로딩 상태 리셋 - useEffect(() => { - setImageLoaded(false); - }, [currentIndex]); + const handleKeyDown = (e) => { + switch (e.key) { + case 'ArrowLeft': + goToPrev(); + break; + case 'ArrowRight': + goToNext(); + break; + case 'Escape': + onClose(); + break; + default: + break; + } + }; - return ( - - {isOpen && images.length > 0 && ( - - {/* 내부 컨테이너 */} -
- {/* 닫기 버튼 */} - + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isOpen, goToPrev, goToNext, onClose]); - {/* 이전 버튼 */} - {images.length > 1 && ( - - )} + // 이미지가 바뀔 때 로딩 상태 리셋 + useEffect(() => { + setImageLoaded(false); + }, [currentIndex]); - {/* 로딩 스피너 */} - {!imageLoaded && ( -
-
-
- )} + // 현재 사진의 메타데이터 + const currentPhoto = photos?.[currentIndex]; + const photoTitle = currentPhoto?.title; + const hasValidTitle = photoTitle && photoTitle.trim() && photoTitle !== 'Default'; + const photoMembers = currentPhoto?.members; + const hasMembers = photoMembers && String(photoMembers).trim(); - {/* 이미지 */} -
- e.stopPropagation()} - onLoad={() => setImageLoaded(true)} - initial={{ x: slideDirection * 100 }} - animate={{ x: 0 }} - transition={{ duration: 0.25, ease: 'easeOut' }} - /> -
- - {/* 다음 버튼 */} - {images.length > 1 && ( - - )} - - {/* 인디케이터 */} - {images.length > 1 && ( - - )} -
-
+ return ( + + {isOpen && images.length > 0 && ( + + {/* 내부 컨테이너 */} +
+ {/* 카운터 */} + {showCounter && images.length > 1 && ( +
+ {currentIndex + 1} / {images.length} +
)} - - ); + + {/* 상단 버튼들 */} +
+ {showDownload && ( + + )} + +
+ + {/* 이전 버튼 */} + {images.length > 1 && ( + + )} + + {/* 로딩 스피너 */} + {!imageLoaded && ( +
+
+
+ )} + + {/* 이미지/비디오 + 메타데이터 */} +
+ {teasers?.[currentIndex]?.media_type === 'video' ? ( + e.stopPropagation()} + onCanPlay={() => setImageLoaded(true)} + initial={{ x: slideDirection * 100 }} + animate={{ x: 0 }} + transition={{ duration: 0.25, ease: 'easeOut' }} + controls + autoPlay + /> + ) : ( + e.stopPropagation()} + onLoad={() => setImageLoaded(true)} + initial={{ x: slideDirection * 100 }} + animate={{ x: 0 }} + transition={{ duration: 0.25, ease: 'easeOut' }} + /> + )} + + {/* 컨셉/멤버 정보 */} + {imageLoaded && (hasValidTitle || hasMembers) && ( +
+ {hasValidTitle && ( + + {photoTitle} + + )} + {hasMembers && ( +
+ {String(photoMembers) + .split(',') + .map((member, idx) => ( + + {member.trim()} + + ))} +
+ )} +
+ )} +
+ + {/* 다음 버튼 */} + {images.length > 1 && ( + + )} + + {/* 인디케이터 */} + {images.length > 1 && ( + + )} +
+
+ )} +
+ ); } export default Lightbox; diff --git a/frontend/src/components/common/LightboxIndicator.jsx b/frontend/src/components/common/LightboxIndicator.jsx index b0f7ba1..ef025f4 100644 --- a/frontend/src/components/common/LightboxIndicator.jsx +++ b/frontend/src/components/common/LightboxIndicator.jsx @@ -5,39 +5,53 @@ import { memo } from 'react'; * 이미지 갤러리에서 현재 위치를 표시하는 슬라이딩 점 인디케이터 * CSS transition 사용으로 GPU 가속 */ -const LightboxIndicator = memo(function LightboxIndicator({ count, currentIndex, goToIndex, width = 200 }) { - const halfWidth = width / 2; - const translateX = -(currentIndex * 18) + halfWidth - 6; - - return ( -
- {/* 양옆 페이드 그라데이션 */} -
- {/* 슬라이딩 컨테이너 - CSS transition으로 GPU 가속 */} -
- {Array.from({ length: count }).map((_, i) => ( -
-
- ); +const LightboxIndicator = memo(function LightboxIndicator({ + count, + currentIndex, + goToIndex, + width = 200, +}) { + const halfWidth = width / 2; + const translateX = -(currentIndex * 18) + halfWidth - 6; + + return ( +
+ {/* 양옆 페이드 그라데이션 */} +
+ {/* 슬라이딩 컨테이너 - CSS transition으로 GPU 가속 */} +
+ {Array.from({ length: count }).map((_, i) => ( +
+
+ ); }); export default LightboxIndicator; diff --git a/frontend-temp/src/components/common/Loading.jsx b/frontend/src/components/common/Loading.jsx similarity index 100% rename from frontend-temp/src/components/common/Loading.jsx rename to frontend/src/components/common/Loading.jsx diff --git a/frontend-temp/src/components/common/MobileLightbox.jsx b/frontend/src/components/common/MobileLightbox.jsx similarity index 100% rename from frontend-temp/src/components/common/MobileLightbox.jsx rename to frontend/src/components/common/MobileLightbox.jsx diff --git a/frontend-temp/src/components/common/ScrollToTop.jsx b/frontend/src/components/common/ScrollToTop.jsx similarity index 100% rename from frontend-temp/src/components/common/ScrollToTop.jsx rename to frontend/src/components/common/ScrollToTop.jsx diff --git a/frontend-temp/src/components/common/Toast.jsx b/frontend/src/components/common/Toast.jsx similarity index 100% rename from frontend-temp/src/components/common/Toast.jsx rename to frontend/src/components/common/Toast.jsx diff --git a/frontend-temp/src/components/common/Tooltip.jsx b/frontend/src/components/common/Tooltip.jsx similarity index 100% rename from frontend-temp/src/components/common/Tooltip.jsx rename to frontend/src/components/common/Tooltip.jsx diff --git a/frontend-temp/src/components/common/index.js b/frontend/src/components/common/index.js similarity index 100% rename from frontend-temp/src/components/common/index.js rename to frontend/src/components/common/index.js diff --git a/frontend-temp/src/components/index.js b/frontend/src/components/index.js similarity index 100% rename from frontend-temp/src/components/index.js rename to frontend/src/components/index.js diff --git a/frontend/src/components/mobile/Layout.jsx b/frontend/src/components/mobile/Layout.jsx deleted file mode 100644 index 08bf3a0..0000000 --- a/frontend/src/components/mobile/Layout.jsx +++ /dev/null @@ -1,94 +0,0 @@ -import { NavLink, useLocation } from 'react-router-dom'; -import { Home, Users, Disc3, Calendar } from 'lucide-react'; -import { useEffect } from 'react'; -import '../../mobile.css'; - -// 모바일 헤더 컴포넌트 -function MobileHeader({ title, noShadow = false }) { - return ( -
-
- {title ? ( - {title} - ) : ( - - fromis_9 - - )} -
-
- ); -} - -// 모바일 하단 네비게이션 -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 ( - - ); -} - -// 모바일 레이아웃 컴포넌트 -// pageTitle: 헤더에 표시할 제목 (없으면 fromis_9) -// hideHeader: true면 헤더 숨김 (일정 페이지처럼 자체 헤더가 있는 경우) -// useCustomLayout: true면 자체 레이아웃 사용 (mobile-layout-container를 페이지에서 관리) -function MobileLayout({ children, pageTitle, hideHeader = false, useCustomLayout = false, noShadow = false }) { - // 모바일 레이아웃 활성화 (body 스크롤 방지) - useEffect(() => { - document.documentElement.classList.add('mobile-layout'); - return () => { - document.documentElement.classList.remove('mobile-layout'); - }; - }, []); - - // 자체 레이아웃 사용 시 (Schedule 페이지 등) - if (useCustomLayout) { - return ( -
- {children} - -
- ); - } - - return ( -
- {!hideHeader && } -
{children}
- -
- ); -} - -export default MobileLayout; - diff --git a/frontend-temp/src/components/mobile/index.js b/frontend/src/components/mobile/index.js similarity index 100% rename from frontend-temp/src/components/mobile/index.js rename to frontend/src/components/mobile/index.js diff --git a/frontend-temp/src/components/mobile/layout/BottomNav.jsx b/frontend/src/components/mobile/layout/BottomNav.jsx similarity index 100% rename from frontend-temp/src/components/mobile/layout/BottomNav.jsx rename to frontend/src/components/mobile/layout/BottomNav.jsx diff --git a/frontend-temp/src/components/mobile/layout/Header.jsx b/frontend/src/components/mobile/layout/Header.jsx similarity index 100% rename from frontend-temp/src/components/mobile/layout/Header.jsx rename to frontend/src/components/mobile/layout/Header.jsx diff --git a/frontend-temp/src/components/mobile/layout/Layout.jsx b/frontend/src/components/mobile/layout/Layout.jsx similarity index 100% rename from frontend-temp/src/components/mobile/layout/Layout.jsx rename to frontend/src/components/mobile/layout/Layout.jsx diff --git a/frontend-temp/src/components/mobile/layout/index.js b/frontend/src/components/mobile/layout/index.js similarity index 100% rename from frontend-temp/src/components/mobile/layout/index.js rename to frontend/src/components/mobile/layout/index.js diff --git a/frontend-temp/src/components/mobile/schedule/BirthdayCard.jsx b/frontend/src/components/mobile/schedule/BirthdayCard.jsx similarity index 100% rename from frontend-temp/src/components/mobile/schedule/BirthdayCard.jsx rename to frontend/src/components/mobile/schedule/BirthdayCard.jsx diff --git a/frontend-temp/src/components/mobile/schedule/Calendar.jsx b/frontend/src/components/mobile/schedule/Calendar.jsx similarity index 100% rename from frontend-temp/src/components/mobile/schedule/Calendar.jsx rename to frontend/src/components/mobile/schedule/Calendar.jsx diff --git a/frontend-temp/src/components/mobile/schedule/ScheduleCard.jsx b/frontend/src/components/mobile/schedule/ScheduleCard.jsx similarity index 100% rename from frontend-temp/src/components/mobile/schedule/ScheduleCard.jsx rename to frontend/src/components/mobile/schedule/ScheduleCard.jsx diff --git a/frontend-temp/src/components/mobile/schedule/ScheduleListCard.jsx b/frontend/src/components/mobile/schedule/ScheduleListCard.jsx similarity index 100% rename from frontend-temp/src/components/mobile/schedule/ScheduleListCard.jsx rename to frontend/src/components/mobile/schedule/ScheduleListCard.jsx diff --git a/frontend-temp/src/components/mobile/schedule/ScheduleSearchCard.jsx b/frontend/src/components/mobile/schedule/ScheduleSearchCard.jsx similarity index 100% rename from frontend-temp/src/components/mobile/schedule/ScheduleSearchCard.jsx rename to frontend/src/components/mobile/schedule/ScheduleSearchCard.jsx diff --git a/frontend-temp/src/components/mobile/schedule/index.js b/frontend/src/components/mobile/schedule/index.js similarity index 100% rename from frontend-temp/src/components/mobile/schedule/index.js rename to frontend/src/components/mobile/schedule/index.js diff --git a/frontend/src/components/pc/Footer.jsx b/frontend/src/components/pc/Footer.jsx deleted file mode 100644 index c9dc0fb..0000000 --- a/frontend/src/components/pc/Footer.jsx +++ /dev/null @@ -1,16 +0,0 @@ -function Footer() { - const currentYear = new Date().getFullYear(); - - return ( -
-
- {/* 저작권 */} -
-

© {currentYear} fromis_9 Fan Site. This is an unofficial fan-made website.

-
-
-
- ); -} - -export default Footer; diff --git a/frontend/src/components/pc/Header.jsx b/frontend/src/components/pc/Header.jsx deleted file mode 100644 index 163b8ce..0000000 --- a/frontend/src/components/pc/Header.jsx +++ /dev/null @@ -1,79 +0,0 @@ -import { NavLink } from 'react-router-dom'; -import { Instagram, Youtube } from 'lucide-react'; -import { socialLinks } from '../../data/dummy'; - -// X (Twitter) 아이콘 컴포넌트 -const XIcon = ({ size = 20 }) => ( - - - -); - -function Header() { - const navItems = [ - { path: '/', label: '홈' }, - { path: '/members', label: '멤버' }, - { path: '/album', label: '앨범' }, - { path: '/schedule', label: '일정' }, - ]; - - return ( -
-
-
- {/* 로고 */} - - fromis_9 - - - {/* 네비게이션 */} - - - {/* SNS 링크 */} - -
-
-
- ); -} - -export default Header; diff --git a/frontend/src/components/pc/Layout.jsx b/frontend/src/components/pc/Layout.jsx deleted file mode 100644 index f388727..0000000 --- a/frontend/src/components/pc/Layout.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useLocation } from 'react-router-dom'; -import Header from './Header'; -import Footer from './Footer'; -import '../../pc.css'; - -function Layout({ children }) { - const location = useLocation(); - - // Footer 숨김 페이지 (화면 고정 레이아웃) - const hideFooterPages = ['/schedule', '/members', '/album']; - const hideFooter = hideFooterPages.some(path => - location.pathname === path || location.pathname.startsWith(path + '/') - ); - - // 일정 페이지에서는 스크롤바도 숨김 (내부에서 자체 스크롤 처리) - const isSchedulePage = location.pathname === '/schedule'; - - return ( -
-
-
-
- {children} -
- {!hideFooter &&
} -
-
- ); -} - -export default Layout; diff --git a/frontend-temp/src/components/pc/admin/album/BulkEditPanel.jsx b/frontend/src/components/pc/admin/album/BulkEditPanel.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/album/BulkEditPanel.jsx rename to frontend/src/components/pc/admin/album/BulkEditPanel.jsx diff --git a/frontend-temp/src/components/pc/admin/album/PendingFileItem.jsx b/frontend/src/components/pc/admin/album/PendingFileItem.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/album/PendingFileItem.jsx rename to frontend/src/components/pc/admin/album/PendingFileItem.jsx diff --git a/frontend-temp/src/components/pc/admin/album/PhotoGrid.jsx b/frontend/src/components/pc/admin/album/PhotoGrid.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/album/PhotoGrid.jsx rename to frontend/src/components/pc/admin/album/PhotoGrid.jsx diff --git a/frontend-temp/src/components/pc/admin/album/PhotoPreviewModal.jsx b/frontend/src/components/pc/admin/album/PhotoPreviewModal.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/album/PhotoPreviewModal.jsx rename to frontend/src/components/pc/admin/album/PhotoPreviewModal.jsx diff --git a/frontend-temp/src/components/pc/admin/album/TrackItem.jsx b/frontend/src/components/pc/admin/album/TrackItem.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/album/TrackItem.jsx rename to frontend/src/components/pc/admin/album/TrackItem.jsx diff --git a/frontend-temp/src/components/pc/admin/album/index.js b/frontend/src/components/pc/admin/album/index.js similarity index 100% rename from frontend-temp/src/components/pc/admin/album/index.js rename to frontend/src/components/pc/admin/album/index.js diff --git a/frontend-temp/src/components/pc/admin/bot/BotCard.jsx b/frontend/src/components/pc/admin/bot/BotCard.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/bot/BotCard.jsx rename to frontend/src/components/pc/admin/bot/BotCard.jsx diff --git a/frontend-temp/src/components/pc/admin/bot/index.js b/frontend/src/components/pc/admin/bot/index.js similarity index 100% rename from frontend-temp/src/components/pc/admin/bot/index.js rename to frontend/src/components/pc/admin/bot/index.js diff --git a/frontend-temp/src/components/pc/admin/common/ConfirmDialog.jsx b/frontend/src/components/pc/admin/common/ConfirmDialog.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/common/ConfirmDialog.jsx rename to frontend/src/components/pc/admin/common/ConfirmDialog.jsx diff --git a/frontend-temp/src/components/pc/admin/common/CustomSelect.jsx b/frontend/src/components/pc/admin/common/CustomSelect.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/common/CustomSelect.jsx rename to frontend/src/components/pc/admin/common/CustomSelect.jsx diff --git a/frontend-temp/src/components/pc/admin/common/DatePicker.jsx b/frontend/src/components/pc/admin/common/DatePicker.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/common/DatePicker.jsx rename to frontend/src/components/pc/admin/common/DatePicker.jsx diff --git a/frontend-temp/src/components/pc/admin/common/NumberPicker.jsx b/frontend/src/components/pc/admin/common/NumberPicker.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/common/NumberPicker.jsx rename to frontend/src/components/pc/admin/common/NumberPicker.jsx diff --git a/frontend-temp/src/components/pc/admin/common/TimePicker.jsx b/frontend/src/components/pc/admin/common/TimePicker.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/common/TimePicker.jsx rename to frontend/src/components/pc/admin/common/TimePicker.jsx diff --git a/frontend-temp/src/components/pc/admin/common/index.js b/frontend/src/components/pc/admin/common/index.js similarity index 100% rename from frontend-temp/src/components/pc/admin/common/index.js rename to frontend/src/components/pc/admin/common/index.js diff --git a/frontend-temp/src/components/pc/admin/index.js b/frontend/src/components/pc/admin/index.js similarity index 100% rename from frontend-temp/src/components/pc/admin/index.js rename to frontend/src/components/pc/admin/index.js diff --git a/frontend-temp/src/components/pc/admin/layout/Header.jsx b/frontend/src/components/pc/admin/layout/Header.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/layout/Header.jsx rename to frontend/src/components/pc/admin/layout/Header.jsx diff --git a/frontend-temp/src/components/pc/admin/layout/Layout.jsx b/frontend/src/components/pc/admin/layout/Layout.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/layout/Layout.jsx rename to frontend/src/components/pc/admin/layout/Layout.jsx diff --git a/frontend-temp/src/components/pc/admin/layout/index.js b/frontend/src/components/pc/admin/layout/index.js similarity index 100% rename from frontend-temp/src/components/pc/admin/layout/index.js rename to frontend/src/components/pc/admin/layout/index.js diff --git a/frontend-temp/src/components/pc/admin/schedule/AdminScheduleCard.jsx b/frontend/src/components/pc/admin/schedule/AdminScheduleCard.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/schedule/AdminScheduleCard.jsx rename to frontend/src/components/pc/admin/schedule/AdminScheduleCard.jsx diff --git a/frontend-temp/src/components/pc/admin/schedule/CategoryFormModal.jsx b/frontend/src/components/pc/admin/schedule/CategoryFormModal.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/schedule/CategoryFormModal.jsx rename to frontend/src/components/pc/admin/schedule/CategoryFormModal.jsx diff --git a/frontend-temp/src/components/pc/admin/schedule/CategorySelector.jsx b/frontend/src/components/pc/admin/schedule/CategorySelector.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/schedule/CategorySelector.jsx rename to frontend/src/components/pc/admin/schedule/CategorySelector.jsx diff --git a/frontend-temp/src/components/pc/admin/schedule/ImageUploader.jsx b/frontend/src/components/pc/admin/schedule/ImageUploader.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/schedule/ImageUploader.jsx rename to frontend/src/components/pc/admin/schedule/ImageUploader.jsx diff --git a/frontend-temp/src/components/pc/admin/schedule/LocationSearchDialog.jsx b/frontend/src/components/pc/admin/schedule/LocationSearchDialog.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/schedule/LocationSearchDialog.jsx rename to frontend/src/components/pc/admin/schedule/LocationSearchDialog.jsx diff --git a/frontend-temp/src/components/pc/admin/schedule/MemberSelector.jsx b/frontend/src/components/pc/admin/schedule/MemberSelector.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/schedule/MemberSelector.jsx rename to frontend/src/components/pc/admin/schedule/MemberSelector.jsx diff --git a/frontend-temp/src/components/pc/admin/schedule/ScheduleItem.jsx b/frontend/src/components/pc/admin/schedule/ScheduleItem.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/schedule/ScheduleItem.jsx rename to frontend/src/components/pc/admin/schedule/ScheduleItem.jsx diff --git a/frontend-temp/src/components/pc/admin/schedule/WordItem.jsx b/frontend/src/components/pc/admin/schedule/WordItem.jsx similarity index 100% rename from frontend-temp/src/components/pc/admin/schedule/WordItem.jsx rename to frontend/src/components/pc/admin/schedule/WordItem.jsx diff --git a/frontend-temp/src/components/pc/admin/schedule/index.js b/frontend/src/components/pc/admin/schedule/index.js similarity index 100% rename from frontend-temp/src/components/pc/admin/schedule/index.js rename to frontend/src/components/pc/admin/schedule/index.js diff --git a/frontend-temp/src/components/pc/public/index.js b/frontend/src/components/pc/public/index.js similarity index 100% rename from frontend-temp/src/components/pc/public/index.js rename to frontend/src/components/pc/public/index.js diff --git a/frontend-temp/src/components/pc/public/layout/Footer.jsx b/frontend/src/components/pc/public/layout/Footer.jsx similarity index 100% rename from frontend-temp/src/components/pc/public/layout/Footer.jsx rename to frontend/src/components/pc/public/layout/Footer.jsx diff --git a/frontend-temp/src/components/pc/public/layout/Header.jsx b/frontend/src/components/pc/public/layout/Header.jsx similarity index 100% rename from frontend-temp/src/components/pc/public/layout/Header.jsx rename to frontend/src/components/pc/public/layout/Header.jsx diff --git a/frontend-temp/src/components/pc/public/layout/Layout.jsx b/frontend/src/components/pc/public/layout/Layout.jsx similarity index 100% rename from frontend-temp/src/components/pc/public/layout/Layout.jsx rename to frontend/src/components/pc/public/layout/Layout.jsx diff --git a/frontend-temp/src/components/pc/public/layout/index.js b/frontend/src/components/pc/public/layout/index.js similarity index 100% rename from frontend-temp/src/components/pc/public/layout/index.js rename to frontend/src/components/pc/public/layout/index.js diff --git a/frontend-temp/src/components/pc/public/schedule/BirthdayCard.jsx b/frontend/src/components/pc/public/schedule/BirthdayCard.jsx similarity index 100% rename from frontend-temp/src/components/pc/public/schedule/BirthdayCard.jsx rename to frontend/src/components/pc/public/schedule/BirthdayCard.jsx diff --git a/frontend-temp/src/components/pc/public/schedule/Calendar.jsx b/frontend/src/components/pc/public/schedule/Calendar.jsx similarity index 100% rename from frontend-temp/src/components/pc/public/schedule/Calendar.jsx rename to frontend/src/components/pc/public/schedule/Calendar.jsx diff --git a/frontend-temp/src/components/pc/public/schedule/CategoryFilter.jsx b/frontend/src/components/pc/public/schedule/CategoryFilter.jsx similarity index 100% rename from frontend-temp/src/components/pc/public/schedule/CategoryFilter.jsx rename to frontend/src/components/pc/public/schedule/CategoryFilter.jsx diff --git a/frontend-temp/src/components/pc/public/schedule/ScheduleCard.jsx b/frontend/src/components/pc/public/schedule/ScheduleCard.jsx similarity index 100% rename from frontend-temp/src/components/pc/public/schedule/ScheduleCard.jsx rename to frontend/src/components/pc/public/schedule/ScheduleCard.jsx diff --git a/frontend-temp/src/components/pc/public/schedule/index.js b/frontend/src/components/pc/public/schedule/index.js similarity index 100% rename from frontend-temp/src/components/pc/public/schedule/index.js rename to frontend/src/components/pc/public/schedule/index.js diff --git a/frontend-temp/src/constants/index.js b/frontend/src/constants/index.js similarity index 100% rename from frontend-temp/src/constants/index.js rename to frontend/src/constants/index.js diff --git a/frontend/src/data/dummy.js b/frontend/src/data/dummy.js deleted file mode 100644 index 17b5ad9..0000000 --- a/frontend/src/data/dummy.js +++ /dev/null @@ -1,125 +0,0 @@ -// 더미 멤버 데이터 -export const members = [ - { - id: 1, - name: "송하영", - birthDate: "1997.03.25", - position: "리더, 메인보컬, 메인댄서", - imageUrl: - "https://i.namu.wiki/i/M9XsM0yLkPOEwQJBAnVn2rMqo4kXRfv-AkvWNvgAX1m2IUWX1IECPIZ1sqH6tDMAmpqYQefaP1ydJz7hUZ6Zxw.webp", - instagram: "https://www.instagram.com/hayoung_0325/", - }, - { - id: 2, - name: "박지원", - birthDate: "1998.10.20", - position: "메인보컬", - imageUrl: - "https://i.namu.wiki/i/M9XsM0yLkPOEwQJBAnVn2rMqo4kXRfv-AkvWNvgAX1lE7WfQKMuTmQCCJcXhPLv65cQCTMHWFhzhjJZIEbAKbQ.webp", - instagram: "https://www.instagram.com/jiwon_1020/", - }, - { - id: 3, - name: "이채영", - birthDate: "2000.06.14", - position: "래퍼", - imageUrl: - "https://i.namu.wiki/i/M9XsM0yLkPOEwQJBAnVn2rMqo4kXRfv-AkvWNvgAX1lqvGHXpz1iuLHTnVmAEdbgahPaQEZA7VOxLLdB-pK_hQ.webp", - instagram: "https://www.instagram.com/chaeyoung_0614/", - }, - { - id: 4, - name: "이나경", - birthDate: "2000.05.01", - position: "리드보컬", - imageUrl: - "https://i.namu.wiki/i/M9XsM0yLkPOEwQJBAnVn2rMqo4kXRfv-AkvWNvgAX1kGZHhKV9nNDi9mSwBCKKl5QGcHNT8_EZxLKSv6QmtVmg.webp", - instagram: "https://www.instagram.com/nakyung_0501/", - }, - { - id: 5, - name: "백지헌", - birthDate: "2003.04.17", - position: "막내", - imageUrl: - "https://i.namu.wiki/i/M9XsM0yLkPOEwQJBAnVn2rMqo4kXRfv-AkvWNvgAX1n2pqk4sWJWC_FGvQpEmLxSJC1tuhIwq8cWnqt-HMv_gw.webp", - instagram: "https://www.instagram.com/jiheon_0417/", - }, -]; - -// 더미 앨범 데이터 -export const albums = [ - { - id: 1, - title: "Unlock My World", - albumType: "정규", - releaseDate: "2023.06.05", - titleTrack: "Unlock My World", - coverUrl: - "https://i.namu.wiki/i/KDu-5K16r75g3eCGiYiOqRBNJLKfmimubzXR1bSY2C39dxGbAG1nZqKYn_uqxjShKmN0HRjqJjM1DQfbHwXhsQ.webp", - }, - { - id: 2, - title: "from our Memento Box", - albumType: "미니", - releaseDate: "2022.06.27", - titleTrack: "Stay This Way", - coverUrl: - "https://i.namu.wiki/i/RJ-v7VHvnbV2-M7S1YBn35LLQ1PV4rrLM-QE5qE0-C_p5xYx6vb0fB0O5oXPZsMo0kOWPLyE_45Cpt1kkQa5xg.webp", - }, - { - id: 3, - title: "Midnight Guest", - albumType: "미니", - releaseDate: "2022.01.17", - titleTrack: "DM", - coverUrl: - "https://i.namu.wiki/i/yPAOeX6xBp_Bxs0wNLX-4IWLvqoTJoiZPAcDFdFHiUXTM3YLKtfSbEOqr3ofAZGdPNnBxNBKXQ0bZKBJjWG1hg.webp", - }, - { - id: 4, - title: "Talk & Talk", - albumType: "싱글", - releaseDate: "2024.09.05", - titleTrack: "Supersonic", - coverUrl: - "https://i.namu.wiki/i/jYC5xE6SyC-eDlQWMHrx2OIKHKkJGkgj-_zCeMIgw8CbvU-d6c5kGE4Zy3cwkiZ7kpRG5Hmo5WfCN0uqUWpyiw.webp", - }, -]; - -// 더미 스케줄 데이터 -export const schedules = [ - { - id: 1, - date: "2025-01-02", - time: "19:00", - title: "유튜브 [스프 : 스튜디오 프로미스나인 30화]", - platform: "YouTube", - members: ["전원"], - }, - { - id: 2, - date: "2025-01-05", - time: "18:00", - title: "SBS 인기가요 출연", - platform: "SBS", - members: ["전원"], - }, - { - id: 3, - date: "2025-01-10", - time: "20:00", - title: '팬미팅 "FLOVER DAY"', - platform: "올림픽공원", - members: ["전원"], - }, -]; - -// 공식 SNS 링크 -export const socialLinks = { - youtube: "https://www.youtube.com/@fromis9_official", - instagram: "https://www.instagram.com/officialfromis_9", - twitter: "https://twitter.com/realfromis_9", - tiktok: "https://www.tiktok.com/@officialfromis_9", - fancafe: "https://cafe.daum.net/officialfromis9", -}; diff --git a/frontend-temp/src/hooks/common/index.js b/frontend/src/hooks/common/index.js similarity index 100% rename from frontend-temp/src/hooks/common/index.js rename to frontend/src/hooks/common/index.js diff --git a/frontend-temp/src/hooks/common/useAlbumData.js b/frontend/src/hooks/common/useAlbumData.js similarity index 100% rename from frontend-temp/src/hooks/common/useAlbumData.js rename to frontend/src/hooks/common/useAlbumData.js diff --git a/frontend-temp/src/hooks/common/useMemberData.js b/frontend/src/hooks/common/useMemberData.js similarity index 100% rename from frontend-temp/src/hooks/common/useMemberData.js rename to frontend/src/hooks/common/useMemberData.js diff --git a/frontend-temp/src/hooks/common/useScheduleData.js b/frontend/src/hooks/common/useScheduleData.js similarity index 100% rename from frontend-temp/src/hooks/common/useScheduleData.js rename to frontend/src/hooks/common/useScheduleData.js diff --git a/frontend-temp/src/hooks/common/useToast.js b/frontend/src/hooks/common/useToast.js similarity index 100% rename from frontend-temp/src/hooks/common/useToast.js rename to frontend/src/hooks/common/useToast.js diff --git a/frontend-temp/src/hooks/index.js b/frontend/src/hooks/index.js similarity index 100% rename from frontend-temp/src/hooks/index.js rename to frontend/src/hooks/index.js diff --git a/frontend-temp/src/hooks/pc/admin/index.js b/frontend/src/hooks/pc/admin/index.js similarity index 100% rename from frontend-temp/src/hooks/pc/admin/index.js rename to frontend/src/hooks/pc/admin/index.js diff --git a/frontend-temp/src/hooks/pc/admin/useAdminAuth.js b/frontend/src/hooks/pc/admin/useAdminAuth.js similarity index 100% rename from frontend-temp/src/hooks/pc/admin/useAdminAuth.js rename to frontend/src/hooks/pc/admin/useAdminAuth.js diff --git a/frontend-temp/src/hooks/pc/admin/useScheduleSearch.js b/frontend/src/hooks/pc/admin/useScheduleSearch.js similarity index 100% rename from frontend-temp/src/hooks/pc/admin/useScheduleSearch.js rename to frontend/src/hooks/pc/admin/useScheduleSearch.js diff --git a/frontend/src/hooks/useAdminAuth.js b/frontend/src/hooks/useAdminAuth.js deleted file mode 100644 index 451b0a5..0000000 --- a/frontend/src/hooks/useAdminAuth.js +++ /dev/null @@ -1,39 +0,0 @@ -import { useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useQuery } from '@tanstack/react-query'; -import { fetchAdminApi } from '../api'; - -/** - * 어드민 인증 상태 관리 훅 - * - 토큰 유효성 검증 - * - 미인증 시 로그인 페이지로 리다이렉트 - */ -function useAdminAuth() { - const navigate = useNavigate(); - const token = localStorage.getItem('adminToken'); - - const { data, isLoading, isError } = useQuery({ - queryKey: ['admin', 'auth'], - queryFn: () => fetchAdminApi('/api/auth/verify'), - enabled: !!token, - retry: false, - staleTime: 1000 * 60 * 5, // 5분간 캐시 - }); - - // 토큰 없거나 검증 실패 시 로그인 페이지로 이동 - useEffect(() => { - if (!token || isError) { - localStorage.removeItem('adminToken'); - localStorage.removeItem('adminUser'); - navigate('/admin'); - } - }, [token, isError, navigate]); - - return { - user: data?.user || null, - isLoading: !token ? false : isLoading, - isAuthenticated: !!data?.valid, - }; -} - -export default useAdminAuth; diff --git a/frontend/src/hooks/useToast.js b/frontend/src/hooks/useToast.js deleted file mode 100644 index de39f46..0000000 --- a/frontend/src/hooks/useToast.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Toast 상태 관리 커스텀 훅 - * 자동 숨김 타이머 및 상태 관리를 제공 - */ -import { useState, useEffect, useCallback } from "react"; - -function useToast(duration = 3000) { - const [toast, setToast] = useState(null); - - // Toast 자동 숨김 - useEffect(() => { - if (toast) { - const timer = setTimeout(() => setToast(null), duration); - return () => clearTimeout(timer); - } - }, [toast, duration]); - - // Toast 표시 함수 - const showToast = useCallback((message, type = "info") => { - setToast({ message, type }); - }, []); - - // 편의 메서드 - const showSuccess = useCallback( - (message) => showToast(message, "success"), - [showToast] - ); - const showError = useCallback( - (message) => showToast(message, "error"), - [showToast] - ); - const showWarning = useCallback( - (message) => showToast(message, "warning"), - [showToast] - ); - const showInfo = useCallback( - (message) => showToast(message, "info"), - [showToast] - ); - - // Toast 숨김 함수 - const hideToast = useCallback(() => setToast(null), []); - - return { - toast, - setToast, - showToast, - showSuccess, - showError, - showWarning, - showInfo, - hideToast, - }; -} - -export default useToast; diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index b5521d3..975ee56 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,23 +1,23 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import App from './App'; -import './index.css'; +import React from "react"; +import ReactDOM from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import App from "./App"; +import "./index.css"; -// React Query 클라이언트 생성 const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 1000 * 60 * 5, // 5분간 캐시 유지 - refetchOnWindowFocus: false, - }, + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, // 5분 + retry: 1, + refetchOnWindowFocus: false, }, + }, }); -ReactDOM.createRoot(document.getElementById('root')).render( - - - - - +ReactDOM.createRoot(document.getElementById("root")).render( + + + + + ); diff --git a/frontend-temp/src/pages/mobile/.gitkeep b/frontend/src/pages/mobile/.gitkeep similarity index 100% rename from frontend-temp/src/pages/mobile/.gitkeep rename to frontend/src/pages/mobile/.gitkeep diff --git a/frontend-temp/src/pages/mobile/album/Album.jsx b/frontend/src/pages/mobile/album/Album.jsx similarity index 100% rename from frontend-temp/src/pages/mobile/album/Album.jsx rename to frontend/src/pages/mobile/album/Album.jsx diff --git a/frontend-temp/src/pages/mobile/album/AlbumDetail.jsx b/frontend/src/pages/mobile/album/AlbumDetail.jsx similarity index 100% rename from frontend-temp/src/pages/mobile/album/AlbumDetail.jsx rename to frontend/src/pages/mobile/album/AlbumDetail.jsx diff --git a/frontend-temp/src/pages/mobile/album/AlbumGallery.jsx b/frontend/src/pages/mobile/album/AlbumGallery.jsx similarity index 100% rename from frontend-temp/src/pages/mobile/album/AlbumGallery.jsx rename to frontend/src/pages/mobile/album/AlbumGallery.jsx diff --git a/frontend-temp/src/pages/mobile/album/TrackDetail.jsx b/frontend/src/pages/mobile/album/TrackDetail.jsx similarity index 100% rename from frontend-temp/src/pages/mobile/album/TrackDetail.jsx rename to frontend/src/pages/mobile/album/TrackDetail.jsx diff --git a/frontend-temp/src/pages/mobile/common/NotFound.jsx b/frontend/src/pages/mobile/common/NotFound.jsx similarity index 100% rename from frontend-temp/src/pages/mobile/common/NotFound.jsx rename to frontend/src/pages/mobile/common/NotFound.jsx diff --git a/frontend-temp/src/pages/mobile/home/Home.jsx b/frontend/src/pages/mobile/home/Home.jsx similarity index 100% rename from frontend-temp/src/pages/mobile/home/Home.jsx rename to frontend/src/pages/mobile/home/Home.jsx diff --git a/frontend-temp/src/pages/mobile/members/Members.jsx b/frontend/src/pages/mobile/members/Members.jsx similarity index 100% rename from frontend-temp/src/pages/mobile/members/Members.jsx rename to frontend/src/pages/mobile/members/Members.jsx diff --git a/frontend/src/pages/mobile/public/Album.jsx b/frontend/src/pages/mobile/public/Album.jsx deleted file mode 100644 index 1fca0a7..0000000 --- a/frontend/src/pages/mobile/public/Album.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import { motion } from 'framer-motion'; -import { useQuery } from '@tanstack/react-query'; -import { useNavigate } from 'react-router-dom'; -import { getAlbums } from '../../../api/public/albums'; - -// 모바일 앨범 목록 페이지 -function MobileAlbum() { - const navigate = useNavigate(); - - // useQuery로 앨범 데이터 로드 - const { data: albums = [], isLoading: loading } = useQuery({ - queryKey: ['albums'], - queryFn: getAlbums, - }); - - - if (loading) { - return ( -
-
-
- ); - } - - return ( -
-
- {albums.map((album, index) => ( - navigate(`/album/${album.folder_name}`)} - className="bg-white rounded-2xl overflow-hidden shadow-md" - > -
- {album.cover_thumb_url && ( - {album.title} - )} -
-
-

{album.title}

-

- {album.album_type_short} · {album.release_date?.slice(0, 4)} -

-
-
- ))} -
-
- ); -} - -export default MobileAlbum; diff --git a/frontend/src/pages/mobile/public/AlbumDetail.jsx b/frontend/src/pages/mobile/public/AlbumDetail.jsx deleted file mode 100644 index aeb716e..0000000 --- a/frontend/src/pages/mobile/public/AlbumDetail.jsx +++ /dev/null @@ -1,465 +0,0 @@ -import { motion, AnimatePresence } from 'framer-motion'; -import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; -import { useQuery } from '@tanstack/react-query'; -import { Play, Calendar, Music2, Clock, X, Download, ChevronDown, ChevronUp, FileText, ChevronRight } from 'lucide-react'; -import { Swiper, SwiperSlide } from 'swiper/react'; -import { Virtual } from 'swiper/modules'; -import 'swiper/css'; -import { getAlbumByName } from '../../../api/public/albums'; -import { formatDate } from '../../../utils/date'; -import LightboxIndicator from '../../../components/common/LightboxIndicator'; - -function MobileAlbumDetail() { - const { name } = useParams(); - const navigate = useNavigate(); - const [lightbox, setLightbox] = useState({ open: false, images: [], index: 0, showNav: true }); - const [showAllTracks, setShowAllTracks] = useState(false); - const [showDescriptionModal, setShowDescriptionModal] = useState(false); - const swiperRef = useRef(null); - - // useQuery로 앨범 데이터 로드 - const { data: album, isLoading: loading } = useQuery({ - queryKey: ['album', name], - queryFn: () => getAlbumByName(name), - enabled: !!name, - }); - - // 라이트박스 열기 - 히스토리 추가 - const openLightbox = useCallback((images, index, options = {}) => { - setLightbox({ open: true, images, index, showNav: options.showNav !== false, teasers: options.teasers }); - window.history.pushState({ lightbox: true }, ''); - }, []); - - const closeLightbox = useCallback(() => { - setLightbox(prev => ({ ...prev, open: false })); - }, []); - - // 앨범 소개 열기 - 히스토리 추가 - const openDescriptionModal = useCallback(() => { - setShowDescriptionModal(true); - window.history.pushState({ description: true }, ''); - }, []); - - const closeDescriptionModal = useCallback(() => { - setShowDescriptionModal(false); - }, []); - - // 뒤로가기 처리 - useEffect(() => { - const handlePopState = () => { - if (showDescriptionModal) { - setShowDescriptionModal(false); - } else if (lightbox.open) { - setLightbox(prev => ({ ...prev, open: false })); - } - }; - - window.addEventListener('popstate', handlePopState); - return () => window.removeEventListener('popstate', handlePopState); - }, [showDescriptionModal, lightbox.open]); - - // 이미지 다운로드 - const downloadImage = useCallback(async () => { - const imageUrl = lightbox.images[lightbox.index]; - if (!imageUrl) return; - - try { - const response = await fetch(imageUrl); - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `fromis9_photo_${String(lightbox.index + 1).padStart(2, '0')}.webp`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - window.URL.revokeObjectURL(url); - } catch (error) { - console.error('다운로드 오류:', error); - } - }, [lightbox.images, lightbox.index]); - - // 라이트박스 body 스크롤 방지 - useEffect(() => { - if (lightbox.open || showDescriptionModal) { - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = ''; - } - return () => { document.body.style.overflow = ''; }; - }, [lightbox.open, showDescriptionModal]); - - // 총 재생 시간 계산 - const getTotalDuration = () => { - if (!album?.tracks) return ''; - let totalSeconds = 0; - album.tracks.forEach(track => { - if (track.duration) { - const parts = track.duration.split(':'); - totalSeconds += parseInt(parts[0]) * 60 + parseInt(parts[1]); - } - }); - const mins = Math.floor(totalSeconds / 60); - const secs = totalSeconds % 60; - return `${mins}:${String(secs).padStart(2, '0')}`; - }; - - if (loading) { - return ( -
-
-
- ); - } - - if (!album) { - return ( -
-

앨범을 찾을 수 없습니다

-
- ); - } - - // 모든 컨셉 포토를 하나의 배열로 - const allPhotos = album.conceptPhotos ? Object.values(album.conceptPhotos).flat() : []; - const displayTracks = showAllTracks ? album.tracks : album.tracks?.slice(0, 5); - - return ( - <> -
- {/* 앨범 히어로 섹션 - 커버 이미지 배경 */} -
- {/* 배경 블러 이미지 */} -
- -
-
- - {/* 콘텐츠 */} -
-
- {/* 앨범 커버 */} - openLightbox( - [album.cover_original_url || album.cover_medium_url], - 0, - { showNav: false } - )} - > - {album.title} - - - {/* 앨범 정보 */} - - - {album.album_type} - -

{album.title}

- - {/* 메타 정보 */} -
-
- - {formatDate(album.release_date, 'YYYY.MM.DD')} -
-
- - {album.tracks?.length || 0}곡 -
-
- - {getTotalDuration()} -
-
- - {/* 앨범 소개 버튼 */} - {album.description && ( - - )} -
-
-
-
- - {/* 티저 이미지 */} - {album.teasers && album.teasers.length > 0 && ( - -

티저 이미지

-
- {album.teasers.map((teaser, index) => ( -
openLightbox( - album.teasers.map(t => - t.media_type === 'video' ? (t.video_url || t.original_url) : t.original_url - ), - index, - { teasers: album.teasers, showNav: true } - )} - className="w-24 h-24 flex-shrink-0 bg-gray-100 rounded-2xl overflow-hidden relative shadow-sm" - > - {`Teaser - {teaser.media_type === 'video' && ( -
-
- -
-
- )} -
- ))} -
-
- )} - - {/* 수록곡 */} - {album.tracks && album.tracks.length > 0 && ( - -

수록곡

-
- {displayTracks?.map((track) => ( -
navigate(`/album/${encodeURIComponent(album.title)}/track/${encodeURIComponent(track.title)}`)} - className="flex items-center gap-3 py-2.5 px-3 rounded-xl hover:bg-gray-50 active:bg-gray-100 transition-colors cursor-pointer" - > - - {String(track.track_number).padStart(2, '0')} - -
-

- {track.title} -

- {track.is_title_track === 1 && ( - - TITLE - - )} -
- - {track.duration || '-'} - -
- ))} -
- {/* 더보기/접기 버튼 */} - {album.tracks.length > 5 && ( - - )} -
- )} - - {/* 컨셉 포토 */} - {allPhotos.length > 0 && ( - -

컨셉 포토

-
- {allPhotos.slice(0, 6).map((photo, idx) => ( -
openLightbox( - [photo.original_url], - 0, - { showNav: false } - )} - className="aspect-square bg-gray-100 rounded-xl overflow-hidden shadow-sm" - > - {`컨셉 -
- ))} -
- {/* 전체보기 버튼 - 모바일 스타일 */} - -
- )} -
- - {/* 앨범 소개 다이얼로그 */} - - {showDescriptionModal && album?.description && ( - window.history.back()} - > - { - if (info.offset.y > 100 || info.velocity.y > 300) { - window.history.back(); - } - }} - className="bg-white rounded-t-3xl w-full max-h-[80vh] overflow-hidden" - onClick={(e) => e.stopPropagation()} - > - {/* 드래그 핸들 */} -
-
-
- {/* 헤더 */} -
-

앨범 소개

- -
- {/* 내용 */} -
-

- {album.description} -

-
- - - )} - - - {/* 라이트박스 - Swiper ViewPager 스타일 */} - - {lightbox.open && ( - - {/* 상단 헤더 - 3등분 */} -
-
- -
- {lightbox.showNav && lightbox.images.length > 1 && ( - - {lightbox.index + 1} / {lightbox.images.length} - - )} -
- -
-
- - {/* Swiper */} - { swiperRef.current = swiper; }} - onSlideChange={(swiper) => setLightbox(prev => ({ ...prev, index: swiper.activeIndex }))} - className="w-full h-full" - spaceBetween={0} - slidesPerView={1} - resistance={true} - resistanceRatio={0.5} - > - {lightbox.images.map((url, index) => ( - -
- {lightbox.teasers?.[index]?.media_type === 'video' ? ( -
-
- ))} -
- - {/* 모바일용 인디케이터 */} - {lightbox.showNav && lightbox.images.length > 1 && ( - swiperRef.current?.slideTo(i)} - width={120} - /> - )} -
- )} -
- - ); -} - -export default MobileAlbumDetail; diff --git a/frontend/src/pages/mobile/public/AlbumGallery.jsx b/frontend/src/pages/mobile/public/AlbumGallery.jsx deleted file mode 100644 index 8d19b7c..0000000 --- a/frontend/src/pages/mobile/public/AlbumGallery.jsx +++ /dev/null @@ -1,351 +0,0 @@ -import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; -import { useQuery } from '@tanstack/react-query'; -import { X, Download, ChevronRight, Info, Users, Tag } from 'lucide-react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { Swiper, SwiperSlide } from 'swiper/react'; -import { Virtual } from 'swiper/modules'; -import 'swiper/css'; -import { getAlbumByName } from '../../../api/public/albums'; -import LightboxIndicator from '../../../components/common/LightboxIndicator'; - -// 모바일 앨범 갤러리 페이지 -function MobileAlbumGallery() { - const { name } = useParams(); - const navigate = useNavigate(); - const [selectedIndex, setSelectedIndex] = useState(null); - const [showInfo, setShowInfo] = useState(false); - const swiperRef = useRef(null); - - // useQuery로 앨범 데이터 로드 - const { data: album, isLoading: loading } = useQuery({ - queryKey: ['album', name], - queryFn: () => getAlbumByName(name), - enabled: !!name, - }); - - // 앨범 데이터에서 사진 목록 추출 - const photos = useMemo(() => { - if (!album?.conceptPhotos) return []; - const allPhotos = []; - Object.entries(album.conceptPhotos).forEach(([concept, conceptPhotos]) => { - conceptPhotos.forEach(p => allPhotos.push({ - ...p, - concept: concept !== 'Default' ? concept : null - })); - }); - return allPhotos; - }, [album]); - - // 라이트박스 열기 - 히스토리 추가 - const openLightbox = useCallback((index) => { - setSelectedIndex(index); - window.history.pushState({ lightbox: true }, ''); - }, []); - - // 라이트박스 닫기 - const closeLightbox = useCallback(() => { - setSelectedIndex(null); - setShowInfo(false); - }, []); - - // 정보 시트 열기 - 히스토리 추가 - const openInfo = useCallback(() => { - setShowInfo(true); - window.history.pushState({ infoSheet: true }, ''); - }, []); - - // 정보 시트 닫기 - const closeInfo = useCallback(() => { - setShowInfo(false); - }, []); - - // 뒤로가기 처리 - useEffect(() => { - const handlePopState = (e) => { - if (showInfo) { - setShowInfo(false); - } else if (selectedIndex !== null) { - setSelectedIndex(null); - } - }; - - window.addEventListener('popstate', handlePopState); - return () => window.removeEventListener('popstate', handlePopState); - }, [showInfo, selectedIndex]); - - // 이미지 다운로드 - const downloadImage = useCallback(async () => { - const photo = photos[selectedIndex]; - if (!photo) return; - - try { - const response = await fetch(photo.original_url); - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `fromis9_${album?.title || 'photo'}_${String(selectedIndex + 1).padStart(2, '0')}.webp`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - window.URL.revokeObjectURL(url); - } catch (error) { - console.error('다운로드 오류:', error); - } - }, [photos, selectedIndex, album?.title]); - - // 바디 스크롤 방지 - useEffect(() => { - if (selectedIndex !== null) { - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = ''; - } - return () => { document.body.style.overflow = ''; }; - }, [selectedIndex]); - - // 사진을 2열로 균등 분배 (높이 기반) - const distributePhotos = () => { - const leftColumn = []; - const rightColumn = []; - let leftHeight = 0; - let rightHeight = 0; - - photos.forEach((photo, index) => { - // 이미지 높이 비율 계산 (width가 동일하다고 가정하면 height/width 비율 사용) - const aspectRatio = photo.height && photo.width ? photo.height / photo.width : 1; - - // 더 짧은 열에 사진 추가 - if (leftHeight <= rightHeight) { - leftColumn.push({ ...photo, originalIndex: index }); - leftHeight += aspectRatio; - } else { - rightColumn.push({ ...photo, originalIndex: index }); - rightHeight += aspectRatio; - } - }); - - return { leftColumn, rightColumn }; - }; - - const { leftColumn, rightColumn } = distributePhotos(); - - // 현재 사진 정보 - const currentPhoto = selectedIndex !== null ? photos[selectedIndex] : null; - const hasInfo = currentPhoto?.concept || currentPhoto?.members; - - // 정보 시트 드래그 핸들러 - const handleInfoDragEnd = (_, info) => { - if (info.offset.y > 100 || info.velocity.y > 300) { - window.history.back(); - } - }; - - if (loading) { - return ( -
-
-
- ); - } - - return ( - <> -
- {/* 앨범 헤더 카드 */} -
navigate(-1)} - > - {album?.cover_thumb_url && ( - {album.title} - )} -
-

컨셉 포토

-

{album?.title}

-

{photos.length}장의 사진

-
- -
- - {/* 2열 그리드 */} -
-
- {leftColumn.map((photo) => ( - openLightbox(photo.originalIndex)} - className="cursor-pointer overflow-hidden rounded-xl bg-gray-100" - > - - - ))} -
-
- {rightColumn.map((photo) => ( - openLightbox(photo.originalIndex)} - className="cursor-pointer overflow-hidden rounded-xl bg-gray-100" - > - - - ))} -
-
-
- - {/* 풀스크린 라이트박스 */} - - {selectedIndex !== null && ( - - {/* 상단 헤더 - 3등분 */} -
-
- -
- - {selectedIndex + 1} / {photos.length} - -
- {hasInfo && ( - - )} - -
-
- - {/* Swiper */} - { swiperRef.current = swiper; }} - onSlideChange={(swiper) => setSelectedIndex(swiper.activeIndex)} - className="w-full h-full" - spaceBetween={0} - slidesPerView={1} - resistance={true} - resistanceRatio={0.5} - > - {photos.map((photo, index) => ( - -
- -
-
- ))} -
- - {/* 모바일용 인디케이터 (좁은 width) */} - swiperRef.current?.slideTo(i)} - width={120} - /> - - {/* 정보 바텀시트 */} - - {showInfo && hasInfo && ( - window.history.back()} - > - e.stopPropagation()} - > - {/* 드래그 핸들 */} -
-
-
- - {/* 정보 내용 */} -
-

사진 정보

- - {currentPhoto?.members && ( -
-
- -
-
-

멤버

-

{currentPhoto.members}

-
-
- )} - - {currentPhoto?.concept && ( -
-
- -
-
-

컨셉

-

{currentPhoto.concept}

-
-
- )} -
- - - )} - - - )} - - - ); -} - -export default MobileAlbumGallery; diff --git a/frontend/src/pages/mobile/public/Home.jsx b/frontend/src/pages/mobile/public/Home.jsx deleted file mode 100644 index d4454f4..0000000 --- a/frontend/src/pages/mobile/public/Home.jsx +++ /dev/null @@ -1,267 +0,0 @@ -import { motion } from 'framer-motion'; -import { ChevronRight, Clock, Tag } from 'lucide-react'; -import { useQuery } from '@tanstack/react-query'; -import { useNavigate } from 'react-router-dom'; -import { getTodayKST } from '../../../utils/date'; -import { getMembers } from '../../../api/public/members'; -import { getAlbums } from '../../../api/public/albums'; -import { getUpcomingSchedules } from '../../../api/public/schedules'; - -// 모바일 홈 페이지 -function MobileHome() { - const navigate = useNavigate(); - - // useQuery로 멤버 데이터 로드 (활동 중인 멤버만) - const { data: members = [] } = useQuery({ - queryKey: ['members'], - queryFn: getMembers, - select: (data) => data.filter(m => !m.is_former), - }); - - // useQuery로 앨범 로드 (최신 2개) - const { data: albums = [] } = useQuery({ - queryKey: ['albums'], - queryFn: getAlbums, - select: (data) => data.slice(0, 2), - }); - - // useQuery로 다가오는 일정 로드 - const { data: schedules = [] } = useQuery({ - queryKey: ['upcomingSchedules', 3], - queryFn: () => getUpcomingSchedules(3), - }); - - - return ( -
- {/* 히어로 섹션 */} - -
- -

fromis_9

-

프로미스나인

-

- 인사드리겠습니다. 둘, 셋!
- 이제는 약속해 소중히 간직해,
- 당신의 아이돌로 성장하겠습니다! -

-
- {/* 장식 */} -
-
- - - {/* 멤버 섹션 */} - -
-

멤버

- -
-
- {members.map((member, index) => ( - -
- {member.image_url && ( - {member.name} - )} -
-

{member.name}

-
- ))} -
-
- - {/* 앨범 섹션 */} - -
-

앨범

- -
-
- {albums.map((album, index) => ( - navigate(`/album/${album.folder_name}`)} - className="bg-white rounded-xl overflow-hidden shadow-md" - initial={{ opacity: 0, y: 20 }} - animate={{ opacity: 1, y: 0 }} - transition={{ delay: 0.6 + index * 0.1, duration: 0.3 }} - whileTap={{ scale: 0.98 }} - > -
- {album.cover_thumb_url && ( - {album.title} - )} -
-
-

{album.title}

-

{album.release_date?.slice(0, 4)}

-
-
- ))} -
-
- - {/* 일정 섹션 */} - -
-

다가오는 일정

- -
- {schedules.length > 0 ? ( -
- {schedules.map((schedule, index) => { - const scheduleDate = new Date(schedule.date); - const today = new Date(); - const currentYear = today.getFullYear(); - const currentMonth = today.getMonth(); - - const scheduleYear = scheduleDate.getFullYear(); - const scheduleMonth = scheduleDate.getMonth(); - const isCurrentYear = scheduleYear === currentYear; - const isCurrentMonth = isCurrentYear && scheduleMonth === currentMonth; - - // 멤버 처리 - const memberList = schedule.member_names - ? schedule.member_names.split(',').map(n => n.trim()).filter(Boolean) - : schedule.members?.map(m => m.name) || []; - - return ( - navigate('/schedule')} - > - {/* 날짜 영역 */} -
- {/* 현재 년도가 아니면 년.월 표시 */} - {!isCurrentYear && ( - - {scheduleYear}.{scheduleMonth + 1} - - )} - {/* 현재 달이 아니면 월 표시 (현재 년도일 때) */} - {isCurrentYear && !isCurrentMonth && ( - - {scheduleMonth + 1}월 - - )} - - {scheduleDate.getDate()} - - - {['일', '월', '화', '수', '목', '금', '토'][scheduleDate.getDay()]} - -
- - {/* 세로 구분선 */} -
- - {/* 내용 영역 */} -
-

- {schedule.title} -

- {/* 시간 + 카테고리 (PC버전 스타일) */} -
- {schedule.time && ( - - - {schedule.time.slice(0, 5)} - - )} - {schedule.category_name && ( - - - {schedule.category_name} - - )} -
- {/* 멤버 */} - {memberList.length > 0 && ( -
- {memberList.map((name, i) => ( - - {name.trim()} - - ))} -
- )} -
- - ); - })} -
- ) : ( -
-

다가오는 일정이 없습니다

-
- )} - -
- ); -} - -export default MobileHome; diff --git a/frontend/src/pages/mobile/public/Members.jsx b/frontend/src/pages/mobile/public/Members.jsx deleted file mode 100644 index 5d46f17..0000000 --- a/frontend/src/pages/mobile/public/Members.jsx +++ /dev/null @@ -1,223 +0,0 @@ -import { motion } from 'framer-motion'; -import { useState, useMemo, useRef, useEffect } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { Instagram, Calendar } from 'lucide-react'; -import { Swiper, SwiperSlide } from 'swiper/react'; -import 'swiper/css'; -import { getMembers } from '../../../api/public/members'; - -// 모바일 멤버 페이지 - 카드 스와이프 스타일 -function MobileMembers() { - const [currentIndex, setCurrentIndex] = useState(0); - const swiperRef = useRef(null); - const indicatorRef = useRef(null); - - // useQuery로 멤버 데이터 로드 - const { data: allMembers = [] } = useQuery({ - queryKey: ['members'], - queryFn: getMembers, - }); - - // useMemo로 현재/전 멤버 정렬 (현재 멤버 먼저, 전 멤버 나중) - const members = useMemo(() => { - return [...allMembers].sort((a, b) => { - if (a.is_former !== b.is_former) { - return a.is_former ? 1 : -1; - } - return 0; - }); - }, [allMembers]); - - // 나이 계산 - const calculateAge = (birthDate) => { - if (!birthDate) return null; - const birth = new Date(birthDate); - const today = new Date(); - let age = today.getFullYear() - birth.getFullYear(); - const monthDiff = today.getMonth() - birth.getMonth(); - if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) { - age--; - } - return age; - }; - - // 인디케이터 자동 스크롤 - useEffect(() => { - if (indicatorRef.current && members.length > 0) { - const container = indicatorRef.current; - const itemWidth = 64; // 52px 썸네일 + 12px 간격 - const containerWidth = container.offsetWidth; - const paddingLeft = 16; // px-4 - const targetScroll = paddingLeft + (currentIndex * itemWidth) + 26 - (containerWidth / 2); - - container.scrollTo({ - left: Math.max(0, targetScroll), - behavior: 'smooth' - }); - } - }, [currentIndex, members.length]); - - // 인디케이터 클릭 핸들러 - const handleIndicatorClick = (index) => { - if (swiperRef.current) { - swiperRef.current.slideTo(index); - } - }; - - if (members.length === 0) { - return ( -
-

멤버 정보가 없습니다

-
- ); - } - - return ( -
- {/* 상단 썸네일 인디케이터 */} - -
- {members.map((member, index) => { - const isSelected = index === currentIndex; - const isFormer = member.is_former; - - return ( - - ); - })} -
-
- - {/* 메인 카드 영역 */} - - { swiperRef.current = swiper; }} - onSlideChange={(swiper) => setCurrentIndex(swiper.activeIndex)} - slidesPerView={1.12} - centeredSlides={true} - spaceBetween={0} - className="h-full !overflow-visible [&>.swiper-wrapper]:!overflow-visible" - style={{ padding: '8px 0' }} - > - {members.map((member, index) => { - const isFormer = member.is_former; - const age = calculateAge(member.birth_date); - - return ( - - {({ isActive }) => ( -
- {/* 배경 이미지 */} - {member.image_url ? ( - {member.name} - ) : ( -
- )} - - {/* 하단 그라데이션 오버레이 */} -
- - {/* 전 멤버 라벨 */} - {isFormer && ( -
- 전 멤버 -
- )} - - {/* 멤버 정보 */} -
- {/* 이름 */} -

- {member.name} -

- - {/* 생일 정보 */} - {member.birth_date && ( -
- - - {member.birth_date?.slice(0, 10).replaceAll('-', '.')} - - {age && ( - - {age}세 - - )} -
- )} - - {/* 인스타그램 버튼 */} - {!isFormer && member.instagram && ( - - - Instagram - - )} -
-
- )} - - ); - })} - - -
- ); -} - -export default MobileMembers; diff --git a/frontend/src/pages/mobile/public/NotFound.jsx b/frontend/src/pages/mobile/public/NotFound.jsx deleted file mode 100644 index aa64f50..0000000 --- a/frontend/src/pages/mobile/public/NotFound.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import { motion } from "framer-motion"; -import { Link } from "react-router-dom"; -import { Home, ArrowLeft } from "lucide-react"; - -function MobileNotFound() { - return ( -
-
- {/* 404 숫자 */} - -

- 404 -

-
- - {/* 메시지 */} - -

- 페이지를 찾을 수 없습니다 -

-

- 요청하신 페이지가 존재하지 않거나 -
- 이동되었을 수 있습니다. -

-
- - {/* 장식 요소 */} - - {[...Array(5)].map((_, i) => ( - - ))} - - - {/* 버튼들 */} - - - - 홈으로 가기 - - - -
-
- ); -} - -export default MobileNotFound; diff --git a/frontend/src/pages/mobile/public/Schedule.jsx b/frontend/src/pages/mobile/public/Schedule.jsx deleted file mode 100644 index 548507b..0000000 --- a/frontend/src/pages/mobile/public/Schedule.jsx +++ /dev/null @@ -1,1530 +0,0 @@ -import { useState, useEffect, useMemo, useRef, useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { motion, AnimatePresence } from 'framer-motion'; -import { Clock, Tag, Link2, ChevronLeft, ChevronRight, ChevronDown, Search, X, Calendar } from 'lucide-react'; -import { useQuery, useInfiniteQuery } from '@tanstack/react-query'; -import { useInView } from 'react-intersection-observer'; -import { useVirtualizer } from '@tanstack/react-virtual'; -import confetti from 'canvas-confetti'; -import { getTodayKST } from '../../../utils/date'; -import { getSchedules, searchSchedules } from '../../../api/public/schedules'; -import useScheduleStore from '../../../stores/useScheduleStore'; - -// 멤버 리스트 추출 (검색 결과와 일반 데이터 모두 처리) -const getMemberList = (schedule) => { - // member_names 문자열이 있으면 사용 - if (schedule.member_names) { - return schedule.member_names.split(',').map(n => n.trim()).filter(Boolean); - } - // members 배열이 있으면 - if (Array.isArray(schedule.members) && schedule.members.length > 0) { - // 문자열 배열인 경우 (검색 결과) - if (typeof schedule.members[0] === 'string') { - return schedule.members.filter(Boolean); - } - // 객체 배열인 경우 (일반 데이터) - return schedule.members.map(m => m.name).filter(Boolean); - } - return []; -}; - -// 폭죽 애니메이션 함수 -const 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: 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: 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: colors, - shapes: ['circle', 'square'], - startVelocity: 45, - }); -}; - -// HTML 엔티티 디코딩 함수 -const decodeHtmlEntities = (text) => { - if (!text) return ''; - const textarea = document.createElement('textarea'); - textarea.innerHTML = text; - return textarea.value; -}; - -// 모바일 생일 카드 컴포넌트 -function MobileBirthdayCard({ schedule, onClick, delay = 0 }) { - return ( - -
- {/* 배경 장식 */} -
-
-
-
🎉
-
- -
- {/* 멤버 사진 */} - {schedule.member_image && ( -
-
- {schedule.member_names} -
-
- )} - - {/* 내용 */} -
- 🎂 -

- {decodeHtmlEntities(schedule.title)} -

-
-
-
- - ); -} - -// 모바일 일정 페이지 -function MobileSchedule() { - const navigate = useNavigate(); - - // zustand store에서 상태 가져오기 - const { - selectedDate: storedSelectedDate, - setSelectedDate: setStoredSelectedDate, - } = useScheduleStore(); - - // 선택된 날짜 (store에 없으면 오늘 날짜) - const selectedDate = storedSelectedDate || new Date(); - const setSelectedDate = (date) => setStoredSelectedDate(date); - - const [isSearchMode, setIsSearchMode] = useState(false); - const [searchInput, setSearchInput] = useState(''); // 입력값 - const [searchTerm, setSearchTerm] = useState(''); // 실제 검색어 - const [showCalendar, setShowCalendar] = useState(false); - const [calendarViewDate, setCalendarViewDate] = useState(() => new Date(selectedDate)); // 달력 뷰 날짜 - const [calendarShowYearMonth, setCalendarShowYearMonth] = useState(false); // 달력 년월 선택 모드 - const contentRef = useRef(null); // 스크롤 초기화용 - const searchContainerRef = useRef(null); // 검색 컨테이너 (외부 클릭 감지용) - const searchInputRef = useRef(null); // 검색 입력 필드 (키패드 닫기용) - - // 검색 추천 관련 상태 - const [showSuggestions, setShowSuggestions] = useState(false); - const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1); - const [originalSearchQuery, setOriginalSearchQuery] = useState(''); // 필터링용 원본 쿼리 - const [suggestions, setSuggestions] = useState([]); // 추천 검색어 목록 - const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false); - const [lastSearchTerm, setLastSearchTerm] = useState(''); // 마지막 검색어 (복원용) - const [showSuggestionsScreen, setShowSuggestionsScreen] = useState(false); // 추천 검색어 화면 표시 여부 - - // 검색 모드 진입 함수 (history 상태 추가) - const enterSearchMode = () => { - setIsSearchMode(true); - window.history.pushState({ searchMode: true }, ''); - }; - - // 검색 모드 종료 함수 - const exitSearchMode = () => { - setIsSearchMode(false); - setSearchInput(''); - setOriginalSearchQuery(''); - setSearchTerm(''); - setLastSearchTerm(''); - setShowSuggestions(false); - setShowSuggestionsScreen(false); - setSelectedSuggestionIndex(-1); - }; - - // 추천 검색어 화면 숨기고 검색 결과로 돌아가기 - const hideSuggestionsScreen = () => { - setShowSuggestionsScreen(false); - setSearchInput(lastSearchTerm); // 검색어 복원 - setOriginalSearchQuery(lastSearchTerm); - }; - - // 뒤로가기 버튼 처리 - useEffect(() => { - const handlePopState = (e) => { - if (isSearchMode) { - // 추천 검색어 화면이고 검색 결과가 있으면 → 검색 결과로 돌아가기 - if (showSuggestionsScreen && searchTerm) { - hideSuggestionsScreen(); - window.history.pushState({ searchMode: true }, ''); - } else { - // 그 외에는 검색 모드 종료 - exitSearchMode(); - } - } - }; - - window.addEventListener('popstate', handlePopState); - return () => window.removeEventListener('popstate', handlePopState); - }, [isSearchMode, showSuggestionsScreen, searchTerm, lastSearchTerm]); - - // 달력 월 변경 함수 - const changeCalendarMonth = (delta) => { - const newDate = new Date(calendarViewDate); - newDate.setMonth(newDate.getMonth() + delta); - setCalendarViewDate(newDate); - }; - - const SEARCH_LIMIT = 20; // 페이지당 20개 - const ESTIMATED_ITEM_HEIGHT = 100; // 아이템 추정 높이 (동적 측정) - const scrollContainerRef = useRef(null); // 가상 스크롤 컨테이너 - const { ref: loadMoreRef, inView } = useInView({ threshold: 0, rootMargin: '100px' }); - - // 검색 무한 스크롤 - const { - data: searchData, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - isLoading: searchLoading, - } = useInfiniteQuery({ - queryKey: ['mobileScheduleSearch', searchTerm], - queryFn: async ({ pageParam = 0 }) => { - return searchSchedules(searchTerm, { offset: pageParam, limit: SEARCH_LIMIT }); - }, - getNextPageParam: (lastPage) => { - if (lastPage.hasMore) { - return lastPage.offset + lastPage.schedules.length; - } - return undefined; - }, - enabled: !!searchTerm && isSearchMode, - }); - - const searchResults = useMemo(() => { - if (!searchData?.pages) return []; - return searchData.pages.flatMap(page => page.schedules); - }, [searchData]); - - // 가상 스크롤 설정 (검색 모드에서만 활성화, 동적 높이 지원) - const virtualizer = useVirtualizer({ - count: isSearchMode && searchTerm ? searchResults.length : 0, - getScrollElement: () => scrollContainerRef.current, - estimateSize: () => ESTIMATED_ITEM_HEIGHT, - overscan: 5, // 버퍼 아이템 수 - }); - - // 검색어 변경 시 스크롤 위치 초기화 - useEffect(() => { - if (searchTerm && !showSuggestionsScreen) { - // 약간의 지연 후 스크롤 초기화 (렌더링 완료 후) - requestAnimationFrame(() => { - virtualizer.scrollToOffset(0); - if (scrollContainerRef.current) { - scrollContainerRef.current.scrollTop = 0; - } - }); - } - }, [searchTerm, showSuggestionsScreen]); - - useEffect(() => { - if (inView && hasNextPage && !isFetchingNextPage && isSearchMode && searchTerm) { - fetchNextPage(); - } - }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage, isSearchMode, searchTerm]); - - // 일정 및 카테고리 로드 (useQuery) - const viewYear = selectedDate.getFullYear(); - const viewMonth = selectedDate.getMonth() + 1; - - // 월별 일정 데이터 로드 - const { data: schedules = [], isLoading: loading } = useQuery({ - queryKey: ['schedules', viewYear, viewMonth], - queryFn: () => getSchedules(viewYear, viewMonth), - }); - - // 카테고리는 일정 데이터에서 추출 - const categories = useMemo(() => { - const categoryMap = new Map(); - schedules.forEach(s => { - const catId = s.category?.id || s.category_id; - if (catId && !categoryMap.has(catId)) { - categoryMap.set(catId, { - id: catId, - name: s.category?.name || s.category_name, - color: s.category?.color || s.category_color, - }); - } - }); - return Array.from(categoryMap.values()); - }, [schedules]); - - // 달력 표시용 일정 데이터 (calendarViewDate 기준) - const calendarYear = calendarViewDate.getFullYear(); - const calendarMonth = calendarViewDate.getMonth() + 1; - const isSameMonth = viewYear === calendarYear && viewMonth === calendarMonth; - - const { data: calendarSchedules = [] } = useQuery({ - queryKey: ['schedules', calendarYear, calendarMonth], - queryFn: () => getSchedules(calendarYear, calendarMonth), - enabled: !isSameMonth, // 같은 월이면 중복 조회 안함 - }); - - // 생일 폭죽 효과 (하루에 한 번만) - useEffect(() => { - if (loading || schedules.length === 0) return; - - const today = getTodayKST(); - const confettiKey = `birthday-confetti-${today}`; - - // 이미 오늘 폭죽을 봤으면 스킵 - if (localStorage.getItem(confettiKey)) return; - - const hasBirthdayToday = schedules.some(s => { - if (!s.is_birthday) return false; - const scheduleDate = s.date ? s.date.split('T')[0] : ''; - return scheduleDate === today; - }); - - if (hasBirthdayToday) { - // 약간의 딜레이 후 폭죽 발사 (페이지 렌더링 완료 후) - const timer = setTimeout(() => { - fireBirthdayConfetti(); - localStorage.setItem(confettiKey, 'true'); - }, 500); - return () => clearTimeout(timer); - } - }, [schedules, loading]); - - // 2017년 1월 이전으로 이동 불가 - const canGoPrevMonth = !(selectedDate.getFullYear() === 2017 && selectedDate.getMonth() === 0); - - // 월 변경 - const changeMonth = (delta) => { - if (delta < 0 && !canGoPrevMonth) return; - - const newDate = new Date(selectedDate); - newDate.setMonth(newDate.getMonth() + delta); - - // 이번 달이면 오늘 날짜, 다른 달이면 1일 선택 - const today = new Date(); - if (newDate.getFullYear() === today.getFullYear() && newDate.getMonth() === today.getMonth()) { - newDate.setDate(today.getDate()); - } else { - newDate.setDate(1); - } - - setSelectedDate(newDate); - setCalendarViewDate(newDate); - }; - - // 날짜 변경 시 스크롤 맨 위로 초기화 - useEffect(() => { - if (contentRef.current) { - contentRef.current.scrollTop = 0; - } - }, [selectedDate]); - - - // 캘린더가 열릴 때 배경 스크롤 방지 - useEffect(() => { - const preventScroll = (e) => e.preventDefault(); - - if (showCalendar) { - document.addEventListener('touchmove', preventScroll, { passive: false }); - } else { - document.removeEventListener('touchmove', preventScroll); - } - return () => { - document.removeEventListener('touchmove', preventScroll); - }; - }, [showCalendar]); - - // 검색 추천 드롭다운 외부 클릭 감지 - useEffect(() => { - const handleClickOutside = (event) => { - if (searchContainerRef.current && !searchContainerRef.current.contains(event.target)) { - setShowSuggestions(false); - setSelectedSuggestionIndex(-1); - } - }; - - if (showSuggestions) { - document.addEventListener('mousedown', handleClickOutside); - document.addEventListener('touchstart', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - document.removeEventListener('touchstart', handleClickOutside); - }; - }, [showSuggestions]); - - // 검색어 자동완성 API 호출 (debounce 적용) - useEffect(() => { - // 검색어가 비어있으면 초기화 - if (!originalSearchQuery || originalSearchQuery.trim().length === 0) { - setSuggestions([]); - return; - } - - // debounce: 200ms 후에 API 호출 - const timeoutId = setTimeout(async () => { - setIsLoadingSuggestions(true); - try { - const response = await fetch(`/api/schedules/suggestions?q=${encodeURIComponent(originalSearchQuery)}&limit=10`); - if (response.ok) { - const data = await response.json(); - setSuggestions(data.suggestions || []); - } - } catch (error) { - console.error('추천 검색어 API 오류:', error); - setSuggestions([]); - } finally { - setIsLoadingSuggestions(false); - } - }, 200); - - return () => clearTimeout(timeoutId); - }, [originalSearchQuery]); - - // 카테고리 색상 - const getCategoryColor = (categoryId) => { - const category = categories.find(c => c.id === categoryId); - return category?.color || '#6b7280'; - }; - - // 날짜별 일정 그룹화 (생일 우선) - const groupedSchedules = useMemo(() => { - const groups = {}; - schedules.forEach(schedule => { - const date = schedule.date; - if (!groups[date]) groups[date] = []; - groups[date].push(schedule); - }); - // 각 날짜 그룹 내에서 생일 우선 정렬 - Object.values(groups).forEach(group => { - group.sort((a, b) => { - const aIsBirthday = a.is_birthday || String(a.id).startsWith('birthday-'); - const bIsBirthday = b.is_birthday || String(b.id).startsWith('birthday-'); - if (aIsBirthday && !bIsBirthday) return -1; - if (!aIsBirthday && bIsBirthday) return 1; - return 0; - }); - }); - return Object.entries(groups).sort((a, b) => a[0].localeCompare(b[0])); - }, [schedules]); - - // 해당 달의 모든 날짜 배열 - const daysInMonth = useMemo(() => { - const year = selectedDate.getFullYear(); - const month = selectedDate.getMonth(); - const lastDay = new Date(year, month + 1, 0).getDate(); - const days = []; - for (let d = 1; d <= lastDay; d++) { - days.push(new Date(year, month, d)); - } - return days; - }, [selectedDate]); - - // 선택된 날짜의 일정 (생일 우선) - const selectedDateSchedules = useMemo(() => { - // KST 기준 날짜 문자열 생성 - const year = selectedDate.getFullYear(); - const month = String(selectedDate.getMonth() + 1).padStart(2, '0'); - const day = String(selectedDate.getDate()).padStart(2, '0'); - const dateStr = `${year}-${month}-${day}`; - // API 응답의 date는 ISO 형식이므로 T 이전 부분만 비교 - return schedules - .filter(s => s.date.split('T')[0] === dateStr) - .sort((a, b) => { - const aIsBirthday = a.is_birthday || String(a.id).startsWith('birthday-'); - const bIsBirthday = b.is_birthday || String(b.id).startsWith('birthday-'); - if (aIsBirthday && !bIsBirthday) return -1; - if (!aIsBirthday && bIsBirthday) return 1; - return 0; - }); - }, [schedules, selectedDate]); - - // 요일 이름 - const getDayName = (date) => { - return ['일', '월', '화', '수', '목', '금', '토'][date.getDay()]; - }; - - // 오늘 여부 - const isToday = (date) => { - const today = new Date(); - return date.getDate() === today.getDate() && - date.getMonth() === today.getMonth() && - date.getFullYear() === today.getFullYear(); - }; - - // 선택된 날짜 여부 - const isSelected = (date) => { - return date.getDate() === selectedDate.getDate() && - date.getMonth() === selectedDate.getMonth() && - date.getFullYear() === selectedDate.getFullYear(); - }; - - // 날짜 선택 컨테이너 ref - const dateScrollRef = useRef(null); - - // 선택된 날짜로 자동 스크롤 - useEffect(() => { - // 검색 모드가 아닐 때만 스크롤 조정 - if (!isSearchMode && dateScrollRef.current) { - const selectedDay = selectedDate.getDate(); - const buttons = dateScrollRef.current.querySelectorAll('button'); - if (buttons[selectedDay - 1]) { - // 약간의 지연 후 스크롤 (DOM 렌더링 완료 후) - setTimeout(() => { - buttons[selectedDay - 1].scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); - }, 50); - } - } - }, [selectedDate, isSearchMode]); - - return ( - <> - {/* 툴바 (헤더 + 날짜 선택기) */} -
- {isSearchMode ? ( -
-
- - { - setSearchInput(e.target.value); - setOriginalSearchQuery(e.target.value); - setShowSuggestions(true); - setShowSuggestionsScreen(true); - setSelectedSuggestionIndex(-1); - }} - onFocus={() => { - setShowSuggestions(true); - setShowSuggestionsScreen(true); - }} - onKeyDown={(e) => { - if (e.key === 'ArrowDown') { - e.preventDefault(); - const newIndex = selectedSuggestionIndex < suggestions.length - 1 - ? selectedSuggestionIndex + 1 - : 0; - setSelectedSuggestionIndex(newIndex); - if (suggestions[newIndex]) { - setSearchInput(suggestions[newIndex]); - } - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - const newIndex = selectedSuggestionIndex > 0 - ? selectedSuggestionIndex - 1 - : suggestions.length - 1; - setSelectedSuggestionIndex(newIndex); - if (suggestions[newIndex]) { - setSearchInput(suggestions[newIndex]); - } - } else if (e.key === 'Enter') { - e.preventDefault(); - const term = selectedSuggestionIndex >= 0 && suggestions[selectedSuggestionIndex] - ? suggestions[selectedSuggestionIndex] - : searchInput.trim(); - if (term) { - setSearchInput(term); - setSearchTerm(term); - setLastSearchTerm(term); // 검색어 저장 - setShowSuggestionsScreen(false); - } - setShowSuggestions(false); - setSelectedSuggestionIndex(-1); - // 키패드 닫기 - searchInputRef.current?.blur(); - } - }} - className="flex-1 bg-transparent outline-none text-sm min-w-0 [&::-webkit-search-cancel-button]:hidden" - autoFocus={!searchTerm} - /> - {searchInput && ( - - )} -
- -
- ) : ( -
- {showCalendar ? ( - // 달력 열렸을 때: 년월은 absolute로 가운데 고정, 드롭다운은 바로 옆에 - <> -
- - -
- - {/* 년월 텍스트: absolute로 정확히 가운데 고정, 클릭하면 드롭다운 토글 */} - - - {/* 드롭다운 버튼: 년월 텍스트 바로 옆에 위치하도록 가운데 배치 */} - - -
- - -
- - ) : ( - // 달력 닫혔을 때: 기존 UI - <> -
- - -
- - {selectedDate.getFullYear()}년 {selectedDate.getMonth() + 1}월 - -
- - -
- - )} -
- )} - - {/* 가로 스크롤 날짜 선택기 */} - {!isSearchMode && ( -
- {daysInMonth.map((date) => { - const dayOfWeek = date.getDay(); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const dateStr = `${year}-${month}-${day}`; - - // 해당 날짜의 일정 목록 (최대 3개) - const daySchedules = schedules - .filter(s => s.date?.split('T')[0] === dateStr) - .slice(0, 3); - - return ( - - ); - })} -
- )} -
- - {/* 달력 팝업 - fixed로 위에 띄우기 */} - - {showCalendar && !isSearchMode && ( - - { - setSelectedDate(date); - setCalendarViewDate(date); - setCalendarShowYearMonth(false); - setShowCalendar(false); - }} - /> - - )} - - - {/* 캘린더 배경 오버레이 */} - - {showCalendar && !isSearchMode && ( - setShowCalendar(false)} - className="fixed inset-0 bg-black/40 z-40" - style={{ top: 0 }} - /> - )} - - - {/* 컨텐츠 영역 */} -
-
- {isSearchMode ? ( - // 검색 모드 - showSuggestionsScreen ? ( - // 추천 검색어 화면 (유튜브 스타일) -
- {suggestions.length === 0 ? ( -
- 검색어를 입력하세요 -
- ) : ( - suggestions.map((suggestion, index) => ( - - )) - )} -
- ) : !searchTerm ? ( - // 검색어 없음 (첫 진입) -
- 검색어를 입력하세요 -
- ) : searchLoading ? ( -
-
-
- ) : searchResults.length === 0 ? ( -
- 검색 결과가 없습니다 -
- ) : ( - <> -
- {virtualizer.getVirtualItems().map((virtualItem) => { - const schedule = searchResults[virtualItem.index]; - if (!schedule) return null; - - return ( -
-
- navigate(`/schedule/${schedule.id}`)} - /> -
-
- ); - })} -
- {/* 무한 스크롤 트리거 */} -
- {isFetchingNextPage && ( -
-
-
- )} -
- - ) - ) : loading ? ( -
-
-
- ) : selectedDateSchedules.length === 0 ? ( -
- {selectedDate.getMonth() + 1}월 {selectedDate.getDate()}일 일정이 없습니다 -
- ) : ( - // 선택된 날짜의 일정 -
- {selectedDateSchedules.map((schedule, index) => { - const isBirthday = schedule.is_birthday || String(schedule.id).startsWith('birthday-'); - - if (isBirthday) { - return ( - { - const scheduleYear = new Date(schedule.date).getFullYear(); - const memberName = schedule.member_names; - navigate(`/birthday/${encodeURIComponent(memberName)}/${scheduleYear}`); - }} - /> - ); - } - - return ( - navigate(`/schedule/${schedule.id}`)} - /> - ); - })} -
- )} -
-
- - ); -} - -// 일정 카드 컴포넌트 (검색용) - 날짜 포함 모던 디자인 -function ScheduleCard({ schedule, categoryColor, categories, delay = 0, onClick }) { - const categoryName = categories.find(c => c.id === (schedule.category?.id || schedule.category_id))?.name || '미분류'; - const memberList = getMemberList(schedule); - - // 날짜 파싱 - const parseDate = (dateStr) => { - if (!dateStr) return null; - const date = new Date(dateStr); - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - const weekdays = ['일', '월', '화', '수', '목', '금', '토']; - const weekday = weekdays[date.getDay()]; - const isWeekend = date.getDay() === 0 || date.getDay() === 6; - const isSunday = date.getDay() === 0; - return { year, month, day, weekday, isWeekend, isSunday }; - }; - - const dateInfo = parseDate(schedule.date); - - return ( - - {/* 카드 본체 */} -
-
- {/* 왼쪽 날짜 영역 */} - {dateInfo && ( -
- {dateInfo.year} - {dateInfo.month}.{dateInfo.day} - - {dateInfo.weekday}요일 - -
- )} - - {/* 오른쪽 콘텐츠 영역 */} -
- {/* 시간 및 카테고리 뱃지 */} -
- {schedule.time && ( -
- - {schedule.time.slice(0, 5)} -
- )} - - {categoryName} - -
- - {/* 제목 */} -

- {decodeHtmlEntities(schedule.title)} -

- - {/* 출처 */} - {schedule.source?.name && ( -
- - {schedule.source?.name} -
- )} - - {/* 멤버 */} - {memberList.length > 0 && ( -
- {memberList.map((name, i) => ( - - {name.trim()} - - ))} -
- )} -
-
-
-
- ); -} - -// 타임라인용 일정 카드 컴포넌트 - 모던 디자인 -function TimelineScheduleCard({ schedule, categoryColor, categories, delay = 0, onClick }) { - const categoryName = categories.find(c => c.id === (schedule.category?.id || schedule.category_id))?.name || '미분류'; - const memberList = getMemberList(schedule); - - return ( - - {/* 카드 본체 */} -
-
- - {/* 시간 및 카테고리 뱃지 */} -
- {schedule.time && ( -
- - {schedule.time.slice(0, 5)} -
- )} - - {categoryName} - -
- - {/* 제목 */} -

- {decodeHtmlEntities(schedule.title)} -

- - {/* 출처 */} - {schedule.source?.name && ( -
- - {schedule.source?.name} -
- )} - - {/* 멤버 */} - {memberList.length > 0 && ( -
- {memberList.map((name, i) => ( - - {name.trim()} - - ))} -
- )} -
-
-
- ); -} - -// 달력 선택기 컴포넌트 -function CalendarPicker({ - selectedDate, - schedules = [], - categories = [], - onSelectDate, - hideHeader = false, // 헤더 숨김 여부 - externalViewDate, // 외부에서 제어하는 viewDate - onViewDateChange, // viewDate 변경 콜백 - externalShowYearMonth, // 외부에서 제어하는 년월 선택 모드 - onShowYearMonthChange // 년월 선택 모드 변경 콜백 -}) { - const [internalViewDate, setInternalViewDate] = useState(new Date(selectedDate)); - - // 외부 viewDate가 있으면 사용, 없으면 내부 상태 사용 - const viewDate = externalViewDate || internalViewDate; - const setViewDate = (date) => { - if (onViewDateChange) { - onViewDateChange(date); - } else { - setInternalViewDate(date); - } - }; - - // 터치 스와이프 핸들링 - const touchStartX = useRef(0); - const touchEndX = useRef(0); - - // 날짜별 일정 존재 여부 및 카테고리 색상 - const scheduleDates = useMemo(() => { - const dateMap = {}; - schedules.forEach(schedule => { - const date = schedule.date.split('T')[0]; // YYYY-MM-DD 형식으로 통일 - if (!dateMap[date]) { - dateMap[date] = []; - } - const category = categories.find(c => c.id === (schedule.category?.id || schedule.category_id)); - dateMap[date].push(category?.color || '#6b7280'); - }); - return dateMap; - }, [schedules, categories]); - - // 날짜별 일정 목록 가져오기 (점 표시용, 최대 3개) - const getDaySchedules = (date) => { - const y = date.getFullYear(); - const m = String(date.getMonth() + 1).padStart(2, '0'); - const d = String(date.getDate()).padStart(2, '0'); - const dateStr = `${y}-${m}-${d}`; - return schedules.filter(s => s.date?.split('T')[0] === dateStr).slice(0, 3); - }; - - const year = viewDate.getFullYear(); - const month = viewDate.getMonth(); - - // 2017년 1월 이전으로 이동 불가 - const canGoPrevMonth = !(year === 2017 && month === 0); - - // 달력 데이터 생성 함수 - const getCalendarDays = useCallback((y, m) => { - const firstDay = new Date(y, m, 1); - const lastDay = new Date(y, m + 1, 0); - const startDay = firstDay.getDay(); - const daysInMonth = lastDay.getDate(); - - const days = []; - - // 이전 달 날짜 - const prevMonth = new Date(y, m, 0); - for (let i = startDay - 1; i >= 0; i--) { - days.push({ - day: prevMonth.getDate() - i, - isCurrentMonth: false, - date: new Date(y, m - 1, prevMonth.getDate() - i) - }); - } - - // 현재 달 날짜 - for (let i = 1; i <= daysInMonth; i++) { - days.push({ - day: i, - isCurrentMonth: true, - date: new Date(y, m, i) - }); - } - - // 다음 달 날짜 (현재 줄만 채우기) - const remaining = (7 - (days.length % 7)) % 7; - for (let i = 1; i <= remaining; i++) { - days.push({ - day: i, - isCurrentMonth: false, - date: new Date(y, m + 1, i) - }); - } - - return days; - }, []); - - const changeMonth = useCallback((delta) => { - if (delta < 0 && !canGoPrevMonth) return; - const newDate = new Date(viewDate); - newDate.setMonth(newDate.getMonth() + delta); - setViewDate(newDate); - }, [viewDate, canGoPrevMonth]); - - const isToday = (date) => { - const today = new Date(); - return date.getDate() === today.getDate() && - date.getMonth() === today.getMonth() && - date.getFullYear() === today.getFullYear(); - }; - - // 선택된 날짜인지 확인 - const isSelected = (date) => { - return date.getDate() === selectedDate.getDate() && - date.getMonth() === selectedDate.getMonth() && - date.getFullYear() === selectedDate.getFullYear(); - }; - - // 년월 선택 모드 - 외부에서 제어 가능 - const [internalShowYearMonth, setInternalShowYearMonth] = useState(false); - const showYearMonth = externalShowYearMonth !== undefined ? externalShowYearMonth : internalShowYearMonth; - const setShowYearMonth = (value) => { - if (onShowYearMonthChange) { - onShowYearMonthChange(value); - } else { - setInternalShowYearMonth(value); - } - }; - - const MIN_YEAR = 2017; - const [yearRangeStart, setYearRangeStart] = useState(MIN_YEAR); - const yearRange = Array.from({ length: 12 }, (_, i) => yearRangeStart + i); - const canGoPrevYearRange = yearRangeStart > MIN_YEAR; - - // 배경 스크롤 막기 - useEffect(() => { - document.body.style.overflow = 'hidden'; - return () => { document.body.style.overflow = ''; }; - }, []); - - // 현재 달 캘린더 데이터 - const currentMonthDays = useMemo(() => { - return getCalendarDays(year, month); - }, [year, month, getCalendarDays]); - - // 터치 핸들러 - const handleTouchStart = (e) => { - touchStartX.current = e.touches[0].clientX; - }; - - const handleTouchMove = (e) => { - touchEndX.current = e.touches[0].clientX; - }; - - const handleTouchEnd = () => { - const diff = touchStartX.current - touchEndX.current; - const threshold = 50; - - if (Math.abs(diff) > threshold) { - if (diff > 0) { - changeMonth(1); - } else { - changeMonth(-1); - } - } - touchStartX.current = 0; - touchEndX.current = 0; - }; - - // 월 렌더링 컴포넌트 - const renderMonth = (days) => ( -
- {/* 요일 헤더 */} -
- {['일', '월', '화', '수', '목', '금', '토'].map((day, i) => ( -
- {day} -
- ))} -
- - {/* 날짜 그리드 */} -
- {days.map((item, index) => { - const dayOfWeek = index % 7; - const isSunday = dayOfWeek === 0; - const isSaturday = dayOfWeek === 6; - const daySchedules = item.isCurrentMonth ? getDaySchedules(item.date) : []; - - return ( - - ); - })} -
-
- ); - - return ( -
- - {showYearMonth ? ( - // 년월 선택 UI - - {/* 년도 범위 헤더 */} -
- - - {yearRangeStart} - {yearRangeStart + 11} - - -
- - {/* 년도 선택 */} -
년도
-
- {yearRange.map(y => { - const isCurrentYear = y === new Date().getFullYear(); - return ( - - ); - })} -
- - {/* 월 선택 */} -
-
- {Array.from({ length: 12 }, (_, i) => i + 1).map(m => { - const today = new Date(); - const isCurrentMonth = year === today.getFullYear() && m === today.getMonth() + 1; - return ( - - ); - })} -
-
- ) : ( - - {/* 달력 헤더 - hideHeader일 때 숨김 */} - {!hideHeader && ( -
- - - -
- )} - - {/* 달력 (터치 스와이프 지원) */} - {renderMonth(currentMonthDays)} - - {/* 오늘 버튼 */} -
- -
-
- )} -
-
- ); -} - -export default MobileSchedule; diff --git a/frontend/src/pages/mobile/public/ScheduleDetail.jsx b/frontend/src/pages/mobile/public/ScheduleDetail.jsx deleted file mode 100644 index 00df894..0000000 --- a/frontend/src/pages/mobile/public/ScheduleDetail.jsx +++ /dev/null @@ -1,613 +0,0 @@ -import { useParams, Link } from 'react-router-dom'; -import { useQuery, keepPreviousData } from '@tanstack/react-query'; -import { useEffect, useState, useRef } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { Calendar, Clock, ChevronLeft, Link2, X, ChevronRight } from 'lucide-react'; -import Linkify from 'react-linkify'; -import { getSchedule } from '../../../api/public/schedules'; -import { formatXDateTime } from '../../../utils/date'; -import '../../../mobile.css'; - -// 전체화면 시 자동 가로 회전 훅 (숏츠가 아닐 때만) -function useFullscreenOrientation(isShorts) { - useEffect(() => { - // 숏츠는 세로 유지 - if (isShorts) return; - - const handleFullscreenChange = async () => { - const isFullscreen = !!document.fullscreenElement; - - if (isFullscreen) { - // 전체화면 진입 시 가로 모드로 전환 시도 - try { - if (screen.orientation && screen.orientation.lock) { - await screen.orientation.lock('landscape'); - } - } catch (e) { - // 지원하지 않는 브라우저이거나 권한이 없는 경우 무시 - } - } else { - // 전체화면 종료 시 세로 모드로 복귀 - try { - if (screen.orientation && screen.orientation.unlock) { - screen.orientation.unlock(); - } - } catch (e) { - // 무시 - } - } - }; - - document.addEventListener('fullscreenchange', handleFullscreenChange); - document.addEventListener('webkitfullscreenchange', handleFullscreenChange); - - return () => { - document.removeEventListener('fullscreenchange', handleFullscreenChange); - document.removeEventListener('webkitfullscreenchange', handleFullscreenChange); - }; - }, [isShorts]); -} - -// 카테고리 ID 상수 -const CATEGORY_ID = { - YOUTUBE: 2, - X: 3, -}; - -// HTML 엔티티 디코딩 함수 -const decodeHtmlEntities = (text) => { - if (!text) return ''; - const textarea = document.createElement('textarea'); - textarea.innerHTML = text; - return textarea.value; -}; - -// 날짜 포맷팅 -const formatFullDate = (dateStr) => { - if (!dateStr) return ''; - const date = new Date(dateStr); - const dayNames = ['일', '월', '화', '수', '목', '금', '토']; - return `${date.getFullYear()}. ${date.getMonth() + 1}. ${date.getDate()}. (${dayNames[date.getDay()]})`; -}; - -// 시간 포맷팅 -const formatTime = (timeStr) => { - if (!timeStr) return null; - return timeStr.slice(0, 5); -}; - -// 유튜브 섹션 컴포넌트 -function YoutubeSection({ schedule }) { - const videoId = schedule.videoId; - const isShorts = schedule.videoType === 'shorts'; - - // 전체화면 시 가로 회전 (숏츠 제외) - useFullscreenOrientation(isShorts); - const members = schedule.members || []; - const isFullGroup = members.length === 5; - - if (!videoId) return null; - - return ( -
- {/* 영상 임베드 */} - -
-