diff --git a/backend-node/src/controllers/authController.ts b/backend-node/src/controllers/authController.ts index 62854788..ad66a7c2 100644 --- a/backend-node/src/controllers/authController.ts +++ b/backend-node/src/controllers/authController.ts @@ -103,7 +103,10 @@ export class AuthController { } else if (popResult.childMenus.length === 1) { popLandingPath = popResult.childMenus[0].menu_url; } else if (popResult.childMenus.length > 1) { - popLandingPath = "/pop"; + const userCompanyCode = loginResult.userInfo.companyCode; + if (userCompanyCode && userCompanyCode !== "*") { + popLandingPath = `/${userCompanyCode}/pop/main`; + } } logger.debug(`POP 랜딩 경로: ${popLandingPath}`); diff --git a/frontend/app/(main)/COMPANY_10/pop/_components/common/PopShell.tsx b/frontend/app/(main)/COMPANY_10/pop/_components/common/PopShell.tsx new file mode 100644 index 00000000..456dc0e0 --- /dev/null +++ b/frontend/app/(main)/COMPANY_10/pop/_components/common/PopShell.tsx @@ -0,0 +1,445 @@ +"use client"; + +import React, { useState, useEffect, useRef, ReactNode } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/hooks/useAuth"; +import { usePopSettings } from "@/hooks/pop/usePopSettings"; +import { CompanySwitchModal } from "@/components/pop/shell/CompanySwitchModal"; + +interface PopShellProps { + children: ReactNode; + showBanner?: boolean; + title?: string; + showBack?: boolean; + headerRight?: ReactNode; + fullBleed?: boolean; +} + +export function PopShell({ children, showBanner = true, title, showBack = false, headerRight, fullBleed = false }: PopShellProps) { + const router = useRouter(); + // 회사 고정 PopShell — popHomePath는 해당 회사 메인으로 직박 + const popHomePath = "/COMPANY_10/pop/main"; + const { user, logout, switchCompany } = useAuth(); + const displayName = user?.userName || user?.userId || "사용자"; + const deptName = user?.deptName || ""; + const initial = displayName.charAt(0); + const isSuperAdmin = user?.userType === "SUPER_ADMIN"; + const [companySwitchOpen, setCompanySwitchOpen] = useState(false); + const [mounted, setMounted] = useState(false); + const [showFullscreenSplash, setShowFullscreenSplash] = useState(false); + const [hours, setHours] = useState("00"); + const [minutes, setMinutes] = useState("00"); + const [seconds, setSeconds] = useState("00"); + const [dateStr, setDateStr] = useState("2026-01-01"); + const [colonVisible, setColonVisible] = useState(true); + const [profileOpen, setProfileOpen] = useState(false); + const profileRef = useRef(null); + + // 전체화면이 아닌 상태에서 POP 진입 시 스플래시 표시 — 세션당 1회 + useEffect(() => { + if (sessionStorage.getItem("pop-fullscreen-asked")) return; + if (!document.fullscreenElement) { + setShowFullscreenSplash(true); + sessionStorage.setItem("pop-fullscreen-asked", "1"); + } + }, []); + + const handleEnterFullscreen = async () => { + try { + if (!document.fullscreenElement) { + await document.documentElement.requestFullscreen(); + } + } catch { + // 전체화면 미지원 시 무시 + } + setShowFullscreenSplash(false); + }; + + const handleSkipFullscreen = () => { + setShowFullscreenSplash(false); + }; + + useEffect(() => { + setMounted(true); + + function tick() { + const now = new Date(); + setHours(String(now.getHours()).padStart(2, "0")); + setMinutes(String(now.getMinutes()).padStart(2, "0")); + setSeconds(String(now.getSeconds()).padStart(2, "0")); + setDateStr( + `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}` + ); + } + + tick(); + const clockInterval = setInterval(tick, 1000); + const blinkInterval = setInterval(() => { + setColonVisible((v) => !v); + }, 500); + + return () => { + clearInterval(clockInterval); + clearInterval(blinkInterval); + }; + }, []); + + // Profile dropdown: close on outside click + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (profileRef.current && !profileRef.current.contains(e.target as Node)) { + setProfileOpen(false); + } + } + if (profileOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [profileOpen]); + + const handlePcMode = async () => { + setProfileOpen(false); + if (document.fullscreenElement) { + try { await document.exitFullscreen(); } catch {} + } + router.push("/"); + }; + + const handlePopHome = () => { + setProfileOpen(false); + router.push(popHomePath); + }; + + const toggleFullscreen = async () => { + setProfileOpen(false); + try { + if (document.fullscreenElement) { + await document.exitFullscreen(); + } else { + await document.documentElement.requestFullscreen(); + } + } catch { + // fullscreen not supported + } + }; + + const handleLogout = () => { + setProfileOpen(false); + logout(); + }; + + const handleCompanySwitch = async (companyCode: string) => { + const currentCode = user?.companyCode || user?.company_code; + if (companyCode === currentCode) return; + const result = await switchCompany(companyCode); + if (result.success) { + window.location.reload(); + } else { + alert(result.message || "회사 전환에 실패했습니다."); + } + }; + + // POP 설정에서 배너 텍스트 로드 (POP화면설정에서 관리) + const { settings: popSettings } = usePopSettings(); + const homeConfig = (popSettings as any)?.screens?.home; + const bannerEnabled = homeConfig?.bannerEnabled ?? true; + const bannerText = homeConfig?.bannerText; + const marqueeText = bannerText || "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. | [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 | [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!"; + + return ( +
+ {/* ===== FULLSCREEN SPLASH ===== */} + {showFullscreenSplash && ( +
+
+
+ + + +
+
+

POP 모드

+

최적의 현장 환경을 위해 전체화면을 권장합니다

+
+
+ + +
+
+
+ )} + + {/* ===== HEADER ===== */} +
+ {/* Left: Back + Logo + Company */} +
+ {showBack && ( + + )} +
router.push(popHomePath)} + > +
+ + + +
+
+ {title ? ( + + {title} + + ) : ( + <> + + {user?.companyName || "POP"} + + + 현장 관리 시스템 + + + )} +
+
+
+ + {/* Center: Clock (desktop) */} +
+ {mounted && ( + <> +
+ {hours} + + : + + {minutes} + + : + + {seconds} +
+ {dateStr} + + )} +
+ + {/* Right: Mobile clock + Profile */} +
+ {/* Mobile clock */} + {mounted && ( +
+ + + + + {hours}:{minutes} + +
+ )} + + {/* Custom header right content (e.g. cart icon) */} + {headerRight} + + {/* 회사 전환 버튼 (최고관리자만) */} + {isSuperAdmin && ( + + )} + +
+ + {/* Profile with Dropdown */} +
+ + + {/* Profile Dropdown */} +
+ {/* User Info */} +
+

{displayName}

+

{deptName || user?.userId}

+
+ + {/* Menu Items */} +
+ + + +
+ + {/* Logout */} +
+ +
+
+
+
+
+ + {/* 회사 전환 모달 (최고관리자만) */} + {isSuperAdmin && ( + setCompanySwitchOpen(false)} + onSelect={handleCompanySwitch} + currentCompanyCode={user?.companyCode || user?.company_code} + /> + )} + + {/* ===== NOTICE BANNER (Marquee) ===== */} + {showBanner && bannerEnabled &&
+
+ 📢 + 공지 +
+
+
+ {marqueeText} +
+
+
} + + {/* ===== MAIN CONTENT ===== */} +
+ {children} +
+ + {/* FOOTER 삭제 — POP 화면에서 불필요 */} + + {/* Marquee keyframes */} + +
+ ); +} diff --git a/frontend/app/(main)/COMPANY_10/pop/layout.tsx b/frontend/app/(main)/COMPANY_10/pop/layout.tsx index 0c898212..19c421c3 100644 --- a/frontend/app/(main)/COMPANY_10/pop/layout.tsx +++ b/frontend/app/(main)/COMPANY_10/pop/layout.tsx @@ -2,7 +2,7 @@ import { usePathname } from "next/navigation"; import type { ReactNode } from "react"; -import { PopShell } from "@/components/pop/hardcoded"; +import { PopShell } from "./_components/common/PopShell"; /** * COMPANY_7 POP 전용 layout diff --git a/frontend/app/(main)/COMPANY_16/pop/_components/common/PopShell.tsx b/frontend/app/(main)/COMPANY_16/pop/_components/common/PopShell.tsx new file mode 100644 index 00000000..045607c8 --- /dev/null +++ b/frontend/app/(main)/COMPANY_16/pop/_components/common/PopShell.tsx @@ -0,0 +1,445 @@ +"use client"; + +import React, { useState, useEffect, useRef, ReactNode } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/hooks/useAuth"; +import { usePopSettings } from "@/hooks/pop/usePopSettings"; +import { CompanySwitchModal } from "@/components/pop/shell/CompanySwitchModal"; + +interface PopShellProps { + children: ReactNode; + showBanner?: boolean; + title?: string; + showBack?: boolean; + headerRight?: ReactNode; + fullBleed?: boolean; +} + +export function PopShell({ children, showBanner = true, title, showBack = false, headerRight, fullBleed = false }: PopShellProps) { + const router = useRouter(); + // 회사 고정 PopShell — popHomePath는 해당 회사 메인으로 직박 + const popHomePath = "/COMPANY_16/pop/main"; + const { user, logout, switchCompany } = useAuth(); + const displayName = user?.userName || user?.userId || "사용자"; + const deptName = user?.deptName || ""; + const initial = displayName.charAt(0); + const isSuperAdmin = user?.userType === "SUPER_ADMIN"; + const [companySwitchOpen, setCompanySwitchOpen] = useState(false); + const [mounted, setMounted] = useState(false); + const [showFullscreenSplash, setShowFullscreenSplash] = useState(false); + const [hours, setHours] = useState("00"); + const [minutes, setMinutes] = useState("00"); + const [seconds, setSeconds] = useState("00"); + const [dateStr, setDateStr] = useState("2026-01-01"); + const [colonVisible, setColonVisible] = useState(true); + const [profileOpen, setProfileOpen] = useState(false); + const profileRef = useRef(null); + + // 전체화면이 아닌 상태에서 POP 진입 시 스플래시 표시 — 세션당 1회 + useEffect(() => { + if (sessionStorage.getItem("pop-fullscreen-asked")) return; + if (!document.fullscreenElement) { + setShowFullscreenSplash(true); + sessionStorage.setItem("pop-fullscreen-asked", "1"); + } + }, []); + + const handleEnterFullscreen = async () => { + try { + if (!document.fullscreenElement) { + await document.documentElement.requestFullscreen(); + } + } catch { + // 전체화면 미지원 시 무시 + } + setShowFullscreenSplash(false); + }; + + const handleSkipFullscreen = () => { + setShowFullscreenSplash(false); + }; + + useEffect(() => { + setMounted(true); + + function tick() { + const now = new Date(); + setHours(String(now.getHours()).padStart(2, "0")); + setMinutes(String(now.getMinutes()).padStart(2, "0")); + setSeconds(String(now.getSeconds()).padStart(2, "0")); + setDateStr( + `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}` + ); + } + + tick(); + const clockInterval = setInterval(tick, 1000); + const blinkInterval = setInterval(() => { + setColonVisible((v) => !v); + }, 500); + + return () => { + clearInterval(clockInterval); + clearInterval(blinkInterval); + }; + }, []); + + // Profile dropdown: close on outside click + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (profileRef.current && !profileRef.current.contains(e.target as Node)) { + setProfileOpen(false); + } + } + if (profileOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [profileOpen]); + + const handlePcMode = async () => { + setProfileOpen(false); + if (document.fullscreenElement) { + try { await document.exitFullscreen(); } catch {} + } + router.push("/"); + }; + + const handlePopHome = () => { + setProfileOpen(false); + router.push(popHomePath); + }; + + const toggleFullscreen = async () => { + setProfileOpen(false); + try { + if (document.fullscreenElement) { + await document.exitFullscreen(); + } else { + await document.documentElement.requestFullscreen(); + } + } catch { + // fullscreen not supported + } + }; + + const handleLogout = () => { + setProfileOpen(false); + logout(); + }; + + const handleCompanySwitch = async (companyCode: string) => { + const currentCode = user?.companyCode || user?.company_code; + if (companyCode === currentCode) return; + const result = await switchCompany(companyCode); + if (result.success) { + window.location.reload(); + } else { + alert(result.message || "회사 전환에 실패했습니다."); + } + }; + + // POP 설정에서 배너 텍스트 로드 (POP화면설정에서 관리) + const { settings: popSettings } = usePopSettings(); + const homeConfig = (popSettings as any)?.screens?.home; + const bannerEnabled = homeConfig?.bannerEnabled ?? true; + const bannerText = homeConfig?.bannerText; + const marqueeText = bannerText || "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. | [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 | [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!"; + + return ( +
+ {/* ===== FULLSCREEN SPLASH ===== */} + {showFullscreenSplash && ( +
+
+
+ + + +
+
+

POP 모드

+

최적의 현장 환경을 위해 전체화면을 권장합니다

+
+
+ + +
+
+
+ )} + + {/* ===== HEADER ===== */} +
+ {/* Left: Back + Logo + Company */} +
+ {showBack && ( + + )} +
router.push(popHomePath)} + > +
+ + + +
+
+ {title ? ( + + {title} + + ) : ( + <> + + {user?.companyName || "POP"} + + + 현장 관리 시스템 + + + )} +
+
+
+ + {/* Center: Clock (desktop) */} +
+ {mounted && ( + <> +
+ {hours} + + : + + {minutes} + + : + + {seconds} +
+ {dateStr} + + )} +
+ + {/* Right: Mobile clock + Profile */} +
+ {/* Mobile clock */} + {mounted && ( +
+ + + + + {hours}:{minutes} + +
+ )} + + {/* Custom header right content (e.g. cart icon) */} + {headerRight} + + {/* 회사 전환 버튼 (최고관리자만) */} + {isSuperAdmin && ( + + )} + +
+ + {/* Profile with Dropdown */} +
+ + + {/* Profile Dropdown */} +
+ {/* User Info */} +
+

