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
@@ -5,9 +5,10 @@ import {
5
5
  parseCsrfToken,
6
6
  parseDashboard,
7
7
  parseApplicationHistory,
8
- parseEventList,
8
+ parseScheduleList,
9
9
  parseMemberInfo,
10
10
  parseMentoringDetail,
11
+ parseMentoringEditForm,
11
12
  parseMentoringList,
12
13
  parseNoticeDetail,
13
14
  parseNoticeList,
@@ -15,6 +16,8 @@ import {
15
16
  parseReportDetail,
16
17
  parseReportList,
17
18
  parseRoomList,
19
+ parseRoomReservationDetail,
20
+ parseRoomReservationList,
18
21
  parseRoomSlots,
19
22
  parseTeamInfo,
20
23
  } from './formatters'
@@ -205,6 +208,146 @@ describe('formatters', () => {
205
208
  ])
206
209
  })
207
210
 
211
+ it('parses a room reservation detail form into the mutable fields used by update.do', () => {
212
+ const html = `
213
+ <form id="frm" method="post">
214
+ <input type="hidden" name="pageQueryString" value="menuNo=200058&pageIndex=" />
215
+ <input type="hidden" name="rentId" value="18718" />
216
+ <input type="hidden" name="csrfToken" id="csrfToken" value="" />
217
+ <input type="hidden" name="menuNo" id="menuNo" value="200058" />
218
+ <input type="hidden" name="itemId" id="itemId" value="17" />
219
+ <input type="hidden" name="receiptStatCd" id="receiptStatCd" value="RS001" />
220
+ <input type="hidden" name="title" value="멘토링" />
221
+ <input type="hidden" name="rentDt" value="2026-05-31" />
222
+ <input type="hidden" name="rentBgnde" value="2026-05-31 21:00:00.0" />
223
+ <input type="hidden" name="rentEndde" value="2026-05-31 21:30:00.0" />
224
+ <input type="hidden" name="infoCn" value="리뷰 세션" />
225
+ <input type="hidden" name="rentNum" value="4" />
226
+ </form>
227
+ `
228
+
229
+ expect(parseRoomReservationDetail(html)).toEqual({
230
+ rentId: 18718,
231
+ itemId: 17,
232
+ title: '멘토링',
233
+ date: '2026-05-31',
234
+ startTime: '21:00',
235
+ endTime: '21:30',
236
+ attendees: 4,
237
+ notes: '리뷰 세션',
238
+ status: 'confirmed',
239
+ statusCode: 'RS001',
240
+ })
241
+ })
242
+
243
+ it('reports cancelled status and recovers the date from rentBgnde when rentDt is missing', () => {
244
+ const html = `
245
+ <form id="frm" method="post">
246
+ <input type="hidden" name="rentId" value="18715" />
247
+ <input type="hidden" name="itemId" value="17" />
248
+ <input type="hidden" name="receiptStatCd" value="RS002" />
249
+ <input type="hidden" name="title" value="취소된 예약" />
250
+ <input type="hidden" name="rentBgnde" value="2026-06-02 10:00:00.0" />
251
+ <input type="hidden" name="rentEndde" value="2026-06-02 10:30:00.0" />
252
+ <input type="hidden" name="infoCn" value="" />
253
+ <input type="hidden" name="rentNum" value="2" />
254
+ </form>
255
+ `
256
+
257
+ expect(parseRoomReservationDetail(html)).toEqual({
258
+ rentId: 18715,
259
+ itemId: 17,
260
+ title: '취소된 예약',
261
+ date: '2026-06-02',
262
+ startTime: '10:00',
263
+ endTime: '10:30',
264
+ attendees: 2,
265
+ notes: '',
266
+ status: 'cancelled',
267
+ statusCode: 'RS002',
268
+ })
269
+ })
270
+
271
+ it('parses the 7-column room reservation list table with rentId, venue, title, and time range', () => {
272
+ const html = `
273
+ <div class="bbs-list">
274
+ <ul class="bbs-total">
275
+ <li><strong>Total :</strong> 15</li>
276
+ <li><span>1</span>/2 Page</li>
277
+ </ul>
278
+ <table>
279
+ <thead>
280
+ <tr>
281
+ <th>NO.</th><th>회의실 명</th><th>제목</th><th>사용기간</th>
282
+ <th>작성자</th><th>상태</th><th>등록일</th>
283
+ </tr>
284
+ </thead>
285
+ <tbody>
286
+ <tr>
287
+ <td>15</td>
288
+ <td><a href="/sw/mypage/itemRent/view.do?rentId=18618&menuNo=200058">스페이스 M1</a></td>
289
+ <td class="tit">
290
+ <div class="rel">
291
+ <a href="/sw/mypage/itemRent/view.do?rentId=18618&menuNo=200058">멘토 특강</a>
292
+ <span class="ab">예약완료</span>
293
+ </div>
294
+ </td>
295
+ <td>2026.05.31 16:00 ~ 17:30</td>
296
+ <td>전수열</td>
297
+ <td><span class="label-state y">예약완료</span></td>
298
+ <td>2026.04.20</td>
299
+ </tr>
300
+ <tr>
301
+ <td>14</td>
302
+ <td><a href="/sw/mypage/itemRent/view.do?rentId=18616&menuNo=200058">스페이스 A3</a></td>
303
+ <td class="tit">
304
+ <div class="rel">
305
+ <a href="/sw/mypage/itemRent/view.do?rentId=18616&menuNo=200058">자유 멘토링</a>
306
+ <span class="ab">예약취소</span>
307
+ </div>
308
+ </td>
309
+ <td>2026.05.31 13:00 ~ 14:30</td>
310
+ <td>전수열</td>
311
+ <td><span class="label-state n">예약취소</span></td>
312
+ <td>2026.04.20</td>
313
+ </tr>
314
+ </tbody>
315
+ </table>
316
+ </div>
317
+ `
318
+
319
+ expect(parseRoomReservationList(html)).toEqual([
320
+ {
321
+ rentId: 18618,
322
+ venue: '스페이스 M1',
323
+ title: '멘토 특강',
324
+ date: '2026-05-31',
325
+ startTime: '16:00',
326
+ endTime: '17:30',
327
+ author: '전수열',
328
+ status: 'confirmed',
329
+ statusLabel: '예약완료',
330
+ registeredAt: '2026.04.20',
331
+ },
332
+ {
333
+ rentId: 18616,
334
+ venue: '스페이스 A3',
335
+ title: '자유 멘토링',
336
+ date: '2026-05-31',
337
+ startTime: '13:00',
338
+ endTime: '14:30',
339
+ author: '전수열',
340
+ status: 'cancelled',
341
+ statusLabel: '예약취소',
342
+ registeredAt: '2026.04.20',
343
+ },
344
+ ])
345
+ })
346
+
347
+ it('returns an empty reservation list when the 7-column table is missing', () => {
348
+ expect(parseRoomReservationList('<div>nothing here</div>')).toEqual([])
349
+ })
350
+
208
351
  it('parses the rentTime fragment for room slot availability and reservation info', () => {
209
352
  const html = `
210
353
  <span class="ck-st2" data-hour="09" data-minute="00">
@@ -270,6 +413,7 @@ describe('formatters', () => {
270
413
  members: '김개발, 이개발',
271
414
  mentor: '전수열',
272
415
  },
416
+ teams: [],
273
417
  mentoringSessions: [
274
418
  {
275
419
  title: '게임 개발 AI 활용법',
@@ -336,17 +480,82 @@ describe('formatters', () => {
336
480
  <ul class="bbs-team">
337
481
  <li>
338
482
  <div class="top">
339
- <strong class="t"><a href="javascript:void(0);" onclick="teamPageGo('전수열','a','b');">김앤강</a></strong>
483
+ <strong class="t">
484
+ <a href="javascript:void(0);" onclick="teamPageGo('전수열','owner-1','team-a');">김앤강</a>
485
+ </strong>
486
+ <span class="add-txt"></span>
487
+ <ul class="info">
488
+ <li><strong>팀장 : </strong> <span><a class="sui">강동우</a></span></li>
489
+ <li>
490
+ <strong>팀원 : </strong>
491
+ <span><a href="javascript: popuser('uid-a')" class="sui">강경현</a></span>
492
+ <span><a href="javascript: popuser('uid-b')" class="sui">강동우</a></span>
493
+ </li>
494
+ <li><strong>멘토 : </strong></li>
495
+ </ul>
496
+ </div>
497
+ <div class="bot">
498
+ <ul class="ict">
499
+ <li>ICT기술분류(대) : <span>방송·콘텐츠</span></li>
500
+ <li>ICT기술분류(중) : <span>콘텐츠</span></li>
501
+ </ul>
502
+ <div class="team-com"></div>
503
+ <div class="btn_w">
504
+ <button type="button" class="btn-team bg-light inTeam" value="team-a">참여</button>
505
+ </div>
506
+ </div>
507
+ </li>
508
+ <li>
509
+ <div class="top">
510
+ <strong class="t">
511
+ <a href="javascript:void(0);" onclick="teamPageGo('전수열','owner-2','team-b');">오픈소마</a>
512
+ </strong>
513
+ <span class="add-txt">Previzion</span>
514
+ <ul class="info">
515
+ <li><strong>팀장 : </strong> <span><a class="sui">전수열</a></span></li>
516
+ <li>
517
+ <strong>팀원 : </strong>
518
+ <span><a href="javascript: popuser('uid-1')" class="sui">전수열</a></span>
519
+ </li>
520
+ <li>
521
+ <strong>멘토 : </strong>
522
+ <span><a href="javascript: popuser('uid-m1')" class="sui">문승현</a></span>
523
+ </li>
524
+ </ul>
525
+ </div>
526
+ <div class="bot">
527
+ <ul class="ict">
528
+ <li>ICT기술분류(대) : <span>SW·SI</span></li>
529
+ <li>ICT기술분류(중) : <span>응용SW</span></li>
530
+ </ul>
531
+ <div class="team-com">
532
+ <span class="t2">멘토 구성 완료</span>
533
+ <span class="t1">팀 구성 완료</span>
534
+ </div>
535
+ <div class="btn_w">
536
+ <a href="javascript:void(0);" class="btn-team bg-black">완료</a>
537
+ </div>
340
538
  </div>
341
- <p>팀원 3명</p>
342
- <button type="button">참여중</button>
343
539
  </li>
344
540
  <li>
345
541
  <div class="top">
346
- <strong class="t"><a href="javascript:void(0);" onclick="teamPageGo('전수열','c','d');">오픈소마</a></strong>
542
+ <strong class="t">
543
+ <a href="javascript:void(0);" onclick="teamPageGo('전수열','owner-3','team-c');">LAUNS</a>
544
+ </strong>
545
+ <span class="add-txt"></span>
546
+ <ul class="info">
547
+ <li><strong>팀장 : </strong> <span><a class="sui">홍길동</a></span></li>
548
+ <li><strong>팀원 : </strong></li>
549
+ <li><strong>멘토 : </strong></li>
550
+ </ul>
551
+ </div>
552
+ <div class="bot">
553
+ <ul class="ict"></ul>
554
+ <div class="team-com"></div>
555
+ <div class="btn_w">
556
+ <button type="button" class="btn-team bg-blue outTeam" value="team-c">탈퇴</button>
557
+ </div>
347
558
  </div>
348
- <p>팀원 5명</p>
349
- <button type="button">참여하기</button>
350
559
  </li>
351
560
  </ul>
352
561
  <p class="ico-team">현재 참여중인 방은 <strong class="color-blue">1</strong>/100팀 입니다</p>
@@ -354,14 +563,91 @@ describe('formatters', () => {
354
563
 
355
564
  expect(parseTeamInfo(html)).toEqual({
356
565
  teams: [
357
- { name: '김앤강', memberCount: 3, joinStatus: '참여중' },
358
- { name: '오픈소마', memberCount: 5, joinStatus: '참여하기' },
566
+ {
567
+ name: '김앤강',
568
+ projectName: '',
569
+ ownerId: 'owner-1',
570
+ teamId: 'team-a',
571
+ leader: '강동우',
572
+ members: [
573
+ { name: '강경현', userId: 'uid-a' },
574
+ { name: '강동우', userId: 'uid-b' },
575
+ ],
576
+ mentors: [],
577
+ ictCategoryMajor: '방송·콘텐츠',
578
+ ictCategoryMinor: '콘텐츠',
579
+ teamCompleted: false,
580
+ mentorCompleted: false,
581
+ joinStatus: '참여',
582
+ },
583
+ {
584
+ name: '오픈소마',
585
+ projectName: 'Previzion',
586
+ ownerId: 'owner-2',
587
+ teamId: 'team-b',
588
+ leader: '전수열',
589
+ members: [{ name: '전수열', userId: 'uid-1' }],
590
+ mentors: [{ name: '문승현', userId: 'uid-m1' }],
591
+ ictCategoryMajor: 'SW·SI',
592
+ ictCategoryMinor: '응용SW',
593
+ teamCompleted: true,
594
+ mentorCompleted: true,
595
+ joinStatus: '완료',
596
+ },
597
+ {
598
+ name: 'LAUNS',
599
+ projectName: '',
600
+ ownerId: 'owner-3',
601
+ teamId: 'team-c',
602
+ leader: '홍길동',
603
+ members: [],
604
+ mentors: [],
605
+ ictCategoryMajor: '',
606
+ ictCategoryMinor: '',
607
+ teamCompleted: false,
608
+ mentorCompleted: false,
609
+ joinStatus: '탈퇴',
610
+ },
359
611
  ],
360
612
  currentTeams: 1,
361
613
  maxTeams: 100,
362
614
  })
363
615
  })
364
616
 
617
+ it('parses native trainee team summary when no max team count is rendered', () => {
618
+ const html = `
619
+ <ul class="bbs-team">
620
+ <li>
621
+ <div class="top">
622
+ <strong class="t">
623
+ <a href="javascript:void(0);" onclick="teamPageGo('Member A','owner-1','team-alpha');">Team Alpha</a>
624
+ </strong>
625
+ <span class="add-txt"></span>
626
+ <ul class="info">
627
+ <li><strong>팀장 : </strong> <span><a class="sui">Member A</a></span></li>
628
+ <li><strong>팀원 : </strong><span><a href="javascript: popuser('member-a')">Member A</a></span></li>
629
+ <li><strong>멘토 : </strong><span><a href="javascript: popuser('mentor-one')">Mentor One</a></span></li>
630
+ </ul>
631
+ </div>
632
+ <div class="bot">
633
+ <ul class="ict"></ul>
634
+ <div class="team-com"></div>
635
+ <div class="btn_w">
636
+ <button type="button" class="btn-team bg-blue outTeam" value="team-alpha">탈퇴</button>
637
+ </div>
638
+ </div>
639
+ </li>
640
+ </ul>
641
+ <p class="ico-team">현재 참여중인 방은 <strong class="color-blue">1</strong>팀 입니다</p>
642
+ `
643
+
644
+ const result = parseTeamInfo(html)
645
+
646
+ expect(result.currentTeams).toBe(1)
647
+ expect(result.maxTeams).toBe(0)
648
+ expect(result.teams.map((team) => team.name)).toEqual(['Team Alpha'])
649
+ })
650
+
365
651
  it('parses member info from <dl> definition pairs', () => {
366
652
  const html = `
367
653
  <dl><dt><span class="point">아이디</span></dt><dd>devxoul@gmail.com</dd></dl>
@@ -384,27 +670,61 @@ describe('formatters', () => {
384
670
  })
385
671
  })
