giftcon_dev/resources/views/web/partials/dev_session_overlay.blade.php
2026-03-03 15:13:16 +09:00

219 lines
9.0 KiB
PHP

{{-- resources/views/web/partials/dev_session_overlay.blade.php --}}
@php
// 개발 모드에서만 노출
$show = config('app.debug') || app()->environment('local');
// 이 overlay 자체가 세션을 수정하는 "dev action" 처리(컨트롤러/라우트 없이)
if ($show && request()->isMethod('post') && request()->has('_dev_sess_action')) {
// CSRF는 web 미들웨어에 걸려있으니 토큰 포함된 요청만 처리됨.
$action = request()->input('_dev_sess_action');
if ($action === 'flush') {
session()->flush();
session()->save();
}
if ($action === 'put') {
$k = (string) request()->input('_dev_sess_key', '');
$v = (string) request()->input('_dev_sess_value', '');
// 빈 키 방지
if ($k !== '') {
// "a.b.c" 점 표기 지원
data_set(session()->all(), $k, $v);
// data_set은 배열에만 반영되므로 세션에 직접 put
session()->put($k, $v);
session()->save();
}
}
// POST 재전송 방지 + 현재 페이지로 되돌리기
$redir = url()->current();
$qs = request()->query();
if (!empty($qs)) $redir .= '?' . http_build_query($qs);
header('Location: ' . $redir, true, 302);
exit;
}
// 세션 전체
$sess = session()->all();
// 민감값 마스킹
$maskKeys = [];
// $maskKeys = [
// 'password', 'passwd', 'pw', 'token', 'access_token', 'refresh_token',
// 'api_key', 'secret', 'authorization', 'csrf', '_token',
// 'g-recaptcha-response', 'recaptcha', 'otp',
// 'ci', 'di', 'phone', 'mobile', 'email',
// ];
$mask = function ($key, $val) use ($maskKeys) {
$k = strtolower((string)$key);
foreach ($maskKeys as $mk) {
if (str_contains($k, $mk)) return '***';
}
return $val;
};
// key:value 라인 생성(재귀)
$lines = [];
$dump = function ($data, $prefix = '') use (&$dump, &$lines, $mask) {
foreach ((array)$data as $k => $v) {
$key = $prefix . (string)$k;
if (is_array($v)) {
$lines[] = $key . ' : [';
$dump($v, $prefix . ' ');
$lines[] = $prefix . ']';
} else {
if (is_bool($v)) $v = $v ? 'true' : 'false';
if ($v === null) $v = 'null';
$vv = is_string($v) ? (mb_strlen($v) > 260 ? mb_substr($v, 0, 260) . '…' : $v) : (string)$v;
$vv = $mask($k, $vv);
$lines[] = $key . ' : ' . $vv;
}
}
};
$dump($sess);
$text = implode("\n", $lines);
@endphp
@if($show)
<div id="dev-session-overlay" style="
position: fixed; left: 12px; bottom: 12px; z-index: 999999;
width: 620px; max-width: calc(100vw - 24px);
background: rgba(10,10,10,.92); color: #eaeaea;
border: 1px solid rgba(255,255,255,.14);
border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,.35);
font: 12px/1.35 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
">
<div style="display:flex; align-items:center; justify-content:space-between; padding:10px 12px; gap:8px;">
<div style="font-weight:800;">
SESSION
<span style="opacity:.65; font-weight:500;">({{ count($sess) }} keys)</span>
</div>
<div style="display:flex; gap:6px;">
<button type="button" id="devSessCopy" style="
border: 1px solid rgba(255,255,255,.18); background: rgba(255,255,255,.06);
color:#fff; padding:5px 10px; border-radius:10px; cursor:pointer;
">Copy</button>
<button type="button" id="devSessToggle" style="
border: 1px solid rgba(255,255,255,.18); background: rgba(255,255,255,.06);
color:#fff; padding:5px 10px; border-radius:10px; cursor:pointer;
">Hide</button>
</div>
</div>
<div id="devSessBody" style="padding:0 12px 12px 12px;">
<div style="opacity:.7; margin-bottom:8px;">
{{ request()->method() }} {{ request()->path() }}
</div>
{{-- Controls --}}
<div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:10px;">
<form method="POST" action="{{ route('dev.session') }}" style="display:flex; gap:6px; align-items:center; margin:0;">
@csrf
<input type="hidden" name="_dev_return" value="{{ url()->full() }}">
<input type="hidden" name="_dev_sess_action" value="flush">
<input type="hidden" name="_dev_return" value="{{ url()->full() }}">
<button type="submit" style="
border: 1px solid rgba(255,90,90,.35);
background: rgba(255,90,90,.14);
color:#fff; padding:6px 10px; border-radius:10px; cursor:pointer;
" onclick="return confirm('세션을 초기화할까요? (dev 전용)');">세션 초기화</button>
</form>
<form method="POST" action="{{ route('dev.session') }}" style="display:flex; gap:6px; align-items:center; margin:0; flex:1;">
<input type="hidden" name="_dev_return" value="{{ url()->full() }}">
@csrf
<input type="hidden" name="_dev_sess_action" value="put">
<input type="hidden" name="_dev_return" value="{{ url()->full() }}">
<input name="_dev_sess_key" placeholder="key (예: register.pass_verified)" style="
flex: 0 0 240px; max-width: 45%;
border: 1px solid rgba(255,255,255,.16);
background: rgba(255,255,255,.06);
color:#fff; padding:6px 8px; border-radius:10px; outline:none;
">
<input name="_dev_sess_value" placeholder="value (문자열로 저장)" style="
flex: 1 1 auto;
border: 1px solid rgba(255,255,255,.16);
background: rgba(255,255,255,.06);
color:#fff; padding:6px 8px; border-radius:10px; outline:none;
">
<button type="submit" style="
border: 1px solid rgba(90,180,255,.35);
background: rgba(90,180,255,.14);
color:#fff; padding:6px 10px; border-radius:10px; cursor:pointer;
">등록</button>
</form>
</div>
<div style="
padding:10px; border-radius:12px;
background: rgba(255,255,255,.05);
max-height: 360px; overflow:auto; white-space: pre;
border: 1px solid rgba(255,255,255,.10);
">
<pre id="devSessPre" style="margin:0;">{!! e($text) !!}</pre>
</div>
</div>
</div>
<script>
(function(){
const box = document.getElementById('dev-session-overlay');
const body = document.getElementById('devSessBody');
const toggle = document.getElementById('devSessToggle');
const copyBtn = document.getElementById('devSessCopy');
const pre = document.getElementById('devSessPre');
// 상태 기억
const key = 'devSessOverlayCollapsed';
const collapsed = localStorage.getItem(key) === '1';
if (collapsed) { body.style.display='none'; toggle.textContent='Show'; }
toggle.addEventListener('click', () => {
const isHidden = body.style.display === 'none';
body.style.display = isHidden ? '' : 'none';
toggle.textContent = isHidden ? 'Hide' : 'Show';
localStorage.setItem(key, isHidden ? '0' : '1');
});
copyBtn.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(pre.innerText);
copyBtn.textContent = 'Copied';
setTimeout(()=>copyBtn.textContent='Copy', 800);
} catch(e) {
alert('복사 실패(브라우저 권한 확인)');
}
});
// 드래그 이동
let dragging=false, sx=0, sy=0, ox=0, oy=0;
box.firstElementChild.style.cursor = 'move';
box.firstElementChild.addEventListener('mousedown', (e)=>{
dragging=true; sx=e.clientX; sy=e.clientY;
const r = box.getBoundingClientRect();
ox=r.left; oy=r.top;
box.style.right='auto'; box.style.bottom='auto';
document.body.style.userSelect='none';
});
window.addEventListener('mousemove', (e)=>{
if(!dragging) return;
const nx = ox + (e.clientX - sx);
const ny = oy + (e.clientY - sy);
box.style.left = Math.max(0, nx) + 'px';
box.style.top = Math.max(0, ny) + 'px';
});
window.addEventListener('mouseup', ()=>{
dragging=false;
document.body.style.userSelect='';
});
})();
</script>
@endif