오늘 인터파크 티켓팅 매크로 패치를 시작하면서 공지할 점이 있어 글을 쓰게 되었다.

 

많은 분들이 오류를 겪고 댓글로 알려 주시고 있는데, 안심예매 정도야 문제를 구현할 수 있으나 일부 내가 경험하기 힘든 문제가 있다.

 

뭐든지 문제를 고치기 위해선 문제를 내가 직접 겪어 봐야만 알 수 있다.

 

그래서 가급적 문제를 보고할 때는 어느 공연에서 문제가 발생했는지, 팝업이 떴으면 어떤 팝업이 떴는지에 대한 내용도 같이 알려 주시면 좋겠다.

 

 

지난번 글에서 인터파크 티켓팅 매크로 코드를 공개했는데, 보면 프로그램의 기능을 담당하는 .py 스크립트와 UI를 담당하는 .ui 파일이 분리되어 있는 것을 알 수 있다.

 

나는 보통 pyuic를 통해 .ui 파일에서 .py로 스크립트를 변경해 썼다. 

 

이 방법이 여러모로 편리하지만, 원치 않는 찌거기같은 코드가 마구 딸려와서 코드가 지저분해지는 경향이 있었고, UI의 변경이 어려웠다. 난 코드로 UI 디자인을 거의 못하기 때문에 Qt Designer을 쓰는데, 그런 나에겐 최악의 상황이다.

 

따라서 uic.loadUiType을 통해 ui를 따로 로드해서 프로그램을 만들어 보기로 했고 이 티켓팅 매크로가 그 첫 시도의 결과물이다.

 

ui를 따로 로드했을 때의 단점은 텍스트 에디터에서 보조를 받기 쉽지 않다는 점과, pyinstaller로 빌드 시 파일을 포함해서 빌드하지 않는다는 점이다. 따라서 .py 스크립트만 빌드하면 반드시 ui 파일이 따라다녀야 한다. 코드 공개를 원치 않는 사람에게는 별로 좋지 못하다. 그리고 파일을 한꺼번에 합치고 싶은 사람에게도 별로 좋지 못하다.

 

그래서 방법을 알아보다가 찾아서 공유한다.

 

일단 기본적으로 아래 코드를 최상단에 추가한다. 

1
2
3
4
5
6
7
import sys
import os
 
def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, relative_path)
cs

stack overflow에서 구한 코드다. 정확히 무슨 역할을 하는지는 모르겠지만 아마도 바이너리 파일 내의 파일을 일반적인 파일처럼 열 수 있도록 해주는 코드 같다.

 

그러고 나서는 이제 pyinstaller에게 ui파일을 포함하도록 알려 줘야 하는데, spec 파일이 그것이다. spec 파일은 프로그램 빌드 시 다양한 옵션을 줄 수 있도록 하는 파일이다. 먼저 .py 스크립트만 빌드를 하면 spec 파일이 자동으로 생성된다. 그러면 그걸 수정하고 다시 빌드하면 된다.

 

우리는 ui 파일만 포함시키면 되므로, 아래와 같이 해주면 된다

1
2
3
4
a = Analysis(
    datas=[ ('TicketingMacro.ui', '.') ],
    ...
    )
cs

그러고 난 후 다시 원래 .py 스크립트로 돌아가서

1
2
3
form = resource_path('TicketingMacro.ui')
 
