import { createRequire } from 'module'; const require = createRequire(import.meta.url); import playwright from 'playwright-extra'; const { chromium } = playwright; import fs from 'fs'; import path from 'path'; import AdmZip from 'adm-zip'; import { parse } from 'csv-parse/sync'; // JSON 파일에서 세션 상태를 불러오기 const loadSession = async (filePath) => { const jsonData = fs.readFileSync(filePath, 'utf-8'); return JSON.parse(jsonData); }; function ensureDirExists(dirPath) { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } async function ensureChecked(dlg,page, label) { const cb = dlg.getByRole('checkbox', { name: label, exact: true }); await cb.scrollIntoViewIfNeeded(); let state = await cb.getAttribute('aria-checked'); if (state !== 'true') { await cb.click({ force: true }); // 재확인 루프 for (let i = 0; i < 5; i++) { await page.waitForTimeout(100); state = await cb.getAttribute('aria-checked'); if (state === 'true') break; } if (state !== 'true') throw new Error(`체크 실패: ${label}`); } } const loginWithJson = async () => { const optionsBrowser = { headless: false, args: [ '--disable-blink-features=AutomationControlled', '--no-sandbox', '--disable-web-security', '--disable-infobars', '--disable-extensions', '--start-maximized', '--window-size=1280,720', ], }; const browser = await chromium.launch(optionsBrowser); const sessionData = await loadSession('everlogin.json'); const optionsContext = { userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36', locale: 'ko-KR', viewport: { width: 1280, height: 720 }, deviceScaleFactor: 1, acceptDownloads: true, storageState: sessionData, extraHTTPHeaders: { 'sec-ch-ua': '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"', 'sec-ch-ua-arch': '"arm"', 'sec-ch-ua-bitness': '"64"', 'sec-ch-ua-form-factors': '"Desktop"', 'sec-ch-ua-full-version': '"139.0.7258.154"', 'sec-ch-ua-full-version-list': '"Not;A=Brand";v="99.0.0.0", "Google Chrome";v="139.0.7258.154", "Chromium";v="139.0.7258.154"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-model': '""', 'sec-ch-ua-platform': '"macOS"', 'sec-ch-ua-platform-version': '"15.6.1"', 'sec-ch-ua-wow64': '?0', } }; const context = await browser.newContext(optionsContext); const page = await context.newPage(); await page.goto('https://studio.youtube.com/'); await page.locator('ytcp-navigation-drawer').getByRole('button', { name: '분석', exact: true }).click(); await page.getByRole('link', { name: '고급 모드', exact: true }).click(); //await page.locator('yta-explore-date-picker-dropdown ytcp-dropdown-trigger[role="button"]').click(); // 1) id=picker-trigger 컨테이너 내의 드롭다운 버튼 await page.locator('yta-time-picker #picker-trigger ytcp-dropdown-trigger[role="button"]').click(); await page.locator('tp-yt-paper-item[test-id="week"]').click(); await page.locator('yta-time-picker #picker-trigger ytcp-dropdown-trigger[role="button"]').click(); await page.locator('tp-yt-paper-item[test-id="fixed"]').click(); const caldlg = page.locator('tp-yt-paper-dialog:has(ytcp-date-period-picker)'); await caldlg.waitFor({ state: 'visible' }); // 끝날 값 읽기 const endInput = caldlg.locator('#end-date input'); await endInput.waitFor({ state: 'visible' }); const endVal = await endInput.inputValue(); // 시작날을 끝날과 동일하게 설정 const startInput = caldlg.locator('#start-date input'); await startInput.fill(endVal); // 적용 클릭 (활성 버튼만) await caldlg.locator('#apply-button[aria-disabled="false"] button').click(); // title="측정항목" 이 붙은 드롭다운 버튼 await page.locator('yta-explore-column-picker-dropdown[title="측정항목"] ytcp-dropdown-trigger').click(); const dlg = page.getByRole('dialog', { name: '측정항목' }); await page.locator('h2.picker-text', { hasText: 'Premium' }).click(); // 1) 전체 선택 해제 await dlg.getByRole('button', { name: '전체 선택 해제' }).click(); // 3) 필요한 항목들 체크 await ensureChecked(dlg,page, '조회수'); // id=EXTERNAL_VIEWS await ensureChecked(dlg,page, '유효 조회수'); // id=ENGAGED_VIEWS await ensureChecked(dlg,page,'시청 시간(단위: 시간)'); // id=EXTERNAL_WATCH_TIME await ensureChecked(dlg,page,'YouTube Premium 조회수'); // id=EXTERNAL_YOUTUBE_RED_VIEWS // 4) 적용 await dlg.getByRole('button', { name: '적용' }).click(); const downloadDir = path.resolve(process.cwd(), 'downloads'); ensureDirExists(downloadDir); const [download] = await Promise.all([ page.waitForEvent('download'), (async () => { await page.locator('ytcp-icon-button#export-button, ytcp-icon-button[aria-label="현재 화면 내보내기"]').click(); await page.locator('tp-yt-paper-item[test-id="CSV"]').click(); })() ]); const suggested = download.suggestedFilename(); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const zipPath = path.join(downloadDir, `${timestamp}-${suggested || 'export.zip'}`); await download.saveAs(zipPath); console.log(`ZIP 저장: ${zipPath}`); const unzipDir = path.join(downloadDir, `${timestamp}-unzipped`); ensureDirExists(unzipDir); const zip = new AdmZip(zipPath); zip.extractAllTo(unzipDir, true); console.log(`압축 해제: ${unzipDir}`); const files = fs.readdirSync(unzipDir).filter(f => f.toLowerCase().endsWith('.csv')); if (files.length === 0) { throw new Error('압축 해제 폴더에 CSV가 없습니다.'); } const targetCsvPattern = /표 데이터\.csv$/; // '표 데이터.csv' 우선 선택 const targetFile = targetCsvPattern ? (files.find(f => targetCsvPattern.test(f)) || files[0]) : files[0]; const targetPath = path.join(unzipDir, targetFile); console.log(`선택된 CSV: ${targetPath}`); const csvContent = fs.readFileSync(targetPath, 'utf-8'); const records = parse(csvContent, { columns: true, skip_empty_lines: true, bom: true, relax_column_count: true, trim: true, }); console.log(JSON.stringify(records, null, 2)); const waitAfterLoginMs = 60_000; console.log(`로그인 후 ${waitAfterLoginMs / 1000}초 대기합니다...`); await new Promise((resolve) => setTimeout(resolve, waitAfterLoginMs)); try { await browser.close(); } catch (e) { console.log('브라우저가 이미 종료되어 close를 건너뜁니다.'); } }; loginWithJson();