opensoma 0.3.0 → 0.5.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/client.d.ts +3 -3
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +37 -5
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands/auth.d.ts +1 -1
- package/dist/src/commands/auth.d.ts.map +1 -1
- package/dist/src/commands/auth.js +94 -52
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/mentoring.d.ts.map +1 -1
- package/dist/src/commands/mentoring.js +26 -20
- package/dist/src/commands/mentoring.js.map +1 -1
- package/dist/src/commands/room.d.ts.map +1 -1
- package/dist/src/commands/room.js +25 -2
- package/dist/src/commands/room.js.map +1 -1
- package/dist/src/constants.d.ts +52 -9
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +65 -9
- package/dist/src/constants.js.map +1 -1
- package/dist/src/formatters.d.ts.map +1 -1
- package/dist/src/formatters.js +79 -39
- package/dist/src/formatters.js.map +1 -1
- package/dist/src/http.d.ts +3 -0
- package/dist/src/http.d.ts.map +1 -1
- package/dist/src/http.js +42 -1
- package/dist/src/http.js.map +1 -1
- package/dist/src/index.d.ts +3 -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/shared/utils/html.d.ts +3 -0
- package/dist/src/shared/utils/html.d.ts.map +1 -0
- package/dist/src/shared/utils/html.js +12 -0
- package/dist/src/shared/utils/html.js.map +1 -0
- package/dist/src/shared/utils/swmaestro.d.ts +2 -0
- package/dist/src/shared/utils/swmaestro.d.ts.map +1 -1
- package/dist/src/shared/utils/swmaestro.js +28 -5
- package/dist/src/shared/utils/swmaestro.js.map +1 -1
- package/dist/src/shared/utils/toz.d.ts +23 -0
- package/dist/src/shared/utils/toz.d.ts.map +1 -0
- package/dist/src/shared/utils/toz.js +72 -0
- package/dist/src/shared/utils/toz.js.map +1 -0
- package/dist/src/token-extractor.d.ts +9 -1
- package/dist/src/token-extractor.d.ts.map +1 -1
- package/dist/src/token-extractor.js +54 -10
- package/dist/src/token-extractor.js.map +1 -1
- package/dist/src/toz-formatters.d.ts +9 -0
- package/dist/src/toz-formatters.d.ts.map +1 -0
- package/dist/src/toz-formatters.js +151 -0
- package/dist/src/toz-formatters.js.map +1 -0
- package/dist/src/toz-http.d.ts +27 -0
- package/dist/src/toz-http.d.ts.map +1 -0
- package/dist/src/toz-http.js +154 -0
- package/dist/src/toz-http.js.map +1 -0
- package/dist/src/types.d.ts +88 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +65 -1
- package/dist/src/types.js.map +1 -1
- package/package.json +1 -1
- package/src/__fixtures__/toz/toz_all_branches.json +211 -0
- package/src/__fixtures__/toz/toz_booking.html +2190 -0
- package/src/__fixtures__/toz/toz_boothes.json +59 -0
- package/src/__fixtures__/toz/toz_duration.json +25 -0
- package/src/__fixtures__/toz/toz_mypage_response.html +388 -0
- package/src/__fixtures__/toz/toz_page.html +211 -0
- package/src/client.test.ts +63 -16
- package/src/client.ts +43 -6
- package/src/commands/auth.ts +107 -50
- package/src/commands/mentoring.ts +34 -26
- package/src/commands/room.ts +31 -3
- package/src/constants.ts +74 -9
- package/src/formatters.test.ts +6 -2
- package/src/formatters.ts +92 -45
- package/src/http.test.ts +215 -0
- package/src/http.ts +45 -1
- package/src/index.ts +3 -0
- package/src/shared/utils/html.ts +12 -0
- package/src/shared/utils/swmaestro.test.ts +44 -0
- package/src/shared/utils/swmaestro.ts +30 -5
- package/src/shared/utils/toz.test.ts +133 -0
- package/src/shared/utils/toz.ts +100 -0
- package/src/token-extractor.test.ts +30 -5
- package/src/token-extractor.ts +65 -13
- package/src/toz-formatters.test.ts +197 -0
- package/src/toz-formatters.ts +211 -0
- package/src/toz-http.test.ts +157 -0
- package/src/toz-http.ts +188 -0
- package/src/types.test.ts +4 -1
- package/src/types.ts +81 -1
package/src/commands/auth.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createInterface, type Interface as ReadlineInterface } from 'node:readline'
|
|
2
|
+
|
|
1
3
|
import { Command } from 'commander'
|
|
2
4
|
|
|
3
5
|
import { CredentialManager } from '../credential-manager'
|
|
@@ -7,63 +9,99 @@ import { handleError } from '../shared/utils/error-handler'
|
|
|
7
9
|
import { formatOutput } from '../shared/utils/output'
|
|
8
10
|
import type { ExtractedSessionCandidate } from '../token-extractor'
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
function ask(rl: ReadlineInterface, message: string): Promise<string> {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question(message, (answer) => {
|
|
15
|
+
resolve(answer.trim())
|
|
16
|
+
})
|
|
17
|
+
})
|
|
14
18
|
}
|
|
15
19
|
|
|
16
|
-
async function
|
|
20
|
+
async function promptPasswordTTY(message: string): Promise<string> {
|
|
17
21
|
process.stdout.write(message)
|
|
18
22
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (originalStdin?.setRawMode) {
|
|
24
|
-
originalStdin.setRawMode(true)
|
|
23
|
+
const stdin = process.stdin as typeof process.stdin & { setRawMode?: (mode: boolean) => void }
|
|
24
|
+
if (stdin.setRawMode) {
|
|
25
|
+
stdin.setRawMode(true)
|
|
25
26
|
}
|
|
27
|
+
stdin.resume()
|
|
26
28
|
|
|
27
29
|
let password = ''
|
|
28
|
-
const decoder = new TextDecoder()
|
|
29
30
|
|
|
30
31
|
try {
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
32
|
+
return await new Promise<string>((resolve) => {
|
|
33
|
+
const onData = (chunk: Buffer | string): void => {
|
|
34
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8')
|
|
35
|
+
for (const char of text) {
|
|
36
|
+
const code = char.charCodeAt(0)
|
|
37
|
+
if (code === 13 || code === 10) {
|
|
38
|
+
// Enter key
|
|
39
|
+
stdin.removeListener('data', onData)
|
|
40
|
+
process.stdout.write('\n')
|
|
41
|
+
resolve(password)
|
|
42
|
+
return
|
|
43
|
+
} else if (code === 3) {
|
|
44
|
+
// Ctrl+C
|
|
45
|
+
stdin.removeListener('data', onData)
|
|
46
|
+
process.exit(1)
|
|
47
|
+
} else if (code === 127 || code === 8) {
|
|
48
|
+
// Backspace / Delete
|
|
49
|
+
if (password.length > 0) {
|
|
50
|
+
password = password.slice(0, -1)
|
|
51
|
+
process.stdout.write('\b \b')
|
|
52
|
+
}
|
|
53
|
+
} else if (code >= 32 && code <= 126) {
|
|
54
|
+
// Printable characters
|
|
55
|
+
password += char
|
|
56
|
+
process.stdout.write('*')
|
|
47
57
|
}
|
|
48
|
-
} else if (code >= 32 && code <= 126) {
|
|
49
|
-
// Printable characters
|
|
50
|
-
password += char
|
|
51
|
-
process.stdout.write('*')
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
|
-
|
|
60
|
+
|
|
61
|
+
stdin.on('data', onData)
|
|
62
|
+
})
|
|
55
63
|
} finally {
|
|
56
|
-
if (
|
|
57
|
-
|
|
64
|
+
if (stdin.setRawMode) {
|
|
65
|
+
stdin.setRawMode(false)
|
|
58
66
|
}
|
|
67
|
+
stdin.pause()
|
|
59
68
|
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function promptCredentials(
|
|
72
|
+
needUsername: boolean,
|
|
73
|
+
needPassword: boolean,
|
|
74
|
+
): Promise<{ username?: string; password?: string }> {
|
|
75
|
+
const result: { username?: string; password?: string } = {}
|
|
60
76
|
|
|
61
|
-
|
|
77
|
+
if (needUsername) {
|
|
78
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false })
|
|
79
|
+
try {
|
|
80
|
+
result.username = await ask(rl, 'Username: ')
|
|
81
|
+
} finally {
|
|
82
|
+
rl.close()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (needPassword) {
|
|
87
|
+
if (process.stdin.isTTY) {
|
|
88
|
+
result.password = await promptPasswordTTY('Password: ')
|
|
89
|
+
} else {
|
|
90
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false })
|
|
91
|
+
try {
|
|
92
|
+
result.password = await ask(rl, 'Password: ')
|
|
93
|
+
} finally {
|
|
94
|
+
rl.close()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result
|
|
62
100
|
}
|
|
63
101
|
|
|
64
102
|
type LoginOptions = { username?: string; password?: string; pretty?: boolean }
|
|
65
103
|
type StatusOptions = { pretty?: boolean }
|
|
66
|
-
type ExtractOptions = { pretty?: boolean }
|
|
104
|
+
type ExtractOptions = { debug?: boolean; pretty?: boolean }
|
|
67
105
|
type ExtractedSessionValidator = Pick<SomaHttp, 'checkLogin' | 'extractCsrfToken'>
|
|
68
106
|
type CredentialStore = Pick<CredentialManager, 'getCredentials' | 'remove' | 'setCredentials'>
|
|
69
107
|
type StatusValidator = Pick<SomaHttp, 'checkLogin'>
|
|
@@ -87,21 +125,34 @@ export async function resolveExtractedCredentials(
|
|
|
87
125
|
candidates: ExtractedSessionCandidate[],
|
|
88
126
|
createValidator: (sessionCookie: string) => ExtractedSessionValidator = (sessionCookie) =>
|
|
89
127
|
new SomaHttp({ sessionCookie }),
|
|
128
|
+
debug?: (message: string) => void,
|
|
90
129
|
): Promise<{ csrfToken: string; sessionCookie: string } | null> {
|
|
130
|
+
debug?.(`Validating ${candidates.length} candidate(s) against server...`)
|
|
131
|
+
|
|
91
132
|
for (const candidate of candidates) {
|
|
92
133
|
const http = createValidator(candidate.sessionCookie)
|
|
134
|
+
debug?.(` ${candidate.browser} / ${candidate.profile}: checkLogin...`)
|
|
93
135
|
|
|
94
136
|
try {
|
|
95
137
|
const valid = Boolean(await http.checkLogin())
|
|
96
138
|
if (!valid) {
|
|
139
|
+
debug?.(` ${candidate.browser} / ${candidate.profile}: session invalid`)
|
|
97
140
|
continue
|
|
98
141
|
}
|
|
99
142
|
|
|
143
|
+
debug?.(` ${candidate.browser} / ${candidate.profile}: valid! Extracting CSRF token...`)
|
|
144
|
+
const csrfToken = await http.extractCsrfToken()
|
|
145
|
+
debug?.(` CSRF token obtained (${csrfToken.length} chars)`)
|
|
146
|
+
|
|
100
147
|
return {
|
|
101
148
|
sessionCookie: candidate.sessionCookie,
|
|
102
|
-
csrfToken
|
|
149
|
+
csrfToken,
|
|
103
150
|
}
|
|
104
|
-
} catch {
|
|
151
|
+
} catch (error) {
|
|
152
|
+
debug?.(
|
|
153
|
+
` ${candidate.browser} / ${candidate.profile}: error: ${error instanceof Error ? error.message : String(error)}`,
|
|
154
|
+
)
|
|
155
|
+
}
|
|
105
156
|
}
|
|
106
157
|
|
|
107
158
|
return null
|
|
@@ -112,12 +163,9 @@ async function loginAction(options: LoginOptions): Promise<void> {
|
|
|
112
163
|
let username = options.username ?? process.env.OPENSOMA_USERNAME
|
|
113
164
|
let password = options.password ?? process.env.OPENSOMA_PASSWORD
|
|
114
165
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (!password) {
|
|
119
|
-
password = await promptPassword('Password: ')
|
|
120
|
-
}
|
|
166
|
+
const prompted = await promptCredentials(!username, !password)
|
|
167
|
+
username ??= prompted.username
|
|
168
|
+
password ??= prompted.password
|
|
121
169
|
|
|
122
170
|
if (!username || !password) {
|
|
123
171
|
throw new Error('Username and password are required')
|
|
@@ -264,22 +312,30 @@ async function statusAction(options: StatusOptions): Promise<void> {
|
|
|
264
312
|
}
|
|
265
313
|
|
|
266
314
|
async function extractAction(options: ExtractOptions): Promise<void> {
|
|
315
|
+
const log = options.debug ? (message: string) => process.stderr.write(`[extract] ${message}\n`) : undefined
|
|
316
|
+
|
|
267
317
|
try {
|
|
268
318
|
const { TokenExtractor } = (await import('../token-extractor')) as {
|
|
269
|
-
TokenExtractor: new (
|
|
319
|
+
TokenExtractor: new (options?: { debug?: boolean }) => {
|
|
320
|
+
extractCandidates: () => Promise<ExtractedSessionCandidate[]>
|
|
321
|
+
}
|
|
270
322
|
}
|
|
271
|
-
const extractor = new TokenExtractor()
|
|
323
|
+
const extractor = new TokenExtractor({ debug: options.debug })
|
|
272
324
|
const candidates = await extractor.extractCandidates()
|
|
273
325
|
if (candidates.length === 0) {
|
|
274
326
|
throw new Error(
|
|
275
|
-
'No SWMaestro session found in any browser. Login to swmaestro.ai in a supported Chromium browser (Chrome, Edge, Brave, Arc, Vivaldi) first.',
|
|
327
|
+
'No SWMaestro session found in any browser. Login to swmaestro.ai or opensoma.dev in a supported Chromium browser (Chrome, Edge, Brave, Arc, Vivaldi) first.',
|
|
276
328
|
)
|
|
277
329
|
}
|
|
278
330
|
|
|
279
|
-
|
|
331
|
+
log?.(
|
|
332
|
+
`Extracted ${candidates.length} candidate(s): ${candidates.map((c) => `${c.browser}/${c.profile}`).join(', ')}`,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
const credentials = await resolveExtractedCredentials(candidates, undefined, log)
|
|
280
336
|
if (!credentials) {
|
|
281
337
|
throw new Error(
|
|
282
|
-
'Found SWMaestro session cookies in supported browsers, but none are valid. Refresh your swmaestro.ai login in a supported Chromium browser and try again.',
|
|
338
|
+
'Found SWMaestro session cookies in supported browsers, but none are valid. Refresh your swmaestro.ai or opensoma.dev login in a supported Chromium browser and try again.',
|
|
283
339
|
)
|
|
284
340
|
}
|
|
285
341
|
|
|
@@ -319,6 +375,7 @@ export const authCommand = new Command('auth')
|
|
|
319
375
|
.addCommand(
|
|
320
376
|
new Command('extract')
|
|
321
377
|
.description('Extract browser credentials')
|
|
378
|
+
.option('--debug', 'Show debug output')
|
|
322
379
|
.option('--pretty', 'Pretty print JSON output')
|
|
323
380
|
.action(extractAction),
|
|
324
381
|
)
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
buildDeleteMentoringPayload,
|
|
12
12
|
buildMentoringPayload,
|
|
13
13
|
buildUpdateMentoringPayload,
|
|
14
|
+
toMentoringType,
|
|
14
15
|
} from '../shared/utils/swmaestro'
|
|
15
16
|
import { getHttpOrExit } from './helpers'
|
|
16
17
|
|
|
@@ -36,12 +37,12 @@ type CreateOptions = {
|
|
|
36
37
|
pretty?: boolean
|
|
37
38
|
}
|
|
38
39
|
type UpdateOptions = {
|
|
39
|
-
title
|
|
40
|
-
type
|
|
41
|
-
date
|
|
42
|
-
start
|
|
43
|
-
end
|
|
44
|
-
venue
|
|
40
|
+
title?: string
|
|
41
|
+
type?: 'public' | 'lecture'
|
|
42
|
+
date?: string
|
|
43
|
+
start?: string
|
|
44
|
+
end?: string
|
|
45
|
+
venue?: string
|
|
45
46
|
maxAttendees?: string
|
|
46
47
|
regStart?: string
|
|
47
48
|
regEnd?: string
|
|
@@ -96,7 +97,7 @@ async function getAction(id: string, options: GetOptions): Promise<void> {
|
|
|
96
97
|
async function createAction(options: CreateOptions): Promise<void> {
|
|
97
98
|
try {
|
|
98
99
|
const http = await getHttpOrExit()
|
|
99
|
-
await http.
|
|
100
|
+
await http.postForm(
|
|
100
101
|
'/mypage/mentoLec/insert.do',
|
|
101
102
|
buildMentoringPayload({
|
|
102
103
|
title: options.title,
|
|
@@ -120,19 +121,26 @@ async function createAction(options: CreateOptions): Promise<void> {
|
|
|
120
121
|
async function updateAction(id: string, options: UpdateOptions): Promise<void> {
|
|
121
122
|
try {
|
|
122
123
|
const http = await getHttpOrExit()
|
|
123
|
-
|
|
124
|
+
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)
|
|
130
|
+
|
|
131
|
+
await http.postForm(
|
|
124
132
|
'/mypage/mentoLec/update.do',
|
|
125
|
-
buildUpdateMentoringPayload(
|
|
126
|
-
title: options.title,
|
|
127
|
-
type: options.type,
|
|
128
|
-
date: options.date,
|
|
129
|
-
startTime: options.start,
|
|
130
|
-
endTime: options.end,
|
|
131
|
-
venue: options.venue,
|
|
132
|
-
maxAttendees: options.maxAttendees ? Number.parseInt(options.maxAttendees, 10) :
|
|
133
|
-
regStart: options.regStart,
|
|
134
|
-
regEnd: options.regEnd,
|
|
135
|
-
content: options.content,
|
|
133
|
+
buildUpdateMentoringPayload(numId, {
|
|
134
|
+
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,
|
|
136
144
|
}),
|
|
137
145
|
)
|
|
138
146
|
console.log(formatOutput({ ok: true }, options.pretty))
|
|
@@ -235,14 +243,14 @@ export const mentoringCommand = new Command('mentoring')
|
|
|
235
243
|
)
|
|
236
244
|
.addCommand(
|
|
237
245
|
new Command('update')
|
|
238
|
-
.description('Update a mentoring session')
|
|
246
|
+
.description('Update a mentoring session (partial update - only specified fields are changed)')
|
|
239
247
|
.argument('<id>')
|
|
240
|
-
.
|
|
241
|
-
.
|
|
242
|
-
.
|
|
243
|
-
.
|
|
244
|
-
.
|
|
245
|
-
.
|
|
248
|
+
.option('--title <title>', 'Title')
|
|
249
|
+
.option('--type <type>', 'Mentoring type (public|lecture)')
|
|
250
|
+
.option('--date <date>', 'Session date')
|
|
251
|
+
.option('--start <time>', 'Start time')
|
|
252
|
+
.option('--end <time>', 'End time')
|
|
253
|
+
.option('--venue <venue>', 'Venue')
|
|
246
254
|
.option('--max-attendees <count>', 'Maximum attendees')
|
|
247
255
|
.option('--reg-start <date>', 'Registration start date')
|
|
248
256
|
.option('--reg-end <date>', 'Registration end date')
|
package/src/commands/room.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { formatOutput } from '../shared/utils/output'
|
|
|
6
6
|
import { buildRoomReservationPayload, resolveRoomId } from '../shared/utils/swmaestro'
|
|
7
7
|
import { getHttpOrExit } from './helpers'
|
|
8
8
|
|
|
9
|
-
type ListOptions = { date?: string; room?: string; pretty?: boolean }
|
|
9
|
+
type ListOptions = { date?: string; room?: string; reservations?: boolean; pretty?: boolean }
|
|
10
10
|
type AvailableOptions = { date: string; pretty?: boolean }
|
|
11
11
|
type ReserveOptions = {
|
|
12
12
|
room: string
|
|
@@ -21,12 +21,39 @@ type ReserveOptions = {
|
|
|
21
21
|
async function listAction(options: ListOptions): Promise<void> {
|
|
22
22
|
try {
|
|
23
23
|
const http = await getHttpOrExit()
|
|
24
|
+
const date = options.date ?? new Date().toISOString().slice(0, 10)
|
|
24
25
|
const html = await http.post('/mypage/officeMng/list.do', {
|
|
25
26
|
menuNo: '200058',
|
|
26
|
-
sdate:
|
|
27
|
+
sdate: date,
|
|
27
28
|
searchItemId: options.room ? String(resolveRoomId(options.room)) : '',
|
|
28
29
|
})
|
|
29
|
-
|
|
30
|
+
const rooms = formatters.parseRoomList(html)
|
|
31
|
+
|
|
32
|
+
if (!options.reservations) {
|
|
33
|
+
console.log(formatOutput(rooms, options.pretty))
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const enrichedRooms = await Promise.all(
|
|
38
|
+
rooms.map(async (room) => {
|
|
39
|
+
try {
|
|
40
|
+
const detailHtml = await http.post('/mypage/officeMng/rentTime.do', {
|
|
41
|
+
viewType: 'CONTBODY',
|
|
42
|
+
itemId: String(room.itemId),
|
|
43
|
+
rentDt: date,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
...room,
|
|
48
|
+
timeSlots: formatters.parseRoomSlots(detailHtml),
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
return room
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
console.log(formatOutput(enrichedRooms, options.pretty))
|
|
30
57
|
} catch (error) {
|
|
31
58
|
handleError(error)
|
|
32
59
|
}
|
|
@@ -76,6 +103,7 @@ export const roomCommand = new Command('room')
|
|
|
76
103
|
.description('List rooms')
|
|
77
104
|
.option('--date <date>', 'Reservation date')
|
|
78
105
|
.option('--room <room>', 'Room filter')
|
|
106
|
+
.option('--reservations', 'Include reservation info in time slots')
|
|
79
107
|
.option('--pretty', 'Pretty print JSON output')
|
|
80
108
|
.action(listAction),
|
|
81
109
|
)
|
package/src/constants.ts
CHANGED
|
@@ -26,15 +26,15 @@ export const ROOM_IDS: Record<string, number> = {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export const VENUES = {
|
|
29
|
-
TOZ_GWANGHWAMUN: '
|
|
30
|
-
TOZ_YANGJAE: '
|
|
31
|
-
TOZ_GANGNAM_CONFERENCE_CENTER: '
|
|
32
|
-
TOZ_KONKUK: '
|
|
33
|
-
TOZ_GANGNAM_TOWER: '
|
|
34
|
-
TOZ_SEOLLEUNG: '
|
|
35
|
-
TOZ_YEOKSAM: '
|
|
36
|
-
TOZ_HONGDAE: '
|
|
37
|
-
TOZ_SINCHON_BUSINESS_CENTER: '
|
|
29
|
+
TOZ_GWANGHWAMUN: '토즈-광화문점',
|
|
30
|
+
TOZ_YANGJAE: '토즈-양재점',
|
|
31
|
+
TOZ_GANGNAM_CONFERENCE_CENTER: '토즈-강남컨퍼런스센터점',
|
|
32
|
+
TOZ_KONKUK: '토즈-건대점',
|
|
33
|
+
TOZ_GANGNAM_TOWER: '토즈-강남역토즈타워점',
|
|
34
|
+
TOZ_SEOLLEUNG: '토즈-선릉점',
|
|
35
|
+
TOZ_YEOKSAM: '토즈-역삼점',
|
|
36
|
+
TOZ_HONGDAE: '토즈-홍대점',
|
|
37
|
+
TOZ_SINCHON_BUSINESS_CENTER: '토즈-신촌비즈니스센터점',
|
|
38
38
|
ONLINE_WEBEX: '온라인(Webex)',
|
|
39
39
|
SPACE_A1: '스페이스 A1',
|
|
40
40
|
SPACE_A2: '스페이스 A2',
|
|
@@ -47,8 +47,23 @@ export const VENUES = {
|
|
|
47
47
|
SPACE_M1: '스페이스 M1',
|
|
48
48
|
SPACE_M2: '스페이스 M2',
|
|
49
49
|
SPACE_S: '스페이스 S',
|
|
50
|
+
EXPERT_LOUNGE: '(엑스퍼트) 연수센터_라운지',
|
|
51
|
+
EXPERT_CAFE: '(엑스퍼트) 외부_카페',
|
|
50
52
|
} as const
|
|
51
53
|
|
|
54
|
+
export const VENUE_ALIASES: Record<string, string> = {
|
|
55
|
+
광화문점: '토즈-광화문점',
|
|
56
|
+
양재점: '토즈-양재점',
|
|
57
|
+
강남컨퍼런스센터점: '토즈-강남컨퍼런스센터점',
|
|
58
|
+
건대점: '토즈-건대점',
|
|
59
|
+
강남역토즈타워점: '토즈-강남역토즈타워점',
|
|
60
|
+
선릉점: '토즈-선릉점',
|
|
61
|
+
역삼점: '토즈-역삼점',
|
|
62
|
+
홍대점: '토즈-홍대점',
|
|
63
|
+
신촌비즈니스센터점: '연수센터-7',
|
|
64
|
+
'토즈-신촌비즈니스센터점': '연수센터-7',
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
export const REPORT_CD = {
|
|
53
68
|
PUBLIC_MENTORING: 'MRC010',
|
|
54
69
|
MENTOR_LECTURE: 'MRC020',
|
|
@@ -70,3 +85,53 @@ function createTimeSlots(): string[] {
|
|
|
70
85
|
function formatTime(hour: number, minute: number): string {
|
|
71
86
|
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`
|
|
72
87
|
}
|
|
88
|
+
|
|
89
|
+
export const TOZ_BASE_URL = 'http://partner.toz.co.kr/partner/reservation/fkii3/swmaestro'
|
|
90
|
+
export const TOZ_PARTNER = 'fkii3'
|
|
91
|
+
export const TOZ_COMPANY = 'swmaestro'
|
|
92
|
+
export const TOZ_MEMBER_COMPANY_ID = '25408'
|
|
93
|
+
|
|
94
|
+
// Static fallback list extracted from booking.htm. Use TozClient.branches() at runtime
|
|
95
|
+
// to fetch the live list — the SW마에스트로 partnership branch set may change.
|
|
96
|
+
export const TOZ_BRANCHES = [
|
|
97
|
+
{ id: 27, name: '강남토즈타워점' },
|
|
98
|
+
{ id: 145, name: '강남컨퍼런스센터' },
|
|
99
|
+
{ id: 19, name: '양재점' },
|
|
100
|
+
{ id: 20, name: '건대점' },
|
|
101
|
+
{ id: 15, name: '선릉점' },
|
|
102
|
+
{ id: 139, name: '마이스 역삼센터' },
|
|
103
|
+
{ id: 134, name: '마이스 광화문센터' },
|
|
104
|
+
{ id: 30, name: '신촌비즈센터' },
|
|
105
|
+
{ id: 149, name: '홍대점' },
|
|
106
|
+
] as const
|
|
107
|
+
|
|
108
|
+
export const TOZ_PHONE_PREFIXES = ['010', '011', '016', '017', '018', '019'] as const
|
|
109
|
+
|
|
110
|
+
export const TOZ_EMAIL_DOMAINS = [
|
|
111
|
+
'hanmail.net',
|
|
112
|
+
'gmail.com',
|
|
113
|
+
'nate.com',
|
|
114
|
+
'naver.com',
|
|
115
|
+
'daum.net',
|
|
116
|
+
'dreamwiz.com',
|
|
117
|
+
'yahoo.com',
|
|
118
|
+
'yahoo.co.kr',
|
|
119
|
+
'msn.com',
|
|
120
|
+
'paran.com',
|
|
121
|
+
'korea.com',
|
|
122
|
+
'freechal.com',
|
|
123
|
+
'lycos.co.kr',
|
|
124
|
+
'msn.co.kr',
|
|
125
|
+
'empal.com',
|
|
126
|
+
'hotmail.com',
|
|
127
|
+
] as const
|
|
128
|
+
|
|
129
|
+
export const TOZ_EMAIL_DOMAIN_CUSTOM = '직접입력'
|
|
130
|
+
|
|
131
|
+
export const TOZ_NEW_MEETING_VALUE = '새모임'
|
|
132
|
+
|
|
133
|
+
export const TOZ_MIN_DURATION_MINUTES = 120
|
|
134
|
+
export const TOZ_MAX_DURATION_MINUTES = 180
|
|
135
|
+
export const TOZ_SESSION_HOLD_SECONDS = 300
|
|
136
|
+
|
|
137
|
+
export const TOZ_MAX_CHECK_TIMES = 6
|
package/src/formatters.test.ts
CHANGED
|
@@ -214,14 +214,18 @@ describe('formatters', () => {
|
|
|
214
214
|
<input type="hidden" name="chkData_1" value="09:00" />
|
|
215
215
|
<span class="ck-st2 disabled" data-hour="12" data-minute="00">
|
|
216
216
|
<input type="checkbox" name="time" id="time1_7" value="7" disabled="disabled">
|
|
217
|
-
<label for="time1_7">PM 12:00</label>
|
|
217
|
+
<label for="time1_7" title="점심 회의<br>예약자 : 김오픈">PM 12:00</label>
|
|
218
218
|
</span>
|
|
219
219
|
<input type="hidden" name="chkData_7" value="12:00" />
|
|
220
220
|
`
|
|
221
221
|
|
|
222
222
|
expect(parseRoomSlots(html)).toEqual([
|
|
223
223
|
{ time: '09:00', available: true },
|
|
224
|
-
{
|
|
224
|
+
{
|
|
225
|
+
time: '12:00',
|
|
226
|
+
available: false,
|
|
227
|
+
reservation: { title: '점심 회의', bookedBy: '김오픈' },
|
|
228
|
+
},
|
|
225
229
|
])
|
|
226
230
|
})
|
|
227
231
|
|