배경(동기)
홍길동이 있었다. 홍길동은 최근 외부에서 코칭 교육을 마치고 바로, 대구에 내려갈 일이 있었다.
교육이 아침 7시부터 시작해서 몇 시에 마칠지 몰라서, 그리고, 당연히 평일 오후 시간라면 기차표가 있을 것이라 생각하고 예매를 하지 않았었다. 그런데, 교육을 마친 4시경, 기차표를 예약하려고 하니, 모두 매진인 상황이었다. 기차역으로 몸을 향하면서 휴대폰으로 취소표를 구하려고 반복 또 반복을 하였으나, 표 구하는 것은 어려웠다. 반환표가 보여서 예약하러 들어가면 이미 누군가가 예약을 해버렸다고 나온다.
다행히 오랜시간 동안 반복을 해서 한참 만에 기차표를 구해서 대구에 내려갔었다.
지난 주말에, 불현듯 당시 상황이 기억이 나서, 또 동일한 상황이 생기면 그 때를 준비를 해야 겠다는 생각이 들었고, 기차표 예약하는 매크로 프로그램을 만들어 봐야겠다는 생각을 했다. 예전, 25년이나 지났나…… 2000년초, 휴대폰용과 인터넷에서 사용할 수 있는 기차표 예약 서비스를 만들어서, 일반에게 공개를 한 적이 있었다.
내가 필요한 기차표에 대한 정보를 등록해두면, 프로그램이 자동으로 반환표를 반복 체크하고 예약을 진행해주게 만들었었다. 물론, 예약이 완료되면 휴대폰 문자 메시지 완료 되었다고 보내주는 친절함도 추가하였다.
이 사이트를 사용하는 사람들에게서는 게시판에 고맙다는 칭송(?)을 받아왔었던 데, 이 서비스는 몇 년 후에 중단하게 되었다. 주말 뉴스를 보고 있었는데, 누군가가 골프장 부킹 프로그램을 만들어서, 부킹 하나 해주고 돈을 받았었고, 이 사람이 경찰에 잡혔다는 뉴스였다. 그 죄는 업무방해죄로, 골프장에서의 예약을 방해했다는 죄로 입건되었다고 했다. 그 날 부터 며칠동안 홍길동에는 지인을 통해서 그 뉴스 봤냐는 연락을 여러 건 받게 되었다고 한다. 다들 그 뉴스를 보면서, 홍길동이 한 서비스가 생각났었다고 했다. 그 시점에 바로, 해당 서비스를 스스로 폐지 했다.
무엇을 어떻게 할것인가?
매크로 프로그램은 긍정적으로 표현하면, 업무 자동화 라는 영역으로 표현할 수 있다. 또한, 서비스 측면에서는 시스템에 부하를 일으킬 수 있기 때문에 외부에서의 매크로는 해킹 프로그램 영역으로 인식되는 경우도 많다. 아래에 언급하는 것은 웹의 확장프로그램을 이용하여, 어떻게 기존 페이지를 수정하게 할 수 있는지와 어떤 방식으로 매크로가 작동되는지를 알아보려 한다. 가끔 기술 블로그를 적고 있는데, 요즘은 잘 적지를 못해서.. 그리고, 아래에 기술적이 내용을 적지 못하고, LLM 만을 이용한, 어떻게 보면 LLM이라는 툴 사용법을 알려주는 내용이 될 것 같다.
이 사례를 통해서, 업무 자동화 측면에서 접근할 수도 있고, 매크로가 처리하는 방식을 이해해서 이를 차단하는 방법에 대한 고민도 할 수 있으리라 생각한다.
여기서 만들 매크로는 크롬 브라우저의 확장 프로그램을 이용한 방식으로, 크롬의 웹스토어에 등록해서 확산도 가능하다. 웹확장 프로그램은 요즘은 크롬 뿐만 아니라, Whale 브라우저, firefox에서도 제공되는 부분이다. 물론, 각각 구조적으로는 비슷하지만, 만드는 방법은 조금씩 다르다.
요즘 세상에서 프로그램을 “쌩(날것)”으로, 바닥부터 직접 코딩한다는 것은 말이 안되니, LLM을 이용해 보았다. 시작할 때는 과연 이것이 될까? 하는 생각을 갖고 시작을 했는데, 코드 최적화 부분은 모르겠고…. 최종 결과물은 딱 1시간 만에 원하는 프로그램이 만들어졌다. LLM이 DOM을 이해를 하고 구문 분석을 할 줄 알고 있었고, 크롬의 웹 확장 프로그램을 어떻게 만들어야 하는지도 너무 잘 알고 있었기에, 내가 크게 할 것은 없었다. 그냥 결과 나온 것에서 이상한 부분을 고치기 위해 문제점을 얘기를 하는 것과 좀 더 빨리 디버깅하기 위해, 결과로 나온 html의 소스에서 문제가 되는 부분을 발췌해서 LLM에게 전달해주는 역할만 반복 수행하였다.
최종 내가 원하는 프로그램의 결과 모습을 먼저 그려보았다. 어떤 모양으로 만들까? 아래 처럼 만들려 한다. 기존 예매 사이트의 UI에 매크로를 설정하는 메뉴를 추가하고, 열차목록에서 내가 예약할 기차를 여러 개를 선택하게 한다. 그런 후, 반복적으로(자동) 목록을 조회하고 반환 기차표가 있을 경우 자동으로 예약까지 되도록 한다.
예약 가능한 반환표가 나왔을 경우, 사람이 클릭할 때 보다, 매크로가 대신 하면 조금 더 빠를 것이라 생각해서… ^^

