// 확장된 두 앱 감사: 신규 뷰 전부 + 기존 기능 회귀 + 오버플로 import puppeteer from "puppeteer-core"; import path from "node:path"; const root = "D:/workspace/designpaca/apps/site"; const chrome = "C:/Program Files/Google/Chrome/Application/chrome.exe"; const results = []; const pass = (name, ok, detail = "") => results.push(`${ok ? "PASS" : "FAIL"} ${name}${detail ? ` — ${detail}` : ""}`); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const browser = await puppeteer.launch({ executablePath: chrome, headless: "new", args: ["--no-sandbox", "--disable-gpu", "--force-device-scale-factor=1"], defaultViewport: { width: 1440, height: 900 }, }); const page = await browser.newPage(); page.on("pageerror", (e) => results.push(`FAIL pageerror — ${e.message}`)); const jclick = (sel) => page.evaluate((s) => { const el = document.querySelector(s); if (!el) throw new Error("no " + s); el.click(); }, sel); const over = () => page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); const gotoFile = async (app) => page.goto(`file:///${path.join(root, "public/work", app, "index.html")}`, { waitUntil: "networkidle0" }); // ══════════ 가온 학적부 (8 뷰) ══════════ await gotoFile("gaon-lms"); for (const v of ["dashboard","students","attendance","grades","files","calendar","counsel","report","notice"]) { await jclick(`[data-view="${v}"]`); pass(`가온 뷰 ${v}`, await page.$eval(`#view-${v}`, (el) => el.classList.contains("is-on"))); } pass("가온 aria-current(공지)", await page.$eval('[data-view="notice"]', (el) => el.getAttribute("aria-current") === "page")); // 사이드바 그룹 헤더 3개 pass("가온 사이드 그룹 3개", (await page.$$eval(".side-group", (n) => n.length)) === 3); // 출석부: 셀 순환 + 주 전환 + 반 전환 await jclick('[data-view="attendance"]'); const cellBefore = await page.$eval(".att-btn", (b) => b.textContent); await jclick(".att-btn"); const cellAfter = await page.$eval(".att-btn", (b) => b.textContent); pass("가온 출석 순환 ○→△", cellBefore === "○" && cellAfter === "△"); await jclick("#att-prev"); pass("가온 주 전환(1주)", (await page.$eval("#att-week", (el) => el.textContent)) === "3월 1주"); await jclick("#att-next"); await page.evaluate(() => document.querySelector("#att-class [data-sub='코딩 입문']").click()); await sleep(150); const codingRows = await page.$$eval("#att-table tbody tr", (rows) => rows.map((r) => r.textContent.slice(0, 12))); pass("가온 반 전환(코딩 입문 3행, 휴학 포함)", codingRows.length === 3, codingRows.join("|")); // 자료실: 필터 + 배부 토글 await jclick('[data-view="files"]'); await page.evaluate(() => document.querySelector('#file-chips [data-kind="PDF"]').click()); await sleep(150); pass("가온 자료 필터 PDF", (await page.$$("#file-body tr")).length === 2); const giveBefore = await page.$eval("[data-file]", (b) => b.getAttribute("aria-pressed")); await jclick("[data-file]"); pass("가온 배부 토글", (await page.$eval("[data-file]", (b) => b.getAttribute("aria-pressed"))) !== giveBefore); // 달력: 42칸 + 오늘 1 + 예정 7 await jclick('[data-view="calendar"]'); pass("가온 달력 42칸·오늘 1", (await page.$$("#cal-table td")).length === 42 && (await page.$$(".is-today")).length === 1); pass("가온 다가오는 일정 7건", (await page.$$("#up-list li")).length === 7); // 상담: 필터 + 카드→학생 모달 await jclick('[data-view="counsel"]'); await page.evaluate(() => document.querySelector('#co-chips [data-sub="국어 논술 A"]').click()); await sleep(150); pass("가온 상담 필터(국어 2건)", (await page.$$(".counsel-card")).length === 2); await jclick(".counsel-card"); pass("가온 상담 카드→학생 모달", await page.$eval("#stu-dialog", (d) => d.open)); await page.keyboard.press("Escape"); // 리포트: 4행 + 수학 행 계산값 await jclick('[data-view="report"]'); pass("가온 리포트 4행", (await page.$$("#report-table tbody tr")).length === 4); const repMath = await page.$eval("#report-table tbody tr", (tr) => tr.textContent.replace(/\s+/g, " ")); pass("가온 리포트 수학 행 실계산", repMath.includes("3명") && repMath.includes("91%") && repMath.includes("96%") && repMath.includes("84.4점")); // 회귀: 검색/칩/학생 모달/메모/채점 await jclick('[data-view="students"]'); await page.type("#q", "박"); await sleep(200); pass("가온 검색 축소(회귀)", (await page.$$("#view-students [data-stu]")).length === 1); await page.$eval("#q", (el) => { el.value = ""; el.dispatchEvent(new Event("input", { bubbles: true })); }); await jclick("#view-students [data-stu]"); await page.waitForSelector("#stu-dialog[open]"); await page.type("#note-input", "확정 회귀 검사"); await jclick("#note-form button[type=submit]"); await sleep(150); pass("가온 메모 추가(회귀)", (await page.$$("#stu-note-list li")).length === 2); await jclick("#stu-dialog .modal-close"); await jclick('[data-view="grades"]'); await page.evaluate(() => document.querySelector('[data-assign="a2"]').click()); await page.waitForSelector("#as-dialog[open]"); await jclick("#as-save"); await sleep(150); pass("가온 채점 상태 갱신(회귀)", (await page.$eval('#as-table .row-open[data-assign="a2"]', (b) => b.closest("tr").querySelector(".state").textContent)) === "채점 완료"); await page.keyboard.press("Escape"); pass("가온 1440 오버플로", (await over()) <= 0, `+${await over()}px`); // ── 뷰별 프로덕션 요소 (v10) ── await jclick('[data-view="students"]'); pass("가온 수강생 보호자 열", await page.$eval("#stu-table thead", (t) => t.textContent.includes("보호자"))); pass("가온 수강생 요약 스트립", (await page.$$eval("#stu-stats > div", (n) => n.length)) === 5); await jclick('[data-view="attendance"]'); pass("가온 출석 일별 합계", (await page.$eval("#att-foot", (f) => f.textContent.includes("/")))); await jclick('[data-view="grades"]'); await page.evaluate(() => document.querySelector('[data-as="과학 실험"]').click()); pass("가온 성적 과목 필터", (await page.$$eval("#as-table tbody tr:not([hidden])", (n) => n.length)) === 1); await page.evaluate(() => document.querySelector('[data-as="all"]').click()); await jclick('[data-view="calendar"]'); pass("가온 일정 유형 배지·범례", (await page.$$eval(".cal-ev.is-exam", (n) => n.length)) === 2 && (await page.$$eval(".cal-legend .key", (n) => n.length)) === 4); pass("가온 일정 D-day", (await page.$eval("#up-list", (u) => u.textContent.includes("D-")))); await jclick('[data-view="counsel"]'); pass("가온 예정 상담 3건", (await page.$$eval('section[aria-labelledby="co-next-h"] .due-row', (n) => n.length)) === 3); await jclick('[data-view="report"]'); pass("가온 학기 요약 카드", (await page.$$eval("#rep-stats > div", (n) => n.length)) === 4); await jclick('[data-view="notice"]'); await page.evaluate(() => document.querySelector('[data-nt="소통"]').click()); pass("가온 공지 분류 필터", (await page.$$eval(".notice-row:not([hidden])", (n) => n.length)) === 3); await page.evaluate(() => document.querySelector('[data-nt="전체"]').click()); // ══════════ 두레대학 (6 탭) ══════════ await gotoFile("dure-enrollment"); for (const v of ["catalog","starred","enrollments","credits","archive","notices"]) { await jclick(`.tab[data-view="${v}"]`); pass(`두레 탭 ${v}`, await page.$eval(`#view-${v}`, (el) => el.classList.contains("is-on"))); } pass("두레 aria-current(공지)", await page.$eval('.tab[data-view="notices"]', (el) => el.getAttribute("aria-current") === "page")); pass("두레 빈 상태 2종", await page.$eval("#en-empty", (el) => !el.hidden)); // 관심: 모달에서 남기기 → 관심 뷰 1행 await jclick('.tab[data-view="catalog"]'); await jclick('[data-course="phil115"]'); await jclick("#cd-star"); await jclick("#course-dialog .modal-close"); await jclick('.tab[data-view="starred"]'); pass("두레 관심 1행 + 목록 ★", (await page.$$("#starred-list .course-row")).length === 1 && (await page.$$("#starred-list .c-star")).length === 1); // 신청 → 내역·이수 반영 → 총람 소계 → 공지 await jclick('.tab[data-view="catalog"]'); await jclick('[data-course="econ210"]'); await jclick("#cd-apply"); await sleep(200); pass("두레 신청 블록 2(회귀)", (await page.$$(".tt-block")).length === 2); await jclick('.tab[data-view="enrollments"]'); pass("두레 내역 1행·3학점", (await page.$$("#en-table tbody tr")).length === 1 && (await page.$eval("#en-credits", (el) => el.textContent)) === "3"); await jclick('.tab[data-view="credits"]'); const crMajor = await page.$eval("#cr-table tbody tr", (tr) => tr.textContent.replace(/\s+/g, " ")); pass("두레 이수 전공 반영", crMajor.includes("+ 3") && crMajor.includes("48"), crMajor); await jclick('.tab[data-view="archive"]'); pass("두레 총람 12행(10+소계2)", (await page.$$("#ar-table tbody tr")).length === 12); pass("두레 총람 여석 임박 표시", (await page.$$("#ar-table .low-seats")).length >= 1); await jclick('.tab[data-view="notices"]'); pass("두레 공지 6건", (await page.$$("#notice-list li")).length === 6); // 겹침 회귀: bio105 차단 await jclick('.tab[data-view="catalog"]'); await jclick('[data-course="econ210"]'); // 이미 신청됨 → 취소 버튼 await page.waitForSelector("#course-dialog[open]"); await jclick("#cd-cancel"); await sleep(150); await jclick('[data-course="econ210"]'); await jclick("#cd-apply"); await sleep(150); await jclick('[data-course="bio105"]'); await page.waitForSelector("#course-dialog[open]"); pass("두레 겹침 차단(회귀)", (await page.$eval("#cd-warn", (el) => !el.hidden && el.textContent.includes("겹칩니다"))) && (await page.$eval("#cd-apply", (b) => b.disabled))); await jclick("#course-dialog .modal-close"); await jclick('.tab[data-view="enrollments"]'); await jclick("[data-cancel]"); pass("두레 내역 화면 취소", (await page.$$("#en-table tbody tr")).length === 0); pass("두레 1440 오버플로", (await over()) <= 0, `+${await over()}px`); // ══════════ 좁은 폭 ══════════ for (const w of [320, 390]) { await page.setViewport({ width: w, height: 800 }); await gotoFile("gaon-lms"); const og = await over(); pass(`가온 ${w}px`, og <= 0, `+${og}px`); await gotoFile("dure-enrollment"); const od = await over(); pass(`두레 ${w}px`, od <= 0, `+${od}px`); } // ── CSS 중괄호 균형 (v11 교훈) ── import fs2 from "node:fs"; for (const f of ["gaon-lms/styles/main.css", "dure-enrollment/styles/main.css", "gaon-lms/styles/tokens.css", "dure-enrollment/styles/tokens.css"]) { const c = fs2.readFileSync(path.join(root, "public/work", f), "utf8"); let depth = 0; for (const ch of c) { if (ch === "{") depth++; if (ch === "}") depth--; } pass(`CSS 중괄호 균형 ${f}`, depth === 0, `depth ${depth}`); } await browser.close(); console.log(results.join("\n")); const fails = results.filter((r) => r.startsWith("FAIL")).length; console.log(fails === 0 ? "\nALL PASS" : `\n${fails} FAIL`); process.exit(fails === 0 ? 0 : 1);