Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/#79: 학과 알림받기 구독하는 로직 추가 #80

Merged
merged 11 commits into from
Aug 21, 2023
16 changes: 8 additions & 8 deletions .env.vault
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@
#/--------------------------------------------------/

# development
DOTENV_VAULT_DEVELOPMENT="OfeN4/QneMTx3gVfMLZBjpivo2KL/7BU/Q9Pucv4JTdoSf7jtpLOyuWAcb+7/uoDE5kqdCV9p6IpFfGPF0EwEQB50jxorQPn1S8GKgvjkPQIm3534Naqh3uGU6QSEwLxewChT6AHUEfuelYmxye/fe19aRSnwvI6MX8yL+c4aUYGoCRzUk1PSW8Z8iO1mYaBhEq+/pCDc6Qf3gUsBPsyAQS880+xWsyNcMA/42Rs+vTO9VO1zpMZKfm/MAeGIhKohccIAH6QEbL0yy+KVFpHR2CXV4v4xfJ6PjNZDHz1SNy2M7I3TUcZ21Tno1ClAY1YJgO63hey5MMOpG62QRnmBiKIVftvGTu9EJRkMqDPwdmkiblOpHhrrCipotlAQZgIT1SMXQC9baiL4i5Rm/Me2ZUamrQ4JEMTi0vmLJxe1ZdpyWAFesyyCwl7JED2HkcdoYBuNJ9ocmCh0z435nIovV4L4Kw+SdC1VkZ8WfMBrQzLhScgbmiXBXz5Ktv0mKNKJ6FGcMJl/JlX1fQhI1OsXoNVrMx0gzcA1a/1qnfnIYvgPD5heQSpxgqdtorguqU6/9vjxe/OWnSlpwvHAOKv/A=="
DOTENV_VAULT_DEVELOPMENT_VERSION=6
DOTENV_VAULT_DEVELOPMENT="YMXJ0T6xLPdrArH0Bg4reWcwhX3rMQZY9+kARQ0BIyaCu0RKl32qff2cgi2V3JS7umvlTET28RpzKp3vEfmKWgaM7VCzHY8C8mgBEGg+Sp4tFvv0waCzKcfb5Jhc0n6WBYZpkQ0cObYymXLRZd06QR5PuEggzM88MO4pS0JZFRnEVfB1itOt+ncTmERUgucYS8ephnfLD/lpbZ53CdGsKefwhKW7Q7fte7aX9m3ZWXl26FbkqedXjgb3vkjGUcvCvj5RnuYlEvgJ7bBgoQLb3W2/e0TFYC0wwmqWryc7E9XaURypwnW26NmYEfDGT31EH0Srj/Q6fHjTZQla/wa1tyTckpHAGhxDojJ2iRQTzplHYQvZDVFXSme85dSzBUIRjOhg57bdTuBkEmJiCMUPQ3SOXtOb7aONOOUpBAig+vhPkh6sPM9wtgKe1bDsVo12YGW8QCSsMmymleEVuYP7Y8kqr6sRoE5fIdx2Pk5nZBTIDKZ/L062YkrffW+mrbQlGUdAal6x1OEZXwJrrwmKKo6RpQlxg5hLlx5K+aXDMXIMDQBqDWX+zpXYcR4vvaojEJ+RI9Mb82eNhjtt+nMES9kyokNcGPvrCxY1EC/kOocIltOQKaQfVZ5wn3Es6BXpKApWxAI46lTsrCetnZW89uu4eGPf46TnXOMzXZWKnFljyPU2WFfztJXcV3a71s0TfnUc55iuD7ijs89zrDPkk/NNoDFI6xzNK8BFnYSIggPtsx5KNATg3tJXKLKRqC5qH2R0wsB5iVeEXYTu6rvXdhL7+USjpfAK28BpFvIROmujXAjyxER76TZ9Kx74xUHOC1E6YQjfIAcOtQS3tPkYznxGamz/xL54pHLA0k3zTU0HPM9VmxdLQIRBWjBnMsHGox7/5tA="
DOTENV_VAULT_DEVELOPMENT_VERSION=7