{displayName}

+

{deptName || user?.userId}

+
+ + {/* Menu Items */} +
+ + + +
+ + {/* Logout */} +
+ +
+
+
+
+
+ + {/* 회사 전환 모달 (최고관리자만) */} + {isSuperAdmin && ( + setCompanySwitchOpen(false)} + onSelect={handleCompanySwitch} + currentCompanyCode={user?.companyCode || user?.company_code} + /> + )} + + {/* ===== NOTICE BANNER (Marquee) ===== */} + {showBanner && bannerEnabled &&
+
+ 📢 + 공지 +
+
+
+ {marqueeText} +
+
+
} + + {/* ===== MAIN CONTENT ===== */} +
+ {children} +
+ + {/* FOOTER 삭제 — POP 화면에서 불필요 */} + + {/* Marquee keyframes */} + +
+ ); +} diff --git a/frontend/app/(main)/COMPANY_16/pop/layout.tsx b/frontend/app/(main)/COMPANY_16/pop/layout.tsx index 0c898212..19c421c3 100644 --- a/frontend/app/(main)/COMPANY_16/pop/layout.tsx +++ b/frontend/app/(main)/COMPANY_16/pop/layout.tsx @@ -2,7 +2,7 @@ import { usePathname } from "next/navigation"; import type { ReactNode } from "react"; -import { PopShell } from "@/components/pop/hardcoded"; +import { PopShell } from "./_components/common/PopShell"; /** * COMPANY_7 POP 전용 layout diff --git a/frontend/app/(main)/COMPANY_29/pop/_components/common/PopShell.tsx b/frontend/app/(main)/COMPANY_29/pop/_components/common/PopShell.tsx new file mode 100644 index 00000000..ad2eb100 --- /dev/null +++ b/frontend/app/(main)/COMPANY_29/pop/_components/common/PopShell.tsx @@ -0,0 +1,445 @@ +"use client"; + +import React, { useState, useEffect, useRef, ReactNode } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/hooks/useAuth"; +import { usePopSettings } from "@/hooks/pop/usePopSettings"; +import { CompanySwitchModal } from "@/components/pop/shell/CompanySwitchModal"; + +interface PopShellProps { + children: ReactNode; + showBanner?: boolean; + title?: string; + showBack?: boolean; + headerRight?: ReactNode; + fullBleed?: boolean; +} + +export function PopShell({ children, showBanner = true, title, showBack = false, headerRight, fullBleed = false }: PopShellProps) { + const router = useRouter(); + // 회사 고정 PopShell — popHomePath는 해당 회사 메인으로 직박 + const popHomePath = "/COMPANY_29/pop/main"; + const { user, logout, switchCompany } = useAuth(); + const displayName = user?.userName || user?.userId || "사용자"; + const deptName = user?.deptName || ""; + const initial = displayName.charAt(0); + const isSuperAdmin = user?.userType === "SUPER_ADMIN"; + const [companySwitchOpen, setCompanySwitchOpen] = useState(false); + const [mounted, setMounted] = useState(false); + const [showFullscreenSplash, setShowFullscreenSplash] = useState(false); + const [hours, setHours] = useState("00"); + const [minutes, setMinutes] = useState("00"); + const [seconds, setSeconds] = useState("00"); + const [dateStr, setDateStr] = useState("2026-01-01"); + const [colonVisible, setColonVisible] = useState(true); + const [profileOpen, setProfileOpen] = useState(false); + const profileRef = useRef(null); + + // 전체화면이 아닌 상태에서 POP 진입 시 스플래시 표시 — 세션당 1회 + useEffect(() => { + if (sessionStorage.getItem("pop-fullscreen-asked")) return; + if (!document.fullscreenElement) { + setShowFullscreenSplash(true); + sessionStorage.setItem("pop-fullscreen-asked", "1"); + } + }, []); + + const handleEnterFullscreen = async () => { + try { + if (!document.fullscreenElement) { + await document.documentElement.requestFullscreen(); + } + } catch { + // 전체화면 미지원 시 무시 + } + setShowFullscreenSplash(false); + }; + + const handleSkipFullscreen = () => { + setShowFullscreenSplash(false); + }; + + useEffect(() => { + setMounted(true); + + function tick() { + const now = new Date(); + setHours(String(now.getHours()).padStart(2, "0")); + setMinutes(String(now.getMinutes()).padStart(2, "0")); + setSeconds(String(now.getSeconds()).padStart(2, "0")); + setDateStr( + `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}` + ); + } + + tick(); + const clockInterval = setInterval(tick, 1000); + const blinkInterval = setInterval(() => { + setColonVisible((v) => !v); + }, 500); + + return () => { + clearInterval(clockInterval); + clearInterval(blinkInterval); + }; + }, []); + + // Profile dropdown: close on outside click + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (profileRef.current && !profileRef.current.contains(e.target as Node)) { + setProfileOpen(false); + } + } + if (profileOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [profileOpen]); + + const handlePcMode = async () => { + setProfileOpen(false); + if (document.fullscreenElement) { + try { await document.exitFullscreen(); } catch {} + } + router.push("/"); + }; + + const handlePopHome = () => { + setProfileOpen(false); + router.push(popHomePath); + }; + + const toggleFullscreen = async () => { + setProfileOpen(false); + try { + if (document.fullscreenElement) { + await document.exitFullscreen(); + } else { + await document.documentElement.requestFullscreen(); + } + } catch { + // fullscreen not supported + } + }; + + const handleLogout = () => { + setProfileOpen(false); + logout(); + }; + + const handleCompanySwitch = async (companyCode: string) => { + const currentCode = user?.companyCode || user?.company_code; + if (companyCode === currentCode) return; + const result = await switchCompany(companyCode); + if (result.success) { + window.location.reload(); + } else { + alert(result.message || "회사 전환에 실패했습니다."); + } + }; + + // POP 설정에서 배너 텍스트 로드 (POP화면설정에서 관리) + const { settings: popSettings } = usePopSettings(); + const homeConfig = (popSettings as any)?.screens?.home; + const bannerEnabled = homeConfig?.bannerEnabled ?? true; + const bannerText = homeConfig?.bannerText; + const marqueeText = bannerText || "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. | [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 | [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!"; + + return ( +
+ {/* ===== FULLSCREEN SPLASH ===== */} + {showFullscreenSplash && ( +
+
+
+ + + +
+
+

POP 모드

+

최적의 현장 환경을 위해 전체화면을 권장합니다

+
+
+ + +
+
+
+ )} + + {/* ===== HEADER ===== */} +
+ {/* Left: Back + Logo + Company */} +
+ {showBack && ( + + )} +
router.push(popHomePath)} + > +
+ + + +
+
+ {title ? ( + + {title} + + ) : ( + <> + + {user?.companyName || "POP"} + + + 현장 관리 시스템 + + + )} +
+
+
+ + {/* Center: Clock (desktop) */} +
+ {mounted && ( + <> +
+ {hours} + + : + + {minutes} + + : + + {seconds} +
+ {dateStr} + + )} +
+ + {/* Right: Mobile clock + Profile */} +
+ {/* Mobile clock */} + {mounted && ( +
+ + + + + {hours}:{minutes} + +
+ )} + + {/* Custom header right content (e.g. cart icon) */} + {headerRight} + + {/* 회사 전환 버튼 (최고관리자만) */} + {isSuperAdmin && ( + + )} + +
+ + {/* Profile with Dropdown */} +
+ + + {/* Profile Dropdown */} +
+ {/* User Info */} +
+