form_class = uic.loadUiType(form)[0
cs

위와 같이 resource_path 함수 안에 .ui파일을 적어 주고 반환값을 uic.loadUiType에다가 넘겨 주면 된다.

  1. helpi 2020.08.14 14:58

    안녕하세요 좋은글 감사드립니다.

    이전(후) 티켓팅 관련 배포파일 잘 보았습니다. 티케팅목적은 아니고 ui파일과 py 파일을 이용하여 exe파일을 만드는 것을 공부하다가 발견하였는데요.
    ui파일을 py파일로 만들지 않고
    pyinstaller를 이용하여 exe파일을 만드려고 하였으나 자꾸 창이 나타나지 않고 바로 꺼지니 이게 무슨이유인가 싶어서 올려주신 파일도 해보았으나 바로 꺼지길래 exe파일로 만드는 과정에서 문제가 있나 해서 질문드립니다.
    올려주신 TicketingMacro 소스파일 3개(ui, spec, py)를 이용해 pyinstaller를 사용하였는데 해당 문구는 다음과 같았습니다.
    pyinstaller --onefile TicketingMacro.py
    이동 경로를 제대로 하고 시도하였는데 exe실행 시 창이 뜨지 않고 바로 종료되었습니다. pyinstaller 시도 당시에 어떻게 해야 해당 세 소스파일을 제대로 빌드 할 수 있을까요??

    • 페이지다운 2020.08.14 16:36 신고

      .py파일 말고 .spec 파일로 빌드해야 합니다. pyinstaller --windowed -F TicketingMacro.spec 이렇게 하시면 됩니다.

  2. helpi 2020.08.19 15:49

    안녕하세요 바로 위에 댓글 올렸던 사람입니다.

    파이썬으로 파일 하나 만들어보겠다고 이곳저곳 끙끙댔는데 문제점에 대해서 알려주셔서 처음으로 프로그램 만들어 보았습니다.
    spec파일로 빌드하는 것, spec 파일에 data부분에 ui를 추가하는 것 두개가 핵심이었네요
    덕분에 PC로 실행하는 첫 프로그램 잘 실행되게 만들었습니다. 도움 주셔서 정말 감사드립니다. 항상 좋은일만 가득하시길 바라겠습니다.

    실행파일만이 아니라 소스파일(py, ui, spec)파일 같이 올려주셔서 감사합니다 덕분에 공부에 큰 도움되었습니다.

  3. lia 2020.09.10 16:45

    안녕하세요.
    저도 계속 ui 파일을 py 파일로 변경하는게 번거로워서 알아보던 중 요 글 보고 큰 도움 받아 해결하였습니다!
    너무 감사드립니다 ^^

  4. s_i 2020.09.18 02:05

    안녕하세요. 저도 올려주신 코드를 보고 이리저리 바꿔서 좀 더 공부해보려고 해당 파일을 수정해 빌드해보았는데요. 위에 질문처럼 exe파일을 만들어 실행하였으나 저는 해당 파일이 여전히 바로 종료되어서 궁금해서 질문 남깁니다. dist 폴더 안에 들어있는 파일을 실행시키는 것이 맞는지요? 처음엔 py파일로 빌드 후, spec 파일로 빌드하는 것이 맞나요? 계속 헤매다가 질문 남깁니다. 감사합니다.

    • 페이지다운 2020.09.18 17:23 신고

      처음에 py로 빌드하고 생성된 spec파일을 수정해 그걸로 빌드하면 됩니다. 만약 오류가 나면서 바로 꺼진다면 pyqt가 문제일 수 있으므로 재설치하고 시도해 보시기 바랍니다

이번엔 인터파크 티켓팅 매크로에 대한 코드를 공유하겠다. 자유롭게 수정해서 쓰고, 재배포 시 출처를 명확히 표기할 경우 허용하지만, 상업적 용도로 내 코드 전체 또는 일부를 사용하는 것은 금지다.

 

일반 버전

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
#인터파크 티켓팅 매크로
#만든 이 : PageDown
#https://pagedown.n-e.kr/
 
 
import sys
import random
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from selenium import webdriver
 
form_class = uic.loadUiType('TicketingMacro.ui')[0
 
 
class MyWindow(QtWidgets.QMainWindow, form_class):
    is_data_disabled = False  # 정보 입력창 비활성화 여부
    time = None  # 스레드로부터 받아오는 현재 시간
 
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.user_password_input.setEchoMode(QtWidgets.QLineEdit.Password)
        self.seats_number_spinbox.setValue(1)
        self.date_edit.setDate(QtCore.QDate.currentDate())
        self.time_edit.setTime(QtCore.QTime.currentTime())
        self.disableElements(self.time_edit, self.label_auto_start)
 
        self.apply_btn.clicked.connect(self.initData)  # 확인 버튼 클릭
        self.login_btn.clicked.connect(self.startLogin)  # 로그인 버튼 클릭
        self.start_ticketing_btn.clicked.connect(self.startTicketing)  # 시작 버튼 클릭
        self.seats_number_spinbox.valueChanged.connect(self.checkSeatsNumber)  # 좌석 개수 변경
        self.use_auto_start.stateChanged.connect(self.useAutoStart)  # 자동 시작 여부 변경
 
    def initData(self):
        if not self.is_data_disabled:  # 정보 입력창 활성화 시
            self.user_id = self.user_id_input.text()  # 아이디
            self.user_password = self.user_password_input.text()  # 비밀번호
            self.product_code = self.product_code_input.text()  # 공연 코드
            self.seats_number = self.seats_number_spinbox.value()  # 좌석 개수
            self.use_random_seat = self.use_seat_select.isChecked()  # 중간(랜덤) 선택 여부
            self.is_canceled_ticketing = self.canceled_ticket_mode.isChecked()  # 취켓팅 여부
            if self.use_auto_start.isChecked():  # 자동 시작 시 시간 24시간 방식 -> 12시간 방식 변경
                self.time = self.time_edit.time()
                self.time = str(self.time.toPyTime())[:6+ '00'
                if int(self.time[0:2]) > 12:
                    self.new_time_h = int(self.time[0:2]) - 12
                    self.time = str(self.new_time_h) + self.time[2:]
                    if self.new_time_h < 10:
                        self.time = '0' + self.time
                elif self.time[0:2== '00':
                    self.time = '12' + self.time[2:]
            self.date = self.date_edit.date()  # 날짜
            self.date = self.date.toPyDate()
            self.date = str(self.date).replace('-''')
            self.disableElements(self.user_id_input, self.user_password_input, self.product_code_input, self.date_edit,
                                 self.seats_number_spinbox, self.use_seat_select, self.use_auto_start, self.time_edit, self.canceled_ticket_mode)  # 요소 비활성화
            self.is_data_disabled = True
            self.apply_btn.setText('수정')
            QtWidgets.QMessageBox.information(self, '완료''정보 입력이 완료되었습니다.')
        else:
            self.is_data_disabled = False
            self.apply_btn.setText('확인')
            self.enableElements(self.user_id_input, self.user_password_input, self.product_code_input, self.date_edit,
                                self.seats_number_spinbox, self.use_seat_select, self.use_auto_start, self.time_edit, self.canceled_ticket_mode)  # 요소 활성화
 
    # 좌석 개수 변경
    def checkSeatsNumber(self):
        if self.seats_number_spinbox.value() > 4:
            self.seats_number_spinbox.setValue(4)
            QtWidgets.QMessageBox.critical(self, '오류''최대 좌석 개수는 4개입니다.')
        if self.seats_number_spinbox.value() < 1:
            self.seats_number_spinbox.setValue(1)
            QtWidgets.QMessageBox.critical(self, '오류''최소 좌석 개수는 1개입니다.')
 
    # 요소들 입력받아 한꺼번에 활성화
    def enableElements(self, *elements):
        for element in elements:
            element.setEnabled(True)
 
    # 요소들 입력받아 한꺼번에 비활성화
    def disableElements(self, *elements):
        for element in elements:
            element.setEnabled(False)
 
    # 자동시작 시
    def useAutoStart(self):
        if self.use_auto_start.isChecked():
            self.enableElements(self.time_edit, self.label_auto_start)
        else:
            self.time = None
            self.disableElements(self.time_edit, self.label_auto_start)
 
    # 로그인
    def startLogin(self):
        self.time_th = TimeThread()
        if self.apply_btn.text() == '확인':  # 확인 버튼을 누르지 않았을 때
            QtWidgets.QMessageBox.critical(self, '오류''좌석 정보를 입력하세요.')
        else:
            # Dictionary 형식으로 정보 전달
            ticketing_data_to_send = {
                'user_id': self.user_id,  # 아이디
                'user_pw': self.user_password,  # 비밀번호
                'product_code': self.product_code,  # 공연 번호
                'date': self.date,  # 공연 날짜
                'time': self.time,  # 자동시작 시간
                'seats_number': self.seats_number,  # 좌석 개수
                'use_random_seat': self.use_random_seat,  # 랜덤 선택 여부
                'time_signal': self.time_th.time_signal,  # 시간 스레드
                'is_canceled': self.is_canceled_ticketing  # 취켓팅 여부
            }
            self.apply_btn.setEnabled(False)  # 확인(수정) 버튼 비활성화
            self.login_btn.setEnabled(False)  # 로그인 버튼 비활성화
            # 시간/티켓팅 스레드 시작
            self.time_th.start()
            self.time_th.time_signal.connect(self.changeTime)
            self.ticketing_th = TicketingThread(ticketing_data=ticketing_data_to_send)
            self.ticketing_th.start()
 
    # 티켓팅 시작
    def startTicketing(self):
        self.ticketing_th.start_ticketing = True
 
    # 타이머 시작
    def startTimer(self):
        self.time_th = TimeThread()
        self.time_th.start()
        self.time_th.time_signal.connect(self.changeTime)
 
    @QtCore.pyqtSlot(str)
    def changeTime(self, time):
        if time == 'error':  # 웹드라이버(시계) 종료시 알림
            QtWidgets.QMessageBox.critical(self, '오류''타이머를 끄지 마십시오.')
        else:
            self.now_time.setText(time)
 
 
class TimeThread(QtCore.QThread):
    time_signal = QtCore.pyqtSignal(str)
 
    def run(self):
        while True:
            self.driver = webdriver.Chrome('chromedriver.exe')
            #네이버 시계 접속
            self.driver.get('https://search.naver.com/search.naver?sm=tab_hty.top&where=nexearch&query=%EB%84%A4%EC%9D%B4%EB%B2%84+%EC%8B%9C%EA%B3%84&oquery=%EC%8B%9C%EA%B3%84&tqi=UEMiLdprvTossFQzFhCssssssKG-165498')
            while True:
                try:
                    text = self.driver.find_element_by_css_selector('#_cs_domestic_clock > div._timeLayer.time_bx > div > div').text  # 네이버 시계 text
                    text = text.replace('\n''').replace(' ''')[0:8]
                except:
                    self.time_signal.emit('error')  # 웹드라이버(시계) 종료 시 error emit
                    break
                self.time_signal.emit(''.join(text))
 
 
class TicketingThread(QtCore.QThread):
    def __init__(self, ticketing_data, parent=None):
        QtCore.QThread.__init__(self, parent)
        self.user_id = ticketing_data['user_id']
        self.user_password = ticketing_data['user_pw']
        self.product_code = ticketing_data['product_code']
        self.date = ticketing_data['date']
        self.set_time = ticketing_data['time']
        self.seats_number = ticketing_data['seats_number']
        self.use_random_seat = ticketing_data['use_random_seat']
        self.time_signal = ticketing_data['time_signal']
        self.is_canceled_ticketing = ticketing_data['is_canceled']
        self.driver = self.driver = webdriver.Chrome('chromedriver.exe')
        self.time_signal.connect(self.checkTime)  # 시간 스레드와 연결(자동시작)
        self.is_logined = False  # 로그인 여부 False
        self.start_ticketing = False  # 시작 여부 False
 
    # 요소는 반드시 CSS 셀렉터로만(ID 우선)
 
    # 로그인
    def login(self):
        self.driver.get('https://ticket.interpark.com/Gate/TPLogin.asp')
        iframes = self.driver.find_elements_by_tag_name('iframe')
        self.driver.switch_to.frame(iframes[0])
        self.driver.find_element_by_id('userId').send_keys(self.user_id)
        self.driver.find_element_by_id('userPwd').send_keys(self.user_password)
        self.driver.find_element_by_id('btn_login').click()
        self.driver.get('https://ticket.interpark.com/')  # 인터파크 메인 페이지로 강제 접속
 
    def selectSeat(self):
        self.failed_to_get_ticket = False  # 취켓팅용-기본적으로 성공으로 표시
        # 직링 생성
        self.url = 'http://poticket.interpark.com/Book/BookSession.asp?GroupCode={}&Tiki=N&Point=N&PlayDate={}&PlaySeq=001&BizCode=&BizMemberCode='.format(self.product_code, self.date)
        self.driver.get(self.url)
 
        # 요소를 찾을 때까지 무한 루프(찾지 못하면 예외)
        while True:
            try:
                first_iframe = self.driver.find_element_by_id('ifrmSeat')  # 첫번째 아이프레임
            except:
                continue
            else:
                break
        self.driver.switch_to.frame(first_iframe)
        while True:
            try:
                next_iframe = self.driver.find_element_by_id('ifrmSeatDetail')  # 두번째 좌석선택 아이프레임
            except:
                continue
            else:
                break
        self.driver.switch_to.frame(next_iframe)
 
        self.loop_time = 0  #취켓팅용
        while True:
            try:
                elements = self.driver.find_elements_by_class_name('stySeat')
                self.loop_time += 1
                # 취켓팅하면서 루프를 100번 돌면 루프 탈출
                if self.is_canceled_ticketing and (self.loop_time > 100):
                    self.failed_to_get_ticket = True  # 잔여 좌석 인식 실패(개수 0)
                    break
            except:
                continue
            else:
                if len(elements) == 0:  # 예외는 나지 않았지만 좌석이 인식되지 못한 경우
                    continue
                else:
                    break
 
        while len(elements) > 0:
            # 첫번째부터 1개 선택
            if (not self.use_random_seat) and (self.seats_number == 1):
                self.driver.find_element_by_css_selector('#TmgsTable > tbody > tr > td > img:nth-child(3)').click()  # 바로 첫번째 요소 선택
 
            # 여러개 선택
            elif not self.use_random_seat:
                seat_count = 0
                for element in elements:
                    if seat_count < self.seats_number:
                        element.click()
                        seat_count += 1
 
            # 랜덤/여러개 선택
            elif self.use_random_seat:
                start_seat = random.randint(
                    int(len(elements) / 2), len(elements) - self.seats_number)
                for i in range(start_seat, start_seat + self.seats_number):
                    if start_seat < self.seats_number + start_seat:
                        elements[i].click()
            try:
                self.driver.switch_to.default_content()  # 제일 바깥으로 아이프레임 벗어남
                self.driver.switch_to.frame(first_iframe)  # 첫번째 아이프레임으로 다시 변경
                self.driver.find_element_by_id('NextStepImage').click()  # 다음 버튼 클릭
                self.driver.find_element_by_class_name('title')  # 임의의 요소 찾아서 크롬 종료 방어
            except:
                continue
            else:
                break
 
    @QtCore.pyqtSlot(str)
    def checkTime(self, data):
        self.time = data
 
    def run(self):
        # 로그인되지 않았다면 로그인
        if not self.is_logined:
            self.login()
            self.is_logined = True
        while True:
            if str(self.set_time) == str(self.time):  # 입력한 시간하고 일치한다면
                self.start_ticketing = True
            if self.start_ticketing:
                self.selectSeat()  # 티켓팅 시작
                if not self.is_canceled_ticketing:  # 취켓팅이 아니고 끝난 경우는 성공으로 처리
                    self.start_ticketing = False
                if not self.failed_to_get_ticket:  # 취켓팅인데 성공한 경우
                    self.start_ticketing = False
                if self.is_canceled_ticketing and self.failed_to_get_ticket:  # 취켓팅인데 실패한 경우
                    pass
 
 
if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    myWindow = MyWindow()
    myWindow.show()
    app.exec_()
 
cs

일반 버전 ui 파일

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>TicketingMacro</class>
 <widget class="QMainWindow" name="TicketingMacro">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>304</width>
    <height>429</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>인터파크 티켓팅 매크로</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <widget class="QLabel" name="label_title">
      <property name="sizePolicy">
       <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
        <horstretch>0</horstretch>
        <verstretch>0</verstretch>
       </sizepolicy>
      </property>
      <property name="text">
       <string>인터파크 티켓팅 매크로</string>
      </property>
      <property name="alignment">
       <set>Qt::AlignCenter</set>
      </property>
     </widget>
    </item>
    <item>
     <widget class="QGroupBox" name="basic_data_box">
      <property name="title">
       <string>기본 정보</string>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout_4">
       <item>
        <layout class="QHBoxLayout" name="horizontalLayout_0">
         <property name="topMargin">
          <number>0</number>
         </property>
         <item>
          <widget class="QLabel" name="label_id">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>아이디:</string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QLineEdit" name="user_id_input"/>
         </item>
         <item>
          <widget class="QLabel" name="label_pw">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>비밀번호:</string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QLineEdit" name="user_password_input"/>
         </item>
        </layout>
       </item>
       <item>
        <layout class="QHBoxLayout" name="horizentalLayout_1">
         <property name="sizeConstraint">
          <enum>QLayout::SetDefaultConstraint</enum>
         </property>
         <item>
          <widget class="QLabel" name="label_pd_code">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>공연코드: </string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QLineEdit" name="product_code_input"/>
         </item>
         <item>
          <widget class="QLabel" name="label_date">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>날짜: </string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QDateEdit" name="date_edit"/>
         </item>
        </layout>
       </item>
       <item>
        <layout class="QHBoxLayout" name="horizontalLayout_2">
         <property name="topMargin">
          <number>0</number>
         </property>
         <item>
          <widget class="QLabel" name="label_seat_num">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>좌석매수(최대 4개): </string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QSpinBox" name="seats_number_spinbox"/>
         </item>
        </layout>
       </item>
       <item>
        <widget class="QCheckBox" name="use_seat_select">
         <property name="text">
          <string>중간부터 선택(랜덤)</string>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
    </item>
    <item>
     <widget class="QGroupBox" name="auto_start_box">
      <property name="title">
       <string>시간 설정</string>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout_3">
       <item>
        <widget class="QLabel" name="label_manual">
         <property name="text">
          <string>*로그인을 클릭하면 자동으로 시계가 시작합니다.
*오후 말고 항상 오전만을 선택하세요.</string>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QCheckBox" name="use_auto_start">
         <property name="text">
          <string>자동 시작 기능 사용</string>
         </property>
        </widget>
       </item>
       <item>
        <layout class="QHBoxLayout" name="horizontalLayout_3">
         <property name="topMargin">
          <number>0</number>
         </property>
         <item>
          <widget class="QTimeEdit" name="time_edit"/>
         </item>
         <item>
          <widget class="QLabel" name="label_auto_start">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>에 티켓팅을 자동 시작합니다.</string>
           </property>
          </widget>
         </item>
        </layout>
       </item>
       <item>
        <widget class="QLabel" name="now_time">
         <property name="font">
          <font>
           <pointsize>20</pointsize>
          </font>
         </property>
         <property name="text">
          <string>00 : 00 : 00</string>
         </property>
         <property name="alignment">
          <set>Qt::AlignCenter</set>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
    </item>
    <item>
     <widget class="QGroupBox" name="canceled_ticket_box">
      <property name="title">
       <string>취켓팅</string>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout_5">
       <item>
        <widget class="QCheckBox" name="canceled_ticket_mode">
         <property name="text">
          <string>취켓팅 모드</string>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
    </item>
    <item>
     <widget class="QPushButton" name="apply_btn">
      <property name="text">
       <string>확인</string>
      </property>
     </widget>
    </item>
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout_4">
      <item>
       <widget class="QPushButton" name="login_btn">
        <property name="text">
         <string>로그인</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QPushButton" name="start_ticketing_btn">
        <property name="text">
         <string>시작</string>
        </property>
       </widget>
      </item>
     </layout>
    </item>
   </layout>
  </widget>
  <action name="action">
   <property name="text">
    <string>고급 설정</string>
   </property>
  </action>
  <action name="action_2">
   <property name="text">
    <string>도움말</string>
   </property>
  </action>
 </widget>
 <resources/>
 <connections/>
</ui>
 
cs

라이트 버전

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
#인터파크 티켓팅 매크로 Light
#만든 이 : PageDown
#https://pagedown.n-e.kr/
 
 
import sys
import random
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from selenium import webdriver
 
form_class = uic.loadUiType('TicketingMacro_Light.ui')[0
 
 
class MyWindow(QtWidgets.QMainWindow, form_class):
    is_data_disabled = False  # 정보 입력창 비활성화 여부
    time = None  # 스레드로부터 받아오는 현재 시간
 
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.user_password_input.setEchoMode(QtWidgets.QLineEdit.Password)
        self.seats_number_spinbox.setValue(1)
        self.date_edit.setDate(QtCore.QDate.currentDate())
        self.time_edit.setTime(QtCore.QTime.currentTime())
        self.disableElements(self.time_edit, self.label_auto_start)
 
        self.apply_btn.clicked.connect(self.initData)  # 확인 버튼 클릭
        self.login_btn.clicked.connect(self.startLogin)  # 로그인 버튼 클릭
        self.start_ticketing_btn.clicked.connect(self.startTicketing)  # 시작 버튼 클릭
 
        self.seats_number_spinbox.valueChanged.connect(self.checkSeatsNumber)  # 좌석 개수 변경
        self.use_auto_start.stateChanged.connect(self.useAutoStart)  # 자동 시작 여부 변경
 
    def initData(self):
        if not self.is_data_disabled:  # 정보 입력창 활성화 시
            self.user_id = self.user_id_input.text()  # 아이디
            self.user_password = self.user_password_input.text()  # 비밀번호
            self.product_code = self.product_code_input.text()  # 공연 코드
            self.seats_number = self.seats_number_spinbox.value()  # 좌석 개수
            self.use_random_seat = self.use_seat_select.isChecked()  # 중간(랜덤) 선택 여부
            if self.use_auto_start.isChecked():  # 자동 시작 시 시간 24시간 방식 -> 12시간 방식 변경
                self.time = self.time_edit.time()
                self.time = str(self.time.toPyTime())[:6+ '00'
                if int(self.time[0:2]) > 12:
                    self.new_time_h = int(self.time[0:2]) - 12
                    self.time = str(self.new_time_h) + self.time[2:]
                    if self.new_time_h < 10:
                        self.time = '0' + self.time
                elif self.time[0:2== '00':
                    self.time = '12' + self.time[2:]
            self.date = self.date_edit.date()  # 날짜
            self.date = self.date.toPyDate()
            self.date = str(self.date).replace('-''')
            self.disableElements(self.user_id_input, self.user_password_input, self.product_code_input, self.date_edit,
                                 self.seats_number_spinbox, self.use_seat_select, self.use_auto_start, self.time_edit)  # 요소 비활성화
            self.is_data_disabled = True
            self.apply_btn.setText('수정')
            QtWidgets.QMessageBox.information(self, '완료''정보 입력이 완료되었습니다.')
        else:
            self.is_data_disabled = False
            self.apply_btn.setText('확인')
            self.enableElements(self.user_id_input, self.user_password_input, self.product_code_input, self.date_edit,
                                self.seats_number_spinbox, self.use_seat_select, self.use_auto_start, self.time_edit)  # 요소 활성화
 
    # 좌석 개수 변경
    def checkSeatsNumber(self):
        if self.seats_number_spinbox.value() > 4:
            self.seats_number_spinbox.setValue(4)
            QtWidgets.QMessageBox.critical(self, '오류''최대 좌석 개수는 4개입니다.')
        if self.seats_number_spinbox.value() < 1:
            self.seats_number_spinbox.setValue(1)
            QtWidgets.QMessageBox.critical(self, '오류''최소 좌석 개수는 1개입니다.')
 
    # 요소들 입력받아 한꺼번에 활성화
    def enableElements(self, *elements):
        for element in elements:
            element.setEnabled(True)
 
    # 요소들 입력받아 한꺼번에 비활성화
    def disableElements(self, *elements):
        for element in elements:
            element.setEnabled(False)
 
    # 자동시작 시
    def useAutoStart(self):
        if self.use_auto_start.isChecked():
            self.enableElements(self.time_edit, self.label_auto_start)
        else:
            self.time = None
            self.disableElements(self.time_edit, self.label_auto_start)
 
    # 로그인
    def startLogin(self):
        self.time_th = TimeThread()
        if self.apply_btn.text() == '확인':  # 확인 버튼을 누르지 않았을 때
            QtWidgets.QMessageBox.critical(self, '오류''좌석 정보를 입력하세요.')
        else:
            # Dictionary 형식으로 정보 전달
            ticketing_data_to_send = {
                'user_id': self.user_id,  # 아이디
                'user_pw': self.user_password,  # 비밀번호
                'product_code': self.product_code,  # 공연 번호
                'date': self.date,  # 공연 날짜
                'time': self.time,  # 자동시작 시간
                'seats_number': self.seats_number,  # 좌석 개수
                'use_random_seat': self.use_random_seat,  # 랜덤 선택 여부
                'time_signal': self.time_th.time_signal  # 시간 스레드
            }
            self.apply_btn.setEnabled(False)  # 확인(수정) 버튼 비활성화
            self.login_btn.setEnabled(False)  # 로그인 버튼 비활성화
            # 시간/티켓팅 스레드 시작
            self.time_th.start()
            self.time_th.time_signal.connect(self.changeTime)
            self.ticketing_th = TicketingThread(ticketing_data=ticketing_data_to_send)
            self.ticketing_th.start()
 
    # 티켓팅 시작
    def startTicketing(self):
        self.ticketing_th.start_ticketing = True
 
    # 타이머 시작
    def startTimer(self):
        self.time_th = TimeThread()
        self.time_th.start()
        self.time_th.time_signal.connect(self.changeTime)
 
    @QtCore.pyqtSlot(str)
    def changeTime(self, time):
        if time == 'error':  # 웹드라이버(시계) 종료시 알림
            QtWidgets.QMessageBox.critical(self, '오류''타이머를 끄지 마십시오.')
        else:
            self.now_time.setText(time)
 
 
class TimeThread(QtCore.QThread):
    time_signal = QtCore.pyqtSignal(str)
 
    def run(self):
        while True:
            self.driver = webdriver.Chrome('chromedriver.exe')
            self.driver.get('https://search.naver.com/search.naver?sm=tab_hty.top&where=nexearch&query=%EB%84%A4%EC%9D%B4%EB%B2%84+%EC%8B%9C%EA%B3%84&oquery=%EC%8B%9C%EA%B3%84&tqi=UEMiLdprvTossFQzFhCssssssKG-165498')
            while True:
                try:
                    text = self.driver.find_element_by_css_selector('#_cs_domestic_clock > div._timeLayer.time_bx > div > div').text  # 네이버 시계 text
                    text = text.replace('\n''').replace(' ''')[0:8]
                except:
                    self.time_signal.emit('error')  # 웹드라이버(시계) 종료 시 error emit
                    break
                self.time_signal.emit(''.join(text))
 
 
class TicketingThread(QtCore.QThread):
    def __init__(self, ticketing_data, parent=None):
        QtCore.QThread.__init__(self, parent)
        self.user_id = ticketing_data['user_id']
        self.user_password = ticketing_data['user_pw']
        self.product_code = ticketing_data['product_code']
        self.date = ticketing_data['date']
        self.set_time = ticketing_data['time']
        self.seats_number = ticketing_data['seats_number']
        self.use_random_seat = ticketing_data['use_random_seat']
        self.time_signal = ticketing_data['time_signal']
        self.driver = self.driver = webdriver.Chrome('chromedriver.exe')
        self.time_signal.connect(self.checkTime)  # 시간 스레드와 연결(자동시작)
        self.is_logined = False  # 로그인 여부 False
        self.start_ticketing = False  # 시작 여부 False
 
    # 요소는 반드시 CSS 셀렉터로만(ID 우선)
 
    # 로그인
    def login(self):
        self.driver.get('https://ticket.interpark.com/Gate/TPLogin.asp')
        iframes = self.driver.find_elements_by_tag_name('iframe')
        self.driver.switch_to.frame(iframes[0])
        self.driver.find_element_by_id('userId').send_keys(self.user_id)
        self.driver.find_element_by_id('userPwd').send_keys(self.user_password)
        self.driver.find_element_by_id('btn_login').click()
        self.driver.get('https://ticket.interpark.com/')  # 인터파크 메인 페이지로 강제 접속
 
    def selectSeat(self):
        # 직링 생성
        self.url = 'http://poticket.interpark.com/Book/BookSession.asp?GroupCode={}&Tiki=N&Point=N&PlayDate={}&PlaySeq=001&BizCode=&BizMemberCode='.format(
            self.product_code, self.date)
        self.driver.get(self.url)
 
        # 요소를 찾을 때까지 무한 루프(찾지 못하면 예외)
        while True:
            try:
                first_iframe = self.driver.find_element_by_id('ifrmSeat')  # 첫번째 아이프레임
            except:
                continue
            else:
                break
        self.driver.switch_to.frame(first_iframe)
        while True:
            try:
                next_iframe = self.driver.find_element_by_id('ifrmSeatDetail')  # 두번째 좌석선택 아이프레임
            except:
                continue
            else:
                break
        self.driver.switch_to.frame(next_iframe)
 
        while True:
            try:
                elements = self.driver.find_elements_by_class_name('stySeat')
            except:
                continue
            else:
                if len(elements) == 0:  # 예외는 나지 않았지만 좌석이 인식되지 못한 경우
                    continue
                else:
                    break
 
        while len(elements) > 0:
            # 첫번째부터 1개 선택
            if (not self.use_random_seat) and (self.seats_number == 1):
                self.driver.find_element_by_css_selector('#TmgsTable > tbody > tr > td > img:nth-child(3)').click()  # 바로 첫번째 요소 선택
 
            # 여러개 선택
            elif not self.use_random_seat:
                seat_count = 0
                for element in elements:
                    if seat_count < self.seats_number:
                        element.click()
                        seat_count += 1
 
            # 랜덤/여러개 선택
            elif self.use_random_seat:
                start_seat = random.randint(
                    int(len(elements) / 2), len(elements) - self.seats_number)
                for i in range(start_seat, start_seat + self.seats_number):
                    if start_seat < self.seats_number + start_seat:
                        elements[i].click()
            try:
                self.driver.switch_to.default_content()  # 제일 바깥으로 아이프레임 벗어남
                self.driver.switch_to.frame(first_iframe)  # 첫번째 아이프레임으로 다시 변경
                self.driver.find_element_by_id('NextStepImage').click()  # 다음 버튼 클릭
                self.driver.find_element_by_class_name('title')  # 임의의 요소 찾아서 크롬 종료 방어
            except:
                continue
            else:
                break
 
    @QtCore.pyqtSlot(str)
    def checkTime(self, data):
        self.time = data
 
    def run(self):
        # 로그인되지 않았다면 로그인
        if not self.is_logined:
            self.login()
            self.is_logined = True
        while True:
            if str(self.set_time) == str(self.time):  # 입력한 시간하고 일치한다면
                self.start_ticketing = True
            if self.start_ticketing:
                self.selectSeat()  # 티켓팅 시작
                self.start_ticketing = False  # 성공으로 처리
 
 
if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    myWindow = MyWindow()
    myWindow.show()
    app.exec_()
 
cs

라이트 버전 ui 파일

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>TicketingMacro_Light</class>
 <widget class="QMainWindow" name="TicketingMacro_Light">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>304</width>
    <height>375</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>인터파크 티켓팅 매크로 Light</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <widget class="QLabel" name="label_title">
      <property name="sizePolicy">
       <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
        <horstretch>0</horstretch>
        <verstretch>0</verstretch>
       </sizepolicy>
      </property>
      <property name="text">
       <string>인터파크 티켓팅 매크로</string>
      </property>
      <property name="alignment">
       <set>Qt::AlignCenter</set>
      </property>
     </widget>
    </item>
    <item>
     <widget class="QGroupBox" name="basic_data_box">
      <property name="title">
       <string>기본 정보</string>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout_4">
       <item>
        <layout class="QHBoxLayout" name="horizontalLayout_0">
         <property name="topMargin">
          <number>0</number>
         </property>
         <item>
          <widget class="QLabel" name="label_id">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>아이디:</string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QLineEdit" name="user_id_input"/>
         </item>
         <item>
          <widget class="QLabel" name="label_pw">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>비밀번호:</string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QLineEdit" name="user_password_input"/>
         </item>
        </layout>
       </item>
       <item>
        <layout class="QHBoxLayout" name="horizentalLayout_1">
         <property name="sizeConstraint">
          <enum>QLayout::SetDefaultConstraint</enum>
         </property>
         <item>
          <widget class="QLabel" name="label_pd_code">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>공연코드: </string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QLineEdit" name="product_code_input"/>
         </item>
         <item>
          <widget class="QLabel" name="label_date">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>날짜: </string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QDateEdit" name="date_edit"/>
         </item>
        </layout>
       </item>
       <item>
        <layout class="QHBoxLayout" name="horizontalLayout_2">
         <property name="topMargin">
          <number>0</number>
         </property>
         <item>
          <widget class="QLabel" name="label_seat_num">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>좌석매수(최대 4개): </string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QSpinBox" name="seats_number_spinbox"/>
         </item>
        </layout>
       </item>
       <item>
        <widget class="QCheckBox" name="use_seat_select">
         <property name="text">
          <string>중간부터 선택(랜덤)</string>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
    </item>
    <item>
     <widget class="QGroupBox" name="auto_start_box">
      <property name="title">
       <string>시간 설정</string>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout_3">
       <item>
        <widget class="QLabel" name="label_manual">
         <property name="text">
          <string>*로그인을 클릭하면 자동으로 시계가 시작합니다.
*오후 말고 항상 오전만을 선택하세요.</string>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QCheckBox" name="use_auto_start">
         <property name="text">
          <string>자동 시작 기능 사용</string>
         </property>
        </widget>
       </item>
       <item>
        <layout class="QHBoxLayout" name="horizontalLayout_3">
         <property name="topMargin">
          <number>0</number>
         </property>
         <item>
          <widget class="QTimeEdit" name="time_edit"/>
         </item>
         <item>
          <widget class="QLabel" name="label_auto_start">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>에 티켓팅을 자동 시작합니다.</string>
           </property>
          </widget>
         </item>
        </layout>
       </item>
       <item>
        <widget class="QLabel" name="now_time">
         <property name="font">
          <font>
           <pointsize>20</pointsize>
          </font>
         </property>
         <property name="text">
          <string>00 : 00 : 00</string>
         </property>
         <property name="alignment">
          <set>Qt::AlignCenter</set>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
    </item>
    <item>
     <widget class="QPushButton" name="apply_btn">
      <property name="text">
       <string>확인</string>
      </property>
     </widget>
    </item>
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout_4">
      <item>
       <widget class="QPushButton" name="login_btn">
        <property name="text">
         <string>로그인</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QPushButton" name="start_ticketing_btn">
        <property name="text">
         <string>시작</string>
        </property>
       </widget>
      </item>
     </layout>
    </item>
   </layout>
  </widget>
  <action name="action">
   <property name="text">
    <string>고급 설정</string>
   </property>
  </action>
  <action name="action_2">
   <property name="text">
    <string>도움말</string>
   </property>
  </action>
 </widget>
 <resources/>
 <connections/>
</ui>
cs

+ Recent posts