# ci
DOTENV_VAULT_CI="Vc7bBZpRO8YVib6M+jGtpbfnkvIEeNH7a2Q2p403iyMIrtuLBnH0NFsHH0e2+aw84VmtpBc+UwITMJXpGa65NFL/fZeyC+2yRjDOJxLoZzTQchOxQJ87puQEGvoxPdnJBwo2dETdY3hD0QfVaif0y7e4fHANVTIZdBfhPVyP0/xENhonSYTRR2uKA9atx8k+OvcOV+RPcBFu7eqjmK3NC3N6ONAl9fIoGDmQXJg155/m1Agn61qv2XNjOfQ9fLvI5g5l9ZA6780VBfVll6oN9g=="
DOTENV_VAULT_CI_VERSION=4
DOTENV_VAULT_CI="utr3sRXvPbP2ppnMdT0EXe5XONwdJWZwMmiSKchVLTUXRd8nbCQdMTXyvKWaMARgrlD74pnNnaQtvpw24UsQSCVIVjVEpiLqejyRzCCNVHFihmhSxbe44j2gTCYbN0dI7RVhi5X8CCO4igeNqf4etd6DM/8bEmCm5c1uYPNvOl6GNGKMo8fAfNFqbbc292tCUgI5P862XRhoGpGhpsptUUoqWFH93OVXENs4i9Q2yjhaC93zQ3j0TtpMLNrAMH2ZdvWOkzSB/Um9n2hLGquWZMFaLR2jMFwdOxjbTkVnWEc5dIXVQ7Eb+xr0quNrUucpYt22v+Ay4Pe0lmPOMrr13VH6FhWiUaJiLRm1F2C/uPuRJ7VdI04pRQ=="
DOTENV_VAULT_CI_VERSION=6

# staging
DOTENV_VAULT_STAGING="lddYvKn+UowBMkO+9+pOIQXCiH12H6kCaMbywNXBK+rauhTNd06Xj+Ylx9s8pjC6zGnKCnxv95Q4rAoZvG4Fe821JZYNNrtD5512uA3NAf8NIWPNBOHQyi8RlEZGFOmLYTj4+pV+t+v0pS70ZDBR0aQz1rGmLXG4rPOCsLtZOK8//ghbTOXeyFj790Rr6KrPJ1vlDNpI73cPvUns2elZAloEpSxX66q9yZZchI6m65zq1ZaeUEPwTj2/VFzYKbCoUvhpkJbE85fWvKi1388ODw=="
DOTENV_VAULT_STAGING_VERSION=4
DOTENV_VAULT_STAGING="62UtrqNcwf2WqzC1GagIidoM0BUZ6tCKrbIzAMwAhTZwYJeyGP3/G3tF+q4zesdmr1/AvIbKemK7mzk1OVDxPnl9SyKfHTMcMiz3tWn6owYMV/0lyy1pOU1XiX7OsbKgfrd21jtQGy3PR/ulk/+aBBj1vGRAroHdZtdE6TAMX/VotlWLymc/UWORdH3F1zqqVy3X7ed8KOjnJrbQBaKzHpGYGr3TddvF1JRN6U1hjQJHwBWh1zkL1j0IjC8YVGnuDFRf8qZtlgWbh+mjAfpiEi3vCccHIBfkz/Ulrzy+BdDxk28OuOBNiDW3BQ2N6wxfGpTLnZvGI1QcNhzChdelbhCN44LSiinUmw7PQnRkE0dFb3krZbrP9A=="
DOTENV_VAULT_STAGING_VERSION=6

