opensoma 0.4.0 → 0.5.1
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/client.d.ts +7 -1
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +13 -11
- package/dist/src/client.js.map +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/swmaestro.d.ts.map +1 -1
- package/dist/src/shared/utils/swmaestro.js +1 -5
- package/dist/src/shared/utils/swmaestro.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/client.test.ts +135 -117
- package/src/client.ts +16 -12
- package/src/commands/auth.test.ts +7 -7
- package/src/commands/auth.ts +107 -50
- package/src/commands/helpers.test.ts +8 -8
- package/src/commands/report.test.ts +7 -7
- package/src/constants.ts +50 -0
- package/src/credential-manager.test.ts +5 -5
- package/src/formatters.test.ts +22 -22
- package/src/formatters.ts +44 -16
- package/src/http.test.ts +37 -37
- package/src/index.ts +3 -0
- package/src/shared/utils/mentoring-params.test.ts +16 -16
- package/src/shared/utils/swmaestro.test.ts +87 -8
- package/src/shared/utils/swmaestro.ts +1 -6
- package/src/shared/utils/toz.test.ts +138 -0
- package/src/shared/utils/toz.ts +100 -0
- package/src/token-extractor.test.ts +40 -15
- 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.test.ts +220 -204
- 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/client.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, describe, expect,
|
|
1
|
+
import { afterEach, describe, expect, it, mock } from 'bun:test'
|
|
2
2
|
import { mkdtemp } from 'node:fs/promises'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
@@ -7,31 +7,80 @@ import { SomaClient } from './client'
|
|
|
7
7
|
import { MENU_NO } from './constants'
|
|
8
8
|
import { CredentialManager } from './credential-manager'
|
|
9
9
|
import { AuthenticationError } from './errors'
|
|
10
|
-
import
|
|
10
|
+
import { SomaHttp, type UserIdentity } from './http'
|
|
11
11
|
|
|
12
12
|
afterEach(() => {
|
|
13
13
|
mock.restore()
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
+
type HttpCall = { method: string; path: string; data: Record<string, string> | undefined }
|
|
17
|
+
|
|
18
|
+
interface FakeHttpConfig {
|
|
19
|
+
identity?: UserIdentity | null
|
|
20
|
+
getBody?: (path: string, data?: Record<string, string>) => string
|
|
21
|
+
postBody?: (path: string, data: Record<string, string>) => string
|
|
22
|
+
postFormBody?: (path: string, data: Record<string, string>) => string
|
|
23
|
+
sessionCookie?: string
|
|
24
|
+
csrfToken?: string | null
|
|
25
|
+
onLogin?: (username: string, password: string) => void
|
|
26
|
+
onLogout?: () => void
|
|
27
|
+
checkLoginSequence?: Array<UserIdentity | null>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createFakeHttp(config: FakeHttpConfig = {}): { http: SomaHttp; calls: HttpCall[] } {
|
|
31
|
+
const calls: HttpCall[] = []
|
|
32
|
+
const sequence = config.checkLoginSequence ? [...config.checkLoginSequence] : null
|
|
33
|
+
|
|
34
|
+
const fake = {
|
|
35
|
+
checkLogin: async () => {
|
|
36
|
+
if (sequence) {
|
|
37
|
+
return sequence.shift() ?? config.identity ?? null
|
|
38
|
+
}
|
|
39
|
+
return config.identity ?? null
|
|
40
|
+
},
|
|
41
|
+
get: async (path: string, data?: Record<string, string>) => {
|
|
42
|
+
calls.push({ method: 'get', path, data })
|
|
43
|
+
return config.getBody ? config.getBody(path, data) : ''
|
|
44
|
+
},
|
|
45
|
+
post: async (path: string, data: Record<string, string>) => {
|
|
46
|
+
calls.push({ method: 'post', path, data })
|
|
47
|
+
return config.postBody ? config.postBody(path, data) : ''
|
|
48
|
+
},
|
|
49
|
+
postForm: async (path: string, data: Record<string, string>) => {
|
|
50
|
+
calls.push({ method: 'postForm', path, data })
|
|
51
|
+
return config.postFormBody ? config.postFormBody(path, data) : ''
|
|
52
|
+
},
|
|
53
|
+
postMultipart: async () => '',
|
|
54
|
+
login: async (username: string, password: string) => {
|
|
55
|
+
config.onLogin?.(username, password)
|
|
56
|
+
},
|
|
57
|
+
logout: async () => {
|
|
58
|
+
config.onLogout?.()
|
|
59
|
+
},
|
|
60
|
+
getSessionCookie: () => config.sessionCookie,
|
|
61
|
+
getCsrfToken: () => config.csrfToken ?? null,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { http: fake as unknown as SomaHttp, calls }
|
|
65
|
+
}
|
|
66
|
+
|
|
16
67
|
describe('SomaClient', () => {
|
|
17
|
-
|
|
68
|
+
it('exposes session state passed through options', () => {
|
|
18
69
|
const client = new SomaClient({ sessionCookie: 'session-1', csrfToken: 'csrf-1' })
|
|
19
|
-
const http = Reflect.get(client, 'http') as SomaHttp
|
|
20
70
|
|
|
21
|
-
expect(
|
|
22
|
-
|
|
71
|
+
expect(client.getSessionData()).toEqual({
|
|
72
|
+
sessionCookie: 'session-1',
|
|
73
|
+
csrfToken: 'csrf-1',
|
|
74
|
+
})
|
|
23
75
|
})
|
|
24
76
|
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
get: async (path: string, data?: Record<string, string>) => {
|
|
31
|
-
calls.push({ method: 'get', path, data })
|
|
32
|
-
return '<table><tbody><tr><td>1</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=123">[자유 멘토링] 제목 [접수중]</a></td><td>2026-04-01 ~ 2026-04-02</td><td>2026-04-03(목) 10:00 ~ 11:00</td><td>1 /4</td><td>OK</td><td>[접수중]</td><td>작성자</td><td>2026-04-01</td></tr></tbody></table><ul class="bbs-total"><li>Total : 1</li><li>1/1 Page</li></ul>'
|
|
33
|
-
},
|
|
77
|
+
it('lists mentoring sessions with parsed items and pagination', async () => {
|
|
78
|
+
const { http, calls } = createFakeHttp({
|
|
79
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
80
|
+
getBody: () =>
|
|
81
|
+
'<table><tbody><tr><td>1</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=123">[자유 멘토링] 제목 [접수중]</a></td><td>2026-04-01 ~ 2026-04-02</td><td>2026-04-03(목) 10:00 ~ 11:00</td><td>1 /4</td><td>OK</td><td>[접수중]</td><td>작성자</td><td>2026-04-01</td></tr></tbody></table><ul class="bbs-total"><li>Total : 1</li><li>1/1 Page</li></ul>',
|
|
34
82
|
})
|
|
83
|
+
const client = new SomaClient({ http })
|
|
35
84
|
|
|
36
85
|
const result = await client.mentoring.list({ status: 'open', type: 'public', page: 2 })
|
|
37
86
|
|
|
@@ -51,41 +100,34 @@ describe('SomaClient', () => {
|
|
|
51
100
|
expect(result.pagination).toEqual({ total: 1, currentPage: 1, totalPages: 1 })
|
|
52
101
|
})
|
|
53
102
|
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
get: async (path: string, data?: Record<string, string>) => {
|
|
60
|
-
captured = { path, data }
|
|
61
|
-
return '<input type="hidden" name="qustnrSn" value="99"><table><tr><th>모집 명</th><td>상세</td><th>상태</th><td>접수중</td></tr><tr><th>접수 기간</th><td>2026-04-01 ~ 2026-04-02</td><th>강의날짜</th><td>2026-04-03(목) 10:00 ~ 11:00</td></tr><tr><th>장소</th><td>온라인(Webex)</td><th>모집인원</th><td>4명</td></tr><tr><th>작성자</th><td>작성자</td><th>등록일</th><td>2026-04-01</td></tr></table><div data-content><p>본문</p></div>'
|
|
62
|
-
},
|
|
103
|
+
it('fetches a single mentoring session from the detail endpoint', async () => {
|
|
104
|
+
const { http, calls } = createFakeHttp({
|
|
105
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
106
|
+
getBody: () =>
|
|
107
|
+
'<input type="hidden" name="qustnrSn" value="99"><table><tr><th>모집 명</th><td>상세</td><th>상태</th><td>접수중</td></tr><tr><th>접수 기간</th><td>2026-04-01 ~ 2026-04-02</td><th>강의날짜</th><td>2026-04-03(목) 10:00 ~ 11:00</td></tr><tr><th>장소</th><td>온라인(Webex)</td><th>모집인원</th><td>4명</td></tr><tr><th>작성자</th><td>작성자</td><th>등록일</th><td>2026-04-01</td></tr></table><div data-content><p>본문</p></div>',
|
|
63
108
|
})
|
|
109
|
+
const client = new SomaClient({ http })
|
|
64
110
|
|
|
65
111
|
const result = await client.mentoring.get(99)
|
|
66
112
|
|
|
67
|
-
expect(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
113
|
+
expect(calls).toEqual([
|
|
114
|
+
{
|
|
115
|
+
method: 'get',
|
|
116
|
+
path: '/mypage/mentoLec/view.do',
|
|
117
|
+
data: { menuNo: MENU_NO.MENTORING, qustnrSn: '99' },
|
|
118
|
+
},
|
|
119
|
+
])
|
|
71
120
|
expect(result).toMatchObject({ id: 99, title: '상세', venue: '온라인(Webex)' })
|
|
72
121
|
})
|
|
73
122
|
|
|
74
|
-
|
|
123
|
+
it('posts the expected payloads for create, update, delete, apply, cancel, reserve, and event apply', async () => {
|
|
75
124
|
const mentoringDetailHtml =
|
|
76
125
|
'<div class="group"><strong class="t">모집 명</strong><div class="c">[자유 멘토링] 기존 멘토링</div></div><div class="group"><strong class="t">접수 기간</strong><div class="c">2026.03.01 ~ 2026.03.15</div></div><div class="group"><strong class="t">강의날짜</strong><div class="c"><span>2026.03.20 10:00시 ~ 12:00시</span></div></div><div class="group"><strong class="t">장소</strong><div class="c">온라인(Webex)</div></div><div class="group"><strong class="t">모집인원</strong><div class="c">5명</div></div><div class="group"><strong class="t">작성자</strong><div class="c">전수열</div></div><div class="group"><strong class="t">등록일</strong><div class="c">2026.03.01</div></div><div class="cont"><p>기존 내용</p></div>'
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
calls.push({ method, path, data })
|
|
81
|
-
return ''
|
|
82
|
-
}
|
|
83
|
-
Reflect.set(client, 'http', {
|
|
84
|
-
checkLogin: async () => ({ userId: 'user@example.com', userNm: 'Test' }),
|
|
85
|
-
get: async () => mentoringDetailHtml,
|
|
86
|
-
post: captor('post'),
|
|
87
|
-
postForm: captor('postForm'),
|
|
126
|
+
const { http, calls } = createFakeHttp({
|
|
127
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
128
|
+
getBody: () => mentoringDetailHtml,
|
|
88
129
|
})
|
|
130
|
+
const client = new SomaClient({ http })
|
|
89
131
|
|
|
90
132
|
await client.mentoring.create({
|
|
91
133
|
title: '새 멘토링',
|
|
@@ -115,7 +157,8 @@ describe('SomaClient', () => {
|
|
|
115
157
|
})
|
|
116
158
|
await client.event.apply(11)
|
|
117
159
|
|
|
118
|
-
|
|
160
|
+
const writes = calls.filter((call) => call.method !== 'get')
|
|
161
|
+
expect(writes.map((call) => `${call.method}:${call.path}`)).toEqual([
|
|
119
162
|
'postForm:/mypage/mentoLec/insert.do',
|
|
120
163
|
'postForm:/mypage/mentoLec/update.do',
|
|
121
164
|
'post:/mypage/mentoLec/delete.do',
|
|
@@ -124,41 +167,41 @@ describe('SomaClient', () => {
|
|
|
124
167
|
'post:/mypage/itemRent/insert.do',
|
|
125
168
|
'post:/application/application/application.do',
|
|
126
169
|
])
|
|
127
|
-
expect(
|
|
170
|
+
expect(writes[0]?.data).toMatchObject({
|
|
128
171
|
menuNo: MENU_NO.MENTORING,
|
|
129
172
|
reportCd: 'MRC020',
|
|
130
173
|
qustnrSj: '새 멘토링',
|
|
131
174
|
})
|
|
132
|
-
expect(
|
|
175
|
+
expect(writes[1]?.data).toMatchObject({
|
|
133
176
|
menuNo: MENU_NO.MENTORING,
|
|
134
177
|
reportCd: 'MRC010',
|
|
135
178
|
qustnrSj: '수정된 멘토링',
|
|
136
179
|
qustnrSn: '42',
|
|
137
180
|
})
|
|
138
|
-
expect(
|
|
181
|
+
expect(writes[2]?.data).toEqual({
|
|
139
182
|
menuNo: MENU_NO.MENTORING,
|
|
140
183
|
qustnrSn: '7',
|
|
141
184
|
pageQueryString: '',
|
|
142
185
|
})
|
|
143
|
-
expect(
|
|
186
|
+
expect(writes[3]?.data).toEqual({
|
|
144
187
|
menuNo: MENU_NO.EVENT,
|
|
145
188
|
qustnrSn: '8',
|
|
146
189
|
applyGb: 'C',
|
|
147
190
|
stepHeader: '0',
|
|
148
191
|
})
|
|
149
|
-
expect(
|
|
192
|
+
expect(writes[4]?.data).toEqual({
|
|
150
193
|
menuNo: MENU_NO.APPLICATION_HISTORY,
|
|
151
194
|
applySn: '9',
|
|
152
195
|
qustnrSn: '10',
|
|
153
196
|
})
|
|
154
|
-
expect(
|
|
197
|
+
expect(writes[5]?.data).toMatchObject({
|
|
155
198
|
menuNo: MENU_NO.ROOM,
|
|
156
199
|
itemId: '17',
|
|
157
200
|
title: '회의',
|
|
158
201
|
'time[0]': '10:00',
|
|
159
202
|
'time[1]': '10:30',
|
|
160
203
|
})
|
|
161
|
-
expect(
|
|
204
|
+
expect(writes[6]?.data).toEqual({
|
|
162
205
|
menuNo: MENU_NO.EVENT,
|
|
163
206
|
qustnrSn: '11',
|
|
164
207
|
applyGb: 'C',
|
|
@@ -166,22 +209,18 @@ describe('SomaClient', () => {
|
|
|
166
209
|
})
|
|
167
210
|
})
|
|
168
211
|
|
|
169
|
-
|
|
212
|
+
it('merges partial update params with the existing mentoring data', async () => {
|
|
170
213
|
const mentoringDetailHtml =
|
|
171
214
|
'<div class="group"><strong class="t">모집 명</strong><div class="c">[멘토 특강] 웹 성능 특강</div></div><div class="group"><strong class="t">접수 기간</strong><div class="c">2026.04.01 ~ 2026.04.10</div></div><div class="group"><strong class="t">강의날짜</strong><div class="c"><span>2026.04.11 14:00시 ~ 15:30시</span></div></div><div class="group"><strong class="t">장소</strong><div class="c">온라인(Webex)</div></div><div class="group"><strong class="t">모집인원</strong><div class="c">20명</div></div><div class="group"><strong class="t">작성자</strong><div class="c">전수열</div></div><div class="group"><strong class="t">등록일</strong><div class="c">2026.04.01</div></div><div class="cont"><p>세션 본문</p></div>'
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
checkLogin: async () => ({ userId: 'user@example.com', userNm: 'Test' }),
|
|
176
|
-
get: async () => mentoringDetailHtml,
|
|
177
|
-
postForm: async (path: string, data: Record<string, string>) => {
|
|
178
|
-
postFormCalls.push({ path, data })
|
|
179
|
-
return ''
|
|
180
|
-
},
|
|
215
|
+
const { http, calls } = createFakeHttp({
|
|
216
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
217
|
+
getBody: () => mentoringDetailHtml,
|
|
181
218
|
})
|
|
219
|
+
const client = new SomaClient({ http })
|
|
182
220
|
|
|
183
221
|
await client.mentoring.update(9572, { title: '변경된 제목' })
|
|
184
222
|
|
|
223
|
+
const postFormCalls = calls.filter((c) => c.method === 'postForm')
|
|
185
224
|
expect(postFormCalls).toHaveLength(1)
|
|
186
225
|
expect(postFormCalls[0]?.data).toMatchObject({
|
|
187
226
|
qustnrSj: '변경된 제목',
|
|
@@ -198,13 +237,10 @@ describe('SomaClient', () => {
|
|
|
198
237
|
})
|
|
199
238
|
})
|
|
200
239
|
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
checkLogin: async () => ({ userId: 'neo@example.com', userNm: '전수열' }),
|
|
206
|
-
get: async (path: string, data?: Record<string, string>) => {
|
|
207
|
-
calls.push({ method: 'get', path, data })
|
|
240
|
+
it('routes room, dashboard, notice, team, member, event, and history calls to the expected endpoints', async () => {
|
|
241
|
+
const { http, calls } = createFakeHttp({
|
|
242
|
+
identity: { userId: 'neo@example.com', userNm: '전수열' },
|
|
243
|
+
getBody: (path) => {
|
|
208
244
|
if (path === '/mypage/myMain/dashboard.do') {
|
|
209
245
|
return '<ul class="dash-top"><li class="dash-card"><div class="dash-etc"><span>소속 :<br> Indent</span><span>직책 :<br> </span></div><div class="dash-state"><div class="top"><span class="bg-orange label"><span>멘토</span></span><div class="welcome"><strong>전수열</strong>님 안녕하세요.</div></div></div></li></ul><ul class="bbs-dash_w"><li>멘토링 · 멘토특강<li><a href="/sw/mypage/mentoLec/view.do?qustnrSn=9582">게임 개발 AI 활용법 접수중</a></li></li></ul>'
|
|
210
246
|
}
|
|
@@ -231,14 +267,14 @@ describe('SomaClient', () => {
|
|
|
231
267
|
}
|
|
232
268
|
return '<table><tbody><tr><td>1</td><td>멘토 특강</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=1">접수내역</a></td><td>전수열</td><td>2026.04.11</td><td>2026-04-01</td><td>[신청완료]</td><td>[OK]</td><td>승인대기</td><td>-</td></tr></tbody></table><ul class="bbs-total"><li>Total : 1</li><li>1/1 Page</li></ul>'
|
|
233
269
|
},
|
|
234
|
-
|
|
235
|
-
calls.push({ method: 'post', path, data })
|
|
270
|
+
postBody: (path) => {
|
|
236
271
|
if (path === '/mypage/officeMng/rentTime.do') {
|
|
237
272
|
return '<span class="ck-st2 disabled" data-hour="09" data-minute="00"><input type="checkbox" disabled="disabled"><label title="아침 회의<br>예약자 : 김오픈">오전 09:00</label></span>'
|
|
238
273
|
}
|
|
239
274
|
return '<ul class="bbs-reserve"><li class="item"><a href="javascript:void(0);" onclick="location.href=\'/sw/mypage/officeMng/view.do?itemId=17\';"><div class="cont"><h4 class="tit">스페이스 A1</h4><ul class="txt bul-dot grey"><li>이용기간 : 2026-04-01 ~ 2026-12-31</li><li><p>설명 : 4인</p></li><li class="time-list"><div class="time-grid"><span>09:00</span></div></li></ul></div></a></li></ul>'
|
|
240
275
|
},
|
|
241
276
|
})
|
|
277
|
+
const client = new SomaClient({ http })
|
|
242
278
|
|
|
243
279
|
const roomList = await client.room.list({ date: '2026-04-01', room: 'A1', includeReservations: true })
|
|
244
280
|
const roomSlots = await client.room.available(17, '2026-04-01')
|
|
@@ -289,8 +325,7 @@ describe('SomaClient', () => {
|
|
|
289
325
|
note: '-',
|
|
290
326
|
})
|
|
291
327
|
|
|
292
|
-
|
|
293
|
-
expect(dashboardCallIndex).toBeGreaterThanOrEqual(0)
|
|
328
|
+
expect(calls.some((c) => c.path === '/mypage/myMain/dashboard.do')).toBe(true)
|
|
294
329
|
expect(calls).toContainEqual({
|
|
295
330
|
method: 'post',
|
|
296
331
|
path: '/mypage/officeMng/rentTime.do',
|
|
@@ -305,53 +340,44 @@ describe('SomaClient', () => {
|
|
|
305
340
|
})
|
|
306
341
|
})
|
|
307
342
|
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
calls.push(`${username}:${password}`)
|
|
314
|
-
},
|
|
315
|
-
checkLogin: async () => ({ userId: 'neo@example.com', userNm: '전수열' }),
|
|
343
|
+
it('delegates login and isLoggedIn to SomaHttp', async () => {
|
|
344
|
+
const loginCalls: string[] = []
|
|
345
|
+
const { http } = createFakeHttp({
|
|
346
|
+
identity: { userId: 'neo@example.com', userNm: '전수열' },
|
|
347
|
+
onLogin: (username, password) => loginCalls.push(`${username}:${password}`),
|
|
316
348
|
})
|
|
349
|
+
const client = new SomaClient({ username: 'neo@example.com', password: 'secret', http })
|
|
317
350
|
|
|
318
351
|
await client.login()
|
|
319
352
|
|
|
320
|
-
expect(
|
|
353
|
+
expect(loginCalls).toEqual(['neo@example.com:secret'])
|
|
321
354
|
await expect(client.isLoggedIn()).resolves.toBe(true)
|
|
322
355
|
})
|
|
323
356
|
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
authChecks += 1
|
|
331
|
-
return authChecks >= 2 ? { userId: 'neo@example.com', userNm: '전수열' } : null
|
|
332
|
-
},
|
|
333
|
-
login: async (username: string, password: string) => {
|
|
334
|
-
calls.push(`${username}:${password}`)
|
|
335
|
-
},
|
|
336
|
-
get: async () =>
|
|
357
|
+
it('re-logs in automatically when username and password are configured', async () => {
|
|
358
|
+
const loginCalls: string[] = []
|
|
359
|
+
const { http } = createFakeHttp({
|
|
360
|
+
checkLoginSequence: [null, { userId: 'neo@example.com', userNm: '전수열' }],
|
|
361
|
+
onLogin: (username, password) => loginCalls.push(`${username}:${password}`),
|
|
362
|
+
getBody: () =>
|
|
337
363
|
'<table><tbody><tr><td>1</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=123">[자유 멘토링] 제목 [접수중]</a></td><td>2026-04-01 ~ 2026-04-02</td><td>2026-04-03(목) 10:00 ~ 11:00</td><td>1 /4</td><td>OK</td><td>[접수중]</td><td>작성자</td><td>2026-04-01</td></tr></tbody></table><ul class="bbs-total"><li>Total : 1</li><li>1/1 Page</li></ul>',
|
|
338
364
|
})
|
|
365
|
+
const client = new SomaClient({ username: 'neo@example.com', password: 'secret', http })
|
|
339
366
|
|
|
340
367
|
await expect(client.mentoring.list()).resolves.toMatchObject({
|
|
341
368
|
items: [expect.objectContaining({ id: 123, title: '제목' })],
|
|
342
369
|
})
|
|
343
|
-
expect(
|
|
370
|
+
expect(loginCalls).toEqual(['neo@example.com:secret'])
|
|
344
371
|
})
|
|
345
372
|
|
|
346
|
-
|
|
347
|
-
const client = new SomaClient()
|
|
373
|
+
it('persists the credentials used by login() when saveCredentials is called', async () => {
|
|
348
374
|
const dir = await mkdtemp(join(tmpdir(), 'opensoma-client-save-'))
|
|
349
375
|
const manager = new CredentialManager(dir)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
getCsrfToken: () => 'csrf-1',
|
|
376
|
+
const { http } = createFakeHttp({
|
|
377
|
+
sessionCookie: 'session-1',
|
|
378
|
+
csrfToken: 'csrf-1',
|
|
354
379
|
})
|
|
380
|
+
const client = new SomaClient({ http })
|
|
355
381
|
|
|
356
382
|
await client.login('neo@example.com', 'secret')
|
|
357
383
|
await client.saveCredentials(manager)
|
|
@@ -367,25 +393,19 @@ describe('SomaClient', () => {
|
|
|
367
393
|
await manager.remove()
|
|
368
394
|
})
|
|
369
395
|
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
logout: async () => {
|
|
375
|
-
calls.push('logout')
|
|
376
|
-
},
|
|
377
|
-
})
|
|
396
|
+
it('delegates logout to SomaHttp', async () => {
|
|
397
|
+
const logoutCalls: string[] = []
|
|
398
|
+
const { http } = createFakeHttp({ onLogout: () => logoutCalls.push('logout') })
|
|
399
|
+
const client = new SomaClient({ http })
|
|
378
400
|
|
|
379
401
|
await client.logout()
|
|
380
402
|
|
|
381
|
-
expect(
|
|
403
|
+
expect(logoutCalls).toEqual(['logout'])
|
|
382
404
|
})
|
|
383
405
|
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
checkLogin: async () => null,
|
|
388
|
-
})
|
|
406
|
+
it('throws AuthenticationError from every auth-required operation when not logged in', async () => {
|
|
407
|
+
const { http } = createFakeHttp({ identity: null })
|
|
408
|
+
const client = new SomaClient({ http })
|
|
389
409
|
|
|
390
410
|
await expect(client.mentoring.list()).rejects.toBeInstanceOf(AuthenticationError)
|
|
391
411
|
await expect(client.mentoring.get(1)).rejects.toBeInstanceOf(AuthenticationError)
|
|
@@ -428,11 +448,9 @@ describe('SomaClient', () => {
|
|
|
428
448
|
await expect(client.event.apply(1)).rejects.toBeInstanceOf(AuthenticationError)
|
|
429
449
|
})
|
|
430
450
|
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
checkLogin: async () => null,
|
|
435
|
-
})
|
|
451
|
+
it('includes helpful login hints in the AuthenticationError message', async () => {
|
|
452
|
+
const { http } = createFakeHttp({ identity: null })
|
|
453
|
+
const client = new SomaClient({ http })
|
|
436
454
|
|
|
437
455
|
try {
|
|
438
456
|
await client.mentoring.list()
|