opensoma 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) 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 +8 -5
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/client.d.ts +34 -7
  10. package/dist/src/client.d.ts.map +1 -1
  11. package/dist/src/client.js +224 -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 +3 -1
  30. package/dist/src/commands/index.d.ts.map +1 -1
  31. package/dist/src/commands/index.js +3 -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/commands/toz.d.ts +16 -0
  53. package/dist/src/commands/toz.d.ts.map +1 -0
  54. package/dist/src/commands/toz.js +488 -0
  55. package/dist/src/commands/toz.js.map +1 -0
  56. package/dist/src/constants.d.ts +5 -5
  57. package/dist/src/constants.d.ts.map +1 -1
  58. package/dist/src/constants.js +20 -8
  59. package/dist/src/constants.js.map +1 -1
  60. package/dist/src/credential-manager.d.ts +15 -0
  61. package/dist/src/credential-manager.d.ts.map +1 -1
  62. package/dist/src/credential-manager.js +46 -0
  63. package/dist/src/credential-manager.js.map +1 -1
  64. package/dist/src/formatters.d.ts +11 -3
  65. package/dist/src/formatters.d.ts.map +1 -1
  66. package/dist/src/formatters.js +281 -52
  67. package/dist/src/formatters.js.map +1 -1
  68. package/dist/src/http.d.ts +8 -0
  69. package/dist/src/http.d.ts.map +1 -1
  70. package/dist/src/http.js +29 -1
  71. package/dist/src/http.js.map +1 -1
  72. package/dist/src/index.d.ts +8 -1
  73. package/dist/src/index.d.ts.map +1 -1
  74. package/dist/src/index.js +4 -1
  75. package/dist/src/index.js.map +1 -1
  76. package/dist/src/session-recovery.js +2 -0
  77. package/dist/src/session-recovery.js.map +1 -1
  78. package/dist/src/shared/utils/swmaestro.d.ts +34 -1
  79. package/dist/src/shared/utils/swmaestro.d.ts.map +1 -1
  80. package/dist/src/shared/utils/swmaestro.js +102 -39
  81. package/dist/src/shared/utils/swmaestro.js.map +1 -1
  82. package/dist/src/shared/utils/team-action-params.d.ts +3 -0
  83. package/dist/src/shared/utils/team-action-params.d.ts.map +1 -0
  84. package/dist/src/shared/utils/team-action-params.js +10 -0
  85. package/dist/src/shared/utils/team-action-params.js.map +1 -0
  86. package/dist/src/shared/utils/team-params.d.ts +12 -0
  87. package/dist/src/shared/utils/team-params.d.ts.map +1 -0
  88. package/dist/src/shared/utils/team-params.js +38 -0
  89. package/dist/src/shared/utils/team-params.js.map +1 -0
  90. package/dist/src/toz-client.d.ts +89 -0
  91. package/dist/src/toz-client.d.ts.map +1 -0
  92. package/dist/src/toz-client.js +204 -0
  93. package/dist/src/toz-client.js.map +1 -0
  94. package/dist/src/toz-pending-store.d.ts +30 -0
  95. package/dist/src/toz-pending-store.d.ts.map +1 -0
  96. package/dist/src/toz-pending-store.js +36 -0
  97. package/dist/src/toz-pending-store.js.map +1 -0
  98. package/dist/src/types.d.ts +147 -10
  99. package/dist/src/types.d.ts.map +1 -1
  100. package/dist/src/types.js +74 -6
  101. package/dist/src/types.js.map +1 -1
  102. package/package.json +5 -1
  103. package/src/agent-browser-launcher.test.ts +263 -0
  104. package/src/agent-browser-launcher.ts +159 -0
  105. package/src/cli.ts +10 -5
  106. package/src/client.test.ts +673 -30
  107. package/src/client.ts +287 -67
  108. package/src/commands/agent-browser.ts +33 -0
  109. package/src/commands/auth.test.ts +77 -26
  110. package/src/commands/auth.ts +5 -3
  111. package/src/commands/dashboard.test.ts +57 -0
  112. package/src/commands/dashboard.ts +22 -19
  113. package/src/commands/helpers.test.ts +76 -25
  114. package/src/commands/helpers.ts +3 -3
  115. package/src/commands/index.ts +3 -1
  116. package/src/commands/mentoring.ts +60 -29
  117. package/src/commands/notice.ts +2 -1
  118. package/src/commands/report.ts +4 -2
  119. package/src/commands/room.ts +160 -1
  120. package/src/commands/schedule.ts +32 -0
  121. package/src/commands/team.ts +73 -5
  122. package/src/commands/toz.test.ts +51 -0
  123. package/src/commands/toz.ts +607 -0
  124. package/src/constants.ts +20 -8
  125. package/src/credential-manager.test.ts +98 -0
  126. package/src/credential-manager.ts +50 -0
  127. package/src/formatters.test.ts +528 -33
  128. package/src/formatters.ts +309 -55
  129. package/src/http.test.ts +71 -2
  130. package/src/http.ts +41 -2
  131. package/src/index.ts +23 -1
  132. package/src/session-recovery.ts +2 -0
  133. package/src/shared/utils/swmaestro.test.ts +245 -9
  134. package/src/shared/utils/swmaestro.ts +150 -47
  135. package/src/shared/utils/team-action-params.test.ts +32 -0
  136. package/src/shared/utils/team-action-params.ts +10 -0
  137. package/src/shared/utils/team-params.test.ts +141 -0
  138. package/src/shared/utils/team-params.ts +53 -0
  139. package/src/toz-client.test.ts +243 -0
  140. package/src/toz-client.ts +311 -0
  141. package/src/toz-pending-store.test.ts +91 -0
  142. package/src/toz-pending-store.ts +62 -0
  143. package/src/types.test.ts +26 -13
  144. package/src/types.ts +87 -7
  145. package/dist/src/commands/event.d.ts +0 -3
  146. package/dist/src/commands/event.d.ts.map +0 -1
  147. package/dist/src/commands/event.js +0 -58
  148. package/dist/src/commands/event.js.map +0 -1
  149. package/src/commands/event.ts +0 -73
