opensoma 0.5.1 → 0.7.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 +5 -1
- package/dist/src/agent-browser-launcher.d.ts +43 -0
- package/dist/src/agent-browser-launcher.d.ts.map +1 -0
- package/dist/src/agent-browser-launcher.js +97 -0
- package/dist/src/agent-browser-launcher.js.map +1 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +8 -5
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +34 -7
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +224 -52
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands/agent-browser.d.ts +3 -0
- package/dist/src/commands/agent-browser.d.ts.map +1 -0
- package/dist/src/commands/agent-browser.js +27 -0
- package/dist/src/commands/agent-browser.js.map +1 -0
- 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 +4 -2
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/dashboard.d.ts +13 -0
- package/dist/src/commands/dashboard.d.ts.map +1 -1
- package/dist/src/commands/dashboard.js +10 -18
- package/dist/src/commands/dashboard.js.map +1 -1
- package/dist/src/commands/helpers.d.ts +1 -1
- package/dist/src/commands/helpers.d.ts.map +1 -1
- package/dist/src/commands/helpers.js +2 -2
- package/dist/src/commands/helpers.js.map +1 -1
- package/dist/src/commands/index.d.ts +3 -1
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +3 -1
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/mentoring.d.ts.map +1 -1
- package/dist/src/commands/mentoring.js +54 -29
- package/dist/src/commands/mentoring.js.map +1 -1
- package/dist/src/commands/notice.d.ts.map +1 -1
- package/dist/src/commands/notice.js +2 -1
- package/dist/src/commands/notice.js.map +1 -1
- package/dist/src/commands/report.d.ts.map +1 -1
- package/dist/src/commands/report.js +4 -2
- package/dist/src/commands/report.js.map +1 -1
- package/dist/src/commands/room.d.ts.map +1 -1
- package/dist/src/commands/room.js +125 -2
- package/dist/src/commands/room.js.map +1 -1
- package/dist/src/commands/schedule.d.ts +3 -0
- package/dist/src/commands/schedule.d.ts.map +1 -0
- package/dist/src/commands/schedule.js +27 -0
- package/dist/src/commands/schedule.js.map +1 -0
- package/dist/src/commands/team.d.ts.map +1 -1
- package/dist/src/commands/team.js +55 -4
- package/dist/src/commands/team.js.map +1 -1
- package/dist/src/commands/toz.d.ts +16 -0
- package/dist/src/commands/toz.d.ts.map +1 -0
- package/dist/src/commands/toz.js +488 -0
- package/dist/src/commands/toz.js.map +1 -0
- package/dist/src/constants.d.ts +5 -5
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +20 -8
- package/dist/src/constants.js.map +1 -1
- package/dist/src/credential-manager.d.ts +15 -0
- package/dist/src/credential-manager.d.ts.map +1 -1
- package/dist/src/credential-manager.js +46 -0
- package/dist/src/credential-manager.js.map +1 -1
- package/dist/src/formatters.d.ts +11 -3
- package/dist/src/formatters.d.ts.map +1 -1
- package/dist/src/formatters.js +281 -52
- package/dist/src/formatters.js.map +1 -1
- package/dist/src/http.d.ts +8 -0
- package/dist/src/http.d.ts.map +1 -1
- package/dist/src/http.js +29 -1
- package/dist/src/http.js.map +1 -1
- package/dist/src/index.d.ts +8 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/session-recovery.js +2 -0
- package/dist/src/session-recovery.js.map +1 -1
- package/dist/src/shared/utils/swmaestro.d.ts +34 -1
- package/dist/src/shared/utils/swmaestro.d.ts.map +1 -1
- package/dist/src/shared/utils/swmaestro.js +102 -39
- package/dist/src/shared/utils/swmaestro.js.map +1 -1
- package/dist/src/shared/utils/team-action-params.d.ts +3 -0
- package/dist/src/shared/utils/team-action-params.d.ts.map +1 -0
- package/dist/src/shared/utils/team-action-params.js +10 -0
- package/dist/src/shared/utils/team-action-params.js.map +1 -0
- package/dist/src/shared/utils/team-params.d.ts +12 -0
- package/dist/src/shared/utils/team-params.d.ts.map +1 -0
- package/dist/src/shared/utils/team-params.js +38 -0
- package/dist/src/shared/utils/team-params.js.map +1 -0
- package/dist/src/toz-client.d.ts +89 -0
- package/dist/src/toz-client.d.ts.map +1 -0
- package/dist/src/toz-client.js +204 -0
- package/dist/src/toz-client.js.map +1 -0
- package/dist/src/toz-pending-store.d.ts +30 -0
- package/dist/src/toz-pending-store.d.ts.map +1 -0
- package/dist/src/toz-pending-store.js +36 -0
- package/dist/src/toz-pending-store.js.map +1 -0
- package/dist/src/types.d.ts +147 -10
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +74 -6
- package/dist/src/types.js.map +1 -1
- package/package.json +5 -1
- package/src/agent-browser-launcher.test.ts +263 -0
- package/src/agent-browser-launcher.ts +159 -0
- package/src/cli.ts +10 -5
- package/src/client.test.ts +673 -30
- package/src/client.ts +287 -67
- package/src/commands/agent-browser.ts +33 -0
- package/src/commands/auth.test.ts +77 -26
- package/src/commands/auth.ts +5 -3
- package/src/commands/dashboard.test.ts +57 -0
- package/src/commands/dashboard.ts +22 -19
- package/src/commands/helpers.test.ts +76 -25
- package/src/commands/helpers.ts +3 -3
- package/src/commands/index.ts +3 -1
- package/src/commands/mentoring.ts +60 -29
- package/src/commands/notice.ts +2 -1
- package/src/commands/report.ts +4 -2
- package/src/commands/room.ts +160 -1
- package/src/commands/schedule.ts +32 -0
- package/src/commands/team.ts +73 -5
- package/src/commands/toz.test.ts +51 -0
- package/src/commands/toz.ts +607 -0
- package/src/constants.ts +20 -8
- package/src/credential-manager.test.ts +98 -0
- package/src/credential-manager.ts +50 -0
- package/src/formatters.test.ts +528 -33
- package/src/formatters.ts +309 -55
- package/src/http.test.ts +71 -2
- package/src/http.ts +41 -2
- package/src/index.ts +23 -1
- package/src/session-recovery.ts +2 -0
- package/src/shared/utils/swmaestro.test.ts +245 -9
- package/src/shared/utils/swmaestro.ts +150 -47
- package/src/shared/utils/team-action-params.test.ts +32 -0
- package/src/shared/utils/team-action-params.ts +10 -0
- package/src/shared/utils/team-params.test.ts +141 -0
- package/src/shared/utils/team-params.ts +53 -0
- package/src/toz-client.test.ts +243 -0
- package/src/toz-client.ts +311 -0
- package/src/toz-pending-store.test.ts +91 -0
- package/src/toz-pending-store.ts +62 -0
- package/src/types.test.ts +26 -13
- package/src/types.ts +87 -7
- package/dist/src/commands/event.d.ts +0 -3
- package/dist/src/commands/event.d.ts.map +0 -1
- package/dist/src/commands/event.js +0 -58
- package/dist/src/commands/event.js.map +0 -1
- package/src/commands/event.ts +0 -73
package/src/client.test.ts
CHANGED
|
@@ -7,7 +7,7 @@ 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 { SomaHttp, type UserIdentity } from './http'
|
|
10
|
+
import { SomaHttp, UserGb, type UserIdentity } from './http'
|
|
11
11
|
|
|
12
12
|
afterEach(() => {
|
|
13
13
|
mock.restore()
|
|
@@ -20,6 +20,7 @@ interface FakeHttpConfig {
|
|
|
20
20
|
getBody?: (path: string, data?: Record<string, string>) => string
|
|
21
21
|
postBody?: (path: string, data: Record<string, string>) => string
|
|
22
22
|
postFormBody?: (path: string, data: Record<string, string>) => string
|
|
23
|
+
postJsonBody?: (path: string, data: Record<string, string>) => unknown
|
|
23
24
|
sessionCookie?: string
|
|
24
25
|
csrfToken?: string | null
|
|
25
26
|
onLogin?: (username: string, password: string) => void
|
|
@@ -27,6 +28,45 @@ interface FakeHttpConfig {
|
|
|
27
28
|
checkLoginSequence?: Array<UserIdentity | null>
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
function buildMentoringEditFormFixture(fields: {
|
|
32
|
+
qustnrSn: string
|
|
33
|
+
qustnrSj: string
|
|
34
|
+
reportCd: 'MRC010' | 'MRC020'
|
|
35
|
+
receiptType: 'UNTIL_LECTURE' | 'DIRECT'
|
|
36
|
+
bgndeDate: string
|
|
37
|
+
bgndeTime: string
|
|
38
|
+
enddeDate: string
|
|
39
|
+
enddeTime: string
|
|
40
|
+
eventDt: string
|
|
41
|
+
eventStime: string
|
|
42
|
+
eventEtime: string
|
|
43
|
+
applyCnt: string
|
|
44
|
+
place: string
|
|
45
|
+
}): string {
|
|
46
|
+
const option = (id: string, value: string, selected: string) =>
|
|
47
|
+
`<option value="${value}"${value === selected ? ' selected' : ''}>${id}</option>`
|
|
48
|
+
const radio = (name: string, value: string, checked: string) =>
|
|
49
|
+
`<input type="radio" name="${name}" value="${value}"${value === checked ? ' checked' : ''} />`
|
|
50
|
+
return `<form id="board">
|
|
51
|
+
<input type="hidden" name="qustnrSn" value="${fields.qustnrSn}" />
|
|
52
|
+
<input type="hidden" name="stateCd" value="A" />
|
|
53
|
+
${radio('reportCd', 'MRC010', fields.reportCd)}
|
|
54
|
+
${radio('reportCd', 'MRC020', fields.reportCd)}
|
|
55
|
+
<input type="text" name="qustnrSj" value="${fields.qustnrSj}" />
|
|
56
|
+
${radio('receiptType', 'UNTIL_LECTURE', fields.receiptType)}
|
|
57
|
+
${radio('receiptType', 'DIRECT', fields.receiptType)}
|
|
58
|
+
<input type="text" name="bgndeDate" value="${fields.bgndeDate}" />
|
|
59
|
+
<select name="bgndeTime">${option(fields.bgndeTime, fields.bgndeTime, fields.bgndeTime)}</select>
|
|
60
|
+
<input type="text" name="enddeDate" value="${fields.enddeDate}" />
|
|
61
|
+
<select name="enddeTime">${option(fields.enddeTime, fields.enddeTime, fields.enddeTime)}</select>
|
|
62
|
+
<input type="text" name="eventDt" value="${fields.eventDt}" />
|
|
63
|
+
<select name="eventStime">${option(fields.eventStime, fields.eventStime, fields.eventStime)}</select>
|
|
64
|
+
<select name="eventEtime">${option(fields.eventEtime, fields.eventEtime, fields.eventEtime)}</select>
|
|
65
|
+
<input type="text" name="applyCnt" value="${fields.applyCnt}" />
|
|
66
|
+
<select name="place">${option(fields.place, fields.place, fields.place)}</select>
|
|
67
|
+
</form>`
|
|
68
|
+
}
|
|
69
|
+
|
|
30
70
|
function createFakeHttp(config: FakeHttpConfig = {}): { http: SomaHttp; calls: HttpCall[] } {
|
|
31
71
|
const calls: HttpCall[] = []
|
|
32
72
|
const sequence = config.checkLoginSequence ? [...config.checkLoginSequence] : null
|
|
@@ -50,6 +90,10 @@ function createFakeHttp(config: FakeHttpConfig = {}): { http: SomaHttp; calls: H
|
|
|
50
90
|
calls.push({ method: 'postForm', path, data })
|
|
51
91
|
return config.postFormBody ? config.postFormBody(path, data) : ''
|
|
52
92
|
},
|
|
93
|
+
postJson: async (path: string, data: Record<string, string>) => {
|
|
94
|
+
calls.push({ method: 'postJson', path, data })
|
|
95
|
+
return config.postJsonBody ? config.postJsonBody(path, data) : {}
|
|
96
|
+
},
|
|
53
97
|
postMultipart: async () => '',
|
|
54
98
|
login: async (username: string, password: string) => {
|
|
55
99
|
config.onLogin?.(username, password)
|
|
@@ -120,12 +164,27 @@ describe('SomaClient', () => {
|
|
|
120
164
|
expect(result).toMatchObject({ id: 99, title: '상세', venue: '온라인(Webex)' })
|
|
121
165
|
})
|
|
122
166
|
|
|
123
|
-
it('posts the expected payloads for create, update, delete, apply, cancel,
|
|
167
|
+
it('posts the expected payloads for create, update, delete, apply, cancel, and reserve', async () => {
|
|
124
168
|
const mentoringDetailHtml =
|
|
125
169
|
'<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>'
|
|
170
|
+
const mentoringEditFormHtml = buildMentoringEditFormFixture({
|
|
171
|
+
qustnrSn: '42',
|
|
172
|
+
qustnrSj: '기존 멘토링',
|
|
173
|
+
reportCd: 'MRC010',
|
|
174
|
+
receiptType: 'UNTIL_LECTURE',
|
|
175
|
+
bgndeDate: '2026-03-01',
|
|
176
|
+
bgndeTime: '00:00',
|
|
177
|
+
enddeDate: '2026-03-20',
|
|
178
|
+
enddeTime: '10:00',
|
|
179
|
+
eventDt: '2026-03-20',
|
|
180
|
+
eventStime: '10:00',
|
|
181
|
+
eventEtime: '12:00',
|
|
182
|
+
applyCnt: '5',
|
|
183
|
+
place: '온라인(Webex)',
|
|
184
|
+
})
|
|
126
185
|
const { http, calls } = createFakeHttp({
|
|
127
186
|
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
128
|
-
getBody: () => mentoringDetailHtml,
|
|
187
|
+
getBody: (path) => (path === '/mypage/mentoLec/forUpdate.do' ? mentoringEditFormHtml : mentoringDetailHtml),
|
|
129
188
|
})
|
|
130
189
|
const client = new SomaClient({ http })
|
|
131
190
|
|
|
@@ -155,7 +214,6 @@ describe('SomaClient', () => {
|
|
|
155
214
|
slots: ['10:00', '10:30'],
|
|
156
215
|
title: '회의',
|
|
157
216
|
})
|
|
158
|
-
await client.event.apply(11)
|
|
159
217
|
|
|
160
218
|
const writes = calls.filter((call) => call.method !== 'get')
|
|
161
219
|
expect(writes.map((call) => `${call.method}:${call.path}`)).toEqual([
|
|
@@ -165,7 +223,6 @@ describe('SomaClient', () => {
|
|
|
165
223
|
'post:/application/application/application.do',
|
|
166
224
|
'post:/mypage/userAnswer/cancel.do',
|
|
167
225
|
'post:/mypage/itemRent/insert.do',
|
|
168
|
-
'post:/application/application/application.do',
|
|
169
226
|
])
|
|
170
227
|
expect(writes[0]?.data).toMatchObject({
|
|
171
228
|
menuNo: MENU_NO.MENTORING,
|
|
@@ -184,7 +241,7 @@ describe('SomaClient', () => {
|
|
|
184
241
|
pageQueryString: '',
|
|
185
242
|
})
|
|
186
243
|
expect(writes[3]?.data).toEqual({
|
|
187
|
-
menuNo: MENU_NO.
|
|
244
|
+
menuNo: MENU_NO.MENTORING,
|
|
188
245
|
qustnrSn: '8',
|
|
189
246
|
applyGb: 'C',
|
|
190
247
|
stepHeader: '0',
|
|
@@ -201,20 +258,276 @@ describe('SomaClient', () => {
|
|
|
201
258
|
'time[0]': '10:00',
|
|
202
259
|
'time[1]': '10:30',
|
|
203
260
|
})
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('fetches, updates, and cancels room reservations via itemRent endpoints', async () => {
|
|
264
|
+
const detailHtml = `
|
|
265
|
+
<form id="frm" method="post">
|
|
266
|
+
<input type="hidden" name="rentId" value="18718" />
|
|
267
|
+
<input type="hidden" name="itemId" value="17" />
|
|
268
|
+
<input type="hidden" name="receiptStatCd" value="RS001" />
|
|
269
|
+
<input type="hidden" name="title" value="멘토링" />
|
|
270
|
+
<input type="hidden" name="rentDt" value="2026-05-31" />
|
|
271
|
+
<input type="hidden" name="rentBgnde" value="2026-05-31 21:00:00.0" />
|
|
272
|
+
<input type="hidden" name="rentEndde" value="2026-05-31 21:30:00.0" />
|
|
273
|
+
<input type="hidden" name="infoCn" value="" />
|
|
274
|
+
<input type="hidden" name="rentNum" value="4" />
|
|
275
|
+
</form>
|
|
276
|
+
`
|
|
277
|
+
const { http, calls } = createFakeHttp({
|
|
278
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
279
|
+
getBody: (path) => (path === '/mypage/itemRent/view.do' ? detailHtml : ''),
|
|
280
|
+
})
|
|
281
|
+
const client = new SomaClient({ http })
|
|
282
|
+
|
|
283
|
+
const detail = await client.room.get(18718)
|
|
284
|
+
expect(detail).toEqual({
|
|
285
|
+
rentId: 18718,
|
|
286
|
+
itemId: 17,
|
|
287
|
+
title: '멘토링',
|
|
288
|
+
date: '2026-05-31',
|
|
289
|
+
startTime: '21:00',
|
|
290
|
+
endTime: '21:30',
|
|
291
|
+
attendees: 4,
|
|
292
|
+
notes: '',
|
|
293
|
+
status: 'confirmed',
|
|
294
|
+
statusCode: 'RS001',
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
await client.room.update(18718, { title: '스터디', notes: '회고 공유' })
|
|
298
|
+
await client.room.update(18718, { slots: ['22:00', '22:30'] })
|
|
299
|
+
await client.room.cancel(18718)
|
|
300
|
+
|
|
301
|
+
const updateCalls = calls.filter((call) => call.path === '/mypage/itemRent/update.do')
|
|
302
|
+
expect(updateCalls).toHaveLength(3)
|
|
303
|
+
expect(updateCalls[0]?.data).toMatchObject({
|
|
304
|
+
rentId: '18718',
|
|
305
|
+
itemId: '17',
|
|
306
|
+
receiptStatCd: 'RS001',
|
|
307
|
+
title: '스터디',
|
|
308
|
+
infoCn: '회고 공유',
|
|
309
|
+
rentBgnde: '2026-05-31 21:00:00',
|
|
310
|
+
rentEndde: '2026-05-31 21:30:00',
|
|
311
|
+
})
|
|
312
|
+
expect(updateCalls[0]?.data?.['time[0]']).toBeUndefined()
|
|
313
|
+
expect(updateCalls[1]?.data).toMatchObject({
|
|
314
|
+
rentId: '18718',
|
|
315
|
+
title: '멘토링',
|
|
316
|
+
rentBgnde: '2026-05-31 22:00:00',
|
|
317
|
+
rentEndde: '2026-05-31 22:59:00',
|
|
318
|
+
'time[0]': '22:00',
|
|
319
|
+
'time[1]': '22:30',
|
|
320
|
+
chkData_1: '2026-05-31|22:00|17',
|
|
321
|
+
chkData_2: '2026-05-31|22:30|17',
|
|
322
|
+
})
|
|
323
|
+
expect(updateCalls[2]?.data).toMatchObject({
|
|
324
|
+
rentId: '18718',
|
|
325
|
+
receiptStatCd: 'RS002',
|
|
326
|
+
title: '멘토링',
|
|
327
|
+
rentBgnde: '2026-05-31 21:00:00',
|
|
328
|
+
rentEndde: '2026-05-31 21:30:00',
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
const viewCalls = calls.filter((call) => call.path === '/mypage/itemRent/view.do')
|
|
332
|
+
expect(viewCalls).toHaveLength(4)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('swallows the SWMaestro success-alert thrown by SomaHttp on room update', async () => {
|
|
336
|
+
const detailHtml = `
|
|
337
|
+
<form id="frm" method="post">
|
|
338
|
+
<input type="hidden" name="rentId" value="18718" />
|
|
339
|
+
<input type="hidden" name="itemId" value="17" />
|
|
340
|
+
<input type="hidden" name="receiptStatCd" value="RS001" />
|
|
341
|
+
<input type="hidden" name="title" value="멘토링" />
|
|
342
|
+
<input type="hidden" name="rentDt" value="2026-05-31" />
|
|
343
|
+
<input type="hidden" name="rentBgnde" value="2026-05-31 21:00:00.0" />
|
|
344
|
+
<input type="hidden" name="rentEndde" value="2026-05-31 21:30:00.0" />
|
|
345
|
+
<input type="hidden" name="infoCn" value="" />
|
|
346
|
+
<input type="hidden" name="rentNum" value="4" />
|
|
347
|
+
</form>
|
|
348
|
+
`
|
|
349
|
+
const { http } = createFakeHttp({
|
|
350
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
351
|
+
getBody: () => detailHtml,
|
|
352
|
+
postBody: (path) => {
|
|
353
|
+
if (path === '/mypage/itemRent/update.do') {
|
|
354
|
+
throw new Error('정상적으로 수정하였습니다.')
|
|
355
|
+
}
|
|
356
|
+
return ''
|
|
357
|
+
},
|
|
209
358
|
})
|
|
359
|
+
const client = new SomaClient({ http })
|
|
360
|
+
|
|
361
|
+
await expect(client.room.update(18718, { title: '변경' })).resolves.toBeUndefined()
|
|
362
|
+
await expect(client.room.cancel(18718)).resolves.toBeUndefined()
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('lists room reservations filtered by default to confirmed status via itemRent list endpoint', async () => {
|
|
366
|
+
const listHtml = `
|
|
367
|
+
<ul class="bbs-total">
|
|
368
|
+
<li><strong>Total :</strong> 2</li>
|
|
369
|
+
<li><span>1</span>/1 Page</li>
|
|
370
|
+
</ul>
|
|
371
|
+
<table>
|
|
372
|
+
<thead>
|
|
373
|
+
<tr><th>NO.</th><th>회의실 명</th><th>제목</th><th>사용기간</th><th>작성자</th><th>상태</th><th>등록일</th></tr>
|
|
374
|
+
</thead>
|
|
375
|
+
<tbody>
|
|
376
|
+
<tr>
|
|
377
|
+
<td>2</td>
|
|
378
|
+
<td><a href="/sw/mypage/itemRent/view.do?rentId=18618">스페이스 M1</a></td>
|
|
379
|
+
<td>
|
|
380
|
+
<div class="rel">
|
|
381
|
+
<a href="/sw/mypage/itemRent/view.do?rentId=18618">멘토 특강</a>
|
|
382
|
+
<span>예약완료</span>
|
|
383
|
+
</div>
|
|
384
|
+
</td>
|
|
385
|
+
<td>2026.05.31 16:00 ~ 17:30</td>
|
|
386
|
+
<td>전수열</td>
|
|
387
|
+
<td>예약완료</td>
|
|
388
|
+
<td>2026.04.20</td>
|
|
389
|
+
</tr>
|
|
390
|
+
<tr>
|
|
391
|
+
<td>1</td>
|
|
392
|
+
<td><a href="/sw/mypage/itemRent/view.do?rentId=18616">스페이스 A3</a></td>
|
|
393
|
+
<td>
|
|
394
|
+
<div class="rel">
|
|
395
|
+
<a href="/sw/mypage/itemRent/view.do?rentId=18616">자유 멘토링</a>
|
|
396
|
+
<span>예약완료</span>
|
|
397
|
+
</div>
|
|
398
|
+
</td>
|
|
399
|
+
<td>2026.05.24 10:00 ~ 11:00</td>
|
|
400
|
+
<td>전수열</td>
|
|
401
|
+
<td>예약완료</td>
|
|
402
|
+
<td>2026.04.15</td>
|
|
403
|
+
</tr>
|
|
404
|
+
</tbody>
|
|
405
|
+
</table>
|
|
406
|
+
`
|
|
407
|
+
const { http, calls } = createFakeHttp({
|
|
408
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
409
|
+
getBody: (path) => (path === '/mypage/itemRent/list.do' ? listHtml : ''),
|
|
410
|
+
})
|
|
411
|
+
const client = new SomaClient({ http })
|
|
412
|
+
|
|
413
|
+
const result = await client.room.reservations({ startDate: '2026-01-01', endDate: '2026-12-31' })
|
|
414
|
+
|
|
415
|
+
expect(calls).toEqual([
|
|
416
|
+
{
|
|
417
|
+
method: 'get',
|
|
418
|
+
path: '/mypage/itemRent/list.do',
|
|
419
|
+
data: {
|
|
420
|
+
menuNo: MENU_NO.ROOM,
|
|
421
|
+
pageIndex: '1',
|
|
422
|
+
sdate: '2026-01-01',
|
|
423
|
+
edate: '2026-12-31',
|
|
424
|
+
searchStat: 'RS001',
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
])
|
|
428
|
+
expect(result.pagination).toEqual({ total: 2, currentPage: 1, totalPages: 1 })
|
|
429
|
+
expect(result.items).toEqual([
|
|
430
|
+
{
|
|
431
|
+
rentId: 18618,
|
|
432
|
+
venue: '스페이스 M1',
|
|
433
|
+
title: '멘토 특강',
|
|
434
|
+
date: '2026-05-31',
|
|
435
|
+
startTime: '16:00',
|
|
436
|
+
endTime: '17:30',
|
|
437
|
+
author: '전수열',
|
|
438
|
+
status: 'confirmed',
|
|
439
|
+
statusLabel: '예약완료',
|
|
440
|
+
registeredAt: '2026.04.20',
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
rentId: 18616,
|
|
444
|
+
venue: '스페이스 A3',
|
|
445
|
+
title: '자유 멘토링',
|
|
446
|
+
date: '2026-05-24',
|
|
447
|
+
startTime: '10:00',
|
|
448
|
+
endTime: '11:00',
|
|
449
|
+
author: '전수열',
|
|
450
|
+
status: 'confirmed',
|
|
451
|
+
statusLabel: '예약완료',
|
|
452
|
+
registeredAt: '2026.04.15',
|
|
453
|
+
},
|
|
454
|
+
])
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('omits the searchStat filter when status is "all"', async () => {
|
|
458
|
+
const { http, calls } = createFakeHttp({
|
|
459
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
460
|
+
getBody: () => '<ul class="bbs-total"><li>Total : 0</li></ul>',
|
|
461
|
+
})
|
|
462
|
+
const client = new SomaClient({ http })
|
|
463
|
+
|
|
464
|
+
await client.room.reservations({ status: 'all', page: 3 })
|
|
465
|
+
|
|
466
|
+
expect(calls[0]?.data).toEqual({ menuNo: MENU_NO.ROOM, pageIndex: '3' })
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('sends searchStat=RS002 when listing cancelled reservations', async () => {
|
|
470
|
+
const { http, calls } = createFakeHttp({
|
|
471
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
472
|
+
getBody: () => '<ul class="bbs-total"><li>Total : 0</li></ul>',
|
|
473
|
+
})
|
|
474
|
+
const client = new SomaClient({ http })
|
|
475
|
+
|
|
476
|
+
await client.room.reservations({ status: 'cancelled' })
|
|
477
|
+
|
|
478
|
+
expect(calls[0]?.data).toMatchObject({ searchStat: 'RS002' })
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
it('re-raises genuine errors thrown by SomaHttp on room update', async () => {
|
|
482
|
+
const detailHtml = `
|
|
483
|
+
<form id="frm" method="post">
|
|
484
|
+
<input type="hidden" name="rentId" value="18718" />
|
|
485
|
+
<input type="hidden" name="itemId" value="17" />
|
|
486
|
+
<input type="hidden" name="receiptStatCd" value="RS001" />
|
|
487
|
+
<input type="hidden" name="title" value="멘토링" />
|
|
488
|
+
<input type="hidden" name="rentDt" value="2026-05-31" />
|
|
489
|
+
<input type="hidden" name="rentBgnde" value="2026-05-31 21:00:00.0" />
|
|
490
|
+
<input type="hidden" name="rentEndde" value="2026-05-31 21:30:00.0" />
|
|
491
|
+
<input type="hidden" name="infoCn" value="" />
|
|
492
|
+
<input type="hidden" name="rentNum" value="4" />
|
|
493
|
+
</form>
|
|
494
|
+
`
|
|
495
|
+
const { http } = createFakeHttp({
|
|
496
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
497
|
+
getBody: () => detailHtml,
|
|
498
|
+
postBody: (path) => {
|
|
499
|
+
if (path === '/mypage/itemRent/update.do') {
|
|
500
|
+
throw new Error('권한이 없습니다.')
|
|
501
|
+
}
|
|
502
|
+
return ''
|
|
503
|
+
},
|
|
504
|
+
})
|
|
505
|
+
const client = new SomaClient({ http })
|
|
506
|
+
|
|
507
|
+
await expect(client.room.update(18718, { title: '변경' })).rejects.toThrow('권한이 없습니다.')
|
|
210
508
|
})
|
|
211
509
|
|
|
212
510
|
it('merges partial update params with the existing mentoring data', async () => {
|
|
213
511
|
const mentoringDetailHtml =
|
|
214
512
|
'<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>'
|
|
513
|
+
const mentoringEditFormHtml = buildMentoringEditFormFixture({
|
|
514
|
+
qustnrSn: '9572',
|
|
515
|
+
qustnrSj: '웹 성능 특강',
|
|
516
|
+
reportCd: 'MRC020',
|
|
517
|
+
receiptType: 'UNTIL_LECTURE',
|
|
518
|
+
bgndeDate: '2026-04-01',
|
|
519
|
+
bgndeTime: '00:00',
|
|
520
|
+
enddeDate: '2026-04-11',
|
|
521
|
+
enddeTime: '14:00',
|
|
522
|
+
eventDt: '2026-04-11',
|
|
523
|
+
eventStime: '14:00',
|
|
524
|
+
eventEtime: '15:30',
|
|
525
|
+
applyCnt: '20',
|
|
526
|
+
place: '온라인(Webex)',
|
|
527
|
+
})
|
|
215
528
|
const { http, calls } = createFakeHttp({
|
|
216
529
|
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
217
|
-
getBody: () => mentoringDetailHtml,
|
|
530
|
+
getBody: (path) => (path === '/mypage/mentoLec/forUpdate.do' ? mentoringEditFormHtml : mentoringDetailHtml),
|
|
218
531
|
})
|
|
219
532
|
const client = new SomaClient({ http })
|
|
220
533
|
|
|
@@ -231,13 +544,18 @@ describe('SomaClient', () => {
|
|
|
231
544
|
eventEtime: '15:30',
|
|
232
545
|
place: '온라인(Webex)',
|
|
233
546
|
applyCnt: '20',
|
|
234
|
-
|
|
235
|
-
|
|
547
|
+
bgndeDate: '2026-04-01',
|
|
548
|
+
bgndeTime: '00:00',
|
|
549
|
+
enddeDate: '2026-04-11',
|
|
550
|
+
enddeTime: '14:00',
|
|
551
|
+
receiptType: 'UNTIL_LECTURE',
|
|
552
|
+
stateCd: 'A',
|
|
553
|
+
qustnrAt: 'N',
|
|
236
554
|
qestnarCn: '<p>세션 본문</p>',
|
|
237
555
|
})
|
|
238
556
|
})
|
|
239
557
|
|
|
240
|
-
it('routes room, dashboard, notice, team, member,
|
|
558
|
+
it('routes room, dashboard, notice, team, member, and history calls to the expected endpoints', async () => {
|
|
241
559
|
const { http, calls } = createFakeHttp({
|
|
242
560
|
identity: { userId: 'neo@example.com', userNm: '전수열' },
|
|
243
561
|
getBody: (path) => {
|
|
@@ -259,12 +577,6 @@ describe('SomaClient', () => {
|
|
|
259
577
|
if (path === '/mypage/myInfo/forUpdateMy.do') {
|
|
260
578
|
return '<dl><dt><span class="point">아이디</span></dt><dd>neo@example.com</dd></dl><dl><dt><span class="point">이름</span></dt><dd>전수열</dd></dl><dl><dt><span class="point">성별</span></dt><dd>남자</dd></dl><dl><dt><span class="point">생년월일</span></dt><dd>1995-01-14</dd></dl><dl><dt><span class="point">연락처</span></dt><dd>01012345678</dd></dl><dl><dt><span class="point">소속</span></dt><dd>Indent</dd></dl><dl><dt><span class="point">직책</span></dt><dd></dd></dl>'
|
|
261
579
|
}
|
|
262
|
-
if (path === '/mypage/applicants/list.do') {
|
|
263
|
-
return '<table><tbody><tr><td>1</td><td>행사</td><td>행사</td><td>2026-04-01 ~ 2026-04-02</td><td>2026-04-03 ~ 2026-04-03</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>'
|
|
264
|
-
}
|
|
265
|
-
if (path === '/mypage/applicants/view.do') {
|
|
266
|
-
return '<input name="bbsId" value="1"><table><tr><th>제목</th><td>행사 상세</td></tr></table><div data-content><p>본문</p></div>'
|
|
267
|
-
}
|
|
268
580
|
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>'
|
|
269
581
|
},
|
|
270
582
|
postBody: (path) => {
|
|
@@ -281,10 +593,8 @@ describe('SomaClient', () => {
|
|
|
281
593
|
const dashboard = await client.dashboard.get()
|
|
282
594
|
const noticeList = await client.notice.list({ page: 2 })
|
|
283
595
|
const noticeDetail = await client.notice.get(1)
|
|
284
|
-
const team = await client.team.
|
|
596
|
+
const team = await client.team.list()
|
|
285
597
|
const member = await client.member.show()
|
|
286
|
-
const events = await client.event.list({ page: 3 })
|
|
287
|
-
const eventDetail = await client.event.get(1)
|
|
288
598
|
const history = await client.mentoring.history({ page: 4 })
|
|
289
599
|
|
|
290
600
|
expect(roomList[0]?.itemId).toBe(17)
|
|
@@ -306,16 +616,16 @@ describe('SomaClient', () => {
|
|
|
306
616
|
type: '멘토 특강',
|
|
307
617
|
},
|
|
308
618
|
])
|
|
619
|
+
expect(dashboard.teams.map((t) => t.name)).toEqual(['오픈소마'])
|
|
309
620
|
expect(noticeList.items[0]?.title).toBe('공지')
|
|
310
621
|
expect(noticeDetail).toMatchObject({ id: 1, title: '공지' })
|
|
311
622
|
expect(team.teams[0]?.name).toBe('오픈소마')
|
|
312
623
|
expect(member.email).toBe('neo@example.com')
|
|
313
|
-
expect(events.items[0]?.title).toBe('행사')
|
|
314
|
-
expect(eventDetail).toMatchObject({ id: 1, title: '행사 상세' })
|
|
315
624
|
expect(history.items[0]).toEqual({
|
|
316
625
|
id: 1,
|
|
317
626
|
category: '멘토 특강',
|
|
318
627
|
title: '접수내역',
|
|
628
|
+
url: '/sw/mypage/mentoLec/view.do?qustnrSn=1',
|
|
319
629
|
author: '전수열',
|
|
320
630
|
sessionDate: '2026-04-11',
|
|
321
631
|
appliedAt: '2026-04-01',
|
|
@@ -340,6 +650,292 @@ describe('SomaClient', () => {
|
|
|
340
650
|
})
|
|
341
651
|
})
|
|
342
652
|
|
|
653
|
+
it('fills trainee dashboard mentoring sessions from all upcoming application history pages', async () => {
|
|
654
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
655
|
+
const todayDotted = today.replaceAll('-', '.')
|
|
656
|
+
const { http, calls } = createFakeHttp({
|
|
657
|
+
identity: { userId: 'trainee@example.com', userNm: '김연수' },
|
|
658
|
+
getBody: (path, data) => {
|
|
659
|
+
if (path === '/mypage/myMain/dashboard.do') {
|
|
660
|
+
return '<ul class="dash-top"><li class="dash-card"><div class="dash-etc"><span>소속 :<br> OpenSoma</span><span>직책 :<br> </span></div><div class="dash-state"><div class="top"><span class="bg-blue label"><span>17기 연수생</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=999">네이티브 항목 접수중</a></li></li></ul>'
|
|
661
|
+
}
|
|
662
|
+
if (path === '/mypage/userAnswer/history.do') {
|
|
663
|
+
const page = Number(data?.pageIndex ?? '1')
|
|
664
|
+
if (page === 1) {
|
|
665
|
+
return `<table><tbody>
|
|
666
|
+
<tr><td>45</td><td>멘토특강</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=39">미래 특강</a></td><td>김멘토</td><td>2099.01.01(목) 10:00:00 ~ 12:00:00</td><td>2026-04-20 22:41</td><td>[접수완료]</td><td>[OK]</td><td>-</td><td>-</td></tr>
|
|
667
|
+
<tr><td>44</td><td>멘토특강</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=40">이미 지난 특강</a></td><td>장영원</td><td>2000.01.01(토) 20:00:00 ~ 22:30:00</td><td>2026-04-24 22:41</td><td>[접수완료]</td><td>[OK]</td><td>-</td><td>-</td></tr>
|
|
668
|
+
<tr><td>43</td><td>자유멘토링</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=41">취소한 멘토링</a></td><td>이현수</td><td>2099.01.01(목) 20:00:00 ~ 21:30:00</td><td>2026-04-26 03:18</td><td>[접수취소]</td><td>[OK]</td><td>-</td><td>-</td></tr>
|
|
669
|
+
</tbody></table><ul class="bbs-total"><li>Total : 5</li><li>1/2 Page</li></ul>`
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return `<table><tbody>
|
|
673
|
+
<tr><td>42</td><td>자유멘토링</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=44">팀 프로젝트 방향성 피드백</a></td><td>이상철</td><td>${todayDotted}(화) 22:00:00 ~ 24:00:00</td><td>2026-04-27 22:41</td><td>[접수완료]</td><td>[OK]</td><td>-</td><td>-</td></tr>
|
|
674
|
+
<tr><td>41</td><td>행사</td><td><a href="/sw/mypage/applicants/view.do?bbsId=38">멘토링이 아닌 행사</a></td><td>관리자</td><td>2099.01.02(금) 10:00:00 ~ 12:00:00</td><td>2026-04-20 22:41</td><td>[접수완료]</td><td>[OK]</td><td>-</td><td>-</td></tr>
|
|
675
|
+
</tbody></table><ul class="bbs-total"><li>Total : 5</li><li>2/2 Page</li></ul>`
|
|
676
|
+
}
|
|
677
|
+
return ''
|
|
678
|
+
},
|
|
679
|
+
})
|
|
680
|
+
const client = new SomaClient({ http })
|
|
681
|
+
|
|
682
|
+
const dashboard = await client.dashboard.get()
|
|
683
|
+
|
|
684
|
+
expect(dashboard.role).toBe('17기 연수생')
|
|
685
|
+
expect(dashboard.mentoringSessions).toEqual([
|
|
686
|
+
{
|
|
687
|
+
title: '팀 프로젝트 방향성 피드백',
|
|
688
|
+
url: '/sw/mypage/mentoLec/view.do?qustnrSn=44',
|
|
689
|
+
status: '접수완료',
|
|
690
|
+
date: today,
|
|
691
|
+
time: '22:00',
|
|
692
|
+
timeEnd: '24:00',
|
|
693
|
+
type: '자유 멘토링',
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
title: '미래 특강',
|
|
697
|
+
url: '/sw/mypage/mentoLec/view.do?qustnrSn=39',
|
|
698
|
+
status: '접수완료',
|
|
699
|
+
date: '2099-01-01',
|
|
700
|
+
time: '10:00',
|
|
701
|
+
timeEnd: '12:00',
|
|
702
|
+
type: '멘토 특강',
|
|
703
|
+
},
|
|
704
|
+
])
|
|
705
|
+
const historyPages = calls
|
|
706
|
+
.filter((c) => c.path === '/mypage/userAnswer/history.do')
|
|
707
|
+
.map((c) => c.data?.pageIndex ?? '1')
|
|
708
|
+
.sort()
|
|
709
|
+
expect(historyPages).toEqual(['1', '2'])
|
|
710
|
+
expect(calls.some((c) => c.path === '/mypage/mentoLec/list.do')).toBe(false)
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
it('enriches dashboard with every team the mentor belongs to', async () => {
|
|
714
|
+
const teamCard = (name: string, ownerId: string, teamId: string) =>
|
|
715
|
+
`<li><div class="top"><strong class="t"><a href="javascript:void(0);" onclick="teamPageGo('${name}','${ownerId}','${teamId}');">${name}</a></strong></div><p>팀원 3명</p><button type="button">참여중</button></li>`
|
|
716
|
+
const { http, calls } = createFakeHttp({
|
|
717
|
+
identity: { userId: 'mentor@example.com', userNm: 'Mentor One' },
|
|
718
|
+
getBody: (path) => {
|
|
719
|
+
if (path === '/mypage/myMain/dashboard.do') {
|
|
720
|
+
return '<ul class="dash-top"><li class="dash-card"><div class="dash-etc"><span>소속 :<br> Org</span><span>직책 :<br> </span></div><div class="dash-state"><div class="top"><span class="bg-orange label"><span>멘토</span></span><div class="welcome"><strong>Mentor One</strong>님 안녕하세요.</div></div><ul class="dash-box"><li><strong class="t">팀명</strong><div class="c">Team Alpha</div></li><li><strong class="t">팀원</strong><div class="c">Member A,Member B,Member C</div></li><li><strong class="t">멘토</strong><div class="c">Mentor One,Mentor Two</div></li></ul></div></li></ul>'
|
|
721
|
+
}
|
|
722
|
+
if (path === '/mypage/myTeam/team.do') {
|
|
723
|
+
return `<ul class="bbs-team">${teamCard('Team Alpha', 'Mentor One', 'team-alpha')}${teamCard('Team Beta', 'Mentor Three', 'team-beta')}</ul><p class="ico-team">현재 참여중인 방은 <strong class="color-blue">2</strong>/100팀 입니다</p>`
|
|
724
|
+
}
|
|
725
|
+
return '<table><tbody></tbody></table><ul class="bbs-total"><li>Total : 0</li><li>1/1 Page</li></ul>'
|
|
726
|
+
},
|
|
727
|
+
})
|
|
728
|
+
const client = new SomaClient({ http })
|
|
729
|
+
|
|
730
|
+
const dashboard = await client.dashboard.get()
|
|
731
|
+
|
|
732
|
+
expect(dashboard.team).toEqual({
|
|
733
|
+
name: 'Team Alpha',
|
|
734
|
+
members: 'Member A,Member B,Member C',
|
|
735
|
+
mentor: 'Mentor One,Mentor Two',
|
|
736
|
+
})
|
|
737
|
+
expect(dashboard.teams.map((t) => ({ name: t.name, teamId: t.teamId }))).toEqual([
|
|
738
|
+
{ name: 'Team Alpha', teamId: 'team-alpha' },
|
|
739
|
+
{ name: 'Team Beta', teamId: 'team-beta' },
|
|
740
|
+
])
|
|
741
|
+
const teamCall = calls.find((c) => c.path === '/mypage/myTeam/team.do')
|
|
742
|
+
expect(teamCall?.data).toMatchObject({
|
|
743
|
+
menuNo: MENU_NO.TEAM,
|
|
744
|
+
searchCnd: '2',
|
|
745
|
+
searchWrd: 'Mentor One',
|
|
746
|
+
})
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
it('enriches trainee dashboard with every team the trainee belongs to', async () => {
|
|
750
|
+
const { http, calls } = createFakeHttp({
|
|
751
|
+
identity: { userId: 'trainee@example.com', userNm: 'Trainee One' },
|
|
752
|
+
getBody: (path) => {
|
|
753
|
+
if (path === '/mypage/myMain/dashboard.do') {
|
|
754
|
+
return '<ul class="dash-top"><li class="dash-card"><div class="dash-etc"><span>소속 :<br> Org</span><span>직책 :<br> </span></div><div class="dash-state"><div class="top"><span class="bg-blue label"><span>17기 연수생</span></span><div class="welcome"><strong>Trainee One</strong>님 안녕하세요.</div></div></div></li></ul>'
|
|
755
|
+
}
|
|
756
|
+
if (path === '/mypage/myTeam/team.do') {
|
|
757
|
+
return `<ul class="bbs-team"><li><div class="top"><strong class="t"><a href="javascript:void(0);" onclick="teamPageGo('Mentor One','owner-1','team-1');">Team Alpha</a></strong></div><p>팀원 3명</p><button type="button">참여중</button></li></ul><p class="ico-team">현재 참여중인 방은 <strong class="color-blue">1</strong>/100팀 입니다</p>`
|
|
758
|
+
}
|
|
759
|
+
return '<table><tbody></tbody></table><ul class="bbs-total"><li>Total : 0</li><li>1/1 Page</li></ul>'
|
|
760
|
+
},
|
|
761
|
+
})
|
|
762
|
+
const client = new SomaClient({ http })
|
|
763
|
+
|
|
764
|
+
const dashboard = await client.dashboard.get()
|
|
765
|
+
|
|
766
|
+
expect(dashboard.role).toBe('17기 연수생')
|
|
767
|
+
expect(dashboard.teams.map((t) => t.name)).toEqual(['Team Alpha'])
|
|
768
|
+
const teamCall = calls.find((c) => c.path === '/mypage/myTeam/team.do')
|
|
769
|
+
expect(teamCall?.data).toMatchObject({
|
|
770
|
+
menuNo: MENU_NO.TEAM,
|
|
771
|
+
searchCnd: '1',
|
|
772
|
+
searchWrd: 'Trainee One',
|
|
773
|
+
})
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
it('routes schedule calls to the expected endpoint', async () => {
|
|
777
|
+
const { http, calls } = createFakeHttp({
|
|
778
|
+
identity: { userId: 'neo@example.com', userNm: '전수열' },
|
|
779
|
+
getBody: (path) => {
|
|
780
|
+
if (path === '/mypage/schedule/list.do') {
|
|
781
|
+
return '<table><tbody><tr><td>1</td><td>팀78</td><td>배준서</td><td>이유제, 이중곤</td><td>-</td><td>-</td><td>-</td><td>-</td></tr></tbody></table><table><thead><tr><th>날짜</th><th>구분</th><th>제목</th></tr></thead><tbody><tr><td>2026-04-01 ~ 2026-04-02</td><td>행사</td><td>행사</td></tr></tbody></table>'
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return ''
|
|
785
|
+
},
|
|
786
|
+
})
|
|
787
|
+
const client = new SomaClient({ http })
|
|
788
|
+
|
|
789
|
+
const schedule = await client.schedule.list({ page: 5 })
|
|
790
|
+
|
|
791
|
+
expect(schedule.items).toEqual([
|
|
792
|
+
{
|
|
793
|
+
id: 1,
|
|
794
|
+
category: '행사',
|
|
795
|
+
title: '행사',
|
|
796
|
+
period: { start: '2026-04-01', end: '2026-04-02' },
|
|
797
|
+
},
|
|
798
|
+
])
|
|
799
|
+
expect(schedule.pagination).toEqual({ total: 1, currentPage: 1, totalPages: 1 })
|
|
800
|
+
expect(calls).toContainEqual({
|
|
801
|
+
method: 'get',
|
|
802
|
+
path: '/mypage/schedule/list.do',
|
|
803
|
+
data: { menuNo: MENU_NO.SCHEDULE, pageIndex: '5' },
|
|
804
|
+
})
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
it('joins a team by POSTing the native payload to updateUserTeamIn.json', async () => {
|
|
808
|
+
const { http, calls } = createFakeHttp({
|
|
809
|
+
identity: {
|
|
810
|
+
userId: 'neo@example.com',
|
|
811
|
+
userNm: '전수열',
|
|
812
|
+
userNo: 'f6d192ad3b3e4ee29f1d238714ab92c1',
|
|
813
|
+
userGb: UserGb.Mentor,
|
|
814
|
+
},
|
|
815
|
+
postJsonBody: () => ({ resultCode: 'success' }),
|
|
816
|
+
})
|
|
817
|
+
const client = new SomaClient({ http })
|
|
818
|
+
|
|
819
|
+
await client.team.join('60e6785c8c404142b12cf9ed2a3d811f')
|
|
820
|
+
|
|
821
|
+
expect(calls).toContainEqual({
|
|
822
|
+
method: 'postJson',
|
|
823
|
+
path: '/mypage/myTeam/updateUserTeamIn.json',
|
|
824
|
+
data: {
|
|
825
|
+
userNo: 'f6d192ad3b3e4ee29f1d238714ab92c1',
|
|
826
|
+
userNm: '전수열',
|
|
827
|
+
userGb: UserGb.Mentor,
|
|
828
|
+
teamNo: '60e6785c8c404142b12cf9ed2a3d811f',
|
|
829
|
+
},
|
|
830
|
+
})
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
it('leaves a team by POSTing the native payload to updateUserTeamOut.json', async () => {
|
|
834
|
+
const { http, calls } = createFakeHttp({
|
|
835
|
+
identity: {
|
|
836
|
+
userId: 'neo@example.com',
|
|
837
|
+
userNm: '전수열',
|
|
838
|
+
userNo: 'f6d192ad3b3e4ee29f1d238714ab92c1',
|
|
839
|
+
userGb: UserGb.Mentor,
|
|
840
|
+
},
|
|
841
|
+
postJsonBody: () => ({ resultCode: 'success' }),
|
|
842
|
+
})
|
|
843
|
+
const client = new SomaClient({ http })
|
|
844
|
+
|
|
845
|
+
await client.team.leave('60e6785c8c404142b12cf9ed2a3d811f')
|
|
846
|
+
|
|
847
|
+
expect(calls).toContainEqual({
|
|
848
|
+
method: 'postJson',
|
|
849
|
+
path: '/mypage/myTeam/updateUserTeamOut.json',
|
|
850
|
+
data: {
|
|
851
|
+
userNo: 'f6d192ad3b3e4ee29f1d238714ab92c1',
|
|
852
|
+
userNm: '전수열',
|
|
853
|
+
userGb: UserGb.Mentor,
|
|
854
|
+
teamNo: '60e6785c8c404142b12cf9ed2a3d811f',
|
|
855
|
+
},
|
|
856
|
+
})
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
it('throws when team.join receives a non-success resultCode', async () => {
|
|
860
|
+
const { http } = createFakeHttp({
|
|
861
|
+
identity: {
|
|
862
|
+
userId: 'neo@example.com',
|
|
863
|
+
userNm: '전수열',
|
|
864
|
+
userNo: 'f6d192ad3b3e4ee29f1d238714ab92c1',
|
|
865
|
+
userGb: UserGb.Mentor,
|
|
866
|
+
},
|
|
867
|
+
postJsonBody: () => ({ resultCode: 'fail' }),
|
|
868
|
+
})
|
|
869
|
+
const client = new SomaClient({ http })
|
|
870
|
+
|
|
871
|
+
await expect(client.team.join('team-1')).rejects.toThrow('팀 참여에 실패했습니다.')
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
it('throws when team.leave receives a non-success resultCode', async () => {
|
|
875
|
+
const { http } = createFakeHttp({
|
|
876
|
+
identity: {
|
|
877
|
+
userId: 'neo@example.com',
|
|
878
|
+
userNm: '전수열',
|
|
879
|
+
userNo: 'f6d192ad3b3e4ee29f1d238714ab92c1',
|
|
880
|
+
userGb: UserGb.Mentor,
|
|
881
|
+
},
|
|
882
|
+
postJsonBody: () => ({ resultCode: 'fail' }),
|
|
883
|
+
})
|
|
884
|
+
const client = new SomaClient({ http })
|
|
885
|
+
|
|
886
|
+
await expect(client.team.leave('team-1')).rejects.toThrow('팀 탈퇴에 실패했습니다.')
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
it('throws when team.join cannot resolve userNo', async () => {
|
|
890
|
+
const { http } = createFakeHttp({
|
|
891
|
+
identity: {
|
|
892
|
+
userId: 'neo@example.com',
|
|
893
|
+
userNm: '전수열',
|
|
894
|
+
userNo: '',
|
|
895
|
+
userGb: UserGb.Mentor,
|
|
896
|
+
},
|
|
897
|
+
})
|
|
898
|
+
const client = new SomaClient({ http })
|
|
899
|
+
|
|
900
|
+
await expect(client.team.join('team-1')).rejects.toThrow('userNo')
|
|
901
|
+
})
|
|
902
|
+
|
|
903
|
+
it('exhausts mentoring pagination so dashboard sessions span all pages', async () => {
|
|
904
|
+
const buildMentoringRow = (id: number, sessionDate: string) =>
|
|
905
|
+
`<tr><td>1</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=${id}">[멘토 특강] 세션 ${id} [접수중]</a></td><td>${sessionDate} ~ ${sessionDate}</td><td>${sessionDate}(목) 10:00 ~ 11:00</td><td>1 /4</td><td>OK</td><td>[접수중]</td><td>전수열</td><td>${sessionDate}</td></tr>`
|
|
906
|
+
const buildPaginatedListBody = (rows: string, currentPage: number, totalPages: number, total: number) =>
|
|
907
|
+
`<table><tbody>${rows}</tbody></table><ul class="bbs-total"><li>Total : ${total}</li><li>${currentPage}/${totalPages} Page</li></ul>`
|
|
908
|
+
const { http, calls } = createFakeHttp({
|
|
909
|
+
identity: { userId: 'neo@example.com', userNm: '전수열' },
|
|
910
|
+
getBody: (path, data) => {
|
|
911
|
+
if (path === '/mypage/myMain/dashboard.do') {
|
|
912
|
+
return ''
|
|
913
|
+
}
|
|
914
|
+
if (path === '/mypage/mentoLec/list.do') {
|
|
915
|
+
const page = Number(data?.pageIndex ?? '1')
|
|
916
|
+
if (page === 1) return buildPaginatedListBody(buildMentoringRow(103, '2026-04-17'), 1, 3, 3)
|
|
917
|
+
if (page === 2) return buildPaginatedListBody(buildMentoringRow(101, '2026-04-03'), 2, 3, 3)
|
|
918
|
+
if (page === 3) return buildPaginatedListBody(buildMentoringRow(102, '2026-04-10'), 3, 3, 3)
|
|
919
|
+
}
|
|
920
|
+
return ''
|
|
921
|
+
},
|
|
922
|
+
})
|
|
923
|
+
const client = new SomaClient({ http })
|
|
924
|
+
|
|
925
|
+
const dashboard = await client.dashboard.get()
|
|
926
|
+
|
|
927
|
+
expect(dashboard.mentoringSessions.map((s) => s.url)).toEqual([
|
|
928
|
+
'/mypage/mentoLec/view.do?qustnrSn=101',
|
|
929
|
+
'/mypage/mentoLec/view.do?qustnrSn=102',
|
|
930
|
+
'/mypage/mentoLec/view.do?qustnrSn=103',
|
|
931
|
+
])
|
|
932
|
+
const listPages = calls
|
|
933
|
+
.filter((c) => c.path === '/mypage/mentoLec/list.do')
|
|
934
|
+
.map((c) => c.data?.pageIndex ?? '1')
|
|
935
|
+
.sort()
|
|
936
|
+
expect(listPages).toEqual(['1', '2', '3'])
|
|
937
|
+
})
|
|
938
|
+
|
|
343
939
|
it('delegates login and isLoggedIn to SomaHttp', async () => {
|
|
344
940
|
const loginCalls: string[] = []
|
|
345
941
|
const { http } = createFakeHttp({
|
|
@@ -438,14 +1034,17 @@ describe('SomaClient', () => {
|
|
|
438
1034
|
await expect(
|
|
439
1035
|
client.room.reserve({ roomId: 1, date: '2026-04-01', slots: ['10:00'], title: 'Test' }),
|
|
440
1036
|
).rejects.toBeInstanceOf(AuthenticationError)
|
|
1037
|
+
await expect(client.room.get(1)).rejects.toBeInstanceOf(AuthenticationError)
|
|
1038
|
+
await expect(client.room.update(1, { title: 'x' })).rejects.toBeInstanceOf(AuthenticationError)
|
|
1039
|
+
await expect(client.room.cancel(1)).rejects.toBeInstanceOf(AuthenticationError)
|
|
441
1040
|
await expect(client.dashboard.get()).rejects.toBeInstanceOf(AuthenticationError)
|
|
442
1041
|
await expect(client.notice.list()).rejects.toBeInstanceOf(AuthenticationError)
|
|
443
1042
|
await expect(client.notice.get(1)).rejects.toBeInstanceOf(AuthenticationError)
|
|
444
|
-
await expect(client.team.
|
|
1043
|
+
await expect(client.team.list()).rejects.toBeInstanceOf(AuthenticationError)
|
|
1044
|
+
await expect(client.team.join('team-1')).rejects.toBeInstanceOf(AuthenticationError)
|
|
1045
|
+
await expect(client.team.leave('team-1')).rejects.toBeInstanceOf(AuthenticationError)
|
|
445
1046
|
await expect(client.member.show()).rejects.toBeInstanceOf(AuthenticationError)
|
|
446
|
-
await expect(client.
|
|
447
|
-
await expect(client.event.get(1)).rejects.toBeInstanceOf(AuthenticationError)
|
|
448
|
-
await expect(client.event.apply(1)).rejects.toBeInstanceOf(AuthenticationError)
|
|
1047
|
+
await expect(client.schedule.list()).rejects.toBeInstanceOf(AuthenticationError)
|
|
449
1048
|
})
|
|
450
1049
|
|
|
451
1050
|
it('includes helpful login hints in the AuthenticationError message', async () => {
|
|
@@ -461,4 +1060,48 @@ describe('SomaClient', () => {
|
|
|
461
1060
|
expect((error as Error).message).toContain('opensoma auth extract')
|
|
462
1061
|
}
|
|
463
1062
|
})
|
|
1063
|
+
|
|
1064
|
+
it('single-flights relogin across concurrent auth-required calls', async () => {
|
|
1065
|
+
const loginCalls: string[] = []
|
|
1066
|
+
let resolveLoginStarted: (() => void) | undefined
|
|
1067
|
+
const loginStarted = new Promise<void>((resolve) => {
|
|
1068
|
+
resolveLoginStarted = resolve
|
|
1069
|
+
})
|
|
1070
|
+
let allowLogin: (() => void) | undefined
|
|
1071
|
+
const loginGate = new Promise<void>((resolve) => {
|
|
1072
|
+
allowLogin = resolve
|
|
1073
|
+
})
|
|
1074
|
+
let loggedIn = false
|
|
1075
|
+
|
|
1076
|
+
const fake = {
|
|
1077
|
+
checkLogin: async () => (loggedIn ? { userId: 'neo@example.com', userNm: '전수열' } : null),
|
|
1078
|
+
get: async () =>
|
|
1079
|
+
'<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>',
|
|
1080
|
+
post: async () => '',
|
|
1081
|
+
postForm: async () => '',
|
|
1082
|
+
postMultipart: async () => '',
|
|
1083
|
+
login: async (username: string, password: string) => {
|
|
1084
|
+
loginCalls.push(`${username}:${password}`)
|
|
1085
|
+
resolveLoginStarted?.()
|
|
1086
|
+
await loginGate
|
|
1087
|
+
loggedIn = true
|
|
1088
|
+
},
|
|
1089
|
+
logout: async () => {},
|
|
1090
|
+
getSessionCookie: () => 'session-after-relogin',
|
|
1091
|
+
getCsrfToken: () => 'csrf-after-relogin',
|
|
1092
|
+
}
|
|
1093
|
+
const client = new SomaClient({
|
|
1094
|
+
username: 'neo@example.com',
|
|
1095
|
+
password: 'secret',
|
|
1096
|
+
http: fake as unknown as SomaHttp,
|
|
1097
|
+
})
|
|
1098
|
+
|
|
1099
|
+
const parallel = Promise.all([client.mentoring.list(), client.mentoring.list(), client.mentoring.list()])
|
|
1100
|
+
await loginStarted
|
|
1101
|
+
expect(loginCalls).toEqual(['neo@example.com:secret'])
|
|
1102
|
+
allowLogin?.()
|
|
1103
|
+
await parallel
|
|
1104
|
+
|
|
1105
|
+
expect(loginCalls).toEqual(['neo@example.com:secret'])
|
|
1106
|
+
})
|
|
464
1107
|
})
|