자주사용하는 것들 - 탭처리

2026.01.28 13:30

메인 페이지: ctxKey 발급 + 세션에 element 저장

“ajax 호출 전에 변수들이 이미 선언”하고, 그 변수들을 element 슬롯에 포함

<?php
session_start();

$pageToken = bin2hex(random_bytes(8));

$_SESSION['tab_ctx'][$pageToken] = [
  'e1' => $element1 ?? '',
  'e2' => $element2 ?? '',
  'e3' => $element3 ?? '',
  'e4' => $element4 ?? '',
  'ts' => time(),
];

// TTL 정리 (30분)
$ttl = 1800;
foreach ($_SESSION['tab_ctx'] ?? [] as $k => $v) {
  if (time() - ($v['ts'] ?? 0) > $ttl) {
    unset($_SESSION['tab_ctx'][$k]);
  }
}

그리고 폼(아무 폼) 안에 ctxKey만 hidden으로 삽입

<input type="hidden" name="pageToken" value="<?=htmlspecialchars($pageToken)?>">


탭 HTML

<div class="tabbox" data-tabbox="box1" data-form="#FormID" data-allow-form="1" data-url-prefix="./ajax_tab_" data-default-tab="Tab1">
  <div class="tabbox-tabs">
    <div class="tabbox-tab active" data-tab="Tab1">Tab1</div>
    <div class="tabbox-tab" data-tab="Tab2">Tab2</div>
    <div class="tabbox-tab" data-tab="Tab3">Tab3</div>
  </div>
  <div class="tabbox-panel"><div class="coming">Loading....</div></div>
</div>

data-allow-form="1" 없으면 data-form은 있어도 전송 안 함


JS: 폼을 그대로 보내되, 서버는 ctxKey만 믿고 처리

<script>
function safeKey(s){
  return String(s || '').replace(/[^0-9A-Za-z_\-]/g, '');
}
function makeUrl(prefix, tabKey){
  const key = safeKey(tabKey);
  if(!key) return '';
  return String(prefix || '') + key + '.php'; // ./ajax_tab_Tab1.php
}

function getPageToken(rootEl){
  // 1) tabbox에서 토큰 selector 지정하면 그걸 사용
  //    예: data-page-token="#pageToken"
  const sel = rootEl.dataset.pageToken || '#pageToken';
  const el = document.querySelector(sel);
  if(!el) return '';
  return ('value' in el) ? String(el.value || '') : String(el.textContent || '');
}

function getFormFromTabbox(rootEl){
  // data-form="#FormID" 우선, 없으면 가장 가까운 form
  const sel = rootEl.dataset.form || '';
  if(sel){
    const f = document.querySelector(sel);
    if(f && f.tagName === 'FORM') return f;
  }
  const near = rootEl.closest('form');
  return (near && near.tagName === 'FORM') ? near : null;
}

function setActive(tabsWrap, tabKey){
  tabsWrap.querySelectorAll('.tabbox-tab').forEach(t => {
    t.classList.toggle('active', t.dataset.tab === tabKey);
  });
}

function runScripts(container){
  container.querySelectorAll('script').forEach(old => {
    const s = document.createElement('script');
    if(old.src) s.src = old.src;
    s.text = old.textContent;
    old.replaceWith(s);
  });
}

