opensoma 0.3.0 → 0.4.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/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 +12 -9
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +23 -9
- package/dist/src/constants.js.map +1 -1
- package/dist/src/formatters.d.ts.map +1 -1
- package/dist/src/formatters.js +37 -23
- 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/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/types.d.ts +36 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +19 -1
- package/dist/src/types.js.map +1 -1
- package/package.json +1 -1
- package/src/client.test.ts +63 -16
- package/src/client.ts +43 -6
- package/src/commands/mentoring.ts +34 -26
- package/src/commands/room.ts +31 -3
- package/src/constants.ts +24 -9
- package/src/formatters.test.ts +6 -2
- package/src/formatters.ts +48 -29
- package/src/http.test.ts +215 -0
- package/src/http.ts +45 -1
- 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/types.test.ts +4 -1
- package/src/types.ts +23 -1
package/src/client.test.ts
CHANGED
|
@@ -72,14 +72,19 @@ describe('SomaClient', () => {
|
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
test('mutating operations post expected payloads', async () => {
|
|
75
|
+
const mentoringDetailHtml =
|
|
76
|
+
'<div class="group"><strong class="t">모집 명</strong><div class="c">[자유 멘토링] 기존 멘토링</div></div><div class="group"><strong class="t">접수 기간</strong><div class="c">2026.03.01 ~ 2026.03.15</div></div><div class="group"><strong class="t">강의날짜</strong><div class="c"><span>2026.03.20 10:00시 ~ 12:00시</span></div></div><div class="group"><strong class="t">장소</strong><div class="c">온라인(Webex)</div></div><div class="group"><strong class="t">모집인원</strong><div class="c">5명</div></div><div class="group"><strong class="t">작성자</strong><div class="c">전수열</div></div><div class="group"><strong class="t">등록일</strong><div class="c">2026.03.01</div></div><div class="cont"><p>기존 내용</p></div>'
|
|
75
77
|
const client = new SomaClient()
|
|
76
|
-
const calls: Array<{ path: string; data: Record<string, string> }> = []
|
|
78
|
+
const calls: Array<{ method: string; path: string; data: Record<string, string> }> = []
|
|
79
|
+
const captor = (method: string) => async (path: string, data: Record<string, string>) => {
|
|
80
|
+
calls.push({ method, path, data })
|
|
81
|
+
return ''
|
|
82
|
+
}
|
|
77
83
|
Reflect.set(client, 'http', {
|
|
78
84
|
checkLogin: async () => ({ userId: 'user@example.com', userNm: 'Test' }),
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
},
|
|
85
|
+
get: async () => mentoringDetailHtml,
|
|
86
|
+
post: captor('post'),
|
|
87
|
+
postForm: captor('postForm'),
|
|
83
88
|
})
|
|
84
89
|
|
|
85
90
|
await client.mentoring.create({
|
|
@@ -110,14 +115,14 @@ describe('SomaClient', () => {
|
|
|
110
115
|
})
|
|
111
116
|
await client.event.apply(11)
|
|
112
117
|
|
|
113
|
-
expect(calls.map((call) => call.path)).toEqual([
|
|
114
|
-
'
|
|
115
|
-
'
|
|
116
|
-
'
|
|
117
|
-
'
|
|
118
|
-
'
|
|
119
|
-
'
|
|
120
|
-
'
|
|
118
|
+
expect(calls.map((call) => `${call.method}:${call.path}`)).toEqual([
|
|
119
|
+
'postForm:/mypage/mentoLec/insert.do',
|
|
120
|
+
'postForm:/mypage/mentoLec/update.do',
|
|
121
|
+
'post:/mypage/mentoLec/delete.do',
|
|
122
|
+
'post:/application/application/application.do',
|
|
123
|
+
'post:/mypage/userAnswer/cancel.do',
|
|
124
|
+
'post:/mypage/itemRent/insert.do',
|
|
125
|
+
'post:/application/application/application.do',
|
|
121
126
|
])
|
|
122
127
|
expect(calls[0]?.data).toMatchObject({
|
|
123
128
|
menuNo: MENU_NO.MENTORING,
|
|
@@ -161,6 +166,38 @@ describe('SomaClient', () => {
|
|
|
161
166
|
})
|
|
162
167
|
})
|
|
163
168
|
|
|
169
|
+
test('mentoring.update merges partial params with existing data', async () => {
|
|
170
|
+
const mentoringDetailHtml =
|
|
171
|
+
'<div class="group"><strong class="t">모집 명</strong><div class="c">[멘토 특강] 웹 성능 특강</div></div><div class="group"><strong class="t">접수 기간</strong><div class="c">2026.04.01 ~ 2026.04.10</div></div><div class="group"><strong class="t">강의날짜</strong><div class="c"><span>2026.04.11 14:00시 ~ 15:30시</span></div></div><div class="group"><strong class="t">장소</strong><div class="c">온라인(Webex)</div></div><div class="group"><strong class="t">모집인원</strong><div class="c">20명</div></div><div class="group"><strong class="t">작성자</strong><div class="c">전수열</div></div><div class="group"><strong class="t">등록일</strong><div class="c">2026.04.01</div></div><div class="cont"><p>세션 본문</p></div>'
|
|
172
|
+
const client = new SomaClient()
|
|
173
|
+
const postFormCalls: Array<{ path: string; data: Record<string, string> }> = []
|
|
174
|
+
Reflect.set(client, 'http', {
|
|
175
|
+
checkLogin: async () => ({ userId: 'user@example.com', userNm: 'Test' }),
|
|
176
|
+
get: async () => mentoringDetailHtml,
|
|
177
|
+
postForm: async (path: string, data: Record<string, string>) => {
|
|
178
|
+
postFormCalls.push({ path, data })
|
|
179
|
+
return ''
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
await client.mentoring.update(9572, { title: '변경된 제목' })
|
|
184
|
+
|
|
185
|
+
expect(postFormCalls).toHaveLength(1)
|
|
186
|
+
expect(postFormCalls[0]?.data).toMatchObject({
|
|
187
|
+
qustnrSj: '변경된 제목',
|
|
188
|
+
qustnrSn: '9572',
|
|
189
|
+
reportCd: 'MRC020',
|
|
190
|
+
eventDt: '2026-04-11',
|
|
191
|
+
eventStime: '14:00',
|
|
192
|
+
eventEtime: '15:30',
|
|
193
|
+
place: '온라인(Webex)',
|
|
194
|
+
applyCnt: '20',
|
|
195
|
+
bgnde: '2026-04-01',
|
|
196
|
+
endde: '2026-04-10',
|
|
197
|
+
qestnarCn: '<p>세션 본문</p>',
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
164
201
|
test('room, dashboard, notice, team, member, event, and history routes use expected endpoints', async () => {
|
|
165
202
|
const client = new SomaClient()
|
|
166
203
|
const calls: Array<{ method: string; path: string; data: Record<string, string> | undefined }> = []
|
|
@@ -197,13 +234,13 @@ describe('SomaClient', () => {
|
|
|
197
234
|
post: async (path: string, data: Record<string, string>) => {
|
|
198
235
|
calls.push({ method: 'post', path, data })
|
|
199
236
|
if (path === '/mypage/officeMng/rentTime.do') {
|
|
200
|
-
return '<
|
|
237
|
+
return '<span class="ck-st2 disabled" data-hour="09" data-minute="00"><input type="checkbox" disabled="disabled"><label title="아침 회의<br>예약자 : 김오픈">오전 09:00</label></span>'
|
|
201
238
|
}
|
|
202
239
|
return '<ul class="bbs-reserve"><li class="item"><a href="javascript:void(0);" onclick="location.href=\'/sw/mypage/officeMng/view.do?itemId=17\';"><div class="cont"><h4 class="tit">스페이스 A1</h4><ul class="txt bul-dot grey"><li>이용기간 : 2026-04-01 ~ 2026-12-31</li><li><p>설명 : 4인</p></li><li class="time-list"><div class="time-grid"><span>09:00</span></div></li></ul></div></a></li></ul>'
|
|
203
240
|
},
|
|
204
241
|
})
|
|
205
242
|
|
|
206
|
-
const roomList = await client.room.list({ date: '2026-04-01', room: 'A1' })
|
|
243
|
+
const roomList = await client.room.list({ date: '2026-04-01', room: 'A1', includeReservations: true })
|
|
207
244
|
const roomSlots = await client.room.available(17, '2026-04-01')
|
|
208
245
|
const dashboard = await client.dashboard.get()
|
|
209
246
|
const noticeList = await client.notice.list({ page: 2 })
|
|
@@ -215,7 +252,12 @@ describe('SomaClient', () => {
|
|
|
215
252
|
const history = await client.mentoring.history({ page: 4 })
|
|
216
253
|
|
|
217
254
|
expect(roomList[0]?.itemId).toBe(17)
|
|
218
|
-
expect(
|
|
255
|
+
expect(roomList[0]?.timeSlots).toEqual([
|
|
256
|
+
{ time: '09:00', available: false, reservation: { title: '아침 회의', bookedBy: '김오픈' } },
|
|
257
|
+
])
|
|
258
|
+
expect(roomSlots).toEqual([
|
|
259
|
+
{ time: '09:00', available: false, reservation: { title: '아침 회의', bookedBy: '김오픈' } },
|
|
260
|
+
])
|
|
219
261
|
expect(dashboard.name).toBe('전수열')
|
|
220
262
|
expect(dashboard.mentoringSessions).toEqual([
|
|
221
263
|
{
|
|
@@ -249,6 +291,11 @@ describe('SomaClient', () => {
|
|
|
249
291
|
|
|
250
292
|
const dashboardCallIndex = calls.findIndex((c) => c.path === '/mypage/myMain/dashboard.do')
|
|
251
293
|
expect(dashboardCallIndex).toBeGreaterThanOrEqual(0)
|
|
294
|
+
expect(calls).toContainEqual({
|
|
295
|
+
method: 'post',
|
|
296
|
+
path: '/mypage/officeMng/rentTime.do',
|
|
297
|
+
data: { viewType: 'CONTBODY', itemId: '17', rentDt: '2026-04-01' },
|
|
298
|
+
})
|
|
252
299
|
const mentoringListCall = calls.find((c) => c.path === '/mypage/mentoLec/list.do')
|
|
253
300
|
expect(mentoringListCall?.data).toEqual({
|
|
254
301
|
menuNo: MENU_NO.MENTORING,
|
package/src/client.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
buildUpdateMentoringPayload,
|
|
17
17
|
parseEventDetail,
|
|
18
18
|
resolveRoomId,
|
|
19
|
+
toMentoringType,
|
|
19
20
|
toRegionCode,
|
|
20
21
|
toReportTypeCd,
|
|
21
22
|
} from './shared/utils/swmaestro'
|
|
@@ -27,6 +28,7 @@ import type {
|
|
|
27
28
|
MemberInfo,
|
|
28
29
|
MentoringDetail,
|
|
29
30
|
MentoringListItem,
|
|
31
|
+
MentoringUpdateOptions,
|
|
30
32
|
NoticeDetail,
|
|
31
33
|
NoticeListItem,
|
|
32
34
|
Pagination,
|
|
@@ -71,7 +73,7 @@ export class SomaClient {
|
|
|
71
73
|
regEnd?: string
|
|
72
74
|
content?: string
|
|
73
75
|
}): Promise<void>
|
|
74
|
-
update(id: number, params:
|
|
76
|
+
update(id: number, params: MentoringUpdateOptions): Promise<void>
|
|
75
77
|
delete(id: number): Promise<void>
|
|
76
78
|
apply(id: number): Promise<void>
|
|
77
79
|
cancel(params: { applySn: number; qustnrSn: number }): Promise<void>
|
|
@@ -79,7 +81,7 @@ export class SomaClient {
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
readonly room: {
|
|
82
|
-
list(options?: { date?: string; room?: string }): Promise<RoomCard[]>
|
|
84
|
+
list(options?: { date?: string; room?: string; includeReservations?: boolean }): Promise<RoomCard[]>
|
|
83
85
|
available(roomId: number, date: string): Promise<RoomCard['timeSlots']>
|
|
84
86
|
reserve(params: {
|
|
85
87
|
roomId: number
|
|
@@ -176,14 +178,27 @@ export class SomaClient {
|
|
|
176
178
|
},
|
|
177
179
|
create: async (params) => {
|
|
178
180
|
await this.requireAuth()
|
|
179
|
-
const html = await this.http.
|
|
181
|
+
const html = await this.http.postForm('/mypage/mentoLec/insert.do', buildMentoringPayload(params))
|
|
180
182
|
if (this.containsErrorIndicator(html)) {
|
|
181
183
|
throw new Error(this.extractErrorMessage(html) || '멘토링 등록에 실패했습니다.')
|
|
182
184
|
}
|
|
183
185
|
},
|
|
184
186
|
update: async (id, params) => {
|
|
185
187
|
await this.requireAuth()
|
|
186
|
-
const
|
|
188
|
+
const existing = await this.mentoring.get(id)
|
|
189
|
+
const merged = buildUpdateMentoringPayload(id, {
|
|
190
|
+
title: params.title ?? existing.title,
|
|
191
|
+
type: params.type ?? toMentoringType(existing.type),
|
|
192
|
+
date: params.date ?? existing.sessionDate,
|
|
193
|
+
startTime: params.startTime ?? existing.sessionTime.start,
|
|
194
|
+
endTime: params.endTime ?? existing.sessionTime.end,
|
|
195
|
+
venue: params.venue ?? existing.venue,
|
|
196
|
+
maxAttendees: params.maxAttendees ?? existing.attendees.max,
|
|
197
|
+
regStart: params.regStart ?? existing.registrationPeriod.start,
|
|
198
|
+
regEnd: params.regEnd ?? existing.registrationPeriod.end,
|
|
199
|
+
content: params.content ?? existing.content,
|
|
200
|
+
})
|
|
201
|
+
const html = await this.http.postForm('/mypage/mentoLec/update.do', merged)
|
|
187
202
|
if (this.containsErrorIndicator(html)) {
|
|
188
203
|
throw new Error(this.extractErrorMessage(html) || '멘토링 수정에 실패했습니다.')
|
|
189
204
|
}
|
|
@@ -216,13 +231,35 @@ export class SomaClient {
|
|
|
216
231
|
this.room = {
|
|
217
232
|
list: async (options) => {
|
|
218
233
|
await this.requireAuth()
|
|
219
|
-
|
|
234
|
+
const date = options?.date ?? new Date().toISOString().slice(0, 10)
|
|
235
|
+
const rooms = formatters.parseRoomList(
|
|
220
236
|
await this.http.post('/mypage/officeMng/list.do', {
|
|
221
237
|
menuNo: MENU_NO.ROOM,
|
|
222
|
-
sdate:
|
|
238
|
+
sdate: date,
|
|
223
239
|
searchItemId: options?.room ? String(resolveRoomId(options.room)) : '',
|
|
224
240
|
}),
|
|
225
241
|
)
|
|
242
|
+
|
|
243
|
+
if (!options?.includeReservations) return rooms
|
|
244
|
+
|
|
245
|
+
return Promise.all(
|
|
246
|
+
rooms.map(async (room) => {
|
|
247
|
+
try {
|
|
248
|
+
const html = await this.http.post('/mypage/officeMng/rentTime.do', {
|
|
249
|
+
viewType: 'CONTBODY',
|
|
250
|
+
itemId: String(room.itemId),
|
|
251
|
+
rentDt: date,
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
...room,
|
|
256
|
+
timeSlots: formatters.parseRoomSlots(html),
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
return room
|
|
260
|
+
}
|
|
261
|
+
}),
|
|
262
|
+
)
|
|
226
263
|
},
|
|
227
264
|
available: async (roomId, date) => {
|
|
228
265
|
await this.requireAuth()
|
|
@@ -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',
|
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
|
|
package/src/formatters.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { type HTMLElement, parse } from 'node-html-parser'
|
|
2
2
|
|
|
3
|
+
import { decodeHtmlEntities } from './shared/utils/html'
|
|
3
4
|
import {
|
|
4
|
-
ApplicationHistoryItemSchema,
|
|
5
5
|
type ApplicationHistoryItem,
|
|
6
|
-
|
|
6
|
+
ApplicationHistoryItemSchema,
|
|
7
7
|
type ApprovalListItem,
|
|
8
|
-
|
|
9
|
-
EventListItemSchema,
|
|
8
|
+
ApprovalListItemSchema,
|
|
10
9
|
type Dashboard,
|
|
10
|
+
DashboardSchema,
|
|
11
11
|
type EventListItem,
|
|
12
|
+
EventListItemSchema,
|
|
12
13
|
type MemberInfo,
|
|
13
14
|
MemberInfoSchema,
|
|
14
15
|
type MentoringDetail,
|
|
@@ -98,7 +99,7 @@ export function parseMentoringDetail(html: string, id = 0): MentoringDetail {
|
|
|
98
99
|
status: extractStatus(labels.상태 || rawTitle),
|
|
99
100
|
author: labels['작성자'] || '',
|
|
100
101
|
createdAt: labels['등록일'] || '',
|
|
101
|
-
content: contentNode?.innerHTML.trim() ?? '',
|
|
102
|
+
content: decodeHtmlEntities(contentNode?.innerHTML.trim() ?? ''),
|
|
102
103
|
venue: labels['장소'] || '',
|
|
103
104
|
applicants,
|
|
104
105
|
})
|
|
@@ -131,20 +132,7 @@ export function parseRoomSlots(html: string): RoomCard['timeSlots'] {
|
|
|
131
132
|
return parseTimeSlotsFromRoot(root)
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
return slots
|
|
135
|
-
.map((slot) => {
|
|
136
|
-
const hour = slot.getAttribute('data-hour') ?? ''
|
|
137
|
-
const minute = slot.getAttribute('data-minute') ?? ''
|
|
138
|
-
const checkbox = slot.querySelector('input[type="checkbox"]')
|
|
139
|
-
const className = slot.getAttribute('class') ?? ''
|
|
140
|
-
const time = hour && minute ? `${hour}:${minute}` : normalizeTime(cleanText(slot.querySelector('label') ?? slot))
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
time,
|
|
144
|
-
available: !checkbox?.hasAttribute('disabled') && !className.includes('disabled'),
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
.filter((slot) => Boolean(slot.time))
|
|
135
|
+
return slots.map(parseRoomTimeSlot).filter((slot) => Boolean(slot.time))
|
|
148
136
|
}
|
|
149
137
|
|
|
150
138
|
export function parseDashboard(html: string): Dashboard {
|
|
@@ -202,7 +190,7 @@ export function parseNoticeDetail(html: string, id = 0): NoticeDetail {
|
|
|
202
190
|
title: cleanText(top?.querySelector('.tit')) || labels['제목'] || cleanText(root.querySelector('h1, h2, .title')),
|
|
203
191
|
author,
|
|
204
192
|
createdAt,
|
|
205
|
-
content: contentNode?.innerHTML.trim() ?? '',
|
|
193
|
+
content: decodeHtmlEntities(contentNode?.innerHTML.trim() ?? ''),
|
|
206
194
|
})
|
|
207
195
|
}
|
|
208
196
|
|
|
@@ -464,15 +452,46 @@ function parseTimeSlotsFromRoot(root: HTMLElement): RoomCard['timeSlots'] {
|
|
|
464
452
|
? grid.querySelectorAll('span')
|
|
465
453
|
: root.querySelectorAll('.time-grid span, [class*="time-slot"], .slot')
|
|
466
454
|
|
|
467
|
-
return spans
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
455
|
+
return spans.map(parseRoomTimeSlot).filter((slot) => Boolean(slot.time))
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function parseRoomTimeSlot(slot: HTMLElement): RoomCard['timeSlots'][number] {
|
|
459
|
+
const label = slot.querySelector('label')
|
|
460
|
+
const hour = slot.getAttribute('data-hour') ?? ''
|
|
461
|
+
const minute = slot.getAttribute('data-minute') ?? ''
|
|
462
|
+
const checkbox = slot.querySelector('input[type="checkbox"]')
|
|
463
|
+
const className = slot.getAttribute('class') ?? ''
|
|
464
|
+
const available =
|
|
465
|
+
!checkbox?.hasAttribute('disabled') &&
|
|
466
|
+
!className.includes('not-reserve') &&
|
|
467
|
+
!className.includes('booked') &&
|
|
468
|
+
!className.includes('disabled')
|
|
469
|
+
const time = hour && minute ? `${hour}:${minute}` : normalizeTime(cleanText(label ?? slot))
|
|
470
|
+
const reservation = !available ? extractReservation(label) : undefined
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
time,
|
|
474
|
+
available,
|
|
475
|
+
...(reservation ? { reservation } : {}),
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function extractReservation(label: HTMLElement | null): { title: string; bookedBy: string } | undefined {
|
|
480
|
+
const rawTitle = label?.getAttribute('title')
|
|
481
|
+
if (!rawTitle) {
|
|
482
|
+
return undefined
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const [title = '', bookedByLine = ''] = decodeHtmlEntities(rawTitle)
|
|
486
|
+
.split(/<br\s*\/?>/i)
|
|
487
|
+
.map((part) => part.trim())
|
|
488
|
+
const bookedBy = bookedByLine.replace(/^예약자\s*:\s*/, '').trim()
|
|
489
|
+
|
|
490
|
+
if (!title || !bookedBy) {
|
|
491
|
+
return undefined
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return { title, bookedBy }
|
|
476
495
|
}
|
|
477
496
|
|
|
478
497
|
function findDashboardValue(items: HTMLElement[], label: string): string {
|