feat(mobile): admin 메뉴 토글 + 발주 카드/리스트 뷰 + admin-panel PC 전용 안내
모바일 더보기에 admin 메뉴 전환 기능, 발주 페이지에 카드/리스트 뷰 선택,
admin-panel 페이지의 모바일 진입 가드를 함께 추가. PC 동작 영향 없음.
- /m/more: user.isAdmin 인 경우에만 [관리자 메뉴 보기] 토글 노출.
ON 시 menu_info.menu_type=0 그룹(권한/부서/사용자/공통코드 등)으로 메뉴바 전환.
사용자 카드 색도 amber 로 바뀌어 현재 모드를 시각적으로 표시. 다시 누르면
사용자 메뉴(거래처 주문/마스터/매입/출고/통계)로 복귀. 모드는 localStorage
("momo_admin_mode") 에 저장되어 새로고침 후에도 유지.
- /m/orders/new: 카드 뷰(2열 그리드)와 리스트 뷰(가로 한 줄 + 썸네일)를 토글로
전환. PC 트리·모바일 트리 양쪽에 동일 적용, 같은 viewMode state 공유. 사용자
선택은 localStorage ("momo_orders_new_view_mode") 에 저장.
- /admin-panel: 모바일(md 미만) 진입 시 안내 화면(PC에서 사용해주세요 + 모바일
메뉴 복귀 링크) 노출. md+ 에서는 기존 데스크탑 패널 그대로. /m/more 의
관리자 메뉴 항목들이 admin-panel 로 이동할 때 모바일에서 화면 깨짐 방지.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
LogOut, ChevronRight,
|
||||
LogOut, ChevronRight, ShieldCheck, ShieldOff,
|
||||
LayoutDashboard, TrendingUp, FolderKanban, Package, ListChecks,
|
||||
ShoppingCart, FileText, Warehouse, Boxes, Factory, Wrench,
|
||||
Headset, Clock, Calculator, Coins, Truck, Settings,
|
||||
@@ -38,10 +38,24 @@ interface MenuGroup {
|
||||
children: { objid: string; name: string; href: string }[];
|
||||
}
|
||||
|
||||
const ADMIN_MODE_KEY = "momo_admin_mode";
|
||||
|
||||
export default function MorePage() {
|
||||
const { user, logout } = useAuthStore();
|
||||
const [companyName, setCompanyName] = useState<string | null>(null);
|
||||
const [groups, setGroups] = useState<MenuGroup[]>([]);
|
||||
// 모드별로 메뉴 그룹을 따로 들고 토글로 노출 전환.
|
||||
const [userGroups, setUserGroups] = useState<MenuGroup[]>([]);
|
||||
const [adminGroups, setAdminGroups] = useState<MenuGroup[]>([]);
|
||||
// 'user' | 'admin' — admin 만 토글 가능. 새로고침 후 유지를 위해 localStorage 동기화.
|
||||
const [mode, setMode] = useState<"user" | "admin">("user");
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
// 모드 초기 로드 (localStorage)
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const saved = window.localStorage.getItem(ADMIN_MODE_KEY);
|
||||
if (saved === "admin") setMode("admin");
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/auth/profile")
|
||||
@@ -53,29 +67,32 @@ export default function MorePage() {
|
||||
}, []);
|
||||
|
||||
// PC 사이드바와 동일한 소스(DB MENU_INFO) 에서 메뉴 트리를 받아온다.
|
||||
// 구조: 최상위(top, parent=0) → 중간(parents, level 1) → 리프(children, level 2)
|
||||
// 모바일 더보기는 중간을 그룹 헤더, 리프를 클릭 가능한 메뉴 항목으로 노출.
|
||||
// "관리자" top menu 의 자식 = 시스템 관리(menu_type=0) 메뉴들 → adminGroups
|
||||
// 그 외 top menu 들의 자식 = 운영 메뉴(menu_type=1) → userGroups
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
const topRes = await fetch("/api/menu/top");
|
||||
if (!topRes.ok) return;
|
||||
if (!topRes.ok) {
|
||||
setLoaded(true);
|
||||
return;
|
||||
}
|
||||
const topJson = await topRes.json();
|
||||
const tops: { OBJID: string; MENU_NAME_KOR: string }[] = topJson.menus || [];
|
||||
|
||||
const all: MenuGroup[] = [];
|
||||
for (const t of tops) {
|
||||
const buildGroupsFor = async (topObjId: string): Promise<MenuGroup[]> => {
|
||||
const sideRes = await fetch("/api/menu", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ MENUOBJID: t.OBJID }),
|
||||
body: JSON.stringify({ MENUOBJID: topObjId }),
|
||||
});
|
||||
if (!sideRes.ok) continue;
|
||||
if (!sideRes.ok) return [];
|
||||
const sideJson = await sideRes.json();
|
||||
const items = (sideJson.RESULT || []) as MenuItem[];
|
||||
|
||||
const parents = items.filter((i) => i.level === "1" || Number(i.level) < 2);
|
||||
const children = items.filter((i) => i.level !== "1" && Number(i.level) >= 2);
|
||||
|
||||
const result: MenuGroup[] = [];
|
||||
for (const p of parents) {
|
||||
const kids = children.filter((c) => c.parentObjId === p.objid);
|
||||
// 동일 이름 중복 제거 (sidebar 와 동일 규칙)
|
||||
@@ -86,7 +103,7 @@ export default function MorePage() {
|
||||
return true;
|
||||
});
|
||||
if (uniqueKids.length === 0) continue;
|
||||
all.push({
|
||||
result.push({
|
||||
objid: p.objid,
|
||||
name: p.menuNameKor,
|
||||
children: uniqueKids.map((c) => ({
|
||||
@@ -96,8 +113,33 @@ export default function MorePage() {
|
||||
})),
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const adminTop = tops.find((m) => m.MENU_NAME_KOR === "관리자");
|
||||
const userTops = tops.filter((m) => m.MENU_NAME_KOR !== "관리자");
|
||||
|
||||
const userResults: MenuGroup[] = [];
|
||||
for (const t of userTops) {
|
||||
const g = await buildGroupsFor(t.OBJID);
|
||||
userResults.push(...g);
|
||||
}
|
||||
setGroups(all);
|
||||
setUserGroups(userResults);
|
||||
|
||||
if (adminTop) {
|
||||
// 관리자 메뉴 항목은 /admin-panel?menuId={리프 menu objid} 로 통일.
|
||||
// (admin-panel 페이지가 menuId 쿼리에 따라 내부 탭 전환)
|
||||
const raw = await buildGroupsFor(adminTop.OBJID);
|
||||
const remapped: MenuGroup[] = raw.map((g) => ({
|
||||
...g,
|
||||
children: g.children.map((c) => ({
|
||||
...c,
|
||||
href: `/admin-panel?menuId=${c.objid}`,
|
||||
})),
|
||||
}));
|
||||
setAdminGroups(remapped);
|
||||
}
|
||||
setLoaded(true);
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
@@ -115,18 +157,96 @@ export default function MorePage() {
|
||||
await logout();
|
||||
};
|
||||
|
||||
const toggleMode = () => {
|
||||
setMode((prev) => {
|
||||
const next = prev === "admin" ? "user" : "admin";
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.setItem(ADMIN_MODE_KEY, next);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const canShowAdminToggle = user?.isAdmin && adminGroups.length > 0;
|
||||
const groups = mode === "admin" ? adminGroups : userGroups;
|
||||
const isAdminMode = mode === "admin";
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="bg-gradient-to-br from-emerald-700 to-emerald-600 rounded-2xl px-5 py-6 text-white shadow-md">
|
||||
{/* 사용자 카드 — 관리자 모드일 땐 amber 톤으로 시각 구분 */}
|
||||
<div
|
||||
className={
|
||||
isAdminMode
|
||||
? "bg-gradient-to-br from-amber-600 to-amber-500 rounded-2xl px-5 py-6 text-white shadow-md"
|
||||
: "bg-gradient-to-br from-emerald-700 to-emerald-600 rounded-2xl px-5 py-6 text-white shadow-md"
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm mb-1 opacity-90">
|
||||
{isAdminMode ? (
|
||||
<>
|
||||
<ShieldCheck size={16} />
|
||||
<span className="font-semibold tracking-wide">관리자 모드</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-2xl font-bold tracking-tight">
|
||||
{companyName ?? user?.userName ?? "사용자"}님
|
||||
</div>
|
||||
<div className="text-emerald-100/90 text-sm mt-1">{user?.userId}</div>
|
||||
<div className={isAdminMode ? "text-amber-50/90 text-sm mt-1" : "text-emerald-100/90 text-sm mt-1"}>
|
||||
{user?.userId}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{groups.length === 0 ? (
|
||||
{/*
|
||||
관리자 토글 버튼 — user.isAdmin 이고 관리자 메뉴 그룹이 있을 때만 노출.
|
||||
ON 일 땐 메뉴바가 관리자 메뉴(권한·사용자·기준정보 관리 등)로 교체.
|
||||
*/}
|
||||
{canShowAdminToggle && (
|
||||
<button
|
||||
onClick={toggleMode}
|
||||
className={
|
||||
"w-full flex items-center justify-between px-5 h-16 rounded-2xl border-2 transition shadow-sm " +
|
||||
(isAdminMode
|
||||
? "bg-emerald-50 border-emerald-300 active:bg-emerald-100"
|
||||
: "bg-amber-50 border-amber-300 active:bg-amber-100")
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{isAdminMode ? (
|
||||
<ShieldOff size={24} className="text-emerald-700" />
|
||||
) : (
|
||||
<ShieldCheck size={24} className="text-amber-600" />
|
||||
)}
|
||||
<div className="text-left">
|
||||
<div
|
||||
className={
|
||||
"text-base font-extrabold " +
|
||||
(isAdminMode ? "text-emerald-800" : "text-amber-800")
|
||||
}
|
||||
>
|
||||
{isAdminMode ? "사용자 메뉴로 돌아가기" : "관리자 메뉴 보기"}
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"text-[11px] " +
|
||||
(isAdminMode ? "text-emerald-700/80" : "text-amber-700/80")
|
||||
}
|
||||
>
|
||||
{isAdminMode
|
||||
? "거래처 주문 · 마스터 · 매입 · 출고 · 통계"
|
||||
: "권한 · 부서 · 사용자 · 기준정보 관리"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight size={20} className={isAdminMode ? "text-emerald-500" : "text-amber-500"} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!loaded ? (
|
||||
<div className="text-slate-400 text-sm text-center py-8">메뉴를 불러오는 중...</div>
|
||||
) : groups.length === 0 ? (
|
||||
<div className="text-slate-400 text-sm text-center py-8">
|
||||
메뉴를 불러오는 중...
|
||||
{isAdminMode ? "관리자 메뉴가 없습니다." : "표시할 메뉴가 없습니다."}
|
||||
</div>
|
||||
) : (
|
||||
groups.map((group) => (
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Search, ShoppingCart, Plus, Minus, X } from "lucide-react";
|
||||
import { Search, ShoppingCart, Plus, Minus, X, LayoutGrid, List } from "lucide-react";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
interface Item {
|
||||
@@ -21,6 +21,9 @@ interface CartLine { item: Item; qty: number }
|
||||
|
||||
const fmt = (n: number) => Number(n || 0).toLocaleString("ko-KR");
|
||||
|
||||
// 모바일 발주 페이지의 뷰 모드 (card/list) 사용자 선택 기억
|
||||
const VIEW_MODE_KEY = "momo_orders_new_view_mode";
|
||||
|
||||
// PC 와 모바일에 서로 다른 디자인을 보여주지만, state(cart, items 등)는 부모에서 한 번만 관리.
|
||||
// 두 트리는 hidden md:flex / md:hidden 로 분기되며, 보이는 한 쪽만 사용자가 만진다.
|
||||
export default function ItemsBrowse() {
|
||||
@@ -31,6 +34,19 @@ export default function ItemsBrowse() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cart, setCart] = useState<CartLine[]>([]);
|
||||
const [cartOpen, setCartOpen] = useState(false);
|
||||
// 모바일 뷰 모드 — card(기본, 2열 그리드) / list(가로 한 줄)
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("card");
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const saved = window.localStorage.getItem(VIEW_MODE_KEY);
|
||||
if (saved === "list" || saved === "card") setViewMode(saved);
|
||||
}, []);
|
||||
|
||||
const changeViewMode = (m: "card" | "list") => {
|
||||
setViewMode(m);
|
||||
if (typeof window !== "undefined") window.localStorage.setItem(VIEW_MODE_KEY, m);
|
||||
};
|
||||
|
||||
const fetchItems = async () => {
|
||||
setLoading(true);
|
||||
@@ -256,11 +272,44 @@ export default function ItemsBrowse() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 카드 / 리스트 뷰 토글 — PC. viewMode state 는 모바일 트리와 공유 */}
|
||||
{!loading && items.length > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-slate-500">총 {items.length}개</div>
|
||||
<div className="flex bg-slate-100 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => changeViewMode("card")}
|
||||
className={
|
||||
"h-8 px-3 rounded-md flex items-center gap-1.5 text-xs font-bold transition " +
|
||||
(viewMode === "card"
|
||||
? "bg-white text-emerald-700 shadow-sm"
|
||||
: "text-slate-500 hover:text-slate-700")
|
||||
}
|
||||
aria-pressed={viewMode === "card"}
|
||||
>
|
||||
<LayoutGrid size={14} strokeWidth={2.4} /> 카드
|
||||
</button>
|
||||
<button
|
||||
onClick={() => changeViewMode("list")}
|
||||
className={
|
||||
"h-8 px-3 rounded-md flex items-center gap-1.5 text-xs font-bold transition " +
|
||||
(viewMode === "list"
|
||||
? "bg-white text-emerald-700 shadow-sm"
|
||||
: "text-slate-500 hover:text-slate-700")
|
||||
}
|
||||
aria-pressed={viewMode === "list"}
|
||||
>
|
||||
<List size={14} strokeWidth={2.4} /> 리스트
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="text-slate-400 text-center py-12">불러오는 중...</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="text-slate-400 text-center py-12 bg-white rounded-xl border border-slate-100">검색 결과가 없습니다.</div>
|
||||
) : (
|
||||
) : viewMode === "card" ? (
|
||||
<div className="grid sm:grid-cols-2 xl:grid-cols-3 gap-3">
|
||||
{items.map((it) => (
|
||||
<div key={it.OBJID} className="bg-white border border-slate-200 rounded-xl p-4 hover:shadow-md transition">
|
||||
@@ -295,6 +344,47 @@ export default function ItemsBrowse() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
/* PC 리스트 뷰 — 가로 한 줄. 여유 공간이 더 많으니 정보를 펼쳐 표시. */
|
||||
<div className="space-y-2">
|
||||
{items.map((it) => (
|
||||
<div
|
||||
key={it.OBJID}
|
||||
className="flex items-center gap-4 bg-white border border-slate-200 rounded-xl p-3 hover:shadow-md transition"
|
||||
>
|
||||
<div className="w-16 h-16 rounded-lg bg-slate-50 overflow-hidden flex items-center justify-center shrink-0">
|
||||
{it.IMAGE_URL ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={it.IMAGE_URL} alt={it.ITEM_NAME} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="text-slate-300 text-[10px]">이미지 없음</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-0.5">
|
||||
<div className="font-bold text-sm text-slate-900 truncate">{it.ITEM_NAME}</div>
|
||||
{it.IS_TAX_FREE === "Y" && (
|
||||
<span className="shrink-0 px-1.5 py-0.5 rounded bg-violet-100 text-violet-700 text-[10px] font-bold">면세</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 truncate">{it.MAKER_NAME || "-"}</div>
|
||||
</div>
|
||||
<div className="text-right shrink-0 w-32">
|
||||
<div className="font-bold text-base text-slate-900 tabular-nums">₩{fmt(it.UNIT_PRICE)}</div>
|
||||
<div className={`text-xs font-semibold ${Number(it.STOCK_QTY) > 0 ? "text-emerald-700" : "text-rose-500"}`}>
|
||||
재고 {fmt(it.STOCK_QTY)} {it.UNIT}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
disabled={Number(it.STOCK_QTY) === 0}
|
||||
onClick={() => addToCart(it)}
|
||||
className="h-10 px-4 rounded-lg bg-emerald-700 text-white text-xs font-bold hover:bg-emerald-800 disabled:bg-slate-200 disabled:text-slate-400 disabled:cursor-not-allowed flex items-center justify-center gap-1 shrink-0"
|
||||
>
|
||||
<Plus size={14} /> 담기
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -324,13 +414,46 @@ export default function ItemsBrowse() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 카드 / 리스트 뷰 모드 토글 — 결과 개수와 함께 같은 줄에 배치 */}
|
||||
{!loading && items.length > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-slate-500">총 {items.length}개</div>
|
||||
<div className="flex bg-slate-100 rounded-xl p-1">
|
||||
<button
|
||||
onClick={() => changeViewMode("card")}
|
||||
className={
|
||||
"h-10 px-3 rounded-lg flex items-center gap-1.5 text-sm font-bold transition " +
|
||||
(viewMode === "card"
|
||||
? "bg-white text-emerald-700 shadow-sm"
|
||||
: "text-slate-500 active:text-slate-700")
|
||||
}
|
||||
aria-pressed={viewMode === "card"}
|
||||
>
|
||||
<LayoutGrid size={16} strokeWidth={2.4} /> 카드
|
||||
</button>
|
||||
<button
|
||||
onClick={() => changeViewMode("list")}
|
||||
className={
|
||||
"h-10 px-3 rounded-lg flex items-center gap-1.5 text-sm font-bold transition " +
|
||||
(viewMode === "list"
|
||||
? "bg-white text-emerald-700 shadow-sm"
|
||||
: "text-slate-500 active:text-slate-700")
|
||||
}
|
||||
aria-pressed={viewMode === "list"}
|
||||
>
|
||||
<List size={16} strokeWidth={2.4} /> 리스트
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="text-slate-400 text-center py-16">불러오는 중...</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="text-slate-400 text-center py-16 bg-white rounded-xl border border-slate-100">
|
||||
검색 결과가 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
) : viewMode === "card" ? (
|
||||
<div className="grid grid-cols-2 gap-3 pb-32">
|
||||
{items.map((it) => (
|
||||
<div key={it.OBJID} className="bg-white border border-slate-200 rounded-2xl p-3 flex flex-col">
|
||||
@@ -362,6 +485,53 @@ export default function ItemsBrowse() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
/* 리스트 뷰 — 가로 한 줄, 썸네일+정보+담기 버튼 */
|
||||
<div className="space-y-2 pb-32">
|
||||
{items.map((it) => (
|
||||
<div
|
||||
key={it.OBJID}
|
||||
className="flex items-center gap-3 bg-white border border-slate-200 rounded-2xl p-3"
|
||||
>
|
||||
<div className="w-20 h-20 rounded-xl bg-slate-50 overflow-hidden flex items-center justify-center shrink-0">
|
||||
{it.IMAGE_URL ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={it.IMAGE_URL} alt={it.ITEM_NAME} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="text-slate-300 text-[10px] text-center px-1">이미지 없음</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-bold text-base text-slate-900 leading-snug line-clamp-1">
|
||||
{it.ITEM_NAME}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 truncate mt-0.5">{it.MAKER_NAME || "-"}</div>
|
||||
<div className="flex items-baseline gap-2 mt-1.5">
|
||||
<div className="font-extrabold text-slate-900 tabular-nums text-lg">
|
||||
₩{fmt(it.UNIT_PRICE)}
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"text-xs font-semibold tabular-nums " +
|
||||
(Number(it.STOCK_QTY) > 0 ? "text-emerald-700" : "text-rose-500")
|
||||
}
|
||||
>
|
||||
{Number(it.STOCK_QTY) > 0
|
||||
? `재고 ${fmt(it.STOCK_QTY)}${it.UNIT || ""}`
|
||||
: "재고 없음"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
disabled={Number(it.STOCK_QTY) === 0}
|
||||
onClick={() => addToCart(it)}
|
||||
className="h-12 px-4 rounded-xl bg-emerald-700 text-white text-sm font-bold active:bg-emerald-800 disabled:bg-slate-200 disabled:text-slate-400 disabled:cursor-not-allowed flex items-center justify-center gap-1 shrink-0"
|
||||
>
|
||||
<Plus size={16} strokeWidth={2.5} /> 담기
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 모바일 floating 카트 바 (BottomNav 위에 위치) */}
|
||||
|
||||
@@ -121,7 +121,14 @@ export default function AdminPanelPage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-100">
|
||||
<>
|
||||
{/*
|
||||
모바일(md 미만)에서 진입했을 때 안내 — admin-panel 자체는 1630×950 데스크탑 팝업 전용.
|
||||
PC 동작에는 영향 없음 (md 이상에서는 hidden).
|
||||
*/}
|
||||
<MobilePcOnlyNotice />
|
||||
|
||||
<div className="hidden md:flex h-screen bg-gray-100">
|
||||
{/* 좌측 메뉴 (adminMenu.jsp 대응) */}
|
||||
<aside className="w-[220px] bg-[#2a2a2a] text-gray-300 flex flex-col shrink-0">
|
||||
<div className="px-4 py-3 border-b border-white/10">
|
||||
@@ -221,6 +228,31 @@ export default function AdminPanelPage() {
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// 모바일에서 admin-panel 진입 시 안내. md+ 에선 hidden.
|
||||
function MobilePcOnlyNotice() {
|
||||
return (
|
||||
<div className="md:hidden min-h-screen bg-slate-50 flex flex-col items-center justify-center px-6 py-10 text-center">
|
||||
<div className="w-20 h-20 rounded-full bg-amber-100 flex items-center justify-center mb-5">
|
||||
<Shield size={40} className="text-amber-600" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-extrabold text-slate-900 mb-2">관리자 패널</h1>
|
||||
<p className="text-slate-600 text-base leading-relaxed max-w-xs mb-1">
|
||||
이 기능은 <span className="font-bold">PC에서만</span> 사용할 수 있어요.
|
||||
</p>
|
||||
<p className="text-slate-500 text-sm leading-relaxed max-w-xs mb-8">
|
||||
편한 PC로 접속해서 메뉴·권한·사용자·기준정보를 관리해주세요.
|
||||
</p>
|
||||
<a
|
||||
href="/m/more"
|
||||
className="inline-flex items-center justify-center gap-2 h-12 px-6 rounded-xl bg-emerald-700 text-white text-base font-bold active:bg-emerald-800 hover:bg-emerald-800 transition shadow-sm"
|
||||
>
|
||||
← 모바일 메뉴로 돌아가기
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user