opensoma 0.6.0 → 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.
- package/dist/package.json +1 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +6 -4
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +4 -0
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +6 -0
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands/index.d.ts +1 -0
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +1 -0
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/toz.d.ts +16 -0
- package/dist/src/commands/toz.d.ts.map +1 -0
- package/dist/src/commands/toz.js +488 -0
- package/dist/src/commands/toz.js.map +1 -0
- package/dist/src/credential-manager.d.ts +8 -2
- package/dist/src/credential-manager.d.ts.map +1 -1
- package/dist/src/credential-manager.js +25 -3
- package/dist/src/credential-manager.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/session-recovery.js +2 -0
- package/dist/src/session-recovery.js.map +1 -1
- package/dist/src/toz-client.d.ts +89 -0
- package/dist/src/toz-client.d.ts.map +1 -0
- package/dist/src/toz-client.js +204 -0
- package/dist/src/toz-client.js.map +1 -0
- package/dist/src/toz-pending-store.d.ts +30 -0
- package/dist/src/toz-pending-store.d.ts.map +1 -0
- package/dist/src/toz-pending-store.js +36 -0
- package/dist/src/toz-pending-store.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +6 -3
- package/src/client.ts +10 -0
- package/src/commands/helpers.test.ts +4 -0
- package/src/commands/index.ts +1 -0
- package/src/commands/toz.test.ts +51 -0
- package/src/commands/toz.ts +607 -0
- package/src/credential-manager.test.ts +54 -0
- package/src/credential-manager.ts +26 -3
- package/src/index.ts +13 -0
- package/src/session-recovery.ts +2 -0
- package/src/toz-client.test.ts +243 -0
- package/src/toz-client.ts +311 -0
- package/src/toz-pending-store.test.ts +91 -0
- package/src/toz-pending-store.ts +62 -0
|
@@ -78,8 +78,8 @@ export class CredentialManager {
|
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
80
|
* Wipe ephemeral session fields (sessionCookie, csrfToken, loggedInAt) while
|
|
81
|
-
* preserving long-term
|
|
82
|
-
* server-side session expired but we still want automatic recovery on the
|
|
81
|
+
* preserving long-term material (username, password, TOZ identity). Used when
|
|
82
|
+
* the server-side session expired but we still want automatic recovery on the
|
|
83
83
|
* next run via `recoverSession()`.
|
|
84
84
|
*
|
|
85
85
|
* No-op when no credentials file exists or no recovery material is stored.
|
|
@@ -90,7 +90,7 @@ export class CredentialManager {
|
|
|
90
90
|
return
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
if (!current.username && !current.password) {
|
|
93
|
+
if (!current.username && !current.password && !current.tozName && !current.tozPhone) {
|
|
94
94
|
await this.remove()
|
|
95
95
|
return
|
|
96
96
|
}
|
|
@@ -100,9 +100,32 @@ export class CredentialManager {
|
|
|
100
100
|
csrfToken: '',
|
|
101
101
|
username: current.username,
|
|
102
102
|
password: current.password,
|
|
103
|
+
tozName: current.tozName,
|
|
104
|
+
tozPhone: current.tozPhone,
|
|
103
105
|
})
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
async setTozIdentity(name: string, phone: string): Promise<void> {
|
|
109
|
+
const existing = await this.getCredentials()
|
|
110
|
+
if (!existing) {
|
|
111
|
+
throw new Error('SWMaestro credentials not found. Run: opensoma auth login first.')
|
|
112
|
+
}
|
|
113
|
+
await this.setCredentials({ ...existing, tozName: name, tozPhone: phone })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async clearTozIdentity(): Promise<void> {
|
|
117
|
+
const existing = await this.getCredentials()
|
|
118
|
+
if (!existing) return
|
|
119
|
+
const { tozName: _name, tozPhone: _phone, ...rest } = existing
|
|
120
|
+
await this.setCredentials(rest)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async getTozIdentity(): Promise<{ name: string; phone: string } | null> {
|
|
124
|
+
const creds = await this.getCredentials()
|
|
125
|
+
if (!creds?.tozName || !creds?.tozPhone) return null
|
|
126
|
+
return { name: creds.tozName, phone: creds.tozPhone }
|
|
127
|
+
}
|
|
128
|
+
|
|
106
129
|
private async hydrateCredentials(credentials: StoredCredentials): Promise<Credentials> {
|
|
107
130
|
if (!credentials.encryptedPassword) {
|
|
108
131
|
return credentials
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,19 @@ export type { UserIdentity } from './http'
|
|
|
14
14
|
export { CredentialManager } from './credential-manager'
|
|
15
15
|
export { TozHttp } from './toz-http'
|
|
16
16
|
export type { TozHttpOptions, TozHttpState } from './toz-http'
|
|
17
|
+
export { TozClient } from './toz-client'
|
|
18
|
+
export type {
|
|
19
|
+
TozAvailabilityQuery,
|
|
20
|
+
TozCancelArgs,
|
|
21
|
+
TozCheckQuery,
|
|
22
|
+
TozCheckResult,
|
|
23
|
+
TozClientOptions,
|
|
24
|
+
TozConfirmArgs,
|
|
25
|
+
TozReserveBoothArgs,
|
|
26
|
+
TozSearchArgs,
|
|
27
|
+
} from './toz-client'
|
|
28
|
+
export { TozPendingStore } from './toz-pending-store'
|
|
29
|
+
export type { TozPendingReservation } from './toz-pending-store'
|
|
17
30
|
export * from './toz-formatters'
|
|
18
31
|
export * from './types'
|
|
19
32
|
export * from './constants'
|
package/src/session-recovery.ts
CHANGED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { afterEach, describe, expect, mock, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { TOZ_BASE_URL } from './constants'
|
|
4
|
+
import { TozClient } from './toz-client'
|
|
5
|
+
|
|
6
|
+
const originalFetch = globalThis.fetch
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
globalThis.fetch = originalFetch
|
|
10
|
+
mock.restore()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
function jsonResponse(body: unknown, cookies: string[] = []): Response {
|
|
14
|
+
const response = new Response(JSON.stringify(body), {
|
|
15
|
+
status: 200,
|
|
16
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17
|
+
})
|
|
18
|
+
Object.defineProperty(response.headers, 'getSetCookie', {
|
|
19
|
+
value: () => cookies,
|
|
20
|
+
configurable: true,
|
|
21
|
+
})
|
|
22
|
+
return response
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function textResponse(body: string, cookies: string[] = []): Response {
|
|
26
|
+
const response = new Response(body, { status: 200, headers: { 'Content-Type': 'text/html' } })
|
|
27
|
+
Object.defineProperty(response.headers, 'getSetCookie', {
|
|
28
|
+
value: () => cookies,
|
|
29
|
+
configurable: true,
|
|
30
|
+
})
|
|
31
|
+
return response
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('TozClient.available', () => {
|
|
35
|
+
test('builds correct AJAX payload and parses booth response', async () => {
|
|
36
|
+
const calls: { url: string; body: string }[] = []
|
|
37
|
+
globalThis.fetch = mock(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
38
|
+
const url = String(input)
|
|
39
|
+
const body = String(init?.body ?? '')
|
|
40
|
+
calls.push({ url, body })
|
|
41
|
+
|
|
42
|
+
if (url.endsWith('/index.htm')) return textResponse('ok', ['JSESSIONID=ABC'])
|
|
43
|
+
if (url.endsWith('/ajaxGetEnableBoothes.htm')) {
|
|
44
|
+
return jsonResponse([
|
|
45
|
+
{
|
|
46
|
+
resultMsg: 'SUCCESS',
|
|
47
|
+
id: 740,
|
|
48
|
+
name: '304 _ A',
|
|
49
|
+
branchName: '토즈타워점',
|
|
50
|
+
branchTel: '02-3454-0116',
|
|
51
|
+
minUseUserCount: 1,
|
|
52
|
+
enableMaxUserCount: 2,
|
|
53
|
+
boothGroupName: '2인부스 A타입',
|
|
54
|
+
boothGroupUrl: null,
|
|
55
|
+
boothMemoForUser: '15,000원',
|
|
56
|
+
isLargeBooth: false,
|
|
57
|
+
},
|
|
58
|
+
])
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`Unexpected URL: ${url}`)
|
|
61
|
+
}) as typeof fetch
|
|
62
|
+
|
|
63
|
+
const client = new TozClient()
|
|
64
|
+
const booths = await client.available({
|
|
65
|
+
date: '2026-04-21',
|
|
66
|
+
startTime: '14:00',
|
|
67
|
+
durationMinutes: 120,
|
|
68
|
+
userCount: 2,
|
|
69
|
+
branchIds: [27, 145],
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
expect(booths).toHaveLength(1)
|
|
73
|
+
expect(booths[0]?.id).toBe(740)
|
|
74
|
+
expect(calls[1]?.url).toBe(`${TOZ_BASE_URL}/ajaxGetEnableBoothes.htm`)
|
|
75
|
+
expect(calls[1]?.body).toBe(
|
|
76
|
+
'basedate=2026-04-21&starttime=1400&durationTime=0200&userCount=2&branchIds=27%2C145%2C',
|
|
77
|
+
)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('throws on out-of-range duration', async () => {
|
|
81
|
+
const client = new TozClient()
|
|
82
|
+
await expect(
|
|
83
|
+
client.available({ date: '2026-04-21', startTime: '14:00', durationMinutes: 60, userCount: 2, branchIds: [27] }),
|
|
84
|
+
).rejects.toThrow(/2h and 3h/)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('TozClient.check', () => {
|
|
89
|
+
test('rejects empty time list', async () => {
|
|
90
|
+
const client = new TozClient()
|
|
91
|
+
await expect(
|
|
92
|
+
client.check({ date: '2026-04-21', startTimes: [], durationMinutes: 120, userCount: 2, branchIds: [27] }),
|
|
93
|
+
).rejects.toThrow(/at least one --time/)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('rejects more than 6 times', async () => {
|
|
97
|
+
const client = new TozClient()
|
|
98
|
+
await expect(
|
|
99
|
+
client.check({
|
|
100
|
+
date: '2026-04-21',
|
|
101
|
+
startTimes: ['10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00'],
|
|
102
|
+
durationMinutes: 120,
|
|
103
|
+
userCount: 2,
|
|
104
|
+
branchIds: [27],
|
|
105
|
+
}),
|
|
106
|
+
).rejects.toThrow(/Too many times/)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('returns one result per time, captures errors per slot', async () => {
|
|
110
|
+
let call = 0
|
|
111
|
+
globalThis.fetch = mock(async (input: RequestInfo | URL) => {
|
|
112
|
+
const url = String(input)
|
|
113
|
+
if (url.endsWith('/index.htm')) return textResponse('ok', ['JSESSIONID=A'])
|
|
114
|
+
call += 1
|
|
115
|
+
if (call === 1) return jsonResponse([])
|
|
116
|
+
if (call === 2) return jsonResponse([{ resultMsg: '예약 가능한 부스가 없습니다.' }])
|
|
117
|
+
throw new Error(`Unexpected call ${call}`)
|
|
118
|
+
}) as typeof fetch
|
|
119
|
+
|
|
120
|
+
const client = new TozClient()
|
|
121
|
+
const results = await client.check({
|
|
122
|
+
date: '2026-04-21',
|
|
123
|
+
startTimes: ['10:00', '14:00'],
|
|
124
|
+
durationMinutes: 120,
|
|
125
|
+
userCount: 2,
|
|
126
|
+
branchIds: [27],
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
expect(results).toHaveLength(2)
|
|
130
|
+
expect(results[0]).toEqual({ startTime: '10:00', booths: [] })
|
|
131
|
+
expect(results[1]?.startTime).toBe('14:00')
|
|
132
|
+
expect(results[1]?.error).toContain('예약 가능한 부스가 없습니다.')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('does not sleep after the last slot (single-time check is near-instant)', async () => {
|
|
136
|
+
globalThis.fetch = mock(async (input: RequestInfo | URL) => {
|
|
137
|
+
const url = String(input)
|
|
138
|
+
if (url.endsWith('/index.htm')) return textResponse('ok', ['JSESSIONID=A'])
|
|
139
|
+
return jsonResponse([])
|
|
140
|
+
}) as typeof fetch
|
|
141
|
+
|
|
142
|
+
const client = new TozClient()
|
|
143
|
+
const start = Date.now()
|
|
144
|
+
await client.check({
|
|
145
|
+
date: '2026-04-21',
|
|
146
|
+
startTimes: ['10:00'],
|
|
147
|
+
durationMinutes: 120,
|
|
148
|
+
userCount: 2,
|
|
149
|
+
branchIds: [27],
|
|
150
|
+
})
|
|
151
|
+
expect(Date.now() - start).toBeLessThan(400)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('TozClient identity fallback', () => {
|
|
156
|
+
test('uses constructor-provided name/phone when args omit them', async () => {
|
|
157
|
+
const calls: { url: string; body: string }[] = []
|
|
158
|
+
globalThis.fetch = mock(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
159
|
+
const url = String(input)
|
|
160
|
+
calls.push({ url, body: String(init?.body ?? '') })
|
|
161
|
+
if (url.endsWith('/index.htm')) return textResponse('ok', ['JSESSIONID=A'])
|
|
162
|
+
if (url.endsWith('/mypage_login_.htm')) return textResponse('ok')
|
|
163
|
+
if (url.endsWith('/mypage.htm')) return textResponse('<html></html>')
|
|
164
|
+
throw new Error(`Unexpected ${url}`)
|
|
165
|
+
}) as typeof fetch
|
|
166
|
+
|
|
167
|
+
const client = new TozClient({ name: '홍길동', phone: '010-1234-5678' })
|
|
168
|
+
await client.myReservations()
|
|
169
|
+
|
|
170
|
+
const loginCall = calls.find((c) => c.url.endsWith('/mypage_login_.htm'))
|
|
171
|
+
expect(loginCall?.body).toContain('name=%ED%99%8D%EA%B8%B8%EB%8F%99')
|
|
172
|
+
expect(loginCall?.body).toContain('phone1=010')
|
|
173
|
+
expect(loginCall?.body).toContain('phone2=1234')
|
|
174
|
+
expect(loginCall?.body).toContain('phone3=5678')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('throws a clear error when neither args nor identity are provided', async () => {
|
|
178
|
+
const client = new TozClient()
|
|
179
|
+
await expect(client.myReservations()).rejects.toThrow(/name is required/)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('TozClient.reserveBooth', () => {
|
|
184
|
+
test('rejects large booth', async () => {
|
|
185
|
+
globalThis.fetch = mock(async (input: RequestInfo | URL) => {
|
|
186
|
+
const url = String(input)
|
|
187
|
+
if (url.endsWith('/index.htm')) return textResponse('ok', ['JSESSIONID=A'])
|
|
188
|
+
if (url.endsWith('/ajaxReservationBooth.htm')) {
|
|
189
|
+
return jsonResponse({
|
|
190
|
+
result: 'SUCCESS',
|
|
191
|
+
resultMsg: 'res-1',
|
|
192
|
+
branchName: 'X',
|
|
193
|
+
branchTel: 'tel',
|
|
194
|
+
boothGroupName: '대형부스',
|
|
195
|
+
boothIsLarge: true,
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
throw new Error(url)
|
|
199
|
+
}) as typeof fetch
|
|
200
|
+
|
|
201
|
+
const client = new TozClient()
|
|
202
|
+
await expect(
|
|
203
|
+
client.reserveBooth({ date: '2026-04-21', startTime: '14:00', durationMinutes: 120, userCount: 8, boothId: 999 }),
|
|
204
|
+
).rejects.toThrow(/대형부스/)
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe('TozClient.confirm', () => {
|
|
209
|
+
test('throws on non-success message', async () => {
|
|
210
|
+
globalThis.fetch = mock(async () => jsonResponse({ resultMsg: '인증번호가 올바르지 않습니다.' })) as typeof fetch
|
|
211
|
+
|
|
212
|
+
const client = new TozClient({ sessionCookie: 'A' })
|
|
213
|
+
await expect(
|
|
214
|
+
client.confirm({
|
|
215
|
+
reservationId: 'r1',
|
|
216
|
+
date: '2026-04-21',
|
|
217
|
+
startTime: '14:00',
|
|
218
|
+
durationMinutes: 120,
|
|
219
|
+
name: '홍길동',
|
|
220
|
+
phone: '010-1234-5678',
|
|
221
|
+
email: 'me@gmail.com',
|
|
222
|
+
pinNum: '000000',
|
|
223
|
+
meetingId: 1234,
|
|
224
|
+
}),
|
|
225
|
+
).rejects.toThrow('인증번호가 올바르지 않습니다.')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('requires meetingId or newMeetingName', async () => {
|
|
229
|
+
const client = new TozClient({ sessionCookie: 'A' })
|
|
230
|
+
await expect(
|
|
231
|
+
client.confirm({
|
|
232
|
+
reservationId: 'r1',
|
|
233
|
+
date: '2026-04-21',
|
|
234
|
+
startTime: '14:00',
|
|
235
|
+
durationMinutes: 120,
|
|
236
|
+
name: '홍길동',
|
|
237
|
+
phone: '010-1234-5678',
|
|
238
|
+
email: 'me@gmail.com',
|
|
239
|
+
pinNum: '123456',
|
|
240
|
+
}),
|
|
241
|
+
).rejects.toThrow(/meetingId/)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
@@ -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
|
+
}
|