opensoma 0.1.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/.claude-plugin/README.md +145 -0
- package/.claude-plugin/plugin.json +23 -0
- package/.github/workflows/release.yml +86 -0
- package/.oxfmtrc.json +9 -0
- package/.oxlintrc.json +4 -0
- package/AGENTS.md +78 -0
- package/README.md +249 -0
- package/bun.lock +297 -0
- package/bunfig.toml +2 -0
- package/dist/package.json +56 -0
- package/dist/src/cli.d.ts +5 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +39 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/client.d.ts +98 -0
- package/dist/src/client.d.ts.map +1 -0
- package/dist/src/client.js +141 -0
- package/dist/src/client.js.map +1 -0
- package/dist/src/commands/auth.d.ts +3 -0
- package/dist/src/commands/auth.d.ts.map +1 -0
- package/dist/src/commands/auth.js +125 -0
- package/dist/src/commands/auth.js.map +1 -0
- package/dist/src/commands/dashboard.d.ts +3 -0
- package/dist/src/commands/dashboard.d.ts.map +1 -0
- package/dist/src/commands/dashboard.js +33 -0
- package/dist/src/commands/dashboard.js.map +1 -0
- package/dist/src/commands/event.d.ts +3 -0
- package/dist/src/commands/event.d.ts.map +1 -0
- package/dist/src/commands/event.js +58 -0
- package/dist/src/commands/event.js.map +1 -0
- package/dist/src/commands/helpers.d.ts +3 -0
- package/dist/src/commands/helpers.d.ts.map +1 -0
- package/dist/src/commands/helpers.js +12 -0
- package/dist/src/commands/helpers.js.map +1 -0
- package/dist/src/commands/index.d.ts +9 -0
- package/dist/src/commands/index.d.ts.map +1 -0
- package/dist/src/commands/index.js +9 -0
- package/dist/src/commands/index.js.map +1 -0
- package/dist/src/commands/member.d.ts +3 -0
- package/dist/src/commands/member.d.ts.map +1 -0
- package/dist/src/commands/member.js +23 -0
- package/dist/src/commands/member.js.map +1 -0
- package/dist/src/commands/mentoring.d.ts +3 -0
- package/dist/src/commands/mentoring.d.ts.map +1 -0
- package/dist/src/commands/mentoring.js +154 -0
- package/dist/src/commands/mentoring.js.map +1 -0
- package/dist/src/commands/notice.d.ts +3 -0
- package/dist/src/commands/notice.d.ts.map +1 -0
- package/dist/src/commands/notice.js +42 -0
- package/dist/src/commands/notice.js.map +1 -0
- package/dist/src/commands/room.d.ts +3 -0
- package/dist/src/commands/room.d.ts.map +1 -0
- package/dist/src/commands/room.js +79 -0
- package/dist/src/commands/room.js.map +1 -0
- package/dist/src/commands/team.d.ts +3 -0
- package/dist/src/commands/team.d.ts.map +1 -0
- package/dist/src/commands/team.js +20 -0
- package/dist/src/commands/team.js.map +1 -0
- package/dist/src/constants.d.ts +43 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +62 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/credential-manager.d.ts +15 -0
- package/dist/src/credential-manager.d.ts.map +1 -0
- package/dist/src/credential-manager.js +40 -0
- package/dist/src/credential-manager.js.map +1 -0
- package/dist/src/formatters.d.ts +15 -0
- package/dist/src/formatters.d.ts.map +1 -0
- package/dist/src/formatters.js +382 -0
- package/dist/src/formatters.js.map +1 -0
- package/dist/src/http.d.ts +32 -0
- package/dist/src/http.d.ts.map +1 -0
- package/dist/src/http.js +143 -0
- package/dist/src/http.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +6 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/shared/utils/error-handler.d.ts +2 -0
- package/dist/src/shared/utils/error-handler.d.ts.map +1 -0
- package/dist/src/shared/utils/error-handler.js +7 -0
- package/dist/src/shared/utils/error-handler.js.map +1 -0
- package/dist/src/shared/utils/mentoring-params.d.ts +15 -0
- package/dist/src/shared/utils/mentoring-params.d.ts.map +1 -0
- package/dist/src/shared/utils/mentoring-params.js +39 -0
- package/dist/src/shared/utils/mentoring-params.js.map +1 -0
- package/dist/src/shared/utils/output.d.ts +2 -0
- package/dist/src/shared/utils/output.d.ts.map +1 -0
- package/dist/src/shared/utils/output.js +4 -0
- package/dist/src/shared/utils/output.js.map +1 -0
- package/dist/src/shared/utils/stderr.d.ts +5 -0
- package/dist/src/shared/utils/stderr.d.ts.map +1 -0
- package/dist/src/shared/utils/stderr.js +19 -0
- package/dist/src/shared/utils/stderr.js.map +1 -0
- package/dist/src/shared/utils/swmaestro.d.ts +33 -0
- package/dist/src/shared/utils/swmaestro.d.ts.map +1 -0
- package/dist/src/shared/utils/swmaestro.js +164 -0
- package/dist/src/shared/utils/swmaestro.js.map +1 -0
- package/dist/src/token-extractor.d.ts +23 -0
- package/dist/src/token-extractor.d.ts.map +1 -0
- package/dist/src/token-extractor.js +163 -0
- package/dist/src/token-extractor.js.map +1 -0
- package/dist/src/types.d.ts +176 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +110 -0
- package/dist/src/types.js.map +1 -0
- package/e2e/.gitkeep +0 -0
- package/package.json +56 -0
- package/scripts/postbuild.ts +11 -0
- package/scripts/prepublish.ts +9 -0
- package/scripts/test.ts +82 -0
- package/skills/opensoma/SKILL.md +345 -0
- package/skills/opensoma/references/common-patterns.md +182 -0
- package/skills/opensoma/references/output-format.md +130 -0
- package/src/cli.ts +57 -0
- package/src/client.test.ts +210 -0
- package/src/client.ts +264 -0
- package/src/commands/auth.ts +153 -0
- package/src/commands/dashboard.ts +39 -0
- package/src/commands/event.ts +74 -0
- package/src/commands/helpers.ts +12 -0
- package/src/commands/index.ts +8 -0
- package/src/commands/member.ts +29 -0
- package/src/commands/mentoring.ts +209 -0
- package/src/commands/notice.ts +56 -0
- package/src/commands/room.ts +102 -0
- package/src/commands/team.ts +26 -0
- package/src/constants.ts +70 -0
- package/src/credential-manager.test.ts +66 -0
- package/src/credential-manager.ts +52 -0
- package/src/formatters.test.ts +382 -0
- package/src/formatters.ts +489 -0
- package/src/http.test.ts +152 -0
- package/src/http.ts +196 -0
- package/src/index.ts +6 -0
- package/src/shared/utils/error-handler.ts +7 -0
- package/src/shared/utils/mentoring-params.test.ts +112 -0
- package/src/shared/utils/mentoring-params.ts +57 -0
- package/src/shared/utils/output.ts +3 -0
- package/src/shared/utils/stderr.ts +23 -0
- package/src/shared/utils/swmaestro.ts +218 -0
- package/src/token-extractor.test.ts +119 -0
- package/src/token-extractor.ts +205 -0
- package/src/types.test.ts +172 -0
- package/src/types.ts +134 -0
- package/tsconfig.json +38 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdtemp, stat } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { CredentialManager } from '@/credential-manager'
|
|
7
|
+
|
|
8
|
+
let createdDirs: string[] = []
|
|
9
|
+
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
for (const dir of createdDirs) {
|
|
12
|
+
await new CredentialManager(dir).remove()
|
|
13
|
+
}
|
|
14
|
+
createdDirs = []
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('CredentialManager', () => {
|
|
18
|
+
test('loads empty config when file does not exist', async () => {
|
|
19
|
+
const dir = await makeTempDir()
|
|
20
|
+
const manager = new CredentialManager(dir)
|
|
21
|
+
|
|
22
|
+
await expect(manager.load()).resolves.toEqual({ credentials: null })
|
|
23
|
+
await expect(manager.getCredentials()).resolves.toBeNull()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('saves and loads credentials with secure permissions', async () => {
|
|
27
|
+
const dir = await makeTempDir()
|
|
28
|
+
const manager = new CredentialManager(dir)
|
|
29
|
+
|
|
30
|
+
await manager.setCredentials({
|
|
31
|
+
sessionCookie: 'session-value',
|
|
32
|
+
csrfToken: 'csrf-value',
|
|
33
|
+
username: 'neo@example.com',
|
|
34
|
+
loggedInAt: '2026-04-09T00:00:00.000Z',
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
await expect(manager.getCredentials()).resolves.toEqual({
|
|
38
|
+
sessionCookie: 'session-value',
|
|
39
|
+
csrfToken: 'csrf-value',
|
|
40
|
+
username: 'neo@example.com',
|
|
41
|
+
loggedInAt: '2026-04-09T00:00:00.000Z',
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const fileStat = await stat(join(dir, 'credentials.json'))
|
|
45
|
+
expect(fileStat.mode & 0o777).toBe(0o600)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('removes credentials file', async () => {
|
|
49
|
+
const dir = await makeTempDir()
|
|
50
|
+
const manager = new CredentialManager(dir)
|
|
51
|
+
|
|
52
|
+
await manager.setCredentials({
|
|
53
|
+
sessionCookie: 'session-value',
|
|
54
|
+
csrfToken: 'csrf-value',
|
|
55
|
+
})
|
|
56
|
+
await manager.remove()
|
|
57
|
+
|
|
58
|
+
await expect(manager.getCredentials()).resolves.toBeNull()
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
async function makeTempDir(): Promise<string> {
|
|
63
|
+
const dir = await mkdtemp(join(tmpdir(), 'opensoma-credentials-'))
|
|
64
|
+
createdDirs.push(dir)
|
|
65
|
+
return dir
|
|
66
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import type { Credentials } from '@/types'
|
|
7
|
+
|
|
8
|
+
export interface CredentialConfig {
|
|
9
|
+
credentials: Credentials | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class CredentialManager {
|
|
13
|
+
private configDir: string
|
|
14
|
+
private credentialsPath: string
|
|
15
|
+
|
|
16
|
+
constructor(configDir?: string) {
|
|
17
|
+
this.configDir = configDir ?? join(homedir(), '.config', 'opensoma')
|
|
18
|
+
this.credentialsPath = join(this.configDir, 'credentials.json')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async load(): Promise<CredentialConfig> {
|
|
22
|
+
if (!existsSync(this.credentialsPath)) {
|
|
23
|
+
return { credentials: null }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const content = await readFile(this.credentialsPath, 'utf8')
|
|
28
|
+
return JSON.parse(content) as CredentialConfig
|
|
29
|
+
} catch {
|
|
30
|
+
return { credentials: null }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async save(config: CredentialConfig): Promise<void> {
|
|
35
|
+
await mkdir(this.configDir, { recursive: true })
|
|
36
|
+
await writeFile(this.credentialsPath, JSON.stringify(config, null, 2))
|
|
37
|
+
await chmod(this.credentialsPath, 0o600)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async getCredentials(): Promise<Credentials | null> {
|
|
41
|
+
const config = await this.load()
|
|
42
|
+
return config.credentials
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async setCredentials(credentials: Credentials): Promise<void> {
|
|
46
|
+
await this.save({ credentials })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async remove(): Promise<void> {
|
|
50
|
+
await rm(this.credentialsPath, { force: true })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
parseCsrfToken,
|
|
5
|
+
parseDashboard,
|
|
6
|
+
parseApplicationHistory,
|
|
7
|
+
parseEventList,
|
|
8
|
+
parseMemberInfo,
|
|
9
|
+
parseMentoringDetail,
|
|
10
|
+
parseMentoringList,
|
|
11
|
+
parseNoticeDetail,
|
|
12
|
+
parseNoticeList,
|
|
13
|
+
parsePagination,
|
|
14
|
+
parseRoomList,
|
|
15
|
+
parseRoomSlots,
|
|
16
|
+
parseTeamInfo,
|
|
17
|
+
} from '@/formatters'
|
|
18
|
+
|
|
19
|
+
describe('formatters', () => {
|
|
20
|
+
test('parseMentoringList parses real list rows', () => {
|
|
21
|
+
const html = `
|
|
22
|
+
<table>
|
|
23
|
+
<thead><tr><th>NO.</th><th>제목</th><th>접수기간</th><th>진행날짜</th><th>모집인원</th><th>개설승인</th><th>상태</th><th>작성자</th><th>등록일</th></tr></thead>
|
|
24
|
+
<tbody>
|
|
25
|
+
<tr>
|
|
26
|
+
<td>584</td>
|
|
27
|
+
<td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=9482&menuNo=200046&pageIndex=1&searchStatMentolec=">[자유 멘토링] 초기 제품 개발 준비를 위한 전략 가이드 [접수중]</a></td>
|
|
28
|
+
<td>2026-04-08 ~ 2026-04-23</td>
|
|
29
|
+
<td>2026-04-30(목)
|
|
30
|
+
19:00 ~ 22:00</td>
|
|
31
|
+
<td>3 /4</td>
|
|
32
|
+
<td>OK</td>
|
|
33
|
+
<td>[접수중]</td>
|
|
34
|
+
<td>김태성</td>
|
|
35
|
+
<td>2026-04-08</td>
|
|
36
|
+
</tr>
|
|
37
|
+
</tbody>
|
|
38
|
+
</table>
|
|
39
|
+
`
|
|
40
|
+
|
|
41
|
+
expect(parseMentoringList(html)).toEqual([
|
|
42
|
+
{
|
|
43
|
+
id: 9482,
|
|
44
|
+
title: '초기 제품 개발 준비를 위한 전략 가이드',
|
|
45
|
+
type: '자유 멘토링',
|
|
46
|
+
registrationPeriod: { start: '2026-04-08', end: '2026-04-23' },
|
|
47
|
+
sessionDate: '2026-04-30',
|
|
48
|
+
sessionTime: { start: '19:00', end: '22:00' },
|
|
49
|
+
attendees: { current: 3, max: 4 },
|
|
50
|
+
approved: true,
|
|
51
|
+
status: '접수중',
|
|
52
|
+
author: '김태성',
|
|
53
|
+
createdAt: '2026-04-08',
|
|
54
|
+
},
|
|
55
|
+
])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('parseMentoringDetail parses real key-value detail view', () => {
|
|
59
|
+
const html = `
|
|
60
|
+
<input type="hidden" name="reportCd" value="MRC020">
|
|
61
|
+
<div class="top">
|
|
62
|
+
<div class="group"><strong class="t">모집 명</strong><div class="c">[멘토 특강] 웹 성능 특강 [마감]</div></div>
|
|
63
|
+
<div class="group"><strong class="t">상태</strong><div class="c"><strong class="color-red">[마감]</strong></div></div>
|
|
64
|
+
<div class="group"><strong class="t">개설 승인</strong><div class="c"><strong class="color-blue2">OK</strong></div></div>
|
|
65
|
+
<div class="group"><strong class="t">접수 기간</strong><div class="c">2026.04.01 ~ 2026.04.10</div></div>
|
|
66
|
+
<div class="group"><strong class="t">강의날짜</strong><div class="c"><span>2026.04.11 14:00시 ~ 15:30시</span></div></div>
|
|
67
|
+
<div class="group"><strong class="t">장소</strong><div class="c">온라인(Webex)</div></div>
|
|
68
|
+
<div class="group"><strong class="t">모집인원</strong><div class="c">20명</div></div>
|
|
69
|
+
<div class="group"><strong class="t">작성자</strong><div class="c">전수열</div></div>
|
|
70
|
+
<div class="group"><strong class="t">등록일</strong><div class="c">2026.04.01</div></div>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="cont"><p>세션 본문</p></div>
|
|
73
|
+
`
|
|
74
|
+
|
|
75
|
+
expect(parseMentoringDetail(html, 9572)).toEqual({
|
|
76
|
+
id: 9572,
|
|
77
|
+
title: '웹 성능 특강',
|
|
78
|
+
type: '멘토 특강',
|
|
79
|
+
registrationPeriod: { start: '2026-04-01', end: '2026-04-10' },
|
|
80
|
+
sessionDate: '2026-04-11',
|
|
81
|
+
sessionTime: { start: '14:00', end: '15:30' },
|
|
82
|
+
attendees: { current: 0, max: 20 },
|
|
83
|
+
approved: true,
|
|
84
|
+
status: '마감',
|
|
85
|
+
author: '전수열',
|
|
86
|
+
createdAt: '2026.04.01',
|
|
87
|
+
content: '<p>세션 본문</p>',
|
|
88
|
+
venue: '온라인(Webex)',
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('parseRoomList parses real room cards with embedded time slots', () => {
|
|
93
|
+
const html = `
|
|
94
|
+
<ul class="bbs-reserve">
|
|
95
|
+
<li class="item">
|
|
96
|
+
<a href="javascript:void(0);" onclick="location.href='/sw/mypage/officeMng/view.do?menuNo=200058&sdate=2026-04-10&pageIndex=1&itemId=17';">
|
|
97
|
+
<div class="cont">
|
|
98
|
+
<h4 class="tit">스페이스 A1</h4>
|
|
99
|
+
<ul class="txt bul-dot grey">
|
|
100
|
+
<li>이용기간 : 2026-04-06 ~ 2026-04-30</li>
|
|
101
|
+
<li><p>스페이스 A1 회의실 : 4인</p></li>
|
|
102
|
+
<li class="time-list">
|
|
103
|
+
<span>가용시간</span>
|
|
104
|
+
<div class="time-grid">
|
|
105
|
+
<span>09:00</span>
|
|
106
|
+
<span class="not-reserve">09:30</span>
|
|
107
|
+
</div>
|
|
108
|
+
</li>
|
|
109
|
+
</ul>
|
|
110
|
+
</div>
|
|
111
|
+
</a>
|
|
112
|
+
</li>
|
|
113
|
+
</ul>
|
|
114
|
+
`
|
|
115
|
+
|
|
116
|
+
expect(parseRoomList(html)).toEqual([
|
|
117
|
+
{
|
|
118
|
+
itemId: 17,
|
|
119
|
+
name: '스페이스 A1',
|
|
120
|
+
capacity: 4,
|
|
121
|
+
availablePeriod: { start: '2026-04-06', end: '2026-04-30' },
|
|
122
|
+
description: '스페이스 A1 회의실 : 4인',
|
|
123
|
+
timeSlots: [
|
|
124
|
+
{ time: '09:00', available: true },
|
|
125
|
+
{ time: '09:30', available: false },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
])
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('parseRoomSlots parses rentTime fragment', () => {
|
|
132
|
+
const html = `
|
|
133
|
+
<span class="ck-st2" data-hour="09" data-minute="00">
|
|
134
|
+
<input type="checkbox" name="time" id="time1_1" value="1">
|
|
135
|
+
<label for="time1_1">AM 09:00</label>
|
|
136
|
+
</span>
|
|
137
|
+
<input type="hidden" name="chkData_1" value="09:00" />
|
|
138
|
+
<span class="ck-st2 disabled" data-hour="12" data-minute="00">
|
|
139
|
+
<input type="checkbox" name="time" id="time1_7" value="7" disabled="disabled">
|
|
140
|
+
<label for="time1_7">PM 12:00</label>
|
|
141
|
+
</span>
|
|
142
|
+
<input type="hidden" name="chkData_7" value="12:00" />
|
|
143
|
+
`
|
|
144
|
+
|
|
145
|
+
expect(parseRoomSlots(html)).toEqual([
|
|
146
|
+
{ time: '09:00', available: true },
|
|
147
|
+
{ time: '12:00', available: false },
|
|
148
|
+
])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('parseDashboard parses real dashboard sections', () => {
|
|
152
|
+
const html = `
|
|
153
|
+
<ul class="dash-top">
|
|
154
|
+
<li class="dash-card">
|
|
155
|
+
<div class="dash-etc">
|
|
156
|
+
<span>소속 :<br> Indent</span>
|
|
157
|
+
<span>직책 :<br> </span>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="dash-state">
|
|
160
|
+
<div class="top">
|
|
161
|
+
<span class="bg-orange label"><span>멘토</span></span>
|
|
162
|
+
<div class="welcome"><strong>전수열</strong>님 안녕하세요.</div>
|
|
163
|
+
</div>
|
|
164
|
+
<ul class="dash-box">
|
|
165
|
+
<li><strong class="t">팀명</strong> <div class="c">OpenSoma</div></li>
|
|
166
|
+
<li><strong class="t">팀원</strong> <div class="c">김개발, 이개발</div></li>
|
|
167
|
+
<li><strong class="t">멘토</strong> <div class="c">전수열</div></li>
|
|
168
|
+
</ul>
|
|
169
|
+
</div>
|
|
170
|
+
</li>
|
|
171
|
+
</ul>
|
|
172
|
+
<ul class="bbs-dash_w">
|
|
173
|
+
<li>멘토링 · 멘토특강
|
|
174
|
+
<li><a href="/sw/mypage/mentoLec/view.do?qustnrSn=9582">게임 개발 AI 활용법 접수중</a></li>
|
|
175
|
+
</li>
|
|
176
|
+
<li>회의실 예약현황
|
|
177
|
+
<li><a href="/sw/mypage/itemRent/view.do?rentId=17905">OpenCode 하네스 만들어보기 예약완료</a></li>
|
|
178
|
+
</li>
|
|
179
|
+
</ul>
|
|
180
|
+
`
|
|
181
|
+
|
|
182
|
+
expect(parseDashboard(html)).toEqual({
|
|
183
|
+
name: '전수열',
|
|
184
|
+
role: '멘토',
|
|
185
|
+
organization: 'Indent',
|
|
186
|
+
position: '',
|
|
187
|
+
team: {
|
|
188
|
+
name: 'OpenSoma',
|
|
189
|
+
members: '김개발, 이개발',
|
|
190
|
+
mentor: '전수열',
|
|
191
|
+
},
|
|
192
|
+
mentoringSessions: [
|
|
193
|
+
{
|
|
194
|
+
title: '게임 개발 AI 활용법',
|
|
195
|
+
url: '/sw/mypage/mentoLec/view.do?qustnrSn=9582',
|
|
196
|
+
status: '접수중',
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
roomReservations: [
|
|
200
|
+
{
|
|
201
|
+
title: 'OpenCode 하네스 만들어보기',
|
|
202
|
+
url: '/sw/mypage/itemRent/view.do?rentId=17905',
|
|
203
|
+
status: '예약완료',
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('parseNoticeList and parseNoticeDetail parse real notice structures', () => {
|
|
210
|
+
const listHtml = `
|
|
211
|
+
<table>
|
|
212
|
+
<thead><tr><th>NO.</th><th>제목</th><th>작성자</th><th>등록일</th></tr></thead>
|
|
213
|
+
<tbody>
|
|
214
|
+
<tr>
|
|
215
|
+
<td></td>
|
|
216
|
+
<td><a href="/sw/mypage/myNotice/view.do?nttId=36387&menuNo=200038&pageIndex=1">[센터] 연수센터 이용 규칙 N</a></td>
|
|
217
|
+
<td>AI·SW마에스트로</td>
|
|
218
|
+
<td>2026.04.07 15:14:20</td>
|
|
219
|
+
</tr>
|
|
220
|
+
</tbody>
|
|
221
|
+
</table>
|
|
222
|
+
`
|
|
223
|
+
const detailHtml = `
|
|
224
|
+
<div class="bbs-view">
|
|
225
|
+
<div class="top">
|
|
226
|
+
<div class="tit">[센터] 연수센터 이용 규칙 N</div>
|
|
227
|
+
<div class="etc">
|
|
228
|
+
<span>등록일 : 2026.04.07 15:14:20</span>
|
|
229
|
+
<span>작성자 : AI·SW마에스트로</span>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="cont"><p>상세 내용</p></div>
|
|
233
|
+
</div>
|
|
234
|
+
`
|
|
235
|
+
|
|
236
|
+
expect(parseNoticeList(listHtml)).toEqual([
|
|
237
|
+
{
|
|
238
|
+
id: 36387,
|
|
239
|
+
title: '[센터] 연수센터 이용 규칙 N',
|
|
240
|
+
author: 'AI·SW마에스트로',
|
|
241
|
+
createdAt: '2026.04.07 15:14:20',
|
|
242
|
+
},
|
|
243
|
+
])
|
|
244
|
+
expect(parseNoticeDetail(detailHtml, 36387)).toEqual({
|
|
245
|
+
id: 36387,
|
|
246
|
+
title: '[센터] 연수센터 이용 규칙 N',
|
|
247
|
+
author: 'AI·SW마에스트로',
|
|
248
|
+
createdAt: '2026.04.07 15:14:20',
|
|
249
|
+
content: '<p>상세 내용</p>',
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('parseTeamInfo parses team cards and summary', () => {
|
|
254
|
+
const html = `
|
|
255
|
+
<ul class="bbs-team">
|
|
256
|
+
<li>
|
|
257
|
+
<div class="top">
|
|
258
|
+
<strong class="t"><a href="javascript:void(0);" onclick="teamPageGo('전수열','a','b');">김앤강</a></strong>
|
|
259
|
+
</div>
|
|
260
|
+
<p>팀원 3명</p>
|
|
261
|
+
<button type="button">참여중</button>
|
|
262
|
+
</li>
|
|
263
|
+
<li>
|
|
264
|
+
<div class="top">
|
|
265
|
+
<strong class="t"><a href="javascript:void(0);" onclick="teamPageGo('전수열','c','d');">오픈소마</a></strong>
|
|
266
|
+
</div>
|
|
267
|
+
<p>팀원 5명</p>
|
|
268
|
+
<button type="button">참여하기</button>
|
|
269
|
+
</li>
|
|
270
|
+
</ul>
|
|
271
|
+
<p class="ico-team">현재 참여중인 방은 <strong class="color-blue">1</strong>/100팀 입니다</p>
|
|
272
|
+
`
|
|
273
|
+
|
|
274
|
+
expect(parseTeamInfo(html)).toEqual({
|
|
275
|
+
teams: [
|
|
276
|
+
{ name: '김앤강', memberCount: 3, joinStatus: '참여중' },
|
|
277
|
+
{ name: '오픈소마', memberCount: 5, joinStatus: '참여하기' },
|
|
278
|
+
],
|
|
279
|
+
currentTeams: 1,
|
|
280
|
+
maxTeams: 100,
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('parseMemberInfo parses dl pairs', () => {
|
|
285
|
+
const html = `
|
|
286
|
+
<dl><dt><span class="point">아이디</span></dt><dd>devxoul@gmail.com</dd></dl>
|
|
287
|
+
<dl><dt><span class="point">이름</span></dt><dd>전수열</dd></dl>
|
|
288
|
+
<dl><dt><span class="point">성별</span></dt><dd>남자</dd></dl>
|
|
289
|
+
<dl><dt><span class="point">생년월일</span></dt><dd>1995-01-14</dd></dl>
|
|
290
|
+
<dl><dt><span class="point">연락처</span></dt><dd>01020609858</dd></dl>
|
|
291
|
+
<dl><dt><span class="point">소속</span></dt><dd>Indent</dd></dl>
|
|
292
|
+
<dl><dt><span class="point">직책</span></dt><dd></dd></dl>
|
|
293
|
+
`
|
|
294
|
+
|
|
295
|
+
expect(parseMemberInfo(html)).toEqual({
|
|
296
|
+
email: 'devxoul@gmail.com',
|
|
297
|
+
name: '전수열',
|
|
298
|
+
gender: '남자',
|
|
299
|
+
birthDate: '1995-01-14',
|
|
300
|
+
phone: '01020609858',
|
|
301
|
+
organization: 'Indent',
|
|
302
|
+
position: '',
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
test('parseEventList parses 7-column event table', () => {
|
|
307
|
+
const html = `
|
|
308
|
+
<table>
|
|
309
|
+
<thead><tr><th>NO.</th><th>구분</th><th>제목</th><th>접수기간</th><th>행사기간</th><th>상태</th><th>등록일</th></tr></thead>
|
|
310
|
+
<tbody>
|
|
311
|
+
<tr><td>11</td><td>행사</td><td><a href="/sw/mypage/applicants/view.do?bbsId=77&menuNo=200050">데모데이</a></td><td>2026.04.01 ~ 2026.04.05</td><td>2026.04.10 ~ 2026.04.10</td><td>[접수중]</td><td>2026-03-30</td></tr>
|
|
312
|
+
</tbody>
|
|
313
|
+
</table>
|
|
314
|
+
`
|
|
315
|
+
|
|
316
|
+
expect(parseEventList(html)).toEqual([
|
|
317
|
+
{
|
|
318
|
+
id: 77,
|
|
319
|
+
category: '행사',
|
|
320
|
+
title: '데모데이',
|
|
321
|
+
registrationPeriod: { start: '2026-04-01', end: '2026-04-05' },
|
|
322
|
+
eventPeriod: { start: '2026-04-10', end: '2026-04-10' },
|
|
323
|
+
status: '접수중',
|
|
324
|
+
createdAt: '2026-03-30',
|
|
325
|
+
},
|
|
326
|
+
])
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
test('parseApplicationHistory parses 10-column mentoring history table', () => {
|
|
330
|
+
const html = `
|
|
331
|
+
<table>
|
|
332
|
+
<thead>
|
|
333
|
+
<tr><th>NO.</th><th>구분</th><th>제목</th><th>작성자</th><th>강의날짜</th><th>접수일</th><th>접수상태</th><th>개설승인</th><th>접수내역</th><th>비고</th></tr>
|
|
334
|
+
</thead>
|
|
335
|
+
<tbody>
|
|
336
|
+
<tr>
|
|
337
|
+
<td>1</td>
|
|
338
|
+
<td>멘토 특강</td>
|
|
339
|
+
<td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=9572">웹 성능 특강</a></td>
|
|
340
|
+
<td>전수열</td>
|
|
341
|
+
<td>2026.04.11</td>
|
|
342
|
+
<td>2026.04.02</td>
|
|
343
|
+
<td>[신청완료]</td>
|
|
344
|
+
<td>[OK]</td>
|
|
345
|
+
<td>승인대기</td>
|
|
346
|
+
<td>-</td>
|
|
347
|
+
</tr>
|
|
348
|
+
</tbody>
|
|
349
|
+
</table>
|
|
350
|
+
`
|
|
351
|
+
|
|
352
|
+
expect(parseApplicationHistory(html)).toEqual([
|
|
353
|
+
{
|
|
354
|
+
id: 1,
|
|
355
|
+
category: '멘토 특강',
|
|
356
|
+
title: '웹 성능 특강',
|
|
357
|
+
author: '전수열',
|
|
358
|
+
sessionDate: '2026-04-11',
|
|
359
|
+
appliedAt: '2026.04.02',
|
|
360
|
+
applicationStatus: '신청완료',
|
|
361
|
+
approvalStatus: 'OK',
|
|
362
|
+
applicationDetail: '승인대기',
|
|
363
|
+
note: '-',
|
|
364
|
+
},
|
|
365
|
+
])
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
test('parsePagination parses bbs-total block', () => {
|
|
369
|
+
const html = `
|
|
370
|
+
<ul class="bbs-total">
|
|
371
|
+
<li>Total : 11</li>
|
|
372
|
+
<li>1/2 Page</li>
|
|
373
|
+
</ul>
|
|
374
|
+
`
|
|
375
|
+
|
|
376
|
+
expect(parsePagination(html)).toEqual({ total: 11, currentPage: 1, totalPages: 2 })
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test('parseCsrfToken extracts hidden input', () => {
|
|
380
|
+
expect(parseCsrfToken('<form><input type="hidden" name="csrfToken" value="csrf-123"></form>')).toBe('csrf-123')
|
|
381
|
+
})
|
|
382
|
+
})
|