구조 개편 2단계: features 내부에 pc/ 폴더 생성 + 이동
- features/boss-crystal/pc/: BossCrystal, BossCrystalAdmin, admin/, user/
- features/symbol/pc/: Symbol, SymbolAdmin, admin/
- features/liberation/pc/: Liberation, components/
- store.js, data.js는 feature 루트에 유지 (device 공용)
- registry.js: import.meta.glob 패턴을 './*/\{pc,mobile\}/*.jsx' 로 변경
- getMobileComponent 추가
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4789c56dfa
commit
b423d0ac82
18 changed files with 62 additions and 56 deletions
|
|
@ -1,10 +1,10 @@
|
||||||
import { useEffect, useLayoutEffect } from 'react'
|
import { useEffect, useLayoutEffect } from 'react'
|
||||||
import { useQuery, useQueries } from '@tanstack/react-query'
|
import { useQuery, useQueries } from '@tanstack/react-query'
|
||||||
import { api } from '../../api/client'
|
import { api } from '../../../api/client'
|
||||||
import { useLayout } from '../../components/pc/Layout'
|
import { useLayout } from '../../../components/pc/Layout'
|
||||||
import CharacterPanel from './user/CharacterPanel'
|
import CharacterPanel from './user/CharacterPanel'
|
||||||
import BossSelector from './user/BossSelector'
|
import BossSelector from './user/BossSelector'
|
||||||
import { useBossStore } from './store'
|
import { useBossStore } from '../store'
|
||||||
|
|
||||||
const MAX_PER_CHARACTER = 12
|
const MAX_PER_CHARACTER = 12
|
||||||
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { api } from '../../../api/client'
|
import { api } from '../../../../api/client'
|
||||||
import ConfirmDialog from '../../../components/common/ConfirmDialog'
|
import ConfirmDialog from '../../../../components/common/ConfirmDialog'
|
||||||
import Checkbox from '../../../components/common/Checkbox'
|
import Checkbox from '../../../../components/common/Checkbox'
|
||||||
import Select from '../../../components/common/Select'
|
import Select from '../../../../components/common/Select'
|
||||||
import { useAuthStore } from '../../../stores/auth'
|
import { useAuthStore } from '../../../../stores/auth'
|
||||||
import { DIFFICULTIES, formatMeso, getDifficultyImageUrl } from './constants'
|
import { DIFFICULTIES, formatMeso, getDifficultyImageUrl } from './constants'
|
||||||
|
|
||||||
const PARTY_OPTIONS = [1, 2, 3, 4, 5, 6].map((n) => ({ value: n, label: `${n}인` }))
|
const PARTY_OPTIONS = [1, 2, 3, 4, 5, 6].map((n) => ({ value: n, label: `${n}인` }))
|
||||||
|
|
@ -10,8 +10,8 @@ import {
|
||||||
arrayMove,
|
arrayMove,
|
||||||
} from '@dnd-kit/sortable'
|
} from '@dnd-kit/sortable'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { api } from '../../../api/client'
|
import { api } from '../../../../api/client'
|
||||||
import Tooltip from '../../../components/common/Tooltip'
|
import Tooltip from '../../../../components/common/Tooltip'
|
||||||
import { DIFFICULTIES, formatMeso, getDifficultyBadgeStyle } from './constants'
|
import { DIFFICULTIES, formatMeso, getDifficultyBadgeStyle } from './constants'
|
||||||
|
|
||||||
function BossCardContent({ boss, dragging = false }) {
|
function BossCardContent({ boss, dragging = false }) {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import Select from '../../../components/common/Select'
|
import Select from '../../../../components/common/Select'
|
||||||
import { DIFFICULTIES, formatMeso } from '../admin/constants'
|
import { DIFFICULTIES, formatMeso } from '../admin/constants'
|
||||||
|
|
||||||
const LABEL_EN = { easy: 'EASY', normal: 'NORMAL', hard: 'HARD', chaos: 'CHAOS', extreme: 'EXTREME' }
|
const LABEL_EN = { easy: 'EASY', normal: 'NORMAL', hard: 'HARD', chaos: 'CHAOS', extreme: 'EXTREME' }
|
||||||
|
|
@ -2,11 +2,11 @@ import { useState } from 'react'
|
||||||
import { useMutation } from '@tanstack/react-query'
|
import { useMutation } from '@tanstack/react-query'
|
||||||
import { Reorder, useDragControls } from 'framer-motion'
|
import { Reorder, useDragControls } from 'framer-motion'
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'
|
||||||
import { api } from '../../../api/client'
|
import { api } from '../../../../api/client'
|
||||||
import ConfirmDialog from '../../../components/common/ConfirmDialog'
|
import ConfirmDialog from '../../../../components/common/ConfirmDialog'
|
||||||
import Tooltip from '../../../components/common/Tooltip'
|
import Tooltip from '../../../../components/common/Tooltip'
|
||||||
import CharacterSuggestDropdown from '../../../components/common/CharacterSuggestDropdown'
|
import CharacterSuggestDropdown from '../../../../components/common/CharacterSuggestDropdown'
|
||||||
import { useFitText } from '../../../hooks/useFitText'
|
import { useFitText } from '../../../../hooks/useFitText'
|
||||||
import { DIFFICULTIES, formatMeso, getDifficultyBadgeStyle } from '../admin/constants'
|
import { DIFFICULTIES, formatMeso, getDifficultyBadgeStyle } from '../admin/constants'
|
||||||
|
|
||||||
const MAX_PER_CHARACTER = 12
|
const MAX_PER_CHARACTER = 12
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState, useEffect, useLayoutEffect } from 'react'
|
import { useState, useEffect, useLayoutEffect } from 'react'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { api } from '../../api/client'
|
import { api } from '../../../api/client'
|
||||||
import {
|
import {
|
||||||
GENESIS_CHAPTERS,
|
GENESIS_CHAPTERS,
|
||||||
GENESIS_TOTAL,
|
GENESIS_TOTAL,
|
||||||
|
|
@ -10,15 +10,15 @@ import {
|
||||||
calcPoints,
|
calcPoints,
|
||||||
formatDate,
|
formatDate,
|
||||||
todayKST,
|
todayKST,
|
||||||
} from './data'
|
} from '../data'
|
||||||
import { useLiberationStore } from './store'
|
import { useLiberationStore } from '../store'
|
||||||
import QuestSelector from './components/QuestSelector'
|
import QuestSelector from './components/QuestSelector'
|
||||||
import PointsInput from './components/PointsInput'
|
import PointsInput from './components/PointsInput'
|
||||||
import ProgressBar from './components/ProgressBar'
|
import ProgressBar from './components/ProgressBar'
|
||||||
import WeeklyDefault from './components/WeeklyDefault'
|
import WeeklyDefault from './components/WeeklyDefault'
|
||||||
import DatePicker from '../../components/common/DatePicker'
|
import DatePicker from '../../../components/common/DatePicker'
|
||||||
import ConfirmDialog from '../../components/common/ConfirmDialog'
|
import ConfirmDialog from '../../../components/common/ConfirmDialog'
|
||||||
import { useLayout } from '../../components/pc/Layout'
|
import { useLayout } from '../../../components/pc/Layout'
|
||||||
|
|
||||||
function makeEmptyWeekly() {
|
function makeEmptyWeekly() {
|
||||||
const bosses = {}
|
const bosses = {}
|
||||||
|
|
@ -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 = ['일', '월', '화', '수', '목', '금', '토']
|
const DOW = ['일', '월', '화', '수', '목', '금', '토']
|
||||||
function formatKoreanDate(s) {
|
function formatKoreanDate(s) {
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { GENESIS_CHAPTERS, QUEST_BOSS_IMAGE_BASE } from '../data'
|
import { GENESIS_CHAPTERS, QUEST_BOSS_IMAGE_BASE } from '../../data'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 진행 중인 퀘스트 드롭다운
|
* 진행 중인 퀘스트 드롭다운
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import Select from '../../../components/common/Select'
|
import Select from '../../../../components/common/Select'
|
||||||
import Tooltip from '../../../components/common/Tooltip'
|
import Tooltip from '../../../../components/common/Tooltip'
|
||||||
import WeeklyScheduler from './WeeklyScheduler'
|
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 PARTY_OPTIONS = [1, 2, 3, 4, 5, 6].map((n) => ({ value: n, label: `${n}인` }))
|
||||||
const NONE_DIFFICULTY = { key: 'none', label: '격파 불가', points: 0 }
|
const NONE_DIFFICULTY = { key: 'none', label: '격파 불가', points: 0 }
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import dayjs from 'dayjs'
|
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'
|
import { BossRow } from './WeeklyDefault'
|
||||||
|
|
||||||
function bossEarn(boss, sel) {
|
function bossEarn(boss, sel) {
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
/**
|
/**
|
||||||
* 기능 자동 등록 시스템
|
* 기능 자동 등록 시스템
|
||||||
*
|
*
|
||||||
* - features/{kebab-case}/{PascalCase}.jsx : 사용자 페이지
|
* - features/{kebab-case}/pc/{PascalCase}.jsx : PC 사용자 페이지
|
||||||
* - features/{kebab-case}/{PascalCase}Admin.jsx : 관리자 페이지
|
* - features/{kebab-case}/pc/{PascalCase}Admin.jsx : PC 관리자 페이지
|
||||||
|
* - features/{kebab-case}/mobile/{PascalCase}.jsx : 모바일 사용자 페이지
|
||||||
*
|
*
|
||||||
* 예시:
|
* 예시:
|
||||||
* /boss-crystal → features/boss-crystal/BossCrystal.jsx
|
* /boss-crystal → features/boss-crystal/pc/BossCrystal.jsx
|
||||||
* /admin/boss-crystal → features/boss-crystal/BossCrystalAdmin.jsx
|
* /admin/boss-crystal → features/boss-crystal/pc/BossCrystalAdmin.jsx
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { lazy } from 'react'
|
import { lazy } from 'react'
|
||||||
|
|
||||||
// Vite의 import.meta.glob으로 features 폴더 전체를 스캔
|
const pages = import.meta.glob('./*/{pc,mobile}/*.jsx')
|
||||||
const userPages = import.meta.glob('./*/*.jsx')
|
|
||||||
|
|
||||||
function slugToPascal(slug) {
|
function slugToPascal(slug) {
|
||||||
return slug
|
return slug
|
||||||
|
|
@ -21,33 +21,39 @@ function slugToPascal(slug) {
|
||||||
.join('')
|
.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 컴포넌트 캐시 - 동일 slug에 대해 항상 같은 컴포넌트 인스턴스 반환
|
const userPcCache = new Map()
|
||||||
// (매 렌더마다 새 lazy() 생성하면 React가 unmount/remount하면서 화면 갱신이 깨짐)
|
const adminPcCache = new Map()
|
||||||
const userCache = new Map()
|
const userMobileCache = new Map()
|
||||||
const adminCache = new Map()
|
|
||||||
|
|
||||||
function loadCached(cache, slug, suffix) {
|
function loadCached(cache, slug, device, suffix) {
|
||||||
if (cache.has(slug)) return cache.get(slug)
|
if (cache.has(slug)) return cache.get(slug)
|
||||||
const pascal = slugToPascal(slug)
|
const pascal = slugToPascal(slug)
|
||||||
const path = `./${slug}/${pascal}${suffix}.jsx`
|
const path = `./${slug}/${device}/${pascal}${suffix}.jsx`
|
||||||
const loader = userPages[path]
|
const loader = pages[path]
|
||||||
const component = loader ? lazy(loader) : null
|
const component = loader ? lazy(loader) : null
|
||||||
cache.set(slug, component)
|
cache.set(slug, component)
|
||||||
return component
|
return component
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* slug에 해당하는 사용자 페이지 컴포넌트 반환
|
* slug에 해당하는 PC 사용자 페이지 컴포넌트 반환
|
||||||
*/
|
*/
|
||||||
export function getUserComponent(slug) {
|
export function getUserComponent(slug) {
|
||||||
return loadCached(userCache, slug, '')
|
return loadCached(userPcCache, slug, 'pc', '')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* slug에 해당하는 관리자 페이지 컴포넌트 반환
|
* slug에 해당하는 관리자 페이지 컴포넌트 반환 (PC 전용)
|
||||||
*/
|
*/
|
||||||
export function getAdminComponent(slug) {
|
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', '')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ import { useQuery, useQueries, useMutation } from '@tanstack/react-query'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import utc from 'dayjs/plugin/utc'
|
import utc from 'dayjs/plugin/utc'
|
||||||
import timezone from 'dayjs/plugin/timezone'
|
import timezone from 'dayjs/plugin/timezone'
|
||||||
import { api } from '../../api/client'
|
import { api } from '../../../api/client'
|
||||||
import { useLayout } from '../../components/pc/Layout'
|
import { useLayout } from '../../../components/pc/Layout'
|
||||||
import Select from '../../components/common/Select'
|
import Select from '../../../components/common/Select'
|
||||||
import Tooltip from '../../components/common/Tooltip'
|
import Tooltip from '../../../components/common/Tooltip'
|
||||||
import CharacterSuggestDropdown from '../../components/common/CharacterSuggestDropdown'
|
import CharacterSuggestDropdown from '../../../components/common/CharacterSuggestDropdown'
|
||||||
import { useSymbolStore } from './store'
|
import { useSymbolStore } from '../store'
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { useState, useRef, useEffect } from 'react'
|
import { useState, useRef, useEffect } from 'react'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { api } from '../../../api/client'
|
import { api } from '../../../../api/client'
|
||||||
import Select from '../../../components/common/Select'
|
import Select from '../../../../components/common/Select'
|
||||||
import ConfirmDialog from '../../../components/common/ConfirmDialog'
|
import ConfirmDialog from '../../../../components/common/ConfirmDialog'
|
||||||
import { useAuthStore } from '../../../stores/auth'
|
import { useAuthStore } from '../../../../stores/auth'
|
||||||
|
|
||||||
const TYPE_OPTIONS = [
|
const TYPE_OPTIONS = [
|
||||||
{ value: '아케인', label: '아케인' },
|
{ value: '아케인', label: '아케인' },
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
arrayMove,
|
arrayMove,
|
||||||
} from '@dnd-kit/sortable'
|
} from '@dnd-kit/sortable'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { api } from '../../../api/client'
|
import { api } from '../../../../api/client'
|
||||||
|
|
||||||
const TYPE_STYLE = {
|
const TYPE_STYLE = {
|
||||||
'아케인': {
|
'아케인': {
|
||||||
Loading…
Add table
Reference in a new issue