{displayName}

+

{deptName || user?.userId}

+
+ + {/* Menu Items */} +
+ + + +
+ + {/* Logout */} +
+ +
+
+
+
+
+ + {/* 회사 전환 모달 (최고관리자만) */} + {isSuperAdmin && ( + setCompanySwitchOpen(false)} + onSelect={handleCompanySwitch} + currentCompanyCode={user?.companyCode || user?.company_code} + /> + )} + + {/* ===== NOTICE BANNER (Marquee) ===== */} + {showBanner && bannerEnabled &&
+
+ 📢 + 공지 +
+
+
+ {marqueeText} +
+
+
} + + {/* ===== MAIN CONTENT ===== */} +
+ {children} +
+ + {/* FOOTER 삭제 — POP 화면에서 불필요 */} + + {/* Marquee keyframes */} + +
+ ); +} diff --git a/frontend/app/(main)/COMPANY_29/pop/layout.tsx b/frontend/app/(main)/COMPANY_29/pop/layout.tsx index 0c898212..19c421c3 100644 --- a/frontend/app/(main)/COMPANY_29/pop/layout.tsx +++ b/frontend/app/(main)/COMPANY_29/pop/layout.tsx @@ -2,7 +2,7 @@ import { usePathname } from "next/navigation"; import type { ReactNode } from "react"; -import { PopShell } from "@/components/pop/hardcoded"; +import { PopShell } from "./_components/common/PopShell"; /** * COMPANY_7 POP 전용 layout diff --git a/frontend/app/(main)/COMPANY_30/pop/_components/common/PopShell.tsx b/frontend/app/(main)/COMPANY_30/pop/_components/common/PopShell.tsx new file mode 100644 index 00000000..21b6ff08 --- /dev/null +++ b/frontend/app/(main)/COMPANY_30/pop/_components/common/PopShell.tsx @@ -0,0 +1,445 @@ +"use client"; + +import React, { useState, useEffect, useRef, ReactNode } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/hooks/useAuth"; +import { usePopSettings } from "@/hooks/pop/usePopSettings"; +import { CompanySwitchModal } from "@/components/pop/shell/CompanySwitchModal"; + +interface PopShellProps { + children: ReactNode; + showBanner?: boolean; + title?: string; + showBack?: boolean; + headerRight?: ReactNode; + fullBleed?: boolean; +} + +export function PopShell({ children, showBanner = true, title, showBack = false, headerRight, fullBleed = false }: PopShellProps) { + const router = useRouter(); + // 회사 고정 PopShell — popHomePath는 해당 회사 메인으로 직박 + const popHomePath = "/COMPANY_30/pop/main"; + const { user, logout, switchCompany } = useAuth(); + const displayName = user?.userName || user?.userId || "사용자"; + const deptName = user?.deptName || ""; + const initial = displayName.charAt(0); + const isSuperAdmin = user?.userType === "SUPER_ADMIN"; + const [companySwitchOpen, setCompanySwitchOpen] = useState(false); + const [mounted, setMounted] = useState(false); + const [showFullscreenSplash, setShowFullscreenSplash] = useState(false); + const [hours, setHours] = useState("00"); + const [minutes, setMinutes] = useState("00"); + const [seconds, setSeconds] = useState("00"); + const [dateStr, setDateStr] = useState("2026-01-01"); + const [colonVisible, setColonVisible] = useState(true); + const [profileOpen, setProfileOpen] = useState(false); + const profileRef = useRef(null); + + // 전체화면이 아닌 상태에서 POP 진입 시 스플래시 표시 — 세션당 1회 + useEffect(() => { + if (sessionStorage.getItem("pop-fullscreen-asked")) return; + if (!document.fullscreenElement) { + setShowFullscreenSplash(true); + sessionStorage.setItem("pop-fullscreen-asked", "1"); + } + }, []); + + const handleEnterFullscreen = async () => { + try { + if (!document.fullscreenElement) { + await document.documentElement.requestFullscreen(); + } + } catch { + // 전체화면 미지원 시 무시 + } + setShowFullscreenSplash(false); + }; + + const handleSkipFullscreen = () => { + setShowFullscreenSplash(false); + }; + + useEffect(() => { + setMounted(true); + + function tick() { + const now = new Date(); + setHours(String(now.getHours()).padStart(2, "0")); + setMinutes(String(now.getMinutes()).padStart(2, "0")); + setSeconds(String(now.getSeconds()).padStart(2, "0")); + setDateStr( + `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}` + ); + } + + tick(); + const clockInterval = setInterval(tick, 1000); + const blinkInterval = setInterval(() => { + setColonVisible((v) => !v); + }, 500); + + return () => { + clearInterval(clockInterval); + clearInterval(blinkInterval); + }; + }, []); + + // Profile dropdown: close on outside click + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (profileRef.current && !profileRef.current.contains(e.target as Node)) { + setProfileOpen(false); + } + } + if (profileOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [profileOpen]); + + const handlePcMode = async () => { + setProfileOpen(false); + if (document.fullscreenElement) { + try { await document.exitFullscreen(); } catch {} + } + router.push("/"); + }; + + const handlePopHome = () => { + setProfileOpen(false); + router.push(popHomePath); + }; + + const toggleFullscreen = async () => { + setProfileOpen(false); + try { + if (document.fullscreenElement) { + await document.exitFullscreen(); + } else { + await document.documentElement.requestFullscreen(); + } + } catch { + // fullscreen not supported + } + }; + + const handleLogout = () => { + setProfileOpen(false); + logout(); + }; + + const handleCompanySwitch = async (companyCode: string) => { + const currentCode = user?.companyCode || user?.company_code; + if (companyCode === currentCode) return; + const result = await switchCompany(companyCode); + if (result.success) { + window.location.reload(); + } else { + alert(result.message || "회사 전환에 실패했습니다."); + } + }; + + // POP 설정에서 배너 텍스트 로드 (POP화면설정에서 관리) + const { settings: popSettings } = usePopSettings(); + const homeConfig = (popSettings as any)?.screens?.home; + const bannerEnabled = homeConfig?.bannerEnabled ?? true; + const bannerText = homeConfig?.bannerText; + const marqueeText = bannerText || "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. | [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 | [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!"; + + return ( +
+ {/* ===== FULLSCREEN SPLASH ===== */} + {showFullscreenSplash && ( +
+
+
+ + + +
+
+

POP 모드

+

최적의 현장 환경을 위해 전체화면을 권장합니다

+
+
+ + +
+
+
+ )} + + {/* ===== HEADER ===== */} +
+ {/* Left: Back + Logo + Company */} +
+ {showBack && ( + + )} +
router.push(popHomePath)} + > +
+ + + +
+
+ {title ? ( + + {title} + + ) : ( + <> + + {user?.companyName || "POP"} + + + 현장 관리 시스템 + + + )} +
+
+
+ + {/* Center: Clock (desktop) */} +
+ {mounted && ( + <> +
+ {hours} + + : + + {minutes} + + : + + {seconds} +
+ {dateStr} + + )} +
+ + {/* Right: Mobile clock + Profile */} +
+ {/* Mobile clock */} + {mounted && ( +
+ + + + + {hours}:{minutes} + +
+ )} + + {/* Custom header right content (e.g. cart icon) */} + {headerRight} + + {/* 회사 전환 버튼 (최고관리자만) */} + {isSuperAdmin && ( + + )} + +
+ + {/* Profile with Dropdown */} +
+ + + {/* Profile Dropdown */} +
+ {/* User Info */} +
+

