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
@@ -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
  )
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+
3
+ import { resolveTozIdentity, resolveTozPin } from './toz'
4
+
5
+ describe('resolveTozIdentity', () => {
6
+ it('uses flag values before stored identity', async () => {
7
+ const identity = await resolveTozIdentity('Mentor One', '010-1111-2222', {
8
+ promptPhone: async () => '010-3333-4444',
9
+ store: {
10
+ getTozIdentity: async () => ({ name: 'Mentor Two', phone: '010-5555-6666' }),
11
+ },
12
+ })
13
+
14
+ expect(identity).toEqual({ name: 'Mentor One', phone: '010-1111-2222' })
15
+ })
16
+
17
+ it('prompts for phone when name is available but phone is missing', async () => {
18
+ const identity = await resolveTozIdentity('Mentor One', undefined, {
19
+ promptPhone: async () => '010-3333-4444',
20
+ store: {
21
+ getTozIdentity: async () => null,
22
+ },
23
+ })
24
+
25
+ expect(identity).toEqual({ name: 'Mentor One', phone: '010-3333-4444' })
26
+ })
27
+
28
+ it('keeps non-interactive callers failing when phone is missing', async () => {
29
+ await expect(
30
+ resolveTozIdentity('Mentor One', undefined, {
31
+ store: {
32
+ getTozIdentity: async () => null,
33
+ },
34
+ }),
35
+ ).rejects.toThrow(/Toz identity not set/)
36
+ })
37
+ })
38
+
39
+ describe('resolveTozPin', () => {
40
+ it('uses the flag value before prompting', async () => {
41
+ const pin = await resolveTozPin('123456', async () => '654321')
42
+
43
+ expect(pin).toBe('123456')
44
+ })
45
+
46
+ it('prompts when the flag value is missing', async () => {
47
+ const pin = await resolveTozPin(undefined, async () => '654321')
48
+
49
+ expect(pin).toBe('654321')
50
+ })
51
+ })