Files
jongryangje/app/Views/bag/layout/embed.php
taekyoungc 4d9343e980 feat: GBLS 리브랜딩 + 매뉴얼 보강 + 워크스페이스/코드관리 UX 개선
리브랜딩
- 서비스명 "종량제 시스템" → "GBLS", 헤더 로고에 풀네임(Garbage Bag Logistics System) 병기
  (gov-portal·공통 브랜드·로그인/welcome 셸·타이틀·푸터 전반)

매뉴얼
- 신규 페이지 [로그인·회원가입·계정](01_account.md): 가입 항목·관리자 승인·2차 인증 흐름
- [화면 구성·워크스페이스·단축키]에 계정 전환 시 탭 초기화 안내 추가

워크스페이스(탭)
- 탭 전환 시 좌측 사이드바 강조 동기화(메뉴 없는 화면은 강조 해제, 경로 폴백 매칭)
- 소메뉴 좌측 아이콘(▸/·) 전부 제거 — 활성 메뉴는 배경 강조로만 구분
- 탭을 사용자(mb_idx)별로 격리: 다른 아이디 로그인 시 이전 탭 복원 안 함
- 사이드바 FAQ 링크 제거(자주 묻는 질문은 매뉴얼에 통합)

기본 코드관리 화면
- 업무현황 카드 스타일로 재디자인(가벼운 표·상태/범위 pill·단일 구분선)
- render()에 $bare 옵션 추가 → 이미 카드형인 화면은 바깥 래퍼 생략

기타
- .claude/settings.local.json(개인 권한 설정) .gitignore 추가
- e2e: 워크스페이스(동기화·계정격리) + 매뉴얼(계정·단축키·검색) 케이스 보강

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 14:43:24 +09:00

161 lines
7.7 KiB
PHP