# production
DOTENV_VAULT_PRODUCTION="5r/p0Qi21439F4P/eichNp6utgFHZNAfcrQS41S09Fj4lQ01O7ELE0fgPk/VMrCeJ4rof2Qx3nhDLzkvQ2JKJ5+sD63xQGc5V64LlfCOMxI/mjGedMdyq3/hwyNivbtClDqsP6VM0v3CheRw/Kw3Xfe4TjQakiL5JiOfPvhadND98GvWrvOBjhAJPtDmBMMaq30vntVxd80mE3NRPl07Ph3q/EAhPtrrhyOfhflvHVXyugbhL1nQf8d4wIa0Veo1/N/zF1PNIYYOIqSxw+q0FQ=="
DOTENV_VAULT_PRODUCTION_VERSION=4
DOTENV_VAULT_PRODUCTION="DXxxOyOqrNh6EpO5t0ggIh2Uc7mjP2h/leOcVzfSXvxd8y78G9huW4vGrW4nflyykbi39t61DuJaOPeKNC7QvyIW9JIrxrDek2WXS4+GuqjDeVdwd5lwi2ljsx605QvcOV54+85lG03UeISWGYwWH/5x7X2hgW3e4GwKes2Qi9HTPtEfuEfIhaPMfOs+uqfvPSUnFwojiaksnl3AvKJ4L695P3Mg3PqrUzGWdhS3903ofVgAT0z/Id0QVcbKmT3qBHnj/pfVLhLzSM9kI0HYnyMC5WXtULbr2Ky+HJQhAufES+hqApvfedNZxvIa/Ths+6D2MGdtFfppzsTjDyUAEszkCzB4gTu0PKEa1kXVFhJUsnUfHWIXMg=="
DOTENV_VAULT_PRODUCTION_VERSION=6

#/----------------settings/metadata-----------------/
DOTENV_VAULT="vlt_546827127a49f86a5d8b31465281544134e6a7610ba4d7bbc2b3c15619fd0e71"
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"cheerio": "^1.0.0-rc.12",
"cors": "^2.8.5",
"mysql2": "^3.3.2",
"node-cron": "^3.0.2"
"node-cron": "^3.0.2",
"web-push": "^3.6.4"
},
"devDependencies": {
"@types/cheerio": "^0.22.31",
Expand All @@ -34,6 +35,7 @@
"@types/morgan": "^1.9.4",
"@types/node": "^18.16.3",
"@types/node-cron": "^3.0.8",
"@types/web-push": "^3.3.2",
"@typescript-eslint/eslint-plugin": "5.59.1",
"@typescript-eslint/parser": "5.59.1",
"axios": "^1.4.0",
Expand Down
42 changes: 42 additions & 0 deletions src/apis/subscribe/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
subscribeMajor,
pushNotification,
unsubscribeMajor,
} from '@apis/subscribe/service';
import express, { Request, Response } from 'express';

const router = express.Router();
router.post('/major', async (req: Request, res: Response) => {
try {
const { subscription, major } = req.body.data;
await subscribeMajor(subscription, major);
} catch (error) {
console.error(error);
} finally {
res.status(200).json();
}
});

router.delete('/major', async (req: Request, res: Response) => {
try {
const { subscription, major } = req.body;
await unsubscribeMajor(subscription, major);
} catch (error) {
console.error(error);
} finally {
res.status(200).json();
}
});

router.post('/push', async (req: Request, res: Response) => {
try {
const { major } = req.body.data;
pushNotification(major);
} catch (error) {
console.error(error);
} finally {
res.status(200).json();
}
});

export default router;
87 changes: 87 additions & 0 deletions src/apis/subscribe/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import db from '@db/index';
import webpush from 'web-push';

interface UserPushInfo {
endpoint: string;
expirationTime: string | null;
keys: {
p256dh: string;
auth: string;
};
}

interface PushMessage {
title: string;
body: string;
icon: string;
}

interface SubscribeUser {
user: string;
}

export const subscribeMajor = async (
subscription: UserPushInfo,
major: string,
) => {
return new Promise<boolean>((resolve, reject) => {
try {
const subscribeMajorQuery =
'INSERT INTO ' + major + '구독 (user) VALUES (?)';

db.query(subscribeMajorQuery, [JSON.stringify(subscription)], (error) => {
if (error) {
console.error('구독 실패');
reject(false);
} else {
console.log('구독 성공');
resolve(true);
}
});
} catch (error) {
console.error(error);
reject(false);
}
});
};

