구조 개편 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:
caadiq 2026-04-19 11:26:12 +09:00
parent 4789c56dfa
commit b423d0ac82
18 changed files with 62 additions and 56 deletions

View file

@ -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

View file

@ -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}` }))

View file

@ -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 }) {

View file

@ -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' }

View file

@ -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

View file

@ -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 = {}

View file

@ -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) {

View file

@ -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'
/**
* 진행 중인 퀘스트 드롭다운

View file

@ -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 }

View file

@ -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) {

View file

@ -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', '')
}
/**

View file

@ -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)

View file

@ -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: '아케인' },

View file

@ -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 = {
'아케인': {