ChatGPT 가 HTML 내에서 전체 댓글 수를 가져오는 코드를 만들어줬고, 나는 이를 JS 함수를 만들어 붙여넣기만 했다.

이번에는 실제로 현재 페이지의 전체 댓글 선택 버튼을 클릭하고, 변경메뉴 내의 "휴지통으로 이동" 버튼을 클릭하는 코드를 CoPilot 과 함께 작성해서 완성하려 한다.

 

# 코딩 동반자 CoPilot 

CoPilot 은 왜 이름을 이렇게 지었는지 사용할수록 이해가 된다. 묻고 대답하는 ChatGPT 와 같은 형태 보다는 IDE 의 AutoCompletion 기능을 적극 활용하여 내가 개발하고 있는 코드를 보고 있다가 '아.. 이런 코드 작성하려고 하는거지?' 하며 끊임없이 나의 코드를 자동완성 해주려고 노력해준다.

또는 코드를 개발 시작하기 전에 의사(pseudo)를 코드가 아닌 주석으로 입력해주면 그 의사에 맞는 코드를 뿅~ 하고 만들어주며, 반대로 코딩에 심취한 나머지 주석을 안달고 넘어간 코드에 주석도 달아준다.

CoPilot 도 채팅 기능이 추가되고 지금은 File 내용을 붙여넣지 않아도 파일 자체를 참조할 수 있도록 지정해줄 수도 있어 좀더 광범위한 질답이 가능하며, 출시 예정인 CoPilot Workspace 기능이 나오면 이제 Code Snippet 수준이 아니라 요구사항을 기반으로 Project 자체를 CoPilot 과 함께 개발이 가능해진다.

 

# 코드 개발 시작

아무 이름의 .js 파일을 Visual Studio Code 로 생성 했고, 파일을 열고 일단 ChatGPT 가 만들어준 코드를 내용으로 하는 함수를 위에 붙여 두었다. 그리고 댓글관리 페이지의 HTML 을 크롬 브라우져로 개발자 도구 로 부터 복사해 와 .html 파일로 저장했다.

이제 CoPilot 에게 내가 만들고 싶은 JS 코드를 // 주석으로 설명한다

// ChatGPT 가 만든, 현재 댓글 수를 가져오는 함수
function getCurCount() {
    // Select the h3 element containing the specific txt_count
    const titContElement = document.querySelector('.tit_cont');

    // Select the span element containing the number within the h3 element
    const txtCountElement = titContElement.querySelector('.txt_count');

    // Get the text content from the span element
    const txtCount = txtCountElement.textContent;

    // Remove commas from the string and convert it to a number
    return parseInt(txtCount.replace(/,/g, ''), 10);
}

// list.html 에서 id 가 "checkComments" 인 input 을 클릭하고 "휴지통으로 이동" 이름의 버튼을 누르는 함수
function delComments() {
    // id 가 checkComments 인 element 를 가져와
    var checkComments = document.getElementById("checkComments");
    checkComments.click();

    // type 은 button 이고 class 는 btn_g , value 는 "휴지통으로 이동" 인 input 을 가져옴
    var delButton = document.querySelector("input[type=button].btn_g[value='휴지통으로 이동']");
    delButton.click();
}

// 3초에 1번 씩 getCurCount() 가 100 보다 크면 delComments() 를 실행한다 
setInterval(() => {
    if (getCurCount() > 100) {
        delComments();
    }
}, 3000);

 

코드 에서 getCurCount() 부분은 ChatGPT 가 만들어준 코드이고, 그 아래 delComments() 부터는 // 주석을 이용해 내가 만들고 싶은 코드의 주석을 넣어준 것이다. 처음부터 완전히 깔끔한 코드를 만들어주는것은 아니지만 내가 일부 수정하거나 주석의 내용을 보완/변경 해 나아가면서 좀더 내가 원하는것과 일치한 코드를 만들게할 수 있다.

 

# 브라우져에서 실행 시키기

간단히 저정도의 코드를 만들어서 Chrome 의 개발자 도구 내 console 에 붙여넣고 실행하면 나를 대신하여 댓글 삭제를 진행해준다.

1. 브라우저 콘솔 하단에 위에 만든 코드를 Copy & Paste 해놓고 엔터를 친다!

 

2. 자동으로 화면에 Click 이벤트를 보내며 돌아가는 모습을 구경하고 있으면 된다

 

# 좀더 나은 방향으로..