{displayName}

+

{deptName || user?.userId}

+
+ + {/* Menu Items */} +
+ + + +
+ + {/* Logout */} +
+ +
+
+
+
+
+ + {/* 회사 전환 모달 (최고관리자만) */} + {isSuperAdmin && ( + setCompanySwitchOpen(false)} + onSelect={handleCompanySwitch} + currentCompanyCode={user?.companyCode || user?.company_code} + /> + )} + + {/* ===== NOTICE BANNER (Marquee) ===== */} + {showBanner && bannerEnabled &&
+
+ 📢 + 공지 +
+
+
+ {marqueeText} +
+
+
} + + {/* ===== MAIN CONTENT ===== */} +
+ {children} +
+ + {/* FOOTER 삭제 — POP 화면에서 불필요 */} + + {/* Marquee keyframes */} + +
+ ); +} diff --git a/frontend/app/(main)/COMPANY_30/pop/layout.tsx b/frontend/app/(main)/COMPANY_30/pop/layout.tsx index 0c898212..19c421c3 100644 --- a/frontend/app/(main)/COMPANY_30/pop/layout.tsx +++ b/frontend/app/(main)/COMPANY_30/pop/layout.tsx @@ -2,7 +2,7 @@ import { usePathname } from "next/navigation"; import type { ReactNode } from "react"; -import { PopShell } from "@/components/pop/hardcoded"; +import { PopShell } from "./_components/common/PopShell"; /** * COMPANY_7 POP 전용 layout diff --git a/frontend/app/(main)/COMPANY_7/pop/_components/common/PopShell.tsx b/frontend/app/(main)/COMPANY_7/pop/_components/common/PopShell.tsx new file mode 100644 index 00000000..67b87e4d --- /dev/null +++ b/frontend/app/(main)/COMPANY_7/pop/_components/common/PopShell.tsx @@ -0,0 +1,445 @@ +"use client"; + +import React, { useState, useEffect, useRef, ReactNode } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/hooks/useAuth"; +import { usePopSettings } from "@/hooks/pop/usePopSettings"; +import { CompanySwitchModal } from "@/components/pop/shell/CompanySwitchModal"; + +interface PopShellProps { + children: ReactNode; + showBanner?: boolean; + title?: string; + showBack?: boolean; + headerRight?: ReactNode; + fullBleed?: boolean; +} + +export function PopShell({ children, showBanner = true, title, showBack = false, headerRight, fullBleed = false }: PopShellProps) { + const router = useRouter(); + // 회사 고정 PopShell — popHomePath는 해당 회사 메인으로 직박 + const popHomePath = "/COMPANY_7/pop/main"; + const { user, logout, switchCompany } = useAuth(); + const displayName = user?.userName || user?.userId || "사용자"; + const deptName = user?.deptName || ""; + const initial = displayName.charAt(0); + const isSuperAdmin = user?.userType === "SUPER_ADMIN"; + const [companySwitchOpen, setCompanySwitchOpen] = useState(false); + const [mounted, setMounted] = useState(false); + const [showFullscreenSplash, setShowFullscreenSplash] = useState(false); + const [hours, setHours] = useState("00"); + const [minutes, setMinutes] = useState("00"); + const [seconds, setSeconds] = useState("00"); + const [dateStr, setDateStr] = useState("2026-01-01"); + const [colonVisible, setColonVisible] = useState(true); + const [profileOpen, setProfileOpen] = useState(false); + const profileRef = useRef(null); + + // 전체화면이 아닌 상태에서 POP 진입 시 스플래시 표시 — 세션당 1회 + useEffect(() => { + if (sessionStorage.getItem("pop-fullscreen-asked")) return; + if (!document.fullscreenElement) { + setShowFullscreenSplash(true); + sessionStorage.setItem("pop-fullscreen-asked", "1"); + } + }, []); + + const handleEnterFullscreen = async () => { + try { + if (!document.fullscreenElement) { + await document.documentElement.requestFullscreen(); + } + } catch { + // 전체화면 미지원 시 무시 + } + setShowFullscreenSplash(false); + }; + + const handleSkipFullscreen = () => { + setShowFullscreenSplash(false); + }; + + useEffect(() => { + setMounted(true); + + function tick() { + const now = new Date(); + setHours(String(now.getHours()).padStart(2, "0")); + setMinutes(String(now.getMinutes()).padStart(2, "0")); + setSeconds(String(now.getSeconds()).padStart(2, "0")); + setDateStr( + `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}` + ); + } + + tick(); + const clockInterval = setInterval(tick, 1000); + const blinkInterval = setInterval(() => { + setColonVisible((v) => !v); + }, 500); + + return () => { + clearInterval(clockInterval); + clearInterval(blinkInterval); + }; + }, []); + + // Profile dropdown: close on outside click + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (profileRef.current && !profileRef.current.contains(e.target as Node)) { + setProfileOpen(false); + } + } + if (profileOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [profileOpen]); + + const handlePcMode = async () => { + setProfileOpen(false); + if (document.fullscreenElement) { + try { await document.exitFullscreen(); } catch {} + } + router.push("/"); + }; + + const handlePopHome = () => { + setProfileOpen(false); + router.push(popHomePath); + }; + + const toggleFullscreen = async () => { + setProfileOpen(false); + try { + if (document.fullscreenElement) { + await document.exitFullscreen(); + } else { + await document.documentElement.requestFullscreen(); + } + } catch { + // fullscreen not supported + } + }; + + const handleLogout = () => { + setProfileOpen(false); + logout(); + }; + + const handleCompanySwitch = async (companyCode: string) => { + const currentCode = user?.companyCode || user?.company_code; + if (companyCode === currentCode) return; + const result = await switchCompany(companyCode); + if (result.success) { + window.location.reload(); + } else { + alert(result.message || "회사 전환에 실패했습니다."); + } + }; + + // POP 설정에서 배너 텍스트 로드 (POP화면설정에서 관리) + const { settings: popSettings } = usePopSettings(); + const homeConfig = (popSettings as any)?.screens?.home; + const bannerEnabled = homeConfig?.bannerEnabled ?? true; + const bannerText = homeConfig?.bannerText; + const marqueeText = bannerText || "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. | [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 | [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!"; + + return ( +
+ {/* ===== FULLSCREEN SPLASH ===== */} + {showFullscreenSplash && ( +
+
+
+ + + +
+
+

POP 모드

+

최적의 현장 환경을 위해 전체화면을 권장합니다

+
+
+ + +
+
+
+ )} + + {/* ===== HEADER ===== */} +
+ {/* Left: Back + Logo + Company */} +
+ {showBack && ( + + )} +
router.push(popHomePath)} + > +
+ + + +
+
+ {title ? ( + + {title} + + ) : ( + <> + + {user?.companyName || "POP"} + + + 현장 관리 시스템 + + + )} +
+
+
+ + {/* Center: Clock (desktop) */} +
+ {mounted && ( + <> +
+ {hours} + + : + + {minutes} + + : + + {seconds} +
+ {dateStr} + + )} +
+ + {/* Right: Mobile clock + Profile */} +
+ {/* Mobile clock */} + {mounted && ( +
+ + + + + {hours}:{minutes} + +
+ )} + + {/* Custom header right content (e.g. cart icon) */} + {headerRight} + + {/* 회사 전환 버튼 (최고관리자만) */} + {isSuperAdmin && ( + + )} + +
+ + {/* Profile with Dropdown */} +
+ + + {/* Profile Dropdown */} +
+ {/* User Info */} +
+

