release: 0.1.0
This commit is contained in:
parent
ed0a6edec9
commit
a2c51585b4
18 changed files with 2276 additions and 0 deletions
68
src/client.ts
Normal file
68
src/client.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { BASE_URL, USER_AGENT } from './constants'
|
||||
import DataManager from './data'
|
||||
import type { School } from './models/School'
|
||||
import type { Timetable } from './models/Timetable'
|
||||
import { TimetableManager } from './models/Timetable'
|
||||
import { encodeBase64, encodeEUCKR } from './utils/encode'
|
||||
import { log10int } from './utils/math'
|
||||
import { parseResponse } from './utils/parse'
|
||||
import axios from 'axios'
|
||||
|
||||
export default class Comcigan {
|
||||
private readonly rest = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
},
|
||||
})
|
||||
|
||||
private readonly dataManager = new DataManager(this.rest)
|
||||
|
||||
async searchSchools(schoolName: string): Promise<School[]> {
|
||||
const { mainRoute, searchRoute } = await this.dataManager.getData()
|
||||
const res = await this.rest.get(
|
||||
`${mainRoute}?${searchRoute}l${encodeEUCKR(schoolName)}`,
|
||||
)
|
||||
const { 학교검색: data } = parseResponse<{
|
||||
학교검색: [number, string, string, number][]
|
||||
}>(res.data)
|
||||
|
||||
return data.map(([regionCode, regionName, schoolName, schoolCode]) => ({
|
||||
code: schoolCode,
|
||||
name: schoolName,
|
||||
region: { code: regionCode, name: regionName },
|
||||
}))
|
||||
}
|
||||
|
||||
async getRawTimetable(schoolCode: number): Promise<Timetable[][][][]> {
|
||||
const { mainRoute, timetableRoute, teacherCode, dayCode, subjectCode } =
|
||||
await this.dataManager.getData()
|
||||
const res = await this.rest.get(
|
||||
`${mainRoute}_T?${encodeBase64(`${timetableRoute}_${schoolCode}_0_1`)}`,
|
||||
)
|
||||
const data = parseResponse(res.data)
|
||||
|
||||
const teachers = data[`자료${teacherCode}`] as string[]
|
||||
const teachersLen = log10int(teachers.length - 1) + 1
|
||||
const subjects = data[`자료${subjectCode}`] as string[]
|
||||
|
||||
return (data[`자료${dayCode}`] as number[][][][]).slice(1).map((grade) =>
|
||||
grade.slice(1).map((cls) =>
|
||||
cls.slice(1).map((day) =>
|
||||
day.slice(1).map((period) => {
|
||||
const p = period.toString()
|
||||
|
||||
return {
|
||||
subject: subjects[Number(p.slice(0, p.length - teachersLen - 1))],
|
||||
teacher: teachers[Number(p.slice(-teachersLen))],
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
async getTimetable(schoolCode: number) {
|
||||
return new TimetableManager(await this.getRawTimetable(schoolCode))
|
||||
}
|
||||
}
|
||||
22
src/constants.ts
Normal file
22
src/constants.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export const BASE_URL = 'http://comci.net:4082'
|
||||
|
||||
export const USER_AGENT =
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
|
||||
|
||||
export const RegExes = {
|
||||
MainRoute: /(?<=\.\/)\d+(?=\?\d+l)/,
|
||||
SearchRoute: /(?<=\?)\d+(?=l)/,
|
||||
TimetableRoute: /(?<=')\d+(?=_')/,
|
||||
TeacherCode: /(?<=성명=자료\.자료)\d+/,
|
||||
DayCode: /(?<=일일자료=Q자료\(자료.자료)\d+/,
|
||||
SubjectCode: /(?<=자료.자료)\d+(?=\[sb\])/,
|
||||
WhiteSpace: /\0+$/,
|
||||
}
|
||||
|
||||
export enum Weekday {
|
||||
Monday = 1,
|
||||
Tuesday,
|
||||
Wednesday,
|
||||
Thursday,
|
||||
Friday,
|
||||
}
|
||||
64
src/data.ts
Normal file
64
src/data.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { RegExes } from './constants'
|
||||
import type { AxiosInstance } from 'axios'
|
||||
import { decode } from 'iconv-lite'
|
||||
|
||||
interface Data {
|
||||
mainRoute: string
|
||||
searchRoute: string
|
||||
timetableRoute: string
|
||||
|
||||
teacherCode: string
|
||||
dayCode: string
|
||||
subjectCode: string
|
||||
}
|
||||
|
||||
export default class DataManager {
|
||||
private _data: Data | null = null
|
||||
|
||||
private _lastFetch = 0
|
||||
|
||||
constructor(private readonly rest: AxiosInstance) {}
|
||||
|
||||
private async fetchData(): Promise<Data> {
|
||||
const res = await this.rest.get('/st', {
|
||||
responseType: 'arraybuffer',
|
||||
})
|
||||
const data = decode(Buffer.from(res.data), 'euc-kr')
|
||||
|
||||
const main = RegExes.MainRoute.exec(data)
|
||||
if (!main) throw new Error('Failed to fetch main route')
|
||||
|
||||
const search = RegExes.SearchRoute.exec(data)
|
||||
if (!search) throw new Error('Failed to fetch search route')
|
||||
|
||||
const timetable = RegExes.TimetableRoute.exec(data)
|
||||
if (!timetable) throw new Error('Failed to fetch timetable route')
|
||||
|
||||
const teacher = RegExes.TeacherCode.exec(data)
|
||||
if (!teacher) throw new Error('Failed to fetch teacher code')
|
||||
|
||||
const day = RegExes.DayCode.exec(data)
|
||||
if (!day) throw new Error('Failed to fetch day code')
|
||||
|
||||
const subject = RegExes.SubjectCode.exec(data)
|
||||
if (!subject) throw new Error('Failed to fetch subject code')
|
||||
|
||||
this._lastFetch = Date.now()
|
||||
this._data = {
|
||||
mainRoute: main[0],
|
||||
searchRoute: search[0],
|
||||
timetableRoute: timetable[0],
|
||||
teacherCode: teacher[0],
|
||||
dayCode: day[0],
|
||||
subjectCode: subject[0],
|
||||
}
|
||||
return this._data
|
||||
}
|
||||
|
||||
async getData() {
|
||||
if (this._data && Date.now() - this._lastFetch < 1000 * 60 * 60)
|
||||
return this._data
|
||||
|
||||
return this.fetchData()
|
||||
}
|
||||
}
|
||||
8
src/index.ts
Normal file
8
src/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import Comcigan from './client'
|
||||
|
||||
export default Comcigan
|
||||
|
||||
export * from './models/Region'
|
||||
export * from './models/School'
|
||||
export * from './models/Timetable'
|
||||
export { Weekday } from './constants'
|
||||
6
src/models/Region.ts
Normal file
6
src/models/Region.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export interface Region {
|
||||
/** 지역 코드 // TODO: 지역 코드가 아닌 것으로 보임 */
|
||||
code: number
|
||||
/** 지역 이름 */
|
||||
name: string
|
||||
}
|
||||
10
src/models/School.ts
Normal file
10
src/models/School.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { Region } from './Region'
|
||||
|
||||
export interface School {
|
||||
/** 학교 코드 */
|
||||
code: number
|
||||
/** 학교 이름 */
|
||||
name: string
|
||||
/** 학교 지역 */
|
||||
region: Region
|
||||
}
|
||||
24
src/models/Timetable.ts
Normal file
24
src/models/Timetable.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
export interface Timetable {
|
||||
subject: string
|
||||
teacher: string
|
||||
}
|
||||
|
||||
export class TimetableManager {
|
||||
constructor(private readonly timetables: Timetable[][][][]) {}
|
||||
|
||||
getByGrade(grade: number) {
|
||||
return this.timetables[grade - 1]
|
||||
}
|
||||
|
||||
getByClass(grade: number, cls: number) {
|
||||
return this.timetables[grade - 1][cls - 1]
|
||||
}
|
||||
|
||||
getByDay(grade: number, cls: number, day: number) {
|
||||
return this.timetables[grade - 1][cls - 1][day - 1]
|
||||
}
|
||||
|
||||
getByPeriod(grade: number, cls: number, day: number, period: number) {
|
||||
return this.timetables[grade - 1][cls - 1][day - 1][period - 1]
|
||||
}
|
||||
}
|
||||
6
src/utils/encode.ts
Normal file
6
src/utils/encode.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { encode } from 'iconv-lite'
|
||||
|
||||
export const encodeEUCKR = (str: string) =>
|
||||
[...encode(str, 'euc-kr')].map((v) => '%' + v.toString(16)).join('')
|
||||
|
||||
export const encodeBase64 = (str: string) => Buffer.from(str).toString('base64')
|
||||
1
src/utils/math.ts
Normal file
1
src/utils/math.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const log10int = (n: number) => Math.floor(Math.log10(n))
|
||||
5
src/utils/parse.ts
Normal file
5
src/utils/parse.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { RegExes } from '../constants'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const parseResponse = <T = any>(str: string): T =>
|
||||
JSON.parse(str.replace(RegExes.WhiteSpace, ''))
|
||||
Loading…
Add table
Add a link
Reference in a new issue