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
@@ -0,0 +1,311 @@
1
+ import {
2
+ assertDurationInRange,
3
+ buildBranchIdsParam,
4
+ buildStartTimeParam,
5
+ formatDuration,
6
+ formatPhone,
7
+ parseEmail,
8
+ parsePhone,
9
+ } from './shared/utils/toz'
10
+ import {
11
+ isReservationConfirmSuccess,
12
+ parseBookingPageBranches,
13
+ parseBookingPageMeetings,
14
+ parseMypageReservations,
15
+ parseTozBoothes,
16
+ parseTozDurations,
17
+ parseTozReserved,
18
+ } from './toz-formatters'
19
+ import { TozHttp, type TozHttpOptions } from './toz-http'
20
+ import type { TozBooth, TozBranch, TozDuration, TozMeeting, TozReservation, TozReserved } from './types'
21
+
22
+ export interface TozAvailabilityQuery {
23
+ date: string
24
+ startTime: string
25
+ durationMinutes: number
26
+ userCount: number
27
+ branchIds: readonly number[]
28
+ }
29
+
30
+ export interface TozCheckQuery {
31
+ date: string
32
+ startTimes: readonly string[]
33
+ durationMinutes: number
34
+ userCount: number
35
+ branchIds: readonly number[]
36
+ }
37
+
38
+ export interface TozCheckResult {
39
+ startTime: string
40
+ booths: TozBooth[]
41
+ error?: string
42
+ }
43
+
44
+ export interface TozReserveBoothArgs {
45
+ date: string
46
+ startTime: string
47
+ durationMinutes: number
48
+ userCount: number
49
+ boothId: number
50
+ }
51
+
52
+ export interface TozConfirmArgs {
53
+ reservationId: string
54
+ date: string
55
+ startTime: string
56
+ durationMinutes: number
57
+ name: string
58
+ phone: string
59
+ email: string
60
+ pinNum: string
61
+ meetingId?: number
62
+ newMeetingName?: string
63
+ memo?: string
64
+ }
65
+
66
+ export interface TozSearchArgs {
67
+ name?: string
68
+ phone?: string
69
+ startDate?: string
70
+ endDate?: string
71
+ meetingName?: string
72
+ }
73
+
74
+ export interface TozCancelArgs {
75
+ reservationId: number
76
+ name?: string
77
+ phone?: string
78
+ }
79
+
80
+ export interface TozClientOptions extends TozHttpOptions {
81
+ name?: string
82
+ phone?: string
83
+ }
84
+
85
+ export class TozClient {
86
+ readonly http: TozHttp
87
+ readonly identity: { name?: string; phone?: string }
88
+
89
+ constructor(options: TozClientOptions = {}) {
90
+ const { name, phone, ...httpOptions } = options
91
+ this.http = new TozHttp(httpOptions)
92
+ this.identity = { name, phone }
93
+ }
94
+
95
+ async branches(): Promise<TozBranch[]> {
96
+ await this.http.bootstrap()
97
+ return parseBookingPageBranches(await this.http.get('/booking.htm'))
98
+ }
99
+
100
+ async meetings(): Promise<TozMeeting[]> {
101
+ await this.http.bootstrap()
102
+ return parseBookingPageMeetings(await this.http.get('/booking.htm'))
103
+ }
104
+
105
+ async durations(date: string, startTime: string): Promise<TozDuration[]> {
106
+ await this.http.bootstrap()
107
+ const param = buildStartTimeParam(startTime)
108
+ const json = await this.http.postJson<unknown>('/ajaxCheckDurationTime.htm', {
109
+ basedate: date,
110
+ hour: param.slice(0, 2),
111
+ minute: param.slice(2, 4),
112
+ })
113
+ return parseTozDurations(json)
114
+ }
115
+
116
+ async available(query: TozAvailabilityQuery): Promise<TozBooth[]> {
117
+ assertDurationInRange(query.durationMinutes)
118
+ await this.http.bootstrap()
119
+ const json = await this.http.postJson<unknown>('/ajaxGetEnableBoothes.htm', {
120
+ basedate: query.date,
121
+ starttime: buildStartTimeParam(query.startTime),
122
+ durationTime: formatDuration(query.durationMinutes),
123
+ userCount: String(query.userCount),
124
+ branchIds: buildBranchIdsParam(query.branchIds),
125
+ })
126
+ return parseTozBoothes(json)
127
+ }
128
+
129
+ async check(query: TozCheckQuery): Promise<TozCheckResult[]> {
130
+ if (query.startTimes.length === 0) {
131
+ throw new Error('toz check requires at least one --time. For a single slot, use `toz available` instead.')
132
+ }
133
+ if (query.startTimes.length > 6) {
134
+ throw new Error(
135
+ 'Too many times (max 6 per invocation). Consider `toz available` for a single slot, or split into multiple invocations.',
136
+ )
137
+ }
138
+ assertDurationInRange(query.durationMinutes)
139
+ await this.http.bootstrap()
140
+
141
+ const results: TozCheckResult[] = []
142
+ for (let i = 0; i < query.startTimes.length; i += 1) {
143
+ const startTime = query.startTimes[i]!
144
+ try {
145
+ const booths = await this.available({
146
+ date: query.date,
147
+ startTime,
148
+ durationMinutes: query.durationMinutes,
149
+ userCount: query.userCount,
150
+ branchIds: query.branchIds,
151
+ })
152
+ results.push({ startTime, booths })
153
+ } catch (error) {
154
+ results.push({
155
+ startTime,
156
+ booths: [],
157
+ error: error instanceof Error ? error.message : String(error),
158
+ })
159
+ }
160
+ if (i < query.startTimes.length - 1) await sleep(500)
161
+ }
162
+ return results
163
+ }
164
+
165
+ async reserveBooth(args: TozReserveBoothArgs): Promise<TozReserved> {
166
+ assertDurationInRange(args.durationMinutes)
167
+ await this.http.bootstrap()
168
+ const json = await this.http.postJson<unknown>('/ajaxReservationBooth.htm', {
169
+ basedate: args.date,
170
+ starttime: buildStartTimeParam(args.startTime),
171
+ durationTime: formatDuration(args.durationMinutes),
172
+ userCount: String(args.userCount),
173
+ booth_id: String(args.boothId),
174
+ })
175
+ const reserved = parseTozReserved(json)
176
+ if (reserved.isLargeBooth) {
177
+ throw new Error('대형부스 예약은 선입금 약관 동의가 필요합니다. v1에서는 지원하지 않습니다 (small booths only).')
178
+ }
179
+ return reserved
180
+ }
181
+
182
+ async skipEquipment(args: {
183
+ reservationId: string
184
+ date: string
185
+ startTime: string
186
+ durationMinutes: number
187
+ }): Promise<void> {
188
+ await this.http.postText('/ajaxReservationEquipment.htm', {
189
+ reservationId: args.reservationId,
190
+ basedate: args.date,
191
+ starttime: buildStartTimeParam(args.startTime),
192
+ durationTime: formatDuration(args.durationMinutes),
193
+ equipmentNotebookChecked: 'false',
194
+ equipmentProjectorChecked: 'false',
195
+ equipmentMonitorChecked: 'false',
196
+ equipmentPlayerChecked: 'false',
197
+ equipmentSpeakerChecked: 'false',
198
+ equipmentNotebookCnt: '0',
199
+ equipmentProjectorCnt: '0',
200
+ equipmentMonitorCnt: '0',
201
+ equipmentPlayerCnt: '0',
202
+ equipmentSpeakerCnt: '0',
203
+ })
204
+ }
205
+
206
+ async sendOtp(phone?: string): Promise<void> {
207
+ const parts = parsePhone(this.requirePhone(phone))
208
+ const result = await this.http.postText('/ajaxHpVerify.htm', { ...parts })
209
+ if (result !== 'SUCCESS') {
210
+ throw new Error(`OTP 발송 실패: ${result}`)
211
+ }
212
+ }
213
+
214
+ async confirm(args: TozConfirmArgs): Promise<{ resultMsg: string; isLargeBooth: boolean }> {
215
+ if (!args.meetingId && !args.newMeetingName) {
216
+ throw new Error('meetingId 또는 newMeetingName 중 하나는 필수입니다.')
217
+ }
218
+ const phoneParts = parsePhone(args.phone)
219
+ const emailParts = parseEmail(args.email)
220
+
221
+ const json = await this.http.postJson<{ resultMsg?: string; message3?: string | null }>(
222
+ '/ajaxReservationConfirm.htm',
223
+ {
224
+ reservationId: args.reservationId,
225
+ phone: formatPhone(phoneParts),
226
+ ...phoneParts,
227
+ name: args.name,
228
+ pinNum: args.pinNum,
229
+ ...emailParts,
230
+ meeting_id: args.meetingId ? String(args.meetingId) : '새모임',
231
+ newMeetingName: args.newMeetingName ?? '',
232
+ prepareMemo: args.memo ?? '',
233
+ projectSeq: '',
234
+ tozApplyType: '',
235
+ addedInfo: '',
236
+ attendType: '',
237
+ isMobile: 'false',
238
+ },
239
+ )
240
+
241
+ const resultMsg = json.resultMsg ?? ''
242
+ if (!isReservationConfirmSuccess(resultMsg)) {
243
+ throw new Error(resultMsg || '예약 확정에 실패했습니다.')
244
+ }
245
+ return { resultMsg, isLargeBooth: resultMsg.includes('대형부스') }
246
+ }
247
+
248
+ async destroyHold(): Promise<void> {
249
+ await this.http.postText('/ajaxDestroyReservation.htm', {})
250
+ }
251
+
252
+ async myReservations(args: TozSearchArgs = {}): Promise<TozReservation[]> {
253
+ await this.http.bootstrap()
254
+ await this.mypageLogin(this.requireName(args.name), this.requirePhone(args.phone))
255
+
256
+ const html = await this.http.post('/mypage.htm', {
257
+ opage: '1',
258
+ rpage: '1',
259
+ key: '',
260
+ projectSeq: '',
261
+ tozApplyType: '',
262
+ addedInfo: '',
263
+ startdate: args.startDate ?? '',
264
+ enddate: args.endDate ?? '',
265
+ meetingName: args.meetingName ?? '',
266
+ })
267
+ return parseMypageReservations(html)
268
+ }
269
+
270
+ async cancel(args: TozCancelArgs): Promise<void> {
271
+ await this.http.bootstrap()
272
+ await this.mypageLogin(this.requireName(args.name), this.requirePhone(args.phone))
273
+
274
+ const result = await this.http.postText('/ajaxCancelReservation.htm', {
275
+ reservation_id: String(args.reservationId),
276
+ })
277
+ if (result !== 'SUCCESS') {
278
+ if (result === 'FAILED') throw new Error('취소하지 못했습니다.')
279
+ throw new Error(result || '취소하지 못했습니다.')
280
+ }
281
+ }
282
+
283
+ private requireName(name: string | undefined): string {
284
+ const resolved = name ?? this.identity.name
285
+ if (!resolved) throw new Error('Toz name is required (pass via args or TozClient({ name }))')
286
+ return resolved
287
+ }
288
+
289
+ private requirePhone(phone: string | undefined): string {
290
+ const resolved = phone ?? this.identity.phone
291
+ if (!resolved) throw new Error('Toz phone is required (pass via args or TozClient({ phone }))')
292
+ return resolved
293
+ }
294
+
295
+ private async mypageLogin(name: string, phone: string): Promise<void> {
296
+ const parts = parsePhone(phone)
297
+ await this.http.post('/mypage_login_.htm', {
298
+ key: '',
299
+ projectSeq: '',
300
+ tozApplyType: '',
301
+ addedInfo: '',
302
+ email: '',
303
+ name,
304
+ ...parts,
305
+ })
306
+ }
307
+ }
308
+
309
+ function sleep(ms: number): Promise<void> {
310
+ return new Promise((resolve) => setTimeout(resolve, ms))
311
+ }
@@ -0,0 +1,91 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import { mkdtempSync } from 'node:fs'
3
+ import { readdir, stat } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+
7
+ import { TozPendingStore, type TozPendingReservation } from './toz-pending-store'
8
+
9
+ let configDir: string
10
+ const cleanups: (() => void)[] = []
11
+
12
+ function freshStore(): TozPendingStore {
13
+ configDir = mkdtempSync(join(tmpdir(), 'toz-pending-'))
14
+ cleanups.push(() => {
15
+ require('node:fs').rmSync(configDir, { recursive: true, force: true })
16
+ })
17
+ return new TozPendingStore(configDir)
18
+ }
19
+
20
+ afterEach(() => {
21
+ while (cleanups.length > 0) cleanups.pop()?.()
22
+ })
23
+
24
+ const sample: TozPendingReservation = {
25
+ reservationId: 'abc-123',
26
+ cookies: { JSESSIONID: 'XYZ' },
27
+ branchName: '토즈타워점',
28
+ branchTel: '02-3454-0116',
29
+ boothGroupName: '2인부스 A타입',
30
+ isLargeBooth: false,
31
+ date: '2026-04-21',
32
+ startTime: '14:00',
33
+ endTime: '16:00',
34
+ durationMinutes: 120,
35
+ userCount: 2,
36
+ boothId: 740,
37
+ meetingId: 2305094,
38
+ email: 'me@gmail.com',
39
+ name: '홍길동',
40
+ phone: '010-1234-5678',
41
+ createdAt: '2026-04-17T18:42:13.291Z',
42
+ expiresAt: '2026-04-17T18:47:13.291Z',
43
+ }
44
+
45
+ describe('TozPendingStore', () => {
46
+ test('returns null when no pending file exists', async () => {
47
+ const store = freshStore()
48
+ expect(await store.get()).toBeNull()
49
+ })
50
+
51
+ test('round-trips a reservation through set/get', async () => {
52
+ const store = freshStore()
53
+ await store.set(sample)
54
+ expect(await store.get()).toEqual(sample)
55
+ })
56
+
57
+ test('clear removes the file', async () => {
58
+ const store = freshStore()
59
+ await store.set(sample)
60
+ await store.clear()
61
+ expect(await store.get()).toBeNull()
62
+ })
63
+
64
+ test('clear is no-op when file does not exist', async () => {
65
+ const store = freshStore()
66
+ await store.clear()
67
+ expect(await store.get()).toBeNull()
68
+ })
69
+
70
+ test('overwrites an existing reservation', async () => {
71
+ const store = freshStore()
72
+ await store.set(sample)
73
+ const updated = { ...sample, reservationId: 'new-id' }
74
+ await store.set(updated)
75
+ expect(await store.get()).toEqual(updated)
76
+ })
77
+
78
+ test('writes file with 0o600 permissions at creation (no widened window)', async () => {
79
+ const store = freshStore()
80
+ await store.set(sample)
81
+ const fileStat = await stat(join(configDir, 'toz-pending.json'))
82
+ expect(fileStat.mode & 0o777).toBe(0o600)
83
+ })
84
+
85
+ test('leaves no temp file behind after set', async () => {
86
+ const store = freshStore()
87
+ await store.set(sample)
88
+ const entries = await readdir(configDir)
89
+ expect(entries).toEqual(['toz-pending.json'])
90
+ })
91
+ })
@@ -0,0 +1,62 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
3
+ import { homedir } from 'node:os'
4
+ import { dirname, join } from 'node:path'
5
+
6
+ export interface TozPendingReservation {
7
+ reservationId: string
8
+ cookies: Record<string, string>
9
+ branchName: string
10
+ branchTel: string
11
+ boothGroupName: string
12
+ isLargeBooth: boolean
13
+ date: string
14
+ startTime: string
15
+ endTime: string
16
+ durationMinutes: number
17
+ userCount: number
18
+ boothId: number
19
+ meetingId?: number
20
+ newMeetingName?: string
21
+ email: string
22
+ memo?: string
23
+ name: string
24
+ phone: string
25
+ createdAt: string
26
+ expiresAt: string
27
+ }
28
+
29
+ export class TozPendingStore {
30
+ private readonly path: string
31
+
32
+ constructor(configDir?: string) {
33
+ const dir = configDir ?? join(homedir(), '.config', 'opensoma')
34
+ this.path = join(dir, 'toz-pending.json')
35
+ }
36
+
37
+ async get(): Promise<TozPendingReservation | null> {
38
+ if (!existsSync(this.path)) return null
39
+
40
+ try {
41
+ const raw = await readFile(this.path, 'utf8')
42
+ return JSON.parse(raw) as TozPendingReservation
43
+ } catch {
44
+ return null
45
+ }
46
+ }
47
+
48
+ async set(reservation: TozPendingReservation): Promise<void> {
49
+ const dir = dirname(this.path)
50
+ await mkdir(dir, { recursive: true })
51
+ // Write to a temp file with 0o600 from creation, then rename atomically.
52
+ // Avoids the default 0o644 window between writeFile() and chmod() that
53
+ // would briefly expose the cookie + phone number to other users on the system.
54
+ const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}`
55
+ await writeFile(tmp, JSON.stringify(reservation, null, 2), { mode: 0o600 })
56
+ await rename(tmp, this.path)
57
+ }
58
+
59
+ async clear(): Promise<void> {
60
+ await rm(this.path, { force: true })
61
+ }
62
+ }
package/src/types.test.ts CHANGED
@@ -5,7 +5,6 @@ import {
5
5
  ApprovalListItemSchema,
6
6
  CredentialsSchema,
7
7
  DashboardSchema,
8
- EventListItemSchema,
9
8
  MemberInfoSchema,
10
9
  MentoringDetailSchema,
11
10
  MentoringListItemSchema,
@@ -16,6 +15,7 @@ import {
16
15
  ReportDetailSchema,
17
16
  ReportListItemSchema,
18
17
  RoomCardSchema,
18
+ ScheduleListItemSchema,
19
19
  TeamInfoSchema,
20
20
  } from './types'
21
21
 
@@ -79,6 +79,7 @@ describe('schemas', () => {
79
79
  organization: 'Indent',
80
80
  position: '',
81
81
  team: { name: '오픈소마', members: '전수열, 김개발', mentor: '전수열' },
82
+ teams: [],
82
83
  mentoringSessions: [{ title: 'AI 멘토링', url: '/mentoring/1', status: '접수중' }],
83
84
  roomReservations: [{ title: 'A1 회의', url: '/room/1', status: '예약완료' }],
84
85
  }
@@ -109,8 +110,23 @@ describe('schemas', () => {
109
110
  it('preserves valid TeamInfo values through parse', () => {
110
111
  const input = {
111
112
  teams: [
112
- { name: '오픈소마', memberCount: 3, joinStatus: '참여중' },
113
- { name: '김앤강', memberCount: 5, joinStatus: '참여하기' },
113
+ {
114
+ name: '오픈소마',
115
+ projectName: 'Previzion',
116
+ ownerId: 'owner-1',
117
+ teamId: 'team-a',
118
+ leader: '전수열',
119
+ members: [
120
+ { name: '전수열', userId: 'uid-1' },
121
+ { name: '강동우', userId: 'uid-2' },
122
+ ],
123
+ mentors: [{ name: '문승현', userId: 'uid-m1' }],
124
+ ictCategoryMajor: 'SW·SI',
125
+ ictCategoryMinor: '응용SW',
126
+ teamCompleted: true,
127
+ mentorCompleted: false,
128
+ joinStatus: '완료',
129
+ },
114
130
  ],
115
131
  currentTeams: 1,
116
132
  maxTeams: 100,
@@ -131,17 +147,14 @@ describe('schemas', () => {
131
147
  expect(MemberInfoSchema.parse(input)).toEqual(input)
132
148
  })
133
149
 
134
- it('preserves valid EventListItem values through parse', () => {
150
+ it('preserves valid ScheduleListItem values through parse', () => {
135
151
  const input = {
136
- id: 11,
137
- category: '행사',
138
- title: '데모데이',
139
- registrationPeriod: { start: '2026-04-01', end: '2026-04-05' },
140
- eventPeriod: { start: '2026-04-10', end: '2026-04-10' },
141
- status: '접수중',
142
- createdAt: '2026-03-30',
152
+ id: 1,
153
+ category: '교육',
154
+ title: '[교육] 디자인씽킹 교육',
155
+ period: { start: '2026-04-21', end: '2026-04-26' },
143
156
  }
144
- expect(EventListItemSchema.parse(input)).toEqual(input)
157
+ expect(ScheduleListItemSchema.parse(input)).toEqual(input)
145
158
  })
146
159
 
147
160
  it('preserves valid ApplicationHistoryItem values through parse', () => {
@@ -266,7 +279,7 @@ describe('schemas', () => {
266
279
  ).toThrow()
267
280
  expect(() => TeamInfoSchema.parse({ teams: [{ name: '김개발' }], currentTeams: 1, maxTeams: 100 })).toThrow()
268
281
  expect(() => MemberInfoSchema.parse({ email: 1, name: '전수열' })).toThrow()
269
- expect(() => EventListItemSchema.parse({ id: 1, title: '행사', status: '접수중' })).toThrow()
282
+ expect(() => ScheduleListItemSchema.parse({ id: 1, category: '교육', title: '강의' })).toThrow()
270
283
  expect(() => ApplicationHistoryItemSchema.parse({ id: 1, status: '신청완료' })).toThrow()
271
284
  expect(() => PaginationSchema.parse({ total: '23', currentPage: 2, totalPages: 3 })).toThrow()
272
285
  expect(() => CredentialsSchema.parse({ sessionCookie: 'cookie' })).toThrow()
package/src/types.ts CHANGED
@@ -18,9 +18,22 @@ const DashboardStatusItemSchema = z.object({
18
18
  venue: z.string().optional(),
19
19
  type: z.enum(['자유 멘토링', '멘토 특강']).optional(),
20
20
  })
21
+ const TeamMemberSchema = z.object({
22
+ name: z.string(),
23
+ userId: z.string(),
24
+ })
21
25
  const TeamListItemSchema = z.object({
22
26
  name: z.string(),
23
- memberCount: z.number(),
27
+ projectName: z.string(),
28
+ ownerId: z.string(),
29
+ teamId: z.string(),
30
+ leader: z.string(),
31
+ members: z.array(TeamMemberSchema),
32
+ mentors: z.array(TeamMemberSchema),
33
+ ictCategoryMajor: z.string(),
34
+ ictCategoryMinor: z.string(),
35
+ teamCompleted: z.boolean(),
36
+ mentorCompleted: z.boolean(),
24
37
  joinStatus: z.string(),
25
38
  })
26
39
 
@@ -54,6 +67,23 @@ export const MentoringDetailSchema = MentoringListItemSchema.extend({
54
67
  })
55
68
  export type MentoringDetail = z.infer<typeof MentoringDetailSchema>
56
69
 
70
+ export const MentoringEditFormSchema = z.object({
71
+ id: z.number(),
72
+ title: z.string(),
73
+ reportCd: z.string(),
74
+ receiptType: z.enum(['UNTIL_LECTURE', 'DIRECT']),
75
+ bgndeDate: z.string(),
76
+ bgndeTime: z.string(),
77
+ enddeDate: z.string(),
78
+ enddeTime: z.string(),
79
+ eventDt: z.string(),
80
+ eventStime: z.string(),
81
+ eventEtime: z.string(),
82
+ applyCnt: z.number(),
83
+ place: z.string(),
84
+ })
85
+ export type MentoringEditForm = z.infer<typeof MentoringEditFormSchema>
86
+
57
87
  export const RoomCardSchema = z.object({
58
88
  itemId: z.number(),
59
89
  name: z.string(),
@@ -64,11 +94,55 @@ export const RoomCardSchema = z.object({
64
94
  })
65
95
  export type RoomCard = z.infer<typeof RoomCardSchema>
66
96
 
97
+ export const RoomReservationStatusSchema = z.enum(['confirmed', 'cancelled', 'unknown'])
98
+ export type RoomReservationStatus = z.infer<typeof RoomReservationStatusSchema>
99
+
100
+ export const RoomReservationDetailSchema = z.object({
101
+ rentId: z.number(),
102
+ itemId: z.number(),
103
+ title: z.string(),
104
+ date: z.string(),
105
+ startTime: z.string(),
106
+ endTime: z.string(),
107
+ attendees: z.number(),
108
+ notes: z.string(),
109
+ status: RoomReservationStatusSchema,
110
+ statusCode: z.string(),
111
+ })
112
+ export type RoomReservationDetail = z.infer<typeof RoomReservationDetailSchema>
113
+
114
+ export const RoomReservationListItemSchema = z.object({
115
+ rentId: z.number(),
116
+ venue: z.string(),
117
+ title: z.string(),
118
+ date: z.string(),
119
+ startTime: z.string(),
120
+ endTime: z.string(),
121
+ author: z.string(),
122
+ status: RoomReservationStatusSchema,
123
+ statusLabel: z.string(),
124
+ registeredAt: z.string(),
125
+ })
126
+ export type RoomReservationListItem = z.infer<typeof RoomReservationListItemSchema>
127
+
128
+ export const RoomUpdateOptionsSchema = z.object({
129
+ title: z.string().optional(),
130
+ roomId: z.number().optional(),
131
+ date: z.string().optional(),
132
+ slots: z.array(z.string()).optional(),
133
+ attendees: z.number().optional(),
134
+ notes: z.string().optional(),
135
+ })
136
+ export type RoomUpdateOptions = z.infer<typeof RoomUpdateOptionsSchema>
137
+
67
138
  export const DashboardSchema = z.object({
68
139
  name: z.string(),
69
140
  role: z.string(),
70
141
  organization: z.string(),
71
142
  position: z.string(),
143
+ // Mirrors the single team the native dashboard HTML renders in `ul.dash-box`.
144
+ // The native page only ever surfaces one team here even when the user belongs
145
+ // to multiple. For the full set, see `teams` below.
72
146
  team: z
73
147
  .object({
74
148
  name: z.string(),
@@ -76,6 +150,9 @@ export const DashboardSchema = z.object({
76
150
  mentor: z.string(),
77
151
  })
78
152
  .optional(),
153
+ // Enriched from `team.list()` so consumers see every team the user belongs to,
154
+ // not just the one the native dashboard chose to render.
155
+ teams: z.array(TeamListItemSchema),
79
156
  mentoringSessions: z.array(DashboardStatusItemSchema),
80
157
  roomReservations: z.array(DashboardStatusItemSchema),
81
158
  })
@@ -94,6 +171,8 @@ export const NoticeDetailSchema = NoticeListItemSchema.extend({
94
171
  })
95
172
  export type NoticeDetail = z.infer<typeof NoticeDetailSchema>
96
173
 
174
+ export type TeamMember = z.infer<typeof TeamMemberSchema>
175
+ export type TeamListItem = z.infer<typeof TeamListItemSchema>
97
176
  export const TeamInfoSchema = z.object({
98
177
  teams: z.array(TeamListItemSchema),
99
178
  currentTeams: z.number(),
@@ -112,21 +191,19 @@ export const MemberInfoSchema = z.object({
112
191
  })
113
192
  export type MemberInfo = z.infer<typeof MemberInfoSchema>
114
193
 
115
- export const EventListItemSchema = z.object({
194
+ export const ScheduleListItemSchema = z.object({
116
195
  id: z.number(),
117
196
  category: z.string(),
118
197
  title: z.string(),
119
- registrationPeriod: DateRangeSchema,
120
- eventPeriod: DateRangeSchema,
121
- status: z.string(),
122
- createdAt: z.string(),
198
+ period: DateRangeSchema,
123
199
  })
124
- export type EventListItem = z.infer<typeof EventListItemSchema>
200
+ export type ScheduleListItem = z.infer<typeof ScheduleListItemSchema>
125
201
 
126
202
  export const ApplicationHistoryItemSchema = z.object({
127
203
  id: z.number(),
128
204
  category: z.string(),
129
205
  title: z.string(),
206
+ url: z.string().optional(),
130
207
  author: z.string(),
131
208
  sessionDate: z.string(),
132
209
  appliedAt: z.string(),
@@ -294,8 +371,11 @@ export const MentoringCreateOptionsSchema = z.object({
294
371
  endTime: z.string(),
295
372
  venue: z.string(),
296
373
  maxAttendees: z.number().optional(),
374
+ receiptType: z.enum(['UNTIL_LECTURE', 'DIRECT']).optional(),
297
375
  regStart: z.string().optional(),
376
+ regStartTime: z.string().optional(),
298
377
  regEnd: z.string().optional(),
378
+ regEndTime: z.string().optional(),
299
379
  content: z.string().optional(),
300
380
  })
301
381
  export type MentoringCreateOptions = z.infer<typeof MentoringCreateOptionsSchema>