opensoma 0.1.2 → 0.2.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 +18 -2
- package/dist/src/client.d.ts +8 -0
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +123 -21
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands/auth.d.ts +8 -0
- package/dist/src/commands/auth.d.ts.map +1 -1
- package/dist/src/commands/auth.js +35 -23
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/dashboard.d.ts.map +1 -1
- package/dist/src/commands/dashboard.js +1 -1
- package/dist/src/commands/dashboard.js.map +1 -1
- package/dist/src/commands/event.d.ts.map +1 -1
- package/dist/src/commands/event.js.map +1 -1
- package/dist/src/commands/helpers.d.ts.map +1 -1
- package/dist/src/commands/helpers.js +12 -2
- package/dist/src/commands/helpers.js.map +1 -1
- package/dist/src/commands/member.d.ts.map +1 -1
- package/dist/src/commands/member.js.map +1 -1
- package/dist/src/commands/mentoring.d.ts.map +1 -1
- package/dist/src/commands/mentoring.js +14 -5
- 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.map +1 -1
- package/dist/src/commands/room.d.ts.map +1 -1
- package/dist/src/commands/room.js.map +1 -1
- package/dist/src/commands/team.d.ts.map +1 -1
- package/dist/src/commands/team.js.map +1 -1
- package/dist/src/errors.d.ts +8 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +11 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/formatters.d.ts.map +1 -1
- package/dist/src/formatters.js +54 -7
- package/dist/src/formatters.js.map +1 -1
- package/dist/src/http.d.ts +5 -0
- package/dist/src/http.d.ts.map +1 -1
- package/dist/src/http.js +140 -6
- package/dist/src/http.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/shared/utils/mentoring-params.d.ts.map +1 -1
- package/dist/src/shared/utils/mentoring-params.js +4 -1
- package/dist/src/shared/utils/mentoring-params.js.map +1 -1
- package/dist/src/shared/utils/swmaestro.d.ts.map +1 -1
- package/dist/src/shared/utils/swmaestro.js.map +1 -1
- package/dist/src/token-extractor.d.ts +12 -0
- package/dist/src/token-extractor.d.ts.map +1 -1
- package/dist/src/token-extractor.js +83 -18
- package/dist/src/token-extractor.js.map +1 -1
- package/dist/src/types.d.ts +17 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -1
- package/package.json +17 -1
- package/src/client.test.ts +112 -12
- package/src/client.ts +136 -36
- package/src/commands/auth.test.ts +55 -0
- package/src/commands/auth.ts +57 -33
- package/src/commands/dashboard.ts +5 -6
- package/src/commands/event.ts +5 -6
- package/src/commands/helpers.ts +21 -4
- package/src/commands/member.ts +4 -5
- package/src/commands/mentoring.ts +36 -19
- package/src/commands/notice.ts +4 -5
- package/src/commands/room.ts +4 -5
- package/src/commands/team.ts +4 -5
- package/src/credential-manager.test.ts +1 -1
- package/src/credential-manager.ts +1 -1
- package/src/errors.ts +10 -0
- package/src/formatters.test.ts +1 -1
- package/src/formatters.ts +91 -18
- package/src/http.test.ts +43 -7
- package/src/http.ts +174 -8
- package/src/index.ts +1 -0
- package/src/shared/utils/mentoring-params.test.ts +9 -4
- package/src/shared/utils/mentoring-params.ts +6 -3
- package/src/shared/utils/swmaestro.ts +2 -2
- package/src/token-extractor.test.ts +84 -8
- package/src/token-extractor.ts +118 -22
- package/src/types.test.ts +4 -2
- package/src/types.ts +6 -0
- package/.claude-plugin/README.md +0 -145
- package/.claude-plugin/plugin.json +0 -23
- package/.github/workflows/release.yml +0 -86
- package/.oxfmtrc.json +0 -9
- package/.oxlintrc.json +0 -4
- package/AGENTS.md +0 -78
- package/README.md +0 -252
- package/bun.lock +0 -297
- package/bunfig.toml +0 -2
- package/e2e/.gitkeep +0 -0
- package/skills/opensoma/SKILL.md +0 -345
- package/skills/opensoma/references/common-patterns.md +0 -182
- package/skills/opensoma/references/output-format.md +0 -130
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
|
|
3
|
-
import { MENU_NO } from '
|
|
4
|
-
import * as formatters from '
|
|
5
|
-
import { handleError } from '
|
|
6
|
-
import {
|
|
7
|
-
|
|
3
|
+
import { MENU_NO } from '../constants'
|
|
4
|
+
import * as formatters from '../formatters'
|
|
5
|
+
import { handleError } from '../shared/utils/error-handler'
|
|
6
|
+
import { buildMentoringListParams } from '../shared/utils/mentoring-params'
|
|
7
|
+
import { formatOutput } from '../shared/utils/output'
|
|
8
8
|
import { getHttpOrExit } from './helpers'
|
|
9
|
-
import { buildMentoringListParams } from '@/shared/utils/mentoring-params'
|
|
10
9
|
|
|
11
10
|
type ShowOptions = { pretty?: boolean }
|
|
12
11
|
|
package/src/commands/event.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
|
|
3
|
-
import { MENU_NO } from '
|
|
4
|
-
import * as formatters from '
|
|
5
|
-
import { handleError } from '
|
|
6
|
-
import { formatOutput } from '
|
|
7
|
-
import { buildApplicationPayload, parseEventDetail } from '
|
|
8
|
-
|
|
3
|
+
import { MENU_NO } from '../constants'
|
|
4
|
+
import * as formatters from '../formatters'
|
|
5
|
+
import { handleError } from '../shared/utils/error-handler'
|
|
6
|
+
import { formatOutput } from '../shared/utils/output'
|
|
7
|
+
import { buildApplicationPayload, parseEventDetail } from '../shared/utils/swmaestro'
|
|
9
8
|
import { getHttpOrExit } from './helpers'
|
|
10
9
|
|
|
11
10
|
type ListOptions = { page?: string; pretty?: boolean }
|
package/src/commands/helpers.ts
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
|
-
import { CredentialManager } from '
|
|
2
|
-
import { SomaHttp } from '
|
|
1
|
+
import { CredentialManager } from '../credential-manager'
|
|
2
|
+
import { SomaHttp } from '../http'
|
|
3
3
|
|
|
4
4
|
export async function getHttpOrExit(): Promise<SomaHttp> {
|
|
5
5
|
const manager = new CredentialManager()
|
|
6
6
|
const creds = await manager.getCredentials()
|
|
7
7
|
if (!creds) {
|
|
8
|
-
console.error(
|
|
8
|
+
console.error(
|
|
9
|
+
JSON.stringify({
|
|
10
|
+
error: 'Not logged in. Run: opensoma auth login or opensoma auth extract',
|
|
11
|
+
}),
|
|
12
|
+
)
|
|
9
13
|
process.exit(1)
|
|
10
14
|
}
|
|
11
|
-
|
|
15
|
+
|
|
16
|
+
const http = new SomaHttp({ sessionCookie: creds.sessionCookie, csrfToken: creds.csrfToken })
|
|
17
|
+
|
|
18
|
+
const identity = await http.checkLogin()
|
|
19
|
+
if (!identity) {
|
|
20
|
+
console.error(
|
|
21
|
+
JSON.stringify({
|
|
22
|
+
error: 'Session expired. Run: opensoma auth login or opensoma auth extract',
|
|
23
|
+
}),
|
|
24
|
+
)
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return http
|
|
12
29
|
}
|
package/src/commands/member.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
|
|
3
|
-
import { MENU_NO } from '
|
|
4
|
-
import * as formatters from '
|
|
5
|
-
import { handleError } from '
|
|
6
|
-
import { formatOutput } from '
|
|
7
|
-
|
|
3
|
+
import { MENU_NO } from '../constants'
|
|
4
|
+
import * as formatters from '../formatters'
|
|
5
|
+
import { handleError } from '../shared/utils/error-handler'
|
|
6
|
+
import { formatOutput } from '../shared/utils/output'
|
|
8
7
|
import { getHttpOrExit } from './helpers'
|
|
9
8
|
|
|
10
9
|
type ShowOptions = { pretty?: boolean }
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
|
|
3
|
-
import { MENU_NO } from '
|
|
4
|
-
import * as formatters from '
|
|
5
|
-
import { handleError } from '
|
|
6
|
-
import {
|
|
3
|
+
import { MENU_NO } from '../constants'
|
|
4
|
+
import * as formatters from '../formatters'
|
|
5
|
+
import { handleError } from '../shared/utils/error-handler'
|
|
6
|
+
import { buildMentoringListParams, parseSearchQuery } from '../shared/utils/mentoring-params'
|
|
7
|
+
import { formatOutput } from '../shared/utils/output'
|
|
7
8
|
import {
|
|
8
9
|
buildApplicationPayload,
|
|
9
10
|
buildCancelApplicationPayload,
|
|
10
11
|
buildDeleteMentoringPayload,
|
|
11
12
|
buildMentoringPayload,
|
|
12
|
-
} from '
|
|
13
|
-
|
|
13
|
+
} from '../shared/utils/swmaestro'
|
|
14
14
|
import { getHttpOrExit } from './helpers'
|
|
15
|
-
import { buildMentoringListParams, parseSearchQuery } from '@/shared/utils/mentoring-params'
|
|
16
15
|
|
|
17
|
-
type ListOptions = {
|
|
16
|
+
type ListOptions = {
|
|
17
|
+
status?: string
|
|
18
|
+
type?: string
|
|
19
|
+
search?: string
|
|
20
|
+
page?: string
|
|
21
|
+
pretty?: boolean
|
|
22
|
+
}
|
|
18
23
|
type GetOptions = { pretty?: boolean }
|
|
19
24
|
type CreateOptions = {
|
|
20
25
|
title: string
|
|
@@ -36,17 +41,23 @@ async function listAction(options: ListOptions): Promise<void> {
|
|
|
36
41
|
try {
|
|
37
42
|
const http = await getHttpOrExit()
|
|
38
43
|
const search = options.search ? parseSearchQuery(options.search) : undefined
|
|
39
|
-
const user = search?.me ? (await http.checkLogin()) ?? undefined : undefined
|
|
40
|
-
const html = await http.get(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const user = search?.me ? ((await http.checkLogin()) ?? undefined) : undefined
|
|
45
|
+
const html = await http.get(
|
|
46
|
+
'/mypage/mentoLec/list.do',
|
|
47
|
+
buildMentoringListParams({
|
|
48
|
+
status: options.status,
|
|
49
|
+
type: options.type,
|
|
50
|
+
page: options.page,
|
|
51
|
+
search,
|
|
52
|
+
user,
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
47
55
|
console.log(
|
|
48
56
|
formatOutput(
|
|
49
|
-
{
|
|
57
|
+
{
|
|
58
|
+
items: formatters.parseMentoringList(html),
|
|
59
|
+
pagination: formatters.parsePagination(html),
|
|
60
|
+
},
|
|
50
61
|
options.pretty,
|
|
51
62
|
),
|
|
52
63
|
)
|
|
@@ -58,7 +69,10 @@ async function listAction(options: ListOptions): Promise<void> {
|
|
|
58
69
|
async function getAction(id: string, options: GetOptions): Promise<void> {
|
|
59
70
|
try {
|
|
60
71
|
const http = await getHttpOrExit()
|
|
61
|
-
const html = await http.get('/mypage/mentoLec/view.do', {
|
|
72
|
+
const html = await http.get('/mypage/mentoLec/view.do', {
|
|
73
|
+
menuNo: MENU_NO.MENTORING,
|
|
74
|
+
qustnrSn: id,
|
|
75
|
+
})
|
|
62
76
|
console.log(formatOutput(formatters.parseMentoringDetail(html, Number.parseInt(id, 10)), options.pretty))
|
|
63
77
|
} catch (error) {
|
|
64
78
|
handleError(error)
|
|
@@ -134,7 +148,10 @@ async function historyAction(options: HistoryOptions): Promise<void> {
|
|
|
134
148
|
})
|
|
135
149
|
console.log(
|
|
136
150
|
formatOutput(
|
|
137
|
-
{
|
|
151
|
+
{
|
|
152
|
+
items: formatters.parseApplicationHistory(html),
|
|
153
|
+
pagination: formatters.parsePagination(html),
|
|
154
|
+
},
|
|
138
155
|
options.pretty,
|
|
139
156
|
),
|
|
140
157
|
)
|
package/src/commands/notice.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
|
|
3
|
-
import { MENU_NO } from '
|
|
4
|
-
import * as formatters from '
|
|
5
|
-
import { handleError } from '
|
|
6
|
-
import { formatOutput } from '
|
|
7
|
-
|
|
3
|
+
import { MENU_NO } from '../constants'
|
|
4
|
+
import * as formatters from '../formatters'
|
|
5
|
+
import { handleError } from '../shared/utils/error-handler'
|
|
6
|
+
import { formatOutput } from '../shared/utils/output'
|
|
8
7
|
import { getHttpOrExit } from './helpers'
|
|
9
8
|
|
|
10
9
|
type ListOptions = { page?: string; pretty?: boolean }
|
package/src/commands/room.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
|
|
3
|
-
import * as formatters from '
|
|
4
|
-
import { handleError } from '
|
|
5
|
-
import { formatOutput } from '
|
|
6
|
-
import { buildRoomReservationPayload, resolveRoomId } from '
|
|
7
|
-
|
|
3
|
+
import * as formatters from '../formatters'
|
|
4
|
+
import { handleError } from '../shared/utils/error-handler'
|
|
5
|
+
import { formatOutput } from '../shared/utils/output'
|
|
6
|
+
import { buildRoomReservationPayload, resolveRoomId } from '../shared/utils/swmaestro'
|
|
8
7
|
import { getHttpOrExit } from './helpers'
|
|
9
8
|
|
|
10
9
|
type ListOptions = { date?: string; room?: string; pretty?: boolean }
|
package/src/commands/team.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
|
|
3
|
-
import { MENU_NO } from '
|
|
4
|
-
import * as formatters from '
|
|
5
|
-
import { handleError } from '
|
|
6
|
-
import { formatOutput } from '
|
|
7
|
-
|
|
3
|
+
import { MENU_NO } from '../constants'
|
|
4
|
+
import * as formatters from '../formatters'
|
|
5
|
+
import { handleError } from '../shared/utils/error-handler'
|
|
6
|
+
import { formatOutput } from '../shared/utils/output'
|
|
8
7
|
import { getHttpOrExit } from './helpers'
|
|
9
8
|
|
|
10
9
|
type ShowOptions = { pretty?: boolean }
|
|
@@ -3,7 +3,7 @@ import { mkdtemp, stat } from 'node:fs/promises'
|
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
|
|
6
|
-
import { CredentialManager } from '
|
|
6
|
+
import { CredentialManager } from './credential-manager'
|
|
7
7
|
|
|
8
8
|
let createdDirs: string[] = []
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
|
|
6
|
-
import type { Credentials } from '
|
|
6
|
+
import type { Credentials } from './types'
|
|
7
7
|
|
|
8
8
|
export interface CredentialConfig {
|
|
9
9
|
credentials: Credentials | null
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when authentication is required but not valid.
|
|
3
|
+
* Provides a clear message indicating the need to authenticate.
|
|
4
|
+
*/
|
|
5
|
+
export class AuthenticationError extends Error {
|
|
6
|
+
constructor(message = 'Authentication required. Please login with: opensoma auth login or opensoma auth extract') {
|
|
7
|
+
super(message)
|
|
8
|
+
this.name = 'AuthenticationError'
|
|
9
|
+
}
|
|
10
|
+
}
|
package/src/formatters.test.ts
CHANGED
package/src/formatters.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
RoomCardSchema,
|
|
24
24
|
type TeamInfo,
|
|
25
25
|
TeamInfoSchema,
|
|
26
|
-
} from '
|
|
26
|
+
} from './types'
|
|
27
27
|
|
|
28
28
|
type LabelMap = Record<string, string>
|
|
29
29
|
|
|
@@ -53,17 +53,26 @@ export function parseMentoringDetail(html: string, id = 0): MentoringDetail {
|
|
|
53
53
|
const labels = { ...extractLabelMap(root), ...extractGroupMap(root) }
|
|
54
54
|
const rawTitle = labels['모집 명'] || labels['제목'] || cleanText(root.querySelector('h1, h2, .title'))
|
|
55
55
|
const dateText = labels['강의날짜'] || labels['진행날짜'] || ''
|
|
56
|
-
const contentNode =
|
|
56
|
+
const contentNode =
|
|
57
|
+
root.querySelector('.cont') ??
|
|
58
|
+
root.querySelector('.board-view-content') ??
|
|
59
|
+
root.querySelector('.view-content') ??
|
|
60
|
+
root.querySelector('.content-body') ??
|
|
61
|
+
root.querySelector('#contents')
|
|
57
62
|
|
|
58
63
|
return MentoringDetailSchema.parse({
|
|
59
64
|
id: id || extractNumber(root.querySelector('[name="qustnrSn"]')?.getAttribute('value') ?? ''),
|
|
60
65
|
title: stripMentoringStatus(stripMentoringPrefix(rawTitle)),
|
|
61
|
-
type: extractMentoringType(
|
|
66
|
+
type: extractMentoringType(
|
|
67
|
+
labels['유형'] || root.querySelector('[name="reportCd"]')?.getAttribute('value') || rawTitle,
|
|
68
|
+
),
|
|
62
69
|
registrationPeriod: extractDateRange(labels['접수 기간'] || labels['접수기간'] || ''),
|
|
63
70
|
sessionDate: extractFirstDate(dateText),
|
|
64
71
|
sessionTime: extractTimeRange(dateText),
|
|
65
72
|
attendees: {
|
|
66
|
-
current: extractNumber(
|
|
73
|
+
current: extractNumber(
|
|
74
|
+
labels['신청인원'] || labels['현재인원'] || cleanText(root.querySelector('.total-normal')) || '',
|
|
75
|
+
),
|
|
67
76
|
max: extractNumber(labels['모집인원'] || labels['수강인원'] || ''),
|
|
68
77
|
},
|
|
69
78
|
approved: /OK/i.test(labels['개설 승인'] || labels['개설승인'] || ''),
|
|
@@ -161,7 +170,12 @@ export function parseNoticeDetail(html: string, id = 0): NoticeDetail {
|
|
|
161
170
|
const spans = top?.querySelectorAll('.etc span') ?? []
|
|
162
171
|
const author = extractPrefixedValue(spans, '작성자') || labels['작성자'] || ''
|
|
163
172
|
const createdAt = extractPrefixedValue(spans, '등록일') || labels['등록일'] || ''
|
|
164
|
-
const contentNode =
|
|
173
|
+
const contentNode =
|
|
174
|
+
root.querySelector('.bbs-view .cont') ??
|
|
175
|
+
root.querySelector('.board-view-content') ??
|
|
176
|
+
root.querySelector('.view-content') ??
|
|
177
|
+
root.querySelector('.content-body') ??
|
|
178
|
+
root.querySelector('#contents')
|
|
165
179
|
|
|
166
180
|
return NoticeDetailSchema.parse({
|
|
167
181
|
id: id || extractNumber(root.querySelector('[name="nttId"]')?.getAttribute('value') ?? ''),
|
|
@@ -314,28 +328,42 @@ function extractGroupMap(root: HTMLElement): LabelMap {
|
|
|
314
328
|
function parseDashboardLinks(
|
|
315
329
|
root: HTMLElement,
|
|
316
330
|
predicate: (href: string) => boolean,
|
|
317
|
-
): Array<{
|
|
331
|
+
): Array<{
|
|
332
|
+
title: string
|
|
333
|
+
url: string
|
|
334
|
+
status: string
|
|
335
|
+
date?: string
|
|
336
|
+
time?: string
|
|
337
|
+
venue?: string
|
|
338
|
+
}> {
|
|
318
339
|
return root
|
|
319
340
|
.querySelectorAll('ul.bbs-dash_w a')
|
|
320
341
|
.filter((link) => predicate(link.getAttribute('href') ?? ''))
|
|
321
342
|
.map((link) => {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
343
|
+
const text = cleanText(link)
|
|
344
|
+
const { cleanTitle, date, time, venue } = extractDateTimeFromTitle(text)
|
|
345
|
+
return {
|
|
346
|
+
title: stripTrailingStatus(cleanTitle),
|
|
347
|
+
url: link.getAttribute('href') ?? '',
|
|
348
|
+
status: extractTrailingStatus(text),
|
|
349
|
+
date,
|
|
350
|
+
time,
|
|
351
|
+
venue,
|
|
352
|
+
}
|
|
353
|
+
})
|
|
329
354
|
}
|
|
330
355
|
|
|
331
356
|
function parseTimeSlotsFromRoot(root: HTMLElement): RoomCard['timeSlots'] {
|
|
332
357
|
const grid = root.querySelector('.time-grid')
|
|
333
|
-
const spans = grid
|
|
358
|
+
const spans = grid
|
|
359
|
+
? grid.querySelectorAll('span')
|
|
360
|
+
: root.querySelectorAll('.time-grid span, [class*="time-slot"], .slot')
|
|
334
361
|
|
|
335
362
|
return spans
|
|
336
363
|
.map((slot) => ({
|
|
337
364
|
time: cleanText(slot),
|
|
338
|
-
available:
|
|
365
|
+
available:
|
|
366
|
+
!(slot.getAttribute('class') ?? '').includes('not-reserve') &&
|
|
339
367
|
!(slot.getAttribute('class') ?? '').includes('booked') &&
|
|
340
368
|
!(slot.getAttribute('class') ?? '').includes('disabled'),
|
|
341
369
|
}))
|
|
@@ -349,12 +377,16 @@ function findDashboardValue(items: HTMLElement[], label: string): string {
|
|
|
349
377
|
|
|
350
378
|
function extractDashEtcValue(container: HTMLElement | null | undefined, label: string): string {
|
|
351
379
|
const match = (container?.querySelectorAll('span') ?? []).find((item) => cleanText(item).startsWith(label))
|
|
352
|
-
return cleanText(match)
|
|
380
|
+
return cleanText(match)
|
|
381
|
+
.replace(new RegExp(`^${escapeRegex(label)}\\s*:`), '')
|
|
382
|
+
.trim()
|
|
353
383
|
}
|
|
354
384
|
|
|
355
385
|
function findListText(card: HTMLElement, label: string): string {
|
|
356
386
|
const item = card.querySelectorAll('.txt > li').find((entry) => cleanText(entry).startsWith(label))
|
|
357
|
-
return cleanText(item)
|
|
387
|
+
return cleanText(item)
|
|
388
|
+
.replace(new RegExp(`^${escapeRegex(label)}\\s*:`), '')
|
|
389
|
+
.trim()
|
|
358
390
|
}
|
|
359
391
|
|
|
360
392
|
function extractLocationHref(onclick: string | undefined): string {
|
|
@@ -440,6 +472,45 @@ function extractTrailingStatus(text: string): string {
|
|
|
440
472
|
return match?.[1] ?? ''
|
|
441
473
|
}
|
|
442
474
|
|
|
475
|
+
function extractDateTimeFromTitle(text: string): {
|
|
476
|
+
cleanTitle: string
|
|
477
|
+
date?: string
|
|
478
|
+
time?: string
|
|
479
|
+
venue?: string
|
|
480
|
+
} {
|
|
481
|
+
// Match date patterns: 2025-04-15 or 2025.04.15
|
|
482
|
+
const dateMatch = text.match(/(\d{4}[.-]\d{2}[.-]\d{2})/)
|
|
483
|
+
// Match time patterns: 14:00~16:00 or 14:00
|
|
484
|
+
const timeMatch = text.match(/(\d{2}:\d{2}(?:~\d{2}:\d{2})?)/)
|
|
485
|
+
// Match venue patterns: 스페이스 A1, A1, 강의실, etc.
|
|
486
|
+
const venueMatch = text.match(/(스페이스\s*[A-Z]\d+|강의실\s*\d+|회의실\s*[A-Z]?\d+|A\d|B\d|C\d)/i)
|
|
487
|
+
|
|
488
|
+
let cleanTitle = text
|
|
489
|
+
let date: string | undefined
|
|
490
|
+
let time: string | undefined
|
|
491
|
+
let venue: string | undefined
|
|
492
|
+
|
|
493
|
+
if (dateMatch) {
|
|
494
|
+
date = dateMatch[1].replace(/\./g, '-')
|
|
495
|
+
cleanTitle = cleanTitle.replace(dateMatch[0], '')
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (timeMatch) {
|
|
499
|
+
time = timeMatch[1]
|
|
500
|
+
cleanTitle = cleanTitle.replace(timeMatch[0], '')
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (venueMatch) {
|
|
504
|
+
venue = venueMatch[1]
|
|
505
|
+
cleanTitle = cleanTitle.replace(venueMatch[0], '')
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Clean up remaining whitespace and punctuation
|
|
509
|
+
cleanTitle = cleanTitle.replace(/^[\s\-~]+|[\s\-~]+$/g, '').trim()
|
|
510
|
+
|
|
511
|
+
return { cleanTitle, date, time, venue }
|
|
512
|
+
}
|
|
513
|
+
|
|
443
514
|
function stripTrailingStatus(text: string): string {
|
|
444
515
|
return text.replace(/\s*(예약완료|예약중|대기|접수중|마감|승인완료|신청완료)$/, '').trim()
|
|
445
516
|
}
|
|
@@ -467,7 +538,9 @@ function escapeRegex(text: string): string {
|
|
|
467
538
|
|
|
468
539
|
function extractPrefixedValue(nodes: HTMLElement[], label: string): string {
|
|
469
540
|
const match = nodes.find((node) => cleanText(node).includes(label))
|
|
470
|
-
return cleanText(match)
|
|
541
|
+
return cleanText(match)
|
|
542
|
+
.replace(new RegExp(`^${escapeRegex(label)}\\s*:`), '')
|
|
543
|
+
.trim()
|
|
471
544
|
}
|
|
472
545
|
|
|
473
546
|
function normalizeDate(value: string): string {
|
package/src/http.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, mock, test } from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import { MENU_NO } from '
|
|
4
|
-
import { SomaHttp } from '
|
|
3
|
+
import { MENU_NO } from './constants'
|
|
4
|
+
import { SomaHttp } from './http'
|
|
5
5
|
|
|
6
6
|
const originalFetch = globalThis.fetch
|
|
7
7
|
|
|
@@ -12,8 +12,13 @@ afterEach(() => {
|
|
|
12
12
|
|
|
13
13
|
describe('SomaHttp', () => {
|
|
14
14
|
test('get sends query params and stores cookies', async () => {
|
|
15
|
-
const fetchMock = mock(async (input: RequestInfo | URL) => {
|
|
15
|
+
const fetchMock = mock(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
16
16
|
expect(String(input)).toBe(`https://www.swmaestro.ai/sw/member/user/forLogin.do?menuNo=${MENU_NO.LOGIN}`)
|
|
17
|
+
expect(init?.headers).toEqual({
|
|
18
|
+
'Accept-Language': 'ko,en-US;q=0.9,en;q=0.8',
|
|
19
|
+
'User-Agent':
|
|
20
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36',
|
|
21
|
+
})
|
|
17
22
|
return createResponse('<html></html>', ['JSESSIONID=session-1; Path=/', 'XSRF-TOKEN=csrf-1; Path=/'])
|
|
18
23
|
})
|
|
19
24
|
globalThis.fetch = fetchMock as typeof fetch
|
|
@@ -30,6 +35,9 @@ describe('SomaHttp', () => {
|
|
|
30
35
|
const fetchMock = mock(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
31
36
|
expect(init?.method).toBe('POST')
|
|
32
37
|
expect(init?.headers).toEqual({
|
|
38
|
+
'Accept-Language': 'ko,en-US;q=0.9,en;q=0.8',
|
|
39
|
+
'User-Agent':
|
|
40
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36',
|
|
33
41
|
cookie: 'JSESSIONID=session-1',
|
|
34
42
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
35
43
|
})
|
|
@@ -47,6 +55,9 @@ describe('SomaHttp', () => {
|
|
|
47
55
|
test('postJson returns parsed json', async () => {
|
|
48
56
|
const fetchMock = mock(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
49
57
|
expect(init?.headers).toEqual({
|
|
58
|
+
'Accept-Language': 'ko,en-US;q=0.9,en;q=0.8',
|
|
59
|
+
'User-Agent':
|
|
60
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36',
|
|
50
61
|
cookie: 'JSESSIONID=session-1',
|
|
51
62
|
Accept: 'application/json',
|
|
52
63
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -56,7 +67,9 @@ describe('SomaHttp', () => {
|
|
|
56
67
|
globalThis.fetch = fetchMock as typeof fetch
|
|
57
68
|
|
|
58
69
|
const http = new SomaHttp({ sessionCookie: 'session-1', csrfToken: 'csrf-1' })
|
|
59
|
-
const json = await http.postJson<{ resultCode: string }>('/mypage/officeMng/rentTime.do', {
|
|
70
|
+
const json = await http.postJson<{ resultCode: string }>('/mypage/officeMng/rentTime.do', {
|
|
71
|
+
itemId: '17',
|
|
72
|
+
})
|
|
60
73
|
|
|
61
74
|
expect(json).toEqual({ resultCode: 'SUCCESS' })
|
|
62
75
|
})
|
|
@@ -67,6 +80,11 @@ describe('SomaHttp', () => {
|
|
|
67
80
|
|
|
68
81
|
if (url.includes('/forLogin.do')) {
|
|
69
82
|
expect(init?.method).toBe('GET')
|
|
83
|
+
expect(init?.headers).toEqual({
|
|
84
|
+
'Accept-Language': 'ko,en-US;q=0.9,en;q=0.8',
|
|
85
|
+
'User-Agent':
|
|
86
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36',
|
|
87
|
+
})
|
|
70
88
|
return createResponse('<form><input type="hidden" name="csrfToken" value="csrf-login"></form>', [
|
|
71
89
|
'JSESSIONID=session-2; Path=/',
|
|
72
90
|
])
|
|
@@ -75,6 +93,9 @@ describe('SomaHttp', () => {
|
|
|
75
93
|
expect(url).toBe('https://www.swmaestro.ai/sw/member/user/toLogin.do')
|
|
76
94
|
expect(init?.method).toBe('POST')
|
|
77
95
|
expect(init?.headers).toEqual({
|
|
96
|
+
'Accept-Language': 'ko,en-US;q=0.9,en;q=0.8',
|
|
97
|
+
'User-Agent':
|
|
98
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36',
|
|
78
99
|
cookie: 'JSESSIONID=session-2',
|
|
79
100
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
80
101
|
})
|
|
@@ -96,16 +117,31 @@ describe('SomaHttp', () => {
|
|
|
96
117
|
})
|
|
97
118
|
|
|
98
119
|
test('checkLogin returns user identity when logged in, null otherwise', async () => {
|
|
99
|
-
const loggedInMock = mock(async () =>
|
|
120
|
+
const loggedInMock = mock(async (_input: RequestInfo | URL, init?: RequestInit) =>
|
|
100
121
|
createResponse(
|
|
101
|
-
JSON.stringify({
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
resultCode: 'fail',
|
|
124
|
+
userVO: { userId: 'user@example.com', userNm: 'Test' },
|
|
125
|
+
}),
|
|
102
126
|
[],
|
|
103
127
|
'application/json',
|
|
104
128
|
),
|
|
105
129
|
)
|
|
106
130
|
globalThis.fetch = loggedInMock as typeof fetch
|
|
107
131
|
|
|
108
|
-
await expect(new SomaHttp().checkLogin()).resolves.toEqual({
|
|
132
|
+
await expect(new SomaHttp().checkLogin()).resolves.toEqual({
|
|
133
|
+
userId: 'user@example.com',
|
|
134
|
+
userNm: 'Test',
|
|
135
|
+
})
|
|
136
|
+
expect(loggedInMock).toHaveBeenCalledWith('https://www.swmaestro.ai/sw/member/user/checkLogin.json', {
|
|
137
|
+
method: 'GET',
|
|
138
|
+
headers: {
|
|
139
|
+
'Accept-Language': 'ko,en-US;q=0.9,en;q=0.8',
|
|
140
|
+
'User-Agent':
|
|
141
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36',
|
|
142
|
+
Accept: 'application/json',
|
|
143
|
+
},
|
|
144
|
+
})
|
|
109
145
|
|
|
110
146
|
const notLoggedInMock = mock(async () =>
|
|
111
147
|
createResponse(JSON.stringify({ resultCode: 'fail', userVO: { userId: '', userSn: 0 } }), [], 'application/json'),
|