(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();
})();