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
@@ -11,7 +11,7 @@ import {
11
11
  buildDeleteMentoringPayload,
12
12
  buildMentoringPayload,
13
13
  buildUpdateMentoringPayload,
14
- toMentoringType,
14
+ type ReceiptType,
15
15
  } from '../shared/utils/swmaestro'
16
16
  import { getHttpOrExit } from './helpers'
17
17
 
@@ -32,7 +32,10 @@ type CreateOptions = {
32
32
  venue: string
33
33
  maxAttendees?: string
34
34
  regStart?: string
35
+ regStartTime?: string
35
36
  regEnd?: string
37
+ regEndTime?: string
38
+ receiptType?: string
36
39
  content?: string
37
40
  pretty?: boolean
38
41
  }
@@ -45,7 +48,10 @@ type UpdateOptions = {
45
48
  venue?: string
46
49
  maxAttendees?: string
47
50
  regStart?: string
51
+ regStartTime?: string
48
52
  regEnd?: string
53
+ regEndTime?: string
54
+ receiptType?: string
49
55
  content?: string
50
56
  pretty?: boolean
51
57
  }
@@ -67,11 +73,12 @@ async function listAction(options: ListOptions): Promise<void> {
67
73
  user,
68
74
  }),
69
75
  )
76
+ const items = formatters.parseMentoringList(html)
70
77
  console.log(
71
78
  formatOutput(
72
79
  {
73
- items: formatters.parseMentoringList(html),
74
- pagination: formatters.parsePagination(html),
80
+ items,
81
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
75
82
  },
76
83
  options.pretty,
77
84
  ),
@@ -108,7 +115,10 @@ async function createAction(options: CreateOptions): Promise<void> {
108
115
  venue: options.venue,
109
116
  maxAttendees: options.maxAttendees ? Number.parseInt(options.maxAttendees, 10) : undefined,
110
117
  regStart: options.regStart,
118
+ regStartTime: options.regStartTime,
111
119
  regEnd: options.regEnd,
120
+ regEndTime: options.regEndTime,
121
+ receiptType: parseReceiptType(options.receiptType),
112
122
  content: options.content,
113
123
  }),
114
124
  )
@@ -118,29 +128,43 @@ async function createAction(options: CreateOptions): Promise<void> {
118
128
  }
119
129
  }
120
130
 
