2026-04-02 17:39:42 +09:00
"use client" ;
import React , { useState , useEffect , useRef , ReactNode } from "react" ;
import { useRouter } from "next/navigation" ;
2026-04-05 17:45:33 +09:00
import { useAuth } from "@/hooks/useAuth" ;
2026-04-07 16:30:53 +09:00
import { usePopSettings } from "@/hooks/pop/usePopSettings" ;
2026-04-02 17:39:42 +09:00
interface PopShellProps {
children : ReactNode ;
showBanner? : boolean ;
title? : string ;
showBack? : boolean ;
headerRight? : ReactNode ;
2026-04-10 17:17:23 +09:00
fullBleed? : boolean ;
2026-04-02 17:39:42 +09:00
}
2026-04-10 17:17:23 +09:00
export function PopShell ( { children , showBanner = true , title , showBack = false , headerRight , fullBleed = false } : PopShellProps ) {
2026-04-02 17:39:42 +09:00
const router = useRouter ( ) ;
2026-04-05 17:45:33 +09:00
const { user , logout } = useAuth ( ) ;
const displayName = user ? . userName || user ? . userId || "사용자" ;
const deptName = user ? . deptName || "" ;
const initial = displayName . charAt ( 0 ) ;
2026-04-02 17:39:42 +09:00
const [ mounted , setMounted ] = 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 < HTMLDivElement > ( null ) ;
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 ( "/pop/home" ) ;
} ;
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 ) ;
2026-04-05 17:45:33 +09:00
logout ( ) ;
2026-04-02 17:39:42 +09:00
} ;
2026-04-07 16:30:53 +09:00
// POP 설정에서 배너 텍스트 로드 (POP화면설정에서 관리)
const { settings : popSettings } = usePopSettings ( "/pop/home" ) ;
const homeConfig = ( popSettings as any ) ? . screens ? . home ;
const bannerEnabled = homeConfig ? . bannerEnabled ? ? true ;
const bannerText = homeConfig ? . bannerText ;
const marqueeText = bannerText || "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. \u00a0\u00a0|\u00a0\u00a0 [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 \u00a0\u00a0|\u00a0\u00a0 [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!" ;
2026-04-02 17:39:42 +09:00
return (
< div className = "min-h-screen min-h-dvh flex flex-col" style = { { background : "#F5F5F5" } } >
{ /* ===== HEADER ===== */ }
< header
className = "sticky top-0 z-50 flex items-center justify-between px-4 sm:px-6 lg:px-8 py-3"
style = { { background : "#1a1a2e" } }
>
{ /* Left: Back + Logo + Company */ }
< div className = "flex items-center gap-3 min-w-0" >
{ showBack && (
< button
onClick = { ( ) = > router . back ( ) }
className = "w-10 h-10 rounded-xl bg-white/10 flex items-center justify-center shrink-0 hover:bg-white/20 active:scale-95 transition-all"
>
< svg className = "w-5 h-5 text-white" fill = "none" stroke = "currentColor" strokeWidth = { 2 } viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M15.75 19.5L8.25 12l7.5-7.5" / >
< / svg >
< / button >
) }
< div
className = "flex items-center gap-3 min-w-0 cursor-pointer"
onClick = { ( ) = > router . push ( "/pop/home" ) }
>
< div
className = "w-10 h-10 rounded-xl bg-blue-500 flex items-center justify-center shrink-0"
style = { { boxShadow : "0 4px 12px rgba(59,130,246,.35)" } }
>
< svg
className = "w-5 h-5 text-white"
fill = "none"
stroke = "currentColor"
strokeWidth = { 2 }
viewBox = "0 0 24 24"
>
< path
strokeLinecap = "round"
strokeLinejoin = "round"
d = "M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z"
/ >
< / svg >
< / div >
< div className = "flex flex-col min-w-0" >
{ title ? (
< span className = "text-white text-lg font-bold tracking-tight leading-tight truncate" >
{ title }
< / span >
) : (
< >
< span className = "text-white text-lg font-bold tracking-tight leading-tight truncate" >
2026-04-05 17:45:33 +09:00
{ user ? . companyName || "POP" }
2026-04-02 17:39:42 +09:00
< / span >
< span className = "text-white/50 text-xs font-medium leading-tight" >
현 장 관 리 시 스 템
< / span >
< / >
) }
< / div >
< / div >
< / div >
{ /* Center: Clock (desktop) */ }
< div className = "hidden sm:flex flex-col items-center" >
{ mounted && (
< >
< div
className = "flex items-center text-white font-bold text-2xl tracking-wider"
style = { { fontVariantNumeric : "tabular-nums" } }
>
< span > { hours } < / span >
< span
className = "transition-opacity duration-100"
style = { { opacity : colonVisible ? 1 : 0 } }
>
:
< / span >
< span > { minutes } < / span >
< span
className = "transition-opacity duration-100"
style = { { opacity : colonVisible ? 1 : 0 } }
>
:
< / span >
< span > { seconds } < / span >
< / div >
< span className = "text-white/50 text-xs font-medium mt-0.5" > { dateStr } < / span >
< / >
) }
< / div >
{ /* Right: Mobile clock + Profile */ }
< div className = "flex items-center gap-3" >
{ /* Mobile clock */ }
{ mounted && (
< div className = "sm:hidden flex items-center gap-1.5 text-white/60 text-sm" >
< svg
className = "w-4 h-4"
fill = "none"
stroke = "currentColor"
strokeWidth = { 2 }
viewBox = "0 0 24 24"
>
< path
strokeLinecap = "round"
strokeLinejoin = "round"
d = "M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
/ >
< / svg >
< span >
{ hours } : { minutes }
< / span >
< / div >
) }
{ /* Custom header right content (e.g. cart icon) */ }
{ headerRight }
< div className = "hidden sm:block h-5 w-px bg-white/20" / >
{ /* Profile with Dropdown */ }
< div className = "relative" ref = { profileRef } >
< button
onClick = { ( ) = > setProfileOpen ( ( v ) = > ! v ) }
className = "flex items-center gap-2.5 cursor-pointer"
>
< div className = "hidden sm:flex flex-col items-end" >
2026-04-05 17:45:33 +09:00
< span className = "text-sm text-white/90 font-semibold leading-tight" > { displayName } < / span >
< span className = "text-xs text-white/40 font-medium leading-tight" > { deptName } < / span >
2026-04-02 17:39:42 +09:00
< / div >
< div
className = "w-10 h-10 rounded-full bg-blue-500 flex items-center justify-center text-sm font-bold text-white shrink-0 transition-transform active:scale-95"
style = { { boxShadow : "0 2px 8px rgba(59,130,246,.35)" } }
>
2026-04-05 17:45:33 +09:00
{ initial }
2026-04-02 17:39:42 +09:00
< / div >
< / button >
{ /* Profile Dropdown */ }
< div
className = { ` absolute right-0 top-full mt-2 w-56 bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden z-[60] transition-all duration-200 origin-top-right ${
profileOpen
? "opacity-100 scale-100 translate-y-0"
: "opacity-0 scale-95 -translate-y-1 pointer-events-none"
} ` }
>
{ /* User Info */ }
< div className = "px-4 py-3 border-b border-gray-100" >
2026-04-05 17:45:33 +09:00
< p className = "text-sm font-semibold text-gray-900" > { displayName } < / p >
< p className = "text-xs text-gray-400 mt-0.5" > { deptName || user ? . userId } < / p >
2026-04-02 17:39:42 +09:00
< / div >
{ /* Menu Items */ }
< div className = "py-1" >
< button
onClick = { handlePcMode }
className = "flex items-center gap-3 w-full px-4 text-sm text-gray-700 hover:bg-gray-50 active:scale-95 transition-all"
style = { { minHeight : 48 } }
>
< span className = "text-base" > 🖥 < / span >
< span > PC 모 드 전 환 < / span >
< / button >
< button
onClick = { toggleFullscreen }
className = "flex items-center gap-3 w-full px-4 text-sm text-gray-700 hover:bg-gray-50 active:scale-95 transition-all"
style = { { minHeight : 48 } }
>
< span className = "text-base" > 📱 < / span >
< span > 앱 모 드 ( 전 체 화 면 ) < / span >
< / button >
< button
onClick = { handlePopHome }
className = "flex items-center gap-3 w-full px-4 text-sm text-gray-700 hover:bg-gray-50 active:scale-95 transition-all"
style = { { minHeight : 48 } }
>
< span className = "text-base" > 🏠 < / span >
< span > POP 홈 < / span >
< / button >
< / div >
{ /* Logout */ }
< div className = "border-t border-gray-100 py-1" >
< button
onClick = { handleLogout }
className = "flex items-center gap-3 w-full px-4 text-sm text-red-500 hover:bg-red-50 active:scale-95 transition-all"
style = { { minHeight : 48 } }
>
< span className = "text-base" > 🚪 < / span >
< span > 로 그 아 웃 < / span >
< / button >
< / div >
< / div >
< / div >
< / div >
< / header >
{ /* ===== NOTICE BANNER (Marquee) ===== */ }
2026-04-07 16:30:53 +09:00
{ showBanner && bannerEnabled && < div className = "bg-amber-50 border-b border-amber-200 px-4 py-2 flex items-center gap-3" >
2026-04-02 17:39:42 +09:00
< div className = "flex items-center gap-1.5 shrink-0" >
< span className = "text-amber-600 text-sm" > 📢 < / span >
< span className = "text-xs font-bold text-amber-700" > 공 지 < / span >
< / div >
< div className = "overflow-hidden whitespace-nowrap flex-1" >
< div
className = "inline-block text-sm text-amber-800"
style = { {
animation : "popMarquee 30s linear infinite" ,
} }
>
{ marqueeText }
< / div >
< / div >
< / div > }
{ /* ===== MAIN CONTENT ===== */ }
2026-04-10 17:17:23 +09:00
< main className = { fullBleed
? "flex-1 overflow-hidden"
: "max-w-[1400px] mx-auto w-full px-4 sm:px-6 lg:px-8 py-5 sm:py-6 flex flex-col gap-5 sm:gap-6 flex-1 overflow-y-auto"
} >
2026-04-02 17:39:42 +09:00
{ children }
< / main >
2026-04-06 11:47:13 +09:00
{ /* FOOTER 삭제 — POP 화면에서 불필요 */ }
2026-04-02 17:39:42 +09:00
{ /* Marquee keyframes */ }
< style jsx global > { `
@keyframes popMarquee {
0 % {
transform : translateX ( 100 % ) ;
}
100 % {
transform : translateX ( - 100 % ) ;
}
}
` }</style>
< / div >
) ;
}