워크스페이스(탭) 도입 — 로그인 후 기본 화면을 탭 작업공간으로.

- /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:
taekyoungc
2026-06-08 13:32:53 +09:00
parent 600a79788e
commit c15e01bfa7
10 changed files with 381 additions and 14 deletions

View File

@@ -6,6 +6,7 @@ use CodeIgniter\Router\RouteCollection;
* @var RouteCollection $routes * @var RouteCollection $routes
*/ */
$routes->get('/', 'Home::index'); $routes->get('/', 'Home::index');
$routes->get('workspace', 'Home::workspace');
$routes->get('dashboard', 'Home::dashboard'); $routes->get('dashboard', 'Home::dashboard');
$routes->get('dashboard/simple', 'Home::dashboardSimple'); $routes->get('dashboard/simple', 'Home::dashboardSimple');
$routes->get('dashboard/compact', 'Home::dashboardCompact'); $routes->get('dashboard/compact', 'Home::dashboardCompact');

View File

@@ -214,8 +214,10 @@ class Bag extends BaseController
private function render(string $title, string $viewFile, array $data = []): string private function render(string $title, string $viewFile, array $data = []): string
{ {
// 사이트 업무 페이지 공통 셸: gov-portal 디자인(헤더+대메뉴+클릭형 좌측 사이드바). // /workspace 탭(iframe) 안에서는 임베드 레이아웃(헤더·사이드바 없이 본문만).
return view('bag/layout/portal', [ $layout = $this->isEmbeddedRequest() ? 'bag/layout/embed' : 'bag/layout/portal';
return view($layout, [
'title' => $title, 'title' => $title,
'content' => view($viewFile, $data), 'content' => view($viewFile, $data),
]); ]);

View File

@@ -48,6 +48,20 @@ abstract class BaseController extends Controller
* *
* @param array<string, mixed> $contentData * @param array<string, mixed> $contentData
*/ */
/**
* 워크스페이스 탭(iframe) 안에서 열린 요청인지. ?embed=1 또는 Sec-Fetch-Dest=iframe.
* iframe 내 링크 이동·폼 전송·리다이렉트까지 모두 임베드로 처리되도록 헤더로도 판정한다.
*/
protected function isEmbeddedRequest(): bool
{
if ($this->request->getGet('embed') !== null) {
return true;
}
$dest = strtolower(trim((string) $this->request->getHeaderLine('Sec-Fetch-Dest')));
return $dest === 'iframe' || $dest === 'frame';
}
protected function renderWorkPage(string $title, string $contentView, array $contentData = []): string protected function renderWorkPage(string $title, string $contentView, array $contentData = []): string
{ {
$content = view($contentView, $contentData); $content = view($contentView, $contentData);
@@ -61,8 +75,8 @@ abstract class BaseController extends Controller
$path = substr($path, strlen('index.php/')); $path = substr($path, strlen('index.php/'));
} }
if ($path === 'bag' || str_starts_with($path, 'bag/')) { if ($path === 'bag' || str_starts_with($path, 'bag/')) {
// 사이트 업무 페이지: gov-portal 디자인 셸 적용 // /workspace 탭(iframe) 안에서는 임베드 레이아웃, 아니면 gov-portal
return view('bag/layout/portal', [ return view($this->isEmbeddedRequest() ? 'bag/layout/embed' : 'bag/layout/portal', [
'title' => $title, 'title' => $title,
'content' => $content, 'content' => $content,
]); ]);

View File

@@ -10,19 +10,37 @@ class Home extends BaseController
public function index() public function index()
{ {
if (session()->get('logged_in')) { if (session()->get('logged_in')) {
// 메인(/) — gov-portal 디자인 셸 + 종량제 실데이터 대시보드.
helper('admin'); helper('admin');
return view('bag/layout/portal', [ // 워크스페이스 탭(iframe) 안: 대시보드 본문을 임베드로.
if ($this->isEmbeddedRequest()) {
return view('bag/layout/embed', [
'title' => '업무 현황', 'title' => '업무 현황',
'bare' => true, 'bare' => true,
'content' => view('bag/dashboard_portal', $this->portalDashboardData()), 'content' => view('bag/dashboard_portal', $this->portalDashboardData()),
]); ]);
} }
// 로그인 후 기본 화면 = 워크스페이스(탭). 메뉴를 탭으로 열어 작업 상태 유지.
return view('bag/layout/workspace');
}
return view('welcome_message'); return view('welcome_message');
} }
/**
* 워크스페이스 — 메뉴를 탭(iframe)으로 열어두고 작업 상태를 유지하는 화면.
*/
public function workspace()
{
if (! session()->get('logged_in')) {
return redirect()->to(base_url('login'));
}
helper('admin');
return view('bag/layout/workspace');
}
/** /**
* 메인 대시보드용 — 종량제 시스템에 실제 존재하는 데이터만 집계. * 메인 대시보드용 — 종량제 시스템에 실제 존재하는 데이터만 집계.
* *

View File

@@ -33,6 +33,10 @@ tailwind.config = {
</script> </script>
<style>body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; letter-spacing: -0.01em; }</style> <style>body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; letter-spacing: -0.01em; }</style>
</head> </head>
<script>
// iframe(워크스페이스 탭) 안에서 세션 만료로 로그인이 열리면 상위 창 전체를 로그인으로 전환
if (window.top !== window.self) { try { window.top.location.href = <?= json_encode(base_url('login'), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>; } catch (e) {} }
</script>
<body class="bg-portal-bg text-gray-700 flex flex-col min-h-screen font-sans antialiased"> <body class="bg-portal-bg text-gray-700 flex flex-col min-h-screen font-sans antialiased">
<header class="bg-navy text-white h-12 flex items-center justify-between px-4 shrink-0 shadow"> <header class="bg-navy text-white h-12 flex items-center justify-between px-4 shrink-0 shadow">
<a href="<?= base_url() ?>" class="flex items-center gap-2 shrink-0 text-base font-bold tracking-tight hover:opacity-90" title="종량제 시스템"> <a href="<?= base_url() ?>" class="flex items-center gap-2 shrink-0 text-base font-bold tracking-tight hover:opacity-90" title="종량제 시스템">

View 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>

View File

@@ -93,6 +93,10 @@ tailwind.config = {
<?php if ($lgLabel !== ''): ?><strong><?= esc($lgLabel) ?></strong> · <?php endif; ?> <?php if ($lgLabel !== ''): ?><strong><?= esc($lgLabel) ?></strong> · <?php endif; ?>
<?= esc($levelName) ?> · <?= esc($mbName) ?>님 <?= esc($levelName) ?> · <?= esc($mbName) ?>님
</span> </span>
<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>
<?php if ($isAdmin): ?> <?php if ($isAdmin): ?>
<a href="<?= base_url('admin') ?>" title="관리자 페이지" <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;"> 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;">

View File

@@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
/**
* 워크스페이스(탭) 셸 — 크롬처럼 메뉴를 탭(iframe)으로 열어두고 작업 상태 유지.
* 헤더 + 대메뉴(클릭) + 좌측 사이드바(소메뉴) + 탭바 + iframe 패널.
* 탭은 세션(이 셸이 열려 있는 동안)만 유지된다.
*/
helper('admin');
$gov = gov_portal_nav_context(false);
$navPartial = [
'govNavItems' => $gov['navItems'],
'govNavJson' => $gov['navJson'],
'govActiveParentIdx' => $gov['activeParentIdx'],
'govCurrentPath' => gov_portal_nav_match_path($gov['currentPath']),
'govDashboardAliases' => $gov['dashboardAliases'],
'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 : '';
}
?>
<!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"/>
<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"/>
<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'; ?>
html, body { height: 100%; }
body.gov-portal-shell { height: 100vh; overflow: hidden; }
.ws-main { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.ws-tabbar { display: flex; align-items: stretch; gap: 2px; background: #e9eef5; border-bottom: 1px solid var(--border); padding: 4px 6px 0; overflow-x: auto; min-height: 36px; }
.ws-tab { display: inline-flex; align-items: center; gap: .4rem; max-width: 200px; padding: .35rem .6rem; background: #f5f7fa; border: 1px solid var(--border); border-bottom: none; border-radius: 7px 7px 0 0; font-size: .78rem; color: #555; cursor: pointer; white-space: nowrap; }
.ws-tab .t-name { overflow: hidden; text-overflow: ellipsis; max-width: 150px; }
.ws-tab.active { background: #fff; color: var(--navy); font-weight: 700; box-shadow: 0 -2px 0 #007bff inset; }
.ws-tab .t-close { width: 16px; height: 16px; line-height: 14px; text-align: center; border-radius: 50%; color: #999; font-size: 12px; }
.ws-tab .t-close:hover { background: #e2e8f0; color: #333; }
.ws-panels { flex: 1; position: relative; min-height: 0; background: #f0f4f8; }
.ws-frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: none; background: #f0f4f8; }
.ws-frame.active { display: block; }
.ws-empty { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #94a3b8; font-size: .9rem; gap: .5rem; }
</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('workspace')]) ?>
<?= view('home/_dashboard_gov_portal_topnav_click', $navPartial) ?>
<div class="portal-header-utils" style="display:flex;align-items:center;gap:.5rem;">
<span class="user-line">
<?php if ($lgLabel !== ''): ?><strong><?= esc($lgLabel) ?></strong> · <?php endif; ?>
<?= esc($levelName) ?> · <?= esc($mbName) ?>님
</span>
<?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;"><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;"><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="ws-main">
<div class="ws-tabbar" id="wsTabBar" role="tablist"></div>
<div class="ws-panels" id="wsPanels">
<div class="ws-empty" id="wsEmpty">
<i class="fa-regular fa-window-restore" style="font-size:1.6rem;opacity:.5;"></i>
<div>왼쪽 메뉴를 클릭하면 탭으로 열립니다. 여러 화면을 열어두고 전환해도 작업 내용이 유지됩니다.</div>
</div>
</div>
</main>
</div>
<?= view('home/_dashboard_gov_portal_nav_script_base', $navPartial) ?>
<script>
(function () {
var bar = document.getElementById('wsTabBar');
var panels = document.getElementById('wsPanels');
var empty = document.getElementById('wsEmpty');
var tabs = {}; // id -> {url,title,frame,btn}
var order = [];
var activeId = null;
var MAX = 12;
function norm(u) { try { var a = new URL(u, location.origin); return a.pathname + a.search; } catch (e) { return u; } }
function withEmbed(u) {
try { var a = new URL(u, location.origin); a.searchParams.set('embed', '1'); return a.pathname + a.search; }
catch (e) { return u + (u.indexOf('?') >= 0 ? '&' : '?') + 'embed=1'; }
}
function activate(id) {
if (!tabs[id]) return;
activeId = id;
Object.keys(tabs).forEach(function (k) {
tabs[k].frame.classList.toggle('active', k === id);
tabs[k].btn.classList.toggle('active', k === id);
});
if (empty) empty.style.display = 'none';
}
function closeTab(id) {
if (!tabs[id]) return;
tabs[id].frame.remove();
tabs[id].btn.remove();
delete tabs[id];
order = order.filter(function (x) { return x !== id; });
if (activeId === id) {
var next = order[order.length - 1];
if (next) activate(next); else { activeId = null; if (empty) empty.style.display = 'flex'; }
}
}
function openTab(url, title) {
var id = norm(url);
if (tabs[id]) { activate(id); return; }
if (order.length >= MAX) { closeTab(order[0]); } // 오래된 탭 정리
var frame = document.createElement('iframe');
frame.className = 'ws-frame';
frame.src = withEmbed(url);
frame.setAttribute('title', title || '탭');
panels.appendChild(frame);
var btn = document.createElement('div');
btn.className = 'ws-tab';
btn.setAttribute('role', 'tab');
var nameSpan = document.createElement('span');
nameSpan.className = 't-name';
nameSpan.textContent = title || '탭';
var close = document.createElement('span');
close.className = 't-close';
close.textContent = '×';
btn.appendChild(nameSpan); btn.appendChild(close);
btn.addEventListener('click', function (e) { if (e.target === close) { closeTab(id); } else { activate(id); } });
bar.appendChild(btn);
tabs[id] = { url: url, title: title, frame: frame, btn: btn };
order.push(id);
activate(id);
}
window.wsOpenTab = openTab;
// 좌측 사이드바 소메뉴 클릭 → 탭으로 열기 (전체 페이지 이동 대신)
document.querySelector('.sidebar').addEventListener('click', function (e) {
var a = e.target.closest('a[href]');
if (!a) return;
var href = a.getAttribute('href') || '';
// 외부/특수 링크(로그아웃, 지자체선택 등 사이드바 하단 블록)는 그대로 이동
if (a.closest('.sidebar-blocks')) return;
if (/\/logout|\/login/.test(href) || href.charAt(0) === '#' || href === '') return;
e.preventDefault();
openTab(href, (a.textContent || '').trim());
});
// 상단 대메뉴의 직접 링크(자식 없는 대메뉴)도 탭으로
document.querySelectorAll('.portal-nav-link[href]').forEach(function (lnk) {
lnk.addEventListener('click', function (e) {
var href = lnk.getAttribute('href') || '';
if (!href || href.charAt(0) === '#') return;
e.preventDefault();
openTab(href, (lnk.textContent || '').trim());
});
});
// 첫 화면: 대시보드 탭 자동 열기
openTab('<?= base_url('/') ?>', '업무 현황');
})();
</script>
</body>
</html>

View File

@@ -8,14 +8,15 @@ const { login } = require('./helpers/auth');
* - 대메뉴 클릭 → 좌측 사이드바에 소메뉴 표시 * - 대메뉴 클릭 → 좌측 사이드바에 소메뉴 표시
*/ */
test.describe('gov-portal 전면 적용', () => { test.describe('gov-portal 전면 적용', () => {
test('메인(/)이 포털 대시보드로 렌더', async ({ page }) => { test('메인(/)이 워크스페이스(탭) + 첫 탭 대시보드로 렌더', async ({ page }) => {
await login(page, 'user'); await login(page, 'user');
await page.goto('/'); await page.goto('/');
await expect(page.locator('header.portal-header')).toBeVisible(); await expect(page.locator('header.portal-header')).toBeVisible();
await expect(page.locator('.sidebar')).toBeVisible(); await expect(page.locator('.sidebar')).toBeVisible();
await expect(page.getByText('봉투 재고 총량')).toBeVisible(); await expect(page.locator('.ws-tabbar')).toBeVisible();
await expect(page.getByText('승인 대기')).toBeVisible(); // 첫 탭(대시보드) iframe 안에 실데이터 KPI
// 목업 흔적(가짜 공지/지도)이 없어야 함 await expect(page.frameLocator('.ws-frame.active').getByText('봉투 재고 총량')).toBeVisible({ timeout: 10000 });
// 목업 흔적 없음
await expect(page.getByText('서비스 데스크')).toHaveCount(0); await expect(page.getByText('서비스 데스크')).toHaveCount(0);
}); });

42
e2e/workspace.spec.js Normal file
View File

@@ -0,0 +1,42 @@
const { test, expect } = require('@playwright/test');
const { login } = require('./helpers/auth');
/**
* 워크스페이스(탭) — 메뉴를 탭(iframe)으로 열고 전환해도 작업 상태 유지
*/
test.describe('워크스페이스 탭', () => {
test('탭 열기·전환·상태 유지·닫기', async ({ page }) => {
await login(page, 'admin');
await page.goto('/admin/select-local-government');
await page.evaluate(() => {
const r = document.querySelector('input[name="lg_idx"][value="1"]');
if (r) { r.checked = true; r.form.submit(); }
});
await page.waitForTimeout(700);
await page.goto('/workspace');
await page.waitForTimeout(2500);
// 대시보드 탭이 자동으로 열림
await expect(page.locator('.ws-tab')).toHaveCount(1);
await expect(page.locator('.ws-frame.active')).toBeVisible();
// 대시보드 탭에 입력
await page.frameLocator('.ws-frame.active').locator('#mainMenuSearch').fill('WS_STATE', { timeout: 8000 });
// 사이드바 메뉴 클릭 → 새 탭
await page.locator('.sidebar .my-menu-list a').first().click();
await page.waitForTimeout(1500);
await expect(page.locator('.ws-tab')).toHaveCount(2);
// 첫 탭으로 복귀 → 입력값 유지 확인
await page.locator('.ws-tab').first().click();
await page.waitForTimeout(400);
await expect(page.frameLocator('.ws-frame.active').locator('#mainMenuSearch')).toHaveValue('WS_STATE');
// 탭 닫기
await page.locator('.ws-tab').nth(1).locator('.t-close').click();
await page.waitForTimeout(300);
await expect(page.locator('.ws-tab')).toHaveCount(1);
});
});