diff --git a/frontend/src/features/boss-crystal/BossCrystal.jsx b/frontend/src/features/boss-crystal/pc/BossCrystal.jsx similarity index 96% rename from frontend/src/features/boss-crystal/BossCrystal.jsx rename to frontend/src/features/boss-crystal/pc/BossCrystal.jsx index 7b8185d..f6e13e3 100644 --- a/frontend/src/features/boss-crystal/BossCrystal.jsx +++ b/frontend/src/features/boss-crystal/pc/BossCrystal.jsx @@ -1,10 +1,10 @@ import { useEffect, useLayoutEffect } from 'react' import { useQuery, useQueries } from '@tanstack/react-query' -import { api } from '../../api/client' -import { useLayout } from '../../components/pc/Layout' +import { api } from '../../../api/client' +import { useLayout } from '../../../components/pc/Layout' import CharacterPanel from './user/CharacterPanel' import BossSelector from './user/BossSelector' -import { useBossStore } from './store' +import { useBossStore } from '../store' const MAX_PER_CHARACTER = 12 diff --git a/frontend/src/features/boss-crystal/BossCrystalAdmin.jsx b/frontend/src/features/boss-crystal/pc/BossCrystalAdmin.jsx similarity index 100% rename from frontend/src/features/boss-crystal/BossCrystalAdmin.jsx rename to frontend/src/features/boss-crystal/pc/BossCrystalAdmin.jsx diff --git a/frontend/src/features/boss-crystal/admin/BossForm.jsx b/frontend/src/features/boss-crystal/pc/admin/BossForm.jsx similarity index 97% rename from frontend/src/features/boss-crystal/admin/BossForm.jsx rename to frontend/src/features/boss-crystal/pc/admin/BossForm.jsx index 861ac6c..13b3574 100644 --- a/frontend/src/features/boss-crystal/admin/BossForm.jsx +++ b/frontend/src/features/boss-crystal/pc/admin/BossForm.jsx @@ -1,11 +1,11 @@ import { useState, useEffect, useRef } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { api } from '../../../api/client' -import ConfirmDialog from '../../../components/common/ConfirmDialog' -import Checkbox from '../../../components/common/Checkbox' -import Select from '../../../components/common/Select' -import { useAuthStore } from '../../../stores/auth' +import { api } from '../../../../api/client' +import ConfirmDialog from '../../../../components/common/ConfirmDialog' +import Checkbox from '../../../../components/common/Checkbox' +import Select from '../../../../components/common/Select' +import { useAuthStore } from '../../../../stores/auth' import { DIFFICULTIES, formatMeso, getDifficultyImageUrl } from './constants' const PARTY_OPTIONS = [1, 2, 3, 4, 5, 6].map((n) => ({ value: n, label: `${n}인` })) diff --git a/frontend/src/features/boss-crystal/admin/BossList.jsx b/frontend/src/features/boss-crystal/pc/admin/BossList.jsx similarity index 98% rename from frontend/src/features/boss-crystal/admin/BossList.jsx rename to frontend/src/features/boss-crystal/pc/admin/BossList.jsx index 438ab62..d17f3a8 100644 --- a/frontend/src/features/boss-crystal/admin/BossList.jsx +++ b/frontend/src/features/boss-crystal/pc/admin/BossList.jsx @@ -10,8 +10,8 @@ import { arrayMove, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' -import { api } from '../../../api/client' -import Tooltip from '../../../components/common/Tooltip' +import { api } from '../../../../api/client' +import Tooltip from '../../../../components/common/Tooltip' import { DIFFICULTIES, formatMeso, getDifficultyBadgeStyle } from './constants' function BossCardContent({ boss, dragging = false }) { diff --git a/frontend/src/features/boss-crystal/admin/constants.js b/frontend/src/features/boss-crystal/pc/admin/constants.js similarity index 100% rename from frontend/src/features/boss-crystal/admin/constants.js rename to frontend/src/features/boss-crystal/pc/admin/constants.js diff --git a/frontend/src/features/boss-crystal/user/BossSelector.jsx b/frontend/src/features/boss-crystal/pc/user/BossSelector.jsx similarity index 99% rename from frontend/src/features/boss-crystal/user/BossSelector.jsx rename to frontend/src/features/boss-crystal/pc/user/BossSelector.jsx index 37c6b17..13eb5bf 100644 --- a/frontend/src/features/boss-crystal/user/BossSelector.jsx +++ b/frontend/src/features/boss-crystal/pc/user/BossSelector.jsx @@ -1,4 +1,4 @@ -import Select from '../../../components/common/Select' +import Select from '../../../../components/common/Select' import { DIFFICULTIES, formatMeso } from '../admin/constants' const LABEL_EN = { easy: 'EASY', normal: 'NORMAL', hard: 'HARD', chaos: 'CHAOS', extreme: 'EXTREME' } diff --git a/frontend/src/features/boss-crystal/user/CharacterPanel.jsx b/frontend/src/features/boss-crystal/pc/user/CharacterPanel.jsx similarity index 97% rename from frontend/src/features/boss-crystal/user/CharacterPanel.jsx rename to frontend/src/features/boss-crystal/pc/user/CharacterPanel.jsx index 3aa36c7..5e9a21f 100644 --- a/frontend/src/features/boss-crystal/user/CharacterPanel.jsx +++ b/frontend/src/features/boss-crystal/pc/user/CharacterPanel.jsx @@ -2,11 +2,11 @@ import { useState } from 'react' import { useMutation } from '@tanstack/react-query' import { Reorder, useDragControls } from 'framer-motion' import { OverlayScrollbarsComponent } from 'overlayscrollbars-react' -import { api } from '../../../api/client' -import ConfirmDialog from '../../../components/common/ConfirmDialog' -import Tooltip from '../../../components/common/Tooltip' -import CharacterSuggestDropdown from '../../../components/common/CharacterSuggestDropdown' -import { useFitText } from '../../../hooks/useFitText' +import { api } from '../../../../api/client' +import ConfirmDialog from '../../../../components/common/ConfirmDialog' +import Tooltip from '../../../../components/common/Tooltip' +import CharacterSuggestDropdown from '../../../../components/common/CharacterSuggestDropdown' +import { useFitText } from '../../../../hooks/useFitText' import { DIFFICULTIES, formatMeso, getDifficultyBadgeStyle } from '../admin/constants' const MAX_PER_CHARACTER = 12 diff --git a/frontend/src/features/liberation/Liberation.jsx b/frontend/src/features/liberation/pc/Liberation.jsx similarity index 98% rename from frontend/src/features/liberation/Liberation.jsx rename to frontend/src/features/liberation/pc/Liberation.jsx index 9de99b7..32f28c2 100644 --- a/frontend/src/features/liberation/Liberation.jsx +++ b/frontend/src/features/liberation/pc/Liberation.jsx @@ -1,7 +1,7 @@ import { useState, useEffect, useLayoutEffect } from 'react' import { useQuery } from '@tanstack/react-query' import dayjs from 'dayjs' -import { api } from '../../api/client' +import { api } from '../../../api/client' import { GENESIS_CHAPTERS, GENESIS_TOTAL, @@ -10,15 +10,15 @@ import { calcPoints, formatDate, todayKST, -} from './data' -import { useLiberationStore } from './store' +} from '../data' +import { useLiberationStore } from '../store' import QuestSelector from './components/QuestSelector' import PointsInput from './components/PointsInput' import ProgressBar from './components/ProgressBar' import WeeklyDefault from './components/WeeklyDefault' -import DatePicker from '../../components/common/DatePicker' -import ConfirmDialog from '../../components/common/ConfirmDialog' -import { useLayout } from '../../components/pc/Layout' +import DatePicker from '../../../components/common/DatePicker' +import ConfirmDialog from '../../../components/common/ConfirmDialog' +import { useLayout } from '../../../components/pc/Layout' function makeEmptyWeekly() { const bosses = {} diff --git a/frontend/src/features/liberation/components/PointsInput.jsx b/frontend/src/features/liberation/pc/components/PointsInput.jsx similarity index 100% rename from frontend/src/features/liberation/components/PointsInput.jsx rename to frontend/src/features/liberation/pc/components/PointsInput.jsx diff --git a/frontend/src/features/liberation/components/ProgressBar.jsx b/frontend/src/features/liberation/pc/components/ProgressBar.jsx similarity index 99% rename from frontend/src/features/liberation/components/ProgressBar.jsx rename to frontend/src/features/liberation/pc/components/ProgressBar.jsx index f627c6f..801cec2 100644 --- a/frontend/src/features/liberation/components/ProgressBar.jsx +++ b/frontend/src/features/liberation/pc/components/ProgressBar.jsx @@ -1,4 +1,4 @@ -import { GENESIS_CHAPTERS, GENESIS_TOTAL, QUEST_BOSS_IMAGE_BASE } from '../data' +import { GENESIS_CHAPTERS, GENESIS_TOTAL, QUEST_BOSS_IMAGE_BASE } from '../../data' const DOW = ['일', '월', '화', '수', '목', '금', '토'] function formatKoreanDate(s) { diff --git a/frontend/src/features/liberation/components/QuestSelector.jsx b/frontend/src/features/liberation/pc/components/QuestSelector.jsx similarity index 98% rename from frontend/src/features/liberation/components/QuestSelector.jsx rename to frontend/src/features/liberation/pc/components/QuestSelector.jsx index f199f04..e16d25a 100644 --- a/frontend/src/features/liberation/components/QuestSelector.jsx +++ b/frontend/src/features/liberation/pc/components/QuestSelector.jsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from 'react' import { motion, AnimatePresence } from 'framer-motion' -import { GENESIS_CHAPTERS, QUEST_BOSS_IMAGE_BASE } from '../data' +import { GENESIS_CHAPTERS, QUEST_BOSS_IMAGE_BASE } from '../../data' /** * 진행 중인 퀘스트 드롭다운 diff --git a/frontend/src/features/liberation/components/WeeklyDefault.jsx b/frontend/src/features/liberation/pc/components/WeeklyDefault.jsx similarity index 97% rename from frontend/src/features/liberation/components/WeeklyDefault.jsx rename to frontend/src/features/liberation/pc/components/WeeklyDefault.jsx index a9c04d1..4d83058 100644 --- a/frontend/src/features/liberation/components/WeeklyDefault.jsx +++ b/frontend/src/features/liberation/pc/components/WeeklyDefault.jsx @@ -1,7 +1,7 @@ -import Select from '../../../components/common/Select' -import Tooltip from '../../../components/common/Tooltip' +import Select from '../../../../components/common/Select' +import Tooltip from '../../../../components/common/Tooltip' import WeeklyScheduler from './WeeklyScheduler' -import { WEEKLY_BOSSES, MONTHLY_BOSSES, LIBERATION_BOSS_IMAGE_BASE, calcPoints } from '../data' +import { WEEKLY_BOSSES, MONTHLY_BOSSES, LIBERATION_BOSS_IMAGE_BASE, calcPoints } from '../../data' const PARTY_OPTIONS = [1, 2, 3, 4, 5, 6].map((n) => ({ value: n, label: `${n}인` })) const NONE_DIFFICULTY = { key: 'none', label: '격파 불가', points: 0 } diff --git a/frontend/src/features/liberation/components/WeeklyScheduler.jsx b/frontend/src/features/liberation/pc/components/WeeklyScheduler.jsx similarity index 99% rename from frontend/src/features/liberation/components/WeeklyScheduler.jsx rename to frontend/src/features/liberation/pc/components/WeeklyScheduler.jsx index ded6c0d..525b69e 100644 --- a/frontend/src/features/liberation/components/WeeklyScheduler.jsx +++ b/frontend/src/features/liberation/pc/components/WeeklyScheduler.jsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { motion, AnimatePresence } from 'framer-motion' import dayjs from 'dayjs' -import { LIBERATION_BOSS_IMAGE_BASE, WEEKLY_BOSSES, MONTHLY_BOSSES, calcPoints } from '../data' +import { LIBERATION_BOSS_IMAGE_BASE, WEEKLY_BOSSES, MONTHLY_BOSSES, calcPoints } from '../../data' import { BossRow } from './WeeklyDefault' function bossEarn(boss, sel) { diff --git a/frontend/src/features/registry.js b/frontend/src/features/registry.js index 213e8d0..b53f28c 100644 --- a/frontend/src/features/registry.js +++ b/frontend/src/features/registry.js @@ -1,18 +1,18 @@ /** * 기능 자동 등록 시스템 * - * - features/{kebab-case}/{PascalCase}.jsx : 사용자 페이지 - * - features/{kebab-case}/{PascalCase}Admin.jsx : 관리자 페이지 + * - features/{kebab-case}/pc/{PascalCase}.jsx : PC 사용자 페이지 + * - features/{kebab-case}/pc/{PascalCase}Admin.jsx : PC 관리자 페이지 + * - features/{kebab-case}/mobile/{PascalCase}.jsx : 모바일 사용자 페이지 * * 예시: - * /boss-crystal → features/boss-crystal/BossCrystal.jsx - * /admin/boss-crystal → features/boss-crystal/BossCrystalAdmin.jsx + * /boss-crystal → features/boss-crystal/pc/BossCrystal.jsx + * /admin/boss-crystal → features/boss-crystal/pc/BossCrystalAdmin.jsx */ import { lazy } from 'react' -// Vite의 import.meta.glob으로 features 폴더 전체를 스캔 -const userPages = import.meta.glob('./*/*.jsx') +const pages = import.meta.glob('./*/{pc,mobile}/*.jsx') function slugToPascal(slug) { return slug @@ -21,33 +21,39 @@ function slugToPascal(slug) { .join('') } -// 컴포넌트 캐시 - 동일 slug에 대해 항상 같은 컴포넌트 인스턴스 반환 -// (매 렌더마다 새 lazy() 생성하면 React가 unmount/remount하면서 화면 갱신이 깨짐) -const userCache = new Map() -const adminCache = new Map() +const userPcCache = new Map() +const adminPcCache = new Map() +const userMobileCache = new Map() -function loadCached(cache, slug, suffix) { +function loadCached(cache, slug, device, suffix) { if (cache.has(slug)) return cache.get(slug) const pascal = slugToPascal(slug) - const path = `./${slug}/${pascal}${suffix}.jsx` - const loader = userPages[path] + const path = `./${slug}/${device}/${pascal}${suffix}.jsx` + const loader = pages[path] const component = loader ? lazy(loader) : null cache.set(slug, component) return component } /** - * slug에 해당하는 사용자 페이지 컴포넌트 반환 + * slug에 해당하는 PC 사용자 페이지 컴포넌트 반환 */ export function getUserComponent(slug) { - return loadCached(userCache, slug, '') + return loadCached(userPcCache, slug, 'pc', '') } /** - * slug에 해당하는 관리자 페이지 컴포넌트 반환 + * slug에 해당하는 관리자 페이지 컴포넌트 반환 (PC 전용) */ export function getAdminComponent(slug) { - return loadCached(adminCache, slug, 'Admin') + return loadCached(adminPcCache, slug, 'pc', 'Admin') +} + +/** + * slug에 해당하는 모바일 사용자 페이지 컴포넌트 반환 + */ +export function getMobileComponent(slug) { + return loadCached(userMobileCache, slug, 'mobile', '') } /** diff --git a/frontend/src/features/symbol/Symbol.jsx b/frontend/src/features/symbol/pc/Symbol.jsx similarity index 98% rename from frontend/src/features/symbol/Symbol.jsx rename to frontend/src/features/symbol/pc/Symbol.jsx index ba43172..36d7bd7 100644 --- a/frontend/src/features/symbol/Symbol.jsx +++ b/frontend/src/features/symbol/pc/Symbol.jsx @@ -3,12 +3,12 @@ import { useQuery, useQueries, useMutation } from '@tanstack/react-query' import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' import timezone from 'dayjs/plugin/timezone' -import { api } from '../../api/client' -import { useLayout } from '../../components/pc/Layout' -import Select from '../../components/common/Select' -import Tooltip from '../../components/common/Tooltip' -import CharacterSuggestDropdown from '../../components/common/CharacterSuggestDropdown' -import { useSymbolStore } from './store' +import { api } from '../../../api/client' +import { useLayout } from '../../../components/pc/Layout' +import Select from '../../../components/common/Select' +import Tooltip from '../../../components/common/Tooltip' +import CharacterSuggestDropdown from '../../../components/common/CharacterSuggestDropdown' +import { useSymbolStore } from '../store' dayjs.extend(utc) dayjs.extend(timezone) diff --git a/frontend/src/features/symbol/SymbolAdmin.jsx b/frontend/src/features/symbol/pc/SymbolAdmin.jsx similarity index 100% rename from frontend/src/features/symbol/SymbolAdmin.jsx rename to frontend/src/features/symbol/pc/SymbolAdmin.jsx diff --git a/frontend/src/features/symbol/admin/SymbolForm.jsx b/frontend/src/features/symbol/pc/admin/SymbolForm.jsx similarity index 98% rename from frontend/src/features/symbol/admin/SymbolForm.jsx rename to frontend/src/features/symbol/pc/admin/SymbolForm.jsx index c3eb7fd..3bde41d 100644 --- a/frontend/src/features/symbol/admin/SymbolForm.jsx +++ b/frontend/src/features/symbol/pc/admin/SymbolForm.jsx @@ -1,10 +1,10 @@ import { useState, useRef, useEffect } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { api } from '../../../api/client' -import Select from '../../../components/common/Select' -import ConfirmDialog from '../../../components/common/ConfirmDialog' -import { useAuthStore } from '../../../stores/auth' +import { api } from '../../../../api/client' +import Select from '../../../../components/common/Select' +import ConfirmDialog from '../../../../components/common/ConfirmDialog' +import { useAuthStore } from '../../../../stores/auth' const TYPE_OPTIONS = [ { value: '아케인', label: '아케인' }, diff --git a/frontend/src/features/symbol/admin/SymbolList.jsx b/frontend/src/features/symbol/pc/admin/SymbolList.jsx similarity index 99% rename from frontend/src/features/symbol/admin/SymbolList.jsx rename to frontend/src/features/symbol/pc/admin/SymbolList.jsx index d2e8dd8..a1813ce 100644 --- a/frontend/src/features/symbol/admin/SymbolList.jsx +++ b/frontend/src/features/symbol/pc/admin/SymbolList.jsx @@ -10,7 +10,7 @@ import { arrayMove, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' -import { api } from '../../../api/client' +import { api } from '../../../../api/client' const TYPE_STYLE = { '아케인': {