위의 ChatGPT 와 CoPilot 이 만들어준 코드로 붙여놓고 실행하면 대략 1페이지(15 댓글)을 지우는데에 3~5초 가량의 시간이 소요된다.

1. "8,157 Page * 4초(평균잡아) = 약 9시간" 의 시간이 소요된다. 물론 자동으로 돌고 있을 것이라 그냥 기다려주면 되긴 하지만 왠지 좀더 빠르게 진행되었으면 하는 욕심이 든다..

2. 게다가 현재 방식은 댓글 중 저 요상한 이름의 공격자와는 관계 없는 댓글들 까지도 모두 삭제해버리는 피해를 감수해야 한다.

위 두가지 고민을 해소하기 위해 약간의 시간을 더 투자해 개선을 해보기로 한다! 어떻게~? "영혼의 단짝 코 파일러~ㅅ 과 함께" 

나의 계획은 이렇다

1. 댓글목록 조회는 Ajax 로 XHR GET 요청을 하고 가져온 목록을 화면에 Rendering 하는데, 이때 Query String 으로 page=? 형태로 하여 현재 페이지에 뿌려줄 15개의 항목을 가져온다. 대략 조회는 1~2 초 정도가 소요된다. --> 이 요청을 내가 직접 JS 로 가져오고 병렬로 요청하면 아마도 시간이 대폭 단출 될 것이다!

2. 휴지통으로 이동 버튼 클릭 시에도 Ajax 로 XHR DELETE 요청을 하는데, 이때 form-data 형태로 지울 댓글들의 ID 를 "," separate 하여 보낸다! 15개 기준으로는 2~5초 가량이 소요된다. --> 이 요청에 15개가 아닌 더 많은 수의 item 을 한번에 넣어 보낼 수 있다면? 굿굿~!

3. 1번 + 2번 모두 가능하다면 1 번에서 가져온 item 들 중에서 작성자와 IP 필드가 있을 것이고, 이걸 패턴 매치하여 나쁜놈만 걸러서 삭제할 수 있다! 굿굿굿~!

그래서 일단 1~3 번을 수동으로 테스트 해봤는데~~! 오예~ 모두 가능한 상황이다!

 

# 개선된 코드

CoPilot 과 함께 난 // 주석으로 일을 시키고~ CoPilot 이 열심히 만들고 주인님이 빠꾸 해 가면서 만들어낸 코드는 아래와 같다.
(짧은 시간에 VERSION 3 가 되어버렸는데, VERSION 2는 유사한 방식이나 작업을 fromPage ~ toPage 의 범위로 하게 하였으나 이러면 중간에 삭제 대상이 아닌 댓글들로 목록이 꽉 차게 되면 문제가 생길 수 있어 방식을 변경했다)

아래 코드에서 blockersNamePattern 과 blockersIps 의 내용을 바꾸어서 실행하면 된다.
blockersNamePattern 은 해당 문자열이 author 내용 내에 일부 매치만 되어도 true 가 되므로 잘 생각해서 한다.

실행은 Chrome 개발자도구 Console 에 모든 코드를 붙여넣고 실행한 후,
deleteCommentsForBatchSize(<한번에 delete 요청 보낼 댓글 수>) 를 하면 된다.

////////////////////////////////////////////////////////////
//--- VERSION 3 
// 이거 쓰자.. 브라우져 console 에 다 붙여놓고 deleteCommentsForBatchSize(500) 이런식으로 실행하면 됨   
////////////////////////////////////////////////////////////

let totalCount = 0;
blockersNamePattern = ["Mmz","MjPAz","-1","if(now", "@@"];
blockerIps = ["158.247.***.145"];

function containsBlockerNamePattern(author, ip) {
    for (let i = 0 ; i < blockersNamePattern.length ; i++) {
        if (author.includes(blockersNamePattern[i]))
            return true;
    }

    for (let i = 0 ; i < blockerIps.length ; i++) {
        if (ip.includes(blockerIps[i]))
            return true;
    }

    return false;
}

