워크스페이스(탭) 도입 — 로그인 후 기본 화면을 탭 작업공간으로.
- /workspace: 헤더+사이드바+탭바+iframe 패널. 메뉴 클릭=탭 열기, 전환해도 폼·스크롤·조회결과 등 작업 상태 유지(세션 동안) - 로그인 후 / = 워크스페이스(첫 탭=대시보드). iframe 내부는 임베드 렌더 - 임베드 레이아웃(bag/layout/embed): 헤더·사이드바 없이 본문만 - 임베드 판정: ?embed=1 또는 Sec-Fetch-Dest=iframe (iframe 내 링크·폼· 리다이렉트까지 중첩 크롬 없이 처리) - iframe 안 세션만료 시 상위 창을 로그인으로 전환(auth/_shell) - 포털 헤더에 워크스페이스 진입 링크, E2E(workspace.spec) 추가 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
96
app/Views/bag/layout/embed.php
Normal file
96
app/Views/bag/layout/embed.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* 임베드(탭/iframe) 전용 레이아웃 — 헤더·사이드바 없이 본문만.
|
||||
* /workspace 탭 iframe 안에서 업무 페이지를 중첩 크롬 없이 표시한다.
|
||||
*
|
||||
* @var string $title
|
||||
* @var string $content
|
||||
* @var bool $bare true면 본문을 카드 래퍼 없이 그대로 출력(대시보드용)
|
||||
*/
|
||||
$bare = ! empty($bare);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title><?= esc($title ?? '종량제 시스템') ?></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)): ?>
|
||||
<h1 class="embed-titlebar"><i class="fa-solid fa-folder-open" style="color:#007bff;"></i><?= esc($title) ?></h1>
|
||||
<?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) {}
|
||||
}
|
||||
// 표 '번호' 컬럼 역순 채번 (사이트 레이아웃과 동일)
|
||||
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>
|
||||
Reference in New Issue
Block a user