Skip to content
This repository was archived by the owner on Jul 7, 2023. It is now read-only.

Commit

Permalink
refactor: data accessor에서 setParameters 제거 (#118)
Browse files Browse the repository at this point in the history
* school menu 서비스 리팩토링

* school info 서비스 리팩토링

* 테스트 타이핑 오류 수정

* 타이핑 오류 수정

* 타이핑 오류 수정

* testTimeout 수정
  • Loading branch information
5d-jh authored Mar 23, 2022
1 parent f8a3953 commit 06cb0da
Show file tree
Hide file tree
Showing 16 changed files with 189 additions and 230 deletions.
4 changes: 2 additions & 2 deletions functions/package-common/src/type/Crawler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface Crawler<T> {
get(): Promise<T>
export interface Crawler<T, Q> {
get(identifier: Readonly<Q>): Promise<T>
shouldSave(): boolean
}
2 changes: 0 additions & 2 deletions functions/package-common/src/type/DataAccessor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
export interface DataAccessor<T> {
setParameters(...any): DataAccessor<T>;
close(): any;
}
3 changes: 2 additions & 1 deletion functions/package-function/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node'
testEnvironment: 'node',
testTimeout: 10000
}
21 changes: 8 additions & 13 deletions functions/package-school-info/src/data/NeisCrawler.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,30 @@
import { Crawler } from '@school-api/common'
import { SchoolInfo, StringToKeyMapping } from '../type/SchoolInfo'
import { SchoolInfo, SchoolInfoSearchQuery, StringToKeyMapping } from '../type/SchoolInfo'
import fetch from 'node-fetch'
import { URLSearchParams } from 'url'
import { JSDOM } from 'jsdom'
import { decode } from 'iconv-lite'

process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'

export class NeisCrawler implements Crawler<SchoolInfo[]> {
export class NeisCrawler implements Crawler<SchoolInfo[], SchoolInfoSearchQuery> {
private contentLength: number;
private searchKeyword: string;

setParameters (searchKeyword: string): Crawler<SchoolInfo[]> {
this.searchKeyword = searchKeyword
return this
}

async getSchoolCodes (): Promise<string[]> {
async getSchoolCodes (query: SchoolInfoSearchQuery): Promise<string[]> {
const options = {
method: 'POST',
body: new URLSearchParams({
SEARCH_GS_HANGMOK_CD: '',
SEARCH_GS_HANGMOK_NM: '',
SEARCH_SCHUL_NM: this.searchKeyword,
SEARCH_SCHUL_NM: query.searchKeyword,
SEARCH_GS_BURYU_CD: '',
SEARCH_SIGUNGU: '',
SEARCH_SIDO: '',
SEARCH_FOND_SC_CODE: '',
SEARCH_MODE: '9',
SEARCH_TYPE: '2',
pageNumber: '1',
SEARCH_KEYWORD: this.searchKeyword
SEARCH_KEYWORD: query.searchKeyword
})
}
const url = 'https://www.schoolinfo.go.kr/ei/ss/Pneiss_f01_l0.do'
Expand All @@ -42,6 +36,7 @@ export class NeisCrawler implements Crawler<SchoolInfo[]> {
const $ = require('jquery')(window)

const schoolCodes: string[] = []

$('.basicInfo').map(function () {
schoolCodes.push($(this).attr('class').split(' ')[1].slice(2))
})
Expand Down Expand Up @@ -89,8 +84,8 @@ export class NeisCrawler implements Crawler<SchoolInfo[]> {
return result
}

async get (): Promise<SchoolInfo[]> {
return this.getSchoolCodes()
async get (query: Readonly<SchoolInfoSearchQuery>): Promise<SchoolInfo[]> {
return this.getSchoolCodes(query)
.then(this.getSchoolInfos)
}

Expand Down
60 changes: 19 additions & 41 deletions functions/package-school-info/src/data/SchoolInfoDataAccessor.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,21 @@
import { DataAccessor, InternalServerError } from '@school-api/common'
import { SchoolInfo } from '../type/SchoolInfo'
import { DataAccessor } from '@school-api/common'
import { SchoolInfo, SchoolInfoSearchQuery } from '../type/SchoolInfo'
import { firestore } from 'firebase-admin'

const collectionName = 'schoolinfo'

export class SchoolInfoDataAccessor implements DataAccessor<SchoolInfo[]> {
private db: firestore.Firestore;
private ref: firestore.CollectionReference;
private batch: firestore.WriteBatch;
readonly #firestore: firestore.Firestore;
readonly #collectionReference: firestore.CollectionReference;

constructor (db: firestore.Firestore) {
this.db = db
this.batch = this.db.batch()
this.ref = this.db.collection(collectionName)
this.#firestore = db
this.#collectionReference = this.#firestore.collection(collectionName)
}

setParameters (): DataAccessor<SchoolInfo[]> {
return this
}

async getManyByCodes (codes: string[]): Promise<SchoolInfo[]> {
const refs = codes.map(code => this.ref.doc(code))
return this.db.getAll(...refs)
.then(docs => docs.map(doc => doc.data()))
.then(datas => datas as SchoolInfo[])
}

async getByKeyword (searchKeyword: string): Promise<SchoolInfo[]> {
return this.ref
.where('keywords', 'array-contains', searchKeyword)
async getByKeyword (query: SchoolInfoSearchQuery): Promise<SchoolInfo[]> {
return this.#collectionReference
.where('keywords', 'array-contains', query.searchKeyword)
.get()
.then(({ docs }) => docs.map(doc => doc.data()))
.then(
Expand All @@ -43,45 +30,36 @@ export class SchoolInfoDataAccessor implements DataAccessor<SchoolInfo[]> {

/**
* 외부 데이터와 내부 데이터를 비교하여 내부 데이터를 업데이트
* @param fetchedDatas 외부에서 가져온 데이터
* @param keyword 사용자가 검색한 키워드(검색 결과 개선을 위해 사용)
* @param fetchedData 외부에서 가져온 데이터
* @param query 사용자가 검색한 키워드(검색 결과 개선을 위해 사용)
*/
updateDatasAndKeywords (fetchedDatas: SchoolInfo[], keyword: string) {
const refs = fetchedDatas.map(data => this.ref.doc(data.code))
updateKeywordOrInsert (fetchedData: SchoolInfo[], query: Readonly<SchoolInfoSearchQuery>) {
const refs = fetchedData.map(data => this.#collectionReference.doc(data.code))

return this.db.runTransaction(
return this.#firestore.runTransaction(
t => t.getAll(...refs)
.then(docs => docs.filter(doc => doc.exists))
.then(docs => docs.map(doc => doc.data()))
.then((dbDatas: SchoolInfo[]) => {
const fetchedCodes = fetchedDatas.map(data => data.code) // 외부 데이터의 NEIS코드
const fetchedCodes = fetchedData.map(data => data.code) // 외부 데이터의 NEIS코드
const storedCodes = dbDatas.map(data => data.code) // 내부 데이터의 NEIS코드

fetchedCodes.forEach(fc => {
const doc = this.ref.doc(fc)
const doc = this.#collectionReference.doc(fc)
if (storedCodes.includes(fc)) { // 외부 데이터 코드에 내부 데이터가 있는지 확인
// 있다면 검색 결과 개선을 위해 키워드 추가 (내부 데이터가 있었으나 키워드에 걸리지 않은 경우)
t.update(doc, {
keywords: firestore.FieldValue.arrayUnion(keyword)
keywords: firestore.FieldValue.arrayUnion(query.searchKeyword)
})
} else {
// 없다면 데이터 추가 (내부 데이터가 실제로 없는 경우)
t.create(doc, {
...fetchedDatas.find(d => d.code === fc),
keywords: [keyword]
...fetchedData.find(d => d.code === fc),
keywords: [query.searchKeyword]
})
}
})
})
)
}

updateMany (datas: SchoolInfo[], merge?: boolean) {
datas.forEach(data => this.batch.set(this.ref.doc(data.code), data, { merge }))
return this.batch.commit()
}

close () {
this.db.terminate()
}
}
8 changes: 4 additions & 4 deletions functions/package-school-info/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ export const schoolInfoApp = (firebaseApp: admin.app.App) => {
try {
if (searchKeyword.length > 0) {
const neisCrawler = new NeisCrawler()
.setParameters(searchKeyword)
const schoolInfoDataAccessor = new SchoolInfoDataAccessor(firestore)
const schoolInfoService = new SchoolInfoService(neisCrawler, schoolInfoDataAccessor)
schoolInfos = await schoolInfoService.getSchoolInfos(searchKeyword)

schoolInfos = await schoolInfoService.getSchoolInfos({ searchKeyword })
} else {
schoolInfos = []
}
Expand All @@ -53,10 +53,10 @@ export const schoolInfoApp = (firebaseApp: admin.app.App) => {
try {
if (searchKeyword.length > 0) {
const neisCrawler = new NeisCrawler()
.setParameters(searchKeyword)
const schoolInfoDataAccessor = new SchoolInfoDataAccessor(firestore)
const schoolInfoService = new SchoolInfoService(neisCrawler, schoolInfoDataAccessor)
schoolInfos = await schoolInfoService.getSchoolInfos(searchKeyword)

schoolInfos = await schoolInfoService.getSchoolInfos({ searchKeyword })
} else {
schoolInfos = []
}
Expand Down
20 changes: 10 additions & 10 deletions functions/package-school-info/src/service/SchoolInfoService.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { SchoolInfo } from '../type/SchoolInfo'
import { SchoolInfo, SchoolInfoSearchQuery } from '../type/SchoolInfo'
import { Crawler } from '@school-api/common'
import { SchoolInfoDataAccessor } from '../data/SchoolInfoDataAccessor'

export class SchoolInfoService {
crawler: Crawler<SchoolInfo[]>;
dataAccessor: SchoolInfoDataAccessor;
readonly #crawler: Crawler<SchoolInfo[], SchoolInfoSearchQuery>;
readonly #dataAccessor: SchoolInfoDataAccessor;

constructor (
crawler: Crawler<SchoolInfo[]>,
crawler: Crawler<SchoolInfo[], SchoolInfoSearchQuery>,
dataAccessor: SchoolInfoDataAccessor
) {
this.crawler = crawler
this.dataAccessor = dataAccessor
this.#crawler = crawler
this.#dataAccessor = dataAccessor
}

async getSchoolInfos (searchKeyword: string): Promise<SchoolInfo[]> {
let result = await this.dataAccessor.getByKeyword(searchKeyword)
async getSchoolInfos (query: SchoolInfoSearchQuery): Promise<SchoolInfo[]> {
let result = await this.#dataAccessor.getByKeyword(query)

if (result.length === 0) {
result = await this.crawler.get()
result = await this.#crawler.get(query)

if (result.length !== 0) {
await this.dataAccessor.updateDatasAndKeywords(result, searchKeyword)
await this.#dataAccessor.updateKeywordOrInsert(result, query)
}
}

Expand Down
4 changes: 4 additions & 0 deletions functions/package-school-info/src/type/SchoolInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export const StringToKeyMapping = {
주소: 'address'
}

export type SchoolInfoSearchQuery = {
searchKeyword: string
}

export type SchoolInfo = {
estDivision: string, // 설립구분
estType : string, // 설립유형
Expand Down
15 changes: 6 additions & 9 deletions functions/package-school-info/test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,24 @@ firestore.settings({
})

describe('[SCHOOL-INFO] School info parser', function () {
it('parses text from school info', function (done) {
it('parses text from school info', async () => {
const neisCrawler = new NeisCrawler()
.setParameters('서울')

neisCrawler.get()
.then(data => {
notStrictEqual(data.length, 0)
})
.then(done)
const data = await neisCrawler.get({ searchKeyword: '서울' })
notStrictEqual(data.length, 0)
})
})

describe('[SCHOOL-INFO] School info service', function () {
it('saves keywords or datas', function (done) {
const searchKeyword = '서울'

const neisCrawler = new NeisCrawler().setParameters(searchKeyword)
const neisCrawler = new NeisCrawler()
const schoolInfoDataAccessor = new SchoolInfoDataAccessor(firestore)

const schoolInfoService = new SchoolInfoService(neisCrawler, schoolInfoDataAccessor)

schoolInfoService.getSchoolInfos(searchKeyword)
schoolInfoService.getSchoolInfos({ searchKeyword })
.then(data => {
notStrictEqual(data.length, 0)
})
Expand Down
35 changes: 10 additions & 25 deletions functions/package-school-menu/src/data/MenuDataAccessor.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,40 @@
import { DataAccessor } from '@school-api/common'
import { SchoolMenu } from '../type/SchoolMenu'
import { firestore } from 'firebase-admin'
import { SchoolMenuIdentifier } from '../type/parameter'

const collectionName = 'schoolmenu'

export class MenuDataAccessor implements DataAccessor<SchoolMenu[]> {
private db: firestore.Firestore;
private ref: firestore.CollectionReference;

private schoolCode: string;
private menuYear: number;
private menuMonth: number;

constructor (db: firestore.Firestore) {
this.db = db
this.ref = this.db.collection(collectionName)
}

setParameters (schoolCode: string, menuYear: number, menuMonth: number): MenuDataAccessor {
this.schoolCode = schoolCode
this.menuYear = menuYear
this.menuMonth = menuMonth

return this
}

async get (): Promise<SchoolMenu[]> {
async get (identifier: SchoolMenuIdentifier): Promise<SchoolMenu[]> {
const snapshots = await this.ref
.where('schoolCode', '==', this.schoolCode)
.where('menuYear', '==', this.menuYear)
.where('menuMonth', '==', this.menuMonth)
.where('schoolCode', '==', identifier.schoolCode)
.where('menuYear', '==', identifier.menuYear)
.where('menuMonth', '==', identifier.menuMonth)
.get()

if (snapshots.docs.length == 0) {
if (snapshots.docs.length === 0) {
return null
}

return snapshots.docs[0].data().menu as SchoolMenu[]
}

async put (menu: SchoolMenu[]) {
async put (identifier: SchoolMenuIdentifier, menu: SchoolMenu[]) {
return await this.ref.doc().set({
menu,
version: 2,
schoolCode: this.schoolCode,
menuYear: this.menuYear,
menuMonth: this.menuMonth
schoolCode: identifier.schoolCode,
menuYear: identifier.menuYear,
menuMonth: identifier.menuMonth
})
}

close () {
this.db.terminate()
}
}
Loading

0 comments on commit 06cb0da

Please sign in to comment.