수정본3

Sleeper·2026년 3월 5일
(function () {
  const LOG = (...a) => console.log('[OrderMap]', ...a);

  /* ① calTest 안전하게 읽기 */
  const rawStr = typeof calTest !== 'undefined' ? String(calTest).trim() : '';
  if (!rawStr) { LOG('⚠️ calTest 없음 → 종료'); return; }

  const list = rawStr.split(',').map(v => v.trim()).filter(Boolean).reverse();
  LOG('클릭 순서 →', list);

  const delay = ms => new Promise(r => setTimeout(r, ms));

  const waitForLoad = () => new Promise(r => {
    if (document.readyState === 'complete') r();
    else window.addEventListener('load', r, { once: true });
  });

  /* ② expand 후 자식 <tr> 렌더링 대기 */
  function waitForNewRows(timeout = 2000) {
    const before = document.querySelectorAll('tr[data-row-key]').length;
    return new Promise(resolve => {
      const ob = new MutationObserver(() => {
        if (document.querySelectorAll('tr[data-row-key]').length > before) {
          ob.disconnect(); clearTimeout(t); resolve();
        }
      });
      ob.observe(document.body, { childList: true, subtree: true });
      const t = setTimeout(() => { ob.disconnect(); resolve(); }, timeout);
    });
  }

  /* ③ 텍스트로 <tr> 찾기 (완전일치 → 셀일치 → 포함일치) */
  function findRowByText(txt) {
    const rows = [...document.querySelectorAll('tr[data-row-key]')];
    return (
      rows.find(r => r.innerText.trim() === txt) ||
      rows.find(r => [...r.querySelectorAll('td')].some(td => td.innerText.trim() === txt)) ||
      rows.find(r => r.innerText.includes(txt)) ||
      null
    );
  }

  /* ④ <tr> 폴링 (최대 5초) */
  async function waitForRow(txt, interval = 200, maxTries = 25) {
    for (let i = 0; i < maxTries; i++) {
      const row = findRowByText(txt);
      if (row) return row;
      await delay(interval);
    }
    return null;
  }

  /* ⑤ expand 버튼 클릭 + expanded 클래스 대기 */
  function triggerExpand(btn) {
    return new Promise(resolve => {
      if (btn.classList.contains('ant-table-row-expand-icon-expanded')) {
        LOG('ℹ️ 이미 expanded → 생략'); resolve(); return;
      }
      const ev = { bubbles: true, cancelable: true, view: window };
      ['mousedown', 'mouseup', 'click'].forEach(type =>
        btn.dispatchEvent(new MouseEvent(type, ev))
      );
      const ob = new MutationObserver(() => {
        if (btn.classList.contains('ant-table-row-expand-icon-expanded')) {
          ob.disconnect(); clearTimeout(t); resolve();
        }
      });
      ob.observe(btn, { attributes: true, attributeFilter: ['class'] });
      const t = setTimeout(() => { ob.disconnect(); resolve(); }, 1500);
    });
  }

  /* ⑥ 레이아웃 안정화 대기
     ★ 핵심: row의 top 좌표가 2프레임 연속으로 같은 값이면 레이아웃 확정으로 판단
        → expand 후 자식 행이 추가되면서 레이아웃이 밀리는 타이밍을 정확히 잡음
        maxFrames: 최대 몇 프레임까지 기다릴지 (약 16ms * maxFrames)
  */
  function waitForLayoutStable(row, maxFrames = 30) {
    return new Promise(resolve => {
      let prevTop = null;
      let stableCount = 0;
      let frameCount = 0;

      function check() {
        frameCount++;

        // row가 DOM에서 사라진 경우 즉시 종료
        if (!document.contains(row)) { resolve(); return; }

        const currentTop = row.getBoundingClientRect().top;

        if (prevTop !== null && Math.abs(currentTop - prevTop) < 0.5) {
          stableCount++;
          // 2프레임 연속 같은 값 → 레이아웃 확정
          if (stableCount >= 2) {
            LOG(`📐 레이아웃 안정화 확인 (${frameCount}프레임)`);
            resolve(); return;
          }
        } else {
          stableCount = 0; // 값이 바뀌면 카운트 리셋
        }

        prevTop = currentTop;

        // 최대 프레임 초과 시 강제 진행
        if (frameCount >= maxFrames) {
          LOG(`⚠️ 레이아웃 안정화 타임아웃 (${maxFrames}프레임) → 강제 진행`);
          resolve(); return;
        }

        requestAnimationFrame(check);
      }

      requestAnimationFrame(check);
    });
  }

  /* ⑦ 스크롤 함수
     ★ waitForLayoutStable 이후에 호출하므로 좌표가 항상 정확함
     ★ behavior: 'instant' → DOM 변경에도 스크롤 취소 안됨
  */
  function scrollToRow(row, align = 'top') {
    const container =
      row.closest('.mx-name-container9.approval-user-popup-left') ||
      document.querySelector('.mx-name-container9.approval-user-popup-left');

    if (!container) {
      row.scrollIntoView({ block: align === 'top' ? 'start' : 'center' });
      return;
    }

    if (!document.contains(row)) { LOG('⚠️ row DOM 사라짐 → 스크롤 스킵'); return; }

    const cRect  = container.getBoundingClientRect();
    const rRect  = row.getBoundingClientRect();
    const relTop = container.scrollTop + rRect.top - cRect.top;
    const target = align === 'top'
      ? relTop
      : relTop - (container.clientHeight / 2) + (row.offsetHeight / 2);

    container.scrollTo({ top: target, behavior: 'instant' });
    LOG('✅ 스크롤 완료 → scrollTop:', container.scrollTop, '| target:', target.toFixed(1));
  }

  /* ⑧ 메인 루프 */
  async function run() {
    await waitForLoad();

    for (let i = 0; i < list.length; i++) {
      const txt    = list[i];
      const isLast = i === list.length - 1;
      LOG(`🔎 (${i + 1}/${list.length}) → "${txt}"`);

      const row = await waitForRow(txt);
      if (!row) { LOG(`⚠️ 못 찾음 → "${txt}"`); continue; }

      const btn = row.querySelector('button.ant-table-row-expand-icon');
      if (!btn) { LOG(`⚠️ expand 버튼 없음 → "${txt}"`); continue; }

      await triggerExpand(btn);

      if (!isLast) {
        await waitForNewRows(2000);
        LOG('🕐 자식 행 렌더링 완료');
      }

      if (isLast) {
        LOG('🚀 마지막 → click → 레이아웃 안정화 대기 → 스크롤');
        row.click();

        // ★ 딜레이 대신 실제 레이아웃 안정화를 감지 후 스크롤
        await waitForLayoutStable(row);
        scrollToRow(row, 'top');
      }

      await delay(300);
    }

    LOG('🎉 완료');
  }

  run();
})();

0개의 댓글