async function ajaxLoad(panelEl, url, rootEl){
  if(!panelEl) return;
  panelEl.innerHTML = "<div class='coming'>Loading....</div>";

  try{
    const pageToken = getPageToken(rootEl);
    if(!pageToken){
      panelEl.innerHTML = "<div class='coming'>pageToken 없음</div>";
      return;
    }

    // ✅ 기본: pageToken만 전송 (노출 최소화)
    const fd = new FormData();
    fd.set('pageToken', pageToken);
    fd.set('_ts', String(Date.now()));

    // ✅ 옵션: data-allow-form="1" 일 때만, data-form 폼을 합쳐서 전송
    // (기본 OFF)
    if(rootEl.dataset.allowForm === '1'){
      const formEl = getFormFromTabbox(rootEl);
      if(formEl){
        const extra = new FormData(formEl);
        extra.forEach((v, k) => {
          if(k === 'pageToken' || k === '_ts') return; // 보호
          fd.set(k, v);
        });
      }
    }

    const res = await fetch(url, {
      method:'POST',
      body: fd,
      headers:{ 'X-Requested-With':'XMLHttpRequest' }
    });

    if(res.status === 403){
      panelEl.innerHTML = "<div class='coming'>권한 없음(403)</div>";
      return;
    }
    if(res.status === 410){
      panelEl.innerHTML = "<div class='coming'>만료됨(410) - 새로고침 필요</div>";
      return;
    }
    if(!res.ok){
      panelEl.innerHTML = "<div class='coming'>오류(" + res.status + ")</div>";
      return;
    }

    const html = await res.text();
    panelEl.innerHTML = html || "<div class='coming'>Loading....</div>";
    runScripts(panelEl);

  }catch(e){
    panelEl.innerHTML = "<div class='coming'>Loading....</div>";
  }
}

function initTabBox(rootEl){
  const tabsWrap = rootEl.querySelector('.tabbox-tabs');
  const panel = rootEl.querySelector('.tabbox-panel');
  if(!tabsWrap || !panel) return;

  const urlPrefix = rootEl.dataset.urlPrefix || './ajax_tab_';

  async function loadTab(tabKey){
    const url = makeUrl(urlPrefix, tabKey);
    if(!url) return;
    setActive(tabsWrap, tabKey);
    await ajaxLoad(panel, url, rootEl);
  }

  tabsWrap.querySelectorAll('.tabbox-tab').forEach(tab => {
    tab.addEventListener('click', () => loadTab(tab.dataset.tab || ''));
  });

  const first =
    rootEl.dataset.defaultTab
    || tabsWrap.querySelector('.tabbox-tab.active')?.dataset.tab
    || tabsWrap.querySelector('.tabbox-tab')?.dataset.tab;

  if(first) loadTab(first);
}

document.querySelectorAll('.tabbox[data-tabbox]').forEach(initTabBox);
</script>


ajax 파일: ctxKey로 세션에서 e1~ 꺼내기

<?php
session_start();

/* ✅ ajax 요청인지 최소 확인 (강제는 아니지만 추천) */
$xhr = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? '';
if (strcasecmp($xhr, 'XMLHttpRequest') !== 0) {
  http_response_code(400);
  exit('bad request');
}

/* ✅ pageToken 기반 컨텍스트 검증 */
$pageToken = (string)($_POST['pageToken'] ?? '');
if ($pageToken === '' || empty($_SESSION['tab_ctx'][$pageToken])) {
  http_response_code(403);
  exit('invalid context');
}

$ctx = $_SESSION['tab_ctx'][$pageToken];

/* ✅ TTL 체크 (30분) */
$ttl = 1800;
$ts = (int)($ctx['ts'] ?? 0);
if ($ts <= 0 || (time() - $ts) > $ttl) {
  unset($_SESSION['tab_ctx'][$pageToken]);
  http_response_code(410);
  exit('expired');
}

/* ✅ (선택) 1회 사용 후 폐기: 더 강하게 “단독/재호출” 억제 */
# unset($_SESSION['tab_ctx'][$pageToken]);

$e1 = $ctx['e1'] ?? '';
$e2 = $ctx['e2'] ?? '';
$e3 = $ctx['e3'] ?? '';
$e4 = $ctx['e4'] ?? '';

// 여기서부터 탭 작성자가 알아서 사용


// 여기서부터는 이 탭 작성자가 "e1이 뭔지" 알아서 쓰면 됨

댓글 0

아직 댓글이 없습니다.