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.
- package/dist/package.json +5 -1
- package/dist/src/agent-browser-launcher.d.ts +43 -0
- package/dist/src/agent-browser-launcher.d.ts.map +1 -0
- package/dist/src/agent-browser-launcher.js +97 -0
- package/dist/src/agent-browser-launcher.js.map +1 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +8 -5
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +34 -7
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +224 -52
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands/agent-browser.d.ts +3 -0
- package/dist/src/commands/agent-browser.d.ts.map +1 -0
- package/dist/src/commands/agent-browser.js +27 -0
- package/dist/src/commands/agent-browser.js.map +1 -0
- 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 +4 -2
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/dashboard.d.ts +13 -0
- package/dist/src/commands/dashboard.d.ts.map +1 -1
- package/dist/src/commands/dashboard.js +10 -18
- package/dist/src/commands/dashboard.js.map +1 -1
- package/dist/src/commands/helpers.d.ts +1 -1
- package/dist/src/commands/helpers.d.ts.map +1 -1
- package/dist/src/commands/helpers.js +2 -2
- package/dist/src/commands/helpers.js.map +1 -1
- package/dist/src/commands/index.d.ts +3 -1
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +3 -1
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/mentoring.d.ts.map +1 -1
- package/dist/src/commands/mentoring.js +54 -29
- package/dist/src/commands/mentoring.js.map +1 -1
- package/dist/src/commands/notice.d.ts.map +1 -1
- package/dist/src/commands/notice.js +2 -1
- package/dist/src/commands/notice.js.map +1 -1
- package/dist/src/commands/report.d.ts.map +1 -1
- package/dist/src/commands/report.js +4 -2
- package/dist/src/commands/report.js.map +1 -1
- package/dist/src/commands/room.d.ts.map +1 -1
- package/dist/src/commands/room.js +125 -2
- package/dist/src/commands/room.js.map +1 -1
- package/dist/src/commands/schedule.d.ts +3 -0
- package/dist/src/commands/schedule.d.ts.map +1 -0
- package/dist/src/commands/schedule.js +27 -0
- package/dist/src/commands/schedule.js.map +1 -0
- package/dist/src/commands/team.d.ts.map +1 -1
- package/dist/src/commands/team.js +55 -4
- package/dist/src/commands/team.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/constants.d.ts +5 -5
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +20 -8
- package/dist/src/constants.js.map +1 -1
- package/dist/src/credential-manager.d.ts +15 -0
- package/dist/src/credential-manager.d.ts.map +1 -1
- package/dist/src/credential-manager.js +46 -0
- package/dist/src/credential-manager.js.map +1 -1
- package/dist/src/formatters.d.ts +11 -3
- package/dist/src/formatters.d.ts.map +1 -1
- package/dist/src/formatters.js +281 -52
- package/dist/src/formatters.js.map +1 -1
- package/dist/src/http.d.ts +8 -0
- package/dist/src/http.d.ts.map +1 -1
- package/dist/src/http.js +29 -1
- package/dist/src/http.js.map +1 -1
- package/dist/src/index.d.ts +8 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -1
- 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/shared/utils/swmaestro.d.ts +34 -1
- package/dist/src/shared/utils/swmaestro.d.ts.map +1 -1
- package/dist/src/shared/utils/swmaestro.js +102 -39
- package/dist/src/shared/utils/swmaestro.js.map +1 -1
- package/dist/src/shared/utils/team-action-params.d.ts +3 -0
- package/dist/src/shared/utils/team-action-params.d.ts.map +1 -0
- package/dist/src/shared/utils/team-action-params.js +10 -0
- package/dist/src/shared/utils/team-action-params.js.map +1 -0
- package/dist/src/shared/utils/team-params.d.ts +12 -0
- package/dist/src/shared/utils/team-params.d.ts.map +1 -0
- package/dist/src/shared/utils/team-params.js +38 -0
- package/dist/src/shared/utils/team-params.js.map +1 -0
- 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/dist/src/types.d.ts +147 -10
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +74 -6
- package/dist/src/types.js.map +1 -1
- package/package.json +5 -1
- package/src/agent-browser-launcher.test.ts +263 -0
- package/src/agent-browser-launcher.ts +159 -0
- package/src/cli.ts +10 -5
- package/src/client.test.ts +673 -30
- package/src/client.ts +287 -67
- package/src/commands/agent-browser.ts +33 -0
- package/src/commands/auth.test.ts +77 -26
- package/src/commands/auth.ts +5 -3
- package/src/commands/dashboard.test.ts +57 -0
- package/src/commands/dashboard.ts +22 -19
- package/src/commands/helpers.test.ts +76 -25
- package/src/commands/helpers.ts +3 -3
- package/src/commands/index.ts +3 -1
- package/src/commands/mentoring.ts +60 -29
- package/src/commands/notice.ts +2 -1
- package/src/commands/report.ts +4 -2
- package/src/commands/room.ts +160 -1
- package/src/commands/schedule.ts +32 -0
- package/src/commands/team.ts +73 -5
- package/src/commands/toz.test.ts +51 -0
- package/src/commands/toz.ts +607 -0
- package/src/constants.ts +20 -8
- package/src/credential-manager.test.ts +98 -0
- package/src/credential-manager.ts +50 -0
- package/src/formatters.test.ts +528 -33
- package/src/formatters.ts +309 -55
- package/src/http.test.ts +71 -2
- package/src/http.ts +41 -2
- package/src/index.ts +23 -1
- package/src/session-recovery.ts +2 -0
- package/src/shared/utils/swmaestro.test.ts +245 -9
- package/src/shared/utils/swmaestro.ts +150 -47
- package/src/shared/utils/team-action-params.test.ts +32 -0
- package/src/shared/utils/team-action-params.ts +10 -0
- package/src/shared/utils/team-params.test.ts +141 -0
- package/src/shared/utils/team-params.ts +53 -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
- package/src/types.test.ts +26 -13
- package/src/types.ts +87 -7
- package/dist/src/commands/event.d.ts +0 -3
- package/dist/src/commands/event.d.ts.map +0 -1
- package/dist/src/commands/event.js +0 -58
- package/dist/src/commands/event.js.map +0 -1
- 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
|
-
{
|
|
113
|
-
|
|
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
|
|
150
|
+
it('preserves valid ScheduleListItem values through parse', () => {
|
|
135
151
|
const input = {
|
|
136
|
-
id:
|
|
137
|
-
category: '
|
|
138
|
-
title: '
|
|
139
|
-
|
|
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(
|
|
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(() =>
|
|
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
|
-
|
|
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
|
|
194
|
+
export const ScheduleListItemSchema = z.object({
|
|
116
195
|
id: z.number(),
|
|
117
196
|
category: z.string(),
|
|
118
197
|
title: z.string(),
|
|
119
|
-
|
|
120
|
-
eventPeriod: DateRangeSchema,
|
|
121
|
-
status: z.string(),
|
|
122
|
-
createdAt: z.string(),
|
|
198
|
+
period: DateRangeSchema,
|
|
123
199
|
})
|
|
124
|
-
export type
|
|
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>
|