diff --git a/src/apis/building-info/controllers.ts b/src/apis/building-info/controllers.ts new file mode 100644 index 0000000..1ef0448 --- /dev/null +++ b/src/apis/building-info/controllers.ts @@ -0,0 +1,18 @@ +import express, { Request, Response } from 'express'; + +import fetchBuildingInfo from './service'; + +const router = express.Router(); + +router.get('/', async (req: Request, res: Response) => { + const code = req.query.code; + + try { + const buildingInfo = await fetchBuildingInfo(code as string); + res.json(buildingInfo); + } catch (err) { + console.log(err); + } +}); + +export default router; diff --git a/src/apis/building-info/service.ts b/src/apis/building-info/service.ts new file mode 100644 index 0000000..a95e4f4 --- /dev/null +++ b/src/apis/building-info/service.ts @@ -0,0 +1,99 @@ +import axios, { AxiosResponse } from 'axios'; +import notificationToSlack from 'src/hooks/notificateToSlack'; + +type Floor = 'basement' | 'ground' | 'rooftop'; + +interface Room { + roomNumber: string; + roomName: string; +} + +interface FormattedInfo extends Room { + floor: number; + floorType: Floor; +} + +type TotalInfo = { + [key in Floor]: { + [key in string]: Room[]; + }; +}; + +const configPaylod = (code: string) => { + const payload = new URLSearchParams(); + payload.append('code', code); + payload.append('stat', 'D'); + + return payload; +}; + +const handleLayerType = (layerType: string) => { + if (layerType === '0') return 'ground'; + if (layerType === '1') return 'basement'; + if (layerType === '2') return 'rooftop'; +}; + +const formatTotalInfo = (formattedInfo: FormattedInfo[]) => { + const totalInfo: TotalInfo = { + basement: {}, + ground: {}, + rooftop: {}, + }; + + formattedInfo.forEach(({ roomNumber, roomName, floor, floorType }) => { + const type = floorType as Floor; + + if (!Object.prototype.hasOwnProperty.call(totalInfo[type], floor)) { + totalInfo[type][floor] = [{ roomNumber, roomName }]; + return; + } + + totalInfo[type][floor].push({ roomNumber, roomName }); + }); + + return totalInfo; +}; + +const formatFetchedInfo = (data: AxiosResponse['data']) => { + const formattedData: FormattedInfo[] = data.response.deps2.reduce( + (accData: FormattedInfo[], curr: any) => { + const item = { + roomNumber: curr.roomNo, + roomName: curr.roomName, + floor: curr.layer, + floorType: handleLayerType(curr.layerTyp), + }; + + return [...accData, item]; + }, + [] as FormattedInfo[], + ); + + const formattedInfo = formatTotalInfo(formattedData); + + return formattedInfo; +}; + +const fetchBuildingInfo = async (code: string) => { + const REQUEST_URL = 'https://www.pknu.ac.kr/buildingInfoAjax.do'; + const HEADERS = { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', + }; + const payload = configPaylod(code); + + try { + const response = await axios.post(REQUEST_URL, payload.toString(), { + headers: HEADERS, + }); + + const formattedInfo = formatFetchedInfo(response.data); + + return formattedInfo; + } catch (error) { + notificationToSlack('건물 정보 요청에 문제가 발생했습니다.'); + console.log(error); + } +}; + +export default fetchBuildingInfo; diff --git a/src/db/data/noticeHandler.ts b/src/db/data/noticeHandler.ts index 1a26e14..9f6b543 100644 --- a/src/db/data/noticeHandler.ts +++ b/src/db/data/noticeHandler.ts @@ -17,6 +17,7 @@ export interface PushNoti { interface NotiLink { link: string; + rep_yn: boolean; } export const saveDepartmentToDB = async (college: College[]): Promise => { @@ -74,81 +75,119 @@ const convertAllNoticeToNormalNotice = async ( } }; -const convertSpecificNoticeToPinnedNotice = async ( +const convertSpecificNoticePinned = async ( tableName: string, noticeLink: string, + isPinned: boolean, + connection?: PoolConnection, ): Promise => { - const query = `UPDATE ${tableName} SET rep_yn = true WHERE link = '${noticeLink}';`; + const query = `UPDATE ${tableName} SET rep_yn = ${isPinned} WHERE link = '${noticeLink}';`; try { - await db.execute(query); + if (connection) await connection.execute(query); + else await db.execute(query); } catch (error) { console.log(error.message + '고정 공지로 변경 실패'); // notificationToSlack(error.message + '\n 고정 공지로 변경 실패'); } }; -export const saveMajorNoticeToDB = async ( - connection?: PoolConnection, -): Promise => { - await convertAllNoticeToNormalNotice('major_notices', connection); +export const saveMajorNoticeToDB = async (): Promise => { + // await convertAllNoticeToNormalNotice('major_notices', connection); const query = 'SELECT * FROM departments;'; - const colleges = await selectQuery(query, connection); - - const getNotiLinkQuery = `SELECT link FROM major_notices;`; - const noticeLinksInDB = ( - await selectQuery(getNotiLinkQuery, connection) - ).map((noticeLink) => noticeLink.link); + const colleges = await selectQuery(query); - const savePromises: Promise[] = []; const newNoticeMajor: PushNoti = {}; + const failedMajor: number[] = []; - for (const college of colleges) { - console.log(college.id); - const noticeLink = await noticeCrawling(college); - const noticeLists = await noticeListCrawling(noticeLink); - - const normalNotices = noticeLists.normalNotice; - const pinnedNotices = noticeLists.pinnedNotice; - - if (normalNotices.length === 0) { - notificationToSlack(`${noticeLink} 크롤링 실패`); - continue; - } - - for (const notice of normalNotices) { - const result = await noticeContentCrawling(notice); - if (result.link === '') { - // notificationToSlack(`${notice} 콘텐츠 크롤링 실패`); - continue; + const savePromises = colleges.map(async (college) => { + const connection = await db.getConnection(); + await connection.beginTransaction(); + try { + const noticeLink = await noticeCrawling(college); + const noticeLists = await noticeListCrawling(noticeLink); + const pinnedNotices = noticeLists.pinnedNotice; + const normalNotices = noticeLists.normalNotice; + if (normalNotices.length === 0) { + notificationToSlack(`${noticeLink} 크롤링 실패`); + connection.release(); + return; } - if (noticeLinksInDB.includes(result.link)) continue; - if (!newNoticeMajor[college.id]) newNoticeMajor[college.id] = []; - newNoticeMajor[college.id].push(result.title); - savePromises.push(saveMajorNotice(result, college.id, false, connection)); - } - - if (pinnedNotices) { - for (const notice of pinnedNotices) { + const getNotiLinkQuery = `SELECT link, rep_yn FROM major_notices WHERE department_id = '${college.id}'`; + const noticeDataInDB = await selectQuery( + getNotiLinkQuery, + connection, + ); + const noticeLinksInDB = noticeDataInDB.map((noti) => noti.link); + const pinnedNoticeLinksInDB = noticeDataInDB + .filter((noti) => noti.rep_yn) + .map((noti) => noti.link); + + for (const notice of normalNotices) { const result = await noticeContentCrawling(notice); if (result.link === '') { - notificationToSlack(`${notice} 콘텐츠 크롤링 실패`); + // notificationToSlack(`${notice} 콘텐츠 크롤링 실패`); continue; } - if (!noticeLinksInDB.includes(result.link)) { - savePromises.push( - saveMajorNotice(result, college.id, true, connection), + if (noticeLinksInDB.includes(result.link)) return; + if (!newNoticeMajor[college.id]) newNoticeMajor[college.id] = []; + newNoticeMajor[college.id].push(result.title); + saveMajorNotice(result, college.id, false, connection); + noticeLinksInDB.push(result.link); + } + + if (pinnedNotices) { + await pinnedNoticeLinksInDB + .filter((noti) => !pinnedNotices.includes(noti)) + .map( + async (noti) => + await convertSpecificNoticePinned( + 'major_notices', + noti, + false, + connection, + ), ); - continue; + for (const notice of pinnedNotices) { + const result = await noticeContentCrawling(notice); + if (result.link === '') { + notificationToSlack(`${notice} 콘텐츠 크롤링 실패`); + continue; + } + + if (!noticeLinksInDB.includes(result.link)) { + saveMajorNotice(result, college.id, true, connection); + continue; + } + + if (!pinnedNoticeLinksInDB.includes(result.link)) + convertSpecificNoticePinned( + 'major_notices', + result.link, + true, + connection, + ); } - convertSpecificNoticeToPinnedNotice('major_notices', result.link); } + + connection.commit(); + } catch (error) { + notificationToSlack(college.id + '크롤링 실패' + error.message); + failedMajor.push(college.id); + connection.rollback(); + } finally { + connection.release(); } - } + }); await Promise.all(savePromises); + if (failedMajor.length !== 0) { + const failedMajorList = failedMajor.join(); + notificationToSlack('크롤링 실패한 학과: ' + failedMajorList); + } + return newNoticeMajor; }; @@ -188,7 +227,7 @@ export const saveSchoolNoticeToDB = async (): Promise => { for (const noticeLink of pinnedNotices) { if (schoolNoticeLinksInDB.includes(noticeLink)) { - await convertSpecificNoticeToPinnedNotice('notices', noticeLink); + await convertSpecificNoticePinned('notices', noticeLink, true); continue; } diff --git a/src/hooks/cronNoticeCrawling.ts b/src/hooks/cronNoticeCrawling.ts index d8ced5e..b1f0831 100644 --- a/src/hooks/cronNoticeCrawling.ts +++ b/src/hooks/cronNoticeCrawling.ts @@ -21,34 +21,34 @@ const pushToUsers = async (pushNotiToUserLists: PushNoti) => { if (pushedUserCount.length !== 0) notificationToSlack(pushedUserCount); }; -const cronNoticeCrawling = async () => { - const connection = await db.getConnection(); - await connection.beginTransaction(); +const cronNoticeCrawling = async (reTryCount = 0) => { try { - const pushNotiToUserLists = await saveMajorNoticeToDB(connection); + const pushNotiToUserLists = await saveMajorNoticeToDB(); const today = new Date(); const year = today.getFullYear(); const month = today.getMonth() + 1; // 월은 0부터 시작하므로 1을 더해줍니다. const day = today.getDate(); notificationToSlack(`${year}-${month}-${day} 크롤링 완료`); - await connection.commit(); pushToUsers(pushNotiToUserLists); } catch (error) { - await connection.rollback(); - notificationToSlack(error.message); - cronNoticeCrawling(); - } finally { - connection.release(); + if (reTryCount >= 2) { + notificationToSlack(error.message); + return; + } + cronNoticeCrawling(reTryCount + 1); } }; -const cronExtracurricularCrawling = async () => { +const cronExtracurricularCrawling = async (reTryCount = 0) => { try { await saveSchoolNoticeToDB(); await saveLanguageNoticeToDB(); await saveWhalebeToDB(); } catch (error) { - notificationToSlack(error.message); + if (reTryCount >= 2) { + notificationToSlack(error.message); + return; + } cronExtracurricularCrawling(); } }; diff --git a/src/index.ts b/src/index.ts index 80496f8..be6c876 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,10 @@ +import buildingInfoRouter from '@apis/building-info/controllers'; import graduationRouter from '@apis/graduation/controller'; import majorRouter from '@apis/majorDecision/controller'; import noticeRouter from '@apis/notice/controller'; import subscriptionRouter from '@apis/subscribe/controller'; import suggestionRouter from '@apis/suggestion/controller'; import env from '@config'; -import { saveMajorNoticeToDB } from '@db/data/noticeHandler'; import { corsOptions } from '@middlewares/cors'; import errorHandler from '@middlewares/error-handler'; import cors from 'cors'; @@ -30,6 +30,7 @@ app.use('/api/majorDecision', majorRouter); app.use('/api/announcement', noticeRouter); app.use('/api/graduation', graduationRouter); app.use('/api/subscription', subscriptionRouter); +app.use('/api/buildingInfo', buildingInfoRouter); app.get('/test', (req: Request, res: Response) => { console.log('test'); diff --git a/src/utils/majorUtils.ts b/src/utils/majorUtils.ts index 70cfbcd..5f8eb4e 100644 --- a/src/utils/majorUtils.ts +++ b/src/utils/majorUtils.ts @@ -2,10 +2,7 @@ import { selectQuery } from '@db/query/dbQueryHandler'; import notificationToSlack from 'src/hooks/notificateToSlack'; export const getDepartmentIdByMajor = async (major: string) => { - const [departmentName, departmentSubName] = major.split(' '); - const getDepartmentQuery = `SELECT id FROM departments WHERE department_name = '${departmentName}' ${ - departmentSubName ? `AND department_subname = '${departmentSubName}'` : '' - };`; + const getDepartmentQuery = `SELECT id FROM departments WHERE department_name = '${major}' OR department_subname = '${major}'`; try { const departmentId = await selectQuery<{ id: number }[]>(