From 56dadb34789cb8a34cbcb18beb4a45c1ffecbd66 Mon Sep 17 00:00:00 2001 From: taekyoungc Date: Mon, 15 Jun 2026 13:31:22 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=9B=8C=ED=81=AC=EC=8A=A4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=ED=83=AD=20=EC=A4=91=EC=B2=A9=20=EC=85=B8?= =?UTF-8?q?=20=EB=B0=A9=EC=A7=80=20+=20=EC=84=B8=EC=85=98=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A6=AC?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EmbedRedirectFilter 추가: 임베드(embed=1) 요청의 리다이렉트 Location에 embed 유지(중첩 셸 방지) - bag/* 전체에 loginAuth 적용, 임베드 대시보드 로그아웃 시 로그인으로 이동 - 기본코드 종류 선택 시 embed 유지, 일괄입고 오류 복귀를 명시 URL로(back() 제거) Co-Authored-By: Claude Opus 4.8 --- app/Config/Filters.php | 9 ++++- app/Controllers/Bag.php | 15 +++++--- app/Controllers/Home.php | 6 ++++ app/Filters/EmbedRedirectFilter.php | 54 +++++++++++++++++++++++++++++ app/Views/bag/code_kinds.php | 3 +- 5 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 app/Filters/EmbedRedirectFilter.php diff --git a/app/Config/Filters.php b/app/Config/Filters.php index 204ec53..1c9e138 100644 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -27,6 +27,7 @@ class Filters extends BaseFilters public array $aliases = [ 'adminAuth' => \App\Filters\AdminAuthFilter::class, 'loginAuth' => \App\Filters\LoginAuthFilter::class, + 'embedRedirect' => \App\Filters\EmbedRedirectFilter::class, 'csrf' => CSRF::class, 'toolbar' => DebugToolbar::class, 'honeypot' => Honeypot::class, @@ -81,6 +82,7 @@ class Filters extends BaseFilters 'after' => [ // 'honeypot', // 'secureheaders', + 'embedRedirect', // 임베드(탭) 리다이렉트에 embed=1 유지 → 중첩 셸 방지 ], ]; @@ -108,5 +110,10 @@ class Filters extends BaseFilters * * @var array>> */ - public array $filters = []; + public array $filters = [ + // 모든 업무(bag) 화면은 로그인 필요. 세션 만료 후 어떤 버튼을 눌러도 + // 깨진 화면 대신 로그인으로 리다이렉트되도록 일괄 보호한다. + // (login/logout/register 는 bag/* 가 아니므로 영향 없음. 관리자 화면은 adminAuth 가 별도 처리) + 'loginAuth' => ['before' => ['bag', 'bag/*']], + ]; } diff --git a/app/Controllers/Bag.php b/app/Controllers/Bag.php index bd55e10..4e712d7 100644 --- a/app/Controllers/Bag.php +++ b/app/Controllers/Bag.php @@ -592,6 +592,7 @@ class Bag extends BaseController 'selectedKind' => $selectedKind, 'detailList' => $detailList, 'rowCanEdit' => $rowCanEdit, + 'isEmbed' => $this->isEmbeddedRequest(), // 행 클릭 시 embed 유지(중첩 셸 방지) ], true); // 본문이 이미 카드 2개라 바깥 래퍼 생략 } @@ -4768,14 +4769,18 @@ SQL); $receiverIdx = $this->parseReceiverRefToStoredIdx($lgIdx, $receiverRef); $senderIdx = (int) ($this->request->getPost('br_sender_idx') ?? 0); + // 오류 시 명시적으로 일괄 입고 화면으로 복귀(back()은 도움말 드로어 등이 남긴 + // previous_url 로 잘못 이동할 수 있어 사용하지 않는다) + $batchUrl = site_url('bag/receiving/batch?company_idx=' . $companyIdx . '&bag_code=' . rawurlencode($bagCode)); + if (empty($selected)) { - return redirect()->back()->withInput()->with('error', '일괄 입고할 행을 선택해 주세요.'); + return redirect()->to($batchUrl)->withInput()->with('error', '일괄 입고할 행을 선택해 주세요.'); } if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $receiveDate)) { - return redirect()->back()->withInput()->with('error', '입고일 형식을 확인해 주세요.'); + return redirect()->to($batchUrl)->withInput()->with('error', '입고일 형식을 확인해 주세요.'); } if ($receiverIdx <= 0) { - return redirect()->back()->withInput()->with('error', '인수자를 선택해 주세요.'); + return redirect()->to($batchUrl)->withInput()->with('error', '인수자를 선택해 주세요.'); } $senderResolved = $this->resolveCompanySenderName($lgIdx, $senderIdx); @@ -4816,7 +4821,7 @@ SQL); } if (empty($insertRows)) { - return redirect()->back()->withInput()->with('error', '선택한 행에 입고할 수량이 없습니다.'); + return redirect()->to($batchUrl)->withInput()->with('error', '선택한 행에 입고할 수량이 없습니다.'); } $recvModel = model(BagReceivingModel::class); @@ -4846,7 +4851,7 @@ SQL); $db->transComplete(); if (! $db->transStatus()) { - return redirect()->back()->withInput()->with('error', '일괄 입고 처리 중 오류가 발생했습니다.'); + return redirect()->to($batchUrl)->withInput()->with('error', '일괄 입고 처리 중 오류가 발생했습니다.'); } return redirect()->to(site_url('bag/receiving/batch?company_idx=' . $companyIdx . '&bag_code=' . rawurlencode($bagCode))) diff --git a/app/Controllers/Home.php b/app/Controllers/Home.php index 3ef8cac..6a19789 100644 --- a/app/Controllers/Home.php +++ b/app/Controllers/Home.php @@ -25,6 +25,12 @@ class Home extends BaseController return view('bag/layout/workspace'); } + // 워크스페이스 탭(iframe) 안에서 세션이 만료된 경우: 공개 랜딩 대신 로그인으로 보내 + // 로그인 페이지의 프레임 이탈 스크립트가 상위 창 전체를 로그인으로 전환하게 한다. + if ($this->isEmbeddedRequest()) { + return redirect()->to(base_url('login')); + } + return view('welcome_message'); } diff --git a/app/Filters/EmbedRedirectFilter.php b/app/Filters/EmbedRedirectFilter.php new file mode 100644 index 0000000..817b65e --- /dev/null +++ b/app/Filters/EmbedRedirectFilter.php @@ -0,0 +1,54 @@ +to(...) 하면 Location 에 embed 가 빠지고, iframe 안에서 + * 그 주소로 다시 GET 하면(특히 HTTP 환경에서 Sec-Fetch-Dest 미전송) 전체 셸(헤더·사이드바)이 + * 한 번 더 렌더되어 "화면 안에 전체 화면이 또" 생긴다. 이를 전역에서 한 번에 방지한다. + */ +class EmbedRedirectFilter implements FilterInterface +{ + public function before(RequestInterface $request, $arguments = null) + { + // no-op + } + + /** + * @param array|null $arguments + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + // 임베드 요청에서 시작된 리다이렉트만 처리 + if ($request->getGet('embed') === null) { + return; + } + + $location = $response->getHeaderLine('Location'); + if ($location === '') { + return; // 리다이렉트 응답이 아님 + } + + // 이미 embed 파라미터가 있으면 그대로 둔다 + if (preg_match('/[?&]embed=/', $location) === 1) { + return; + } + + // 인증 흐름(로그인/로그아웃/회원가입)은 상위 프레임에서 처리하므로 embed 를 붙이지 않는다 + if (preg_match('#/(login|logout|register)(/|\?|$)#', $location) === 1) { + return; + } + + $sep = strpos($location, '?') !== false ? '&' : '?'; + $response->setHeader('Location', $location . $sep . 'embed=1'); + } +} diff --git a/app/Views/bag/code_kinds.php b/app/Views/bag/code_kinds.php index b9708de..0b8f028 100644 --- a/app/Views/bag/code_kinds.php +++ b/app/Views/bag/code_kinds.php @@ -12,6 +12,7 @@ $showKindActions = $canManageKinds; $selectedKindId = (int) ($selectedKind->ck_idx ?? 0); $colCount = 6 + ($showKindActions ? 1 : 0); $detailColCount = 7 + ($canManageDetails ? 1 : 0); +$embedQs = ! empty($isEmbed) ? '&embed=1' : ''; // 워크스페이스 탭 안에서는 embed 유지(중첩 셸 방지) /** 상태 배지 (업무현황 스타일의 가벼운 pill) */ $stateBadge = static function (int $state): string { @@ -51,7 +52,7 @@ $stateBadge = static function (int $state): string { ck_idx === $selectedKindId; - $detailUrl = base_url('bag/code-kinds?ck_idx=' . (int) $row->ck_idx); + $detailUrl = base_url('bag/code-kinds?ck_idx=' . (int) $row->ck_idx . $embedQs); ?>