OpenClick(오픈클릭) - 공무원 연수 자동넘김 프로그램

힘센캥거루·2025-06-04

최근에 지인에게서 연수를 자동으로 넘겨주는 닥터클릭이라는 프로그램에 대해 듣게 되었다.

일단 터미널이 열리는 것부터 뭔가 파이썬 냄새가 났다.

그래서 나도 한번 만들어보기로 했다.

이름은 OPEN AI의 이름을 딴 오픈 클릭으로 작명해 보았다.

alt text

1. 목적

나 또한 이 프로그램을 만드는 이유는 닥터클릭 제작자와 다르지 않다.

손목이 아파 마우스나 키보드를 사용하기 힘든 이들을 위해 제작한다.

연수를 자동으로 넘겨주는 동안, 사용자는 앞에서 자리를 지키며 시청만 하면 된다.

물론 사용은 자유지만, 사용에 대한 책임은 본인에게 있음을 인지하길 바란다.

2. 구현 방안

selenium meme

파이썬 라이브러리 중 Selenium이라는 라이브러리를 이용할 것이다.

브라우저를 자동으로 조작할 수 있게 만들어주는 툴이다.

셀레니움으로 접속 -> 사용자 로그인 -> 연수 실행 -> 자동으로 넘겨주기 순으로 진행하면 된다.

xpath나 css 선택자로 요소들을 찾기만 하면 구현은 쉽다.

3. 파일 다운로드

현재 버전은 1.0.0, 암호는 earthscience.kr 이다.

오픈클릭 다운로드

4. 사용법

사용법은 무척 간단하다.

오픈클릭 사용법1

먼저 압축을 풀고 실행을 하면 검은 창이 뜬다.

이 창을 터미널이라고 하는데 해킹이 아니니까 너무 걱정 말자.

브라우저서 뜨는 팝업들은 모두 꺼준 뒤에 로그인을 한다.

오픈클릭 사용법2

로그인 한 뒤 검은 창에서 다시 엔터를 치거나 아무거나 입력하면 수강중인 연수가 나온다.

연수에 맞게 번호를 입력하고 엔터를 치면...

오픈클릭 사용법3

수강하는 과목에서 제일 마지막에 들은 수업으로 이동하고 영상이 계속 넘어가게 된다.

오픈클릭 사용법4

5. 코드 전문

exe파일 실행이 두려운 분들은 파이썬에 아래의 코드를 복사, 붙여넣기 해서 이용해도 된다.

코드 전문을 대충 쓴 주석과 함께 올린다.

from selenium import webdriver
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import UnexpectedAlertPresentException, NoSuchWindowException, NoSuchElementException, ElementClickInterceptedException
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
import time
import traceback
from requests import post
import json
import os
 
# 셀레니움 driver 정의
try : 
    driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))
except :
    driver = webdriver.Chrome()
 
# 요소 찾고 클릭하기. 루프를 돌며 요소가 클릭 가능할 때 까지 장시간 대기함.
# 웹페이지 로딩으로 인한 오류 방지를 위해 씀
def find_and_click(element, num=0):
    for _ in range(10):
        try:
            if isinstance(element, str):
                somethings = driver.find_elements(By.XPATH, element)
                if somethings:
                    something = somethings[num]
                    something = WebDriverWait(driver, 10).until(EC.element_to_be_clickable(something))
                    something.click()
                    return True
            else:
                WebDriverWait(driver, 10).until(EC.element_to_be_clickable(element))
                element.click()
                return True
        except:
            time.sleep(0.5)
    if isinstance(element, str):
        print(f"클릭할 수 없음 : {element}")
    else: print(element.get_attribute('innerText'))
    return False
 
# 연수 사이트 접속하기
def getWebAndSelLec():
    driver.get("https://www.neti.go.kr/")
    driver.maximize_window()
 
    windows = driver.window_handles
    if len(windows) > 1:
        driver.switch_to.window(windows[1])
        find_and_click('//span[contains(text(),"5일")] / ancestor::button[1]')
        driver.switch_to.window(windows[0])            
        
    input('로그인 후 아무키나 입력해주세요. 팝업창은 닫아주세요! : ')
        
    userNmae = driver.find_element(By.XPATH, '//div[@class="my_profile"]/span/em').text
    cardTops = driver.find_elements(By.XPATH, '//ol[@id="mainLoling_99"]/li')
 
    print(f'{userNmae}님 반갑습니다.')
    if len(cardTops) == 0:
        print('현재 들을 수 없는 연수가 없습니다. 연수를 신청 후 .')
    print(f'지금 듣고 계신 수업은 총 {len(cardTops)}개 있습니다.')
    cardDct = {}
 
    for i, cardTop in enumerate(cardTops):
        title = cardTop.find_element(By.XPATH, './/a[@class="title"]').get_attribute('innerText')
        cardDct[i] = title
        print(i, ':' ,title)
 
    while True:
        try: 
            selCardTop = int(input('어떤 연수를 들으시겠어요? : '))
            if 0 <= selCardTop < len(cardTops):
                break
            print('연수 번호가 올바르지 않습니다.')
        except:
            print('숫자를 입력해주세요.')
 
    userNmae = driver.find_element(By.XPATH, '//div[@class="my_profile"]/span/em').text
    cardTops = driver.find_elements(By.XPATH, '//ol[@id="mainLoling_99"]/li')
    selCardHomeBtn = cardTops[selCardTop].find_element(By.XPATH, './/a[contains(text(), "강의실 홈")]')
    print(selCardTop,"번 ",cardDct[selCardTop][0:27], "...", '을 수강합니다.')
 
    find_and_click(selCardHomeBtn)
 