131
+ function parseReceiptType(value: string | undefined): ReceiptType | undefined {
132
+ if (!value) return undefined
133
+ const normalized = value.toLowerCase()
134
+ if (normalized === 'direct') return 'DIRECT'
135
+ if (normalized === 'lecture' || normalized === 'until-lecture' || normalized === 'until_lecture')
136
+ return 'UNTIL_LECTURE'
137
+ throw new Error(`Invalid receipt type: ${value}. Expected "lecture" or "direct".`)
138
+ }
139
+
121
140
  async function updateAction(id: string, options: UpdateOptions): Promise<void> {
122
141
  try {
123
142
  const http = await getHttpOrExit()
124
143
  const numId = Number.parseInt(id, 10)
125
- const html = await http.get('/mypage/mentoLec/view.do', {
126
- menuNo: MENU_NO.MENTORING,
127
- qustnrSn: id,
128
- })
129
- const existing = formatters.parseMentoringDetail(html, numId)
144
+
145
+ const [editHtml, viewHtml] = await Promise.all([
146
+ http.get('/mypage/mentoLec/forUpdate.do', { menuNo: MENU_NO.MENTORING, qustnrSn: id }),
147
+ http.get('/mypage/mentoLec/view.do', { menuNo: MENU_NO.MENTORING, qustnrSn: id }),
148
+ ])
149
+ const existing = formatters.parseMentoringEditForm(editHtml, numId)
150
+ const existingContent = formatters.parseMentoringDetail(viewHtml, numId).content
130
151
 
131
152
  await http.postForm(
132
153
  '/mypage/mentoLec/update.do',
133
154
  buildUpdateMentoringPayload(numId, {
134
155
  title: options.title ?? existing.title,
135
- type: options.type ?? toMentoringType(existing.type),
136
- date: options.date ?? existing.sessionDate,
137
- startTime: options.start ?? existing.sessionTime.start,
138
- endTime: options.end ?? existing.sessionTime.end,
139
- venue: options.venue ?? existing.venue,
140
- maxAttendees: options.maxAttendees ? Number.parseInt(options.maxAttendees, 10) : existing.attendees.max,
141
- regStart: options.regStart ?? existing.registrationPeriod.start,
142
- regEnd: options.regEnd ?? existing.registrationPeriod.end,
143
- content: options.content ?? existing.content,
156
+ type: options.type ?? (existing.reportCd === 'MRC020' ? 'lecture' : 'public'),
157
+ date: options.date ?? existing.eventDt,
158
+ startTime: options.start ?? existing.eventStime,
159
+ endTime: options.end ?? existing.eventEtime,
160
+ venue: options.venue ?? existing.place,
161
+ maxAttendees: options.maxAttendees ? Number.parseInt(options.maxAttendees, 10) : existing.applyCnt,
162
+ receiptType: parseReceiptType(options.receiptType) ?? existing.receiptType,
163
+ regStart: options.regStart ?? existing.bgndeDate,
164
+ regStartTime: options.regStartTime ?? existing.bgndeTime,
165
+ regEnd: options.regEnd ?? existing.enddeDate,
166
+ regEndTime: options.regEndTime ?? existing.enddeTime,
167
+ content: options.content ?? existingContent,
144
168
  }),
145
169
  )
146
170
  console.log(formatOutput({ ok: true }, options.pretty))
@@ -192,11 +216,12 @@ async function historyAction(options: HistoryOptions): Promise<void> {
192
216
  menuNo: MENU_NO.APPLICATION_HISTORY,
193
217
  ...(options.page ? { pageIndex: options.page } : {}),
194
218
  })
219
+ const items = formatters.parseApplicationHistory(html)
195
220
  console.log(
196
221
  formatOutput(
197
222
  {
198
- items: formatters.parseApplicationHistory(html),
199
- pagination: formatters.parsePagination(html),
223
+ items,
224
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
200
225
  },
201
226
  options.pretty,
202
227
  ),
@@ -231,12 +256,15 @@ export const mentoringCommand = new Command('mentoring')
231
256
  .requiredOption('--title <title>', 'Title')
232
257
  .requiredOption('--type <type>', 'Mentoring type (public|lecture)')
233
258
  .requiredOption('--date <date>', 'Session date')
234
- .requiredOption('--start <time>', 'Start time')
235
- .requiredOption('--end <time>', 'End time')
259
+ .requiredOption('--start <time>', 'Start time (HH:MM)')
260
+ .requiredOption('--end <time>', 'End time (HH:MM)')
236
261
  .requiredOption('--venue <venue>', 'Venue')
237
- .option('--max-attendees <count>', 'Maximum attendees')
238
- .option('--reg-start <date>', 'Registration start date')
239
- .option('--reg-end <date>', 'Registration end date')
262
+ .option('--max-attendees <count>', 'Maximum attendees (public: 2-5, lecture: 6+)')
263
+ .option('--receipt-type <type>', 'Registration period type (lecture|direct, default: lecture)')
264
+ .option('--reg-start <date>', 'Registration start date (YYYY-MM-DD, default: session date)')
265
+ .option('--reg-start-time <time>', 'Registration start time (HH:MM, default: 00:00)')
266
+ .option('--reg-end <date>', 'Registration end date (YYYY-MM-DD, required when --receipt-type=direct)')
267
+ .option('--reg-end-time <time>', 'Registration end time (HH:MM, required when --receipt-type=direct)')
240
268
  .option('--content <html>', 'HTML content')
241
269
  .option('--pretty', 'Pretty print JSON output')
242
270
  .action(createAction),
@@ -248,12 +276,15 @@ export const mentoringCommand = new Command('mentoring')
248
276
  .option('--title <title>', 'Title')
249
277
  .option('--type <type>', 'Mentoring type (public|lecture)')
250
278
  .option('--date <date>', 'Session date')
251
- .option('--start <time>', 'Start time')
252
- .option('--end <time>', 'End time')
279
+ .option('--start <time>', 'Start time (HH:MM)')
280
+ .option('--end <time>', 'End time (HH:MM)')
253
281
  .option('--venue <venue>', 'Venue')
254
- .option('--max-attendees <count>', 'Maximum attendees')
255
- .option('--reg-start <date>', 'Registration start date')
256
- .option('--reg-end <date>', 'Registration end date')
282
+ .option('--max-attendees <count>', 'Maximum attendees (public: 2-5, lecture: 6+)')
283
+ .option('--receipt-type <type>', 'Registration period type (lecture|direct)')
284
+ .option('--reg-start <date>', 'Registration start date (YYYY-MM-DD)')
285
+ .option('--reg-start-time <time>', 'Registration start time (HH:MM)')
286
+ .option('--reg-end <date>', 'Registration end date (YYYY-MM-DD)')
287
+ .option('--reg-end-time <time>', 'Registration end time (HH:MM)')
257
288
  .option('--content <html>', 'HTML content')
258
289
  .option('--pretty', 'Pretty print JSON output')
259
290
  .action(updateAction),
@@ -16,9 +16,10 @@ async function listAction(options: ListOptions): Promise<void> {
16
16
  menuNo: MENU_NO.NOTICE,
17
17
  ...(options.page ? { pageIndex: options.page } : {}),
18
18
  })
19
+ const items = formatters.parseNoticeList(html)
19
20
  console.log(
20
21
  formatOutput(
21
- { items: formatters.parseNoticeList(html), pagination: formatters.parsePagination(html) },
22
+ { items, pagination: formatters.parsePagination(html, { itemCount: items.length }) },
22
23
  options.pretty,
23
24
  ),
24
25
  )
@@ -79,9 +79,10 @@ async function listAction(options: ListOptions): Promise<void> {
79
79
  ...(options.search ? { searchWrd: options.search } : {}),
80
80
  })