{displayName}

+

{deptName || user?.userId}

+
+ + {/* Menu Items */} +
+ + + +
+ + {/* Logout */} +
+ +
+
+
+
+
+ + {/* 회사 전환 모달 (최고관리자만) */} + {isSuperAdmin && ( + setCompanySwitchOpen(false)} + onSelect={handleCompanySwitch} + currentCompanyCode={user?.companyCode || user?.company_code} + /> + )} + + {/* ===== NOTICE BANNER (Marquee) ===== */} + {showBanner && bannerEnabled &&
+
+ 📢 + 공지 +
+
+
+ {marqueeText} +
+
+
} + + {/* ===== MAIN CONTENT ===== */} +
+ {children} +
+ + {/* FOOTER 삭제 — POP 화면에서 불필요 */} + + {/* Marquee keyframes */} + +
+ ); +} diff --git a/frontend/app/(main)/COMPANY_7/pop/layout.tsx b/frontend/app/(main)/COMPANY_7/pop/layout.tsx index 0c898212..19c421c3 100644 --- a/frontend/app/(main)/COMPANY_7/pop/layout.tsx +++ b/frontend/app/(main)/COMPANY_7/pop/layout.tsx @@ -2,7 +2,7 @@ import { usePathname } from "next/navigation"; import type { ReactNode } from "react"; -import { PopShell } from "@/components/pop/hardcoded"; +import { PopShell } from "./_components/common/PopShell"; /** * COMPANY_7 POP 전용 layout diff --git a/frontend/app/(main)/COMPANY_8/pop/_components/common/PopShell.tsx b/frontend/app/(main)/COMPANY_8/pop/_components/common/PopShell.tsx new file mode 100644 index 00000000..76c6a09b --- /dev/null +++ b/frontend/app/(main)/COMPANY_8/pop/_components/common/PopShell.tsx @@ -0,0 +1,445 @@ +"use client"; + +import React, { useState, useEffect, useRef, ReactNode } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/hooks/useAuth"; +import { usePopSettings } from "@/hooks/pop/usePopSettings"; +import { CompanySwitchModal } from "@/components/pop/shell/CompanySwitchModal"; + +interface PopShellProps { + children: ReactNode; + showBanner?: boolean; + title?: string; + showBack?: boolean; + headerRight?: ReactNode; + fullBleed?: boolean; +} + +export function PopShell({ children, showBanner = true, title, showBack = false, headerRight, fullBleed = false }: PopShellProps) { + const router = useRouter(); + // 회사 고정 PopShell — popHomePath는 해당 회사 메인으로 직박 + const popHomePath = "/COMPANY_8/pop/main"; + const { user, logout, switchCompany } = useAuth(); + const displayName = user?.userName || user?.userId || "사용자"; + const deptName = user?.deptName || ""; + const initial = displayName.charAt(0); + const isSuperAdmin = user?.userType === "SUPER_ADMIN"; + const [companySwitchOpen, setCompanySwitchOpen] = useState(false); + const [mounted, setMounted] = useState(false); + const [showFullscreenSplash, setShowFullscreenSplash] = useState(false); + const [hours, setHours] = useState("00"); + const [minutes, setMinutes] = useState("00"); + const [seconds, setSeconds] = useState("00"); + const [dateStr, setDateStr] = useState("2026-01-01"); + const [colonVisible, setColonVisible] = useState(true); + const [profileOpen, setProfileOpen] = useState(false); + const profileRef = useRef(null); + + // 전체화면이 아닌 상태에서 POP 진입 시 스플래시 표시 — 세션당 1회 + useEffect(() => { + if (sessionStorage.getItem("pop-fullscreen-asked")) return; + if (!document.fullscreenElement) { + setShowFullscreenSplash(true); + sessionStorage.setItem("pop-fullscreen-asked", "1"); + } + }, []); + + const handleEnterFullscreen = async () => { + try { + if (!document.fullscreenElement) { + await document.documentElement.requestFullscreen(); + } + } catch { + // 전체화면 미지원 시 무시 + } + setShowFullscreenSplash(false); + }; + + const handleSkipFullscreen = () => { + setShowFullscreenSplash(false); + }; + + useEffect(() => { + setMounted(true); + + function tick() { + const now = new Date(); + setHours(String(now.getHours()).padStart(2, "0")); + setMinutes(String(now.getMinutes()).padStart(2, "0")); + setSeconds(String(now.getSeconds()).padStart(2, "0")); + setDateStr( + `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}` + ); + } + + tick(); + const clockInterval = setInterval(tick, 1000); + const blinkInterval = setInterval(() => { + setColonVisible((v) => !v); + }, 500); + + return () => { + clearInterval(clockInterval); + clearInterval(blinkInterval); + }; + }, []); + + // Profile dropdown: close on outside click + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (profileRef.current && !profileRef.current.contains(e.target as Node)) { + setProfileOpen(false); + } + } + if (profileOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [profileOpen]); + + const handlePcMode = async () => { + setProfileOpen(false); + if (document.fullscreenElement) { + try { await document.exitFullscreen(); } catch {} + } + router.push("/"); + }; + + const handlePopHome = () => { + setProfileOpen(false); + router.push(popHomePath); + }; + + const toggleFullscreen = async () => { + setProfileOpen(false); + try { + if (document.fullscreenElement) { + await document.exitFullscreen(); + } else { + await document.documentElement.requestFullscreen(); + } + } catch { + // fullscreen not supported + } + }; + + const handleLogout = () => { + setProfileOpen(false); + logout(); + }; + + const handleCompanySwitch = async (companyCode: string) => { + const currentCode = user?.companyCode || user?.company_code; + if (companyCode === currentCode) return; + const result = await switchCompany(companyCode); + if (result.success) { + window.location.reload(); + } else { + alert(result.message || "회사 전환에 실패했습니다."); + } + }; + + // POP 설정에서 배너 텍스트 로드 (POP화면설정에서 관리) + const { settings: popSettings } = usePopSettings(); + const homeConfig = (popSettings as any)?.screens?.home; + const bannerEnabled = homeConfig?.bannerEnabled ?? true; + const bannerText = homeConfig?.bannerText; + const marqueeText = bannerText || "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. | [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 | [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!"; + + return ( +
+ {/* ===== FULLSCREEN SPLASH ===== */} + {showFullscreenSplash && ( +
+
+
+ + + +
+
+

POP 모드

+

최적의 현장 환경을 위해 전체화면을 권장합니다

+
+
+ + +
+
+
+ )} + + {/* ===== HEADER ===== */} +
+ {/* Left: Back + Logo + Company */} +
+ {showBack && ( + + )} +
router.push(popHomePath)} + > +
+ + + +
+
+ {title ? ( + + {title} + + ) : ( + <> + + {user?.companyName || "POP"} + + + 현장 관리 시스템 + + + )} +
+
+
+ + {/* Center: Clock (desktop) */} +
+ {mounted && ( + <> +
+ {hours} + + : + + {minutes} + + : + + {seconds} +
+ {dateStr} + + )} +
+ + {/* Right: Mobile clock + Profile */} +
+ {/* Mobile clock */} + {mounted && ( +
+ + + + + {hours}:{minutes} + +
+ )} + + {/* Custom header right content (e.g. cart icon) */} + {headerRight} + + {/* 회사 전환 버튼 (최고관리자만) */} + {isSuperAdmin && ( + + )} + +
+ + {/* Profile with Dropdown */} +
+ + + {/* Profile Dropdown */} +
+ {/* User Info */} +
+