export const unsubscribeMajor = async (
subscription: UserPushInfo,
major: string,
) => {
return new Promise<boolean>((resolve, reject) => {
try {
const unSubscribeMajorQuery = `DELETE FROM ${major}구독 WHERE user like '%${subscription.endpoint}%'`;
db.query(unSubscribeMajorQuery, (error, res) => {
if (error) {
console.error('구독취소 실패');
reject(false);
}
console.log('구독취소 성공');
resolve(true);
});
} catch (error) {
console.error(error);
}
});
};

export const pushNotification = (major: string) => {
const query = `SELECT user FROM ${major}구독`;
db.query(query, (err: Error, res: SubscribeUser[]) => {
if (err) console.error(err);

const message: PushMessage = {
title: `${major} 알림`,
body: '새로운 공지가 추가됐어요',
icon: './icons/icon-192x192.png',
};

for (const userInfo of res) {
webpush.sendNotification(
JSON.parse(userInfo.user),
JSON.stringify(message),
);
}
});
};
3 changes: 3 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ export default {
DB_PORT: process.env.DATABASE_PORT,
NOTION_API_KEY: process.env.NOTION_API_KEY,
NOTION_DATABASE_ID: process.env.NOTION_DATABASE_ID,
VAPID_PUBLIC_KEY: process.env.VAPID_PUBLIC_KEY,
VAPID_PRIVATE_KEY: process.env.VAPID_PRIVATE_KEY,
ROOT_EMAIL: process.env.ROOT_EMAIL,
};
10 changes: 10 additions & 0 deletions src/config/webpush.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import env from '@config';
import webpush from 'web-push';

export default (): void => {
webpush.setVapidDetails(
`mailto:${env.ROOT_EMAIL}`,
env.VAPID_PUBLIC_KEY,
env.VAPID_PRIVATE_KEY,
);
};
24 changes: 24 additions & 0 deletions src/db/table/createTables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,29 @@ const createNoticeTable = (college: College[]) => {
}
};

const createSubscribeTable = (college: College[]) => {
for (const data of college) {
const major =
data.departmentSubName !== '-'
? data.departmentSubName
: data.departmentName;

const tableName = `${major}구독`;
const createTableQuery = `CREATE TABLE ${tableName} (
id INT PRIMARY KEY AUTO_INCREMENT,
user VARCHAR(600) NOT NULL UNIQUE
);`;

db.query(createTableQuery, (error) => {
if (error) {
console.log('테이블 생성 실패', error);
} else {
console.log('학과구독 테이블 생성 성공!');
}
});
}
};

const createSchoolNoticeTable = () => {
for (const tableName of [`학교고정`, `학교일반`]) {
const createTableQuery = `CREATE TABLE ${tableName} (
Expand All @@ -86,6 +109,7 @@ const createAllTables = (college: College[]) => {
createGraduationTable();
createSchoolNoticeTable();
createNoticeTable(college);
createSubscribeTable(college);
};

export default createAllTables;
7 changes: 5 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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 { saveGraduationRequirementToDB } from '@db/data/graduation';
import { corsOptions } from '@middlewares/cors';
import errorHandler from '@middlewares/error-handler';
import cors from 'cors';
import express, { Request, Response } from 'express';
import morgan from 'morgan';
import webpush from 'src/config/webpush';
import { initialCrawling } from 'src/hooks/startCrawlingData';
import './hooks/cronNoticeCrawling';

Expand All @@ -20,12 +21,12 @@ app.use(express.json());
app.use(errorHandler);

initialCrawling();
saveGraduationRequirementToDB();

app.use('/api/suggestion', suggestionRouter);
app.use('/api/majorDecision', majorRouter);
app.use('/api/announcement', noticeRouter);
app.use('/api/graduation', graduationRouter);
app.use('/api/subscription', subscriptionRouter);

app.get('/test', (req: Request, res: Response) => {
console.log('tet');
Expand All @@ -35,3 +36,5 @@ app.get('/test', (req: Request, res: Response) => {
app.listen(env.SERVER_PORT, () => {
console.log(env.SERVER_PORT, '포트 서버 실행중');
});

webpush();
Loading