function collectAllCommentIdsUntilLimit(limit) {
    // Create an array to store all comment IDs
    const allCommentIds = [];
    const fetchPromises = [];
    let page = 1; // Initialize page number

    // Function to delay execution
    const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

    // Function to fetch comments and process them
    const fetchAndProcessComments = () => 
        fetch(`https://goodjoon.tistory.com/manage/comments.json?filter=&searchType=writer&searchKeyword=&page=${page}`)
            .then(response => response.json())
            .then(data => {
                // Extract the comment IDs from the response and push them to the allCommentIds array
                const commentIds = data.items
                    .filter(item => containsBlockerNamePattern(item.author, item.ip))
                    .map(item => item.id);
                totalCount = data.totalCount;
                console.log("Page: " + page);
                console.log("commentIds to delete: " + commentIds.length);
                allCommentIds.push(...commentIds);

                // Check if the limit is reached or not
                if (allCommentIds.length < limit ) {
                    page++; // Increment page number for the next fetch
                    return delay(500).then(fetchAndProcessComments); // Continue fetching with delay
                }
            });

    // Start the fetching process
    fetchPromises.push(fetchAndProcessComments());

    // Return a promise that resolves when the limit is reached or all comments are fetched
    return Promise.all(fetchPromises).then(() => allCommentIds);
}

function deleteComments(ids) {
    // Create a FormData object
    const formData = new FormData();

    // Append the IDs to the FormData object
    formData.append('ids', ids.join(','));

    // Make a DELETE request to the comments API
    console.log(ids.length + " 개 삭제 요청 보냄... 기다려..");
    return fetch('<https://goodjoon.tistory.com/manage/comments.json>', {
        method: 'DELETE',
        body: formData
    })
        .then(response => {
            // response 를 출력
            console.log(response);
            return response.json();
        })
        .then(data => console.log(data))
        .then(() => console.log("삭제 완료!"));
}

let timeoutHandle = null;
let itemsPerPage = 15;

function deleteCommentsForBatchSize(batchSize) {
    // Collect all comment IDs from the specified pages
    collectAllCommentIdsUntilLimit(batchSize).then(ids => {
        // Delete the collected comment IDs
        console.log("totalCount: " + totalCount);
        if (ids.length >0 ) { // 총 댓글 수가 (pageTo - pageFrom) * itemsPerPage 보다 크거나 같으면
            console.log("삭제할 댓글 수: " + ids.length);
            deleteComments(ids).then( () =>
                timeoutHandle = setTimeout(() => deleteCommentsForBatchSize(batchSize), 1000)   // 1초 뒤에 deleteIdsOfPage(pageFrom, pageTo) 를 실행
            );
        } else {
            console.log("삭제할 댓글이 없음");
        }
    });
}

테스트 하다보니 제약사항이 있었다!

1. collectAllCommentIdsUntilLimit() 코드 내에 delay 를 위한 Promise 에 전달되는 timeout 값은 500ms 정도가 적당하다. 더 짧게 하다 보니 Tistory 서버가 앞단에서 500번대 에러를 내며 한동안 응답하지 않음을 발견했다

2. batchSize 는 delete 시에 큰 영향이 있는데, 한번에 1,000 개를 넣어보니 Tistory 의 Rev. Proxy 서버 쯤에서 너무 응답이 길어져 Response Timeout 을 낸다. 500 개 정도 부터 시작해봐도 될것 같다

이렇게 작성한 코드를 붙여넣고 위에 설명한대로 deleteCommentsForBatchSize() 를 실행시키면 몇시간 딴짓하고 돌아오면 12만개 쯤이야 삭제를 해놨을 것이다~~

 

# 마치며

이제 Tistory 는 Blog 시장에서 사라져버리는 수순을 기다리고 있는것인지 무언지 정말 실망스러운 운영과 시스템 유지보수가 아쉬움의 선을 넘어선 감정을 갖게 만든다.

무엇이 발전하고 있는지는 모르겠으나 플랫폼이 망가져가고 있는것은 확실히 느낀다.

 

 

반응형
블로그 이미지

Good Joon

IT Professionalist Since 1999

,

지난 글에서는 뭐가 문제인지, 어떻게 할 구상인지에 대해 글을 써봤고, 이번에는 본격적으로 AI 에게 개발을 시켜볼 차례이다.

# ChatGPT (GPT-4o)

 ChatGPT 는 현재 상황을 인식시키고 답을 내놓으라고 할 때 참 착하게 동작해준다. GPT-4o 모델로 image 를 포함한 file 을 upload 하여 일을시킬 수 있어 좀더 여러가지 일을 시킬 수도 있어 좋다.(무료 플랜에서도 제한적으로 사용 가능하다)

 ChatGPT 에게 시킬 일 : ChatGPT 에게는 HTML 코드의 일부를 Image 로 캡춰해서 분석하게 한 후 총 남은 댓글의 개수를 가져오는 Javascript 코드를 만들어 내라고 시켰다. (아래 캡쳐의 122,364 라는 숫자를 가져오고 싶은 것이다)

