From 1de5dcb7b9f02c9092083e188bb74793c2bf0078 Mon Sep 17 00:00:00 2001 From: caadiq Date: Mon, 13 Apr 2026 15:27:04 +0900 Subject: [PATCH] =?UTF-8?q?URL=20slug=20=EA=B8=B0=EB=B0=98=20=EB=8F=99?= =?UTF-8?q?=EC=A0=81=20=EA=B8=B0=EB=8A=A5=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - features/registry.js: import.meta.glob으로 자동 컴포넌트 등록 - /:slug → features/{slug}/{PascalCase}.jsx 매핑 - /admin/:slug → features/{slug}/{PascalCase}Admin.jsx 매핑 - AdminHome 카드 분리 액션 (본체→기능 관리, ⚙→메뉴 정보 편집) - AdminFeaturePage에 메뉴 정보 편집 단축 링크 추가 - 예시: features/boss-crystal/ stub 컴포넌트 Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/App.jsx | 8 +++ frontend/src/features/FeaturePage.jsx | 22 ++++++ .../src/features/admin/AdminFeaturePage.jsx | 67 +++++++++++++++++++ frontend/src/features/admin/AdminHome.jsx | 27 +++++++- .../src/features/boss-crystal/BossCrystal.jsx | 8 +++ .../boss-crystal/BossCrystalAdmin.jsx | 11 +++ frontend/src/features/registry.js | 55 +++++++++++++++ 7 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 frontend/src/features/FeaturePage.jsx create mode 100644 frontend/src/features/admin/AdminFeaturePage.jsx create mode 100644 frontend/src/features/boss-crystal/BossCrystal.jsx create mode 100644 frontend/src/features/boss-crystal/BossCrystalAdmin.jsx create mode 100644 frontend/src/features/registry.js diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a19ebae..1fc3d14 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,22 +1,30 @@ import { Routes, Route } from 'react-router-dom' import Layout from './components/Layout' import Home from './pages/Home' +import FeaturePage from './features/FeaturePage' import AdminLayout from './features/admin/AdminLayout' import AdminHome from './features/admin/AdminHome' import AdminImages from './features/admin/AdminImages' import AdminMenuForm from './features/admin/AdminMenuForm' +import AdminFeaturePage from './features/admin/AdminFeaturePage' export default function App() { return ( }> } /> + + {/* 관리자 */} }> } /> } /> } /> } /> + } /> + + {/* 동적 기능 페이지 */} + } /> ) diff --git a/frontend/src/features/FeaturePage.jsx b/frontend/src/features/FeaturePage.jsx new file mode 100644 index 0000000..f8c7a48 --- /dev/null +++ b/frontend/src/features/FeaturePage.jsx @@ -0,0 +1,22 @@ +import { Suspense } from 'react' +import { useParams, Navigate } from 'react-router-dom' +import { getUserComponent } from './registry' + +export default function FeaturePage() { + const { slug } = useParams() + const Component = getUserComponent(slug) + + if (!Component) { + return + } + + return ( + +
+
+ }> + +
+ ) +} diff --git a/frontend/src/features/admin/AdminFeaturePage.jsx b/frontend/src/features/admin/AdminFeaturePage.jsx new file mode 100644 index 0000000..75bda92 --- /dev/null +++ b/frontend/src/features/admin/AdminFeaturePage.jsx @@ -0,0 +1,67 @@ +import { Suspense } from 'react' +import { useParams, Navigate, Link } from 'react-router-dom' +import { useQuery } from '@tanstack/react-query' +import { getAdminComponent } from '../registry' +import { api } from '../../api/client' + +export default function AdminFeaturePage() { + const { slug } = useParams() + const Component = getAdminComponent(slug) + + // 메뉴 정보 조회 (편집 링크 표시용) + const { data: menus = [] } = useQuery({ + queryKey: ['admin', 'menus'], + queryFn: () => api('/api/admin/menus').catch(() => []), + }) + const menu = menus.find((m) => (m.url || '').replace(/^\/+/, '').split('/')[0] === slug) + + if (!Component) { + return ( +
+ {menu && ( +
+
+

{menu.title}

+

{menu.description}

+
+ + ⚙ 메뉴 정보 편집 + +
+ )} +
+
🛠️
+

이 기능에는 관리 페이지가 없습니다

+

+ features/{slug}/{slug.split('-').map((s) => s[0].toUpperCase() + s.slice(1)).join('')}Admin.jsx +

+
+
+ ) + } + + return ( +
+ {menu && ( +
+ + ⚙ 메뉴 정보 편집 + +
+ )} + +
+
+ }> + +
+
+ ) +} diff --git a/frontend/src/features/admin/AdminHome.jsx b/frontend/src/features/admin/AdminHome.jsx index 812521f..306cf60 100644 --- a/frontend/src/features/admin/AdminHome.jsx +++ b/frontend/src/features/admin/AdminHome.jsx @@ -1,20 +1,41 @@ -import { Link } from 'react-router-dom' +import { Link, useNavigate } from 'react-router-dom' import { useQuery } from '@tanstack/react-query' import { api } from '../../api/client' function MenuCard({ menu }) { + const navigate = useNavigate() + // 메뉴 url에서 slug 추출 (/boss-crystal → boss-crystal) + const slug = (menu.url || '').replace(/^\/+/, '').split('/')[0] + const adminPath = slug ? `/admin/${slug}` : `/admin/menus/${menu.id}` + + const handleEditClick = (e) => { + e.preventDefault() + e.stopPropagation() + navigate(`/admin/menus/${menu.id}`) + } + return (
+ {/* 톱니바퀴 - 메뉴 정보 편집 */} + +
{menu.title}
-
+

{menu.title}

{menu.description}

{menu.url}

diff --git a/frontend/src/features/boss-crystal/BossCrystal.jsx b/frontend/src/features/boss-crystal/BossCrystal.jsx new file mode 100644 index 0000000..11f2f26 --- /dev/null +++ b/frontend/src/features/boss-crystal/BossCrystal.jsx @@ -0,0 +1,8 @@ +export default function BossCrystal() { + return ( +
+

주간 보스 결정석 계산기

+

준비 중입니다.

+
+ ) +} diff --git a/frontend/src/features/boss-crystal/BossCrystalAdmin.jsx b/frontend/src/features/boss-crystal/BossCrystalAdmin.jsx new file mode 100644 index 0000000..8bbb555 --- /dev/null +++ b/frontend/src/features/boss-crystal/BossCrystalAdmin.jsx @@ -0,0 +1,11 @@ +export default function BossCrystalAdmin() { + return ( +
+

보스 결정석 관리

+

보스 정보 및 결정석 가격을 관리합니다

+
+ 준비 중 +
+
+ ) +} diff --git a/frontend/src/features/registry.js b/frontend/src/features/registry.js new file mode 100644 index 0000000..6e93d07 --- /dev/null +++ b/frontend/src/features/registry.js @@ -0,0 +1,55 @@ +/** + * 기능 자동 등록 시스템 + * + * - features/{kebab-case}/{PascalCase}.jsx : 사용자 페이지 + * - features/{kebab-case}/{PascalCase}Admin.jsx : 관리자 페이지 + * + * 예시: + * /boss-crystal → features/boss-crystal/BossCrystal.jsx + * /admin/boss-crystal → features/boss-crystal/BossCrystalAdmin.jsx + */ + +import { lazy } from 'react' + +// Vite의 import.meta.glob으로 features 폴더 전체를 스캔 +const userPages = import.meta.glob('./*/*.jsx') + +function slugToPascal(slug) { + return slug + .split('-') + .map((s) => s.charAt(0).toUpperCase() + s.slice(1)) + .join('') +} + +/** + * slug에 해당하는 사용자 페이지 컴포넌트 반환 + * @returns {React.LazyExoticComponent | null} + */ +export function getUserComponent(slug) { + const pascal = slugToPascal(slug) + const path = `./${slug}/${pascal}.jsx` + const loader = userPages[path] + if (!loader) return null + return lazy(loader) +} + +/** + * slug에 해당하는 관리자 페이지 컴포넌트 반환 + */ +export function getAdminComponent(slug) { + const pascal = slugToPascal(slug) + const path = `./${slug}/${pascal}Admin.jsx` + const loader = userPages[path] + if (!loader) return null + return lazy(loader) +} + +/** + * slug에 해당하는 관리자 페이지가 존재하는지 + */ +export function hasAdminPage(slug) { + if (!slug) return false + const cleaned = slug.replace(/^\/+/, '').split('/')[0] + const pascal = slugToPascal(cleaned) + return !!userPages[`./${cleaned}/${pascal}Admin.jsx`] +}