opensoma 0.5.0 → 0.6.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 +3 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +36 -7
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +231 -63
- 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 +2 -1
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +2 -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/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 +9 -0
- package/dist/src/credential-manager.d.ts.map +1 -1
- package/dist/src/credential-manager.js +24 -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 +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.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 -43
- 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/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 +4 -2
- package/src/client.test.ts +801 -140
- package/src/client.ts +293 -79
- package/src/commands/agent-browser.ts +33 -0
- package/src/commands/auth.test.ts +83 -32
- 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 +79 -32
- package/src/commands/helpers.ts +3 -3
- package/src/commands/index.ts +2 -1
- package/src/commands/mentoring.ts +60 -29
- package/src/commands/notice.ts +2 -1
- package/src/commands/report.test.ts +7 -7
- 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/constants.ts +20 -8
- package/src/credential-manager.test.ts +49 -5
- package/src/credential-manager.ts +27 -0
- package/src/formatters.test.ts +548 -53
- package/src/formatters.ts +309 -55
- package/src/http.test.ts +108 -39
- package/src/http.ts +41 -2
- package/src/index.ts +10 -1
- package/src/shared/utils/mentoring-params.test.ts +16 -16
- package/src/shared/utils/swmaestro.test.ts +326 -11
- package/src/shared/utils/swmaestro.ts +150 -52
- 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/shared/utils/toz.test.ts +12 -7
- package/src/token-extractor.test.ts +12 -12
- package/src/toz-http.test.ts +11 -11
- package/src/types.test.ts +235 -206
- 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
package/src/formatters.ts
CHANGED
|
@@ -8,12 +8,12 @@ import {
|
|
|
8
8
|
ApprovalListItemSchema,
|
|
9
9
|
type Dashboard,
|
|
10
10
|
DashboardSchema,
|
|
11
|
-
type EventListItem,
|
|
12
|
-
EventListItemSchema,
|
|
13
11
|
type MemberInfo,
|
|
14
12
|
MemberInfoSchema,
|
|
15
13
|
type MentoringDetail,
|
|
16
14
|
MentoringDetailSchema,
|
|
15
|
+
type MentoringEditForm,
|
|
16
|
+
MentoringEditFormSchema,
|
|
17
17
|
type MentoringListItem,
|
|
18
18
|
MentoringListItemSchema,
|
|
19
19
|
type NoticeDetail,
|
|
@@ -28,6 +28,13 @@ import {
|
|
|
28
28
|
ReportListItemSchema,
|
|
29
29
|
type RoomCard,
|
|
30
30
|
RoomCardSchema,
|
|
31
|
+
type RoomReservationDetail,
|
|
32
|
+
RoomReservationDetailSchema,
|
|
33
|
+
type RoomReservationListItem,
|
|
34
|
+
RoomReservationListItemSchema,
|
|
35
|
+
type RoomReservationStatus,
|
|
36
|
+
type ScheduleListItem,
|
|
37
|
+
ScheduleListItemSchema,
|
|
31
38
|
type TeamInfo,
|
|
32
39
|
TeamInfoSchema,
|
|
33
40
|
} from './types'
|
|
@@ -105,6 +112,83 @@ export function parseMentoringDetail(html: string, id = 0): MentoringDetail {
|
|
|
105
112
|
})
|
|
106
113
|
}
|
|
107
114
|
|
|
115
|
+
export function parseMentoringEditForm(html: string, id = 0): MentoringEditForm {
|
|
116
|
+
const root = parse(html)
|
|
117
|
+
const form = root.querySelector('form#board') ?? root.querySelector('form')
|
|
118
|
+
const fields = readFormFields(form)
|
|
119
|
+
const initial = parseInitialDataBlock(html)
|
|
120
|
+
|
|
121
|
+
const pick = (key: string): string => {
|
|
122
|
+
const formValue = fields[key]
|
|
123
|
+
if (formValue && formValue.trim() !== '') return formValue
|
|
124
|
+
return initial[key] ?? ''
|
|
125
|
+
}
|
|
126
|
+
const receiptType = pick('receiptType') === 'DIRECT' ? 'DIRECT' : 'UNTIL_LECTURE'
|
|
127
|
+
|
|
128
|
+
return MentoringEditFormSchema.parse({
|
|
129
|
+
id: id || extractNumber(fields.qustnrSn ?? ''),
|
|
130
|
+
title: fields.qustnrSj ?? '',
|
|
131
|
+
reportCd: pick('reportCd'),
|
|
132
|
+
receiptType,
|
|
133
|
+
bgndeDate: pick('bgndeDate'),
|
|
134
|
+
bgndeTime: pick('bgndeTime'),
|
|
135
|
+
enddeDate: pick('enddeDate'),
|
|
136
|
+
enddeTime: pick('enddeTime'),
|
|
137
|
+
eventDt: pick('eventDt'),
|
|
138
|
+
eventStime: pick('eventStime'),
|
|
139
|
+
eventEtime: pick('eventEtime'),
|
|
140
|
+
applyCnt: extractNumber(fields.applyCnt ?? ''),
|
|
141
|
+
place: fields.place ?? '',
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseInitialDataBlock(html: string): Record<string, string> {
|
|
146
|
+
const block = html.match(/var\s+INITIAL_DATA\s*=\s*\{([\s\S]*?)\};/)?.[1]
|
|
147
|
+
if (!block) return {}
|
|
148
|
+
const fields: Record<string, string> = {}
|
|
149
|
+
for (const match of block.matchAll(/(\w+)\s*:\s*'([^']*)'/g)) {
|
|
150
|
+
fields[match[1]] = match[2]
|
|
151
|
+
}
|
|
152
|
+
return fields
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function readFormFields(form: HTMLElement | null): Record<string, string> {
|
|
156
|
+
if (!form) return {}
|
|
157
|
+
const fields: Record<string, string> = {}
|
|
158
|
+
|
|
159
|
+
for (const input of form.querySelectorAll('input')) {
|
|
160
|
+
const name = input.getAttribute('name')
|
|
161
|
+
if (!name) continue
|
|
162
|
+
const type = (input.getAttribute('type') ?? '').toLowerCase()
|
|
163
|
+
if (type === 'radio' || type === 'checkbox') {
|
|
164
|
+
const rawAttrs = input.rawAttrs ?? ''
|
|
165
|
+
const isChecked = /\bchecked\b/.test(rawAttrs)
|
|
166
|
+
if (isChecked) {
|
|
167
|
+
fields[name] = input.getAttribute('value') ?? ''
|
|
168
|
+
} else if (!(name in fields)) {
|
|
169
|
+
fields[name] = ''
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
fields[name] = input.getAttribute('value') ?? ''
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const select of form.querySelectorAll('select')) {
|
|
177
|
+
const name = select.getAttribute('name')
|
|
178
|
+
if (!name) continue
|
|
179
|
+
const selected = select.querySelector('option[selected]')
|
|
180
|
+
fields[name] = selected?.getAttribute('value') ?? ''
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const textarea of form.querySelectorAll('textarea')) {
|
|
184
|
+
const name = textarea.getAttribute('name')
|
|
185
|
+
if (!name) continue
|
|
186
|
+
fields[name] = textarea.text ?? ''
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return fields
|
|
190
|
+
}
|
|
191
|
+
|
|
108
192
|
export function parseRoomList(html: string): RoomCard[] {
|
|
109
193
|
const root = parse(html)
|
|
110
194
|
const cards = root.querySelectorAll('ul.bbs-reserve > li.item')
|
|
@@ -135,6 +219,86 @@ export function parseRoomSlots(html: string): RoomCard['timeSlots'] {
|
|
|
135
219
|
return slots.map(parseRoomTimeSlot).filter((slot) => Boolean(slot.time))
|
|
136
220
|
}
|
|
137
221
|
|
|
222
|
+
export function parseRoomReservationDetail(html: string): RoomReservationDetail {
|
|
223
|
+
const root = parse(html)
|
|
224
|
+
const form = root.querySelector('form#frm') ?? root.querySelector('form')
|
|
225
|
+
const fields: Record<string, string> = {}
|
|
226
|
+
for (const input of form?.querySelectorAll('input, select, textarea') ?? []) {
|
|
227
|
+
const name = input.getAttribute('name')
|
|
228
|
+
if (!name) continue
|
|
229
|
+
fields[name] = input.getAttribute('value') ?? input.text ?? ''
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const statusCode = fields.receiptStatCd ?? ''
|
|
233
|
+
|
|
234
|
+
return RoomReservationDetailSchema.parse({
|
|
235
|
+
rentId: extractNumber(fields.rentId ?? ''),
|
|
236
|
+
itemId: extractNumber(fields.itemId ?? ''),
|
|
237
|
+
title: fields.title ?? '',
|
|
238
|
+
date: fields.rentDt ?? extractReservationDate(fields.rentBgnde),
|
|
239
|
+
startTime: extractReservationTime(fields.rentBgnde),
|
|
240
|
+
endTime: extractReservationTime(fields.rentEndde),
|
|
241
|
+
attendees: extractNumber(fields.rentNum ?? '') || 1,
|
|
242
|
+
notes: fields.infoCn ?? '',
|
|
243
|
+
status: resolveReservationStatus(statusCode),
|
|
244
|
+
statusCode,
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function extractReservationDate(value: string | undefined): string {
|
|
249
|
+
if (!value) return ''
|
|
250
|
+
return value.slice(0, 10)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function extractReservationTime(value: string | undefined): string {
|
|
254
|
+
if (!value) return ''
|
|
255
|
+
const match = value.match(/\d{2}:\d{2}/)
|
|
256
|
+
return match?.[0] ?? ''
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function resolveReservationStatus(code: string): RoomReservationStatus {
|
|
260
|
+
if (code === 'RS001') return 'confirmed'
|
|
261
|
+
if (code === 'RS002') return 'cancelled'
|
|
262
|
+
return 'unknown'
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function parseRoomReservationList(html: string): RoomReservationListItem[] {
|
|
266
|
+
return findTableRows(html, 7).map((cells) => {
|
|
267
|
+
const venueLink = cells[1]?.querySelector('a')
|
|
268
|
+
const rentId = extractUrlParam(venueLink?.getAttribute('href'), 'rentId')
|
|
269
|
+
const titleLink = cells[2]?.querySelector('.rel a')
|
|
270
|
+
const periodText = cleanText(cells[3])
|
|
271
|
+
const statusLabel = cleanText(cells[5])
|
|
272
|
+
|
|
273
|
+
return RoomReservationListItemSchema.parse({
|
|
274
|
+
rentId,
|
|
275
|
+
venue: cleanText(venueLink ?? cells[1]),
|
|
276
|
+
title: cleanText(titleLink),
|
|
277
|
+
date: extractFirstDate(periodText),
|
|
278
|
+
startTime: extractReservationStartTime(periodText),
|
|
279
|
+
endTime: extractReservationEndTime(periodText),
|
|
280
|
+
author: cleanText(cells[4]),
|
|
281
|
+
status: statusLabelToStatus(statusLabel),
|
|
282
|
+
statusLabel,
|
|
283
|
+
registeredAt: cleanText(cells[6]),
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function statusLabelToStatus(label: string): RoomReservationStatus {
|
|
289
|
+
if (label.includes('예약완료')) return 'confirmed'
|
|
290
|
+
if (label.includes('예약취소') || label.includes('취소')) return 'cancelled'
|
|
291
|
+
return 'unknown'
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function extractReservationStartTime(text: string): string {
|
|
295
|
+
return text.match(/\d{2}:\d{2}/)?.[0] ?? ''
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function extractReservationEndTime(text: string): string {
|
|
299
|
+
return text.match(/\d{2}:\d{2}/g)?.[1] ?? ''
|
|
300
|
+
}
|
|
301
|
+
|
|
138
302
|
export function parseDashboard(html: string): Dashboard {
|
|
139
303
|
const root = parse(html)
|
|
140
304
|
const profileCard = root.querySelector('ul.dash-top > li.dash-card')
|
|
@@ -153,6 +317,7 @@ export function parseDashboard(html: string): Dashboard {
|
|
|
153
317
|
organization: extractDashEtcValue(dashEtc, '소속'),
|
|
154
318
|
position: extractDashEtcValue(dashEtc, '직책'),
|
|
155
319
|
team: team.name || team.members || team.mentor ? team : undefined,
|
|
320
|
+
teams: [],
|
|
156
321
|
mentoringSessions: parseDashboardLinks(root, (href) => href.includes('/mentoLec/')),
|
|
157
322
|
roomReservations: parseDashboardLinks(root, (href) => href.includes('/itemRent/') || href.includes('/officeMng/')),
|
|
158
323
|
})
|
|
@@ -198,16 +363,12 @@ export function parseTeamInfo(html: string): TeamInfo {
|
|
|
198
363
|
const root = parse(html)
|
|
199
364
|
const cards = root.querySelectorAll('ul.bbs-team > li')
|
|
200
365
|
const summaryText = cleanText(root.querySelector('p.ico-team'))
|
|
201
|
-
const
|
|
366
|
+
const summary = parseTeamSummary(summaryText)
|
|
202
367
|
|
|
203
368
|
return TeamInfoSchema.parse({
|
|
204
|
-
teams: cards.map((card) => (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
joinStatus: extractJoinStatus(card),
|
|
208
|
-
})),
|
|
209
|
-
currentTeams: summaryNumbers ? Number.parseInt(summaryNumbers[1], 10) : 0,
|
|
210
|
-
maxTeams: summaryNumbers ? Number.parseInt(summaryNumbers[2], 10) : 0,
|
|
369
|
+
teams: cards.map((card) => parseTeamCard(card)),
|
|
370
|
+
currentTeams: summary.current,
|
|
371
|
+
maxTeams: summary.max,
|
|
211
372
|
})
|
|
212
373
|
}
|
|
213
374
|
|
|
@@ -225,26 +386,42 @@ export function parseMemberInfo(html: string): MemberInfo {
|
|
|
225
386
|
})
|
|
226
387
|
}
|
|
227
388
|
|
|
228
|
-
export function
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
389
|
+
export function parseScheduleList(html: string): { items: ScheduleListItem[]; pagination: Pagination } {
|
|
390
|
+
const root = parse(html)
|
|
391
|
+
const monthlyScheduleTable = findTableByHeaders(root, ['날짜', '구분', '제목'])
|
|
392
|
+
const scheduleRows = monthlyScheduleTable?.querySelectorAll('tbody tr') ?? []
|
|
393
|
+
const items = scheduleRows
|
|
394
|
+
.map((row) => row.querySelectorAll('td'))
|
|
395
|
+
.filter((cells) => cells.length === 3)
|
|
396
|
+
.map((cells, index) =>
|
|
397
|
+
ScheduleListItemSchema.parse({
|
|
398
|
+
id: index + 1,
|
|
399
|
+
category: cleanText(cells[1]),
|
|
400
|
+
title: cleanText(cells[2]),
|
|
401
|
+
period: extractDateRange(cleanText(cells[0])),
|
|
402
|
+
}),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
// SWMaestro's monthly schedule page does not render the bbs-total pagination block,
|
|
406
|
+
// so synthesize a single-page response when items exist but parsePagination found none.
|
|
407
|
+
const parsedPagination = parsePagination(html, { itemCount: items.length })
|
|
408
|
+
const pagination =
|
|
409
|
+
parsedPagination.total === 0 && items.length > 0
|
|
410
|
+
? PaginationSchema.parse({ total: items.length, currentPage: 1, totalPages: 1 })
|
|
411
|
+
: parsedPagination
|
|
412
|
+
|
|
413
|
+
return { items, pagination }
|
|
240
414
|
}
|
|
241
415
|
|
|
242
416
|
export function parseApplicationHistory(html: string): ApplicationHistoryItem[] {
|
|
243
|
-
return findTableRows(html, 10).map((cells) =>
|
|
244
|
-
|
|
417
|
+
return findTableRows(html, 10).map((cells) => {
|
|
418
|
+
const link = cells[2]?.querySelector('a')
|
|
419
|
+
const url = link?.getAttribute('href')
|
|
420
|
+
return ApplicationHistoryItemSchema.parse({
|
|
245
421
|
id: extractNumber(cleanText(cells[0])),
|
|
246
422
|
category: cleanText(cells[1]),
|
|
247
|
-
title: cleanText(
|
|
423
|
+
title: cleanText(link ?? cells[2]),
|
|
424
|
+
...(url ? { url } : {}),
|
|
248
425
|
author: cleanText(cells[3]),
|
|
249
426
|
sessionDate: normalizeDate(cleanText(cells[4])),
|
|
250
427
|
appliedAt: cleanText(cells[5]),
|
|
@@ -252,23 +429,46 @@ export function parseApplicationHistory(html: string): ApplicationHistoryItem[]
|
|
|
252
429
|
approvalStatus: stripWrappingBrackets(cleanText(cells[7])),
|
|
253
430
|
applicationDetail: cleanText(cells[8]),
|
|
254
431
|
note: cleanText(cells[9]),
|
|
255
|
-
})
|
|
256
|
-
)
|
|
432
|
+
})
|
|
433
|
+
})
|
|
257
434
|
}
|
|
258
435
|
|
|
259
|
-
export function parsePagination(html: string): Pagination {
|
|
436
|
+
export function parsePagination(html: string, options?: { itemCount?: number }): Pagination {
|
|
260
437
|
const root = parse(html)
|
|
261
438
|
const items = root.querySelectorAll('ul.bbs-total > li').map((item) => cleanText(item))
|
|
262
439
|
const total = extractNumber(items.find((item) => item.includes('Total')) ?? '')
|
|
263
440
|
const pageMatch = items.find((item) => item.includes('Page'))?.match(/(\d+)\s*\/\s*(\d+)\s*Page/i)
|
|
441
|
+
const currentPage = pageMatch ? Number.parseInt(pageMatch[1], 10) : 1
|
|
442
|
+
const parsedTotalPages = pageMatch ? Number.parseInt(pageMatch[2], 10) : 1
|
|
264
443
|
|
|
265
444
|
return PaginationSchema.parse({
|
|
266
445
|
total,
|
|
267
|
-
currentPage
|
|
268
|
-
totalPages:
|
|
446
|
+
currentPage,
|
|
447
|
+
totalPages: deriveTotalPages({ total, currentPage, parsedTotalPages, itemCount: options?.itemCount }),
|
|
269
448
|
})
|
|
270
449
|
}
|
|
271
450
|
|
|
451
|
+
function deriveTotalPages({
|
|
452
|
+
total,
|
|
453
|
+
currentPage,
|
|
454
|
+
parsedTotalPages,
|
|
455
|
+
itemCount,
|
|
456
|
+
}: {
|
|
457
|
+
total: number
|
|
458
|
+
currentPage: number
|
|
459
|
+
parsedTotalPages: number
|
|
460
|
+
itemCount: number | undefined
|
|
461
|
+
}): number {
|
|
462
|
+
if (total <= 0) return 1
|
|
463
|
+
const fallback = Math.max(1, parsedTotalPages, currentPage)
|
|
464
|
+
if (itemCount === undefined) return fallback
|
|
465
|
+
// SWMaestro's `1/50 Page` text is unreliable on some lists (e.g. report) — the server emits a
|
|
466
|
+
// hardcoded cap regardless of actual data. When the visible items already cover the total,
|
|
467
|
+
// there is only one page; correct the bogus value rather than echo it.
|
|
468
|
+
if (currentPage === 1 && total <= itemCount) return 1
|
|
469
|
+
return fallback
|
|
470
|
+
}
|
|
471
|
+
|
|
272
472
|
export function parseCsrfToken(html: string): string {
|
|
273
473
|
const token = parse(html).querySelector('input[name="csrfToken"]')?.getAttribute('value')
|
|
274
474
|
|
|
@@ -281,12 +481,16 @@ export function parseCsrfToken(html: string): string {
|
|
|
281
481
|
|
|
282
482
|
export function parseReportList(html: string): ReportListItem[] {
|
|
283
483
|
return findTableRows(html, 9).map((cells) => {
|
|
284
|
-
|
|
484
|
+
// Native list cell layout: `td.tit > div.date_m > span.l > a` holds the
|
|
485
|
+
// category, and `td.tit > div.rel > a` holds the actual title that matches
|
|
486
|
+
// the `제목` label on the detail page (e.g. "[자유 멘토링] 2026년 04월 30일
|
|
487
|
+
// 멘토링 보고"). Prefer the `.rel a` so list and detail titles agree.
|
|
488
|
+
const titleLink = cells[2]?.querySelector('div.rel a') ?? cells[2]?.querySelector('a')
|
|
285
489
|
|
|
286
490
|
return ReportListItemSchema.parse({
|
|
287
|
-
id: extractUrlParam(
|
|
491
|
+
id: extractUrlParam(titleLink?.getAttribute('href'), 'reportId'),
|
|
288
492
|
category: cleanText(cells[1]),
|
|
289
|
-
title: cleanText(
|
|
493
|
+
title: cleanText(titleLink ?? cells[2]),
|
|
290
494
|
progressDate: cleanText(cells[3]),
|
|
291
495
|
status: cleanText(cells[4]),
|
|
292
496
|
author: cleanText(cells[5]),
|
|
@@ -399,6 +603,14 @@ function findTableRows(html: string, cellCount: number): HTMLElement[][] {
|
|
|
399
603
|
.filter((cells) => cells.length === cellCount)
|
|
400
604
|
}
|
|
401
605
|
|
|
606
|
+
function findTableByHeaders(root: HTMLElement, headers: string[]): HTMLElement | undefined {
|
|
607
|
+
return root.querySelectorAll('table').find((table) => {
|
|
608
|
+
const tableHeaders = table.querySelectorAll('thead th').map((header) => cleanText(header))
|
|
609
|
+
|
|
610
|
+
return headers.every((header, index) => tableHeaders[index] === header)
|
|
611
|
+
})
|
|
612
|
+
}
|
|
613
|
+
|
|
402
614
|
function extractLabelMap(root: HTMLElement): LabelMap {
|
|
403
615
|
const map: LabelMap = {}
|
|
404
616
|
|
|
@@ -560,19 +772,6 @@ function extractUrlParam(url: string | undefined, key: string): number {
|
|
|
560
772
|
}
|
|
561
773
|
}
|
|
562
774
|
|
|
563
|
-
function extractLinkParam(cell: HTMLElement | undefined, keys: string[]): number {
|
|
564
|
-
const href = cell?.querySelector('a')?.getAttribute('href')
|
|
565
|
-
|
|
566
|
-
for (const key of keys) {
|
|
567
|
-
const value = extractUrlParam(href, key)
|
|
568
|
-
if (value > 0) {
|
|
569
|
-
return value
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
return 0
|
|
574
|
-
}
|
|
575
|
-
|
|
576
775
|
function extractDateRange(text: string): { start: string; end: string } {
|
|
577
776
|
const dates = text.match(/\d{4}[.-]\d{2}[.-]\d{2}/g)?.map(normalizeDate) ?? []
|
|
578
777
|
return { start: dates[0] ?? '', end: dates[1] ?? '' }
|
|
@@ -605,18 +804,73 @@ function extractCapacity(text: string): number {
|
|
|
605
804
|
return match ? Number.parseInt(match[1], 10) : 0
|
|
606
805
|
}
|
|
607
806
|
|
|
608
|
-
function
|
|
609
|
-
const
|
|
610
|
-
|
|
807
|
+
function parseTeamCard(card: HTMLElement) {
|
|
808
|
+
const titleAnchor = card.querySelector('.top strong.t a')
|
|
809
|
+
const teamPageGoArgs = parseTeamPageGoArgs(titleAnchor?.getAttribute('onclick') ?? '')
|
|
810
|
+
const infoItems = card.querySelectorAll('.top ul.info > li')
|
|
811
|
+
const ictItems = card.querySelectorAll('.bot ul.ict > li')
|
|
812
|
+
|
|
813
|
+
return {
|
|
814
|
+
name: cleanText(titleAnchor),
|
|
815
|
+
projectName: cleanText(card.querySelector('.top .add-txt')),
|
|
816
|
+
ownerId: teamPageGoArgs[1] ?? '',
|
|
817
|
+
teamId: teamPageGoArgs[2] ?? '',
|
|
818
|
+
leader: extractInfoLeader(infoItems),
|
|
819
|
+
members: extractInfoMembers(infoItems, '팀원'),
|
|
820
|
+
mentors: extractInfoMembers(infoItems, '멘토'),
|
|
821
|
+
ictCategoryMajor: extractIctCategory(ictItems, '대'),
|
|
822
|
+
ictCategoryMinor: extractIctCategory(ictItems, '중'),
|
|
823
|
+
teamCompleted: card.querySelector('.team-com .t1') !== null,
|
|
824
|
+
mentorCompleted: card.querySelector('.team-com .t2') !== null,
|
|
825
|
+
joinStatus: extractJoinStatus(card),
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function parseTeamSummary(text: string): { current: number; max: number } {
|
|
830
|
+
const match = text.match(/(\d+)\s*(?:\/\s*(\d+))?\s*팀/)
|
|
831
|
+
return {
|
|
832
|
+
current: match ? Number.parseInt(match[1], 10) : 0,
|
|
833
|
+
max: match?.[2] ? Number.parseInt(match[2], 10) : 0,
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function parseTeamPageGoArgs(onclick: string): string[] {
|
|
838
|
+
// teamPageGo('전수열','f6d192...','ef9d3b...');
|
|
839
|
+
const match = onclick.match(/teamPageGo\(\s*'([^']*)'\s*,\s*'([^']*)'\s*,\s*'([^']*)'\s*\)/)
|
|
840
|
+
return match ? [match[1], match[2], match[3]] : []
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function findInfoItemByLabel(items: HTMLElement[], label: string): HTMLElement | undefined {
|
|
844
|
+
return items.find((item) => cleanText(item.querySelector('strong')).startsWith(label))
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function extractInfoLeader(items: HTMLElement[]): string {
|
|
848
|
+
const leaderItem = findInfoItemByLabel(items, '팀장')
|
|
849
|
+
return cleanText(leaderItem?.querySelector('span a')) || cleanText(leaderItem?.querySelector('span'))
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function extractInfoMembers(items: HTMLElement[], label: string): { name: string; userId: string }[] {
|
|
853
|
+
const item = findInfoItemByLabel(items, label)
|
|
854
|
+
if (!item) return []
|
|
855
|
+
return item.querySelectorAll('span a').map((anchor) => ({
|
|
856
|
+
name: cleanText(anchor),
|
|
857
|
+
userId: extractPopuserUserId(anchor.getAttribute('href') ?? ''),
|
|
858
|
+
}))
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function extractPopuserUserId(href: string): string {
|
|
862
|
+
// javascript: popuser('113889210806408397ea7db2ea855f71')
|
|
863
|
+
const match = href.match(/popuser\(\s*'([^']*)'/)
|
|
864
|
+
return match ? match[1] : ''
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function extractIctCategory(items: HTMLElement[], group: '대' | '중'): string {
|
|
868
|
+
const item = items.find((node) => cleanText(node).includes(`(${group})`))
|
|
869
|
+
return cleanText(item?.querySelector('span'))
|
|
611
870
|
}
|
|
612
871
|
|
|
613
872
|
function extractJoinStatus(card: HTMLElement): string {
|
|
614
|
-
return (
|
|
615
|
-
cleanText(card.querySelector('button')) ||
|
|
616
|
-
cleanText(card.querySelector('.btn')) ||
|
|
617
|
-
cleanText(card.querySelector('[class*="join"]')) ||
|
|
618
|
-
''
|
|
619
|
-
)
|
|
873
|
+
return cleanText(card.querySelector('.bot .btn_w .btn-team'))
|
|
620
874
|
}
|
|
621
875
|
|
|
622
876
|
function extractTrailingStatus(text: string): string {
|