From 72ff284f200c41ff4fc25da47d69d3a955eb0949 Mon Sep 17 00:00:00 2001 From: caadiq Date: Mon, 13 Apr 2026 14:20:32 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EB=8C=80?= =?UTF-8?q?=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EB=A6=AC=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=8F=20=EB=A9=94=EB=89=B4/=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EA=B4=80=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 관리자 페이지 카드형 메뉴 구조로 개편 (DB 연동 준비) - 메이플스토리 폰트, 단풍잎 favicon 적용 - 헤더 디자인 개선 (백드롭 블러, 단풍잎 로고) - 홈 페이지를 메뉴 동적 로드 형태로 변경 - 보스 계산기 페이지 제거 (DB 기반으로 재구축 예정) - 이미지/메뉴 관리 페이지 라우트 추가 (placeholder) Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/index.html | 8 +- frontend/public/favicon.ico | Bin 0 -> 17309 bytes frontend/public/favicon.svg | 1 - frontend/src/App.jsx | 13 +- frontend/src/components/Layout.jsx | 18 +- frontend/src/features/admin/AdminBoss.jsx | 10 + frontend/src/features/admin/AdminHome.jsx | 116 ++++++ frontend/src/features/admin/AdminImages.jsx | 13 + frontend/src/features/admin/AdminLayout.jsx | 73 ++++ frontend/src/features/admin/AdminMenuForm.jsx | 13 + frontend/src/features/boss/BossPage.jsx | 391 ------------------ frontend/src/index.css | 15 + frontend/src/pages/Admin.jsx | 55 --- frontend/src/pages/Home.jsx | 80 +++- 14 files changed, 331 insertions(+), 475 deletions(-) create mode 100644 frontend/public/favicon.ico delete mode 100644 frontend/public/favicon.svg create mode 100644 frontend/src/features/admin/AdminBoss.jsx create mode 100644 frontend/src/features/admin/AdminHome.jsx create mode 100644 frontend/src/features/admin/AdminImages.jsx create mode 100644 frontend/src/features/admin/AdminLayout.jsx create mode 100644 frontend/src/features/admin/AdminMenuForm.jsx delete mode 100644 frontend/src/features/boss/BossPage.jsx delete mode 100644 frontend/src/pages/Admin.jsx diff --git a/frontend/index.html b/frontend/index.html index e663c8b..4223fe8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,9 +2,13 @@ - + - 메이플스토리 도우미 + + + + + 메이플스토리 유틸리티
diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..746727f3d1c94fa72dad23f7f62694876079a877 GIT binary patch literal 17309 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 \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0a317e6..69d29b1 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,16 +1,21 @@ import { Routes, Route } from 'react-router-dom' import Layout from './components/Layout' import Home from './pages/Home' -import BossPage from './features/boss/BossPage' -import Admin from './pages/Admin' +import AdminLayout from './features/admin/AdminLayout' +import AdminHome from './features/admin/AdminHome' +import AdminImages from './features/admin/AdminImages' +import AdminMenuForm from './features/admin/AdminMenuForm' export default function App() { return ( }> } /> - } /> - } /> + }> + } /> + } /> + } /> + ) diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index f9b260d..363077a 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -2,16 +2,18 @@ import { Outlet, Link } from 'react-router-dom' export default function Layout() { return ( -
-
-
- 메이플스토리 도우미 - +
+
+
+ + + + 메이플스토리 유틸리티 + +
-
+
diff --git a/frontend/src/features/admin/AdminBoss.jsx b/frontend/src/features/admin/AdminBoss.jsx new file mode 100644 index 0000000..524a3e2 --- /dev/null +++ b/frontend/src/features/admin/AdminBoss.jsx @@ -0,0 +1,10 @@ +export default function AdminBoss() { + return ( +
+

보스 수익 계산기 관리

+
+ 준비 중 +
+
+ ) +} diff --git a/frontend/src/features/admin/AdminHome.jsx b/frontend/src/features/admin/AdminHome.jsx new file mode 100644 index 0000000..307bed5 --- /dev/null +++ b/frontend/src/features/admin/AdminHome.jsx @@ -0,0 +1,116 @@ +import { useState, useEffect } from 'react' +import { Link } from 'react-router-dom' +import { api } from '../../api/client' + +function MenuCard({ menu }) { + return ( + +
+ +
+
+ {menu.image_url ? ( + {menu.title} + ) : ( + menu.icon || '📋' + )} +
+
+

{menu.title}

+

{menu.description}

+
+
+ → +
+
+ + ) +} + +function AddCard({ to, icon, label }) { + return ( + +
+ {icon} +
+ {label} + + ) +} + +export default function AdminHome() { + const [menus, setMenus] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + // TODO: 백엔드 구현 후 실제 API 호출 + api('/api/admin/menus') + .then(setMenus) + .catch(() => setMenus([])) + .finally(() => setLoading(false)) + }, []) + + return ( +
+ {/* 메뉴 섹션 */} +
+
+
+

기능 관리

+

메뉴 항목을 추가하거나 관리합니다

+
+
+ +
+ {loading ? ( + Array.from({ length: 3 }).map((_, i) => ( +
+ )) + ) : ( + <> + {menus.map((menu) => ( + + ))} + + + )} +
+
+ + {/* 자원 관리 섹션 */} +
+
+

자원 관리

+

공용 이미지 등 사이트 자원을 관리합니다

+
+ +
+ +
+
+
+ 🖼️ +
+
+

이미지 관리

+

공용 이미지 업로드 및 관리

+
+
+ → +
+
+ +
+
+
+ ) +} diff --git a/frontend/src/features/admin/AdminImages.jsx b/frontend/src/features/admin/AdminImages.jsx new file mode 100644 index 0000000..e5ed85c --- /dev/null +++ b/frontend/src/features/admin/AdminImages.jsx @@ -0,0 +1,13 @@ +export default function AdminImages() { + return ( +
+
+

이미지 관리

+

공용 이미지를 업로드하고 관리합니다

+
+
+ 준비 중 +
+
+ ) +} diff --git a/frontend/src/features/admin/AdminLayout.jsx b/frontend/src/features/admin/AdminLayout.jsx new file mode 100644 index 0000000..45a97f8 --- /dev/null +++ b/frontend/src/features/admin/AdminLayout.jsx @@ -0,0 +1,73 @@ +import { useState, useEffect } from 'react' +import { useSearchParams, Outlet, Navigate, Link, useLocation } from 'react-router-dom' +import { api } from '../../api/client' + +export default function AdminLayout() { + const [searchParams] = useSearchParams() + const [verified, setVerified] = useState(null) + const location = useLocation() + const isRoot = location.pathname === '/admin' || location.pathname === '/admin/' + + useEffect(() => { + const keyFromUrl = searchParams.get('key') + const keyFromStorage = localStorage.getItem('maple-admin-key') + const key = keyFromUrl || keyFromStorage + + if (!key) { + setVerified(false) + return + } + + api('/api/admin/verify', { method: 'POST', body: { key } }) + .then(() => { + localStorage.setItem('maple-admin-key', key) + setVerified(true) + }) + .catch(() => { + localStorage.removeItem('maple-admin-key') + setVerified(false) + }) + }, [searchParams]) + + if (verified === null) { + return ( +
+
+
+ ) + } + + if (!verified) { + return + } + + return ( +
+
+
+ {!isRoot && ( + + ← + + )} +
+
Admin
+

관리자

+
+
+ +
+ + +
+ ) +} diff --git a/frontend/src/features/admin/AdminMenuForm.jsx b/frontend/src/features/admin/AdminMenuForm.jsx new file mode 100644 index 0000000..f0c194a --- /dev/null +++ b/frontend/src/features/admin/AdminMenuForm.jsx @@ -0,0 +1,13 @@ +export default function AdminMenuForm() { + return ( +
+
+

메뉴 항목 추가

+

새 기능 카드를 추가합니다

+
+
+ 준비 중 +
+
+ ) +} diff --git a/frontend/src/features/boss/BossPage.jsx b/frontend/src/features/boss/BossPage.jsx deleted file mode 100644 index 69d71d2..0000000 --- a/frontend/src/features/boss/BossPage.jsx +++ /dev/null @@ -1,391 +0,0 @@ -import { useState } from 'react' -import { api } from '../../api/client' - -const DIFF_KEYS = { '이지': 'easy', '노말': 'normal', '하드': 'hard', '카오스': 'chaos', '익스트림': 'extreme' } -const DIFF_COLORS = { - '이지': 'text-green-400 border-green-400/30 bg-green-400/10', - '노말': 'text-gray-300 border-gray-500/30 bg-gray-500/10', - '하드': 'text-rose-400 border-rose-400/30 bg-rose-400/10', - '카오스': 'text-amber-400 border-amber-400/30 bg-amber-400/10', - '익스트림': 'text-red-500 border-red-500/30 bg-red-500/10', -} - -const DUMMY_BOSSES = [ - { - id: 1, name: '자쿰', imgId: 1, - difficulties: [ - { name: '이지', crystal: 6_612_500, maxParty: 1 }, - { name: '노말', crystal: 16_200_000, maxParty: 1 }, - { name: '카오스', crystal: 81_000_000, maxParty: 1 }, - ], - }, - { - id: 2, name: '힐라', imgId: 3, - difficulties: [ - { name: '노말', crystal: 6_612_500, maxParty: 1 }, - { name: '하드', crystal: 56_250_000, maxParty: 1 }, - ], - }, - { - id: 3, name: '매그너스', imgId: 10, - difficulties: [ - { name: '이지', crystal: 7_200_000, maxParty: 1 }, - { name: '노말', crystal: 19_012_500, maxParty: 1 }, - { name: '하드', crystal: 95_062_500, maxParty: 1 }, - ], - }, - { - id: 4, name: '파풀라투스', imgId: 22, - difficulties: [ - { name: '이지', crystal: 4_012_500, maxParty: 1 }, - { name: '노말', crystal: 13_012_500, maxParty: 1 }, - { name: '카오스', crystal: 79_012_500, maxParty: 1 }, - ], - }, - { - id: 5, name: '듄켈', imgId: 27, - difficulties: [ - { name: '노말', crystal: 92_450_000, maxParty: 1 }, - { name: '하드', crystal: 231_125_000, maxParty: 6 }, - ], - }, - { - id: 6, name: '림보', imgId: 33, - difficulties: [ - { name: '노말', crystal: 140_000_000, maxParty: 1 }, - { name: '하드', crystal: 350_000_000, maxParty: 6 }, - ], - }, -] - -function formatMeso(n) { - if (n >= 100_000_000) { - const uk = Math.floor(n / 100_000_000) - const man = Math.floor((n % 100_000_000) / 10_000) - return man > 0 ? `${uk}억 ${man.toLocaleString()}만` : `${uk}억` - } - if (n >= 10_000) return `${Math.floor(n / 10_000).toLocaleString()}만` - return n.toLocaleString() -} - -/* ── 좌측: 캐릭터 패널 ── */ -function CharacterPanel({ characters, selectedChar, onSelect, onAdd, onRemove }) { - const [name, setName] = useState('') - const [loading, setLoading] = useState(false) - const [error, setError] = useState('') - - const handleSearch = async (e) => { - e.preventDefault() - if (!name.trim()) return - setLoading(true) - setError('') - try { - const data = await api(`/api/characters/search?name=${encodeURIComponent(name.trim())}`) - onAdd(data) - setName('') - } catch (err) { - setError(err.message) - } finally { - setLoading(false) - } - } - - return ( -
-

1. 캐릭터 등록

-
- setName(e.target.value)} - placeholder="닉네임 입력" - className="flex-1 min-w-0 rounded border border-gray-700 bg-gray-900 px-3 py-1.5 text-sm outline-none focus:border-emerald-500 transition" - /> - -
- {error &&

{error}

} - -
- {characters.map((char) => ( -
onSelect(char.character_name)} - className={`flex items-center gap-2 rounded-lg px-2 py-2 cursor-pointer transition group ${ - selectedChar === char.character_name - ? 'bg-emerald-500/10 border border-emerald-500/50' - : 'hover:bg-gray-800/50 border border-transparent' - }`} - > - {char.character_image && ( - - )} -
-
{char.character_name}
-
Lv.{char.character_level} {char.job_name}
-
- { e.stopPropagation(); onRemove(char.character_name) }} - className="text-gray-700 hover:text-red-400 opacity-0 group-hover:opacity-100 transition cursor-pointer text-lg" - > - × - -
- ))} -
-
- ) -} - -/* ── 중앙: 보스 선택 패널 ── */ -function BossPanel({ selectedChar, selections, onChange }) { - if (!selectedChar) { - return ( -
- 캐릭터를 선택해주세요 -
- ) - } - - return ( -
-

2. 보스 선택

- -
- {/* 헤더 */} -
-
보스
-
난이도
-
파티원 수
-
수익
-
- - {/* 보스 행 */} -
- {DUMMY_BOSSES.map((boss) => { - // 현재 캐릭터에서 이 보스의 선택된 난이도 찾기 - const selectedDiffIdx = boss.difficulties.findIndex((_, i) => { - const key = `${boss.id}-${i}` - return selections[key]?.enabled - }) - const sel = selectedDiffIdx >= 0 ? selections[`${boss.id}-${selectedDiffIdx}`] : null - const diff = selectedDiffIdx >= 0 ? boss.difficulties[selectedDiffIdx] : null - const isSelected = !!sel?.enabled - - return ( -
- {/* 보스 이름 + 아이콘 */} -
- {boss.name} - {boss.name} -
- - {/* 난이도 선택 */} -
- {boss.difficulties.map((d, i) => { - const key = `${boss.id}-${i}` - const active = selections[key]?.enabled - return ( - - ) - })} -
- - {/* 파티원 수 */} -
- {isSelected && ( - - )} -
- - {/* 수익 */} -
- {isSelected ? formatMeso(Math.floor(diff.crystal / sel.party)) : '-'} -
-
- ) - })} -
-
-
- ) -} - -/* ── 우측: 결과 패널 ── */ -function ResultPanel({ characters, allSelections }) { - let totalCrystals = 0 - let totalRevenue = 0 - - const charResults = characters.map((char) => { - const charSel = allSelections[char.character_name] || {} - let crystals = 0 - let revenue = 0 - - Object.entries(charSel).forEach(([key, sel]) => { - if (!sel.enabled) return - const [bossId, diffIdx] = key.split('-').map(Number) - const boss = DUMMY_BOSSES.find((b) => b.id === bossId) - if (!boss) return - crystals++ - revenue += Math.floor(boss.difficulties[diffIdx].crystal / sel.party) - }) - - totalCrystals += crystals - totalRevenue += revenue - - return { name: char.character_name, crystals, revenue } - }) - - return ( -
-

3. 결과

- -
- {/* 합산 */} -
-
- 보유 결정석 -
{totalCrystals}/90
-
-
- 총 수익 -
{formatMeso(totalRevenue)}
-
메소
-
-
- - {/* 결정석 게이지 */} -
-
-
- - {/* 캐릭터별 소계 */} - {charResults.length > 0 && ( -
-
캐릭터별
- {charResults.map((r) => ( -
- {r.name} -
- {r.crystals}/12 - 0 ? 'text-green-400' : 'text-gray-600'}>{r.revenue > 0 ? formatMeso(r.revenue) : '-'} -
-
- ))} -
- )} -
-
- ) -} - -/* ── 메인 ── */ -export default function BossPage() { - const [characters, setCharacters] = useState(() => { - const saved = localStorage.getItem('maple-characters') - return saved ? JSON.parse(saved) : [] - }) - const [selectedChar, setSelectedChar] = useState(null) - const [allSelections, setAllSelections] = useState(() => { - const saved = localStorage.getItem('maple-boss-selections') - return saved ? JSON.parse(saved) : {} - }) - - const saveCharacters = (chars) => { - setCharacters(chars) - localStorage.setItem('maple-characters', JSON.stringify(chars)) - } - - const saveSelections = (sels) => { - setAllSelections(sels) - localStorage.setItem('maple-boss-selections', JSON.stringify(sels)) - } - - const handleAddCharacter = (charData) => { - if (characters.find((c) => c.character_name === charData.character_name)) return - saveCharacters([...characters, charData]) - setSelectedChar(charData.character_name) - } - - const handleRemoveCharacter = (name) => { - saveCharacters(characters.filter((c) => c.character_name !== name)) - if (selectedChar === name) setSelectedChar(null) - const newSelections = { ...allSelections } - delete newSelections[name] - saveSelections(newSelections) - } - - const handleBossChange = (charSelections) => { - if (!selectedChar) return - saveSelections({ ...allSelections, [selectedChar]: charSelections }) - } - - const currentSelections = selectedChar ? (allSelections[selectedChar] || {}) : {} - - return ( -
- {/* 좌측 */} -
- -
- - {/* 중앙 */} -
- -
- - {/* 우측 */} -
- -
-
- ) -} diff --git a/frontend/src/index.css b/frontend/src/index.css index f1d8c73..15b0904 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1 +1,16 @@ @import "tailwindcss"; + +@theme { + --font-sans: "Maplestory", "Noto Sans KR", system-ui, -apple-system, sans-serif; + --font-maple: "Maplestory", "Noto Sans KR", sans-serif; +} + +html { + font-family: "Maplestory", "Noto Sans KR", system-ui, sans-serif; +} + +body { + font-feature-settings: "ss01", "ss02"; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx deleted file mode 100644 index ccff0d8..0000000 --- a/frontend/src/pages/Admin.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useState, useEffect } from 'react' -import { useSearchParams, Navigate } from 'react-router-dom' -import { api } from '../api/client' - -export default function Admin() { - const [searchParams] = useSearchParams() - const [verified, setVerified] = useState(null) // null=로딩, true=인증됨, false=실패 - - useEffect(() => { - const keyFromUrl = searchParams.get('key') - const keyFromStorage = localStorage.getItem('maple-admin-key') - const key = keyFromUrl || keyFromStorage - - if (!key) { - setVerified(false) - return - } - - api('/api/admin/verify', { method: 'POST', body: { key } }) - .then(() => { - localStorage.setItem('maple-admin-key', key) - setVerified(true) - }) - .catch(() => { - localStorage.removeItem('maple-admin-key') - setVerified(false) - }) - }, [searchParams]) - - if (verified === null) { - return
인증 중...
- } - - if (!verified) { - return - } - - return ( -
-
-

관리자

- -
- -
- 관리자 페이지 준비 중 -
-
- ) -} diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 6249cb8..064176d 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,22 +1,74 @@ +import { useState, useEffect } from 'react' import { Link } from 'react-router-dom' +import { api } from '../api/client' export default function Home() { + const [menus, setMenus] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + api('/api/menus') + .then(setMenus) + .catch(() => setMenus([])) + .finally(() => setLoading(false)) + }, []) + return ( -
-

메이플스토리 도우미

-

메이플스토리 유틸리티 모음

-
- - 💎 -
-

주간 보스 수익 계산기

-

캐릭터별 보스 결정석 수익을 계산합니다

+
+ {/* Hero */} +
+
+ + MapleStory Utility +
+

+ 메이플스토리 유틸리티 +

+

+ 메이플스토리 플레이를 위한 유용한 도구 모음 +

+
+ + {/* 메뉴 그리드 */} +
+ {loading ? ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ ))}
- -
+ ) : menus.length === 0 ? ( +
+
🍁
+

아직 등록된 기능이 없습니다

+
+ ) : ( +
+ {menus.map((menu) => ( + +
+
+
+ {menu.image_url ? ( + {menu.title} + ) : ( + menu.icon || '📋' + )} +
+
+

{menu.title}

+

{menu.description}

+
+
+ + ))} +
+ )} +
) }