81
81
 
82
+ const items = formatters.parseReportList(html)
82
83
  console.log(
83
84
  formatOutput(
84
- { items: formatters.parseReportList(html), pagination: formatters.parsePagination(html) },
85
+ { items, pagination: formatters.parsePagination(html, { itemCount: items.length }) },
85
86
  options.pretty,
86
87
  ),
87
88
  )
@@ -114,9 +115,10 @@ async function approvalAction(options: ApprovalOptions): Promise<void> {
114
115
  ...(options.type ? { searchReport: options.type } : {}),
115
116
  })
116
117
 
118
+ const items = formatters.parseApprovalList(html)
117
119
  console.log(
118
120
  formatOutput(
119
- { items: formatters.parseApprovalList(html), pagination: formatters.parsePagination(html) },
121
+ { items, pagination: formatters.parsePagination(html, { itemCount: items.length }) },
120
122
  options.pretty,
121
123
  ),
122
124
  )
@@ -1,9 +1,15 @@
1
1
  import { Command } from 'commander'
2
2
 
3
+ import { MENU_NO } from '../constants'
3
4
  import * as formatters from '../formatters'
4
5
  import { handleError } from '../shared/utils/error-handler'
5
6
  import { formatOutput } from '../shared/utils/output'
6
- import { buildRoomReservationPayload, resolveRoomId } from '../shared/utils/swmaestro'
7
+ import {
8
+ buildRoomCancelPayload,
9
+ buildRoomReservationPayload,
10
+ buildRoomUpdatePayload,
11
+ resolveRoomId,
12
+ } from '../shared/utils/swmaestro'
7
13
  import { getHttpOrExit } from './helpers'
8
14
 
9
15
  type ListOptions = { date?: string; room?: string; reservations?: boolean; pretty?: boolean }
@@ -17,6 +23,26 @@ type ReserveOptions = {
17
23
  notes?: string
18
24
  pretty?: boolean
19
25
  }
