opensoma 0.5.1 → 0.6.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 +3 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +30 -7
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +218 -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 +2 -1
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +2 -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/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 +9 -0
- package/dist/src/credential-manager.d.ts.map +1 -1
- package/dist/src/credential-manager.js +24 -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 +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.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/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 +4 -2
- package/src/client.test.ts +673 -30
- package/src/client.ts +277 -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 +72 -25
- package/src/commands/helpers.ts +3 -3
- package/src/commands/index.ts +2 -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/constants.ts +20 -8
- package/src/credential-manager.test.ts +44 -0
- package/src/credential-manager.ts +27 -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 +10 -1
- 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/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
|
@@ -1,17 +1,43 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
buildMentoringPayload,
|
|
5
|
+
buildRoomCancelPayload,
|
|
6
|
+
buildRoomReservationPayload,
|
|
7
|
+
buildRoomUpdatePayload,
|
|
8
|
+
buildUpdateMentoringPayload,
|
|
9
|
+
resolveVenue,
|
|
10
|
+
validateAttendeeCount,
|
|
11
|
+
} from './swmaestro'
|
|
12
|
+
|
|
13
|
+
const baseExisting = {
|
|
14
|
+
rentId: 18718,
|
|
15
|
+
itemId: 17,
|
|
16
|
+
title: '멘토링',
|
|
17
|
+
date: '2026-05-31',
|
|
18
|
+
startTime: '21:00',
|
|
19
|
+
endTime: '21:30',
|
|
20
|
+
attendees: 4,
|
|
21
|
+
notes: '',
|
|
22
|
+
statusCode: 'RS001',
|
|
23
|
+
}
|
|
4
24
|
|
|
5
25
|
describe('resolveVenue', () => {
|
|
6
26
|
it('prepends "토즈-" to bare TOZ location names', () => {
|
|
7
27
|
expect(resolveVenue('광화문점')).toBe('토즈-광화문점')
|
|
8
28
|
expect(resolveVenue('양재점')).toBe('토즈-양재점')
|
|
9
29
|
expect(resolveVenue('강남컨퍼런스센터점')).toBe('토즈-강남컨퍼런스센터점')
|
|
10
|
-
expect(resolveVenue('건대점')).toBe('토즈-건대점')
|
|
11
30
|
expect(resolveVenue('강남역토즈타워점')).toBe('토즈-강남역토즈타워점')
|
|
12
31
|
expect(resolveVenue('선릉점')).toBe('토즈-선릉점')
|
|
13
|
-
|
|
14
|
-
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('preserves trailing spaces that native ships for 건대/역삼/홍대 <option> values', () => {
|
|
35
|
+
expect(resolveVenue('건대점')).toBe('토즈-건대점 ')
|
|
36
|
+
expect(resolveVenue('역삼점')).toBe('토즈-역삼점 ')
|
|
37
|
+
expect(resolveVenue('홍대점')).toBe('토즈-홍대점 ')
|
|
38
|
+
expect(resolveVenue('토즈-건대점')).toBe('토즈-건대점 ')
|
|
39
|
+
expect(resolveVenue('토즈-역삼점')).toBe('토즈-역삼점 ')
|
|
40
|
+
expect(resolveVenue('토즈-홍대점')).toBe('토즈-홍대점 ')
|
|
15
41
|
})
|
|
16
42
|
|
|
17
43
|
it('passes through TOZ locations that already have the prefix', () => {
|
|
@@ -44,7 +70,7 @@ describe('resolveVenue', () => {
|
|
|
44
70
|
})
|
|
45
71
|
|
|
46
72
|
describe('buildRoomReservationPayload', () => {
|
|
47
|
-
it('sets rentEndde
|
|
73
|
+
it('sets rentEndde using the native lastSlot.minute+29 formula so tooltips render :59 like swmaestro.ai', () => {
|
|
48
74
|
const payload = buildRoomReservationPayload({
|
|
49
75
|
roomId: 17,
|
|
50
76
|
date: '2026-04-20',
|
|
@@ -53,7 +79,7 @@ describe('buildRoomReservationPayload', () => {
|
|
|
53
79
|
})
|
|
54
80
|
|
|
55
81
|
expect(payload.rentBgnde).toBe('2026-04-20 13:00:00')
|
|
56
|
-
expect(payload.rentEndde).toBe('2026-04-20 13:
|
|
82
|
+
expect(payload.rentEndde).toBe('2026-04-20 13:59:00')
|
|
57
83
|
expect(payload['time[0]']).toBe('13:00')
|
|
58
84
|
expect(payload['time[1]']).toBe('13:30')
|
|
59
85
|
expect(payload['time[2]']).toBeUndefined()
|
|
@@ -62,7 +88,7 @@ describe('buildRoomReservationPayload', () => {
|
|
|
62
88
|
expect(payload['chkData_3']).toBeUndefined()
|
|
63
89
|
})
|
|
64
90
|
|
|
65
|
-
it('
|
|
91
|
+
it('emits :29 for a single :00 slot so the tooltip mirrors native behavior', () => {
|
|
66
92
|
const payload = buildRoomReservationPayload({
|
|
67
93
|
roomId: 17,
|
|
68
94
|
date: '2026-04-20',
|
|
@@ -71,7 +97,7 @@ describe('buildRoomReservationPayload', () => {
|
|
|
71
97
|
})
|
|
72
98
|
|
|
73
99
|
expect(payload.rentBgnde).toBe('2026-04-20 13:00:00')
|
|
74
|
-
expect(payload.rentEndde).toBe('2026-04-20 13:
|
|
100
|
+
expect(payload.rentEndde).toBe('2026-04-20 13:29:00')
|
|
75
101
|
expect(payload['time[0]']).toBe('13:00')
|
|
76
102
|
expect(payload['time[1]']).toBeUndefined()
|
|
77
103
|
})
|
|
@@ -85,7 +111,7 @@ describe('buildRoomReservationPayload', () => {
|
|
|
85
111
|
})
|
|
86
112
|
|
|
87
113
|
expect(payload.rentBgnde).toBe('2026-04-20 23:00:00')
|
|
88
|
-
expect(payload.rentEndde).toBe('2026-04-20 23:
|
|
114
|
+
expect(payload.rentEndde).toBe('2026-04-20 23:59:00')
|
|
89
115
|
})
|
|
90
116
|
|
|
91
117
|
it('rejects non-consecutive slots', () => {
|
|
@@ -121,3 +147,213 @@ describe('buildRoomReservationPayload', () => {
|
|
|
121
147
|
).toThrow('At least one time slot is required')
|
|
122
148
|
})
|
|
123
149
|
})
|
|
150
|
+
|
|
151
|
+
describe('buildRoomUpdatePayload', () => {
|
|
152
|
+
it('serialises the existing reservation unchanged when no overrides are supplied', () => {
|
|
153
|
+
const payload = buildRoomUpdatePayload(baseExisting)
|
|
154
|
+
|
|
155
|
+
expect(payload).toEqual({
|
|
156
|
+
menuNo: '200058',
|
|
157
|
+
rentId: '18718',
|
|
158
|
+
itemId: '17',
|
|
159
|
+
receiptStatCd: 'RS001',
|
|
160
|
+
title: '멘토링',
|
|
161
|
+
rentDt: '2026-05-31',
|
|
162
|
+
rentBgnde: '2026-05-31 21:00:00',
|
|
163
|
+
rentEndde: '2026-05-31 21:30:00',
|
|
164
|
+
infoCn: '',
|
|
165
|
+
rentNum: '4',
|
|
166
|
+
pageQueryString: '',
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('applies title, attendees, and notes overrides while keeping the schedule fields', () => {
|
|
171
|
+
const payload = buildRoomUpdatePayload(baseExisting, {
|
|
172
|
+
title: '스터디',
|
|
173
|
+
attendees: 6,
|
|
174
|
+
notes: '리뷰 세션',
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
expect(payload.title).toBe('스터디')
|
|
178
|
+
expect(payload.rentNum).toBe('6')
|
|
179
|
+
expect(payload.infoCn).toBe('리뷰 세션')
|
|
180
|
+
expect(payload.rentBgnde).toBe('2026-05-31 21:00:00')
|
|
181
|
+
expect(payload.rentEndde).toBe('2026-05-31 21:30:00')
|
|
182
|
+
expect(payload['time[0]']).toBeUndefined()
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('rewrites schedule fields and re-emits time/chkData entries when slots change', () => {
|
|
186
|
+
const payload = buildRoomUpdatePayload(baseExisting, {
|
|
187
|
+
slots: ['22:00', '22:30', '23:00'],
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
expect(payload.rentBgnde).toBe('2026-05-31 22:00:00')
|
|
191
|
+
expect(payload.rentEndde).toBe('2026-05-31 23:29:00')
|
|
192
|
+
expect(payload['time[0]']).toBe('22:00')
|
|
193
|
+
expect(payload['time[1]']).toBe('22:30')
|
|
194
|
+
expect(payload['time[2]']).toBe('23:00')
|
|
195
|
+
expect(payload['chkData_1']).toBe('2026-05-31|22:00|17')
|
|
196
|
+
expect(payload['chkData_3']).toBe('2026-05-31|23:00|17')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('uses the new roomId and date when both schedule overrides are provided', () => {
|
|
200
|
+
const payload = buildRoomUpdatePayload(baseExisting, {
|
|
201
|
+
roomId: 22,
|
|
202
|
+
date: '2026-06-01',
|
|
203
|
+
slots: ['10:00', '10:30'],
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
expect(payload.itemId).toBe('22')
|
|
207
|
+
expect(payload.rentDt).toBe('2026-06-01')
|
|
208
|
+
expect(payload.rentBgnde).toBe('2026-06-01 10:00:00')
|
|
209
|
+
expect(payload.rentEndde).toBe('2026-06-01 10:59:00')
|
|
210
|
+
expect(payload['chkData_1']).toBe('2026-06-01|10:00|22')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('rejects invalid slot overrides', () => {
|
|
214
|
+
expect(() => buildRoomUpdatePayload(baseExisting, { slots: ['22:00', '23:00'] })).toThrow(
|
|
215
|
+
'Time slots must be consecutive',
|
|
216
|
+
)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('preserves the existing status code so confirmed reservations stay confirmed', () => {
|
|
220
|
+
const payload = buildRoomUpdatePayload({ ...baseExisting, statusCode: 'RS001' }, { title: '수정본' })
|
|
221
|
+
expect(payload.receiptStatCd).toBe('RS001')
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
describe('buildRoomCancelPayload', () => {
|
|
226
|
+
it('flips receiptStatCd to RS002 while keeping every other field identical to the existing reservation', () => {
|
|
227
|
+
const payload = buildRoomCancelPayload(baseExisting)
|
|
228
|
+
|
|
229
|
+
expect(payload.receiptStatCd).toBe('RS002')
|
|
230
|
+
expect(payload.rentId).toBe('18718')
|
|
231
|
+
expect(payload.title).toBe('멘토링')
|
|
232
|
+
expect(payload.rentBgnde).toBe('2026-05-31 21:00:00')
|
|
233
|
+
expect(payload.rentEndde).toBe('2026-05-31 21:30:00')
|
|
234
|
+
expect(payload.rentNum).toBe('4')
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const baseMentoring = {
|
|
239
|
+
title: '스터디',
|
|
240
|
+
type: 'public' as const,
|
|
241
|
+
date: '2026-05-10',
|
|
242
|
+
startTime: '14:00',
|
|
243
|
+
endTime: '15:00',
|
|
244
|
+
venue: '스페이스 A1',
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
describe('buildMentoringPayload', () => {
|
|
248
|
+
it('splits the registration period into date + time fields the new form expects', () => {
|
|
249
|
+
const payload = buildMentoringPayload(baseMentoring)
|
|
250
|
+
|
|
251
|
+
expect(payload.bgndeDate).toBe('2026-05-10')
|
|
252
|
+
expect(payload.bgndeTime).toBe('00:00')
|
|
253
|
+
expect('bgnde' in payload).toBe(false)
|
|
254
|
+
expect('endde' in payload).toBe(false)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('defaults receiptType to UNTIL_LECTURE and aligns enddeDate/enddeTime with the lecture start', () => {
|
|
258
|
+
const payload = buildMentoringPayload(baseMentoring)
|
|
259
|
+
|
|
260
|
+
expect(payload.receiptType).toBe('UNTIL_LECTURE')
|
|
261
|
+
expect(payload.enddeDate).toBe('2026-05-10')
|
|
262
|
+
expect(payload.enddeTime).toBe('14:00')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('uses the user-supplied registration end window when receiptType is DIRECT', () => {
|
|
266
|
+
const payload = buildMentoringPayload({
|
|
267
|
+
...baseMentoring,
|
|
268
|
+
receiptType: 'DIRECT',
|
|
269
|
+
regStart: '2026-05-01',
|
|
270
|
+
regStartTime: '09:00',
|
|
271
|
+
regEnd: '2026-05-09',
|
|
272
|
+
regEndTime: '18:00',
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
expect(payload.receiptType).toBe('DIRECT')
|
|
276
|
+
expect(payload.bgndeDate).toBe('2026-05-01')
|
|
277
|
+
expect(payload.bgndeTime).toBe('09:00')
|
|
278
|
+
expect(payload.enddeDate).toBe('2026-05-09')
|
|
279
|
+
expect(payload.enddeTime).toBe('18:00')
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('sends the stateCd and qustnrAt values the current form posts', () => {
|
|
283
|
+
const payload = buildMentoringPayload(baseMentoring)
|
|
284
|
+
|
|
285
|
+
expect(payload.stateCd).toBe('A')
|
|
286
|
+
expect(payload.qustnrAt).toBe('N')
|
|
287
|
+
expect(payload.openAt).toBe('Y')
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('maps public/lecture types to MRC010/MRC020 and validates the default attendee count', () => {
|
|
291
|
+
expect(buildMentoringPayload({ ...baseMentoring, type: 'public' }).reportCd).toBe('MRC010')
|
|
292
|
+
expect(buildMentoringPayload({ ...baseMentoring, type: 'public' }).applyCnt).toBe('3')
|
|
293
|
+
expect(buildMentoringPayload({ ...baseMentoring, type: 'lecture' }).reportCd).toBe('MRC020')
|
|
294
|
+
expect(buildMentoringPayload({ ...baseMentoring, type: 'lecture' }).applyCnt).toBe('6')
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('rejects attendee counts that violate the form rules', () => {
|
|
298
|
+
expect(() => buildMentoringPayload({ ...baseMentoring, maxAttendees: 1 })).toThrow(
|
|
299
|
+
'자유 멘토링은 2명 이상 5명 이하로 설정해야 합니다.',
|
|
300
|
+
)
|
|
301
|
+
expect(() => buildMentoringPayload({ ...baseMentoring, maxAttendees: 6 })).toThrow(
|
|
302
|
+
'자유 멘토링은 2명 이상 5명 이하로 설정해야 합니다.',
|
|
303
|
+
)
|
|
304
|
+
expect(() => buildMentoringPayload({ ...baseMentoring, type: 'lecture', maxAttendees: 4 })).toThrow(
|
|
305
|
+
'멘토 특강은 6명 이상으로 설정해야 합니다.',
|
|
306
|
+
)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('passes through the venue resolver so TOZ aliases are normalised', () => {
|
|
310
|
+
expect(buildMentoringPayload({ ...baseMentoring, venue: '광화문점' }).place).toBe('토즈-광화문점')
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('mirrors native checkForm() by replacing double quotes in qustnrSj with single quotes', () => {
|
|
314
|
+
const payload = buildMentoringPayload({ ...baseMentoring, title: '"테스트" 멘토링' })
|
|
315
|
+
|
|
316
|
+
expect(payload.qustnrSj).toBe("'테스트' 멘토링")
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('mirrors the native DEXT5 empty-body placeholder when content is missing', () => {
|
|
320
|
+
const nativeEmpty =
|
|
321
|
+
'<p style="font-family: 굴림; font-size: 12pt; line-height: 1.2; margin-top: 0px; margin-bottom: 0px;"> </p>'
|
|
322
|
+
|
|
323
|
+
expect(buildMentoringPayload(baseMentoring).qestnarCn).toBe(nativeEmpty)
|
|
324
|
+
expect(buildMentoringPayload({ ...baseMentoring, content: '' }).qestnarCn).toBe(nativeEmpty)
|
|
325
|
+
expect(buildMentoringPayload({ ...baseMentoring, content: ' \n ' }).qestnarCn).toBe(nativeEmpty)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('passes through rich HTML from the editor unchanged', () => {
|
|
329
|
+
const payload = buildMentoringPayload({ ...baseMentoring, content: '<p>세션 본문</p>' })
|
|
330
|
+
|
|
331
|
+
expect(payload.qestnarCn).toBe('<p>세션 본문</p>')
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
describe('buildUpdateMentoringPayload', () => {
|
|
336
|
+
it('injects the target qustnrSn while reusing the insert payload shape', () => {
|
|
337
|
+
const payload = buildUpdateMentoringPayload(9999, baseMentoring)
|
|
338
|
+
|
|
339
|
+
expect(payload.qustnrSn).toBe('9999')
|
|
340
|
+
expect(payload.bgndeDate).toBe('2026-05-10')
|
|
341
|
+
expect(payload.receiptType).toBe('UNTIL_LECTURE')
|
|
342
|
+
expect(payload.stateCd).toBe('A')
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
describe('validateAttendeeCount', () => {
|
|
347
|
+
it('accepts public counts within 2-5 and lecture counts of 6 or more', () => {
|
|
348
|
+
expect(() => validateAttendeeCount('public', 2)).not.toThrow()
|
|
349
|
+
expect(() => validateAttendeeCount('public', 5)).not.toThrow()
|
|
350
|
+
expect(() => validateAttendeeCount('lecture', 6)).not.toThrow()
|
|
351
|
+
expect(() => validateAttendeeCount('lecture', 100)).not.toThrow()
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('rejects counts outside the server-enforced bounds', () => {
|
|
355
|
+
expect(() => validateAttendeeCount('public', 1)).toThrow()
|
|
356
|
+
expect(() => validateAttendeeCount('public', 6)).toThrow()
|
|
357
|
+
expect(() => validateAttendeeCount('lecture', 5)).toThrow()
|
|
358
|
+
})
|
|
359
|
+
})
|
|
@@ -13,6 +13,29 @@ export function toMentoringType(type: string): 'public' | 'lecture' {
|
|
|
13
13
|
return 'public'
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export type ReceiptType = 'UNTIL_LECTURE' | 'DIRECT'
|
|
17
|
+
|
|
18
|
+
function sanitizeTitle(title: string): string {
|
|
19
|
+
return title.replace(/"/g, "'")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveMaxAttendees(type: 'public' | 'lecture', maxAttendees?: number): number {
|
|
23
|
+
if (maxAttendees !== undefined) {
|
|
24
|
+
validateAttendeeCount(type, maxAttendees)
|
|
25
|
+
return maxAttendees
|
|
26
|
+
}
|
|
27
|
+
return type === 'lecture' ? 6 : 3
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validateAttendeeCount(type: 'public' | 'lecture', count: number): void {
|
|
31
|
+
if (type === 'public' && (count < 2 || count > 5)) {
|
|
32
|
+
throw new Error('자유 멘토링은 2명 이상 5명 이하로 설정해야 합니다.')
|
|
33
|
+
}
|
|
34
|
+
if (type === 'lecture' && count < 6) {
|
|
35
|
+
throw new Error('멘토 특강은 6명 이상으로 설정해야 합니다.')
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
16
39
|
export function buildMentoringPayload(params: {
|
|
17
40
|
title: string
|
|
18
41
|
type: 'public' | 'lecture'
|
|
@@ -22,16 +45,27 @@ export function buildMentoringPayload(params: {
|
|
|
22
45
|
venue: string
|
|
23
46
|
maxAttendees?: number
|
|
24
47
|
regStart?: string
|
|
48
|
+
regStartTime?: string
|
|
25
49
|
regEnd?: string
|
|
50
|
+
regEndTime?: string
|
|
51
|
+
receiptType?: ReceiptType
|
|
26
52
|
content?: string
|
|
27
53
|
}): Record<string, string> {
|
|
54
|
+
const receiptType: ReceiptType = params.receiptType ?? 'UNTIL_LECTURE'
|
|
55
|
+
const bgndeDate = params.regStart ?? params.date
|
|
56
|
+
const bgndeTime = params.regStartTime ?? '00:00'
|
|
57
|
+
const { enddeDate, enddeTime } = resolveReceiptEnd(receiptType, params)
|
|
58
|
+
|
|
28
59
|
return {
|
|
29
60
|
menuNo: MENU_NO.MENTORING,
|
|
30
61
|
reportCd: toReportCd(params.type),
|
|
31
|
-
qustnrSj: params.title,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
62
|
+
qustnrSj: sanitizeTitle(params.title),
|
|
63
|
+
receiptType,
|
|
64
|
+
bgndeDate,
|
|
65
|
+
bgndeTime,
|
|
66
|
+
enddeDate,
|
|
67
|
+
enddeTime,
|
|
68
|
+
applyCnt: String(resolveMaxAttendees(params.type, params.maxAttendees)),
|
|
35
69
|
eventDt: params.date,
|
|
36
70
|
eventStime: params.startTime,
|
|
37
71
|
eventEtime: params.endTime,
|
|
@@ -39,14 +73,27 @@ export function buildMentoringPayload(params: {
|
|
|
39
73
|
qestnarCn: formatEditorContent(params.content ?? ''),
|
|
40
74
|
atchFileId: '',
|
|
41
75
|
fileFieldNm_1: '',
|
|
42
|
-
stateCd: '
|
|
76
|
+
stateCd: 'A',
|
|
43
77
|
openAt: 'Y',
|
|
44
|
-
qustnrAt: '
|
|
78
|
+
qustnrAt: 'N',
|
|
45
79
|
qustnrSn: '',
|
|
46
80
|
pageQueryString: '',
|
|
47
81
|
}
|
|
48
82
|
}
|
|
49
83
|
|
|
84
|
+
function resolveReceiptEnd(
|
|
85
|
+
receiptType: ReceiptType,
|
|
86
|
+
params: { date: string; startTime: string; endTime: string; regEnd?: string; regEndTime?: string },
|
|
87
|
+
): { enddeDate: string; enddeTime: string } {
|
|
88
|
+
if (receiptType === 'UNTIL_LECTURE') {
|
|
89
|
+
return { enddeDate: params.date, enddeTime: params.startTime }
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
enddeDate: params.regEnd ?? params.date,
|
|
93
|
+
enddeTime: params.regEndTime ?? params.startTime,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
50
97
|
export function buildUpdateMentoringPayload(
|
|
51
98
|
id: number,
|
|
52
99
|
params: Parameters<typeof buildMentoringPayload>[0],
|
|
@@ -67,7 +114,7 @@ export function buildDeleteMentoringPayload(id: number): Record<string, string>
|
|
|
67
114
|
|
|
68
115
|
export function buildApplicationPayload(id: number): Record<string, string> {
|
|
69
116
|
return {
|
|
70
|
-
menuNo: MENU_NO.
|
|
117
|
+
menuNo: MENU_NO.MENTORING,
|
|
71
118
|
qustnrSn: String(id),
|
|
72
119
|
applyGb: 'C',
|
|
73
120
|
stepHeader: '0',
|
|
@@ -130,6 +177,17 @@ export function validateReservationSlots(slots: string[]): void {
|
|
|
130
177
|
}
|
|
131
178
|
}
|
|
132
179
|
|
|
180
|
+
// Mirror the native /officeMng/view.do form, which sets rentEndde to
|
|
181
|
+
// `${lastSlot.hour}:${lastSlot.minute + 29}` (see the native JS:
|
|
182
|
+
// `et = last.data('hour')+':'+(last.data('minute')*1+29)`). Last slot '13:30'
|
|
183
|
+
// becomes '13:59'; last slot '12:00' becomes '12:29'. Native does not carry
|
|
184
|
+
// minutes into the hour, so we replicate that string-concat behavior exactly.
|
|
185
|
+
function slotNativeEnd(slot: string): string {
|
|
186
|
+
const [hourPart, minutePart] = slot.split(':')
|
|
187
|
+
const minute = Number(minutePart) + 29
|
|
188
|
+
return `${hourPart}:${String(minute).padStart(2, '0')}`
|
|
189
|
+
}
|
|
190
|
+
|
|
133
191
|
export function buildRoomReservationPayload(params: {
|
|
134
192
|
roomId: number
|
|
135
193
|
date: string
|
|
@@ -147,7 +205,7 @@ export function buildRoomReservationPayload(params: {
|
|
|
147
205
|
menuNo: MENU_NO.ROOM,
|
|
148
206
|
itemId: String(params.roomId),
|
|
149
207
|
rentBgnde: `${params.date} ${firstSlot}:00`,
|
|
150
|
-
rentEndde: `${params.date} ${lastSlot}:00`,
|
|
208
|
+
rentEndde: `${params.date} ${slotNativeEnd(lastSlot)}:00`,
|
|
151
209
|
title: params.title,
|
|
152
210
|
rentDt: params.date,
|
|
153
211
|
rentNum: String(params.attendees ?? 1),
|
|
@@ -164,6 +222,87 @@ export function buildRoomReservationPayload(params: {
|
|
|
164
222
|
return payload
|
|
165
223
|
}
|
|
166
224
|
|
|
225
|
+
export function buildRoomUpdatePayload(
|
|
226
|
+
existing: {
|
|
227
|
+
rentId: number
|
|
228
|
+
itemId: number
|
|
229
|
+
title: string
|
|
230
|
+
date: string
|
|
231
|
+
startTime: string
|
|
232
|
+
endTime: string
|
|
233
|
+
attendees: number
|
|
234
|
+
notes: string
|
|
235
|
+
statusCode: string
|
|
236
|
+
},
|
|
237
|
+
params: {
|
|
238
|
+
title?: string
|
|
239
|
+
roomId?: number
|
|
240
|
+
date?: string
|
|
241
|
+
slots?: string[]
|
|
242
|
+
attendees?: number
|
|
243
|
+
notes?: string
|
|
244
|
+
} = {},
|
|
245
|
+
): Record<string, string> {
|
|
246
|
+
const roomId = params.roomId ?? existing.itemId
|
|
247
|
+
const date = params.date ?? existing.date
|
|
248
|
+
|
|
249
|
+
let startTime = existing.startTime
|
|
250
|
+
let endTime = existing.endTime
|
|
251
|
+
if (params.slots?.length) {
|
|
252
|
+
validateReservationSlots(params.slots)
|
|
253
|
+
startTime = params.slots[0]
|
|
254
|
+
endTime = slotNativeEnd(params.slots[params.slots.length - 1])
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const payload: Record<string, string> = {
|
|
258
|
+
menuNo: MENU_NO.ROOM,
|
|
259
|
+
rentId: String(existing.rentId),
|
|
260
|
+
itemId: String(roomId),
|
|
261
|
+
receiptStatCd: existing.statusCode || 'RS001',
|
|
262
|
+
title: params.title ?? existing.title,
|
|
263
|
+
rentDt: date,
|
|
264
|
+
rentBgnde: `${date} ${startTime}:00`,
|
|
265
|
+
rentEndde: `${date} ${endTime}:00`,
|
|
266
|
+
infoCn: params.notes ?? existing.notes,
|
|
267
|
+
rentNum: String(params.attendees ?? existing.attendees),
|
|
268
|
+
pageQueryString: '',
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (params.slots?.length) {
|
|
272
|
+
params.slots.forEach((slot, index) => {
|
|
273
|
+
payload[`time[${index}]`] = slot
|
|
274
|
+
payload[`chkData_${index + 1}`] = `${date}|${slot}|${roomId}`
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return payload
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function buildRoomCancelPayload(existing: {
|
|
282
|
+
rentId: number
|
|
283
|
+
itemId: number
|
|
284
|
+
title: string
|
|
285
|
+
date: string
|
|
286
|
+
startTime: string
|
|
287
|
+
endTime: string
|
|
288
|
+
attendees: number
|
|
289
|
+
notes: string
|
|
290
|
+
}): Record<string, string> {
|
|
291
|
+
return {
|
|
292
|
+
menuNo: MENU_NO.ROOM,
|
|
293
|
+
rentId: String(existing.rentId),
|
|
294
|
+
itemId: String(existing.itemId),
|
|
295
|
+
receiptStatCd: 'RS002',
|
|
296
|
+
title: existing.title,
|
|
297
|
+
rentDt: existing.date,
|
|
298
|
+
rentBgnde: `${existing.date} ${existing.startTime}:00`,
|
|
299
|
+
rentEndde: `${existing.date} ${existing.endTime}:00`,
|
|
300
|
+
infoCn: existing.notes,
|
|
301
|
+
rentNum: String(existing.attendees),
|
|
302
|
+
pageQueryString: '',
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
167
306
|
export function parseApplicationHistory(html: string): ApplicationHistoryItem[] {
|
|
168
307
|
const root = parse(html)
|
|
169
308
|
const rows =
|
|
@@ -184,49 +323,13 @@ export function parseApplicationHistory(html: string): ApplicationHistoryItem[]
|
|
|
184
323
|
)
|
|
185
324
|
}
|
|
186
325
|
|
|
187
|
-
export function parseEventDetail(html: string): Record<string, unknown> {
|
|
188
|
-
const root = parse(html)
|
|
189
|
-
const labels = extractLabelMap(root)
|
|
190
|
-
const contentNode =
|
|
191
|
-
root.querySelector('[data-content]') ??
|
|
192
|
-
root.querySelector('.board-view-content') ??
|
|
193
|
-
root.querySelector('.view-content') ??
|
|
194
|
-
root.querySelector('.content-body')
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
id: extractNumber(labels.NO ?? labels.번호 ?? root.querySelector('[name="bbsId"]')?.getAttribute('value') ?? '0'),
|
|
198
|
-
title: labels.제목 ?? cleanText(root.querySelector('h1, h2, .title')?.text),
|
|
199
|
-
content: decodeHtmlEntities(contentNode?.innerHTML.trim() ?? ''),
|
|
200
|
-
fields: labels,
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function extractLabelMap(root: ReturnType<typeof parse>): Record<string, string> {
|
|
205
|
-
const map: Record<string, string> = {}
|
|
206
|
-
|
|
207
|
-
for (const row of root.querySelectorAll('tr')) {
|
|
208
|
-
const headers = row.querySelectorAll('th')
|
|
209
|
-
const values = row.querySelectorAll('td')
|
|
210
|
-
|
|
211
|
-
if (headers.length === 1 && values.length === 1) {
|
|
212
|
-
map[cleanText(headers[0]?.text).replace(/:$/, '')] = cleanText(values[0]?.text)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (headers.length > 1 && headers.length === values.length) {
|
|
216
|
-
headers.forEach((header, index) => {
|
|
217
|
-
map[cleanText(header.text).replace(/:$/, '')] = cleanText(values[index]?.text)
|
|
218
|
-
})
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return map
|
|
223
|
-
}
|
|
224
|
-
|
|
225
326
|
const EDITOR_P_STYLE = 'font-family: 굴림; font-size: 12pt; line-height: 1.2; margin-top: 0px; margin-bottom: 0px;'
|
|
327
|
+
const EMPTY_EDITOR_HTML = `<p style="${EDITOR_P_STYLE}"> </p>`
|
|
226
328
|
|
|
227
329
|
function formatEditorContent(content: string): string {
|
|
228
|
-
if (!content) return
|
|
330
|
+
if (!content) return EMPTY_EDITOR_HTML
|
|
229
331
|
const decoded = decodeHtmlEntities(content)
|
|
332
|
+
if (!decoded.trim()) return EMPTY_EDITOR_HTML
|
|
230
333
|
if (/<(?:p|div|h[1-6]|ul|ol|table|br)\b/i.test(decoded)) {
|
|
231
334
|
return decoded
|
|
232
335
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { UserGb } from '../../http'
|
|
4
|
+
import { buildTeamActionPayload } from './team-action-params'
|
|
5
|
+
|
|
6
|
+
describe('buildTeamActionPayload', () => {
|
|
7
|
+
it('builds the native payload from teamId and user identity', () => {
|
|
8
|
+
expect(
|
|
9
|
+
buildTeamActionPayload('60e6785c8c404142b12cf9ed2a3d811f', {
|
|
10
|
+
userId: 'neo@example.com',
|
|
11
|
+
userNm: '전수열',
|
|
12
|
+
userNo: 'f6d192ad3b3e4ee29f1d238714ab92c1',
|
|
13
|
+
userGb: UserGb.Mentor,
|
|
14
|
+
}),
|
|
15
|
+
).toEqual({
|
|
16
|
+
userNo: 'f6d192ad3b3e4ee29f1d238714ab92c1',
|
|
17
|
+
userNm: '전수열',
|
|
18
|
+
userGb: 'T',
|
|
19
|
+
teamNo: '60e6785c8c404142b12cf9ed2a3d811f',
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('falls back to userGb="T" when the identity has no userGb', () => {
|
|
24
|
+
const payload = buildTeamActionPayload('team-1', {
|
|
25
|
+
userId: 'neo@example.com',
|
|
26
|
+
userNm: '전수열',
|
|
27
|
+
userNo: 'abc',
|
|
28
|
+
userGb: UserGb.Unknown,
|
|
29
|
+
})
|
|
30
|
+
expect(payload.userGb).toBe('T')
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { UserGb, type UserIdentity } from '../../http'
|
|
2
|
+
|
|
3
|
+
export function buildTeamActionPayload(teamId: string, user: UserIdentity): Record<string, string> {
|
|
4
|
+
return {
|
|
5
|
+
userNo: user.userNo,
|
|
6
|
+
userNm: user.userNm,
|
|
7
|
+
userGb: user.userGb || UserGb.Mentor,
|
|
8
|
+
teamNo: teamId,
|
|
9
|
+
}
|
|
10
|
+
}
|