package/src/client.ts CHANGED
@@ -12,19 +12,21 @@ 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'
25
+ import { TozClient } from './toz-client'
23
26
  import type {
24
27
  ApplicationHistoryItem,
25
28
  ApprovalListItem,
26
29
  Dashboard,
27
- EventListItem,
28
30
  MemberInfo,
29
31
  MentoringDetail,
30
32
  MentoringListItem,
@@ -37,6 +39,11 @@ import type {
37
39
  ReportListItem,
38
40
  ReportUpdateOptions,
39
41
  RoomCard,
42
+ RoomReservationDetail,
43
+ RoomReservationListItem,
44
+ RoomReservationStatus,
45
+ RoomUpdateOptions,
46
+ ScheduleListItem,
40
47
  TeamInfo,
41
48
  } from './types'
42
49
 
@@ -46,6 +53,8 @@ export interface SomaClientOptions {
46
53
  username?: string
47
54
  password?: string
48
55
  verbose?: boolean
56
+ tozName?: string
57
+ tozPhone?: string
49
58
  /** @internal */
50
59
  http?: SomaHttp
51
60
  }
@@ -54,6 +63,8 @@ export class SomaClient {
54
63
  private readonly http: SomaHttp
55
64
  private readonly options: SomaClientOptions
56
65
  private loginCredentials: { username: string; password: string } | null
66
+ // Single-flight guard: SWMaestro kills a session if it sees parallel logins for it.
67
+ private reloginInFlight: Promise<void> | null = null
57
68
 
58
69
  readonly mentoring: {
59
70
  list(options?: {
@@ -71,8 +82,11 @@ export class SomaClient {
71
82
  endTime: string
72
83
  venue: string
73
84
  maxAttendees?: number
85
+ receiptType?: 'UNTIL_LECTURE' | 'DIRECT'
74
86
  regStart?: string
87
+ regStartTime?: string
75
88
  regEnd?: string
89
+ regEndTime?: string
76
90
  content?: string
77
91
  }): Promise<void>
78
92
  update(id: number, params: MentoringUpdateOptions): Promise<void>
@@ -93,6 +107,15 @@ export class SomaClient {
93
107
  attendees?: number
94
108
  notes?: string
95
109
  }): Promise<void>
110
+ get(rentId: number): Promise<RoomReservationDetail>
111
+ update(rentId: number, params?: RoomUpdateOptions): Promise<void>
112
+ cancel(rentId: number): Promise<void>
113
+ reservations(options?: {
114
+ status?: Exclude<RoomReservationStatus, 'unknown'> | 'all'
115
+ startDate?: string
116
+ endDate?: string
117
+ page?: number
118
+ }): Promise<{ items: RoomReservationListItem[]; pagination: Pagination }>
96
119
  }
97
120
 
98
121
  readonly dashboard: {
@@ -126,19 +149,21 @@ export class SomaClient {
126
149
  }
127
150
 
128
151
  readonly team: {
129
- show(): Promise<TeamInfo>
152
+ list(options?: { search?: TeamSearchQuery }): Promise<TeamInfo>
153
+ join(teamId: string): Promise<void>
154
+ leave(teamId: string): Promise<void>
130
155
  }
131
156
 
132
157
  readonly member: {
133
158
  show(): Promise<MemberInfo>
134
159
  }
135
160
 
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>
161
+ readonly schedule: {
162
+ list(options?: { page?: number }): Promise<{ items: ScheduleListItem[]; pagination: Pagination }>
140
163
  }
141
164
 
165
+ readonly toz: TozClient
166
+
142
167
  constructor(options: SomaClientOptions = {}) {
143
168
  this.options = options
144
169
  this.loginCredentials =
@@ -165,9 +190,10 @@ export class SomaClient {
165
190
  user,
166
191
  }),
167
192
  )
193
+ const items = formatters.parseMentoringList(html)
168
194
  return {
169
- items: formatters.parseMentoringList(html),
170
- pagination: formatters.parsePagination(html),
195
+ items,
196
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
171
197
  }
172
198
  },
173
199
  get: async (id) => {
@@ -189,18 +215,27 @@ export class SomaClient {
189
215
  },
190
216
  update: async (id, params) => {
191
217
  await this.requireAuth()
192
- const existing = await this.mentoring.get(id)
218
+ const [editHtml, viewHtml] = await Promise.all([
219
+ this.http.get('/mypage/mentoLec/forUpdate.do', { menuNo: MENU_NO.MENTORING, qustnrSn: String(id) }),
220
+ this.http.get('/mypage/mentoLec/view.do', { menuNo: MENU_NO.MENTORING, qustnrSn: String(id) }),
221
+ ])
222
+ const existing = formatters.parseMentoringEditForm(editHtml, id)
223
+ const existingContent = formatters.parseMentoringDetail(viewHtml, id).content
224
+
193
225
  const merged = buildUpdateMentoringPayload(id, {
194
226
  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,
227
+ type: params.type ?? (existing.reportCd === 'MRC020' ? 'lecture' : 'public'),
228
+ date: params.date ?? existing.eventDt,
229
+ startTime: params.startTime ?? existing.eventStime,
230
+ endTime: params.endTime ?? existing.eventEtime,
231
+ venue: params.venue ?? existing.place,
232
+ maxAttendees: params.maxAttendees ?? existing.applyCnt,
233
+ receiptType: params.receiptType ?? existing.receiptType,
234
+ regStart: params.regStart ?? existing.bgndeDate,
235
+ regStartTime: params.regStartTime ?? existing.bgndeTime,
236
+ regEnd: params.regEnd ?? existing.enddeDate,
237
+ regEndTime: params.regEndTime ?? existing.enddeTime,
238
+ content: params.content ?? existingContent,
204
239
  })
205
240
  const html = await this.http.postForm('/mypage/mentoLec/update.do', merged)
206
241
  if (this.containsErrorIndicator(html)) {
@@ -225,9 +260,10 @@ export class SomaClient {
225
260
  menuNo: MENU_NO.APPLICATION_HISTORY,
226
261
  ...(options?.page ? { pageIndex: String(options.page) } : {}),
227
262
  })
263
+ const items = formatters.parseApplicationHistory(html)
228
264
  return {
229
- items: formatters.parseApplicationHistory(html),
230
- pagination: formatters.parsePagination(html),
265
+ items,
266
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
231
267
  }
232
268
  },
233
269
  }
@@ -279,24 +315,98 @@ export class SomaClient {
279
315
  await this.requireAuth()
280
316
  await this.http.post('/mypage/itemRent/insert.do', buildRoomReservationPayload(params))
281
317
  },
318
+ get: async (rentId) => {
319
+ await this.requireAuth()
320
+ return formatters.parseRoomReservationDetail(
321
+ await this.http.get('/mypage/itemRent/view.do', {
322
+ menuNo: MENU_NO.ROOM,
323
+ rentId: String(rentId),
324
+ }),
325
+ )
326
+ },
327
+ update: async (rentId, params = {}) => {
328
+ await this.requireAuth()
329
+ const existing = await this.room.get(rentId)
330
+ await this.postRoomUpdate(buildRoomUpdatePayload(existing, params))
331
+ },
332
+ cancel: async (rentId) => {
333
+ await this.requireAuth()
334
+ const existing = await this.room.get(rentId)
335
+ await this.postRoomUpdate(buildRoomCancelPayload(existing))
336
+ },
337
+ reservations: async (options) => {
338
+ await this.requireAuth()
339
+ const params: Record<string, string> = {
340
+ menuNo: MENU_NO.ROOM,
341
+ pageIndex: String(options?.page ?? 1),
342
+ }
343
+ if (options?.startDate) params.sdate = options.startDate
344
+ if (options?.endDate) params.edate = options.endDate
345
+ const status = options?.status ?? 'confirmed'
346
+ if (status !== 'all') {
347
+ params.searchStat = status === 'cancelled' ? 'RS002' : 'RS001'
348
+ }
349
+ const html = await this.http.get('/mypage/itemRent/list.do', params)
350
+ const items = formatters.parseRoomReservationList(html)
351
+ return {
352
+ items,
353
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
354
+ }
355
+ },
282
356
  }