<?php
declare(strict_types=1);
/**
* 임베드(탭/iframe) 전용 레이아웃 — 헤더·사이드바 없이 본문만.
* /workspace 탭 iframe 안에서 업무 페이지를 중첩 크롬 없이 표시한다.
*
* @var string $title
* @var string $content
* @var bool $bare true면 본문을 카드 래퍼 없이 그대로 출력(대시보드용)
*/
$bare = ! empty($bare);
helper('admin');
$helpUrl = function_exists('manual_help_url_for_path') ? manual_help_url_for_path() : '';
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title><?= esc($title ?? 'GBLS') ?></title>
<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>
body { margin: 0; background: #f0f4f8; font-family: 'Pretendard', 'Malgun Gothic', 'Noto Sans KR', sans-serif; letter-spacing: -0.01em; -webkit-font-smoothing: antialiased; }
.embed-titlebar { display: flex; align-items: center; gap: .5rem; font-size: 1.05rem; font-weight: 800; color: #1a2b4b; letter-spacing: -0.03em; margin: 0 0 0.75rem; }
.embed-flash { margin-bottom: .75rem; padding: .6rem .9rem; border-radius: 8px; font-size: .8125rem; }
.embed-flash.ok { background: #ecfdf5; border: 1px solid #a7f3d0; color: #065f46; }
.embed-flash.err { background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; }
.data-table { width: 100%; border-collapse: collapse; font-family: 'Pretendard', 'Malgun Gothic', 'Noto Sans KR', sans-serif; }
.data-table th, .data-table td { border: 1px solid #ccc; padding: 4px 8px; white-space: nowrap; font-size: 13px; }
.data-table th { background-color: #e9ecef; text-align: center; vertical-align: middle; font-weight: bold; color: #333; }
.data-table tbody tr:nth-child(even) td { background-color: #f9f9f9; }
.data-table tbody tr:hover td { background-color: #e6f7ff !important; }
@media print { .no-print { display: none !important; } .embed-titlebar { display: none; } }
</style>
</head>
<body>
<div style="padding: 0.875rem 1rem 1.25rem;">
<?php if (! empty($title) || $helpUrl !== ''): ?>
<div style="display:flex;align-items:center;justify-content:space-between;gap:.5rem;margin:0 0 .75rem;">
<h1 class="embed-titlebar" style="margin:0;"><?php if (! empty($title)): ?><i class="fa-solid fa-folder-open" style="color:#007bff;"></i><?= esc($title) ?><?php endif; ?></h1>
<?php if ($helpUrl !== ''): ?>
<a href="<?= esc($helpUrl, 'attr') ?>" class="embed-help no-print" style="display:inline-flex;align-items:center;gap:.3rem;padding:.3rem .6rem;border-radius:6px;background:#eef2f7;border:1px solid #dde4ec;color:#1a2b4b;text-decoration:none;font-size:.75rem;font-weight:600;white-space:nowrap;">
<i class="fa-regular fa-circle-question"></i> 이 화면 설명
</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (session()->getFlashdata('success')): ?>
<div class="embed-flash ok"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="embed-flash err"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<?php if ($bare): ?>
<?= $content ?>
<?php else: ?>
<div class="bg-white border border-[#dde4ec] rounded-xl shadow-sm p-4">
<?= $content ?>
</div>
<?php endif; ?>
</div>
<script>
(function () {
// 세션 만료 등으로 iframe 안에서 로그인 페이지가 열리면 상위 프레임을 로그인으로 보낸다.
if (window.top !== window.self && /\/login(\/|$)/.test(location.pathname)) {
try { window.top.location.href = location.href; } catch (e) {}
}
// 방문한 업무 메뉴 경로 기록 (대시보드 "최근 방문 메뉴"용). localStorage 는 동일 출처 탭과 공유된다.
try {
var rp = (location.pathname || '').replace(/\/+$/, '') || '/';
if (rp !== '/' && !/\/login|\/logout|\/register|\/manual/.test(rp)) {
var RK = 'jrj_recent_menus';
var ra = JSON.parse(localStorage.getItem(RK) || '[]');
if (!Array.isArray(ra)) ra = [];
ra = ra.filter(function (x) { return x && x.p && x.p !== rp; });
ra.unshift({ p: rp, t: Date.now() });
localStorage.setItem(RK, JSON.stringify(ra.slice(0, 12)));
}
} catch (e) {}
// 탭(iframe) 안에서의 링크·폼 이동은 항상 embed 유지 → 중첩 헤더/사이드바 방지
function withEmbed(href) {
try {
var u = new URL(href, location.href);
if (u.origin !== location.origin) return null;
if (u.searchParams.get('embed') === '1') return null;
u.searchParams.set('embed', '1');
return u.href;
} catch (e) { return null; }
}
// "이 화면 설명" → 워크스페이스 새 탭으로 매뉴얼 열기(없으면 새 창)
document.addEventListener('click', function (e) {
var h = e.target.closest ? e.target.closest('a.embed-help') : null;
if (!h) return;
e.preventDefault(); e.stopPropagation();
var url = h.getAttribute('href');
try {
if (window.parent && window.parent !== window && typeof window.parent.wsOpenTab === 'function') {
window.parent.wsOpenTab(url, '도움말'); return;
}
} catch (err) {}
window.open(url, '_blank');
}, true);
document.addEventListener('click', function (e) {
var a = e.target.closest ? e.target.closest('a[href]') : null;
if (!a || a.classList.contains('embed-help')) return;
var t = (a.getAttribute('target') || '').toLowerCase();
if (t && t !== '_self') return;
if (a.hasAttribute('download')) return;
var href = a.getAttribute('href') || '';
if (!href || href.charAt(0) === '#' || /^(javascript:|mailto:|tel:|data:)/i.test(href)) return;
var ne = withEmbed(href);
if (ne) a.setAttribute('href', ne);
}, true);
document.addEventListener('submit', function (e) {
var f = e.target;
if (!f || f.tagName !== 'FORM') return;
var ne = withEmbed(f.getAttribute('action') || location.href);
if (ne) f.setAttribute('action', ne);
}, true);
// 표 '번호' 컬럼 역순 채번 (사이트 레이아웃과 동일)
var run = function () {
document.querySelectorAll('table').forEach(function (table) {
var head = table.querySelector('thead tr'); if (!head) return;
var ths = Array.prototype.slice.call(head.querySelectorAll('th'));
var col = ths.findIndex(function (th) { return (th.textContent || '').replace(/\s+/g, '').trim() === '번호'; });
if (col < 0) return;
var body = table.querySelector('tbody'); if (!body) return;
var rows = Array.prototype.slice.call(body.querySelectorAll(':scope > tr')).filter(function (tr) {
var c = tr.querySelectorAll('td'); if (!c.length) return false;
if (c.length === 1 && Number(c[0].getAttribute('colspan') || '1') > 1) return false; return true;
});
var n = rows.length;
rows.forEach(function (tr) { var c = tr.querySelectorAll('td'); if (c[col]) c[col].textContent = String(n--); });
});
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run, { once: true }); else run();
})();
</script>
</body>
</html>