opensoma 0.5.0 → 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 (133) 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 +36 -7
  10. package/dist/src/client.d.ts.map +1 -1
  11. package/dist/src/client.js +231 -63
  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 -43
  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 +801 -140
  93. package/src/client.ts +293 -79
  94. package/src/commands/agent-browser.ts +33 -0
  95. package/src/commands/auth.test.ts +83 -32
  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 +79 -32
  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.test.ts +7 -7
  105. package/src/commands/report.ts +4 -2
  106. package/src/commands/room.ts +160 -1
  107. package/src/commands/schedule.ts +32 -0
  108. package/src/commands/team.ts +73 -5
  109. package/src/constants.ts +20 -8
  110. package/src/credential-manager.test.ts +49 -5
  111. package/src/credential-manager.ts +27 -0
  112. package/src/formatters.test.ts +548 -53
  113. package/src/formatters.ts +309 -55
  114. package/src/http.test.ts +108 -39
  115. package/src/http.ts +41 -2
  116. package/src/index.ts +10 -1
  117. package/src/shared/utils/mentoring-params.test.ts +16 -16
  118. package/src/shared/utils/swmaestro.test.ts +326 -11
  119. package/src/shared/utils/swmaestro.ts +150 -52
  120. package/src/shared/utils/team-action-params.test.ts +32 -0
  121. package/src/shared/utils/team-action-params.ts +10 -0
  122. package/src/shared/utils/team-params.test.ts +141 -0
  123. package/src/shared/utils/team-params.ts +53 -0
  124. package/src/shared/utils/toz.test.ts +12 -7
  125. package/src/token-extractor.test.ts +12 -12
  126. package/src/toz-http.test.ts +11 -11
  127. package/src/types.test.ts +235 -206
  128. package/src/types.ts +87 -7
  129. package/dist/src/commands/event.d.ts +0 -3
  130. package/dist/src/commands/event.d.ts.map +0 -1
  131. package/dist/src/commands/event.js +0 -58
  132. package/dist/src/commands/event.js.map +0 -1
  133. 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
 
@@ -46,12 +52,16 @@ export interface SomaClientOptions {
46
52
  username?: string
47
53
  password?: string
48
54
  verbose?: boolean
55
+ /** @internal */
56
+ http?: SomaHttp
49
57
  }
50
58
 