# 제일 마지막 수강 내역을 찾아 영상 재생하기
def getTopLec():
    print('가장 마지막에 수강한 부분부터 진행합니다.')
    while True:
        time.sleep(1)
        windows = driver.window_handles
        if len(windows) > 1:
            driver.switch_to.window(windows[1])
            break
 
    while True:
        tabList = driver.find_elements(By.XPATH, '//ul[@role="tablist"]')
        if tabList:
            break
        time.sleep(0.5)
        
    tabList = driver.find_element(By.XPATH, '//ul[@role="tablist"]')
    lecCategoryBtn = tabList.find_element(By.XPATH, './/a[contains(text(), "학습목록")]')
    find_and_click(lecCategoryBtn)
 
    learnList = driver.find_element(By.XPATH, '//ul[contains(@class,"content_list")]/li')
    if not learnList.is_displayed():
        driver.find_element(By.XPATH, '//li/a[@class="end list_title listTitle01"]').click()
 
    learnBtns = driver.find_elements(By.XPATH, "//div[@class='learn_con']/div[text()='학습중' or text()='학습전' or text()='학습하기'] // following-sibling::a[@class='btn_learning_list']")
    learnTitleBtns = driver.find_elements(By.XPATH, '//a[@class="cnts_title"][span and not(span[contains(., "학습완료")])]')
    driver.execute_script("arguments[0].scrollIntoView(true);", learnTitleBtns[0])
    time.sleep(0.5)
    if not learnBtns[0].is_displayed():
        find_and_click(learnTitleBtns[0])
    driver.execute_script("arguments[0].scrollIntoView(true);", learnBtns[0])
    time.sleep(0.5)
    find_and_click(learnBtns[0])
    time.sleep(1)
 
    find_and_click('//button[@title="동영상 재생하기"]')
    print('동영상 재생 버튼을 클릭합니다.')
 
# 연수가 종료될 때까지 루프를 돌며 영상재생하기
def lectureLoop():
    while True:
        preProhibitDiv = driver.execute_script("return document.getElementById('prohibitDiv').style.width")
        
        stopCount = 0
        isNextLearningClass = True
        while True:
            time.sleep(2)
            playerBox = driver.find_element(By.XPATH, "//div[contains(@class,'player_box')]")
            quizShowBtn = driver.find_elements(By.XPATH, '//p[contains(text(),"완료") and contains(text(),"퀴즈")] /ancestor::div[1]')
            if len(quizShowBtn) > 0:
                if "block" in quizShowBtn[0].get_attribute('style'):
                    find_and_click(quizShowBtn[0])
            
            playState = driver.find_element(By.CSS_SELECTOR, '.class_list .class_list_box li.play').text.split('\n')[1]
            nextProhibitDiv = driver.execute_script("return document.getElementById('prohibitDiv').style.width")
            if nextProhibitDiv == preProhibitDiv and playState == "재생중":
                stopCount += 1
                if stopCount >5:
                    driver.find_elements(By.XPATH, "//li[@id='studyPageLi']//div[contains(@class,'fll flr') or contains(@class, 'fll pre')]//a")[0].click()
                    continue
                
            if '완료' in playState or "flex" in playerBox.get_attribute("style"):
                break
                
            if '완료' in playState and isNextLearningClass:
                nextLearningClass = driver.find_elements(By.XPATH, "//li[@id='studyPageLi']//div[contains(@class,'ing') or contains(@class,'pre')]/b/a")
                if len(nextLearningClass) == 0:
                    try :
                        driver.find_element(By.XPATH, '//a[contains(text(), "다음 차시")]').click()
                    except : 
                        isNextLearningClass = False
                        print('다음 차시가 없습니다.')
                    continue
                driver.execute_script("arguments[0].scrollIntoView(true);", nextLearningClass[0])
                nextLearningClass[0].click()
                continue
            preProhibitDiv = nextProhibitDiv
 
        find_and_click('//div[@id="next-btn"]/button[@title="다음 페이지"]')
        time.sleep(0.5)
        popupText = driver.find_elements(By.XPATH, '//p[contains(text(), "마지막 목차")]')
        if len(popupText) > 0:
            print('마지막 목차입니다.')
            break
    print('모든 연수를 수강했습니다. openClick을 종료합니다.') 
    driver.quit()
    
if __name__ == '__main__':
    getWebAndSelLec()
    getTopLec()
    lectureLoop()

6. 후기

예전에 인스타에서 좋아요를 눌러주는 코드를 짰을 때, 꽤 많은 사람들이 내 블로그를 방문해 주었다.

몇몇 개발자들이 이걸 돈받고 파는걸 보며 이게 그 정도인가 싶었는데, 유로로 잘 운영되고 있는걸 보니 그 정도가 맞았나보다.

앞으로도 계속 유지 보수를 할 계획이지만 자녀가 어려서 빈번하게는 어려울 수도 있다.

어쨌든 유용하게 쓰길 바란다.

오픈소스