무한정 일을 반복시킬 수는 없으므로 처음에는 남은 댓글 수가 0이 될 때 까지 돌려버리려 했다. (나중에는 정말 진심으로 감사드릴 댓글까지 지워지면 안되겠다 싶어 좀더 시간을 들여 수정 했지만..^^)

위 처럼 Chrome 의 개발자도구에 있는 HTML 코드 일부를 Image 로 캡춰해서 필요한 부분을 가져오는 JS 를 만들어달라고 하니 아래 처럼 코드를 생성해준다

나는 Javascript 를 Chrome 개발자도구의 console 에서 직접 실행시킬것이기 때문에 "DOMContentLoaded" 이벤트 Listener 를 구현할 필요는 없고, 그 안의 내용만 있으면 된다.

요즘은 Client 쪽 JS 를 개발할 일이 없기때문에 왠만한 ECMAScript 표준함수 들에 대해서는 다 까먹고 있었는데 궂이 찾아볼 필요 없이 잘 코드를 뽑아준다. HTML 에는 숫자에 자릿수 구분자 "," 가 들어가 있는데, 이걸 replace 해주어 int 로 변환해주는 코드도 친절하게 넣어준다.

 

# 좀더 일 주기

잘 해주었다 GPT-4, 그러나 그리 호락한 주인이 아니어서 일을 좀 더 하라고 지시하고 싶은 부분이 있다.

HTML 구조 상 ChatGPT 가 만들어준 코드 처럼 ".txt_count" 클래스로만 가져오면 혹시나 다른데에서도 저 클래스를 쓰는 경우가 생긴다면 잘못된 값을 가져올 수 있으니 (뭐 현재 페이지 코드를 보면 그렇지는 않았지만~), 좀더 querySelector() 의 query 를 specific 하게 쓰거나 다른 코드를 덧붙였으면 하는 주문을 해봤다

💬 const txtCountElement = document.querySelector('.txt_count');
      여기서 .txt_count 클래스는 다른곳에도 사용될 수 있으니 조금 변경해줘

🤖

다른 곳에서도 .txt_count 클래스가 사용될 수 있기 때문에, 특정 부모 요소 내에서 해당 요소를 선택하도록 수정하겠습니다. tit_cont 클래스를 가진 요소 내에서 .txt_count 클래스를 선택하도록 변경하겠습니다.

<!DOCTYPE html>
....
중략
...

    <script>
        // JavaScript to get the number and convert it
        document.addEventListener('DOMContentLoaded', (event) => {
            // Select the h3 element containing the specific txt_count
            const titContElement = document.querySelector('.tit_cont');

            // Select the span element containing the number within the h3 element
            const txtCountElement = titContElement.querySelector('.txt_count');

            // Get the text content from the span element
            const txtCount = txtCountElement.textContent;

            // Remove commas from the string and convert it to a number
            const number = parseInt(txtCount.replace(/,/g, ''), 10);

            // Log the number to the console
            console.log(number);
        });
    </script>
</body>
</html>

이 코드에서는 .tit_cont 클래스를 가진 부모 요소 내에서 .txt_count 클래스를 가진 자식 요소를 선택합니다. 이를 통해 해당 클래스가 다른 곳에 사용되더라도 정확한 요소를 선택할 수 있습니다.


그래서 일단 전체 댓글의 개수를 가져오는 function 하나는 아래와 같이 getCurCount() 라는 이름으로 만들었다

// ChatGPT 가 만든, 현재 댓글 수를 가져오는 함수
function getCurCount() {
    // Select the h3 element containing the specific txt_count
    const titContElement = document.querySelector('.tit_cont');

    // Select the span element containing the number within the h3 element
    const txtCountElement = titContElement.querySelector('.txt_count');

    // Get the text content from the span element
    const txtCount = txtCountElement.textContent;

    // Remove commas from the string and convert it to a number
    return parseInt(txtCount.replace(/,/g, ''), 10);
}

 

이렇게 ChatGPT 에게 HTML 의 Capture Image 를 주고 코드를 만들게 하고 수정까지 요청하여 간단하지만 코드를 하나 만들었으니, 다음에는 CoPilot 을 옆에 앉혀두고 나머지 기능들을 개발 해 나아가자 (다음글 계속)

 

반응형
블로그 이미지

Good Joon

IT Professionalist Since 1999

,

