조선대학교 에브리타임 강의실 버그 픽스

환리·2025년 3월 5일

시작하기 앞서..

안녕하세요? 시간표 업데이트하는 사람입니다.
지난 에브리타임 시간표 3차 업데이트 이후(약 3/2)에 강의실이 4자리가 아닌 뒤에 끝 3자리만 표시되는 버그를 발견,이에 지속적인 에브리타임 측과 문의과 피드백을 받으면서 해결할려고 하는 중입니다. 학기초, 그것도 신입생/복학생이 들어어와서 해당 3자리만 표시되는 문제는 신입생/복학생 모두에게 혼선을 주고 있습니다. 이에 보다 더 나은 학교생활을 제공하기 위해서 저나 에브리타임이나 열심히 버그픽스중에 있습니다.

현재 에브리타임 측과 문의해본 바, 회사 사정으로 자세한 원인은 알 수 없으나 html파일과 Javascript파일들을 분석해본 바 저의 소견으로는시간표 데이터들을 정상적으로 데이터베이스(db)로 저장되었으나, 해당 저장소를 다시 사용자에게 가져오는 API 혹은웹/앱 구동하는 자바스크립트가 잘못 된거 같습니다.

따라서 제가 버그없이 잘 구동되는 js파일을 따로 작성 에브리타임 측에 보냈습니다

일단 급한데로 인터넷 웹 브라우저(웨일, 마소엣지, 크롬, 브래이브, 파폭 등등) 에서라도 잘 작동되도록 하기 위해서 불러오는 JS파일을 뜯어고치는 방법을 저의 velog에 자세히 적어놓았습니다.

이동통신단말기(aka 스마트폰)로 구동되는 앱은 현재로서는 앱자체를 뜯어고쳐야 하는데 이렇게 되버리면 제가 문제가 나타는 모든 핸드폰에 깔려있는 앱을 수정해야해서 현실적으로 불가능합니다. 이점 양해부탁드립니다.

현재로서는 빠른 시일 내로 에브리타임 자체에서 문제가 해결되길 기다리는 수밖에 없습니다. 감사합니다.

본격적인 시작(사진 글자가 잘 안보이면 확대해서 봐주세요)

STEP1. Tampermonkey 확장 프로그램 다운로드

다음 사이트에 들어가서 Tampermonkey 확장프로그램을 다운로드 받는다.
https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=ko
해당 확장 프로그램은 웹에서 실행되는 자바스크립트 자체를 뜯어고치는 확장 프로그램입니다.
주로 광고차단, 웹해킹 등에서 사용되지만 오늘은 일단 급한데로 강의실4자리가 온전히 표기되도록 하는 스크립트만을 적었습니다.

SPET2. 스크립트 편집기 들어가기

Tampermonkey 다운로드 후 Tampermonkey 아이콘 클릭 -> 대시보드 를 클릭해서 다음과 같이 타고 들어가준다.

이후 +버튼을 눌러준다.

STEP3. 스크립트 복붙

정상적으로 타고 들어갔다면 다음과 같은 창이 뜹니다.

이후 해당 검은박스에 있는 모든 내용을 삭제 후, 아래 JS를 복붙해준다.