이제 만들어 보자.
그러면, LLM을 이용하여 어떻게 만들수 있느냐… 기존에는 chatgpt를 사용한 사례를 자주 얘기해왔는데, 이번에는 gemini LLM(http://gemini.google.com)을 사용한 방법을 아래에 적어보았다. 프리미엄 버전이 아닌, 프리버전을 사용하였다. (물론, 최근에 사용하고 있는 gemini-cli 는 최고 수준이다. 특히, 검정색 화면에서 vi를 사용하는 나에게는 더더욱이 친숙한 화면도 한 몫한 듯 싶다)
개발하기 위한 스펙을 작성해본다. 이것을 프롬프트로 사용하였다. 전체를 정리해서 진행한 것이 아니어서, 최초 작성한 프롬프트에도 부족함과 누락이 있다. 그렇기 때문에, 뒤에서 디버깅에 조금더 시간이 소요되었을 수도 있었다.
개발요구사항 Prompt
크롬 웹 브라우저에 확장 프로그램을 새로 만들어서, 아래의 기능이 작동되게 하려고 한다. 이 기능이 작동되는 확장 프로그램 소스를 만들어줘.
1. 이 확장 프로그램은 https://www.korail.com 사이트에서만 작동되도록 한다.
2. "https://www.korail.com/ticket/search/list" , 이 주소는 열차 승차권 예매할 수 있는 열차 리스트가 보여지는 페이지다.
3. 조회된 열차 리스트에는 열차시간에 따라, 예약이 불가능한 것은 매진으로 표시되어있고, 예약이 가능한 것은 일반실 및 특실을 표시하고 금액이 표시되어 있다.
4. 이 페이지에서 조회된 열차 리스트가 1개 이상이라면, 리스트가 표시되는 윗쪽에 "매크로 실행" 아이콘과 실행시간(초)를 입력받을 INPUT 필드를 추가한다. INPUT 필드의 초기 기본값은 3으로 설정하고, 수정할 수 있다.
5. 조회된 열차 리스트의 각 라인마다 있는 예매가 가능한 일반실, 특실 뿐만 아니라, 예약대기 및 매진이 된 경우에도 선택버튼을 추가하여, 예약을 하고 싶은 열차와 차실을 선택할 수 있게 한다. 이 선택버튼은 각각 선택할 수 있도록 해야 한다.
6. "매크로 실행" 아이콘은 마우스로 선택을 할 수 있도록 하며, 선택이 되면 "실행 취소"로 이름이 변경된다.
7. "실행 취소"로 변경된 버튼을 선택하면, 아래의 반복된 작업을 중단한다.
8. "매크로 실행" 아이콘을 마우스로 클릭했을 때, 아래의 조회한 열차 리스트 중 선택된 것이 0개 라면, "예약할 열차를 선택하세요"라는 메시지박스를 보여주도록 한다.
9. "매크로 실행" 아이콘을 마우스로 클릭했을 때, 출력된 목록에서 예약이 가능한 열차가 없는 경우, "실행시간(초)"의 INPUT필드에 등록된 숫자의 초만큼 대기했다가 화면을 리프레시한다. 입력된 숫자가 없는 경우, 대기시간이 없이 즉시 화면을 리프레시한다.
10. "매크로 실행"을 선택하여 진행되어 화면 리프레시 되었을 때, 기존에 예약할 대상을 선택한 항목은 동일하게 선택되어 있어야 한다.
11. 화면에 표시된 목록에서 예약이 가능한 열차가 있는 경우, 맨 위에서 부터의 우선순위로 예약을 진행한다. 예약을 하는 방법은 "요금"이 표시된 항목을 마우스로 클릭하는 것으로 정의한다.
12. 예약할 열차를 조회한 HTML문서 중에서 열차 목록이 표시되는 부분은 아래와 같다.
<div class="tckWrap"><ul><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_ktx_ticket "><span class="blind">KTX</span></span><span class="num "> 103</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(13:08 ~ 13:48)</span></h3><p class="s_txt">소요시간: 40분</p></div></div></div><div class="price_box fl-l gen "><div class="inner type02"><a href="#none" title=""><p class="txt_ch">일반실</p><p class="txt_price txt_bk">14,100원</p><p class="txt_gr">5%적립</p></a></div></div><div class="price_box fl-l spe sold_out_soon"><div class="inner type02"><a href="#none" title=""><p class="txt_ch">특실(매진임박)</p><p class="txt_price txt_bk">19,700원</p><p class="txt_gr">5%적립</p></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_ktx_ticket "><span class="blind">KTX</span></span><span class="num "> 033</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(13:18 ~ 13:58)</span></h3><p class="s_txt">소요시간: 40분</p></div></div></div><div class="price_box fl-l gen "><div class="inner type02"><a href="#none" title=""><p class="txt_ch">일반실</p><p class="txt_price txt_bk">14,100원</p><p class="txt_gr">5%적립</p></a></div></div><div class="price_box fl-l spe sold_out_soon"><div class="inner type02"><a href="#none" title=""><p class="txt_ch">특실(매진임박)</p><p class="txt_price txt_bk">19,700원</p><p class="txt_gr">5%적립</p></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_ktx_ticket "><span class="blind">KTX</span></span><span class="num "> 213</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(13:38 ~ 14:13)</span></h3><p class="s_txt">소요시간: 35분</p></div></div></div><div class="price_box fl-l gen "><div class="inner type02"><a href="#none" title=""><p class="txt_ch">일반실</p><p class="txt_price txt_bk">14,100원</p><p class="txt_gr">5%적립</p></a></div></div><div class="price_box fl-l spe sold_out_soon"><div class="inner type02"><a href="#none" title=""><p class="txt_ch">특실(매진임박)</p><p class="txt_price txt_bk">19,700원</p><p class="txt_gr">5%적립</p></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_ktx_ticket "><span class="blind">KTX</span></span><span class="num "> 035</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(13:58 ~ 14:34)</span></h3><p class="s_txt">소요시간: 36분</p></div></div></div><div class="price_box fl-l gen "><div class="inner type02"><a href="#none" title=""><p class="txt_ch">일반실</p><p class="txt_price txt_bk">14,100원</p><p class="txt_gr">5%적립</p></a></div></div><div class="price_box fl-l spe sold_out_soon"><div class="inner type02"><a href="#none" title=""><p class="txt_ch">특실(매진임박)</p><p class="txt_price txt_bk">19,700원</p><p class="txt_gr">5%적립</p></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_sancheon_ticket "><span class="blind">KTX-산천</span></span><span class="num "> 039</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(14:28 ~ 15:08)</span></h3><p class="s_txt">소요시간: 40분</p></div></div></div><div class="price_box fl-l sold_out "><div class="inner type02"><a href="#none" title=""><div class="tck_etc_text"><div class="tck_etc_use">매진</div></div></a></div></div><div class="price_box fl-l sold_out "><div class="inner type02"><a href="#none" title=""><div class="tck_etc_text"><div class="tck_etc_use">매진</div></div></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_sancheon_ticket "><span class="blind">KTX-산천</span></span><span class="num "> 287</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(14:28 ~ 15:08)</span></h3><p class="s_txt">소요시간: 40분</p></div></div></div><div class="price_box fl-l sold_out "><div class="inner type02"><a href="#none" title=""><div class="tck_etc_text"><div class="tck_etc_use">매진</div></div></a></div></div><div class="price_box fl-l spe sold_out_soon"><div class="inner type02"><a href="#none" title=""><p class="txt_ch">특실(매진임박)</p><p class="txt_price txt_bk">19,700원</p><p class="txt_gr">5%적립</p></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_ktx_ticket "><span class="blind">KTX</span></span><span class="num "> 245</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(14:33 ~ 15:13)</span></h3><p class="s_txt">소요시간: 40분</p></div></div></div><div class="price_box fl-l gen "><div class="inner type02"><a href="#none" title=""><p class="txt_ch">일반실</p><p class="txt_price txt_bk">14,100원</p><p class="txt_gr">5%적립</p></a></div></div><div class="price_box fl-l spe sold_out_soon"><div class="inner type02"><a href="#none" title=""><p class="txt_ch">특실(매진임박)</p><p class="txt_price txt_bk">19,700원</p><p class="txt_gr">5%적립</p></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_ktx_ticket "><span class="blind">KTX</span></span><span class="num "> 087</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(14:42 ~ 15:22)</span></h3><p class="s_txt">소요시간: 40분</p></div></div></div><div class="price_box fl-l gen "><div class="inner type02"><a href="#none" title=""><p class="txt_ch">일반실</p><p class="txt_price txt_bk">14,100원</p><p class="txt_gr">5%적립</p></a></div></div><div class="price_box fl-l spe sold_out_soon"><div class="inner type02"><a href="#none" title=""><p class="txt_ch">특실(매진임박)</p><p class="txt_price txt_bk">19,700원</p><p class="txt_gr">5%적립</p></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_ktx_ticket "><span class="blind">KTX</span></span><span class="num "> 041</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(14:58 ~ 15:38)</span></h3><p class="s_txt">소요시간: 40분</p></div></div></div><div class="price_box fl-l gen "><div class="inner type02"><a href="#none" title=""><p class="txt_ch">일반실</p><p class="txt_price txt_bk">14,100원</p><p class="txt_gr">5%적립</p></a></div></div><div class="price_box fl-l spe sold_out_soon"><div class="inner type02"><a href="#none" title=""><p class="txt_ch">특실(매진임박)</p><p class="txt_price txt_bk">19,700원</p><p class="txt_gr">5%적립</p></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_sancheon_ticket "><span class="blind">KTX-산천</span></span><span class="num "> 089</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(15:23 ~ 15:58)</span></h3><p class="s_txt">소요시간: 35분</p></div></div></div><div class="price_box fl-l gen "><div class="inner type02"><a href="#none" title=""><p class="txt_ch">일반실</p><p class="txt_price txt_bk">14,100원</p><p class="txt_gr">5%적립</p></a></div></div><div class="price_box fl-l sold_out "><div class="inner type02"><a href="#none" title=""><div class="tck_etc_text"><div class="tck_etc_use">매진</div></div></a></div></div></div></li></ul></div>
위의 HTML문서는 열차 목록을 조회한 화면의 HTML의 일부를 발췌한 것이다. 발췌방법은 크롬브라우저에서 개발자모드로 들어가서 DOM 객체 단위로 조회할 수 있는 기능이 있는데, 이를 이용해서 해당 목록이 있는 부분만 발췌하였다. 이 부분을 추가한 이유는 조금더 gemini가 이해를 해서 여기에 맞는 코드를 만들어 줄 것이라 예상하였다.
gemini 답변
네, 요청하신 기능들을 담은 코레일 승차권 예매 매크로 크롬 확장 프로그램을 만들어 드리겠습니다.
확장 프로그램은 manifest.json과 content.js 두 개의 파일로 구성됩니다. 아래 각 파일의 내용을 확인하고 확장 프로그램으로 추가하여 사용할 수 있습니다.
1. manifest.json
이 파일은 확장 프로그램의 기본 정보를 정의합니다. (첨부: 생략)
2. content.js
이 파일은 확장 프로그램의 주요 로직을 담당하며, 웹 페이지에 직접 삽입되어 작동합니다. (첨부:생략)
3. style.css
선택된 버튼의 스타일을 더 명확하게 하기 위한 CSS 파일입니다. (첨부: 생략)
확장 프로그램 설치 및 사용법
1. 위의 manifest.json, content.js, style.css 세 개의 파일을 같은 폴더에 저장합니다.
2. 크롬 브라우저를 열고 주소창에 chrome://extensions를 입력하여 확장 프로그램 관리 페이지로 이동합니다.
3. 오른쪽 상단의 '개발자 모드'를 활성화합니다.
4. '압축 해제된 확장 프로그램을 로드합니다' 버튼을 클릭합니다.
5. 파일을 저장한 폴더를 선택하고 '폴더 선택'을 클릭합니다.
6. 이제 '코레일 예매 매크로' 확장 프로그램이 추가된 것을 확인할 수 있습니다.
7. www.korail.com 사이트로 이동하여 열차를 조회하면, 열차 리스트 상단에 매크로 실행 UI가 나타나고 각 예매 옵션마다 '선택' 버튼이 추가된 것을 볼 수 있습니다.
8. 이제 원하는 열차의 '선택' 버튼을 누르고 '매크로 실행' 버튼을 클릭하면, 설정된 시간 간격으로 빈자리를 확인하고 예약 가능한 좌석이 나타나면 자동으로 예약을 시도합니다.
위의 설명대로 파일을 폴더에 저장하고, 크롬브라우저의 웹확장프로그램을 등록한 후, 철도 예약 화면에 접속해서 원하는 대로 되어있는지 확인을 해보았다.
그런데, 원하는대로 되어있지 않아서(화면 캡처를 못했다), 문제점을 아래와 같이 작성하여 다시 질문을 하였다. checkbox가 누락된 것은 해당 HTML 소스 부분을 발췌해서 같이 보내줬다. 좀 더 빨리 오류를 찾을 수 있도록 하기 위해서다.
추가 질의(Prompt)
1. 여러가지 문제가 있는데, 확장프로그램이 적용 안된 것 처럼 보인다.
2. 열차 조회한 목록 맨 위에, "매크로 실행" 버튼과 시간값 등록 필드가 표시되지 않는다.
3. 조회한 열차 리스트에서 아래와 같이 차실 표시 앞에 선택할 수 있는 checkbox가 누락되어 있다.
<div class="tckWrap"><ul><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_ktx_ticket "><span class="blind">KTX</span></span><span class="num "> 103</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(13:08 ~ 13:48)</span></h3><p class="s_txt">소요시간: 40분</p></div></div></div><div class="price_box fl-l gen "><div class="inner type02"><a href="#none" title=""><p class="txt_ch">일반실</p><p class="txt_price txt_bk">14,100원</p><p class="txt_gr">5%적립</p></a></div></div><div class="price_box fl-l spe sold_out_soon"><div class="inner type02"><a href="#none" title=""><p class="txt_ch">특실(매진임박)</p><p class="txt_price txt_bk">19,700원</p><p class="txt_gr">5%적립</p></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_ktx_ticket "><span class="blind">KTX</span></span><span class="num "> 033</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(13:18 ~ 13:58)</span></h3><p class="s_txt">소요시간: 40분</p></div></div></div><div class="price_box fl-l gen "><div class="inner type02"><a href="#none" title=""><p class="txt_ch">일반실</p><p class="txt_price txt_bk">14,100원</p><p class="txt_gr">5%적립</p></a></div></div><div class="price_box fl-l spe sold_out_soon"><div class="inner type02"><a href="#none" title=""><p class="txt_ch">특실(매진임박)</p><p class="txt_price txt_bk">19,700원</p><p class="txt_gr">5%적립</p></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_ktx_ticket "><span class="blind">KTX</span></span><span class="num "> 213</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(13:38 ~ 14:13)</span></h3><p class="s_txt">소요시간: 35분</p></div></div></div><div class="price_box fl-l gen "><div class="inner type02"><a href="#none" title=""><p class="txt_ch">일반실</p><p class="txt_price txt_bk">14,100원</p><p class="txt_gr">5%적립</p></a></div></div><div class="price_box fl-l spe sold_out_soon"><div class="inner type02"><a href="#none" title=""><p class="txt_ch">특실(매진임박)</p><p class="txt_price txt_bk">19,700원</p><p class="txt_gr">5%적립</p></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_ktx_ticket "><span class="blind">KTX</span></span><span class="num "> 035</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(13:58 ~ 14:34)</span></h3><p class="s_txt">소요시간: 36분</p></div></div></div><div class="price_box fl-l gen "><div class="inner type02"><a href="#none" title=""><p class="txt_ch">일반실</p><p class="txt_price txt_bk">14,100원</p><p class="txt_gr">5%적립</p></a></div></div><div class="price_box fl-l spe sold_out_soon"><div class="inner type02"><a href="#none" title=""><p class="txt_ch">특실(매진임박)</p><p class="txt_price txt_bk">19,700원</p><p class="txt_gr">5%적립</p></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_sancheon_ticket "><span class="blind">KTX-산천</span></span><span class="num "> 039</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(14:28 ~ 15:08)</span></h3><p class="s_txt">소요시간: 40분</p></div></div></div><div class="price_box fl-l sold_out "><div class="inner type02"><a href="#none" title=""><div class="tck_etc_text"><div class="tck_etc_use">매진</div></div></a></div></div><div class="price_box fl-l sold_out "><div class="inner type02"><a href="#none" title=""><div class="tck_etc_text"><div class="tck_etc_use">매진</div></div></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_sancheon_ticket "><span class="blind">KTX-산천</span></span><span class="num "> 287</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(14:28 ~ 15:08)</span></h3><p class="s_txt">소요시간: 40분</p></div></div></div><div class="price_box fl-l sold_out "><div class="inner type02"><a href="#none" title=""><div class="tck_etc_text"><div class="tck_etc_use">매진</div></div></a></div></div><div class="price_box fl-l spe sold_out_soon"><div class="inner type02"><a href="#none" title=""><p class="txt_ch">특실(매진임박)</p><p class="txt_price txt_bk">19,700원</p><p class="txt_gr">5%적립</p></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_ktx_ticket "><span class="blind">KTX</span></span><span class="num "> 245</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(14:33 ~ 15:13)</span></h3><p class="s_txt">소요시간: 40분</p></div></div></div><div class="price_box fl-l gen "><div class="inner type02"><a href="#none" title=""><p class="txt_ch">일반실</p><p class="txt_price txt_bk">14,100원</p><p class="txt_gr">5%적립</p></a></div></div><div class="price_box fl-l spe sold_out_soon"><div class="inner type02"><a href="#none" title=""><p class="txt_ch">특실(매진임박)</p><p class="txt_price txt_bk">19,700원</p><p class="txt_gr">5%적립</p></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_ktx_ticket "><span class="blind">KTX</span></span><span class="num "> 087</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(14:42 ~ 15:22)</span></h3><p class="s_txt">소요시간: 40분</p></div></div></div><div class="price_box fl-l gen "><div class="inner type02"><a href="#none" title=""><p class="txt_ch">일반실</p><p class="txt_price txt_bk">14,100원</p><p class="txt_gr">5%적립</p></a></div></div><div class="price_box fl-l spe sold_out_soon"><div class="inner type02"><a href="#none" title=""><p class="txt_ch">특실(매진임박)</p><p class="txt_price txt_bk">19,700원</p><p class="txt_gr">5%적립</p></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_ktx_ticket "><span class="blind">KTX</span></span><span class="num "> 041</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(14:58 ~ 15:38)</span></h3><p class="s_txt">소요시간: 40분</p></div></div></div><div class="price_box fl-l gen "><div class="inner type02"><a href="#none" title=""><p class="txt_ch">일반실</p><p class="txt_price txt_bk">14,100원</p><p class="txt_gr">5%적립</p></a></div></div><div class="price_box fl-l spe sold_out_soon"><div class="inner type02"><a href="#none" title=""><p class="txt_ch">특실(매진임박)</p><p class="txt_price txt_bk">19,700원</p><p class="txt_gr">5%적립</p></a></div></div></div></li><li class="tckList clear
"><div class="tck_inner"><div class="info_inner fl-l"><div class="info_box"><div class="tit_box type02"><div class="flag_wrap type2"><span class="train_sancheon_ticket "><span class="blind">KTX-산천</span></span><span class="num "> 089</span></div></div><div class="data_box right"><h3 class="txt_bk"><span>서울</span> → <span>천안아산</span><span>(15:23 ~ 15:58)</span></h3><p class="s_txt">소요시간: 35분</p></div></div></div><div class="price_box fl-l gen "><div class="inner type02"><a href="#none" title=""><p class="txt_ch">일반실</p><p class="txt_price txt_bk">14,100원</p><p class="txt_gr">5%적립</p></a></div></div><div class="price_box fl-l sold_out "><div class="inner type02"><a href="#none" title=""><div class="tck_etc_text"><div class="tck_etc_use">매진</div></div></a></div></div></div></li></ul></div>
gemini 답변
네, 확장 프로그램이 정상적으로 작동하지 않는 문제와 요청하신 체크박스 기능 누락 문제를 해결하기 위해 content.js 파일을 수정하겠습니다.
기존의 '선택' 버튼 방식 대신, 각 좌석 타입 앞에 체크박스를 추가하고 매크로 UI가 안정적으로 표시되도록 코드를 개선했습니다.
아래는 수정된 content.js 파일의 전체 내용입니다. (첨부파일 생략) 이 코드로 교체해 주세요.
다시 프로그램을 크롬 브라우저에 등록한 후, 테스트를 해보니 일부 수정은 되었지만, 완전히 작동되지는 않았다. 글자가 흰색으로 표시되어 있어서 눈에 잘 보이지 않는 문제와 열차 목록에 checkbox가 여전히 표시되지 않고 있었다. 하지만, HTML소스를 확인해보면 checkbox가 표시되도록 수정이 되어 있었다. 그 문제점을 다시 질의해보았다.
질문(Prompt)
추가 문제를 아래에 적는다.
1. "매크로 실행" 글자 색상을 검정색으로 변경해줘. 흰색은 보이지가 않아.
2. 열차 목록에 checkbox가 추가되어있는데, 실제화면에는 보이지 않는다. style.css에 문제가 있어 보이는데, 재확인해줄래.
gemini 답변
네, 알려주신 문제점들을 확인했습니다. 버튼의 글자색이 보이지 않는 문제와 체크박스가 표시되지 않는 문제를 해결하기 위해 style.css 파일을 수정하고, 그에 맞춰 content.js 파일도 일부 변경했습니다.
아래의 수정된 style.css와 content.js 파일로 교체하여 확장 프로그램을 다시 로드해 보세요.
1. style.css (수정)
버튼의 기본 스타일과 매크로 실행 중 스타일을 분리하고, 체크박스가 항상 보이도록 스타일을 강화했습니다.
2. content.js (수정)
style.css 파일에서 버튼 스타일을 제어할 수 있도록, 자바스크립트에서 직접 스타일을 변경하는 대신 CSS 클래스를 추가/제거하도록 수정했습니다.
위의 두 파일로 교체하시면 "매크로 실행" 버튼의 글자색이 검정색으로 보이고, 열차 목록의 체크박스도 정상적으로 표시될 것입니다.
다시 시험을 해보니, 여전히 일부 오류가 있었다. 그 문제점에 대해 추정을 해서 물어보았다. 물론, 내가 95년부터 웹기술에 대한 변천사를 연구(?)해 왔었기에, 즉, 웹에 대한 기본지식을 갖고 있었기에 좀 더 타케팅해서 질문을 해서, 빠른 해결이 되도록 해보았다. ㅎㅎㅎ
질문
checkbox는 여전히 표시가 되지 않는다. css의 border설정에 문제가 있는 것은 아닐까?
gemini 답변
네, 지적해주신 내용이 맞습니다. 코레일 웹사이트의 기본 CSS 스타일이 강력해서 확장 프로그램의 스타일을 덮어쓰는 바람에 체크박스가 보이지 않는 문제로 보입니다. 특히 border를 포함한 여러 속성들이 영향을 줄 수 있습니다.
이 문제를 해결하기 위해, 체크박스가 확실하게 보이도록 content.js에서 직접 스타일을 강제하고, style.css 파일도 더욱 보강했습니다.
아래 두 파일을 모두 업데이트해주세요.
1. content.js (수정)
현재 Canvas에 열려 있는 content.js 파일을 업데이트했습니다. 체크박스를 만들 때 자바스크립트로 직접 핵심 스타일을 적용하여 CSS가 적용되지 않는 만일의 경우에도 보이도록 수정했습니다.
2. style.css (수정)
말씀하신 border 문제를 포함하여 체크박스가 브라우저 기본 스타일로 확실히 보이도록 -webkit-appearance 속성을 추가하고 테두리와 배경색을 명시적으로 지정했습니다.
위와 같이 두 파일을 모두 수정한 후 확장 프로그램을 다시 로드하면 체크박스가 정상적으로 표시될 것입니다.
이 놈(gemini)이 너무 잘 이해를 한 것 같았다. 이제 다시 시험을 해보니, 아래와 같이 원하는 대로 만들어 졌다. (화면캡처를 나중에 한 것이라, 나중에 추가한 “팝업자동단기” 부분은 제외)
기존 기차표 예약하는 서비스에서 원래 제공되는 것 처럼, “매크로 실행” 이라는 버튼과 함께, 열차목록에 checkbox가 건건이 표시되었다.

이제 예약을 해보자. 맨 위에서 예약까지 하는 프롬프트를 만들어두면 한번에 해결되었을 텐데, 개발 스펙 작성이 두 단계로 진행하게 된 것이다. 이렇게 빨리 결과가 잘 나올줄은 몰랐다.
질문
추가 기능을 얘기할께. 아래 내용을 적용해줘.
예매 가능한 차실이 있는 경우, 기존에 얘기한 부분이 수행된 후, 아래의 기능이 작동되게 만들어줘. 화면에 나타난 버튼 중에 "예매"라는 버튼을 마우스로 클릭한 것으로 처리되게 해줘. 실제 예매가 진행되는 것은 아래의 HTML코드의 reservbtnWrap class를 사용하는 것으로 확인되었어.
<div class="ticket_reserv_wrap"><div class="ticket_reserv_inner"><div class="reserv_wrapbtn"><button type="button" class="reserv_btn"><span class="blind">예매 숨기기 버튼</span></button></div><div class="ticket_reserv clear oneline"><ul class="reserv_first clear"><li>일반실</li></ul><ul class="reserv_center clear"><li><a href="#none">열차시각</a></li><li><a href="#none" class="">운임요금</a></li><li><a href="#none">좌석선택</a></li></ul><div class="reservbtnWrap one_btn"><button type="button" class="btn_bn-blue02 reservbtn " tabindex="0">예매</button></div></div></div></div> 그리고, 예약이 완료되었을 때, 예약 완료되었다는 소리가 나도록 변경해줘 마지막으로, 아래와 같이 단순히 알리는 메시지가 출력되는 것은 자동으로 "확인"버튼이 눌리도록 해줘. 단순 알리는 메시지는 아래 Layer와 비슷하게 표시가 된다. <div class="layerWrap"><div class="tit_wrap"><h1 class="tit">이용안내</h1></div><div class="con_Wrap"><div class="type_tckRelay_03"><div class="confirm_message"><img alt="확인" src="/src/assets/ticket/img/common/confirm_message_icon.png" class="m"><div style="white-space: pre-line;">선택하신 열차는 KTX-산천 2개 편성을 연결하여 운행하는 열차로서, 반드시 열차번호와 해당호차를 확인하시고 승차하시기 바랍니다.</div></div><div><p> </p></div><div class="btnWrap"><button type="button" class="btn_bn-blue btn_pop-close">확인</button></div></div></div></div>
gemini 답변
네, 요청하신 추가 기능들을 반영하여 content.js 파일을 업데이트했습니다.
주요 변경 사항:
1. 예약 성공 사운드 추가: 예약이 최종적으로 성공했을 때 간단한 소리가 나도록 playSuccessSound 함수를 추가했습니다.
2. 자동 '확인' 클릭: 열차 선택 후 나타나는 정보 안내 팝업의 '확인' 버튼을 자동으로 클릭합니다.
3. 자동 '예매' 클릭: 최종 '예매' 버튼이 나타나면 자동으로 클릭하여 예약을 완료합니다.
4. 전체 로직 통합: 위의 기능들이 매끄럽게 연동되도록 MutationObserver를 사용하여 페이지의 변화를 감지하고 대응하도록 구현했습니다.
아래는 업데이트된 content.js 파일입니다. 이 내용으로 교체해 주세요. manifest.json과 style.css 파일은 변경할 필요 없습니다.
이제 content.js 파일을 위 코드로 교체한 후 확장 프로그램을 다시 로드하면, 예약 가능한 좌석을 선택한 후 나타나는 팝업과 최종 예매 버튼이 모두 자동으로 처리되며 예약 완료 시 소리로 알려줄 것입니다.
시험을 해보니, 예약은 되는데 소리가 나오지 않았다. 그래서 아래와 같이 재요청하였다.
질문
예약 성공했을 때 사용하는 mp3파일은 local에 있는 것을 사용하게 변경해줘.
그리고, 기존 소스에서는 예약성공했을 때에 mp3파일의 소리가 나지 않았어. 무엇이 문제일까?
gemini답변
네, 로컬 MP3 파일을 사용하는 방법과 기존에 소리가 나지 않았던 문제에 대해 설명해 드리겠습니다.
소리가 나지 않았던 원인
이전 버전에서 예약 성공 시 소리가 재생되지 않은 가장 큰 이유는 두 가지일 가능성이 높습니다.
브라우저 자동 재생 정책 (Autoplay Policy): 최신 웹브라우저는 사용자가 페이지를 직접 클릭하는 등의 상호작용이 없으면 스크립트가 소리를 자동으로 재생하는 것을 막습니다. 매크로가 예약을 완료하는 시점은 사용자의 최초 클릭과 시간 차이가 있어 브라우저가 자동 재생으로 간주하고 차단했을 수 있습니다.
콘텐츠 보안 정책 (CSP): 코레일 웹사이트의 보안 정책이 외부 웹사이트(actions.google.com)의 사운드 파일 로딩을 차단했을 수 있습니다.
로컬 MP3 파일 사용 방법 및 해결책
이 문제들을 해결하기 위해 로컬 파일을 사용하도록 소스를 수정하겠습니다.
중요한 준비 사항: 사용하시려는 MP3 파일(파일 이름을 success.mp3라고 가정하겠습니다)을 manifest.json, content.js 파일과 같은 폴더 안에 복사해두어야 합니다.
그런 다음, 아래와 같이 2개의 파일을 수정해 주세요.
1. manifest.json 수정
확장 프로그램이 폴더 안의 success.mp3 파일을 인식하고 웹 페이지에서 사용할 수 있도록 web_accessible_resources 설정을 추가해야 합니다.
위와 같이 수정한 후, success.mp3 파일을 폴더에 넣고 확장 프로그램을 다시 로드하면 예약 성공 시 로컬 사운드 파일이 정상적으로 재생될 것입니다.
위 답변에서, 권한 설정 때문에 mp3 플레이가 안되었다는 것을 이해하였다. 다시 테스트를 해보니, 정상적으로 작동이 되었다.
여기까지 gemini에게 질의 답변하면서 진행한 시간이 딱 1시간 밖에 소요되지 않았었다.
결과정리
여기에 글을 남기는 것은 매크로를 무조건 만들어 보라는 뜻은 아니다. 개발을 하지 못하더라도 일반적인 지식을 갖고 있으면 아주 쉽고 빠르게, 목적하는 프로그램을 만들수 있는 세상이 성큼 다가와 있다는 것을 생각하고, 한번이라도 시도해봤으면 하는 생각에서 글을 적었다.
한번 해보고 느끼면, LLM이라는 도구가 내 역량을 수배~수십배 올릴 수 있는 필수 동반자 같은 느낌이 들게 될 것 같다. LLM을 정말로 안써 본사람은 있어도, 한번만 써본 사람은 없을 것이라는 말로 연결시키서,… LLM을 단순 검색엔진 용도로만 사용하기 보다는 업무 효율화를 위한 도구로 활용해봤으면 한다.
그리고, 여러가지 LLM 도구를 쇼핑하듯이 돌아다니면서 써봤네 라는 말만 하기 보다는 한개라도 좀 오랫동안 만져봤으면 하는 생각이다. 요즘도 LLM 도구를 갖고 완전하지 않아 라는 말로 이를 하찮게 보는 사람이 있다. 지금의 LLM 도구는 나보다 더 월등한 방대한 지식과 부지런함과 수용하는 태도를 갖고 있다. 이러한 태도의 사람을 직접 옆에 데리고 일을 하면 최상의 실력을 갖고 있는 사람이라 여겨질텐데, 기계라는 것 때문에 사람들은 인식을 덜 하는 듯 하다. 말이 많았다.
최종적으로, gemini가 만든 소스는 아래에 첨부하였다.(실제 사용하기 보다는 공유 용도로 참고 하시길.) 웹확장기능으로 추가하는 방법은…. ㅎㅎ 생략했다. 이것도 하나의 해킹 프로그램이기 때문에, 내용을 파악해 보시고 정말로 니즈가 있다면 스스로 웹확장 기능 등록하는 방법은 찾아보시길… 너무 쉽게 찾을 수 있기에…. (위에서 gemini가 이미 설명을 했었던가? 기억이 가물가물…..)
최종 실행된 화면 사례인데, 원래 페이지에서 빨간색으로 표시한 부분이 원래 있었던 것 처럼 추가가 되어있다.

gemini가 최종적으로 만든 소스는 아래에 첨부하였다.
부연에 부연을….
마지막으로, 보안을 하는 사람 입장에서 이러한 매크로를 막으려면 어떤 방식으로 차단할 수 있을까?
물론, 차단하는 방식을 이해하면 또 다른 우회하는 방법을 찾을 것이 일반적이겠지만, 이러한 행위가 반복되지 않도록 근본적으로 차단될 수 있는 방법은 없을까? ^^
웹확장프로그램 = 매크로 라는 것은 절대 아니다. 웹확장프로그램은 여러가지 용도로 사용할 수도 있다. 예를 들면, chatgpt에서 영어로 출력되는 글을 확장 프로그램이 실시간으로 가져와서 TTS로 출력해주는 프로그램으로 확장할 수도 있다. 확장 프로그램의 활용성은 많다……
실행 버튼이 없어서 다시 질문을 던졌다.(내가 누락 한 것을 다른 분이 보완해준 것을 추가해두었다)
질문
아래와 같은 크롬 확장 프로그램을 가지고 "https://www.korail.com/ticket/search/list" 사이트에 접속했는데
의도한 대로 "매크로 실행" 버튼이 생성되지 않아. 이를 해결해줘.
답변
주어진 Chrome 확장 프로그램 코드와 manifest.json, style.css, content.js 파일을 분석한 결과, "매크로 실행" 버튼이 "코레일 " 페이지에서 생성되지 않는 문제는 content.js 파일 내의 UI 초기화 로직 조건 때문입니다.
content.js 파일의 initInterval 함수 내에서 매크로 UI를 설정하는 setupMacroUI() 함수가 다음 조건이 충족될 때만 호출됩니다: if (tckWrap && tckWrap.querySelectorAll('.tckList').length > 0)
이 조건은 .tckWrap 요소가 존재하고, 그 안에 .tckList 클래스를 가진 요소가 하나 이상 있을 때만 UI 생성을 허용합니다. "코레일 " 페이지에 접속했을 때 초기 로딩 시점에는 아직 열차 목록인 .tckList 요소가 렌더링되지 않아 이 조건이 충족되지 않을 수 있습니다. 따라서 setupMacroUI() 함수가 호출되지 않아 버튼이 생성되지 않는 것입니다.
해결 방법:
content.js 파일의 initInterval 함수 내에서 setupMacroUI() 호출 조건을 수정하여 .tckWrap 요소만 있어도 UI가 생성되도록 변경합니다. 이렇게 하면 열차 목록이 로드되기 전에도 매크로 제어 UI가 페이지에 나타날 수 있습니다.
수정할 부분은 content.js 파일의 initInterval 함수 내에 있습니다:
const initInterval = setInterval(() => {
const tckWrap = document.querySelector('.tckWrap');
// 이 부분을 수정합니다:
// 기존: if (tckWrap && tckWrap.querySelectorAll('.tckList').length > 0) {
// 변경:
if (tckWrap) { // .tckWrap 요소만 있어도 UI 생성
setupMacroUI();
addSelectionCheckboxes(); // 이 함수들은 UI가 생성된 후에도 열차 목록이 로드될 때까지 기다려야 할 수 있습니다.
restoreSelections();
if (sessionStorage.getItem('isMacroRunningAfterReload') === 'true') {
const button = document.getElementById('macroButton');
if (button && document.querySelectorAll('.macro-checkbox:checked').length > 0) {
if (!isMacroRunning) {
toggleMacro();
}
} else {
sessionStorage.removeItem('isMacroRunningAfterReload');
}
}
}
if (!window.autoClickHandlerStarted) {
startAutoClickHandler();
window.autoClickHandlerStarted = true;
}
// UI가 생성되었거나, UI가 필요 없는 페이지(예약 상세)인 경우 인터벌을 종료합니다.
// 이 조건은 그대로 유지합니다.
if (document.getElementById('macroContainer') || !tckWrap) {
clearInterval(initInterval);
}
}, 500);
추가 고려사항:
addSelectionCheckboxes() 및 restoreSelections() 함수는 열차 목록(.tckList)이 실제로 DOM에 추가된 후에 호출되어야 정확하게 동작합니다. 만약 위 수정 후에도 체크박스가 즉시 나타나지 않는다면, addSelectionCheckboxes() 함수 호출을 열차 목록이 동적으로 로드되는 것을 감지하는 MutationObserver 내에서 처리하거나, setupMacroUI() 호출과 별도로 열차 목록 로드 이후에 실행되도록 로직을 분리하는 것을 고려해볼 수 있습니다. 하지만 현재 문제의 핵심은 "매크로 실행" 버튼 자체가 나타나지 않는 것이므로, 위 수정으로 해결될 가능성이 높습니다.
'기타' 카테고리의 다른 글
[헛소리] "좌파세력 민노총이 망친 경제, 국민의힘이 살리겠습니다"? (0) | 2025.05.08 |
---|---|
홍콩 공항 출국할 때, 빈 물병 들고 들어가면 (0) | 2025.03.23 |
코맥스(commax) 현관 카메라 초인종(인터폰 DCR-41QC)의 잦은 버튼 눌림/고장/교체 (1) | 2024.09.20 |
코웨이 정수기 정기적인 필터교체 사기 안 당하려면 꼭 확인해보세요. (1) | 2024.08.12 |
잡담. (0) | 2024.01.29 |