283
357
 
284
358
  this.dashboard = {
285
359
  get: async () => {
286
360
  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 } }),
361
+ const dashboard = formatters.parseDashboard(
362
+ await this.http.get('/mypage/myMain/dashboard.do', { menuNo: MENU_NO.DASHBOARD }),
363
+ )
364
+ const trainee = isTraineeRole(dashboard.role)
365
+ const teamSearchField = trainee ? ('member' as const) : ('mentor' as const)
366
+ if (trainee) {
367
+ const [firstPage, teams] = await Promise.all([
368
+ this.mentoring.history(),
369
+ this.team.list({ search: { field: teamSearchField, value: '@me', me: true } }),
370
+ ])
371
+ const remainingPages = await Promise.all(
372
+ Array.from({ length: Math.max(0, firstPage.pagination.totalPages - 1) }, (_, i) =>
373
+ this.mentoring.history({ page: i + 2 }),
374
+ ),
375
+ )
376
+ const historyItems = [firstPage, ...remainingPages].flatMap((p) => p.items)
377
+ dashboard.mentoringSessions = sortDashboardMentoringItems(
378
+ historyItems
379
+ .map(applicationHistoryToDashboardItem)
380
+ .filter((item): item is Dashboard['mentoringSessions'][number] => item !== null),
381
+ )
382
+ dashboard.teams = teams.teams
383
+ return dashboard
384
+ }
385
+
386
+ const search = { field: 'author' as const, value: '@me', me: true }
387
+ const [firstPage, teams] = await Promise.all([
388
+ this.mentoring.list({ search }),
389
+ this.team.list({ search: { field: teamSearchField, value: '@me', me: true } }),
290
390
  ])
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
- }))
391
+ // Exhaust pagination: dashboard time totals must span the whole month, not just page 1.
392
+ const remainingPages = await Promise.all(
393
+ Array.from({ length: Math.max(0, firstPage.pagination.totalPages - 1) }, (_, i) =>
394
+ this.mentoring.list({ search, page: i + 2 }),
395
+ ),
396
+ )
397
+ const myMentoring = [firstPage, ...remainingPages].flatMap((p) => p.items)
398
+ dashboard.mentoringSessions = sortDashboardMentoringItems(
399
+ myMentoring.map((item) => ({
400
+ title: item.title,
401
+ url: `/mypage/mentoLec/view.do?qustnrSn=${item.id}`,
402
+ status: item.status,
403
+ date: item.sessionDate,
404
+ time: item.sessionTime.start,
405
+ timeEnd: item.sessionTime.end,
406
+ type: item.type,
407
+ })),
408
+ )
409
+ dashboard.teams = teams.teams
300
410
  return dashboard