51
59
  export class SomaClient {
52
60
  private readonly http: SomaHttp
53
61
  private readonly options: SomaClientOptions
54
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
55
65
 
56
66
  readonly mentoring: {
57
67
  list(options?: {
@@ -69,8 +79,11 @@ export class SomaClient {
69
79
  endTime: string
70
80
  venue: string
71
81
  maxAttendees?: number
82
+ receiptType?: 'UNTIL_LECTURE' | 'DIRECT'
72
83
  regStart?: string
84
+ regStartTime?: string
73
85
  regEnd?: string
86
+ regEndTime?: string
74
87
  content?: string
75
88
  }): Promise<void>
76
89
  update(id: number, params: MentoringUpdateOptions): Promise<void>
@@ -91,6 +104,15 @@ export class SomaClient {
91
104
  attendees?: number
92
105
  notes?: string
93
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 }>
94
116
  }
95
117
 
96
118
  readonly dashboard: {
@@ -109,7 +131,7 @@ export class SomaClient {
109
131
  searchKeyword?: string
110
132
  }): Promise<{ items: ReportListItem[]; pagination: Pagination }>
111
133
  get(id: number): Promise<ReportDetail>
112
- create(options: ReportCreateOptions, file: Buffer | string, fileName?: string): Promise<void>
134
+ create(options: ReportCreateOptions, files: Array<{ buffer: Buffer; name: string }>): Promise<void>
113
135
  update(
114
136
  id: number,
115
137
  options: Omit<ReportUpdateOptions, 'id'>,
@@ -124,28 +146,30 @@ export class SomaClient {
124
146
  }
125
147
 
126
148
  readonly team: {
127
- show(): Promise<TeamInfo>
149
+ list(options?: { search?: TeamSearchQuery }): Promise<TeamInfo>
150
+ join(teamId: string): Promise<void>
151
+ leave(teamId: string): Promise<void>
128
152
  }
129
153
 
130
154
  readonly member: {
131
155
  show(): Promise<MemberInfo>
132
156
  }
133
157
 
134
- readonly event: {
135
- list(options?: { page?: number }): Promise<{ items: EventListItem[]; pagination: Pagination }>
136
- get(id: number): Promise<unknown>
137
- apply(id: number): Promise<void>
158
+ readonly schedule: {
159
+ list(options?: { page?: number }): Promise<{ items: ScheduleListItem[]; pagination: Pagination }>
138
160
  }
139
161
 
140
162
  constructor(options: SomaClientOptions = {}) {
141
163
  this.options = options
142
164
  this.loginCredentials =
143
165
  options.username && options.password ? { username: options.username, password: options.password } : null
144
- this.http = new SomaHttp({
145
- sessionCookie: options.sessionCookie,
146
- csrfToken: options.csrfToken,
147
- verbose: options.verbose,
148
- })
166
+ this.http =
167
+ options.http ??
168
+ new SomaHttp({
169
+ sessionCookie: options.sessionCookie,
170
+ csrfToken: options.csrfToken,
171
+ verbose: options.verbose,
172
+ })
149
173
 
150
174
  this.mentoring = {
151
175
  list: async (options) => {
@@ -161,9 +185,10 @@ export class SomaClient {
161
185
  user,
162
186
  }),
163
187
  )
188
+ const items = formatters.parseMentoringList(html)
164
189
  return {
165
- items: formatters.parseMentoringList(html),
166
- pagination: formatters.parsePagination(html),
190
+ items,
191
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
167
192
  }
168
193
  },
169
194
  get: async (id) => {
@@ -185,18 +210,27 @@ export class SomaClient {
185
210
  },
186
211
  update: async (id, params) => {
187
212
  await this.requireAuth()
188
- 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
+
189
220
  const merged = buildUpdateMentoringPayload(id, {
190
221
  title: params.title ?? existing.title,
191
- type: params.type ?? toMentoringType(existing.type),
192
- date: params.date ?? existing.sessionDate,
193
- startTime: params.startTime ?? existing.sessionTime.start,
194
- endTime: params.endTime ?? existing.sessionTime.end,
195
- venue: params.venue ?? existing.venue,
196
- maxAttendees: params.maxAttendees ?? existing.attendees.max,
197
- regStart: params.regStart ?? existing.registrationPeriod.start,
198
- regEnd: params.regEnd ?? existing.registrationPeriod.end,
199
- 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,
200
234
  })
201
235
  const html = await this.http.postForm('/mypage/mentoLec/update.do', merged)
202
236
  if (this.containsErrorIndicator(html)) {
@@ -221,9 +255,10 @@ export class SomaClient {
221
255
  menuNo: MENU_NO.APPLICATION_HISTORY,
222
256
  ...(options?.page ? { pageIndex: String(options.page) } : {}),
223
257
  })
258
+ const items = formatters.parseApplicationHistory(html)
224
259
  return {
225
- items: formatters.parseApplicationHistory(html),
226
- pagination: formatters.parsePagination(html),
260
+ items,
261
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
227
262
  }
228
263
  },
229
264
  }
@@ -275,24 +310,98 @@ export class SomaClient {
275
310
  await this.requireAuth()
276
311
  await this.http.post('/mypage/itemRent/insert.do', buildRoomReservationPayload(params))
277
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
+ },
278
351
  }
279
352
 
280
353
  this.dashboard = {
281
354
  get: async () => {
282
355
  await this.requireAuth()
283
- const [dashboard, { items: myMentoring }] = await Promise.all([
284
- formatters.parseDashboard(await this.http.get('/mypage/myMain/dashboard.do', { menuNo: MENU_NO.DASHBOARD })),
285
- 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 } }),
286
385
  ])
287
- dashboard.mentoringSessions = myMentoring.map((item) => ({
288
- title: item.title,
289
- url: `/mypage/mentoLec/view.do?qustnrSn=${item.id}`,
290
- status: item.status,
291
- date: item.sessionDate,
292
- time: item.sessionTime.start,
293
- timeEnd: item.sessionTime.end,
294
- type: item.type,
295
- }))
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
296
405
  return dashboard
297
406
  },
298
407
  }
@@ -304,9 +413,10 @@ export class SomaClient {
304
413
  menuNo: MENU_NO.NOTICE,
305
414
  ...(options?.page ? { pageIndex: String(options.page) } : {}),
306
415
  })
416
+ const items = formatters.parseNoticeList(html)
307
417
  return {
308
- items: formatters.parseNoticeList(html),
309
- pagination: formatters.parsePagination(html),
418
+ items,
419
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
310
420
  }
311
421
  },
312
422
  get: async (id) => {
@@ -331,9 +441,10 @@ export class SomaClient {
331
441
  if (options?.searchField !== undefined) params.searchCnd = options.searchField
332
442
  if (options?.searchKeyword) params.searchWrd = options.searchKeyword
333
443
  const html = await this.http.get('/mypage/mentoringReport/list.do', params)
444
+ const items = formatters.parseReportList(html)
334
445
  return {
335
- items: formatters.parseReportList(html),
336
- pagination: formatters.parsePagination(html),
446
+ items,
447
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
337
448
  }
338
449
  },
339
450
  get: async (id) => {
@@ -344,7 +455,7 @@ export class SomaClient {
344
455
  })
345
456
  return formatters.parseReportDetail(html, id)
346
457
  },
