release: 0.1.0

This commit is contained in:
Starcea 2024-06-27 23:57:01 +09:00
commit a2c51585b4
No known key found for this signature in database
GPG key ID: B7A77E32374911E1
18 changed files with 2276 additions and 0 deletions

68
src/client.ts Normal file
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,6 @@
export interface Region {
/** 지역 코드 // TODO: 지역 코드가 아닌 것으로 보임 */
code: number
/** 지역 이름 */
name: string
}

10
src/models/School.ts Normal file
View 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
View 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
View 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
View file

@ -0,0 +1 @@
export const log10int = (n: number) => Math.floor(Math.log10(n))

5
src/utils/parse.ts Normal file
View 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, ''))