opensoma 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +1 -1
- package/dist/src/commands/auth.d.ts +1 -1
- package/dist/src/commands/auth.d.ts.map +1 -1
- package/dist/src/commands/auth.js +94 -52
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/constants.d.ts +40 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +42 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/formatters.d.ts.map +1 -1
- package/dist/src/formatters.js +42 -16
- package/dist/src/formatters.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/shared/utils/toz.d.ts +23 -0
- package/dist/src/shared/utils/toz.d.ts.map +1 -0
- package/dist/src/shared/utils/toz.js +72 -0
- package/dist/src/shared/utils/toz.js.map +1 -0
- package/dist/src/token-extractor.d.ts +9 -1
- package/dist/src/token-extractor.d.ts.map +1 -1
- package/dist/src/token-extractor.js +54 -10
- package/dist/src/token-extractor.js.map +1 -1
- package/dist/src/toz-formatters.d.ts +9 -0
- package/dist/src/toz-formatters.d.ts.map +1 -0
- package/dist/src/toz-formatters.js +151 -0
- package/dist/src/toz-formatters.js.map +1 -0
- package/dist/src/toz-http.d.ts +27 -0
- package/dist/src/toz-http.d.ts.map +1 -0
- package/dist/src/toz-http.js +154 -0
- package/dist/src/toz-http.js.map +1 -0
- package/dist/src/types.d.ts +52 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +46 -0
- package/dist/src/types.js.map +1 -1
- package/package.json +1 -1
- package/src/__fixtures__/toz/toz_all_branches.json +211 -0
- package/src/__fixtures__/toz/toz_booking.html +2190 -0
- package/src/__fixtures__/toz/toz_boothes.json +59 -0
- package/src/__fixtures__/toz/toz_duration.json +25 -0
- package/src/__fixtures__/toz/toz_mypage_response.html +388 -0
- package/src/__fixtures__/toz/toz_page.html +211 -0
- package/src/commands/auth.ts +107 -50
- package/src/constants.ts +50 -0
- package/src/formatters.ts +44 -16
- package/src/index.ts +3 -0
- package/src/shared/utils/toz.test.ts +133 -0
- package/src/shared/utils/toz.ts +100 -0
- package/src/token-extractor.test.ts +30 -5
- package/src/token-extractor.ts +65 -13
- package/src/toz-formatters.test.ts +197 -0
- package/src/toz-formatters.ts +211 -0
- package/src/toz-http.test.ts +157 -0
- package/src/toz-http.ts +188 -0
- package/src/types.ts +58 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
5
|
+
<title>토즈</title>
|
|
6
|
+
|
|
7
|
+
<script type="text/javascript" src="/js/jquery-1.4.4.min.js"></script>
|
|
8
|
+
<script type="text/javascript" src="/js/jquery-ui-1.8.9.custom.min.js"></script>
|
|
9
|
+
<script type="text/javascript" src="/js/jquery.ui.datepicker-ko.js"></script>
|
|
10
|
+
<link type="text/css" href="/css/blitzer/jquery-ui-1.8.13.custom.css" rel="stylesheet" />
|
|
11
|
+
<link type="text/css" href="/css/partnerReservation.css" rel="stylesheet" />
|
|
12
|
+
<style>
|
|
13
|
+
.header {
|
|
14
|
+
background: #2c2c2c;
|
|
15
|
+
height: 80px;
|
|
16
|
+
border-bottom: 5px solid #ccc;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.header .logo {
|
|
20
|
+
position: relative;
|
|
21
|
+
width: 950px;
|
|
22
|
+
margin: 0 auto;
|
|
23
|
+
padding: 40px 0 0 0;
|
|
24
|
+
height: 40px;
|
|
25
|
+
}
|
|
26
|
+
.header .partner_logo {
|
|
27
|
+
color: white;
|
|
28
|
+
font-weight: bold;
|
|
29
|
+
font-size: 17px;
|
|
30
|
+
letter-spacing: -1px;
|
|
31
|
+
position: absolute;
|
|
32
|
+
top: 0;
|
|
33
|
+
right: 0;
|
|
34
|
+
padding: 50px 0 0 0;
|
|
35
|
+
height: 40px;
|
|
36
|
+
}
|
|
37
|
+
ul.btn-list {
|
|
38
|
+
position: relative;
|
|
39
|
+
list-style: none;
|
|
40
|
+
margin: 0;
|
|
41
|
+
padding: 0;
|
|
42
|
+
height: 100%;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ul.btn-list li {
|
|
46
|
+
position: relative;
|
|
47
|
+
margin: 0;
|
|
48
|
+
text-align: left;
|
|
49
|
+
width: 100%;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
ul.btn-list li.btn-list-1 {
|
|
53
|
+
padding: 42px 0px;
|
|
54
|
+
}
|
|
55
|
+
ul.btn-list li.btn-list-3 {
|
|
56
|
+
padding: 42px 0px;
|
|
57
|
+
}
|
|
58
|
+
ul.btn-list li.btn-list-4 {
|
|
59
|
+
padding: 49px 0px;
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
62
|
+
</head>
|
|
63
|
+
<body>
|
|
64
|
+
<!-- top -->
|
|
65
|
+
<div class="header" style="padding-bottom: 6px">
|
|
66
|
+
<div class="logo" style="padding-bottom: 6px">
|
|
67
|
+
<div>
|
|
68
|
+
<a href="/partner/reservation/fkii3/swmaestro/index.htm?key=&projectSeq=&addedInfo=&tozApplyType="
|
|
69
|
+
><img src="/images/partner/toz_logo.png"
|
|
70
|
+
/></a>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="partner_logo">
|
|
73
|
+
기업전용 문의접수: <a href="mailto: b2bsb@toz.co.kr" style="color: white">b2bsb@toz.co.kr</a>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<div style="display: flex; flex-direction: row; justify-content: center; margin: -5px 0 0 0">
|
|
78
|
+
<div
|
|
79
|
+
class="layoutBody"
|
|
80
|
+
style="
|
|
81
|
+
position: relative;
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-direction: column;
|
|
84
|
+
justify-content: center;
|
|
85
|
+
border-top: 5px solid #b00e33;
|
|
86
|
+
"
|
|
87
|
+
>
|
|
88
|
+
<div style="display: flex; flex-direction: row; width: 950px; border-bottom: 2px solid #d03f5f">
|
|
89
|
+
<div style="position: relative; width: 700px; z-index: 1">
|
|
90
|
+
<div style="padding-left: 30px; width: 380px; word-break: keep-all">
|
|
91
|
+
<p style="font-weight: bold; color: #890624; font-size: 18px; margin: 20px 0 10px 0">
|
|
92
|
+
(필독) 외부 회의실 예약 및 이용시 주의사항
|
|
93
|
+
</p>
|
|
94
|
+
<p></p>
|
|
95
|
+
<p style="font-size: 13px; color: #939393; font-weight: bold; line-height: 25px">
|
|
96
|
+
· 최소 2시간 부터 사용 가능하며 최대 3시간까지 사용합니다.<br />
|
|
97
|
+
· 예약 후 멘토링/특강 일정 변경시 반드시 예약 취소합니다.<br />
|
|
98
|
+
· 본인의 예약 미취소로 인해 발생한 비용은 개인에게<br />
|
|
99
|
+
<span style="display: inline-block; padding-left: 10px">직접 청구 될 수 있습니다.</span><br />
|
|
100
|
+
· 이용자(연수생/엑스퍼트/멘토) 경고에 해당하는 경우<br />
|
|
101
|
+
<span style="display: inline-block; padding-left: 14px">(1) 예약 후 예약 취소 없이 일방적인 노쇼</span
|
|
102
|
+
><br />
|
|
103
|
+
<span style="display: inline-block; padding-left: 14px"
|
|
104
|
+
>(2) 멘토-연수생, 엑스퍼트-연수생과의 활동이 아닌</span
|
|
105
|
+
><br />
|
|
106
|
+
<span style="display: inline-block; padding-left: 36px">목적 외 활동을 한 경우</span><br />
|
|
107
|
+
<span style="display: inline-block; padding-left: 14px">(3) 기타 부적합한 용도로 사용한 경우</span>
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div style="position: absolute; bottom: 10px; left: 40px">
|
|
112
|
+
<div class="companyName" style="padding: 5px 0">
|
|
113
|
+
<span style="font-weight: bold; color: #2c2c2c; border-bottom: 1px solid #939393; padding-bottom: 2px"
|
|
114
|
+
>AI‧SW마에스트로</span
|
|
115
|
+
>
|
|
116
|
+
</div>
|
|
117
|
+
<div
|
|
118
|
+
class="text"
|
|
119
|
+
style="padding: 5px 0; font-size: 14px; font-weight: bold; color: #939393; letter-spacing: -1px"
|
|
120
|
+
>
|
|
121
|
+
제휴내용은
|
|
122
|
+
</div>
|
|
123
|
+
<div style="padding: 5px 0">
|
|
124
|
+
<span class="desc" style="color: #890624; border-bottom: 1px solid #939393; padding-bottom: 2px"
|
|
125
|
+
>20% 할인</span
|
|
126
|
+
><span class="text"> 입니다.</span>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div style="position: relative; width: 250px; height: 388px">
|
|
131
|
+
<img src="/images/partner/deco_toz_main_1.png" style="position: absolute; bottom: 0px; right: 250px" />
|
|
132
|
+
<ul class="btn-list">
|
|
133
|
+
<li class="btn-list-1" style="background: #890624">
|
|
134
|
+
<a href="/partner/reservation/fkii3/swmaestro/booking.htm?key=&projectSeq=&addedInfo=&tozApplyType="
|
|
135
|
+
><img alt="예약하기" src="/images/partner/btn_toz_main_1.png" style="margin-left: 30px"
|
|
136
|
+
/></a>
|
|
137
|
+
</li>
|
|
138
|
+
|
|
139
|
+
<li class="btn-list-3" style="background: #c42649">
|
|
140
|
+
<a href="/partner/reservation/fkii3/swmaestro/startmypage.htm?key=&projectSeq=&addedInfo=&tozApplyType="
|
|
141
|
+
><img alt="예약확인하기" src="/images/partner/btn_toz_main_3.png" style="margin-left: 30px"
|
|
142
|
+
/></a>
|
|
143
|
+
</li>
|
|
144
|
+
<li class="btn-list-4" style="background: #d03f5f">
|
|
145
|
+
<a href="https://work.toz.co.kr/branchSearch?page=1&onesBranchType=TMC" target="_blank"
|
|
146
|
+
><img alt="토즈모임센터 지점안내" src="/images/partner/btn_toz_main_4.png" style="margin-left: 30px"
|
|
147
|
+
/></a>
|
|
148
|
+
</li>
|
|
149
|
+
</ul>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div
|
|
154
|
+
style="
|
|
155
|
+
display: flex;
|
|
156
|
+
flex-direction: row;
|
|
157
|
+
justify-content: center;
|
|
158
|
+
position: relative;
|
|
159
|
+
width: 950px;
|
|
160
|
+
margin: 0 auto;
|
|
161
|
+
text-align: center;
|
|
162
|
+
border-bottom: 2px solid #d03f5f;
|
|
163
|
+
"
|
|
164
|
+
>
|
|
165
|
+
<img src="/images/partner/btn_link_map.png" usemap="#map_link" style="width: 808px; height: 168px" />
|
|
166
|
+
<map name="map_link">
|
|
167
|
+
<area
|
|
168
|
+
shape="rect"
|
|
169
|
+
coords="10,30,210,160"
|
|
170
|
+
href="https://moim.toz.co.kr"
|
|
171
|
+
title="토즈 서비스 그룹"
|
|
172
|
+
target="_blank"
|
|
173
|
+
/>
|
|
174
|
+
<area
|
|
175
|
+
shape="rect"
|
|
176
|
+
coords="280,30,510,160"
|
|
177
|
+
href="https://moim.toz.co.kr/customerCenter/posts/all"
|
|
178
|
+
title="토즈 고객센터"
|
|
179
|
+
target="_blank"
|
|
180
|
+
/>
|
|
181
|
+
<area
|
|
182
|
+
shape="rect"
|
|
183
|
+
coords="570,25,810,160"
|
|
184
|
+
href="https://moim.toz.co.kr/customerCenter/posts/event"
|
|
185
|
+
title="토즈 이벤트"
|
|
186
|
+
target="_blank"
|
|
187
|
+
/>
|
|
188
|
+
</map>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<!-- google analytics for (http)partner.toz.co.kr -->
|
|
194
|
+
<script type="text/javascript">
|
|
195
|
+
var _gaq = _gaq || []
|
|
196
|
+
_gaq.push(['_setAccount', 'UA-23075111-3'])
|
|
197
|
+
_gaq.push(['_trackPageview'])
|
|
198
|
+
|
|
199
|
+
;(function () {
|
|
200
|
+
var ga = document.createElement('script')
|
|
201
|
+
ga.type = 'text/javascript'
|
|
202
|
+
ga.async = true
|
|
203
|
+
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'
|
|
204
|
+
var s = document.getElementsByTagName('script')[0]
|
|
205
|
+
s.parentNode.insertBefore(ga, s)
|
|
206
|
+
})()
|
|
207
|
+
</script>
|
|
208
|
+
<!-- // google analytics for (http)partner.toz.co.kr -->
|
|
209
|
+
<!-- // bottom -->
|
|
210
|
+
</body>
|
|
211
|
+
</html>
|
package/src/commands/auth.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createInterface, type Interface as ReadlineInterface } from 'node:readline'
|
|
2
|
+
|
|
1
3
|
import { Command } from 'commander'
|
|
2
4
|
|
|
3
5
|
import { CredentialManager } from '../credential-manager'
|
|
@@ -7,63 +9,99 @@ import { handleError } from '../shared/utils/error-handler'
|
|
|
7
9
|
import { formatOutput } from '../shared/utils/output'
|
|
8
10
|
import type { ExtractedSessionCandidate } from '../token-extractor'
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
function ask(rl: ReadlineInterface, message: string): Promise<string> {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question(message, (answer) => {
|
|
15
|
+
resolve(answer.trim())
|
|
16
|
+
})
|
|
17
|
+
})
|
|
14
18
|
}
|
|
15
19
|
|
|
16
|
-
async function
|
|
20
|
+
async function promptPasswordTTY(message: string): Promise<string> {
|
|
17
21
|
process.stdout.write(message)
|
|
18
22
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (originalStdin?.setRawMode) {
|
|
24
|
-
originalStdin.setRawMode(true)
|
|
23
|
+
const stdin = process.stdin as typeof process.stdin & { setRawMode?: (mode: boolean) => void }
|
|
24
|
+
if (stdin.setRawMode) {
|
|
25
|
+
stdin.setRawMode(true)
|
|
25
26
|
}
|
|
27
|
+
stdin.resume()
|
|
26
28
|
|
|
27
29
|
let password = ''
|
|
28
|
-
const decoder = new TextDecoder()
|
|
29
30
|
|
|
30
31
|
try {
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
32
|
+
return await new Promise<string>((resolve) => {
|
|
33
|
+
const onData = (chunk: Buffer | string): void => {
|
|
34
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8')
|
|
35
|
+
for (const char of text) {
|
|
36
|
+
const code = char.charCodeAt(0)
|
|
37
|
+
if (code === 13 || code === 10) {
|
|
38
|
+
// Enter key
|
|
39
|
+
stdin.removeListener('data', onData)
|
|
40
|
+
process.stdout.write('\n')
|
|
41
|
+
resolve(password)
|
|
42
|
+
return
|
|
43
|
+
} else if (code === 3) {
|
|
44
|
+
// Ctrl+C
|
|
45
|
+
stdin.removeListener('data', onData)
|
|
46
|
+
process.exit(1)
|
|
47
|
+
} else if (code === 127 || code === 8) {
|
|
48
|
+
// Backspace / Delete
|
|
49
|
+
if (password.length > 0) {
|
|
50
|
+
password = password.slice(0, -1)
|
|
51
|
+
process.stdout.write('\b \b')
|
|
52
|
+
}
|
|
53
|
+
} else if (code >= 32 && code <= 126) {
|
|
54
|
+
// Printable characters
|
|
55
|
+
password += char
|
|
56
|
+
process.stdout.write('*')
|
|
47
57
|
}
|
|
48
|
-
} else if (code >= 32 && code <= 126) {
|
|
49
|
-
// Printable characters
|
|
50
|
-
password += char
|
|
51
|
-
process.stdout.write('*')
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
|
-
|
|
60
|
+
|
|
61
|
+
stdin.on('data', onData)
|
|
62
|
+
})
|
|
55
63
|
} finally {
|
|
56
|
-
if (
|
|
57
|
-
|
|
64
|
+
if (stdin.setRawMode) {
|
|
65
|
+
stdin.setRawMode(false)
|
|
58
66
|
}
|
|
67
|
+
stdin.pause()
|
|
59
68
|
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function promptCredentials(
|
|
72
|
+
needUsername: boolean,
|
|
73
|
+
needPassword: boolean,
|
|
74
|
+
): Promise<{ username?: string; password?: string }> {
|
|
75
|
+
const result: { username?: string; password?: string } = {}
|
|
60
76
|
|
|
61
|
-
|
|
77
|
+
if (needUsername) {
|
|
78
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false })
|
|
79
|
+
try {
|
|
80
|
+
result.username = await ask(rl, 'Username: ')
|
|
81
|
+
} finally {
|
|
82
|
+
rl.close()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (needPassword) {
|
|
87
|
+
if (process.stdin.isTTY) {
|
|
88
|
+
result.password = await promptPasswordTTY('Password: ')
|
|
89
|
+
} else {
|
|
90
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false })
|
|
91
|
+
try {
|
|
92
|
+
result.password = await ask(rl, 'Password: ')
|
|
93
|
+
} finally {
|
|
94
|
+
rl.close()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result
|
|
62
100
|
}
|
|
63
101
|
|
|
64
102
|
type LoginOptions = { username?: string; password?: string; pretty?: boolean }
|
|
65
103
|
type StatusOptions = { pretty?: boolean }
|
|
66
|
-
type ExtractOptions = { pretty?: boolean }
|
|
104
|
+
type ExtractOptions = { debug?: boolean; pretty?: boolean }
|
|
67
105
|
type ExtractedSessionValidator = Pick<SomaHttp, 'checkLogin' | 'extractCsrfToken'>
|
|
68
106
|
type CredentialStore = Pick<CredentialManager, 'getCredentials' | 'remove' | 'setCredentials'>
|
|
69
107
|
type StatusValidator = Pick<SomaHttp, 'checkLogin'>
|
|
@@ -87,21 +125,34 @@ export async function resolveExtractedCredentials(
|
|
|
87
125
|
candidates: ExtractedSessionCandidate[],
|
|
88
126
|
createValidator: (sessionCookie: string) => ExtractedSessionValidator = (sessionCookie) =>
|
|
89
127
|
new SomaHttp({ sessionCookie }),
|
|
128
|
+
debug?: (message: string) => void,
|
|
90
129
|
): Promise<{ csrfToken: string; sessionCookie: string } | null> {
|
|
130
|
+
debug?.(`Validating ${candidates.length} candidate(s) against server...`)
|
|
131
|
+
|
|
91
132
|
for (const candidate of candidates) {
|
|
92
133
|
const http = createValidator(candidate.sessionCookie)
|
|
134
|
+
debug?.(` ${candidate.browser} / ${candidate.profile}: checkLogin...`)
|
|
93
135
|
|
|
94
136
|
try {
|
|
95
137
|
const valid = Boolean(await http.checkLogin())
|
|
96
138
|
if (!valid) {
|
|
139
|
+
debug?.(` ${candidate.browser} / ${candidate.profile}: session invalid`)
|
|
97
140
|
continue
|
|
98
141
|
}
|
|
99
142
|
|
|
143
|
+
debug?.(` ${candidate.browser} / ${candidate.profile}: valid! Extracting CSRF token...`)
|
|
144
|
+
const csrfToken = await http.extractCsrfToken()
|
|
145
|
+
debug?.(` CSRF token obtained (${csrfToken.length} chars)`)
|
|
146
|
+
|
|
100
147
|
return {
|
|
101
148
|
sessionCookie: candidate.sessionCookie,
|
|
102
|
-
csrfToken
|
|
149
|
+
csrfToken,
|
|
103
150
|
}
|
|
104
|
-
} catch {
|
|
151
|
+
} catch (error) {
|
|
152
|
+
debug?.(
|
|
153
|
+
` ${candidate.browser} / ${candidate.profile}: error: ${error instanceof Error ? error.message : String(error)}`,
|
|
154
|
+
)
|
|
155
|
+
}
|
|
105
156
|
}
|
|
106
157
|
|
|
107
158
|
return null
|
|
@@ -112,12 +163,9 @@ async function loginAction(options: LoginOptions): Promise<void> {
|
|
|
112
163
|
let username = options.username ?? process.env.OPENSOMA_USERNAME
|
|
113
164
|
let password = options.password ?? process.env.OPENSOMA_PASSWORD
|
|
114
165
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (!password) {
|
|
119
|
-
password = await promptPassword('Password: ')
|
|
120
|
-
}
|
|
166
|
+
const prompted = await promptCredentials(!username, !password)
|
|
167
|
+
username ??= prompted.username
|
|
168
|
+
password ??= prompted.password
|
|
121
169
|
|
|
122
170
|
if (!username || !password) {
|
|
123
171
|
throw new Error('Username and password are required')
|
|
@@ -264,22 +312,30 @@ async function statusAction(options: StatusOptions): Promise<void> {
|
|
|
264
312
|
}
|
|
265
313
|
|
|
266
314
|
async function extractAction(options: ExtractOptions): Promise<void> {
|
|
315
|
+
const log = options.debug ? (message: string) => process.stderr.write(`[extract] ${message}\n`) : undefined
|
|
316
|
+
|
|
267
317
|
try {
|
|
268
318
|
const { TokenExtractor } = (await import('../token-extractor')) as {
|
|
269
|
-
TokenExtractor: new (
|
|
319
|
+
TokenExtractor: new (options?: { debug?: boolean }) => {
|
|
320
|
+
extractCandidates: () => Promise<ExtractedSessionCandidate[]>
|
|
321
|
+
}
|
|
270
322
|
}
|
|
271
|
-
const extractor = new TokenExtractor()
|
|
323
|
+
const extractor = new TokenExtractor({ debug: options.debug })
|
|
272
324
|
const candidates = await extractor.extractCandidates()
|
|
273
325
|
if (candidates.length === 0) {
|
|
274
326
|
throw new Error(
|
|
275
|
-
'No SWMaestro session found in any browser. Login to swmaestro.ai in a supported Chromium browser (Chrome, Edge, Brave, Arc, Vivaldi) first.',
|
|
327
|
+
'No SWMaestro session found in any browser. Login to swmaestro.ai or opensoma.dev in a supported Chromium browser (Chrome, Edge, Brave, Arc, Vivaldi) first.',
|
|
276
328
|
)
|
|
277
329
|
}
|
|
278
330
|
|
|
279
|
-
|
|
331
|
+
log?.(
|
|
332
|
+
`Extracted ${candidates.length} candidate(s): ${candidates.map((c) => `${c.browser}/${c.profile}`).join(', ')}`,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
const credentials = await resolveExtractedCredentials(candidates, undefined, log)
|
|
280
336
|
if (!credentials) {
|
|
281
337
|
throw new Error(
|
|
282
|
-
'Found SWMaestro session cookies in supported browsers, but none are valid. Refresh your swmaestro.ai login in a supported Chromium browser and try again.',
|
|
338
|
+
'Found SWMaestro session cookies in supported browsers, but none are valid. Refresh your swmaestro.ai or opensoma.dev login in a supported Chromium browser and try again.',
|
|
283
339
|
)
|
|
284
340
|
}
|
|
285
341
|
|
|
@@ -319,6 +375,7 @@ export const authCommand = new Command('auth')
|
|
|
319
375
|
.addCommand(
|
|
320
376
|
new Command('extract')
|
|
321
377
|
.description('Extract browser credentials')
|
|
378
|
+
.option('--debug', 'Show debug output')
|
|
322
379
|
.option('--pretty', 'Pretty print JSON output')
|
|
323
380
|
.action(extractAction),
|
|
324
381
|
)
|
package/src/constants.ts
CHANGED
|
@@ -85,3 +85,53 @@ function createTimeSlots(): string[] {
|
|
|
85
85
|
function formatTime(hour: number, minute: number): string {
|
|
86
86
|
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
export const TOZ_BASE_URL = 'http://partner.toz.co.kr/partner/reservation/fkii3/swmaestro'
|
|
90
|
+
export const TOZ_PARTNER = 'fkii3'
|
|
91
|
+
export const TOZ_COMPANY = 'swmaestro'
|
|
92
|
+
export const TOZ_MEMBER_COMPANY_ID = '25408'
|
|
93
|
+
|
|
94
|
+
// Static fallback list extracted from booking.htm. Use TozClient.branches() at runtime
|
|
95
|
+
// to fetch the live list — the SW마에스트로 partnership branch set may change.
|
|
96
|
+
export const TOZ_BRANCHES = [
|
|
97
|
+
{ id: 27, name: '강남토즈타워점' },
|
|
98
|
+
{ id: 145, name: '강남컨퍼런스센터' },
|
|
99
|
+
{ id: 19, name: '양재점' },
|
|
100
|
+
{ id: 20, name: '건대점' },
|
|
101
|
+
{ id: 15, name: '선릉점' },
|
|
102
|
+
{ id: 139, name: '마이스 역삼센터' },
|
|
103
|
+
{ id: 134, name: '마이스 광화문센터' },
|
|
104
|
+
{ id: 30, name: '신촌비즈센터' },
|
|
105
|
+
{ id: 149, name: '홍대점' },
|
|
106
|
+
] as const
|
|
107
|
+
|
|
108
|
+
export const TOZ_PHONE_PREFIXES = ['010', '011', '016', '017', '018', '019'] as const
|
|
109
|
+
|
|
110
|
+
export const TOZ_EMAIL_DOMAINS = [
|
|
111
|
+
'hanmail.net',
|
|
112
|
+
'gmail.com',
|
|
113
|
+
'nate.com',
|
|
114
|
+
'naver.com',
|
|
115
|
+
'daum.net',
|
|
116
|
+
'dreamwiz.com',
|
|
117
|
+
'yahoo.com',
|
|
118
|
+
'yahoo.co.kr',
|
|
119
|
+
'msn.com',
|
|
120
|
+
'paran.com',
|
|
121
|
+
'korea.com',
|
|
122
|
+
'freechal.com',
|
|
123
|
+
'lycos.co.kr',
|
|
124
|
+
'msn.co.kr',
|
|
125
|
+
'empal.com',
|
|
126
|
+
'hotmail.com',
|
|
127
|
+
] as const
|
|
128
|
+
|
|
129
|
+
export const TOZ_EMAIL_DOMAIN_CUSTOM = '직접입력'
|
|
130
|
+
|
|
131
|
+
export const TOZ_NEW_MEETING_VALUE = '새모임'
|
|
132
|
+
|
|
133
|
+
export const TOZ_MIN_DURATION_MINUTES = 120
|
|
134
|
+
export const TOZ_MAX_DURATION_MINUTES = 180
|
|
135
|
+
export const TOZ_SESSION_HOLD_SECONDS = 300
|
|
136
|
+
|
|
137
|
+
export const TOZ_MAX_CHECK_TIMES = 6
|
package/src/formatters.ts
CHANGED
|
@@ -299,12 +299,40 @@ export function parseReportList(html: string): ReportListItem[] {
|
|
|
299
299
|
|
|
300
300
|
export function parseReportDetail(html: string, id = 0): ReportDetail {
|
|
301
301
|
const root = parse(html)
|
|
302
|
-
const labels = extractLabelMap(root)
|
|
302
|
+
const labels = { ...extractLabelMap(root), ...extractGroupMap(root) }
|
|
303
303
|
|
|
304
304
|
const progressTimeText = labels['진행시간'] || ''
|
|
305
305
|
const exceptTimeText = labels['제외시간'] || ''
|
|
306
|
-
const
|
|
307
|
-
const
|
|
306
|
+
const progressTimeMatch = progressTimeText.match(/(\d{2}:\d{2})\s*~\s*(\d{2}:\d{2})/)
|
|
307
|
+
const exceptTimeMatch = exceptTimeText.match(/(\d{2}:\d{2})\s*~\s*(\d{2}:\d{2})/)
|
|
308
|
+
|
|
309
|
+
let subject = labels['주제'] || ''
|
|
310
|
+
if (!subject) {
|
|
311
|
+
for (const group of root.querySelectorAll('.group')) {
|
|
312
|
+
if (cleanText(group.querySelector('strong.t')) === '주제') {
|
|
313
|
+
subject = group.querySelector('input')?.getAttribute('value')?.trim() || ''
|
|
314
|
+
break
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const findGroupTextarea = (...names: string[]): string => {
|
|
320
|
+
for (const group of root.querySelectorAll('.group')) {
|
|
321
|
+
const label = cleanText(group.querySelector('strong.t')).replace(/:$/, '')
|
|
322
|
+
if (names.includes(label)) {
|
|
323
|
+
const textarea = group.querySelector('textarea')
|
|
324
|
+
if (textarea) return textarea.text.trim()
|
|
325
|
+
return cleanText(group.querySelector('.c'))
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return ''
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const files = root
|
|
332
|
+
.querySelectorAll('.file_list_new a')
|
|
333
|
+
.map((a) => a.getAttribute('href') || '')
|
|
334
|
+
.filter(Boolean)
|
|
335
|
+
.map((href) => (href.startsWith('http') ? href : `https://www.swmaestro.ai${href}`))
|
|
308
336
|
|
|
309
337
|
return ReportDetailSchema.parse({
|
|
310
338
|
id,
|
|
@@ -312,27 +340,27 @@ export function parseReportDetail(html: string, id = 0): ReportDetail {
|
|
|
312
340
|
title: labels['제목'] || '',
|
|
313
341
|
progressDate: labels['진행 날짜'] || '',
|
|
314
342
|
status: labels['상태'] || '',
|
|
315
|
-
author: labels['작성자'] || '',
|
|
343
|
+
author: labels['작성자'] || labels['진행 멘토 명'] || '',
|
|
316
344
|
createdAt: labels['등록일'] || '',
|
|
317
345
|
acceptedTime: labels['인정시간'] || '',
|
|
318
346
|
payAmount: labels['지급액'] || '',
|
|
319
|
-
content: labels['추진 내용'] || '',
|
|
320
|
-
subject
|
|
321
|
-
menteeRegion: labels['멘토링 대상'] || '',
|
|
347
|
+
content: findGroupTextarea('추진내용', '추진 내용') || labels['추진내용'] || labels['추진 내용'] || '',
|
|
348
|
+
subject,
|
|
349
|
+
menteeRegion: labels['멘토링대상'] || labels['멘토링 대상'] || '',
|
|
322
350
|
reportType: labels['구분'] || '',
|
|
323
351
|
teamNames: labels['팀명'] || '',
|
|
324
352
|
venue: labels['진행 장소'] || '',
|
|
325
|
-
attendanceCount: extractNumber(labels['참석 연수생'] || ''),
|
|
353
|
+
attendanceCount: extractNumber(labels['참석자 인원'] || labels['참석 연수생'] || ''),
|
|
326
354
|
attendanceNames: labels['참석자 이름'] || '',
|
|
327
|
-
progressStartTime:
|
|
328
|
-
progressEndTime:
|
|
329
|
-
exceptStartTime:
|
|
330
|
-
exceptEndTime:
|
|
331
|
-
exceptReason: labels['제외 사유'] || labels['제외이유'] || '',
|
|
332
|
-
mentorOpinion: labels['멘토 의견'] || '',
|
|
355
|
+
progressStartTime: progressTimeMatch?.[1] || '',
|
|
356
|
+
progressEndTime: progressTimeMatch?.[2] || '',
|
|
357
|
+
exceptStartTime: exceptTimeMatch?.[1] || '',
|
|
358
|
+
exceptEndTime: exceptTimeMatch?.[2] || '',
|
|
359
|
+
exceptReason: labels['제외사유'] || labels['제외 사유'] || labels['제외이유'] || '',
|
|
360
|
+
mentorOpinion: findGroupTextarea('멘토의견', '멘토 의견') || labels['멘토의견'] || labels['멘토 의견'] || '',
|
|
333
361
|
nonAttendanceNames: labels['무단불참자'] || '',
|
|
334
|
-
etc: labels['특이사항'] || '',
|
|
335
|
-
files
|
|
362
|
+
etc: findGroupTextarea('기타', '특이사항') || labels['기타'] || labels['특이사항'] || '',
|
|
363
|
+
files,
|
|
336
364
|
})
|
|
337
365
|
}
|
|
338
366
|
|
package/src/index.ts
CHANGED
|
@@ -3,5 +3,8 @@ export type { SomaClientOptions } from './client'
|
|
|
3
3
|
export { AuthenticationError } from './errors'
|
|
4
4
|
export { SomaHttp } from './http'
|
|
5
5
|
export { CredentialManager } from './credential-manager'
|
|
6
|
+
export { TozHttp } from './toz-http'
|
|
7
|
+
export type { TozHttpOptions, TozHttpState } from './toz-http'
|
|
8
|
+
export * from './toz-formatters'
|
|
6
9
|
export * from './types'
|
|
7
10
|
export * from './constants'
|