2026-06-08 09:47:36 +09:00
< ? php
declare ( strict_types = 1 );
/**
* 사이트 업무 페이지 공통 셸 — gov - portal 디자인 적용판 .
* 헤더 + 대메뉴 ( 클릭 ) + 좌측 사이드바 ( 소메뉴 ) + 본문 ( $content ) .
* 본문은 기존 Tailwind 마크업을 그대로 쓰므로 Tailwind CDN·config·data - table 스타일을 함께 로드한다 .
*
* @ var string $title
* @ var string $content ( 이스케이프 없이 출력되는 본문 HTML )
* @ var bool $bare true면 work - surface 카드 / 제목바 없이 본문을 그대로 출력 ( 대시보드용 )
*/
helper ( 'admin' );
$bare = ! empty ( $bare );
$gov = gov_portal_nav_context ( false ); // 업무 셸: 실제 bag/* 링크 유지(코드관리 포털 remap 안 함)
$govActiveChildHref = gov_portal_nav_match_path ( $gov [ 'currentPath' ]);
$navPartial = [
'govNavItems' => $gov [ 'navItems' ],
'govNavJson' => $gov [ 'navJson' ],
'govActiveParentIdx' => $gov [ 'activeParentIdx' ],
'govCurrentPath' => gov_portal_nav_match_path ( $gov [ 'currentPath' ]),
'govDashboardAliases' => $gov [ 'dashboardAliases' ],
'govActiveChildHref' => $govActiveChildHref ,
];
$mbLevel = ( int ) session () -> get ( 'mb_level' );
$mbName = ( string ) ( session () -> get ( 'mb_name' ) ? ? '담당자' );
$levelName = config ( \Config\Roles :: class ) -> getLevelName ( $mbLevel );
$isAdmin = ( $mbLevel === \Config\Roles :: LEVEL_SUPER_ADMIN || $mbLevel === \Config\Roles :: LEVEL_LOCAL_ADMIN );
$effectiveLgIdx = admin_effective_lg_idx ();
$lgLabel = '' ;
if ( $effectiveLgIdx ) {
$lgRow = model ( \App\Models\LocalGovernmentModel :: class ) -> find ( $effectiveLgIdx );
$lgLabel = $lgRow ? ( string ) $lgRow -> lg_name : '' ;
}
2026-06-08 19:04:41 +09:00
$helpUrl = function_exists ( 'manual_help_url_for_path' ) ? manual_help_url_for_path () : '' ;
2026-06-08 09:47:36 +09:00
?>
<! DOCTYPE html >
< html lang = " ko " class = " gov-portal-html " >
< head >
< meta charset = " utf-8 " />
< meta content = " width=device-width, initial-scale=1.0 " name = " viewport " />
2026-06-09 14:43:24 +09:00
< title >< ? = esc ( $title ? ? 'GBLS' ) ?> </title>
2026-06-08 09:47:36 +09:00
< link href = " https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css " rel = " stylesheet " />
< link rel = " stylesheet " href = " https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css " />
< script src = " https://cdn.tailwindcss.com?plugins=forms,container-queries " ></ script >
< script >
tailwind . config = {
theme : {
extend : {
fontFamily : { sans : [ 'Pretendard' , '"Malgun Gothic"' , '"Noto Sans KR"' , 'sans-serif' ] },
colors : {
'system-header' : '#ffffff' ,
'title-bar' : '#1a2b4b' ,
'control-panel' : '#f8f9fa' ,
'btn-search' : '#243a5e' ,
'btn-excel-border' : '#28a745' ,
'btn-excel-text' : '#28a745' ,
'btn-print-border' : '#ced4da' ,
'btn-exit' : '#d9534f' ,
},
fontSize : { 'xxs' : '0.65rem' }
}
}
}
</ script >
< style >
< ? php include __DIR__ . '/../../home/_dashboard_gov_portal_brand_css.php' ; ?>
< ? php include __DIR__ . '/../../home/_dashboard_gov_portal_topnav_css.php' ; ?>
< ? php include __DIR__ . '/../../home/_dashboard_gov_portal_chrome_css.php' ; ?>
/* 업무 본문 표/유틸 (기존 사이트 레이아웃에서 계승) */
. data - table { width : 100 % ; border - collapse : collapse ; font - family : 'Pretendard' , 'Malgun Gothic' , 'Noto Sans KR' , sans - serif ; }
2026-06-11 17:26:36 +09:00
. data - table { font - size : 13 px ; }
. data - table th , . data - table td { text - align : left ; padding : 0.55 rem 0.5 rem ; white - space : nowrap ; border : 0 ; border - bottom : 1 px solid #e5e7eb; }
. data - table thead th { font - size : 0.6875 rem ; font - weight : 600 ; color : #6b7280; background: transparent; vertical-align: middle; }
. data - table tbody td { color : #374151; }
. data - table tbody tr : last - child td { border - bottom : 0 ; }
. data - table tbody tr : hover td { background - color : #f9fafb; }
2026-06-08 09:47:36 +09:00
@ media print {
. portal - header , . sidebar , . portal - footer , . no - print , nav . portal - top - nav { display : none ! important ; }
body . gov - portal - shell { background : #fff; display: block; }
. gov - portal - shell . main . work - main { overflow : visible ! important ; padding : 0 ! important ; }
. print - header { display : block ! important ; }
}
</ style >
</ head >
< body class = " gov-portal-shell " >
< header class = " portal-header " >
< div class = " portal-header-inner " >
< ? = view ( 'home/_dashboard_gov_portal_brand' , [ 'brandHref' => base_url ( '/' )]) ?>
< ? = view ( 'home/_dashboard_gov_portal_topnav_click' , $navPartial ) ?>
< div class = " portal-header-utils " style = " display:flex;align-items:center;gap:.5rem; " >
2026-06-11 17:26:36 +09:00
< div class = " ws-fontctl " title = " 글씨 크기 조절 " style = " display:inline-flex;align-items:center;gap:2px;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.25);border-radius:6px;padding:1px; " >
< button type = " button " id = " wsFontMinus " title = " 글씨 작게 " style = " width:24px;height:22px;border:0;background:transparent;color:#fff;cursor:pointer;font-size:11px;line-height:1;border-radius:5px; " > A− </ button >
< span id = " wsFontPct " style = " min-width:34px;text-align:center;color:#fff;font-size:.68rem;font-weight:600; " > 100 %</ span >
< button type = " button " id = " wsFontPlus " title = " 글씨 크게 " style = " width:24px;height:22px;border:0;background:transparent;color:#fff;cursor:pointer;font-size:14px;line-height:1;border-radius:5px; " > A +</ button >
</ div >
2026-06-08 09:47:36 +09:00
< span class = " user-line " >
< ? php if ( $lgLabel !== '' ) : ?> <strong><?= esc($lgLabel) ?></strong> · <?php endif; ?>
< ? = esc ( $levelName ) ?> · <?= esc($mbName) ?>님
</ span >
2026-06-08 13:32:53 +09:00
< a href = " <?= base_url('workspace') ?> " title = " 워크스페이스(탭으로 여러 화면 열기) "
style = " display:inline-flex;align-items:center;gap:.3rem;padding:.25rem .6rem;border-radius:6px;background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.3);color:#fff;text-decoration:none;font-size:.75rem;font-weight:600;white-space:nowrap; " >
< i class = " fa-regular fa-window-restore " ></ i > 워크스페이스
</ a >
2026-06-08 09:47:36 +09:00
< ? php if ( $isAdmin ) : ?>
< a href = " <?= base_url('admin') ?> " title = " 관리자 페이지 "
style = " display:inline-flex;align-items:center;gap:.3rem;padding:.25rem .6rem;border-radius:6px;background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.3);color:#fff;text-decoration:none;font-size:.75rem;font-weight:600;white-space:nowrap; " >
< i class = " fa-solid fa-gear " ></ i > 관리자
</ a >
< ? php endif ; ?>
< a href = " <?= base_url('logout') ?> " title = " 로그아웃 "
style = " display:inline-flex;align-items:center;gap:.3rem;padding:.25rem .6rem;border-radius:6px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.22);color:#fff;text-decoration:none;font-size:.75rem;font-weight:600;white-space:nowrap; " >
< i class = " fa-solid fa-right-from-bracket " ></ i > 로그아웃
</ a >
</ div >
</ div >
</ header >
< div class = " layout " >
< ? = view ( 'home/_dashboard_gov_portal_sidebar' , $navPartial ) ?>
< main class = " main work-main main-content-area " >
< ? php if ( ! $bare && ! empty ( $title )) : ?>
2026-06-08 19:04:41 +09:00
< div style = " display:flex;align-items:center;justify-content:space-between;gap:.5rem; " >
< h1 class = " work-titlebar " style = " margin-bottom:0; " >< i class = " fa-solid fa-folder-open tb-ico " ></ i >< ? = esc ( $title ) ?> </h1>
< ? php if ( $helpUrl !== '' ) : ?>
2026-06-11 17:26:36 +09:00
< a href = " <?= esc( $helpUrl , 'attr') ?> " rel = " noopener " class = " no-print portal-help " style = " display:inline-flex;align-items:center;gap:.3rem;padding:.3rem .6rem;border-radius:6px;background:#fff;border:1px solid var(--border);color:#1a2b4b;text-decoration:none;font-size:.75rem;font-weight:600;white-space:nowrap; " >
2026-06-08 19:04:41 +09:00
< i class = " fa-regular fa-circle-question " ></ i > 이 화면 설명
</ a >
< ? php endif ; ?>
</ div >
2026-06-08 09:47:36 +09:00
< ? php endif ; ?>
< ? php if ( session () -> getFlashdata ( 'success' )) : ?>
< div class = " work-flash ok " >< ? = esc ( session () -> getFlashdata ( 'success' )) ?> </div>
< ? php endif ; ?>
< ? php if ( session () -> getFlashdata ( 'error' )) : ?>
< div class = " work-flash err " >< ? = esc ( session () -> getFlashdata ( 'error' )) ?> </div>
< ? php endif ; ?>
< ? php if ( $bare ) : ?>
< ? = $content ?>
< ? php else : ?>
< div class = " work-surface " >
< ? = $content ?>
</ div >
< ? php endif ; ?>
</ main >
</ div >
< footer class = " portal-footer " >
2026-06-09 14:43:24 +09:00
< span > GBLS </ span >
2026-06-08 09:47:36 +09:00
< span >< ? = date ( 'Y.m.d (D) H:i' ) ?> </span>
</ footer >
< ? = view ( 'home/_dashboard_gov_portal_nav_script_base' , $navPartial ) ?>
< script >
2026-06-08 12:10:54 +09:00
( function () {
// 방문한 업무 메뉴 경로 기록 (메인 메뉴검색의 "최근 방문 메뉴"용)
try {
var p = ( location . pathname || '' ) . replace ( / \ /+ $ / , '' ) || '/' ;
if ( p === '/' || / \ / login | \ / logout | \ / register /. test ( p )) return ;
var KEY = 'jrj_recent_menus' ;
var arr = JSON . parse ( localStorage . getItem ( KEY ) || '[]' );
if ( ! Array . isArray ( arr )) arr = [];
arr = arr . filter ( function ( x ) { return x && x . p && x . p !== p ; });
arr . unshift ({ p : p , t : Date . now () });
localStorage . setItem ( KEY , JSON . stringify ( arr . slice ( 0 , 12 )));
} catch ( e ) {}
})();
</ script >
< script >
2026-06-08 12:26:19 +09:00
( function () {
// bfcache(뒤로가기/탭 복귀) 복원 시 열린 채 남은 전체화면 모달·팝업으로 인해
// 회색 레이어가 화면을 덮고 클릭이 막히는 문제 방지 — 복원 시 강제로 닫는다.
function closeStuckOverlays () {
document . querySelectorAll ( '.fixed.inset-0[id$="-modal"], .fixed.inset-0[id$="-popup"]' ) . forEach ( function ( el ) {
el . classList . add ( 'hidden' );
el . setAttribute ( 'aria-hidden' , 'true' );
});
document . body . style . overflow = '' ;
}
window . addEventListener ( 'pageshow' , function ( e ) { if ( e . persisted ) closeStuckOverlays (); });
window . addEventListener ( 'pagehide' , closeStuckOverlays );
})();
</ script >
< script >
2026-06-08 09:47:36 +09:00
(() => {
// 표의 '번호' 컬럼 역순 자동 채번 (기존 사이트 레이아웃 계승)
const normalize = ( s ) => String ( s || '' ) . replace ( / \s +/ g , '' ) . trim ();
const renumberTable = ( table ) => {
const headRow = table . querySelector ( 'thead tr' );
if ( ! headRow ) return ;
const headers = Array . from ( headRow . querySelectorAll ( 'th' ));
const numberCol = headers . findIndex (( th ) => normalize ( th . textContent ) === '번호' );
if ( numberCol < 0 ) return ;
const body = table . querySelector ( 'tbody' );
if ( ! body ) return ;
const rows = Array . from ( body . querySelectorAll ( ':scope > tr' )) . filter (( tr ) => {
const cells = tr . querySelectorAll ( 'td' );
if ( cells . length === 0 ) return false ;
if ( cells . length === 1 && Number ( cells [ 0 ] . getAttribute ( 'colspan' ) || '1' ) > 1 ) return false ;
return true ;
});
let no = rows . length ;
rows . forEach (( tr ) => {
const cells = tr . querySelectorAll ( 'td' );
if ( cells [ numberCol ]) cells [ numberCol ] . textContent = String ( no -- );
});
};
const run = () => document . querySelectorAll ( 'table' ) . forEach ( renumberTable );
if ( document . readyState === 'loading' ) document . addEventListener ( 'DOMContentLoaded' , run , { once : true });
else run ();
})();
</ script >
2026-06-11 17:26:36 +09:00
<!-- 화면 설명 드로어 ( 팝업 ) -->
< style >
. help - drawer { position : fixed ; top : 0 ; right : 0 ; bottom : 0 ; width : min ( 460 px , 92 vw ); background : #fff; box-shadow: -8px 0 26px rgba(0,0,0,.18); z-index: 9999; display: none; flex-direction: column; }
. help - drawer . open { display : flex ; }
. help - drawer - head { display : flex ; align - items : center ; justify - content : space - between ; padding : . 5 rem . 75 rem ; background : #1a2b4b; color: #fff; font-size: .8rem; font-weight: 700; flex-shrink: 0; }
. help - drawer - head . hd - btn { color : #fff; background: rgba(255,255,255,.14); border: 0; width: 26px; height: 26px; border-radius: 6px; cursor: pointer; font-size: 13px; display: inline-flex; align-items: center; justify-content: center; text-decoration: none; margin-left: 4px; }
. help - drawer - head . hd - btn : hover { background : rgba ( 255 , 255 , 255 , . 28 ); }
. help - drawer iframe { flex : 1 ; width : 100 % ; border : 0 ; }
. help - drawer - grip { position : absolute ; left : - 4 px ; top : 0 ; bottom : 0 ; width : 8 px ; cursor : col - resize ; }
</ style >
< div id = " helpDrawer " class = " help-drawer no-print " aria - hidden = " true " >
< div class = " help-drawer-grip " id = " helpDrawerGrip " ></ div >
< div class = " help-drawer-head " >
< span >< i class = " fa-regular fa-circle-question " ></ i > 화면 설명 </ span >
< div >< button type = " button " id = " helpDrawerClose " class = " hd-btn " title = " 닫기 " >& times ; </ button ></ div >
</ div >
< iframe id = " helpDrawerFrame " title = " 화면 설명 " ></ iframe >
</ div >
< script >
( function () {
var drawer = document . getElementById ( 'helpDrawer' ), dFrame = document . getElementById ( 'helpDrawerFrame' );
function openHelp ( url ) {
var u = url ; try { var x = new URL ( url , location . href ); x . searchParams . set ( 'embed' , '1' ); u = x . href ; } catch ( e ) {}
if ( dFrame . getAttribute ( 'data-src' ) !== u ) { dFrame . src = u ; dFrame . setAttribute ( 'data-src' , u ); }
drawer . classList . add ( 'open' );
}
function closeHelp () { drawer . classList . remove ( 'open' ); }
document . addEventListener ( 'click' , function ( e ) {
var h = e . target . closest ? e . target . closest ( 'a.portal-help' ) : null ;
if ( ! h ) return ;
e . preventDefault ();
openHelp ( h . getAttribute ( 'href' ));
});
document . getElementById ( 'helpDrawerClose' ) . addEventListener ( 'click' , closeHelp );
document . addEventListener ( 'keydown' , function ( e ) { if ( e . key === 'Escape' ) closeHelp (); });
var grip = document . getElementById ( 'helpDrawerGrip' ), dragging = false ;
grip . addEventListener ( 'mousedown' , function ( e ) { e . preventDefault (); dragging = true ; document . body . style . userSelect = 'none' ; });
document . addEventListener ( 'mousemove' , function ( e ) { if ( ! dragging ) return ; var w = window . innerWidth - e . clientX ; drawer . style . width = Math . min ( window . innerWidth * 0.92 , Math . max ( 300 , w )) + 'px' ; });
document . addEventListener ( 'mouseup' , function () { dragging = false ; document . body . style . userSelect = '' ; });
// 글씨 크기 조절 — 본문(.work-main) 영역에 zoom 적용. 헤더/사이드바는 그대로.
var FONT_KEY = 'jrj_font_scale' ;
var target = document . querySelector ( '.work-main' ) || document . body ;
function curScale () { var s = parseInt ( localStorage . getItem ( FONT_KEY ) || '100' , 10 ); return ( s >= 70 && s <= 150 ) ? s : 100 ; }
function applyScale ( s ) {
s = Math . min ( 150 , Math . max ( 70 , s ));
try { localStorage . setItem ( FONT_KEY , String ( s )); } catch ( e ) {}
target . style . zoom = ( s / 100 );
var pct = document . getElementById ( 'wsFontPct' ); if ( pct ) pct . textContent = s + '%' ;
}
applyScale ( curScale ());
var plus = document . getElementById ( 'wsFontPlus' ), minus = document . getElementById ( 'wsFontMinus' );
if ( plus ) plus . addEventListener ( 'click' , function () { applyScale ( curScale () + 10 ); });
if ( minus ) minus . addEventListener ( 'click' , function () { applyScale ( curScale () - 10 ); });
})();
</ script >
2026-06-08 09:47:36 +09:00
</ body >
</ html >