From c15e01bfa7653782fd1edb6bccfd2261e97aa795 Mon Sep 17 00:00:00 2001 From: taekyoungc Date: Mon, 8 Jun 2026 13:32:53 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9B=8C=ED=81=AC=EC=8A=A4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4(=ED=83=AD)=20=EB=8F=84=EC=9E=85=20=E2=80=94=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=9B=84=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=9D=84=20=ED=83=AD=20=EC=9E=91=EC=97=85?= =?UTF-8?q?=EA=B3=B5=EA=B0=84=EC=9C=BC=EB=A1=9C.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /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 --- app/Config/Routes.php | 1 + app/Controllers/Bag.php | 6 +- app/Controllers/BaseController.php | 18 ++- app/Controllers/Home.php | 30 ++++- app/Views/auth/_shell.php | 4 + app/Views/bag/layout/embed.php | 96 +++++++++++++++ app/Views/bag/layout/portal.php | 4 + app/Views/bag/layout/workspace.php | 185 +++++++++++++++++++++++++++++ e2e/redesign.spec.js | 9 +- e2e/workspace.spec.js | 42 +++++++ 10 files changed, 381 insertions(+), 14 deletions(-) create mode 100644 app/Views/bag/layout/embed.php create mode 100644 app/Views/bag/layout/workspace.php create mode 100644 e2e/workspace.spec.js diff --git a/app/Config/Routes.php b/app/Config/Routes.php index f76c537..cd32c2b 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -6,6 +6,7 @@ use CodeIgniter\Router\RouteCollection; * @var RouteCollection $routes */ $routes->get('/', 'Home::index'); +$routes->get('workspace', 'Home::workspace'); $routes->get('dashboard', 'Home::dashboard'); $routes->get('dashboard/simple', 'Home::dashboardSimple'); $routes->get('dashboard/compact', 'Home::dashboardCompact'); diff --git a/app/Controllers/Bag.php b/app/Controllers/Bag.php index 8786818..44d4ffa 100644 --- a/app/Controllers/Bag.php +++ b/app/Controllers/Bag.php @@ -214,8 +214,10 @@ class Bag extends BaseController private function render(string $title, string $viewFile, array $data = []): string { - // 사이트 업무 페이지 공통 셸: gov-portal 디자인(헤더+대메뉴+클릭형 좌측 사이드바). - return view('bag/layout/portal', [ + // /workspace 탭(iframe) 안에서는 임베드 레이아웃(헤더·사이드바 없이 본문만). + $layout = $this->isEmbeddedRequest() ? 'bag/layout/embed' : 'bag/layout/portal'; + + return view($layout, [ 'title' => $title, 'content' => view($viewFile, $data), ]); diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php index 3845550..6c752bc 100644 --- a/app/Controllers/BaseController.php +++ b/app/Controllers/BaseController.php @@ -48,6 +48,20 @@ abstract class BaseController extends Controller * * @param array $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 { $content = view($contentView, $contentData); @@ -61,8 +75,8 @@ abstract class BaseController extends Controller $path = substr($path, strlen('index.php/')); } if ($path === 'bag' || str_starts_with($path, 'bag/')) { - // 사이트 업무 페이지: gov-portal 디자인 셸 적용 - return view('bag/layout/portal', [ + // /workspace 탭(iframe) 안에서는 임베드 레이아웃, 아니면 gov-portal 셸 + return view($this->isEmbeddedRequest() ? 'bag/layout/embed' : 'bag/layout/portal', [ 'title' => $title, 'content' => $content, ]); diff --git a/app/Controllers/Home.php b/app/Controllers/Home.php index 8cf7e3a..1956170 100644 --- a/app/Controllers/Home.php +++ b/app/Controllers/Home.php @@ -10,19 +10,37 @@ class Home extends BaseController public function index() { if (session()->get('logged_in')) { - // 메인(/) — gov-portal 디자인 셸 + 종량제 실데이터 대시보드. helper('admin'); - return view('bag/layout/portal', [ - 'title' => '업무 현황', - 'bare' => true, - 'content' => view('bag/dashboard_portal', $this->portalDashboardData()), - ]); + // 워크스페이스 탭(iframe) 안: 대시보드 본문을 임베드로. + if ($this->isEmbeddedRequest()) { + return view('bag/layout/embed', [ + 'title' => '업무 현황', + 'bare' => true, + 'content' => view('bag/dashboard_portal', $this->portalDashboardData()), + ]); + } + + // 로그인 후 기본 화면 = 워크스페이스(탭). 메뉴를 탭으로 열어 작업 상태 유지. + return view('bag/layout/workspace'); } 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'); + } + /** * 메인 대시보드용 — 종량제 시스템에 실제 존재하는 데이터만 집계. * diff --git a/app/Views/auth/_shell.php b/app/Views/auth/_shell.php index 325413b..d8861bb 100644 --- a/app/Views/auth/_shell.php +++ b/app/Views/auth/_shell.php @@ -33,6 +33,10 @@ tailwind.config = { +
diff --git a/app/Views/bag/layout/embed.php b/app/Views/bag/layout/embed.php new file mode 100644 index 0000000..fc5f8e3 --- /dev/null +++ b/app/Views/bag/layout/embed.php @@ -0,0 +1,96 @@ + + + + + + +<?= esc($title ?? '종량제 시스템') ?> + + + + + + + +
+ +

+ + getFlashdata('success')): ?> +
getFlashdata('success')) ?>
+ + getFlashdata('error')): ?> +
getFlashdata('error')) ?>
+ + + + +
+ +
+ +
+ + + diff --git a/app/Views/bag/layout/portal.php b/app/Views/bag/layout/portal.php index 472d7db..bc1e2de 100644 --- a/app/Views/bag/layout/portal.php +++ b/app/Views/bag/layout/portal.php @@ -93,6 +93,10 @@ tailwind.config = { · · 님 +
+ 워크스페이스 + diff --git a/app/Views/bag/layout/workspace.php b/app/Views/bag/layout/workspace.php new file mode 100644 index 0000000..64860c8 --- /dev/null +++ b/app/Views/bag/layout/workspace.php @@ -0,0 +1,185 @@ + $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 : ''; +} +?> + + + + + +워크스페이스 · 종량제 시스템 + + + + + +
+ +
+ +
+ + +
+
+
+
+ +
왼쪽 메뉴를 클릭하면 탭으로 열립니다. 여러 화면을 열어두고 전환해도 작업 내용이 유지됩니다.
+
+
+
+
+ + + + + diff --git a/e2e/redesign.spec.js b/e2e/redesign.spec.js index de087f4..053d1f3 100644 --- a/e2e/redesign.spec.js +++ b/e2e/redesign.spec.js @@ -8,14 +8,15 @@ const { login } = require('./helpers/auth'); * - 대메뉴 클릭 → 좌측 사이드바에 소메뉴 표시 */ test.describe('gov-portal 전면 적용', () => { - test('메인(/)이 포털 대시보드로 렌더', async ({ page }) => { + test('메인(/)이 워크스페이스(탭) + 첫 탭 대시보드로 렌더', async ({ page }) => { await login(page, 'user'); await page.goto('/'); await expect(page.locator('header.portal-header')).toBeVisible(); await expect(page.locator('.sidebar')).toBeVisible(); - await expect(page.getByText('봉투 재고 총량')).toBeVisible(); - await expect(page.getByText('승인 대기')).toBeVisible(); - // 목업 흔적(가짜 공지/지도)이 없어야 함 + await expect(page.locator('.ws-tabbar')).toBeVisible(); + // 첫 탭(대시보드) iframe 안에 실데이터 KPI + await expect(page.frameLocator('.ws-frame.active').getByText('봉투 재고 총량')).toBeVisible({ timeout: 10000 }); + // 목업 흔적 없음 await expect(page.getByText('서비스 데스크')).toHaveCount(0); }); diff --git a/e2e/workspace.spec.js b/e2e/workspace.spec.js new file mode 100644 index 0000000..e77f4e4 --- /dev/null +++ b/e2e/workspace.spec.js @@ -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); + }); +});