26
+ type GetOptions = { pretty?: boolean }
27
+ type UpdateOptions = {
28
+ title?: string
29
+ room?: string
30
+ date?: string
31
+ slots?: string
32
+ attendees?: string
33
+ notes?: string
34
+ pretty?: boolean
35
+ }
36
+ type CancelOptions = { pretty?: boolean }
37
+ type ReservationsOptions = {
38
+ status?: string
39
+ startDate?: string
40
+ endDate?: string
41
+ page?: string
42
+ pretty?: boolean
43
+ }
44
+
45
+ const ROOM_UPDATE_SUCCESS_PATTERN = /정상적으로|수정하였습니다|수정되었습니다|저장되었습니다|취소되었습니다/
20
46
 
21
47
  async function listAction(options: ListOptions): Promise<void> {
22
48
  try {
@@ -59,6 +85,35 @@ async function listAction(options: ListOptions): Promise<void> {
59
85
  }
60
86
  }
61
87
 
88
+ async function reservationsAction(options: ReservationsOptions): Promise<void> {
89
+ try {
90
+ const status = options.status ?? 'confirmed'
91
+ if (status !== 'confirmed' && status !== 'cancelled' && status !== 'all') {
92
+ throw new Error(`Invalid --status value: ${status}. Use 'confirmed', 'cancelled', or 'all'.`)
93
+ }
94
+
95
+ const http = await getHttpOrExit()
96
+ const params: Record<string, string> = {
97
+ menuNo: MENU_NO.ROOM,
98
+ pageIndex: options.page ?? '1',
99
+ }
100
+ if (options.startDate) params.sdate = options.startDate
101
+ if (options.endDate) params.edate = options.endDate
102
+ if (status === 'confirmed') params.searchStat = 'RS001'
103
+ if (status === 'cancelled') params.searchStat = 'RS002'
104
+
105
+ const html = await http.get('/mypage/itemRent/list.do', params)
106
+ const items = formatters.parseRoomReservationList(html)
107
+ const result = {
108
+ items,
109
+ pagination: formatters.parsePagination(html, { itemCount: items.length }),
110
+ }
111
+ console.log(formatOutput(result, options.pretty))
112
+ } catch (error) {
113
+ handleError(error)
114
+ }
115
+ }
116
+
62
117
  async function availableAction(roomId: string, options: AvailableOptions): Promise<void> {
63
118
  try {
64
119
  const http = await getHttpOrExit()
@@ -96,6 +151,73 @@ async function reserveAction(options: ReserveOptions): Promise<void> {
96
151
  }
97
152
  }
98
153
 
154
+ async function fetchReservationDetail(http: Awaited<ReturnType<typeof getHttpOrExit>>, rentId: number) {
155
+ return formatters.parseRoomReservationDetail(
156
+ await http.get('/mypage/itemRent/view.do', { menuNo: MENU_NO.ROOM, rentId: String(rentId) }),
157
+ )
158
+ }
159
+
160
+ async function postRoomMutation(
161
+ http: Awaited<ReturnType<typeof getHttpOrExit>>,
162
+ payload: Record<string, string>,
163
+ ): Promise<void> {
164
+ try {
165
+ await http.post('/mypage/itemRent/update.do', payload)
166
+ } catch (error) {
167
+ if (error instanceof Error && ROOM_UPDATE_SUCCESS_PATTERN.test(error.message)) {
168
+ return
169
+ }
170
+ throw error
171
+ }
172
+ }
173
+
174
+ async function getAction(rentIdArg: string, options: GetOptions): Promise<void> {
175
+ try {
176
+ const http = await getHttpOrExit()
177
+ const rentId = Number.parseInt(rentIdArg, 10)
178
+ const detail = await fetchReservationDetail(http, rentId)
179
+ console.log(formatOutput(detail, options.pretty))
180
+ } catch (error) {
181
+ handleError(error)
182
+ }
183
+ }
184
+
185
+ async function updateAction(rentIdArg: string, options: UpdateOptions): Promise<void> {
186
+ try {
187
+ const http = await getHttpOrExit()
188
+ const rentId = Number.parseInt(rentIdArg, 10)
189
+ const existing = await fetchReservationDetail(http, rentId)
190
+ const slots = options.slots
191
+ ?.split(',')
192
+ .map((slot) => slot.trim())
193
+ .filter(Boolean)
194
+ const payload = buildRoomUpdatePayload(existing, {
195
+ title: options.title,
196
+ roomId: options.room ? resolveRoomId(options.room) : undefined,
197
+ date: options.date,
198
+ slots: slots?.length ? slots : undefined,
199
+ attendees: options.attendees ? Number.parseInt(options.attendees, 10) : undefined,
200
+ notes: options.notes,
201
+ })
202
+ await postRoomMutation(http, payload)
203
+ console.log(formatOutput({ ok: true, rentId }, options.pretty))
204
+ } catch (error) {
205
+ handleError(error)
206
+ }
207
+ }
208
+
209
+ async function cancelAction(rentIdArg: string, options: CancelOptions): Promise<void> {
210
+ try {
211
+ const http = await getHttpOrExit()
212
+ const rentId = Number.parseInt(rentIdArg, 10)
213
+ const existing = await fetchReservationDetail(http, rentId)
214
+ await postRoomMutation(http, buildRoomCancelPayload(existing))
215
+ console.log(formatOutput({ ok: true, rentId }, options.pretty))
216
+ } catch (error) {
217
+ handleError(error)
218
+ }
219
+ }
220
+
99
221
  export const roomCommand = new Command('room')
100
222
  .description('Manage room reservations')
101
223
  .addCommand(
@@ -127,3 +249,40 @@ export const roomCommand = new Command('room')
127
249
  .option('--pretty', 'Pretty print JSON output')
128
250
  .action(reserveAction),
129
251
  )
252
+ .addCommand(
253
+ new Command('get')
254
+ .description('Show a single reservation by rentId')
255
+ .argument('<rentId>', 'Reservation ID returned from view.do')
256
+ .option('--pretty', 'Pretty print JSON output')
257
+ .action(getAction),
258
+ )
259
+ .addCommand(
260
+ new Command('update')
261
+ .description('Update an existing reservation (any subset of fields)')
262
+ .argument('<rentId>', 'Reservation ID returned from view.do')
263
+ .option('--title <title>', 'New title')
264
+ .option('--room <room>', 'New room ID or short name')
265
+ .option('--date <date>', 'New reservation date (YYYY-MM-DD)')
266
+ .option('--slots <slots>', 'New comma-separated HH:MM values')
267
+ .option('--attendees <count>', 'New number of attendees')
268
+ .option('--notes <notes>', 'New notes')
269
+ .option('--pretty', 'Pretty print JSON output')
270
+ .action(updateAction),
271
+ )
272
+ .addCommand(
273
+ new Command('cancel')
274
+ .description('Cancel an existing reservation')
275
+ .argument('<rentId>', 'Reservation ID returned from view.do')
276
+ .option('--pretty', 'Pretty print JSON output')
277
+ .action(cancelAction),
278
+ )
279
+ .addCommand(
280
+ new Command('reservations')
281
+ .description("List the user's room reservations")
282
+ .option('--status <status>', "Filter by status: 'confirmed' (default), 'cancelled', or 'all'")
283
+ .option('--start-date <date>', 'Earliest reservation date (YYYY-MM-DD)')
284
+ .option('--end-date <date>', 'Latest reservation date (YYYY-MM-DD)')
285
+ .option('--page <page>', 'Page number', '1')
286
+ .option('--pretty', 'Pretty print JSON output')
287
+ .action(reservationsAction),
288
+ )
@@ -0,0 +1,32 @@
1
+ import { Command } from 'commander'
2
+
3
+ import { MENU_NO } from '../constants'
4
+ import * as formatters from '../formatters'
5
+ import { handleError } from '../shared/utils/error-handler'
6
+ import { formatOutput } from '../shared/utils/output'
7
+ import { getHttpOrExit } from './helpers'
8
+
9
+ type ListOptions = { page?: string; pretty?: boolean }
10
+
11
+ async function listAction(options: ListOptions): Promise<void> {
12
+ try {
13
+ const http = await getHttpOrExit()
14
+ const html = await http.get('/mypage/schedule/list.do', {
15
+ menuNo: MENU_NO.SCHEDULE,
16
+ ...(options.page ? { pageIndex: options.page } : {}),
17
+ })
18
+ console.log(formatOutput(formatters.parseScheduleList(html), options.pretty))
19
+ } catch (error) {
20
+ handleError(error)
21
+ }
22
+ }
23
+
24
+ export const scheduleCommand = new Command('schedule')
25
+ .description('Browse monthly schedules')
26
+ .addCommand(
27
+ new Command('list')
28
+ .description('List monthly schedules')
29
+ .option('--page <n>', 'Page number')
30
+ .option('--pretty', 'Pretty print JSON output')
31
+ .action(listAction),
32
+ )
@@ -1,25 +1,93 @@
1
1
  import { Command } from 'commander'
2
2
 
3
- import { MENU_NO } from '../constants'
4
3
  import * as formatters from '../formatters'
5
4
  import { handleError } from '../shared/utils/error-handler'
6
5
  import { formatOutput } from '../shared/utils/output'
6
+ import { buildTeamActionPayload } from '../shared/utils/team-action-params'
7
+ import { buildTeamListParams, parseTeamSearchQuery } from '../shared/utils/team-params'
7
8
  import { getHttpOrExit } from './helpers'
8
9
 
9
- type ShowOptions = { pretty?: boolean }
10
+ type ListOptions = { search?: string; pretty?: boolean }
11
+ type ActionOptions = { pretty?: boolean }
10
12
 
11
- async function showAction(options: ShowOptions): Promise<void> {
13
+ async function listAction(options: ListOptions): Promise<void> {
12
14
  try {
13
15
  const http = await getHttpOrExit()
14
- const html = await http.get('/mypage/myTeam/team.do', { menuNo: MENU_NO.TEAM })
16
+ const search = options.search ? parseTeamSearchQuery(options.search) : undefined
17
+ const user = search?.me ? ((await http.checkLogin()) ?? undefined) : undefined
18
+ const html = await http.get('/mypage/myTeam/team.do', buildTeamListParams({ search, user }))
15
19
  console.log(formatOutput(formatters.parseTeamInfo(html), options.pretty))
16
20
  } catch (error) {
17
21
  handleError(error)
18
22
  }
19
23
  }
20
24
 
25
+ async function joinAction(teamId: string, options: ActionOptions): Promise<void> {
26
+ await runTeamAction({
27
+ teamId,
28
+ path: '/mypage/myTeam/updateUserTeamIn.json',
29
+ failureMessage: '팀 참여에 실패했습니다.',
30
+ pretty: options.pretty,
31
+ })
32
+ }
33
+
34
+ async function leaveAction(teamId: string, options: ActionOptions): Promise<void> {
35
+ await runTeamAction({
36
+ teamId,
37
+ path: '/mypage/myTeam/updateUserTeamOut.json',
38
+ failureMessage: '팀 탈퇴에 실패했습니다.',
39
+ pretty: options.pretty,
40
+ })
41
+ }
42
+
43
+ async function runTeamAction(params: {
44
+ teamId: string
45
+ path: string
46
+ failureMessage: string
47
+ pretty?: boolean
48
+ }): Promise<void> {
49
+ try {
50
+ const http = await getHttpOrExit()
51
+ const user = await http.checkLogin()
52
+ if (!user) throw new Error('Not logged in. Run: opensoma auth login or opensoma auth extract')
53
+ if (!user.userNo) throw new Error('현재 사용자의 userNo를 확인할 수 없습니다.')
54
+
55
+ const response = await http.postJson<{ resultCode?: string }>(
56
+ params.path,
57
+ buildTeamActionPayload(params.teamId, user),
58
+ )
59
+ if (response.resultCode !== 'success') {
60
+ throw new Error(params.failureMessage)
61
+ }
62
+ console.log(formatOutput({ ok: true }, params.pretty))
63
+ } catch (error) {
64
+ handleError(error)
65
+ }
66
+ }
67
+
21
68
  export const teamCommand = new Command('team')
22
69
  .description('Show team information')
23
70
  .addCommand(
24
- new Command('show').description('Show team').option('--pretty', 'Pretty print JSON output').action(showAction),
71
+ new Command('list')
72
+ .description('List teams')
73
+ .option(
74
+ '--search <query>',
75
+ 'Search (e.g. "keyword", "team:오픈소마", "mentor:@me", "member:@me", "project:Previzion")',
76
+ )
77
+ .option('--pretty', 'Pretty print JSON output')
78
+ .action(listAction),
79
+ )
80
+ .addCommand(
81
+ new Command('join')
82
+ .description('Join a team')
83
+ .argument('<teamId>')
84
+ .option('--pretty', 'Pretty print JSON output')
85
+ .action(joinAction),
86
+ )
87
+ .addCommand(
88
+ new Command('leave')
89
+ .description('Leave a team')
90
+ .argument('<teamId>')
91
+ .option('--pretty', 'Pretty print JSON output')
92
+ .action(leaveAction),
25
93
  )
package/src/constants.ts CHANGED
@@ -5,8 +5,8 @@ export const MENU_NO = {
5
5
  DASHBOARD: '200026',
6
6
  NOTICE: '200038',
7
7
  TEAM: '200093',
8
+ SCHEDULE: '200043',
8
9
  MENTORING: '200046',
9
- EVENT: '200045',
10
10
  APPLICATION_HISTORY: '200047',
11
11
  ROOM: '200058',
12
12
  MEMBER_INFO: '200036',
@@ -25,16 +25,20 @@ export const ROOM_IDS: Record<string, number> = {
25
25
  A8: 24,
26
26
  }
27
27
 
28
+ // Values mirror the native <select name="place"> options verbatim, including the
29
+ // trailing spaces on 건대/역삼/홍대 that swmaestro.ai ships in its HTML. Per the
30
+ // mirror rule in AGENTS.md, we send the native bytes exactly, not a "cleaned up"
31
+ // variant. If a future native release drops the spaces, update these here.
28
32
  export const VENUES = {
29
33
  TOZ_GWANGHWAMUN: '토즈-광화문점',
30
34
  TOZ_YANGJAE: '토즈-양재점',
31
35
  TOZ_GANGNAM_CONFERENCE_CENTER: '토즈-강남컨퍼런스센터점',
32
- TOZ_KONKUK: '토즈-건대점',
36
+ TOZ_KONKUK: '토즈-건대점 ',
33
37
  TOZ_GANGNAM_TOWER: '토즈-강남역토즈타워점',
34
38
  TOZ_SEOLLEUNG: '토즈-선릉점',
35
- TOZ_YEOKSAM: '토즈-역삼점',
36
- TOZ_HONGDAE: '토즈-홍대점',
37
- TOZ_SINCHON_BUSINESS_CENTER: '토즈-신촌비즈니스센터점',
39
+ TOZ_YEOKSAM: '토즈-역삼점 ',
40
+ TOZ_HONGDAE: '토즈-홍대점 ',
41
+ TOZ_SINCHON_BUSINESS_CENTER: '연수센터-7',
38
42
  ONLINE_WEBEX: '온라인(Webex)',
39
43
  SPACE_A1: '스페이스 A1',
40
44
  SPACE_A2: '스페이스 A2',
@@ -55,12 +59,20 @@ export const VENUE_ALIASES: Record<string, string> = {
55
59
  광화문점: '토즈-광화문점',
56
60
  양재점: '토즈-양재점',
57
61
  강남컨퍼런스센터점: '토즈-강남컨퍼런스센터점',
58
- 건대점: '토즈-건대점',
62
+ 건대점: '토즈-건대점 ',
59
63
  강남역토즈타워점: '토즈-강남역토즈타워점',
60
64
  선릉점: '토즈-선릉점',
61
- 역삼점: '토즈-역삼점',
62
- 홍대점: '토즈-홍대점',
65
+ 역삼점: '토즈-역삼점 ',
66
+ 홍대점: '토즈-홍대점 ',
63
67
  신촌비즈니스센터점: '연수센터-7',
68
+ '토즈-광화문점': '토즈-광화문점',
69
+ '토즈-양재점': '토즈-양재점',
70
+ '토즈-강남컨퍼런스센터점': '토즈-강남컨퍼런스센터점',
71
+ '토즈-건대점': '토즈-건대점 ',
72
+ '토즈-강남역토즈타워점': '토즈-강남역토즈타워점',
73
+ '토즈-선릉점': '토즈-선릉점',
74
+ '토즈-역삼점': '토즈-역삼점 ',
75
+ '토즈-홍대점': '토즈-홍대점 ',
64
76
  '토즈-신촌비즈니스센터점': '연수센터-7',
65
77
  }
66
78
 
@@ -66,6 +66,50 @@ describe('CredentialManager', () => {
66
66
  await expect(manager.getCredentials()).resolves.toBeNull()
67
67
  })
68
68
 
69
+ it('clearSessionState preserves username and password but wipes session fields', async () => {
70
+ const dir = await makeTempDir()
71
+ const manager = new CredentialManager(dir)
72
+
73
+ await manager.setCredentials({
74
+ sessionCookie: 'session-value',
75
+ csrfToken: 'csrf-value',
76
+ username: 'mentor@example.com',
77
+ password: 'secret-password',
78
+ loggedInAt: '2026-04-09T00:00:00.000Z',
79
+ })
80
+
81
+ await manager.clearSessionState()
82
+
83
+ await expect(manager.getCredentials()).resolves.toEqual({
84
+ sessionCookie: '',
85
+ csrfToken: '',
86
+ username: 'mentor@example.com',
87
+ password: 'secret-password',
88
+ })
89
+ })
90
+
91
+ it('clearSessionState removes the file when no recovery material is stored', async () => {
92
+ const dir = await makeTempDir()
93
+ const manager = new CredentialManager(dir)
94
+
95
+ await manager.setCredentials({
96
+ sessionCookie: 'session-value',
97
+ csrfToken: 'csrf-value',
98
+ })
99
+
100
+ await manager.clearSessionState()
101
+
102
+ await expect(manager.getCredentials()).resolves.toBeNull()
103
+ })
104
+
105
+ it('clearSessionState is a no-op when no credentials file exists', async () => {
106
+ const dir = await makeTempDir()
107
+ const manager = new CredentialManager(dir)
108
+
109
+ await expect(manager.clearSessionState()).resolves.toBeUndefined()
110
+ await expect(manager.getCredentials()).resolves.toBeNull()
111
+ })
112
+
69
113
  it('preserves session credentials but drops the password when the encryption key is missing', async () => {
70
114
  const dir = await makeTempDir()
71
115
  const manager = new CredentialManager(dir)
@@ -76,6 +76,33 @@ export class CredentialManager {
76
76
  await rm(this.encryptionKeyPath, { force: true })
77
77
  }
78
78
 
79
+ /**
80
+ * Wipe ephemeral session fields (sessionCookie, csrfToken, loggedInAt) while
81
+ * preserving long-term re-login material (username, password). Used when the
82
+ * server-side session expired but we still want automatic recovery on the
83
+ * next run via `recoverSession()`.
84
+ *
85
+ * No-op when no credentials file exists or no recovery material is stored.
86
+ */
87
+ async clearSessionState(): Promise<void> {
88
+ const current = await this.getCredentials()
89
+ if (!current) {
90
+ return
91
+ }
92
+
93
+ if (!current.username && !current.password) {
94
+ await this.remove()
95
+ return
96
+ }
97
+
98
+ await this.setCredentials({
99
+ sessionCookie: '',
100
+ csrfToken: '',
101
+ username: current.username,
102
+ password: current.password,
103
+ })
104
+ }
105
+
79
106
  private async hydrateCredentials(credentials: StoredCredentials): Promise<Credentials> {
80
107
  if (!credentials.encryptedPassword) {
81
108
  return credentials