diff --git a/src/app/(mobile)/m/more/page.tsx b/src/app/(mobile)/m/more/page.tsx index 4b99d1b..69cf591 100644 --- a/src/app/(mobile)/m/more/page.tsx +++ b/src/app/(mobile)/m/more/page.tsx @@ -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(null); - const [groups, setGroups] = useState([]); + // 모드별로 메뉴 그룹을 따로 들고 토글로 노출 전환. + const [userGroups, setUserGroups] = useState([]); + const [adminGroups, setAdminGroups] = useState([]); + // '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 => { 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 (
-
+ {/* 사용자 카드 — 관리자 모드일 땐 amber 톤으로 시각 구분 */} +
+
+ {isAdminMode ? ( + <> + + 관리자 모드 + + ) : null} +
{companyName ?? user?.userName ?? "사용자"}님
-
{user?.userId}
+
+ {user?.userId} +
- {groups.length === 0 ? ( + {/* + 관리자 토글 버튼 — user.isAdmin 이고 관리자 메뉴 그룹이 있을 때만 노출. + ON 일 땐 메뉴바가 관리자 메뉴(권한·사용자·기준정보 관리 등)로 교체. + */} + {canShowAdminToggle && ( + + )} + + {!loaded ? ( +
메뉴를 불러오는 중...
+ ) : groups.length === 0 ? (
- 메뉴를 불러오는 중... + {isAdminMode ? "관리자 메뉴가 없습니다." : "표시할 메뉴가 없습니다."}
) : ( groups.map((group) => ( diff --git a/src/app/(mobile)/m/orders/new/page.tsx b/src/app/(mobile)/m/orders/new/page.tsx index 00817b3..632c076 100644 --- a/src/app/(mobile)/m/orders/new/page.tsx +++ b/src/app/(mobile)/m/orders/new/page.tsx @@ -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([]); 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() {
+ {/* 카드 / 리스트 뷰 토글 — PC. viewMode state 는 모바일 트리와 공유 */} + {!loading && items.length > 0 && ( +
+
총 {items.length}개
+
+ + +
+
+ )} + {loading ? (
불러오는 중...
) : items.length === 0 ? (
검색 결과가 없습니다.
- ) : ( + ) : viewMode === "card" ? (
{items.map((it) => (
@@ -295,6 +344,47 @@ export default function ItemsBrowse() {
))}
+ ) : ( + /* PC 리스트 뷰 — 가로 한 줄. 여유 공간이 더 많으니 정보를 펼쳐 표시. */ +
+ {items.map((it) => ( +
+
+ {it.IMAGE_URL ? ( + // eslint-disable-next-line @next/next/no-img-element + {it.ITEM_NAME} + ) : ( +
이미지 없음
+ )} +
+
+
+
{it.ITEM_NAME}
+ {it.IS_TAX_FREE === "Y" && ( + 면세 + )} +
+
{it.MAKER_NAME || "-"}
+
+
+
₩{fmt(it.UNIT_PRICE)}
+
0 ? "text-emerald-700" : "text-rose-500"}`}> + 재고 {fmt(it.STOCK_QTY)} {it.UNIT} +
+
+ +
+ ))} +
)}
@@ -324,13 +414,46 @@ export default function ItemsBrowse() { + {/* 카드 / 리스트 뷰 모드 토글 — 결과 개수와 함께 같은 줄에 배치 */} + {!loading && items.length > 0 && ( +
+
총 {items.length}개
+
+ + +
+
+ )} + {loading ? (
불러오는 중...
) : items.length === 0 ? (
검색 결과가 없습니다.
- ) : ( + ) : viewMode === "card" ? (
{items.map((it) => (
@@ -362,6 +485,53 @@ export default function ItemsBrowse() {
))}
+ ) : ( + /* 리스트 뷰 — 가로 한 줄, 썸네일+정보+담기 버튼 */ +
+ {items.map((it) => ( +
+
+ {it.IMAGE_URL ? ( + // eslint-disable-next-line @next/next/no-img-element + {it.ITEM_NAME} + ) : ( +
이미지 없음
+ )} +
+
+
+ {it.ITEM_NAME} +
+
{it.MAKER_NAME || "-"}
+
+
+ ₩{fmt(it.UNIT_PRICE)} +
+
0 ? "text-emerald-700" : "text-rose-500") + } + > + {Number(it.STOCK_QTY) > 0 + ? `재고 ${fmt(it.STOCK_QTY)}${it.UNIT || ""}` + : "재고 없음"} +
+
+
+ +
+ ))} +
)} {/* 모바일 floating 카트 바 (BottomNav 위에 위치) */} diff --git a/src/app/admin-panel/page.tsx b/src/app/admin-panel/page.tsx index 35cb9c5..c9c6201 100644 --- a/src/app/admin-panel/page.tsx +++ b/src/app/admin-panel/page.tsx @@ -121,7 +121,14 @@ export default function AdminPanelPage() { }, []); return ( -
+ <> + {/* + 모바일(md 미만)에서 진입했을 때 안내 — admin-panel 자체는 1630×950 데스크탑 팝업 전용. + PC 동작에는 영향 없음 (md 이상에서는 hidden). + */} + + +
{/* 좌측 메뉴 (adminMenu.jsp 대응) */}