{displayName}

+

{deptName || user?.userId}

+
+ + {/* Menu Items */} +
+ + + +
+ + {/* Logout */} +
+ +
+
+
+
+
+ + {/* 회사 전환 모달 (최고관리자만) */} + {isSuperAdmin && ( + setCompanySwitchOpen(false)} + onSelect={handleCompanySwitch} + currentCompanyCode={user?.companyCode || user?.company_code} + /> + )} + + {/* ===== NOTICE BANNER (Marquee) ===== */} + {showBanner && bannerEnabled &&
+
+ 📢 + 공지 +
+
+
+ {marqueeText} +
+
+
} + + {/* ===== MAIN CONTENT ===== */} +
+ {children} +
+ + {/* FOOTER 삭제 — POP 화면에서 불필요 */} + + {/* Marquee keyframes */} + +
+ ); +} diff --git a/frontend/app/(main)/COMPANY_8/pop/layout.tsx b/frontend/app/(main)/COMPANY_8/pop/layout.tsx index 0c898212..19c421c3 100644 --- a/frontend/app/(main)/COMPANY_8/pop/layout.tsx +++ b/frontend/app/(main)/COMPANY_8/pop/layout.tsx @@ -2,7 +2,7 @@ import { usePathname } from "next/navigation"; import type { ReactNode } from "react"; -import { PopShell } from "@/components/pop/hardcoded"; +import { PopShell } from "./_components/common/PopShell"; /** * COMPANY_7 POP 전용 layout diff --git a/frontend/app/(main)/COMPANY_9/pop/_components/common/PopShell.tsx b/frontend/app/(main)/COMPANY_9/pop/_components/common/PopShell.tsx new file mode 100644 index 00000000..6357431f --- /dev/null +++ b/frontend/app/(main)/COMPANY_9/pop/_components/common/PopShell.tsx @@ -0,0 +1,445 @@ +"use client"; + +import React, { useState, useEffect, useRef, ReactNode } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/hooks/useAuth"; +import { usePopSettings } from "@/hooks/pop/usePopSettings"; +import { CompanySwitchModal } from "@/components/pop/shell/CompanySwitchModal"; + +interface PopShellProps { + children: ReactNode; + showBanner?: boolean; + title?: string; + showBack?: boolean; + headerRight?: ReactNode; + fullBleed?: boolean; +} + +export function PopShell({ children, showBanner = true, title, showBack = false, headerRight, fullBleed = false }: PopShellProps) { + const router = useRouter(); + // 회사 고정 PopShell — popHomePath는 해당 회사 메인으로 직박 + const popHomePath = "/COMPANY_9/pop/main"; + const { user, logout, switchCompany } = useAuth(); + const displayName = user?.userName || user?.userId || "사용자"; + const deptName = user?.deptName || ""; + const initial = displayName.charAt(0); + const isSuperAdmin = user?.userType === "SUPER_ADMIN"; + const [companySwitchOpen, setCompanySwitchOpen] = useState(false); + const [mounted, setMounted] = useState(false); + const [showFullscreenSplash, setShowFullscreenSplash] = useState(false); + const [hours, setHours] = useState("00"); + const [minutes, setMinutes] = useState("00"); + const [seconds, setSeconds] = useState("00"); + const [dateStr, setDateStr] = useState("2026-01-01"); + const [colonVisible, setColonVisible] = useState(true); + const [profileOpen, setProfileOpen] = useState(false); + const profileRef = useRef(null); + + // 전체화면이 아닌 상태에서 POP 진입 시 스플래시 표시 — 세션당 1회 + useEffect(() => { + if (sessionStorage.getItem("pop-fullscreen-asked")) return; + if (!document.fullscreenElement) { + setShowFullscreenSplash(true); + sessionStorage.setItem("pop-fullscreen-asked", "1"); + } + }, []); + + const handleEnterFullscreen = async () => { + try { + if (!document.fullscreenElement) { + await document.documentElement.requestFullscreen(); + } + } catch { + // 전체화면 미지원 시 무시 + } + setShowFullscreenSplash(false); + }; + + const handleSkipFullscreen = () => { + setShowFullscreenSplash(false); + }; + + useEffect(() => { + setMounted(true); + + function tick() { + const now = new Date(); + setHours(String(now.getHours()).padStart(2, "0")); + setMinutes(String(now.getMinutes()).padStart(2, "0")); + setSeconds(String(now.getSeconds()).padStart(2, "0")); + setDateStr( + `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}` + ); + } + + tick(); + const clockInterval = setInterval(tick, 1000); + const blinkInterval = setInterval(() => { + setColonVisible((v) => !v); + }, 500); + + return () => { + clearInterval(clockInterval); + clearInterval(blinkInterval); + }; + }, []); + + // Profile dropdown: close on outside click + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (profileRef.current && !profileRef.current.contains(e.target as Node)) { + setProfileOpen(false); + } + } + if (profileOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [profileOpen]); + + const handlePcMode = async () => { + setProfileOpen(false); + if (document.fullscreenElement) { + try { await document.exitFullscreen(); } catch {} + } + router.push("/"); + }; + + const handlePopHome = () => { + setProfileOpen(false); + router.push(popHomePath); + }; + + const toggleFullscreen = async () => { + setProfileOpen(false); + try { + if (document.fullscreenElement) { + await document.exitFullscreen(); + } else { + await document.documentElement.requestFullscreen(); + } + } catch { + // fullscreen not supported + } + }; + + const handleLogout = () => { + setProfileOpen(false); + logout(); + }; + + const handleCompanySwitch = async (companyCode: string) => { + const currentCode = user?.companyCode || user?.company_code; + if (companyCode === currentCode) return; + const result = await switchCompany(companyCode); + if (result.success) { + window.location.reload(); + } else { + alert(result.message || "회사 전환에 실패했습니다."); + } + }; + + // POP 설정에서 배너 텍스트 로드 (POP화면설정에서 관리) + const { settings: popSettings } = usePopSettings(); + const homeConfig = (popSettings as any)?.screens?.home; + const bannerEnabled = homeConfig?.bannerEnabled ?? true; + const bannerText = homeConfig?.bannerText; + const marqueeText = bannerText || "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. | [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 | [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!"; + + return ( +
+ {/* ===== FULLSCREEN SPLASH ===== */} + {showFullscreenSplash && ( +
+
+
+ + + +
+
+

POP 모드

+

최적의 현장 환경을 위해 전체화면을 권장합니다

+
+
+ + +
+
+
+ )} + + {/* ===== HEADER ===== */} +
+ {/* Left: Back + Logo + Company */} +
+ {showBack && ( + + )} +
router.push(popHomePath)} + > +
+ + + +
+
+ {title ? ( + + {title} + + ) : ( + <> + + {user?.companyName || "POP"} + + + 현장 관리 시스템 + + + )} +
+
+
+ + {/* Center: Clock (desktop) */} +
+ {mounted && ( + <> +
+ {hours} + + : + + {minutes} + + : + + {seconds} +
+ {dateStr} + + )} +
+ + {/* Right: Mobile clock + Profile */} +
+ {/* Mobile clock */} + {mounted && ( +
+ + + + + {hours}:{minutes} + +
+ )} + + {/* Custom header right content (e.g. cart icon) */} + {headerRight} + + {/* 회사 전환 버튼 (최고관리자만) */} + {isSuperAdmin && ( + + )} + +
+ + {/* Profile with Dropdown */} +
+ + + {/* Profile Dropdown */} +
+ {/* User Info */} +
+