386
672
 
387
- it('parses the 7-column event list table', () => {
673
+ it('parses the 3-column monthly schedule table and synthesizes pagination', () => {
388
674
  const html = `
389
675
  <table>
390
- <thead><tr><th>NO.</th><th>구분</th><th>제목</th><th>접수기간</th><th>행사기간</th><th>상태</th><th>등록일</th></tr></thead>
676
+ <thead><tr><th>NO.</th><th>팀명</th><th>팀장</th><th>팀원</th><th>멘토명</th><th>프로젝트 명</th><th>ICT기술분류(대)</th><th>ICT기술분류(중)</th></tr></thead>
391
677
  <tbody>
392
- <tr><td>11</td><td>행사</td><td><a href="/sw/mypage/applicants/view.do?bbsId=77&menuNo=200050">데모데이</a></td><td>2026.04.01 ~ 2026.04.05</td><td>2026.04.10 ~ 2026.04.10</td><td>[접수중]</td><td>2026-03-30</td></tr>
678
+ <tr><td>1</td><td>팀78</td><td>배준서</td><td>이유제, 이중곤</td><td>-</td><td>-</td><td>-</td><td>-</td></tr>
679
+ </tbody>
680
+ </table>
681
+ <table>
682
+ <thead><tr><th>날짜</th><th>구분</th><th>제목</th></tr></thead>
683
+ <tbody>
684
+ <tr><td>2026-04-21~2026-04-26</td><td>교육</td><td>[교육] 디자인씽킹 교육</td></tr>
393
685
  </tbody>
394
686
  </table>
395
687
  `
396
688
 
397
- expect(parseEventList(html)).toEqual([
398
- {
399
- id: 77,
400
- category: '행사',
401
- title: '데모데이',
402
- registrationPeriod: { start: '2026-04-01', end: '2026-04-05' },
403
- eventPeriod: { start: '2026-04-10', end: '2026-04-10' },
404
- status: '접수중',
405
- createdAt: '2026-03-30',
406
- },
407
- ])
689
+ expect(parseScheduleList(html)).toEqual({
690
+ items: [
691
+ {
692
+ id: 1,
693
+ category: '교육',
694
+ title: '[교육] 디자인씽킹 교육',
695
+ period: { start: '2026-04-21', end: '2026-04-26' },
696
+ },
697
+ ],
698
+ pagination: { total: 1, currentPage: 1, totalPages: 1 },
699
+ })
700
+ })
701
+
702
+ it('returns empty items when the monthly schedule table is missing', () => {
703
+ const html = `
704
+ <table>
705
+ <thead><tr><th>NO.</th><th>팀명</th></tr></thead>
706
+ <tbody><tr><td>1</td><td>팀78</td></tr></tbody>
707
+ </table>
708
+ `
709
+
710
+ expect(parseScheduleList(html)).toEqual({
711
+ items: [],
712
+ pagination: { total: 0, currentPage: 1, totalPages: 1 },
713
+ })
714
+ })
715
+
716
+ it('honors the parsed pagination block when SWMaestro provides one', () => {
717
+ const html = `
718
+ <table>
719
+ <thead><tr><th>날짜</th><th>구분</th><th>제목</th></tr></thead>
720
+ <tbody>
721
+ <tr><td>2026-04-21~2026-04-26</td><td>교육</td><td>강의</td></tr>
722
+ </tbody>
723
+ </table>
724
+ <ul class="bbs-total"><li>Total : 9</li><li>2/3 Page</li></ul>
725
+ `
726
+
727
+ expect(parseScheduleList(html).pagination).toEqual({ total: 9, currentPage: 2, totalPages: 3 })
408
728
  })
409
729
 
410
730
  it('parses the 10-column mentoring application history table', () => {
@@ -435,6 +755,7 @@ describe('formatters', () => {
435
755
  id: 1,
436
756
  category: '멘토 특강',
437
757
  title: '웹 성능 특강',
758
+ url: '/sw/mypage/mentoLec/view.do?qustnrSn=9572',
438
759
  author: '전수열',
439
760
  sessionDate: '2026-04-11',
440
761
  appliedAt: '2026.04.02',
@@ -457,12 +778,35 @@ describe('formatters', () => {
457
778
  expect(parsePagination(html)).toEqual({ total: 11, currentPage: 1, totalPages: 2 })
458
779
  })
459
780
 
781
+ describe('parsePagination with itemCount', () => {
782
+ it('overrides bogus totalPages when all items fit on the first page', () => {
783
+ // SWMaestro returns "1/50 Page" even for 7 items on report/mentoring lists.
784
+ const html = `<ul class="bbs-total"><li>Total : 7</li><li>1/50 Page</li></ul>`
785
+ expect(parsePagination(html, { itemCount: 7 })).toEqual({ total: 7, currentPage: 1, totalPages: 1 })
786
+ })
787
+
788
+ it('honors parsed totalPages when there are genuinely more pages', () => {
789
+ const html = `<ul class="bbs-total"><li>Total : 12</li><li>1/2 Page</li></ul>`
790
+ expect(parsePagination(html, { itemCount: 10 })).toEqual({ total: 12, currentPage: 1, totalPages: 2 })
791
+ })
792
+
793
+ it('returns single page when total is zero', () => {
794
+ const html = `<ul class="bbs-total"><li>Total : 0</li><li>1/1 Page</li></ul>`
795
+ expect(parsePagination(html, { itemCount: 0 })).toEqual({ total: 0, currentPage: 1, totalPages: 1 })
796
+ })
797
+
798
+ it('falls back to parsed totalPages when itemCount is omitted', () => {
799
+ const html = `<ul class="bbs-total"><li>Total : 7</li><li>1/50 Page</li></ul>`
800
+ expect(parsePagination(html)).toEqual({ total: 7, currentPage: 1, totalPages: 50 })
801
+ })
802
+ })
803
+
460
804
  it('extracts the CSRF token from a hidden input field', () => {
461
805
  expect(parseCsrfToken('<form><input type="hidden" name="csrfToken" value="csrf-123"></form>')).toBe('csrf-123')
462
806
  })
463
807
 
464
808
  describe('parseReportList', () => {
465
- it('parses every field of the report list table', () => {
809
+ it('parses every field of the report list table, picking the title from div.rel so it matches the detail view', () => {
466
810
  const html = `
467
811
  <ul class="bbs-total"><li><strong class="color-blue">Total :</strong> 2</li><li><span class="color-blue">1</span>/1 Page</li></ul>
468
812
  <table class=" t">
@@ -475,9 +819,18 @@ describe('formatters', () => {
475
819
  </thead>
476
820
  <tbody>
477
821
  <tr>
478
- <td>2</td>
479
- <td>자유 멘토링</td>
480
- <td><a href="/sw/mypage/mentoringReport/view.do?menuNo=200049&amp;reportId=12345">2026년 04월 10일 멘토링 보고</a></td>
822
+ <td class="pc_only"><a href="/sw/mypage/mentoringReport/view.do?menuNo=200049&amp;reportId=12345">2</a></td>
823
+ <td class="pc_only"><a href="/sw/mypage/mentoringReport/view.do?menuNo=200049&amp;reportId=12345">자유 멘토링</a></td>
824
+ <td class="tit">
825
+ <div class="date_m block-t clearfix">
826
+ <span class="l"><a href="/sw/mypage/mentoringReport/view.do?menuNo=200049&amp;reportId=12345">자유 멘토링</a></span>
827
+ <span class="r">2026-04-10</span>
828
+ </div>
829
+ <div class="rel">
830
+ <a href="/sw/mypage/mentoringReport/view.do?menuNo=200049&amp;reportId=12345">[자유 멘토링] 2026년 04월 10일 멘토링 보고</a>
831
+ <div class="ab color-blue block-t"><strong class="label-state ing">승인</strong></div>
832
+ </div>
833
+ </td>
481
834
  <td>2026-04-10</td>
482
835
  <td>[승인]</td>
483
836
  <td class="pc_only">전수열</td>
@@ -486,9 +839,18 @@ describe('formatters', () => {
486
839
  <td>200,000</td>
487
840
  </tr>
488
841
  <tr>
489
- <td>1</td>
490
- <td>멘토 특강</td>
491
- <td><a href="/sw/mypage/mentoringReport/view.do?menuNo=200049&amp;reportId=67890">2026년 03월 15일 멘토링 보고</a></td>
842
+ <td class="pc_only"><a href="/sw/mypage/mentoringReport/view.do?menuNo=200049&amp;reportId=67890">1</a></td>
843
+ <td class="pc_only"><a href="/sw/mypage/mentoringReport/view.do?menuNo=200049&amp;reportId=67890">멘토 특강</a></td>
844
+ <td class="tit">
845
+ <div class="date_m block-t clearfix">
846
+ <span class="l"><a href="/sw/mypage/mentoringReport/view.do?menuNo=200049&amp;reportId=67890">멘토 특강</a></span>
847
+ <span class="r">2026-03-15</span>
848
+ </div>
849
+ <div class="rel">
850
+ <a href="/sw/mypage/mentoringReport/view.do?menuNo=200049&amp;reportId=67890">[멘토 특강] 2026년 03월 15일 멘토링 보고</a>
851
+ <div class="ab color-blue block-t"><strong class="label-state ing">접수</strong></div>
852
+ </div>
853
+ </td>
492
854
  <td>2026-03-15</td>
493
855
  <td>[접수]</td>
494
856
  <td class="pc_only">전수열</td>
@@ -506,7 +868,7 @@ describe('formatters', () => {
506
868
  expect(result[0]).toEqual({
507
869
  id: 12345,
508
870
  category: '자유 멘토링',
509
- title: '2026년 04월 10일 멘토링 보고',
871
+ title: '[자유 멘토링] 2026년 04월 10일 멘토링 보고',
510
872
  progressDate: '2026-04-10',
511
873
  status: '[승인]',
512
874
  author: '전수열',
@@ -517,7 +879,7 @@ describe('formatters', () => {
517
879
  expect(result[1]).toEqual({
518
880
  id: 67890,
519
881
  category: '멘토 특강',
520
- title: '2026년 03월 15일 멘토링 보고',
882
+ title: '[멘토 특강] 2026년 03월 15일 멘토링 보고',
521
883
  progressDate: '2026-03-15',
522
884
  status: '[접수]',
523
885
  author: '전수열',
@@ -526,7 +888,6 @@ describe('formatters', () => {
526
888
  payAmount: '150,000',
527
889
  })
528
890
 
529
- // Validate against schema
530
891
  result.forEach((item) => {
531
892
  ReportListItemSchema.parse(item)
532
893
  })
@@ -671,3 +1032,137 @@ describe('formatters', () => {
671
1032
  })
672
1033
  })
673
1034
  })
1035
+
1036
+ describe('parseMentoringEditForm', () => {
1037
+ const untilLectureHtml = `
1038
+ <form id="board">
1039
+ <input type="hidden" name="qustnrSn" value="9572" />
1040
+ <input type="radio" name="reportCd" value="MRC010" />
1041
+ <input type="radio" name="reportCd" value="MRC020" checked />
1042
+ <input type="text" name="qustnrSj" value="웹 성능 특강" />
1043
+ <input type="radio" name="receiptType" value="UNTIL_LECTURE" checked />
1044
+ <input type="radio" name="receiptType" value="DIRECT" />
1045
+ <input type="text" name="bgndeDate" value="2026-04-01" />
1046
+ <select name="bgndeTime"><option value="00:00" selected>00시</option></select>
1047
+ <input type="text" name="enddeDate" value="2026-04-11" />
1048
+ <select name="enddeTime"><option value="14:00" selected>14시</option></select>
1049
+ <input type="text" name="eventDt" value="2026-04-11" />
1050
+ <select name="eventStime"><option value="14:00" selected>14시</option></select>
1051
+ <select name="eventEtime"><option value="15:30" selected>15시30분</option></select>
1052
+ <input type="text" name="applyCnt" value="20" />
1053
+ <select name="place"><option value="온라인(Webex)" selected>온라인(Webex)</option></select>
1054
+ </form>
1055
+ `
1056
+
1057
+ const directHtml = `
1058
+ <form id="board">
1059
+ <input type="hidden" name="qustnrSn" value="10551" />
1060
+ <input type="radio" name="reportCd" value="MRC010" />
1061
+ <input type="radio" name="reportCd" value="MRC020" checked />
1062
+ <input type="text" name="qustnrSj" value="리뷰 검증" />
1063
+ <input type="radio" name="receiptType" value="UNTIL_LECTURE" />
1064
+ <input type="radio" name="receiptType" value="DIRECT" checked />
1065
+ <input type="text" name="bgndeDate" value="2026-11-01" />
1066
+ <select name="bgndeTime"><option value="09:00" selected>09시</option></select>
1067
+ <input type="text" name="enddeDate" value="2026-12-20" />
1068
+ <select name="enddeTime"><option value="18:00" selected>18시</option></select>
1069
+ <input type="text" name="eventDt" value="2026-12-30" />
1070
+ <select name="eventStime"><option value="19:00" selected>19시</option></select>
1071
+ <select name="eventEtime"><option value="20:00" selected>20시</option></select>
1072
+ <input type="text" name="applyCnt" value="10" />
1073
+ <select name="place"><option value="온라인(Webex)" selected>온라인(Webex)</option></select>
1074
+ </form>
1075
+ `
1076
+
1077
+ it('extracts the UNTIL_LECTURE receipt window and session schedule verbatim from the form', () => {
1078
+ const form = parseMentoringEditForm(untilLectureHtml)
1079
+
1080
+ expect(form).toMatchObject({
1081
+ id: 9572,
1082
+ title: '웹 성능 특강',
1083
+ reportCd: 'MRC020',
1084
+ receiptType: 'UNTIL_LECTURE',
1085
+ bgndeDate: '2026-04-01',
1086
+ bgndeTime: '00:00',
1087
+ enddeDate: '2026-04-11',
1088
+ enddeTime: '14:00',
1089
+ eventDt: '2026-04-11',
1090
+ eventStime: '14:00',
1091
+ eventEtime: '15:30',
1092
+ applyCnt: 20,
1093
+ place: '온라인(Webex)',
1094
+ })
1095
+ })
1096
+
1097
+ it('preserves the DIRECT receipt window so partial updates do not silently switch to UNTIL_LECTURE defaults', () => {
1098
+ const form = parseMentoringEditForm(directHtml)
1099
+
1100
+ expect(form.receiptType).toBe('DIRECT')
1101
+ expect(form.bgndeDate).toBe('2026-11-01')
1102
+ expect(form.bgndeTime).toBe('09:00')
1103
+ expect(form.enddeDate).toBe('2026-12-20')
1104
+ expect(form.enddeTime).toBe('18:00')
1105
+ })
1106
+
1107
+ it('falls back to the INITIAL_DATA JS block when the server omits option[selected] on time selects', () => {
1108
+ const html = `
1109
+ <form id="board">
1110
+ <input type="hidden" name="qustnrSn" value="10552" />
1111
+ <input type="radio" name="reportCd" value="MRC020" checked />
1112
+ <input type="text" name="qustnrSj" value="리뷰 검증" />
1113
+ <input type="text" name="bgndeDate" value="2026-11-01" />
1114
+ <select id="bgndeTime" name="bgndeTime"></select>
1115
+ <input type="text" name="enddeDate" value="2026-12-20" />
1116
+ <select id="enddeTime" name="enddeTime"></select>
1117
+ <input type="text" name="eventDt" value="2026-12-30" />
1118
+ <select id="eventStime" name="eventStime"></select>
1119
+ <select id="eventEtime" name="eventEtime"></select>
1120
+ <input type="text" name="applyCnt" value="10" />
1121
+ <select name="place"><option value="온라인(Webex)" selected>온라인(Webex)</option></select>
1122
+ </form>
1123
+ <script>
1124
+ var INITIAL_DATA = {
1125
+ bgndeDate: '2026-11-01',
1126
+ bgndeTime: '09:00',
1127
+ enddeDate: '2026-12-20',
1128
+ enddeTime: '18:00',
1129
+ eventDt: '2026-12-30',
1130
+ eventStime: '19:00',
1131
+ eventEtime: '20:00',
1132
+ receiptType: 'DIRECT'
1133
+ };
1134
+ </script>
1135
+ `
1136
+
1137
+ const form = parseMentoringEditForm(html)
1138
+
1139
+ expect(form).toMatchObject({
1140
+ receiptType: 'DIRECT',
1141
+ bgndeDate: '2026-11-01',
1142
+ bgndeTime: '09:00',
1143
+ enddeDate: '2026-12-20',
1144
+ enddeTime: '18:00',
1145
+ eventStime: '19:00',
1146
+ eventEtime: '20:00',
1147
+ })
1148
+ })
1149
+
1150
+ it('uses the provided id argument when the form has no qustnrSn hidden input', () => {
1151
+ const html = `<form id="board">
1152
+ <input type="radio" name="reportCd" value="MRC010" checked />
1153
+ <input type="radio" name="receiptType" value="UNTIL_LECTURE" checked />
1154
+ <input type="text" name="qustnrSj" value="" />
1155
+ <input type="text" name="bgndeDate" value="2026-04-01" />
1156
+ <select name="bgndeTime"><option value="00:00" selected>00시</option></select>
1157
+ <input type="text" name="enddeDate" value="2026-04-01" />
1158
+ <select name="enddeTime"><option value="10:00" selected>10시</option></select>
1159
+ <input type="text" name="eventDt" value="2026-04-01" />
1160
+ <select name="eventStime"><option value="10:00" selected>10시</option></select>
1161
+ <select name="eventEtime"><option value="11:00" selected>11시</option></select>
1162
+ <input type="text" name="applyCnt" value="3" />
1163
+ <select name="place"><option value="온라인(Webex)" selected>온라인(Webex)</option></select>
1164
+ </form>`
1165
+
1166
+ expect(parseMentoringEditForm(html, 77).id).toBe(77)
1167
+ })
1168
+ })