347
- create: async (options, file, fileName) => {
458
+ create: async (options, files) => {
348
459
  await this.requireAuth()
349
460
  const payload = buildReportPayload({
350
461
  menteeRegion: options.menteeRegion,
@@ -369,11 +480,11 @@ export class SomaClient {
369
480
  for (const [key, value] of Object.entries(payload)) {
370
481
  formData.append(key, value)
371
482
  }
372
- const isBuffer = Buffer.isBuffer(file)
373
- const fileBuffer = isBuffer ? file : await readFile(file)
374
- const resolvedFileName = isBuffer ? (fileName ?? 'file') : (file.split('/').pop() ?? 'file')
375
- const uint8Array = new Uint8Array(fileBuffer.buffer, fileBuffer.byteOffset, fileBuffer.byteLength)
376
- formData.append('file_1_1', new Blob([uint8Array as unknown as ArrayBuffer]), resolvedFileName)
483
+ for (let i = 0; i < files.length; i++) {
484
+ const { buffer, name } = files[i]
485
+ const uint8Array = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
486
+ formData.append(`file_1_${i + 1}`, new Blob([uint8Array as unknown as ArrayBuffer]), name)
487
+ }
377
488
  formData.append('fileFieldNm_1', 'file_1')
378
489
  formData.append('atchFileId', '')
379
490
  await this.http.postMultipart('/mypage/mentoringReport/insert.do', formData)
@@ -425,17 +536,27 @@ export class SomaClient {
425
536
  if (options?.month) params.searchMonth = options.month
426
537
  if (options?.reportType !== undefined) params.searchReport = options.reportType
427
538
  const html = await this.http.get('/mypage/mentoringReport/resultList.do', params)
539
+ const items = formatters.parseApprovalList(html)
428
540
  return {
429
- items: formatters.parseApprovalList(html),
430
- pagination: formatters.parsePagination(html),
541
+ items,
542
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
431
543
  }
432
544
  },
433
545
  }
434
546
 
435
547
  this.team = {
436
- show: async () => {
548
+ list: async (options) => {
437
549
  await this.requireAuth()
438
- 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, '팀 탈퇴에 실패했습니다.')
439
560
  },
440
561
  }
441
562
 
@@ -448,30 +569,14 @@ export class SomaClient {
448
569
  },
449
570
  }
450
571
 
451
- this.event = {
572
+ this.schedule = {
452
573
  list: async (options) => {
453
574
  await this.requireAuth()
454
- const html = await this.http.get('/mypage/applicants/list.do', {
455
- menuNo: MENU_NO.EVENT,
575
+ const html = await this.http.get('/mypage/schedule/list.do', {
576
+ menuNo: MENU_NO.SCHEDULE,
456
577
  ...(options?.page ? { pageIndex: String(options.page) } : {}),
457
578
  })
458
- return {
459
- items: formatters.parseEventList(html),
460
- pagination: formatters.parsePagination(html),
461
- }
462
- },
463
- get: async (id) => {
464
- await this.requireAuth()
465
- return parseEventDetail(
466
- await this.http.get('/mypage/applicants/view.do', {
467
- menuNo: MENU_NO.EVENT,
468
- bbsId: String(id),
469
- }),
470
- )
471
- },
472
- apply: async (id) => {
473
- await this.requireAuth()
474
- await this.http.post('/application/application/application.do', buildApplicationPayload(id))
579
+ return formatters.parseScheduleList(html)
475
580
  },
476
581
  }
477
582
  }
@@ -486,7 +591,7 @@ export class SomaClient {
486
591
  private async requireAuth(): Promise<void> {
487
592
  let identity = await this.http.checkLogin()
488
593
  if (!identity && this.loginCredentials) {
489
- await this.http.login(this.loginCredentials.username, this.loginCredentials.password)
594
+ await this.relogin()
490
595
  identity = await this.http.checkLogin()
491
596
  }
492
597
 
@@ -495,6 +600,19 @@ export class SomaClient {
495
600
  }
496
601
  }
497
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
+
498
616
  private async resolveUser(): Promise<UserIdentity | undefined> {
499
617
  const identity = await this.http.checkLogin()
500
618
  return identity ?? undefined
@@ -519,6 +637,10 @@ export class SomaClient {
519
637
  return Boolean(await this.http.checkLogin())
520
638
  }
521
639
 
640
+ async whoami(): Promise<UserIdentity | null> {
641
+ return this.http.checkLogin()
642
+ }
643
+
522
644
  async logout(): Promise<void> {
523
645
  await this.http.logout()
524
646
  }
@@ -540,6 +662,30 @@ export class SomaClient {
540
662
  })
541
663
  }
542
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
+
543
689
  private containsErrorIndicator(html: string): boolean {
544
690
  const errorPatterns = [
545
691
  'class="error"',
@@ -550,14 +696,20 @@ export class SomaClient {
550
696
  '실패하였습니다',
551
697
  '잘못된 접근',
552
698
  '권한이 없습니다',
553
- '<script>alert(',
554
699
  ]
555
- 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
556
708
  }
557
709
 
558
710
  private extractErrorMessage(html: string): string | null {
559
- const alertMatch = html.match(/<script>alert\(['"](.+?)['"]\)/)
560
- if (alertMatch) {
711
+ const alertMatch = html.match(/<script[^>]*>\s*alert\(['"](.+?)['"]\)/)
712
+ if (alertMatch && !isSuccessAlertMessage(alertMatch[1])) {
561
713
  return alertMatch[1]
562
714
  }
563
715
  const errorDivMatch = html.match(/class="error[^"]*"[^>]*>\s*([^<]+)/)
@@ -567,3 +719,65 @@ export class SomaClient {
567
719
  return null
568
720
  }
569
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
+ )