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.
Files changed (128) hide show
  1. package/dist/package.json +5 -1
  2. package/dist/src/agent-browser-launcher.d.ts +43 -0
  3. package/dist/src/agent-browser-launcher.d.ts.map +1 -0
  4. package/dist/src/agent-browser-launcher.js +97 -0
  5. package/dist/src/agent-browser-launcher.js.map +1 -0
  6. package/dist/src/cli.d.ts.map +1 -1
  7. package/dist/src/cli.js +3 -2
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/client.d.ts +30 -7
  10. package/dist/src/client.d.ts.map +1 -1
  11. package/dist/src/client.js +218 -52
  12. package/dist/src/client.js.map +1 -1
  13. package/dist/src/commands/agent-browser.d.ts +3 -0
  14. package/dist/src/commands/agent-browser.d.ts.map +1 -0
  15. package/dist/src/commands/agent-browser.js +27 -0
  16. package/dist/src/commands/agent-browser.js.map +1 -0
  17. package/dist/src/commands/auth.d.ts +1 -1
  18. package/dist/src/commands/auth.d.ts.map +1 -1
  19. package/dist/src/commands/auth.js +4 -2
  20. package/dist/src/commands/auth.js.map +1 -1
  21. package/dist/src/commands/dashboard.d.ts +13 -0
  22. package/dist/src/commands/dashboard.d.ts.map +1 -1
  23. package/dist/src/commands/dashboard.js +10 -18
  24. package/dist/src/commands/dashboard.js.map +1 -1
  25. package/dist/src/commands/helpers.d.ts +1 -1
  26. package/dist/src/commands/helpers.d.ts.map +1 -1
  27. package/dist/src/commands/helpers.js +2 -2
  28. package/dist/src/commands/helpers.js.map +1 -1
  29. package/dist/src/commands/index.d.ts +2 -1
  30. package/dist/src/commands/index.d.ts.map +1 -1
  31. package/dist/src/commands/index.js +2 -1
  32. package/dist/src/commands/index.js.map +1 -1
  33. package/dist/src/commands/mentoring.d.ts.map +1 -1
  34. package/dist/src/commands/mentoring.js +54 -29
  35. package/dist/src/commands/mentoring.js.map +1 -1
  36. package/dist/src/commands/notice.d.ts.map +1 -1
  37. package/dist/src/commands/notice.js +2 -1
  38. package/dist/src/commands/notice.js.map +1 -1
  39. package/dist/src/commands/report.d.ts.map +1 -1
  40. package/dist/src/commands/report.js +4 -2
  41. package/dist/src/commands/report.js.map +1 -1
  42. package/dist/src/commands/room.d.ts.map +1 -1
  43. package/dist/src/commands/room.js +125 -2
  44. package/dist/src/commands/room.js.map +1 -1
  45. package/dist/src/commands/schedule.d.ts +3 -0
  46. package/dist/src/commands/schedule.d.ts.map +1 -0
  47. package/dist/src/commands/schedule.js +27 -0
  48. package/dist/src/commands/schedule.js.map +1 -0
  49. package/dist/src/commands/team.d.ts.map +1 -1
  50. package/dist/src/commands/team.js +55 -4
  51. package/dist/src/commands/team.js.map +1 -1
  52. package/dist/src/constants.d.ts +5 -5
  53. package/dist/src/constants.d.ts.map +1 -1
  54. package/dist/src/constants.js +20 -8
  55. package/dist/src/constants.js.map +1 -1
  56. package/dist/src/credential-manager.d.ts +9 -0
  57. package/dist/src/credential-manager.d.ts.map +1 -1
  58. package/dist/src/credential-manager.js +24 -0
  59. package/dist/src/credential-manager.js.map +1 -1
  60. package/dist/src/formatters.d.ts +11 -3
  61. package/dist/src/formatters.d.ts.map +1 -1
  62. package/dist/src/formatters.js +281 -52
  63. package/dist/src/formatters.js.map +1 -1
  64. package/dist/src/http.d.ts +8 -0
  65. package/dist/src/http.d.ts.map +1 -1
  66. package/dist/src/http.js +29 -1
  67. package/dist/src/http.js.map +1 -1
  68. package/dist/src/index.d.ts +4 -1
  69. package/dist/src/index.d.ts.map +1 -1
  70. package/dist/src/index.js +2 -1
  71. package/dist/src/index.js.map +1 -1
  72. package/dist/src/shared/utils/swmaestro.d.ts +34 -1
  73. package/dist/src/shared/utils/swmaestro.d.ts.map +1 -1
  74. package/dist/src/shared/utils/swmaestro.js +102 -39
  75. package/dist/src/shared/utils/swmaestro.js.map +1 -1
  76. package/dist/src/shared/utils/team-action-params.d.ts +3 -0
  77. package/dist/src/shared/utils/team-action-params.d.ts.map +1 -0
  78. package/dist/src/shared/utils/team-action-params.js +10 -0
  79. package/dist/src/shared/utils/team-action-params.js.map +1 -0
  80. package/dist/src/shared/utils/team-params.d.ts +12 -0
  81. package/dist/src/shared/utils/team-params.d.ts.map +1 -0
  82. package/dist/src/shared/utils/team-params.js +38 -0
  83. package/dist/src/shared/utils/team-params.js.map +1 -0
  84. package/dist/src/types.d.ts +147 -10
  85. package/dist/src/types.d.ts.map +1 -1
  86. package/dist/src/types.js +74 -6
  87. package/dist/src/types.js.map +1 -1
  88. package/package.json +5 -1
  89. package/src/agent-browser-launcher.test.ts +263 -0
  90. package/src/agent-browser-launcher.ts +159 -0
  91. package/src/cli.ts +4 -2
  92. package/src/client.test.ts +673 -30
  93. package/src/client.ts +277 -67
  94. package/src/commands/agent-browser.ts +33 -0
  95. package/src/commands/auth.test.ts +77 -26
  96. package/src/commands/auth.ts +5 -3
  97. package/src/commands/dashboard.test.ts +57 -0
  98. package/src/commands/dashboard.ts +22 -19
  99. package/src/commands/helpers.test.ts +72 -25
  100. package/src/commands/helpers.ts +3 -3
  101. package/src/commands/index.ts +2 -1
  102. package/src/commands/mentoring.ts +60 -29
  103. package/src/commands/notice.ts +2 -1
  104. package/src/commands/report.ts +4 -2
  105. package/src/commands/room.ts +160 -1
  106. package/src/commands/schedule.ts +32 -0
  107. package/src/commands/team.ts +73 -5
  108. package/src/constants.ts +20 -8
  109. package/src/credential-manager.test.ts +44 -0
  110. package/src/credential-manager.ts +27 -0
  111. package/src/formatters.test.ts +528 -33
  112. package/src/formatters.ts +309 -55
  113. package/src/http.test.ts +71 -2
  114. package/src/http.ts +41 -2
  115. package/src/index.ts +10 -1
  116. package/src/shared/utils/swmaestro.test.ts +245 -9
  117. package/src/shared/utils/swmaestro.ts +150 -47
  118. package/src/shared/utils/team-action-params.test.ts +32 -0
  119. package/src/shared/utils/team-action-params.ts +10 -0
  120. package/src/shared/utils/team-params.test.ts +141 -0
  121. package/src/shared/utils/team-params.ts +53 -0
  122. package/src/types.test.ts +26 -13
  123. package/src/types.ts +87 -7
  124. package/dist/src/commands/event.d.ts +0 -3
  125. package/dist/src/commands/event.d.ts.map +0 -1
  126. package/dist/src/commands/event.js +0 -58
  127. package/dist/src/commands/event.js.map +0 -1
  128. package/src/commands/event.ts +0 -73
@@ -1,17 +1,43 @@
1
1
  import { describe, expect, it } from 'bun:test'
2
2
 
3
- import { buildRoomReservationPayload, resolveVenue } from './swmaestro'
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
- expect(resolveVenue('역삼점')).toBe('토즈-역삼점')
14
- expect(resolveVenue('홍대점')).toBe('토즈-홍대점')
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 to the last selected slot so the server reserves only the chosen slots', () => {
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:30:00')
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('handles a single-slot reservation', () => {
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:00:00')
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:30:00')
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;">&nbsp;</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
- bgnde: params.regStart ?? params.date,
33
- endde: params.regEnd ?? params.date,
34
- applyCnt: String(params.maxAttendees ?? (params.type === 'lecture' ? 6 : 1)),
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: 'QST020',
76
+ stateCd: 'A',
43
77
  openAt: 'Y',
44
- qustnrAt: 'Y',
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.EVENT,
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}">&nbsp;</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
+ }