// ==UserScript==
// @name         에브리타임 강의실 정보 자동 수정
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  서버에서 받아온 XML의 강의실 정보를 올바르게 수정하여 적용
// @author       You
// @match        https://everytime.kr/timetable*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 📌 1️⃣ XMLHttpRequest 가로채기 (XML 응답 수정)
    function interceptXHR() {
        const open = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function(method, url) {
            this.addEventListener("readystatechange", function() {
                if (this.readyState === 4 && this.responseText) {
                    try {
                        let parser = new DOMParser();
                        let xmlDoc = parser.parseFromString(this.responseText, "text/xml");

                        console.log("📌 서버 응답 감지 (XML)", xmlDoc);

                        // 모든 subject(과목) 태그를 가져옴
                        let subjects = xmlDoc.getElementsByTagName("subject");

                        for (let subject of subjects) {
                            let timeNode = subject.getElementsByTagName("time")[0]; // <time> 태그
                            let dataNodes = subject.getElementsByTagName("data");  // <data> 태그들

                            if (timeNode && dataNodes.length > 0) {
                                let timeValue = timeNode.getAttribute("value"); // 강의 시간 정보
                                let placeList = extractPlaces(timeValue);

                                console.log("📌 추출된 강의실 목록:", placeList);

                                // <data> 태그 개수와 강의실 목록 개수가 다를 경우, 부족한 부분은 첫 번째 강의실로 채움
                                for (let i = 0; i < dataNodes.length; i++) {
                                    let placeAttribute = dataNodes[i].getAttribute("place");

                                    if (placeAttribute) {
                                        let newPlace = placeList[i] || placeList[0]; // 인덱스 초과 시 첫 번째 강의실 사용
                                        console.log(`📌 강의실 수정: ${placeAttribute} → ${newPlace}`);
                                        dataNodes[i].setAttribute("place", newPlace); // 강의실 정보 업데이트
                                    }
                                }
                            }
                        }

                        // 수정된 XML을 다시 문자열로 변환
                        let serializer = new XMLSerializer();
                        let modifiedXML = serializer.serializeToString(xmlDoc);

                        // 서버 응답을 수정하여 반환
                        Object.defineProperty(this, "responseText", { value: modifiedXML });

                    } catch (error) {
                        console.error("❌ XML 처리 중 오류 발생:", error);
                    }
                }
            });
            return open.apply(this, arguments);
        };
    }

    // 📌 2️⃣ Fetch API 가로채기 (XML 응답 수정)
    function interceptFetch() {
        const originalFetch = window.fetch;
        window.fetch = function(...args) {
            return originalFetch(...args).then(response => {
                return response.clone().text().then(text => {
                    let parser = new DOMParser();
                    let xmlDoc = parser.parseFromString(text, "text/xml");

                    console.log("📌 Fetch 요청 감지 (XML)", xmlDoc);

                    let subjects = xmlDoc.getElementsByTagName("subject");

                    for (let subject of subjects) {
                        let timeNode = subject.getElementsByTagName("time")[0];
                        let dataNodes = subject.getElementsByTagName("data");

                        if (timeNode && dataNodes.length > 0) {
                            let timeValue = timeNode.getAttribute("value");
                            let placeList = extractPlaces(timeValue);

                            console.log("📌 추출된 강의실 목록:", placeList);

                            for (let i = 0; i < dataNodes.length; i++) {
                                let placeAttribute = dataNodes[i].getAttribute("place");
                                if (placeAttribute) {
                                    let newPlace = placeList[i] || placeList[0];
                                    console.log(`📌 강의실 수정: ${placeAttribute} → ${newPlace}`);
                                    dataNodes[i].setAttribute("place", newPlace);
                                }
                            }
                        }
                    }

                    let serializer = new XMLSerializer();
                    let modifiedXML = serializer.serializeToString(xmlDoc);

                    return new Response(modifiedXML, {
                        status: response.status,
                        statusText: response.statusText,
                        headers: response.headers
                    });
                });
            });
        };
    }

    // 📌 3️⃣ 강의실 정보 추출 함수
    function extractPlaces(timeValue) {
        let matches = timeValue.match(/\(([^)]+)\)/g); // 괄호 안의 정보 추출
        if (!matches) return [];

        let placeList = [];
        matches.forEach(match => {
            let places = match.replace(/[()]/g, "").split(","); // 괄호 제거 후 강의실 분리
            placeList.push(...places); // 여러 강의실을 리스트에 추가
        });

        return placeList;
    }

    // 📌 4️⃣ MutationObserver로 DOM 업데이트 감지
    function observeDOMChanges() {
        const observer = new MutationObserver(() => {
            console.log("📌 페이지 업데이트 감지 (강의실 수정)");
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // 📌 5️⃣ Tampermonkey 실행 시 초기화
    window.addEventListener('load', () => {
        interceptXHR();    // XMLHttpRequest 가로채기
        interceptFetch();  // Fetch API 가로채기
        observeDOMChanges(); // DOM 변경 감지 시작
    });

})();

정상적용하면 아래 화면과 같다.

이후 [Ctrl + S] 키를 눌러서 저장해준다.

STEP4. 확인해보기

적용전 (예시 시간표 입니다)

적용후
일부 3자리로 표현되던 강의실이 정상적으로 4자리 모두 출력되는 모습

Q: 어라 계속 3자리인데?
A1: f5(새로고침)한번 해주세요.
A2: 아래사진참고

사진과 같이 초록색바가 표시되어야합니다!

아래 사진과 같이 회색바면 클릭해서 초록색바로 바꿔주세요.

주요 아이디어 설명

(이후 이어질 내용은 준/주전공자들 기준으로 설명드립니다)

일단은 왜 시간표자료가 어긋날까?

🚨 1. 서버에서 XML 데이터가 변형되는 과정에서 발생할 가능성

  • (1) 서버에서 강의실 정보를 잘못 저장하는 경우
    서버에서 강의실 정보를 place="IT융합대학-312"로 저장한 후 클라이언트(브라우저)에서 이를 요청하면, 클라이언트는 그대로 표시.
    하지만 place="IT융합대학-3128"로 저장되어야 하는데, 서버 응답에서 마지막 숫자가 잘려서 3자리(312)만 전송될 수 있음.

  • (2) DB 저장 과정에서 강의실 번호가 잘리는 경우
    강의실 번호가 3자리와 4자리가 혼재된 데이터 구조를 가지고 있을 경우, 특정 프로세스에서 4자리가 3자리로 변환되는 버그 발생 가능.
    예를 들어, VARCHAR(3)처럼 컬럼 길이가 3자리로 제한되어 있다면, "3128" → "312"처럼 잘릴 수 있음.

🚨 2. 클라이언트(브라우저)에서 강의실 번호를 처리하는 과정에서 발생할 가능성

  • (3) 브라우저에서 place 값을 렌더링할 때 잘리는 경우
    XML 데이터를 받아와서 JavaScript로 파싱하는 과정에서 마지막 숫자가 잘리는 문제 발생 가능.
    예를 들어, XMLSerializer()를 사용하거나 innerText를 읽어올 때 숫자가 truncate(잘림)될 가능성 있음.

  • (4) JavaScript 코드에서 place 값을 parseInt() 같은 방식으로 처리할 경우
    강의실 번호가 숫자로만 이루어져 있다면, JavaScript가 이를 자동으로 숫자로 변환할 수도 있음.
    "5109"를 parseInt()로 처리하면 "510"이 될 수 있음.

  • (5) HTML 태그 속성에서 place 값이 CSS에 의해 잘리는 경우
    overflow: hidden 같은 CSS 속성 때문에 화면에 3자리만 보일 가능성도 있음.
    브라우저 개발자 도구에서 실제 place 값이 3자리인지 확인해야 함.

🚨 3. 서버에서 XML을 생성하는 과정에서 생길 가능성

  • (6) XML 응답을 생성할 때 place 값을 잘못 조합하는 경우
    <time> 태그와 <data> 태그에서 사용하는 강의실 정보가 다른 테이블에서 조합될 경우,

<time> 태그는 올바른 4자리 강의실 번호를 가져오고,
<data> 태그는 잘못된 3자리 강의실 번호를 가져오는 매칭 오류 발생 가능.
예를 들면, 다음과 같은 잘못된 SQL 조인이 실행될 수 있음:

SELECT time_table.room, data_table.place 
FROM time_table
JOIN data_table ON time_table.class_id = data_table.class_id;

이후 현재 로컬에서 수정가능한것은 1번밖에 없다고 판단

왜냐

  • 2번같은 경우는 브라우저 버그일 확률이 매우 높은데, 브라우저가 버그날 확률은.. 난 들어본 적이 없다
  • 3번같은 경우는 로컬에서 해결할 수 있는 방법이 없다.

html과 Js 삽질을 통해서 서버에서 XML을 불러오는 것은 확인

시간표나와있는 게시판으로 이동 - 개발자 도구 - network - 최하단 table xml 파일을 보면 다음과 같이 뜬다.
(당연히 아래는 예시 시간표다)

<response>
  <table id="53124329" is_deleted="0" name="2" year="2025" semester="1" priv="0" primary="0" created_at="2025-03-05 09:09:45" updated_at="2025-03-05 17:45:42">
    <subject id="7274959">
      <internal value="00071-01"/>
      <name value="안보학"/>
      <professor value="박주환"/>
      <time value="월09:00~10:00(학군단-1101,학군단-1101), 목09:00~11:00(학군단-1101,학군단-1101)">
        <data day="0" starttime="108" endtime="120" place="학군단-110"/>
        <data day="3" starttime="108" endtime="132" place="학군단-110"/>
      </time>
      <place value=""/>
      <credit value="3"/>
      <closed value="0"/>
    </subject>
    <subject id="7274961">
      <internal value="00089-02"/>
      <name value="예술의이해"/>
      <professor value="장민한,김미영,김현재"/>
      <time value="화10:00~13:00(미술대학 교사-6109)">
        <data day="1" starttime="120" endtime="156" place="미술대학 교사-6109"/>
      </time>
      <place value=""/>
      <credit value="3"/>
      <closed value="0"/>
    </subject>
    <subject id="7274966">
      <internal value="00121-01"/>
      <name value="교육학개론"/>
      <professor value="민형덕"/>
      <time value="월14:00~16:00(제1자연과학관-5516)">
        <data day="0" starttime="168" endtime="192" place="제1자연과학관-5516"/>
      </time>
      <place value=""/>
      <credit value="2"/>
      <closed value="0"/>
    </subject>
    <subject id="7274968">
      <internal value="00121-80"/>
      <name value="교육학개론"/>
      <professor value="고화정"/>
      <time value="화18:00~20:00(사회과학·사범대학-3107)">
        <data day="1" starttime="216" endtime="240" place="사회과학·사범대학-3107"/>
      </time>
      <place value=""/>
      <credit value="2"/>
      <closed value="0"/>
    </subject>
    <subject id="7274969">
      <internal value="00130-01"/>
      <name value="일반생물학실험1"/>
      <professor value="이한용"/>
      <time value="목11:00~12:30(체육대학-9213)">
        <data day="3" starttime="132" endtime="150" place="체육대학-9213"/>
      </time>
      <place value=""/>
      <credit value="1"/>
      <closed value="0"/>
    </subject>
    <subject id="7274971">
      <internal value="00130-03"/>
      <name value="일반생물학실험1"/>
      <professor value="이현화"/>
      <time value="수16:00~17:30(체육대학-9213)">
        <data day="2" starttime="192" endtime="210" place="체육대학-9213"/>
      </time>
      <place value=""/>
      <credit value="1"/>
      <closed value="0"/>
    </subject>
    <subject id="7274992">
      <internal value="00169-80"/>
      <name value="경제원론1"/>
      <professor value="조동민"/>
      <time value="수18:00~21:00(법과대학 및 경상대학-5309)">
        <data day="2" starttime="216" endtime="252" place="법과대학 및 경상대학-5309"/>
      </time>
      <place value=""/>
      <credit value="3"/>
      <closed value="0"/>
    </subject>
    <subject id="7275001">
      <internal value="00176-03"/>
      <name value="현대인의건강관리"/>
      <professor value="서영환"/>
      <time value="수14:00~16:00(체육대학-6213,체육대학-6213), 목16:00~17:00(체육대학-6213,체육대학-6213)">
        <data day="2" starttime="168" endtime="192" place="체육대학-621"/>
        <data day="3" starttime="192" endtime="204" place="체육대학-621"/>
      </time>
      <place value=""/>
      <credit value="3"/>
      <closed value="0"/>
    </subject>
    <subject id="7275022">
      <internal value="20035-03"/>
      <name value="정치학"/>
      <professor value="최선"/>
      <time value="월12:00~14:00(사회과학·사범대학-1204,사회과학·사범대학-1204), 수11:00~12:00(사회과학·사범대학-1204,사회과학·사범대학-1204)">
        <data day="0" starttime="144" endtime="168" place="사회과학·사범대학-120"/>
        <data day="2" starttime="132" endtime="144" place="사회과학·사범대학-120"/>
      </time>
      <place value=""/>
      <credit value="3"/>
      <closed value="0"/>
    </subject>
    <subject id="7275067">
      <internal value="20141-01"/>
      <name value="서양사개론"/>
      <professor value="주의돈"/>
      <time value="화14:00~16:00(대학 본관-4272,대학 본관-4272), 목15:00~16:00(대학 본관-4272,대학 본관-4272)">
        <data day="1" starttime="168" endtime="192" place="대학 본관-427"/>
        <data day="3" starttime="180" endtime="192" place="대학 본관-427"/>
      </time>
      <place value=""/>
      <credit value="3"/>
      <closed value="0"/>
    </subject>
    <subject id="7275087">
      <internal value="20268-02"/>
      <name value="미시경제학"/>
      <professor value="박성훈"/>
      <time value="목14:00~15:00(법과대학 및 경상대학-4319,법과대학 및 경상대학-4319), 금10:00~12:00(법과대학 및 경상대학-4319,법과대학 및 경상대학-4319)">
        <data day="3" starttime="168" endtime="180" place="법과대학 및 경상대학-431"/>
        <data day="4" starttime="120" endtime="144" place="법과대학 및 경상대학-431"/>
      </time>
      <place value=""/>
      <credit value="3"/>
      <closed value="0"/>
    </subject>
  </table>
</response>

찾았죠?
XML이 문제네요.

다음 코드를 봐보자

      <internal value="20268-02"/>
      <name value="미시경제학"/>
      <professor value="박성훈"/>
      <time value="목14:00~15:00(법과대학 및 경상대학-4319,법과대학 및 경상대학-4319), 금10:00~12:00(법과대학 및 경상대학-4319,법과대학 및 경상대학-4319)">
        <data day="3" starttime="168" endtime="180" place="법과대학 및 경상대학-431"/>
        <data day="4" starttime="120" endtime="144" place="법과대학 및 경상대학-431"/>

time value값이 우리가 찾던 정상적인 시간과 건물이 있는 것을 확인했고
place값이 3자리로 잘리는 버그 확인.

이후 XMLHttpRequest()를 가로채서, 수정을 가하는 코드를 짜면 위에 코드와 같이 짜진다.

profile
늙고병듦.

0개의 댓글