301
411
  },
302
412
  }
@@ -308,9 +418,10 @@ export class SomaClient {
308
418
  menuNo: MENU_NO.NOTICE,
309
419
  ...(options?.page ? { pageIndex: String(options.page) } : {}),
310
420
  })
421
+ const items = formatters.parseNoticeList(html)
311
422
  return {
312
- items: formatters.parseNoticeList(html),
313
- pagination: formatters.parsePagination(html),
423
+ items,
424
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
314
425
  }
315
426
  },
316
427
  get: async (id) => {
@@ -335,9 +446,10 @@ export class SomaClient {
335
446
  if (options?.searchField !== undefined) params.searchCnd = options.searchField
336
447
  if (options?.searchKeyword) params.searchWrd = options.searchKeyword
337
448
  const html = await this.http.get('/mypage/mentoringReport/list.do', params)
449
+ const items = formatters.parseReportList(html)
338
450
  return {
339
- items: formatters.parseReportList(html),
340
- pagination: formatters.parsePagination(html),
451
+ items,
452
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
341
453
  }
342
454
  },
343
455
  get: async (id) => {
@@ -429,17 +541,27 @@ export class SomaClient {
429
541
  if (options?.month) params.searchMonth = options.month
430
542
  if (options?.reportType !== undefined) params.searchReport = options.reportType
431
543
  const html = await this.http.get('/mypage/mentoringReport/resultList.do', params)
544
+ const items = formatters.parseApprovalList(html)
432
545
  return {
433
- items: formatters.parseApprovalList(html),
434
- pagination: formatters.parsePagination(html),
546
+ items,
547
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
435
548
  }
436
549
  },
437
550
  }
438
551
 
439
552
  this.team = {
440
- show: async () => {
553
+ list: async (options) => {
441
554
  await this.requireAuth()
442
- return formatters.parseTeamInfo(await this.http.get('/mypage/myTeam/team.do', { menuNo: MENU_NO.TEAM }))
555
+ const user = options?.search?.me ? await this.resolveUser() : undefined
556
+ return formatters.parseTeamInfo(
557
+ await this.http.get('/mypage/myTeam/team.do', buildTeamListParams({ search: options?.search, user })),
558
+ )
559
+ },
560
+ join: async (teamId) => {
561
+ await this.postTeamAction('/mypage/myTeam/updateUserTeamIn.json', teamId, '팀 참여에 실패했습니다.')
562
+ },
563
+ leave: async (teamId) => {
564
+ await this.postTeamAction('/mypage/myTeam/updateUserTeamOut.json', teamId, '팀 탈퇴에 실패했습니다.')
443
565
  },
444
566
  }
445
567
 
@@ -452,32 +574,21 @@ export class SomaClient {
452
574
  },
453
575
  }
454
576
 
455
- this.event = {
577
+ this.schedule = {
456
578
  list: async (options) => {
457
579
  await this.requireAuth()
458
- const html = await this.http.get('/mypage/applicants/list.do', {
459
- menuNo: MENU_NO.EVENT,
580
+ const html = await this.http.get('/mypage/schedule/list.do', {
581
+ menuNo: MENU_NO.SCHEDULE,
460
582
  ...(options?.page ? { pageIndex: String(options.page) } : {}),
461
583
  })
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))
584
+ return formatters.parseScheduleList(html)
479
585
  },
480
586
  }
587
+
588
+ this.toz = new TozClient({
589
+ name: options.tozName,
590
+ phone: options.tozPhone,
591
+ })
481
592
  }
482
593
 
483
594
  getSessionData(): { sessionCookie: string | undefined; csrfToken: string | null } {
@@ -490,7 +601,7 @@ export class SomaClient {
490
601
  private async requireAuth(): Promise<void> {
491
602
  let identity = await this.http.checkLogin()
492
603
  if (!identity && this.loginCredentials) {
493
- await this.http.login(this.loginCredentials.username, this.loginCredentials.password)
604
+ await this.relogin()
494
605
  identity = await this.http.checkLogin()
495
606
  }
496
607
 
@@ -499,6 +610,19 @@ export class SomaClient {
499
610
  }
500
611
  }
501
612
 
613
+ private async relogin(): Promise<void> {
614
+ if (!this.loginCredentials) {
615
+ throw new AuthenticationError()
616
+ }
617
+ if (!this.reloginInFlight) {
618
+ const { username, password } = this.loginCredentials
619
+ this.reloginInFlight = this.http.login(username, password).finally(() => {
620
+ this.reloginInFlight = null
621
+ })
622
+ }
623
+ await this.reloginInFlight
624
+ }
625
+
502
626
  private async resolveUser(): Promise<UserIdentity | undefined> {
503
627
  const identity = await this.http.checkLogin()
504
628
  return identity ?? undefined
@@ -523,6 +647,10 @@ export class SomaClient {
523
647
  return Boolean(await this.http.checkLogin())
524
648
  }
525
649
 
650
+ async whoami(): Promise<UserIdentity | null> {
651
+ return this.http.checkLogin()
652
+ }
653
+
526
654
  async logout(): Promise<void> {
527
655
  await this.http.logout()
528
656
  }
@@ -544,6 +672,30 @@ export class SomaClient {
544
672
  })
545
673
  }
546
674
 
675
+ private async postTeamAction(path: string, teamId: string, fallbackMessage: string): Promise<void> {
676
+ await this.requireAuth()
677
+ const user = await this.resolveUser()
678
+ if (!user) throw new AuthenticationError()
679
+ if (!user.userNo) {
680
+ throw new Error('현재 사용자의 userNo를 확인할 수 없습니다.')
681
+ }
682
+ const response = await this.http.postJson<{ resultCode?: string }>(path, buildTeamActionPayload(teamId, user))
683
+ if (response.resultCode !== 'success') {
684
+ throw new Error(fallbackMessage)
685
+ }
686
+ }
687
+
688
+ private async postRoomUpdate(payload: Record<string, string>): Promise<void> {
689
+ try {
690
+ await this.http.post('/mypage/itemRent/update.do', payload)
691
+ } catch (error) {
692
+ if (error instanceof Error && isSuccessAlertMessage(error.message)) {
693
+ return
694
+ }
695
+ throw error
696
+ }
697
+ }
698
+
547
699
  private containsErrorIndicator(html: string): boolean {
548
700
  const errorPatterns = [
549
701
  'class="error"',
@@ -554,14 +706,20 @@ export class SomaClient {
554
706
  '실패하였습니다',
555
707
  '잘못된 접근',
556
708
  '권한이 없습니다',
557
- '<script>alert(',
558
709
  ]
559
- return errorPatterns.some((pattern) => html.includes(pattern))
710
+ if (errorPatterns.some((pattern) => html.includes(pattern))) {
711
+ return true
712
+ }
713
+ const alertMatch = html.match(/<script[^>]*>\s*alert\(['"](.+?)['"]\)/)
714
+ if (alertMatch && !isSuccessAlertMessage(alertMatch[1])) {
715
+ return true
716
+ }
717
+ return false
560
718
  }
561
719
 
562
720
  private extractErrorMessage(html: string): string | null {
563
- const alertMatch = html.match(/<script>alert\(['"](.+?)['"]\)/)
564
- if (alertMatch) {
721
+ const alertMatch = html.match(/<script[^>]*>\s*alert\(['"](.+?)['"]\)/)
722
+ if (alertMatch && !isSuccessAlertMessage(alertMatch[1])) {
565
723
  return alertMatch[1]
566
724
  }
567
725
  const errorDivMatch = html.match(/class="error[^"]*"[^>]*>\s*([^<]+)/)
@@ -571,3 +729,65 @@ export class SomaClient {
571
729
  return null
572
730
  }
573
731
  }
732
+
733
+ function isTraineeRole(role: string): boolean {
734
+ return role.includes('연수생')
735
+ }
736
+
737
+ function applicationHistoryToDashboardItem(
738
+ item: ApplicationHistoryItem,
739
+ ): Dashboard['mentoringSessions'][number] | null {
740
+ if (item.applicationStatus.includes('취소')) return null
741
+
742
+ const type = applicationCategoryToMentoringType(item.category)
743
+ if (!type) return null
744
+
745
+ const { date, time, timeEnd } = parseApplicationSessionDate(item.sessionDate)
746
+ if (date && date < new Date().toISOString().slice(0, 10)) return null
747
+
748
+ return {
749
+ title: item.title,
750
+ url: item.url ?? '/mentoring/history',
751
+ status: item.applicationStatus,
752
+ ...(date ? { date } : {}),
753
+ ...(time ? { time } : {}),
754
+ ...(timeEnd ? { timeEnd } : {}),
755
+ type,
756
+ }
757
+ }
758
+
759
+ function sortDashboardMentoringItems(items: Dashboard['mentoringSessions']): Dashboard['mentoringSessions'] {
760
+ return [...items].sort((a, b) => dashboardMentoringSortKey(a).localeCompare(dashboardMentoringSortKey(b)))
761
+ }
762
+
763
+ function dashboardMentoringSortKey(item: Dashboard['mentoringSessions'][number]): string {
764
+ return `${item.date || '9999-12-31'} ${item.time || '99:99'}`
765
+ }
766
+
767
+ function applicationCategoryToMentoringType(category: string): '자유 멘토링' | '멘토 특강' | null {
768
+ const compact = category.replace(/\s+/g, '')
769
+ if (compact.includes('특강')) return '멘토 특강'
770
+ if (compact.includes('멘토링')) return '자유 멘토링'
771
+ return null
772
+ }
773
+
774
+ function parseApplicationSessionDate(value: string): { date?: string; time?: string; timeEnd?: string } {
775
+ const date = value.match(/\d{4}-\d{2}-\d{2}/)?.[0]
776
+ const times = value.match(/\d{1,2}:\d{2}(?::\d{2})?/g)?.map(normalizeDashboardTime) ?? []
777
+ return {
778
+ ...(date ? { date } : {}),
779
+ ...(times[0] ? { time: times[0] } : {}),
780
+ ...(times[1] ? { timeEnd: times[1] } : {}),
781
+ }
782
+ }
783
+
784
+ function normalizeDashboardTime(value: string): string {
785
+ const [hours = '', minutes = ''] = value.split(':')
786
+ return `${hours.padStart(2, '0')}:${minutes}`
787
+ }
788
+
789
+ function isSuccessAlertMessage(message: string): boolean {
790
+ return /정상적으로|등록\s?하였습니다|등록\s?되었습니다|수정\s?하였습니다|수정\s?되었습니다|저장\s?되었습니다|완료\s?되었습니다|삭제\s?되었습니다|취소\s?되었습니다/.test(
791
+ message,
792
+ )
793
+ }
@@ -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
+ )