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
package/src/client.ts CHANGED
@@ -12,19 +12,20 @@ import {
12
12
  buildDeleteMentoringPayload,
13
13
  buildMentoringPayload,
14
14
  buildReportPayload,
15
+ buildRoomCancelPayload,
15
16
  buildRoomReservationPayload,
17
+ buildRoomUpdatePayload,
16
18
  buildUpdateMentoringPayload,
17
- parseEventDetail,
18
19
  resolveRoomId,
19
- toMentoringType,
20
20
  toRegionCode,
21
21
  toReportTypeCd,
22
22
  } from './shared/utils/swmaestro'
23
+ import { buildTeamActionPayload } from './shared/utils/team-action-params'
24
+ import { buildTeamListParams, type TeamSearchQuery } from './shared/utils/team-params'
23
25
  import type {
24
26
  ApplicationHistoryItem,
25
27
  ApprovalListItem,
26
28
  Dashboard,
27
- EventListItem,
28
29
  MemberInfo,
29
30
  MentoringDetail,
30
31
  MentoringListItem,
@@ -37,6 +38,11 @@ import type {
37
38
  ReportListItem,
38
39
  ReportUpdateOptions,
39
40
  RoomCard,
41
+ RoomReservationDetail,
42
+ RoomReservationListItem,
43
+ RoomReservationStatus,
44
+ RoomUpdateOptions,
45
+ ScheduleListItem,
40
46
  TeamInfo,
41
47
  } from './types'
42
48
 
@@ -54,6 +60,8 @@ export class SomaClient {
54
60
  private readonly http: SomaHttp
55
61
  private readonly options: SomaClientOptions
56
62
  private loginCredentials: { username: string; password: string } | null
63
+ // Single-flight guard: SWMaestro kills a session if it sees parallel logins for it.
64
+ private reloginInFlight: Promise<void> | null = null
57
65
 
58
66
  readonly mentoring: {
59
67
  list(options?: {
@@ -71,8 +79,11 @@ export class SomaClient {
71
79
  endTime: string
72
80
  venue: string
73
81
  maxAttendees?: number
82
+ receiptType?: 'UNTIL_LECTURE' | 'DIRECT'
74
83
  regStart?: string
84
+ regStartTime?: string
75
85
  regEnd?: string
86
+ regEndTime?: string
76
87
  content?: string
77
88
  }): Promise<void>
78
89
  update(id: number, params: MentoringUpdateOptions): Promise<void>
@@ -93,6 +104,15 @@ export class SomaClient {
93
104
  attendees?: number
94
105
  notes?: string
95
106
  }): Promise<void>
107
+ get(rentId: number): Promise<RoomReservationDetail>
108
+ update(rentId: number, params?: RoomUpdateOptions): Promise<void>
109
+ cancel(rentId: number): Promise<void>
110
+ reservations(options?: {
111
+ status?: Exclude<RoomReservationStatus, 'unknown'> | 'all'
112
+ startDate?: string
113
+ endDate?: string
114
+ page?: number
115
+ }): Promise<{ items: RoomReservationListItem[]; pagination: Pagination }>
96
116
  }
97
117
 
98
118
  readonly dashboard: {
@@ -126,17 +146,17 @@ export class SomaClient {
126
146
  }
127
147
 
128
148
  readonly team: {
129
- show(): Promise<TeamInfo>
149
+ list(options?: { search?: TeamSearchQuery }): Promise<TeamInfo>
150
+ join(teamId: string): Promise<void>
151
+ leave(teamId: string): Promise<void>
130
152
  }
131
153
 
132
154
  readonly member: {
133
155
  show(): Promise<MemberInfo>
134
156
  }
135
157
 
136
- readonly event: {
137
- list(options?: { page?: number }): Promise<{ items: EventListItem[]; pagination: Pagination }>
138
- get(id: number): Promise<unknown>
139
- apply(id: number): Promise<void>
158
+ readonly schedule: {
159
+ list(options?: { page?: number }): Promise<{ items: ScheduleListItem[]; pagination: Pagination }>
140
160
  }
141
161
 
142
162
  constructor(options: SomaClientOptions = {}) {
@@ -165,9 +185,10 @@ export class SomaClient {
165
185
  user,
166
186
  }),
167
187
  )
188
+ const items = formatters.parseMentoringList(html)
168
189
  return {
169
- items: formatters.parseMentoringList(html),
170
- pagination: formatters.parsePagination(html),
190
+ items,
191
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
171
192
  }
172
193
  },
173
194
  get: async (id) => {
@@ -189,18 +210,27 @@ export class SomaClient {
189
210
  },
190
211
  update: async (id, params) => {
191
212
  await this.requireAuth()
192
- const existing = await this.mentoring.get(id)
213
+ const [editHtml, viewHtml] = await Promise.all([
214
+ this.http.get('/mypage/mentoLec/forUpdate.do', { menuNo: MENU_NO.MENTORING, qustnrSn: String(id) }),
215
+ this.http.get('/mypage/mentoLec/view.do', { menuNo: MENU_NO.MENTORING, qustnrSn: String(id) }),
216
+ ])
217
+ const existing = formatters.parseMentoringEditForm(editHtml, id)
218
+ const existingContent = formatters.parseMentoringDetail(viewHtml, id).content
219
+
193
220
  const merged = buildUpdateMentoringPayload(id, {
194
221
  title: params.title ?? existing.title,
195
- type: params.type ?? toMentoringType(existing.type),
196
- date: params.date ?? existing.sessionDate,
197
- startTime: params.startTime ?? existing.sessionTime.start,
198
- endTime: params.endTime ?? existing.sessionTime.end,
199
- venue: params.venue ?? existing.venue,
200
- maxAttendees: params.maxAttendees ?? existing.attendees.max,
201
- regStart: params.regStart ?? existing.registrationPeriod.start,
202
- regEnd: params.regEnd ?? existing.registrationPeriod.end,
203
- content: params.content ?? existing.content,
222
+ type: params.type ?? (existing.reportCd === 'MRC020' ? 'lecture' : 'public'),
223
+ date: params.date ?? existing.eventDt,
224
+ startTime: params.startTime ?? existing.eventStime,
225
+ endTime: params.endTime ?? existing.eventEtime,
226
+ venue: params.venue ?? existing.place,
227
+ maxAttendees: params.maxAttendees ?? existing.applyCnt,
228
+ receiptType: params.receiptType ?? existing.receiptType,
229
+ regStart: params.regStart ?? existing.bgndeDate,
230
+ regStartTime: params.regStartTime ?? existing.bgndeTime,
231
+ regEnd: params.regEnd ?? existing.enddeDate,
232
+ regEndTime: params.regEndTime ?? existing.enddeTime,
233
+ content: params.content ?? existingContent,
204
234
  })
205
235
  const html = await this.http.postForm('/mypage/mentoLec/update.do', merged)
206
236
  if (this.containsErrorIndicator(html)) {
@@ -225,9 +255,10 @@ export class SomaClient {
225
255
  menuNo: MENU_NO.APPLICATION_HISTORY,
226
256
  ...(options?.page ? { pageIndex: String(options.page) } : {}),
227
257
  })
258
+ const items = formatters.parseApplicationHistory(html)
228
259
  return {
229
- items: formatters.parseApplicationHistory(html),
230
- pagination: formatters.parsePagination(html),
260
+ items,
261
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
231
262
  }
232
263
  },
233
264
  }
@@ -279,24 +310,98 @@ export class SomaClient {
279
310
  await this.requireAuth()
280
311
  await this.http.post('/mypage/itemRent/insert.do', buildRoomReservationPayload(params))
281
312
  },
313
+ get: async (rentId) => {
314
+ await this.requireAuth()
315
+ return formatters.parseRoomReservationDetail(
316
+ await this.http.get('/mypage/itemRent/view.do', {
317
+ menuNo: MENU_NO.ROOM,
318
+ rentId: String(rentId),
319
+ }),
320
+ )
321
+ },
322
+ update: async (rentId, params = {}) => {
323
+ await this.requireAuth()
324
+ const existing = await this.room.get(rentId)
325
+ await this.postRoomUpdate(buildRoomUpdatePayload(existing, params))
326
+ },
327
+ cancel: async (rentId) => {
328
+ await this.requireAuth()
329
+ const existing = await this.room.get(rentId)
330
+ await this.postRoomUpdate(buildRoomCancelPayload(existing))
331
+ },
332
+ reservations: async (options) => {
333
+ await this.requireAuth()
334
+ const params: Record<string, string> = {
335
+ menuNo: MENU_NO.ROOM,
336
+ pageIndex: String(options?.page ?? 1),
337
+ }
338
+ if (options?.startDate) params.sdate = options.startDate
339
+ if (options?.endDate) params.edate = options.endDate
340
+ const status = options?.status ?? 'confirmed'
341
+ if (status !== 'all') {
342
+ params.searchStat = status === 'cancelled' ? 'RS002' : 'RS001'
343
+ }
344
+ const html = await this.http.get('/mypage/itemRent/list.do', params)
345
+ const items = formatters.parseRoomReservationList(html)
346
+ return {
347
+ items,
348
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
349
+ }
350
+ },
282
351
  }
283
352
 
284
353
  this.dashboard = {
285
354
  get: async () => {
286
355
  await this.requireAuth()
287
- const [dashboard, { items: myMentoring }] = await Promise.all([
288
- formatters.parseDashboard(await this.http.get('/mypage/myMain/dashboard.do', { menuNo: MENU_NO.DASHBOARD })),
289
- this.mentoring.list({ search: { field: 'author', value: '@me', me: true } }),
356
+ const dashboard = formatters.parseDashboard(
357
+ await this.http.get('/mypage/myMain/dashboard.do', { menuNo: MENU_NO.DASHBOARD }),
358
+ )
359
+ const trainee = isTraineeRole(dashboard.role)
360
+ const teamSearchField = trainee ? ('member' as const) : ('mentor' as const)
361
+ if (trainee) {
362
+ const [firstPage, teams] = await Promise.all([
363
+ this.mentoring.history(),
364
+ this.team.list({ search: { field: teamSearchField, value: '@me', me: true } }),
365
+ ])
366
+ const remainingPages = await Promise.all(
367
+ Array.from({ length: Math.max(0, firstPage.pagination.totalPages - 1) }, (_, i) =>
368
+ this.mentoring.history({ page: i + 2 }),
369
+ ),
370
+ )
371
+ const historyItems = [firstPage, ...remainingPages].flatMap((p) => p.items)
372
+ dashboard.mentoringSessions = sortDashboardMentoringItems(
373
+ historyItems
374
+ .map(applicationHistoryToDashboardItem)
375
+ .filter((item): item is Dashboard['mentoringSessions'][number] => item !== null),
376
+ )
377
+ dashboard.teams = teams.teams
378
+ return dashboard
379
+ }
380
+
381
+ const search = { field: 'author' as const, value: '@me', me: true }
382
+ const [firstPage, teams] = await Promise.all([
383
+ this.mentoring.list({ search }),
384
+ this.team.list({ search: { field: teamSearchField, value: '@me', me: true } }),
290
385
  ])
291
- dashboard.mentoringSessions = myMentoring.map((item) => ({
292
- title: item.title,
293
- url: `/mypage/mentoLec/view.do?qustnrSn=${item.id}`,
294
- status: item.status,
295
- date: item.sessionDate,
296
- time: item.sessionTime.start,
297
- timeEnd: item.sessionTime.end,
298
- type: item.type,
299
- }))
386
+ // Exhaust pagination: dashboard time totals must span the whole month, not just page 1.
387
+ const remainingPages = await Promise.all(
388
+ Array.from({ length: Math.max(0, firstPage.pagination.totalPages - 1) }, (_, i) =>
389
+ this.mentoring.list({ search, page: i + 2 }),
390
+ ),
391
+ )
392
+ const myMentoring = [firstPage, ...remainingPages].flatMap((p) => p.items)
393
+ dashboard.mentoringSessions = sortDashboardMentoringItems(
394
+ myMentoring.map((item) => ({
395
+ title: item.title,
396
+ url: `/mypage/mentoLec/view.do?qustnrSn=${item.id}`,
397
+ status: item.status,
398
+ date: item.sessionDate,
399
+ time: item.sessionTime.start,
400
+ timeEnd: item.sessionTime.end,
401
+ type: item.type,
402
+ })),
403
+ )
404
+ dashboard.teams = teams.teams
300
405
  return dashboard
301
406
  },
302
407
  }
@@ -308,9 +413,10 @@ export class SomaClient {
308
413
  menuNo: MENU_NO.NOTICE,
309
414
  ...(options?.page ? { pageIndex: String(options.page) } : {}),
310
415
  })
416
+ const items = formatters.parseNoticeList(html)
311
417
  return {
312
- items: formatters.parseNoticeList(html),
313
- pagination: formatters.parsePagination(html),
418
+ items,
419
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
314
420
  }
315
421
  },
316
422
  get: async (id) => {
@@ -335,9 +441,10 @@ export class SomaClient {
335
441
  if (options?.searchField !== undefined) params.searchCnd = options.searchField
336
442
  if (options?.searchKeyword) params.searchWrd = options.searchKeyword
337
443
  const html = await this.http.get('/mypage/mentoringReport/list.do', params)
444
+ const items = formatters.parseReportList(html)
338
445
  return {
339
- items: formatters.parseReportList(html),
340
- pagination: formatters.parsePagination(html),
446
+ items,
447
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
341
448
  }
342
449
  },
343
450
  get: async (id) => {
@@ -429,17 +536,27 @@ export class SomaClient {
429
536
  if (options?.month) params.searchMonth = options.month
430
537
  if (options?.reportType !== undefined) params.searchReport = options.reportType
431
538
  const html = await this.http.get('/mypage/mentoringReport/resultList.do', params)
539
+ const items = formatters.parseApprovalList(html)
432
540
  return {
433
- items: formatters.parseApprovalList(html),
434
- pagination: formatters.parsePagination(html),
541
+ items,
542
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
435
543
  }
436
544
  },
437
545
  }
438
546
 
439
547
  this.team = {
440
- show: async () => {
548
+ list: async (options) => {
441
549
  await this.requireAuth()
442
- return formatters.parseTeamInfo(await this.http.get('/mypage/myTeam/team.do', { menuNo: MENU_NO.TEAM }))
550
+ const user = options?.search?.me ? await this.resolveUser() : undefined
551
+ return formatters.parseTeamInfo(
552
+ await this.http.get('/mypage/myTeam/team.do', buildTeamListParams({ search: options?.search, user })),
553
+ )
554
+ },
555
+ join: async (teamId) => {
556
+ await this.postTeamAction('/mypage/myTeam/updateUserTeamIn.json', teamId, '팀 참여에 실패했습니다.')
557
+ },
558
+ leave: async (teamId) => {
559
+ await this.postTeamAction('/mypage/myTeam/updateUserTeamOut.json', teamId, '팀 탈퇴에 실패했습니다.')
443
560
  },
444
561
  }
445
562
 
@@ -452,30 +569,14 @@ export class SomaClient {
452
569
  },
453
570
  }
454
571
 
455
- this.event = {
572
+ this.schedule = {
456
573
  list: async (options) => {
457
574
  await this.requireAuth()
458
- const html = await this.http.get('/mypage/applicants/list.do', {
459
- menuNo: MENU_NO.EVENT,
575
+ const html = await this.http.get('/mypage/schedule/list.do', {
576
+ menuNo: MENU_NO.SCHEDULE,
460
577
  ...(options?.page ? { pageIndex: String(options.page) } : {}),
461
578
  })
462
- return {
463
- items: formatters.parseEventList(html),
464
- pagination: formatters.parsePagination(html),
465
- }
466
- },
467
- get: async (id) => {
468
- await this.requireAuth()
469
- return parseEventDetail(
470
- await this.http.get('/mypage/applicants/view.do', {
471
- menuNo: MENU_NO.EVENT,
472
- bbsId: String(id),
473
- }),
474
- )
475
- },
476
- apply: async (id) => {
477
- await this.requireAuth()
478
- await this.http.post('/application/application/application.do', buildApplicationPayload(id))
579
+ return formatters.parseScheduleList(html)
479
580
  },
480
581
  }
481
582
  }
@@ -490,7 +591,7 @@ export class SomaClient {
490
591
  private async requireAuth(): Promise<void> {
491
592
  let identity = await this.http.checkLogin()
492
593
  if (!identity && this.loginCredentials) {
493
- await this.http.login(this.loginCredentials.username, this.loginCredentials.password)
594
+ await this.relogin()
494
595
  identity = await this.http.checkLogin()
495
596
  }
496
597
 
@@ -499,6 +600,19 @@ export class SomaClient {
499
600
  }
500
601
  }
501
602
 
603
+ private async relogin(): Promise<void> {
604
+ if (!this.loginCredentials) {
605
+ throw new AuthenticationError()
606
+ }
607
+ if (!this.reloginInFlight) {
608
+ const { username, password } = this.loginCredentials
609
+ this.reloginInFlight = this.http.login(username, password).finally(() => {
610
+ this.reloginInFlight = null
611
+ })
612
+ }
613
+ await this.reloginInFlight
614
+ }
615
+
502
616
  private async resolveUser(): Promise<UserIdentity | undefined> {
503
617
  const identity = await this.http.checkLogin()
504
618
  return identity ?? undefined
@@ -523,6 +637,10 @@ export class SomaClient {
523
637
  return Boolean(await this.http.checkLogin())
524
638
  }
525
639
 
640
+ async whoami(): Promise<UserIdentity | null> {
641
+ return this.http.checkLogin()
642
+ }
643
+
526
644
  async logout(): Promise<void> {
527
645
  await this.http.logout()
528
646
  }
@@ -544,6 +662,30 @@ export class SomaClient {
544
662
  })
545
663
  }
546
664
 
665
+ private async postTeamAction(path: string, teamId: string, fallbackMessage: string): Promise<void> {
666
+ await this.requireAuth()
667
+ const user = await this.resolveUser()
668
+ if (!user) throw new AuthenticationError()
669
+ if (!user.userNo) {
670
+ throw new Error('현재 사용자의 userNo를 확인할 수 없습니다.')
671
+ }
672
+ const response = await this.http.postJson<{ resultCode?: string }>(path, buildTeamActionPayload(teamId, user))
673
+ if (response.resultCode !== 'success') {
674
+ throw new Error(fallbackMessage)
675
+ }
676
+ }
677
+
678
+ private async postRoomUpdate(payload: Record<string, string>): Promise<void> {
679
+ try {
680
+ await this.http.post('/mypage/itemRent/update.do', payload)
681
+ } catch (error) {
682
+ if (error instanceof Error && isSuccessAlertMessage(error.message)) {
683
+ return
684
+ }
685
+ throw error
686
+ }
687
+ }
688
+
547
689
  private containsErrorIndicator(html: string): boolean {
548
690
  const errorPatterns = [
549
691
  'class="error"',
@@ -554,14 +696,20 @@ export class SomaClient {
554
696
  '실패하였습니다',
555
697
  '잘못된 접근',
556
698
  '권한이 없습니다',
557
- '<script>alert(',
558
699
  ]
559
- return errorPatterns.some((pattern) => html.includes(pattern))
700
+ if (errorPatterns.some((pattern) => html.includes(pattern))) {
701
+ return true
702
+ }
703
+ const alertMatch = html.match(/<script[^>]*>\s*alert\(['"](.+?)['"]\)/)
704
+ if (alertMatch && !isSuccessAlertMessage(alertMatch[1])) {
705
+ return true
706
+ }
707
+ return false
560
708
  }
561
709
 
562
710
  private extractErrorMessage(html: string): string | null {
563
- const alertMatch = html.match(/<script>alert\(['"](.+?)['"]\)/)
564
- if (alertMatch) {
711
+ const alertMatch = html.match(/<script[^>]*>\s*alert\(['"](.+?)['"]\)/)
712
+ if (alertMatch && !isSuccessAlertMessage(alertMatch[1])) {
565
713
  return alertMatch[1]
566
714
  }
567
715
  const errorDivMatch = html.match(/class="error[^"]*"[^>]*>\s*([^<]+)/)
@@ -571,3 +719,65 @@ export class SomaClient {
571
719
  return null
572
720
  }
573
721
  }
722
+
723
+ function isTraineeRole(role: string): boolean {
724
+ return role.includes('연수생')
725
+ }
726
+
727
+ function applicationHistoryToDashboardItem(
728
+ item: ApplicationHistoryItem,
729
+ ): Dashboard['mentoringSessions'][number] | null {
730
+ if (item.applicationStatus.includes('취소')) return null
731
+
732
+ const type = applicationCategoryToMentoringType(item.category)
733
+ if (!type) return null
734
+
735
+ const { date, time, timeEnd } = parseApplicationSessionDate(item.sessionDate)
736
+ if (date && date < new Date().toISOString().slice(0, 10)) return null
737
+
738
+ return {
739
+ title: item.title,
740
+ url: item.url ?? '/mentoring/history',
741
+ status: item.applicationStatus,
742
+ ...(date ? { date } : {}),
743
+ ...(time ? { time } : {}),
744
+ ...(timeEnd ? { timeEnd } : {}),
745
+ type,
746
+ }
747
+ }
748
+
749
+ function sortDashboardMentoringItems(items: Dashboard['mentoringSessions']): Dashboard['mentoringSessions'] {
750
+ return [...items].sort((a, b) => dashboardMentoringSortKey(a).localeCompare(dashboardMentoringSortKey(b)))
751
+ }
752
+
753
+ function dashboardMentoringSortKey(item: Dashboard['mentoringSessions'][number]): string {
754
+ return `${item.date || '9999-12-31'} ${item.time || '99:99'}`
755
+ }
756
+
757
+ function applicationCategoryToMentoringType(category: string): '자유 멘토링' | '멘토 특강' | null {
758
+ const compact = category.replace(/\s+/g, '')
759
+ if (compact.includes('특강')) return '멘토 특강'
760
+ if (compact.includes('멘토링')) return '자유 멘토링'
761
+ return null
762
+ }
763
+
764
+ function parseApplicationSessionDate(value: string): { date?: string; time?: string; timeEnd?: string } {
765
+ const date = value.match(/\d{4}-\d{2}-\d{2}/)?.[0]
766
+ const times = value.match(/\d{1,2}:\d{2}(?::\d{2})?/g)?.map(normalizeDashboardTime) ?? []
767
+ return {
768
+ ...(date ? { date } : {}),
769
+ ...(times[0] ? { time: times[0] } : {}),
770
+ ...(times[1] ? { timeEnd: times[1] } : {}),
771
+ }
772
+ }
773
+
774
+ function normalizeDashboardTime(value: string): string {
775
+ const [hours = '', minutes = ''] = value.split(':')
776
+ return `${hours.padStart(2, '0')}:${minutes}`
777
+ }
778
+
779
+ function isSuccessAlertMessage(message: string): boolean {
780
+ return /정상적으로|등록\s?하였습니다|등록\s?되었습니다|수정\s?하였습니다|수정\s?되었습니다|저장\s?되었습니다|완료\s?되었습니다|삭제\s?되었습니다|취소\s?되었습니다/.test(
781
+ message,
782
+ )
783
+ }
@@ -0,0 +1,33 @@
1
+ import { Command } from 'commander'
2
+
3
+ import { AgentBrowserLauncher } from '../agent-browser-launcher'
4
+ import { handleError } from '../shared/utils/error-handler'
5
+ import { createAuthenticatedHttp } from './helpers'
6
+
7
+ type LaunchOptions = { binary?: string }
8
+
9
+ async function launchAction(url: string, options: LaunchOptions): Promise<void> {
10
+ try {
11
+ const http = await createAuthenticatedHttp()
12
+ const sessionCookie = http.getSessionCookie()
13
+ if (!sessionCookie) {
14
+ throw new Error('Authenticated session is missing a session cookie. Run: opensoma auth login')
15
+ }
16
+
17
+ const launcher = new AgentBrowserLauncher({ binary: options.binary })
18
+ const { exitCode } = await launcher.launch({ url, sessionCookie })
19
+ process.exit(exitCode)
20
+ } catch (error) {
21
+ handleError(error)
22
+ }
23
+ }
24
+
25
+ export const agentBrowserCommand = new Command('agent-browser')
26
+ .description('Launch agent-browser pre-authenticated to swmaestro.ai')
27
+ .addCommand(
28
+ new Command('launch')
29
+ .description('Open a swmaestro.ai URL in agent-browser with the current opensoma session injected')
30
+ .argument('<url>', 'swmaestro.ai URL to open')
31
+ .option('--binary <path>', 'Path to the agent-browser executable (default: agent-browser on PATH)')
32
+ .action(launchAction),
33
+ )