parse-comcigan
Version:
컴시간 알리미에서 시간표를 가져오는 라이브러리입니다.
335 lines (298 loc) • 11.3 kB
text/typescript
import axios from 'axios';
import iconv from 'iconv-lite';
import Cache from './cache.js';
//#region 타입 export
export type GetTimetableOptions = {
/**
* 컴시간 학교 코드입니다.
*/
"schoolCode": number,
/**
* 구분을 어떻게 할지 여부입니다.
*
* - weekday로 설정하면 시간표[교시][요일]로 과목과 선생님을 불러옵니다.
* - period로 설정하면 시간표[요일][교시]로 과목과 선생님을 불러옵니다.
*/
"criteria": "weekday" | "period",
/**
* 학년입니다.
*/
"grade": number,
/**
* 반입니다.
*/
"classN": number,
/**
* 8교시를 빼고 리턴할지 여부입니다.
*/
"without8th"?: boolean,
};
export interface ComciganInitializeType {
/**
* 이 기간동안 컴시간과 통신한 데이터가 캐시됩니다.
*
* 기본값은 0ms이며, 0ms일때는 캐시를 하지 않습니다.
*/
cacheMs?: number;
/**
* axios에서 요청을 보내고 status가 오류 status일 때 throw할 지 여부입니다.
* 만약 false일 때 요청에 status가 오류 status면 그 타입에서 빈 값을 리턴합니다.
*
* 기본값은 false입니다.
*/
doNotThrow?: boolean;
/**
* 디버그 메시지를 파라메터로 받습니다.
*
* 기본값은 없습니다.
*/
debug?: (msg: string) => any;
/**
* 컴시간알리미의 앱 버전입니다.
*
* 기본값은 2.11입니다.
*/
appVersion?: string;
}
export type ComciganSearched = {
/**
* 학교의 지역입니다.
*/
region: string;
/**
* 학교의 이름입니다.
*/
name: string;
/**
* 학교의 코드입니다.
*/
code: number;
};
export type ComciganTimetableObject = {
/**
* 과목입니다.
*/
subject: string;
/**
* 교사 성명입니다.
*/
teacher: string;
/**
* 만약 변경되었을때 이 값이 주어집니다.
*/
original?: {
/**
* 과목입니다.
*/
subject: string;
/**
* 교사 성명입니다.
*/
teacher: string;
}
};
export type ComciganTimetable = {
/**
* 컴시간알리미 앱의 최신 버전입니다.
*/
latestVersion: string;
/**
* 수정일입니다.
*/
changed: string;
/**
* 시간표입니다.
*/
timetable: ComciganTimetableObject[][];
};
//#endregion
export default class Comcigan {
private _options: ComciganInitializeType = {
"cacheMs": 0,
"doNotThrow": false,
"appVersion": "2.11",
};
private _cache = new Cache();
private mSb(mm: number, m2: number) { if (m2 == 100) { return mm % m2; } return Math.floor(mm / m2); }
private mTh(mm: number, m2: number) { if (m2 == 100) { return Math.floor(mm / m2); } return mm % m2; }
private unusable(object: any, array: any[]) {
if (object === array[0]) return;
return object;
}
private async request<T = any>(route: string, doNotParse?: boolean): Promise<T> {
if (this._cache.has(route)) {
this._options.debug ? this._options.debug(`[request] Return cache with id registered as ${route}`) : 0;
return this._cache.get(route);
}
this._options.debug ? this._options.debug(`[request] Requesting to http://comci.kr:4081/${route}`) : 0;
const r = await axios.get<any>(`http://comci.kr:4081/${route}`, {
...(this._options.doNotThrow && { "validateStatus": () => true })
});
if (doNotParse) {
if (this._options.cacheMs && this._options.cacheMs !== 0) {
this._options.debug ? this._options.debug(`[request] Registered cache with id ${route}`) : 0;
this._cache.register(route, r.data, this._options.cacheMs);
}
return r.data;
}
const data = JSON.parse(r.data.slice(0, r.data.indexOf("}") + 1));
if (this._options.cacheMs && this._options.cacheMs !== 0) {
this._options.debug ? this._options.debug(`[request] Registered cache with id ${route}`) : 0;
this._cache.register(route, data, this._options.cacheMs);
}
return data;
}
private EUC_KR_encodeURI(str: string): string {
const buffer = iconv.encode(str, 'euc-kr');
return Array.from(buffer)
.map((byte) => '%' + byte.toString(16).toUpperCase().padStart(2, '0'))
.join('');
}
/**
* 파서를 초기화 합니다.
* @param options 파서의 옵션입니다.
*/
constructor(options?: ComciganInitializeType) {
this._options = {
...this._options,
...options
};
}
/**
* 학교를 검색합니다.
* 마지막 "없으면 추가 검색 하세요" 문구는 빠져서 나옵니다.
* @param query 검색어
* @returns 검색된 학교들
*/
public async SearchSchool(query: string): Promise<ComciganSearched[]> {
const r = await this.request<RawComciganSearched>(`sc_${this.EUC_KR_encodeURI(query)}`);
return r.학교검색.map(v => ({
"region": v[1],
"name": v[2],
"code": v[3]
})).filter(v => v.code !== 0);
}
/**
* 시간표를 가져옵니다.
*
* @param schoolCode
* @param criteria
*/
public async GetTimetable(options: GetTimetableOptions): Promise<ComciganTimetable | undefined> {
const { schoolCode, criteria, grade, classN, without8th } = options;
const { appVersion } = this._options;
const str = `36174_${schoolCode}_1_4_0_3_${appVersion}`;
const route = (str.substring(9) + str.substring(0, 9)).split("").reverse().join("");
const data = await this.request<string>(`7813?${route}`, true);
const [latestVersion, step1] = data.split("^");
const edited = step1.substring(0, step1.indexOf("{"));
const step2 = step1.slice(step1.indexOf("{"));
const json: RawComciganTimetable = JSON.parse(step2.slice(0, step2.indexOf("}") + 1));
const WEEKDAY = ["월", "화", "수", "목", "금"];
const sep = json.분리 ?? 100;
if (!json.시간표) return undefined;
// 시간표[학년][반][요일][교시]
const origin = json.시간표[grade][classN];
const today = json.학급시간표[grade][classN];
const toReturn: ComciganTimetableObject[][] = [];
if (criteria === "period") {
for (let weekday = 1; weekday < WEEKDAY.length + 1; weekday++) {
const weekdayList: ComciganTimetableObject[] = [];
for (let period = 1; period < (without8th ? 8 : 9); period++) {
const o = origin[weekday][period];
const t = today[weekday][period];
if (o === undefined || t === undefined) {
weekdayList.push({
"subject": "-",
"teacher": "-"
});
continue;
}
const originSubject = json.과목명[this.mSb(o, sep) % sep];
const originTeacher = json.성명[this.mTh(o, sep) % sep];
if (o === t) {
weekdayList.push({
"subject": this.unusable(originSubject, json.과목명) ?? "-",
"teacher": this.unusable(originTeacher, json.성명) ?? "-"
});
continue;
}
const changedSubject = json.과목명[this.mSb(t, sep) % sep];
const changedTeacher = json.성명[this.mTh(t, sep) % sep];
weekdayList.push({
"subject": this.unusable(changedSubject, json.과목명) ?? "-",
"teacher": this.unusable(changedTeacher, json.성명) ?? "-",
"original": {
"subject": this.unusable(originSubject, json.과목명) ?? "-",
"teacher": this.unusable(originTeacher, json.성명) ?? "-",
}
});
}
toReturn.push(weekdayList);
}
} else {
for (let period = 1; period < (without8th ? 8 : 9); period++) {
const periodList: ComciganTimetableObject[] = [];
for (let weekday = 1; weekday < WEEKDAY.length + 1; weekday++) {
const o = origin[weekday][period];
const t = today[weekday][period];
if (!o || !t) {
periodList.push({
"subject": "-",
"teacher": "-"
});
continue;
}
const originSubject = json.과목명[this.mSb(o, sep) % sep];
const originTeacher = json.성명[this.mTh(o, sep) % sep];
if (o === t) {
periodList.push({
"subject": originSubject,
"teacher": originTeacher
});
continue;
}
const changedSubject = json.과목명[this.mSb(t, sep) % sep];
const changedTeacher = json.성명[this.mTh(t, sep) % sep];
periodList.push({
"subject": originSubject,
"teacher": originTeacher,
"original": {
"subject": changedSubject,
"teacher": changedTeacher
}
});
}
toReturn.push(periodList);
}
}
return {
"changed": edited,
latestVersion,
"timetable": toReturn
};
}
/**
* 학급 목록을 가져옵니다.
*
* @param schoolCode 학교 코드입니다.
* @returns (학년)-(반) 문자열이 모인 배열
*/
public async GetClassList(schoolCode: number): Promise<string[]> {
const str = `36174_${schoolCode}_1_4_0_3_1.00`;
const route = (str.substring(9) + str.substring(0, 9)).split("").reverse().join("");
const data = await this.request<string>(`7813?${route}`, true);
const step1 = data.split("^")[1];
const step2 = step1.slice(step1.indexOf("{"));
const json: RawComciganTimetable = JSON.parse(step2.slice(0, step2.indexOf("}") + 1));
const rawClasses = json.학급수;
if (!rawClasses) return [];
const toReturn: string[] = [];
for (let g = 1; g < rawClasses.length; g++) {
for (let c = 1; c < rawClasses[g] + 1; c++) {
toReturn.push(`${g}-${c}`);
}
}
return toReturn;
}
}