{displayName}

+

{deptName || user?.userId}

+
+ + {/* Menu Items */} +
+ + + +
+ + {/* Logout */} +
+ +
+
+
+
+
+ + {/* 회사 전환 모달 (최고관리자만) */} + {isSuperAdmin && ( + setCompanySwitchOpen(false)} + onSelect={handleCompanySwitch} + currentCompanyCode={user?.companyCode || user?.company_code} + /> + )} + + {/* ===== NOTICE BANNER (Marquee) ===== */} + {showBanner && bannerEnabled &&
+
+ 📢 + 공지 +
+
+
+ {marqueeText} +
+
+
} + + {/* ===== MAIN CONTENT ===== */} +
+ {children} +
+ + {/* FOOTER 삭제 — POP 화면에서 불필요 */} + + {/* Marquee keyframes */} + +
+ ); +} diff --git a/frontend/app/(main)/COMPANY_9/pop/layout.tsx b/frontend/app/(main)/COMPANY_9/pop/layout.tsx index 0c898212..19c421c3 100644 --- a/frontend/app/(main)/COMPANY_9/pop/layout.tsx +++ b/frontend/app/(main)/COMPANY_9/pop/layout.tsx @@ -2,7 +2,7 @@ import { usePathname } from "next/navigation"; import type { ReactNode } from "react"; -import { PopShell } from "@/components/pop/hardcoded"; +import { PopShell } from "./_components/common/PopShell"; /** * COMPANY_7 POP 전용 layout diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index 4dd25c95..ede0e1b8 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -54,6 +54,7 @@ import { DialogDescription, } from "@/components/ui/dialog"; import { CompanySwitcher } from "@/components/admin/CompanySwitcher"; +import { CompanySwitchModal } from "@/components/pop/shell/CompanySwitchModal"; import { getIconComponent } from "@/components/admin/MenuIconPicker"; import { TooltipProvider } from "@/components/ui/tooltip"; @@ -263,6 +264,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { return false; }); const [hasPopMenus, setHasPopMenus] = useState(false); + const [popCompanySelectOpen, setPopCompanySelectOpen] = useState(false); const [hoveredCollapsedMenu, setHoveredCollapsedMenu] = useState(null); const toggleSidebarCollapse = () => { @@ -390,6 +392,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { const tabMode = useTabStore((s) => s.mode); const setTabMode = useTabStore((s) => s.setMode); const isAdminMode = tabMode === "admin"; + const isSuperAdmin = (user as ExtendedUserInfo)?.userType === "SUPER_ADMIN"; const isPreviewMode = searchParams.get("preview") === "true"; @@ -522,6 +525,11 @@ function AppLayoutInner({ children }: AppLayoutProps) { // POP 모드 진입 핸들러 const handlePopModeClick = async () => { + if (isSuperAdmin) { + setPopCompanySelectOpen(true); + return; + } + try { // PC → POP 전환 시 전체화면 적용 try { @@ -543,7 +551,12 @@ function AppLayoutInner({ children }: AppLayoutProps) { } else if (childMenus.length === 1) { router.push(childMenus[0].menu_url); } else { - router.push("/pop"); + const userCompanyCode = (user as ExtendedUserInfo)?.companyCode; + if (userCompanyCode && userCompanyCode !== "*") { + router.push(`/${userCompanyCode}/pop/main`); + } else { + toast.info("POP 메뉴가 여러 개 등록되어 있습니다. 관리자에게 [POP_LANDING] 태그 설정을 요청하세요."); + } } } else { toast.info("설정된 POP 화면이 없습니다"); @@ -553,6 +566,23 @@ function AppLayoutInner({ children }: AppLayoutProps) { } }; + // SUPER_ADMIN: 회사 선택 모달에서 회사 선택 시 해당 회사 신 POP 으로 진입 + const handlePopCompanySelect = async (companyCode: string) => { + if (companyCode === "*") { + toast.info("POP 모드는 특정 회사를 선택하세요"); + return; + } + setPopCompanySelectOpen(false); + try { + if (!document.fullscreenElement) { + await document.documentElement.requestFullscreen(); + } + } catch { + // 전체화면 미지원 또는 거부 시 무시 + } + router.push(`/${companyCode}/pop/main`); + }; + // pathname + 활성 탭 기반 활성 메뉴 판별 (탭 네비게이션에서도 사이드바 활성 표시) const isMenuActive = useCallback( (menu: any): boolean => { @@ -790,7 +820,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { 결재함 - {hasPopMenus && ( + {(hasPopMenus || isSuperAdmin) && ( POP 모드 @@ -1084,7 +1114,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { 결재함 - {hasPopMenus && ( + {(hasPopMenus || isSuperAdmin) && ( POP 모드 @@ -1151,6 +1181,12 @@ function AppLayoutInner({ children }: AppLayoutProps) { + + setPopCompanySelectOpen(false)} + onSelect={handlePopCompanySelect} + /> ); } diff --git a/frontend/components/pop/shell/CompanySwitchModal.tsx b/frontend/components/pop/shell/CompanySwitchModal.tsx new file mode 100644 index 00000000..7ecb7dc6 --- /dev/null +++ b/frontend/components/pop/shell/CompanySwitchModal.tsx @@ -0,0 +1,186 @@ +"use client"; + +import React, { useState, useEffect, useMemo } from "react"; +import { apiClient } from "@/lib/api/client"; + +interface Company { + company_code: string; + company_name: string; + status: string; +} + +interface CompanySwitchModalProps { + open: boolean; + onClose: () => void; + onSelect: (companyCode: string) => void; + currentCompanyCode?: string; +} + +export function CompanySwitchModal({ + open, + onClose, + onSelect, + currentCompanyCode, +}: CompanySwitchModalProps) { + const [companies, setCompanies] = useState([]); + const [search, setSearch] = useState(""); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (open) { + fetchCompanies(); + setSearch(""); + } + }, [open]); + + const fetchCompanies = async () => { + try { + setLoading(true); + const response = await apiClient.get("/admin/companies/db"); + + if (response.data.success) { + const activeCompanies = response.data.data + .filter((c: Company) => c.company_code !== "*") + .filter((c: Company) => c.status === "active" || !c.status) + .sort((a: Company, b: Company) => + a.company_name.localeCompare(b.company_name, "ko") + ); + + const companiesWithWace: Company[] = [ + { + company_code: "*", + company_name: "WACE (최고 관리자)", + status: "active", + }, + ...activeCompanies, + ]; + + setCompanies(companiesWithWace); + } + } catch { + setCompanies([]); + } finally { + setLoading(false); + } + }; + + const filtered = useMemo(() => { + if (!search.trim()) return companies; + const q = search.toLowerCase(); + return companies.filter( + (c) => + c.company_name.toLowerCase().includes(q) || + c.company_code.toLowerCase().includes(q) + ); + }, [companies, search]); + + const handleSelect = (companyCode: string) => { + onSelect(companyCode); + onClose(); + }; + + if (!open) return null; + + return ( +
+ {/* Overlay */} +
+ + {/* Modal */} +
+ {/* Header */} +
+

