ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SW Expert Academy 예제 입출력 복사 크롬 확장 프로그램 제작하기
    Programming/TIL 2025. 2. 16. 16:05

    SWEA(SW Expert Academy) 사이트에서 예제 입력과 출력을 쉽게 복사할 수 있는 크롬 확장 프로그램 개발 과정을 상세히 설명합니다. 개발자 도구를 활용한 기능 구현 실험, manifest 파일 작성, 실제 코드 구현, 크롬 웹스토어 등록까지 과정을 다룹니다.

    개요

    웹스토어 승인이 완료되어서 다운로드가 가능합니다. 한 번 받아보세요!
    확장프로그램 다운로드 링크

    SW Expert Academy(이하 SWEA)는 삼성에서 제작한 알고리즘 문제 풀이 사이트입니다.
    많은 사람들이 코딩 테스트 준비, 특히 삼성 SW 역량 테스트 대비에 자주 사용하죠.

    저도 최근 SSAFY에 입과 하면서 SWEA 사이트를 자주 사용하고 있습니다.
    삼성에서 운영하는 교육 프로그램이다 보니 알고리즘 교육과 과제 제출이 모두 SWEA에서 이루어지거든요.

    사이트에 대한 불만은... 많지 않습니다!

    단 하나만 제외하고는요.

    예제 입력과 출력을 받는 게 너무 불편합니다.

    백준 사이트에는 예제 입력/출력을 복사할 수 있는 버튼이 있습니다.
    BOJ CopyPasta

    터미널에 바로 붙여 넣어 테스트가 가능합니다.

    프로그래머스나 LeetCode 같은 사이트는 웹 에디터가 잘 갖춰져 있습니다.
    실행 버튼을 누르면 각 테스트 케이스별 결과를 빠르게 확인하고 비교해 볼 수 있습니다.
    Programmers

    SWEA에도 웹 에디터가 있지만, 대부분 로컬 IDE에서 작업합니다.
    마지막 제출만 SWEA에서 진행하죠.

    이 경우 예제 입력을 복사해서 사용하곤 하는데, SWEA 사이트에는 복사 버튼이 없습니다.

    버튼이 없으면 직접 드래그해서 복사하면 되지 않느냐고요?

    이렇게 예제 입력에 설명을 위한 주석이 달려있거나
    SWEA input with comment

    긴 입력은 잘라서 보여주는 경우가 태반입니다.
    SWEA input cut

    "진짜" 예제 입력을 복사하려면, 사이트에 있는 input.txt 파일을 다운받아 사용해야 합니다.
    이 파일을 문제 풀이 경로에 옮기거나, 메모장으로 열어서 복사하는 방법을 쓰는 경우가 많습니다.

    테스트 입력 받는 데 번거로운 과정이 중간에 너무 많아 불편합니다.
    그냥 버튼 하나만 눌러서 클립보드에 복사할 수 있으면 정말 좋을 텐데요.

    SWEA 사이트에 예제 입출력 복사 버튼을 추가하는 크롬 확장 프로그램을 만든다면 어떨까요?

    앞으로의 SSAFY 라이프가 한결 수월해질 것 같습니다.

    개발자 도구로 실험해보기

    먼저 개발자 도구와 함께 기능 구현 가능성을 실험해 보겠습니다.
    플랫폼의 사이트 구조를 파악하고, 자바스크립트로 클립보드 복사 버튼을 구현해 볼게요.

    SWEA 사이트 구조 파악

    문제 정보 페이지 확인 결과 입력과 출력 정보는 box_type1 div 클래스에 들어있었습니다.
    SWEA box_type1

    입력 정보는 box_type1 .left에 있고, 출력 정보는 box_type1 .right에 있네요.

    사이트에 나와 있는 예제 입출력 텍스트 데이터는 무시하도록 하겠습니다.
    앞서 언급한 주석과 잘린 입력 문제가 있으니까요.

    대신 각 div 하단에 있는 input.txtoutput.txt 파일 링크가 실제 사용해야 할 데이터를 담고 있습니다.

    이런 html 구조로 되어있는데요.

    <div class="down_area">
      <a href="#">
        <span>                                                                        
        </span>
        </a><a href="/main/common/contestProb/contestProbDown.do?downType=in&amp;contestProbId=AV5QSEhaA5sDFAUq&amp;_menuId=AVtnUz06AA3w6KZN&amp;_menuF=true">input.txt</a>
        <i><span class="hide">다운로드</span></i>
    
    </div>

    링크를 클릭하면 예제 입력 텍스트 파일을 다운받는 구조입니다.

    클립보드 복사 기능 구현

    링크를 클릭하면 바로 텍스트 파일이 다운로드되는데, 이 내용을 파일 다운로드 없이 가져올 수는 없을까요?

    다운로드 링크에 그냥 웹 요청을 보내면 어떨까요?
    자바스크립트의 fetch 함수를 사용해 보았습니다.

    개발자 도구 콘솔에 아래 코드를 입력합니다.

    fetch('https://swexpertacademy.com/main/common/contestprob/contestprobdown.do?downtype=in&contestprobid=av5qsehaa5sdfauq&_menuid=avtnuz06aa3w6kzn&_menuf=true')
      .then(response => response.text())
      .then(data => console.log(data));

    출력:

    3
    3 17 1 39 8 41 2 32 99 2
    22 8 5 123 7 2 63 7 3 46
    6 63 2 3 58 76 21 33 8 1

    파일 다운로드 없이, 바로 응답에서 텍스트 데이터를 받아올 수 있었습니다.

    이제 이 내용을 클립보드에 복사하면 되겠네요.

    navigator.clipboard.writeText 함수를 사용해 보겠습니다.

    fetch('https://swexpertacademy.com/main/common/contestProb/contestProbDown.do?downType=in&contestProbId=AV5QSEhaA5sDFAUq&_menuId=AVtnUz06AA3w6KZN&_menuF=true')
      .then(response => response.text())
      .then(data => navigator.clipboard.writeText(data));

    하지만 여기서 오류가 발생했습니다.

    Uncaught (in promise) NotAllowedError: Failed to execute 'writeText' on 'Clipboard': Document is not focused.

    문서에 focus가 되어있지 않다고 합니다.
    저는 개발자 콘솔에서 코드를 실행했으니 그럴만도요...

    찾아보니 보안상의 이유로 클립보드 접근에는 사용자의 직접적인 상호작용이 필요하다고 합니다.

    어차피 저희 확장 프로그램은 버튼을 눌렀을 때 클립보드에 복사하는 구조입니다.
    이참에 버튼을 만들어서 버튼을 눌렀을 때 클립보드에 복사하도록 만들어보겠습니다.

    콘솔에서 버튼을 생성하고 페이지에 추가합니다.

    const button = document.createElement('button');
    button.textContent = "Copy Input";
    button.className = `test-button input`;
    
    const inputDiv = document.querySelector('.box_type1 .left');
    inputDiv.appendChild(button);

    SWEA Copy Button

    멋진 버튼이 생겼습니다.

    클릭 이벤트 리스너를 통해 버튼 클릭 시 요청을 보내고 입력 데이터를 클립보드에 복사하도록 만들어줍니다.

    button.addEventListener('click', () => {
      fetch('https://swexpertacademy.com/main/common/contestProb/contestProbDown.do?downType=in&contestProbId=AV5QSEhaA5sDFAUq&_menuId=AVtnUz06AA3w6KZN&_menuF=true')
        .then(response => response.text())
        .then(data => navigator.clipboard.writeText(data));
    });

    붙여넣기를 해보니 잘 작동합니다.

    다만 버튼을 눌렀을 때 아무 반응이 없으니 조금 아쉬운데요.
    복사가 성공하면 알림을 띄울 수 있으면 좋겠습니다.

    우측 하단 고정된 위치에 알림이 나타날 수 있도록 CSS를 추가해 줍니다.

    const styleElement = document.createElement('style');
    styleElement.textContent = `
      .test-notification {
        position: fixed;
        bottom: 20px;
        right: 20px;
        padding: 12px 24px;
        border-radius: 4px;
        color: white;
        font-size: 14px;
        z-index: 1000;
      }
    
      .test-notification.success {
        background-color: #28a745;
      }
    
      .test-notification.error {
        background-color: #dc3545;
      }
    `;
    document.head.appendChild(styleElement);

    알림을 띄우는 함수를 만들어줍니다.
    성공/실패 여부 따라 다른 색상의 알림이 나타나고, 3초 후에 사라집니다.

    function showTestNotification(message, type) {
      const notification = document.createElement('div');
      notification.className = `test-notification ${type}`;
      notification.textContent = message;
      document.body.appendChild(notification);
    
      setTimeout(() => {
        notification.remove();
      }, 3000);
    }

    복사 작업을 수행하는 함수를 디테일하게 만들어줍니다.

    async function handleCopy(link, type) {
      try {
        if (!link) {
          throw new Error(`${type} link not found`);
        }
        const response = await fetch(link.href);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.text();
        await navigator.clipboard.writeText(data);
        showTestNotification(`${type} copied to clipboard!`, 'success');
      } catch (error) {
        console.error('Error:', error);
        showTestNotification(`Failed to copy ${type}: ${error.message}`, 'error');
      }
    }

    버튼 클릭 시 이 함수를 호출하도록 수정해 줍니다.
    fetch 링크도 querySelector로 찾도록 바꿔주었습니다.

    button.addEventListener('click', () => {
      const inputLink = inputDiv.querySelector("a[href*='downType=in']");
      handleCopy(inputLink, 'Input');
    });

    이제 알림이 잘 뜹니다 ㅎㅎ.
    SWEA Copy Button with Notification

    핵심 기능이 잘 돌아가는 걸 확인했습니다.

    바로 크롬 확장 프로그램으로 만들어보겠습니다.

    크롬 확장 프로그램 개발

    Manifest 파일 작성

    크롬 브라우저 확장 프로그램은 manifest.json 파일을 사용해서 정의합니다.
    프로그램 정보와 함께 필요한 권한과 실행 범위를 명시하는 파일입니다.

    프로그램 이름은... SWEA COPYPASTA extension으로 지었습니다.

    {
      "manifest_version": 3,
      "name": "SWEA COPYPASTA extension",
      "version": "0.1",
      "description": "Software Expert Academy(SWEA) 사이트의 예제 입력/출력을 버튼 하나로 복사할 수 있게 해주는 크롬 확장 프로그램입니다.",
      "permissions": [
        "clipboardWrite"
      ],
      "host_permissions": [
        "https://swexpertacademy.com/*"
      ],
      "content_scripts": [
        {
          "matches": [
            "https://swexpertacademy.com/main/code/problem/problemDetail.do*",
            "https://swexpertacademy.com/main/talk/solvingClub/problemView.do*",
            "https://swexpertacademy.com/main/solvingProblem/solvingProblem.do*",
            "https://swexpertacademy.com/main/learn/course/lectureProblemViewer.do*",
            "https://swexpertacademy.com/main/code/contestProblem/contestProblemDetail.do*",
            "https://swexpertacademy.com/main/code/userProblem/userProblemDetail.do*"
          ],
          "js": [
            "content.js"
          ],
          "css": [
            "styles.css"
          ],
          "run_at": "document_end"
        }
      ]
    }

    주요 내용은 다음과 같습니다:

    • permissions: 확장 프로그램이 사용할 권한을 정의합니다.
      • 클립보드 사용을 위해 clipboardWrite 권한을 추가했습니다.
    • host_permissions: 확장 프로그램이 접근할 수 있는 호스트를 정의합니다.
      • SWEA 사이트에만 동작하도록 제한합니다.
    • content_scripts: 확장 프로그램이 적용될 페이지를 정의합니다.
      • 문제 정보를 보여주는 페이지와 문제 제출 페이지를 대상으로 했습니다.
        • 일반 문제, 솔빙클럽, 강의 문제, 대회 문제 등 종류가 많아서 죄다 추가해 줍니다.
      • content.js 파일을 실행하고, styles.css 파일을 적용합니다.
      • 실행 시점은 문서가 완전히 로드된 후에 실행하도록 했습니다.

    JavsScript 및 CSS 파일 작성

    이전에 실험했던 코드를 바탕으로 content.js 파일을 작성해 줍니다.

    showTestNotificationhandleCopy 함수입니다.

    function showTestNotification(message, type) {
      const notification = document.createElement('div');
      notification.className = `test-notification ${type}`;
      notification.textContent = message;
      document.body.appendChild(notification);
    
      setTimeout(() => {
        notification.remove();
      }, 3000);
    }
    
    // Create copy handler
    async function handleCopy(link, type) {
      try {
        // Prevent SWEA from redirecting
        event.preventDefault();
        event.stopPropagation();
    
        if (!link) {
          throw new Error(`${type} link not found`);
        }
        const response = await fetch(link.href);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.text();
        await navigator.clipboard.writeText(data);
        showTestNotification(`${type} copied to clipboard!`, 'success');
      } catch (error) {
        console.error('Error:', error);
        showTestNotification(`Failed to copy ${type}: ${error.message}`, 'error');
      }
    }

    이전에 작성했던 코드와 거의 동일합니다.

    한가지 차이점으로 event.preventDefault();, event.stopPropagation();을 추가했습니다.
    SWEA 문제 제출 사이트에서 버튼 클릭 시 form을 제출하는 문제가 있어서 이를 막기 위함입니다.

    [!NOTE]

    이후 확인해보니 button 타입을 명시적으로 button으로 설정하는 것으로 form 제출을 막을 수 있었습니다.
    기본 타입이 submit으로 설정되어있어서 form이 제출되는 문제가 발생했습니다.

    버튼도 만들어줍니다.
    .box_type1 .left.box_type1 .right에 버튼을 추가하도록 했습니다.

    버튼이 들어갈 위치는 입력/출력 글자 옆(.title1)으로 두었습니다.

    function createButton(text, className) {
      const button = document.createElement('button');
      button.textContent = text;
      button.className = `test-button ${className}`;
      return button;
    }
    
    // Main function to add buttons
    function addCopyButtons() {
      // SWEA uses box_type1 class for input and output
      // left class for input and right class for output
      const inputDiv = document.querySelector('.box_type1 .left');
      const outputDiv = document.querySelector('.box_type1 .right');
    
      if (!inputDiv || !outputDiv) {
        console.log('Input and output not found');
        return;
      }
    
      // Title of input and output: would be used to append buttons
      const inputDivTitle = inputDiv.querySelector('.title1');
      const outputDivTitle = outputDiv.querySelector('.title1');
    
      if (!inputDivTitle || !outputDivTitle) {
        console.log('Input and output title not found');
        return;
      }
    
      const inputButton = createButton('Copy Input', 'input');
      inputButton.addEventListener('click', (event) => {
        const inputLink = inputDiv.querySelector("a[href*='downType=in']");
        handleCopy(inputLink, 'Input');
      });
      inputDivTitle.appendChild(inputButton);
    
      const outputButton = createButton('Copy Output', 'output');
      outputButton.addEventListener('click', (event) => {
        const outputLink = outputDiv.querySelector("a[href*='downType=out']");
        handleCopy(outputLink, 'Output');
      });
      outputDivTitle.appendChild(outputButton);
    }

    그리고 마지막으로 화면이 로드된 후 버튼을 추가하도록 해줍니다.
    addCopyButtons 함수를 실행하기만 하면 됩니다.

    addCopyButtons();

    CSS는 이렇게 작성해 주었습니다.
    클로드에게 부탁했는데 나쁘지 않아서 수정 없이 그대로 사용했습니다.

    .test-button {
      padding: 4px 8px;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 12px;
      margin-left: 8px;
      vertical-align: middle;
    }
    
    .test-button.input {
      background-color: #28a745;
    }
    
    .test-button.output {
      background-color: #17a2b8;
    }
    
    .test-button:hover {
      opacity: 0.9;
    }
    
    .test-notification {
      position: fixed;
      bottom: 20px;
      right: 20px;
      padding: 12px 24px;
      border-radius: 4px;
      color: white;
      font-size: 14px;
      z-index: 1000;
    }
    
    .test-notification.success {
      background-color: #28a745;
    }
    
    .test-notification.error {
      background-color: #dc3545;
    }

    최종적으로 이런 모양이 되었습니다.
    SWEA COPYPASTA Extension

    시각적으로 더 발전할 구석이 많아 보이지만, 첫 버전으로 이정도면 충분해 보입니다.

    크롬 웹스토어 등록

    지금까지 만든 확장 프로그램을 크롬 웹스토어에 등록해 보겠습니다.

    지금은 확장 프로그램을 사용하려면 파일을 다운받아 직접 브라우저에 추가 해줘야 하는데요.
    웹스토어에 등록하면 클릭 한 번으로 설치가 가능하니 배포가 훨씬 편해집니다.

    웹스토어 최초 등록 시에는 개발자 계정 등록을 위해 등록 비용 5달러가 필요합니다.
    저는 환율이 오르기 전 이미 등록비용을 지불해서 다행입니다.

    확장 프로그램 등록에는 아이콘과 스크린샷, 개인정보 처리 방침 문서 등이 필요합니다.

    프로그램 아이콘 만들기

    프로그램 아이콘은 128x128 크기의 PNG 파일이 필요합니다.

    그래픽 작업이 필요할 텐데요... 저는 후딱 만들기 위해 MS Powerpoint를 사용했습니다.

    파워포인트로 만들면 픽셀 단위 크기 조정이 불가능합니다.
    적당한 크기로 만들고, IloveIMG 사이트에서 대충 크기를 맞춰주었습니다.

    이름에 파스타가 들어가니 포크를 넣었습니다.
    Icon

    개인정보 처리 방침 문서 작성

    웹스토어 등록을 위해서는 개인정보 처리 방침 문서 링크가 필요합니다.
    수집하는 개인정보와 그 정보를 어떻게 사용하는지에 대한 내용을 담고 있어야 합니다.

    지금 프로그램은 개인정보를 수집하지 않고, 사용자의 클립보드에 복사한 데이터만 사용합니다.
    그래서 간단하게 작성할 수 있지만, 문제는 링크를 만들어야 한다는 점입니다.

    즉, 문서를 작성하고 어딘가에서 호스팅해야 합니다.

    Privacy Policy Online같은 사이트를 사용하면 쉽게 만들 수도 있던데요.

    저는 더 간단하게 Notion을 이용해서 개인정보처리방침 문서를 만들었습니다.
    개인정보처리방침 문서

    최종 검수 및 등록

    등록하면 구글에서 검수 과정이 시작됩니다.
    여러 가지 규정을 지키지 않으면 등록이 거부될 수 있고, 다시 수정해서 재등록해야 합니다.

    마치며

    요렇게 SWEA 사이트에서 예제 입력과 출력을 복사할 수 있는 크롬 확장 프로그램을 만들어보았습니다.

    코드도 글도 다소 급하게 작성해서 아쉬움이 조금 남네요.
    최근 SSAFY에서 트랙이동(반 이동)을 한 데다, 다음 주부터 바로 새로운 언어로 시험도 봐야 하는 상황이라 시간이 많이 없었습니다.

    여러 예외 처리나, CSS 및 아이콘 디자인 등 더 발전시킬 부분이 많이 보이지만 일단은 이 정도로 만족하고 나중에 수정해 보려고 합니다.

    도움이 되었으면 좋겠습니다.
    감사합니다.

    댓글