# 댓글 테러 발견

 열정이 식었다기 보다는 다른 것들로 인해 방치되고 있던 나의 티스토리 블로그, 어쩌다 들어와보다 보니 게시물에 댓글들이 엄청 달려있다~! "옷? 사람들이 갑자기 내 블로그에 급 관심 상승할만하게 있었나!?!?!?"

 싶었으나.. 역시나 그럴리는 없고 왠놈이 내 블로그 댓글이 로그인 안한 사람들도 댓글 달 수 있다는 사실을 알고 티스토리를 해킹해보려 시도하다가 잘 안되었는지 (XSS, SQL Injection 을 하려던 흔적이 많이 보였다) 약 12만개 정도의 댓글을 도배 해놨다.

처음에는 원래 티스토리에 있는 기능인 "스팸차단" 기능을 사용하여 작성자와 IP 로 차단을 하면 자동으로 기존 그들이 작성한 댓글들도 삭제된다는 말을 믿고 기능을 실행 해봤다

그러나 이게 뭐지.. 화면에는 아무 반응이 없고 댓글들이 사라지지도 않는다. 왜그런지 크롬 개발자도구 (Option + Cmd + I) 눌러보니 그래도 대형 블로그 플랫폼인 Tistory 에서 서버 에러가 나고 있다

처음에는 임시적인 문제인가 싶었으나 사실 몇달 전에도 이 기능을 실행해봤으나 지금도 결과가 똑같았다

500 에러 나고있는 차단기능

위 처럼 차단 요청에 대해 500 에러가 나고있으며 아마도 오랜동안 쭈욱 저 기능은 안돌고 있는것 같다. 티스토리에 문의를 해봤으나 답변도 없고 답답함의 연속이라 티스토리를 원망하기 시작한다.

혹시나 하여 복수개가 아닌 개별로 선택하고 스팸차단을 실행해봤지만 결과는 아래와 동일하다

 

# 되는건 뭔가

 다행인지 불행인지 차단기능은 안되지만 댓글을 삭제하는 기능은 가능했다. 한땀 한땀 눌러서가 아니라 1 페이지에 15개의 댓글이 뜨는데 그 15개를 전체선택 해서 삭제하는 것(휴지통으로 보내기)은 가능했다.

1 페이지 내의 댓글 전체선택 (15개) 하는 경우,

- 총 122,363 개 댓글이 있으니 8,157 개의 페이지가 있단 얘기이고
- 전체선택 체크박스 클릭 + 변경 버튼 클릭 + 휴지통으로 이동 버튼 클릭 = 3 클릭
- 8,157 x 3 click = 24,471

즉, 24,471번의 클릭을 하면 지울 수 있는 일이다~! 그런데..

- 클릭 1회 당 1초
- 삭제 후 로딩 2초
- 로 계산하면.. 24,471 초 + 8,157페이지 * 2초 로딩 = 40,785 초 = 679.75분 = 11.32 시간 = 약 11시간 20분

즉, 11시간 20분 동안 쉬지도 못하고 화장실도 못가고 클릭을 하고 있어야 한다는 결론이 나왔고, 혹시나 병원비가 나올지도 모르겠다는 생각이 들어서 기계를 시키기로 마음을 먹었다.

 

# 어떻게 할건가

일단 POSTMAN 을 켰고, 티스토리 -> 카카오의 OAuth2 인증을 거쳐 Token 을 받고 인증을 하고 들어가야 하는데.. 이걸 세팅하고 테스트 하고 개발해서 돌리는 시간도 한 11시간은 걸릴것 같다는 판단이 들었고, 최장 2시간 이내에 못끝낼거면 하지 말자는 각오로 20분 정도 진행 하다가 이건 아니다 싶어 방향을 전환했다.

Chrome 의 개발자도구로 돌아가서 페이지를 분석해보니 아래 처럼 현재 페이지의 전체 댓글을 선택하는 체크박스와 변경버튼 눌렀을 때 펼쳐지는 메뉴에서 휴지통으로 이동 버튼의 위치를 찾을 수 있었다.

이제 나의 계획은 이미 OAuth2 인증 다 타고 들어간 저 Session 에서 개발자도구 Console 에서 DOM 으로 올라온 페이지를 보고 Javascript 를 실행시켜 나 대신 클릭을 시키는 것~! (이건 1차 계획이었고, 너무 느려서 방법을 바꿨다.. 이건 추가설명 예정)

자 이제 구상을 했으니 AI 들에게 일감을 던져볼 차례이다 (다음 글에서..)

 

반응형
블로그 이미지

Good Joon

IT Professionalist Since 1999

,