회사 전환

+ +
+ + {/* Search */} +
+
+ + + + setSearch(e.target.value)} + placeholder="회사명 또는 코드 검색..." + className="w-full pl-10 pr-4 py-2.5 rounded-xl text-sm outline-none transition-all bg-[var(--pop-input-bg,#0f172a)] border border-[var(--pop-border,#334155)] focus:border-blue-400 focus:ring-2 focus:ring-blue-400/20 text-[var(--pop-text,#e2e8f0)] placeholder:opacity-50" + /> +
+
+ + {/* Company List */} +
+ {loading ? ( +
+ 불러오는 중... +
+ ) : filtered.length === 0 ? ( +
+ {search ? "검색 결과가 없습니다" : "회사 목록이 없습니다"} +
+ ) : ( + filtered.map((company) => { + const isCurrent = company.company_code === currentCompanyCode; + return ( + + ); + }) + )} +
+
+
+ ); +} diff --git a/frontend/hooks/pop/usePopSettings.ts b/frontend/hooks/pop/usePopSettings.ts index b81ae3d5..c2722496 100644 --- a/frontend/hooks/pop/usePopSettings.ts +++ b/frontend/hooks/pop/usePopSettings.ts @@ -136,10 +136,29 @@ const PATH_TO_SETTINGS_KEY: Record = { "/COMPANY_7/pop/production/process": "processExecution", }; +// 신 URL `/COMPANY_X/pop/` 에서 화면 키 추출 (main → home 정규화) +function extractScreenKey(pathname: string): string | null { + const match = pathname.match(/^\/COMPANY_\d+\/pop\/(.+)$/); + if (!match) return null; + const tail = match[1]; + return tail === "main" ? "home" : tail; +} + function getScreenIdFromPath(pathname: string): number | null { - // Exact match first + // 신 URL 우선 처리 (회사 prefix 제거 후 화면 키 매핑) + const screenKey = extractScreenKey(pathname); + if (screenKey) { + const lookupPath = `/pop/${screenKey}`; + if (POP_SCREEN_MAP[lookupPath]) return POP_SCREEN_MAP[lookupPath]; + const sortedNew = Object.keys(POP_SCREEN_MAP).sort((a, b) => b.length - a.length); + for (const path of sortedNew) { + if (lookupPath.startsWith(path)) return POP_SCREEN_MAP[path]; + } + return null; + } + + // 구 (pop)/ 라우트 호환 fallback if (POP_SCREEN_MAP[pathname]) return POP_SCREEN_MAP[pathname]; - // Longest-prefix match (e.g. /pop/production/process/xxx -> 7) const sorted = Object.keys(POP_SCREEN_MAP).sort((a, b) => b.length - a.length); for (const path of sorted) { if (pathname.startsWith(path)) return POP_SCREEN_MAP[path]; @@ -148,9 +167,20 @@ function getScreenIdFromPath(pathname: string): number | null { } function getSettingsKeyFromPath(pathname: string): keyof PopSettings["screens"] | null { - // Exact match first + // 신 URL 우선 처리 (회사 prefix 제거 후 화면 키 매핑) + const screenKey = extractScreenKey(pathname); + if (screenKey) { + const lookupPath = `/pop/${screenKey}`; + if (PATH_TO_SETTINGS_KEY[lookupPath]) return PATH_TO_SETTINGS_KEY[lookupPath]; + const sortedNew = Object.keys(PATH_TO_SETTINGS_KEY).sort((a, b) => b.length - a.length); + for (const path of sortedNew) { + if (lookupPath.startsWith(path)) return PATH_TO_SETTINGS_KEY[path]; + } + return null; + } + + // 구 (pop)/ 라우트 호환 fallback if (PATH_TO_SETTINGS_KEY[pathname]) return PATH_TO_SETTINGS_KEY[pathname]; - // Longest-prefix match const sorted = Object.keys(PATH_TO_SETTINGS_KEY).sort((a, b) => b.length - a.length); for (const path of sorted) { if (pathname.startsWith(path)) return PATH_TO_SETTINGS_KEY[path];