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

ci: 2024-04-28 배포 #824

Merged
merged 31 commits into from
Apr 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
fa9559c
ci: 타입오류 메시지 상대경로 명시적으로 해소 (#643)
scarf005 Sep 7, 2023
93fc7b4
feat: v2 book api (#746)
JeongJiHwan Sep 7, 2023
8b7c07f
style: prettier 적용 (#761)
scarf005 Sep 7, 2023
c88ca8d
[fix] backend dockerfile error (#764)
weg901127 Sep 8, 2023
1565441
fix: `positiveInt` -> `nonNegativeInt` (#766)
scarf005 Sep 11, 2023
6cbb823
refactor: v2 라우트 정리 적용 (#771)
scarf005 Sep 14, 2023
1ac5b23
feat: add mydata service
jimin52 Sep 21, 2023
7094331
feat: 유저 search 할 때 id 가 undefined 인 경우 핸들링
jimin52 Sep 21, 2023
1c8b49b
feat: add swagger && /me endpoint && apply authValidate
jimin52 Sep 21, 2023
71c1216
Merge branch 'develop' into 778-auth-관련-api-무조건-200-리턴-버그
jimin52 Sep 21, 2023
5754bef
fix: searchUsersById 타입을 이전과 같이 리턴하도록 변경
jimin52 Sep 21, 2023
7fbfa10
Merge branch '778-auth-관련-api-무조건-200-리턴-버그' of https://github.com/ji…
jimin52 Sep 21, 2023
1449028
fix: add librarian validate in search endpoint
jimin52 Sep 21, 2023
cda9990
feat: 로그인한 유저만 본인 정보를 찾을 수 있도록 middleware 에서 권한 체크
jimin52 Sep 26, 2023
7b7ad82
chore: console.log 제거
jimin52 Sep 26, 2023
c5bd7ae
Merge pull request #779 from jiphyeonjeon-42/778-auth-관련-api-무조건-200-…
jimin52 Oct 1, 2023
cf970dd
User API 경로 정리 (#777)
nyj001012 Oct 23, 2023
d7bca34
fix(cursus): Access-Control-Allow-Origin 설정 (#790)
nyj001012 Oct 25, 2023
53da6fd
chore: dependencies 업데이트 (#796)
nyj001012 Nov 6, 2023
6878ceb
fix: users/me 유저권한 all 로 변경
jimin52 Nov 27, 2023
f943af5
fix: 반납 3일 전 알림이 여러 번 전송됨 (#801)
nyj001012 Jan 13, 2024
8bee80b
fix: `dev/v2` 경로 복구 (#808)
scarf005 Jan 27, 2024
35a8192
security: 보안 취약점 해결 (#818)
nyj001012 Jan 27, 2024
59c4eec
fix(auth): /get/me시, id가 null이면 400 status code 반환 (#816)
nyj001012 Jan 27, 2024
7e6ccb3
fix: 이미지 빌드 에러 수정 (#820)
nyj001012 Feb 24, 2024
066d868
Merge branch 'main' into develop
nyj001012 Mar 2, 2024
3026751
fix: 스케줄러에 의한 예약 만료 및 재할당 코드 (#814)
jhj9109 Apr 27, 2024
2cc10c6
cd: nginx 설정 파일에 ssl 설정 추가 (#823)
nyj001012 Apr 27, 2024
9adc701
User API 경로 정리 (#777)
nyj001012 Oct 23, 2023
9060a48
Revert "User API 경로 정리 (#777)"
nyj001012 Apr 27, 2024
803a034
Merge branch 'main' into develop
nyj001012 Apr 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 15 additions & 129 deletions backend/src/v1/notifications/notifications.service.ts
Original file line number Diff line number Diff line change
@@ -1,138 +1,24 @@
import { executeQuery, makeExecuteQuery, pool } from '~/mysql';
import { executeQuery } from '~/mysql';
import { publishMessage } from '../slack/slack.service';
import { handleReservationOverdueAndAssignReservationToNextWaitingUser } from '../reservations/reservations.service';
import { logger } from '~/logger';

const succeedReservation = async (reservation: { bookId: number; bookInfoId: number }) => {
const conn = await pool.getConnection();
const transactionExecuteQuery = makeExecuteQuery(conn);
const sendSlackMessage = async (slack: string, message: string) => {
try {
const candidates: {
id: number;
slack: string;
title: string;
}[] = await transactionExecuteQuery(
`
SELECT
reservation.id AS id,
user.slack AS slack,
book_info.title AS title
FROM
reservation
LEFT JOIN user ON
user.id = reservation.userId
LEFT JOIN book_info ON
book_info.id = reservation.bookInfoId
WHERE
reservation.status = 0 AND
reservation.bookInfoId = ?
ORDER BY
reservation.createdAt DESC
LIMIT 1
`,
[reservation.bookInfoId],
);
if (candidates.length !== 0) {
await transactionExecuteQuery(
`
UPDATE
reservation
SET
bookId = ?,
endAt = DATE_ADD(NOW(), INTERVAL 3 DAY)
WHERE
reservation.id = ?
`,
[reservation.bookId, candidates[0].id],
);
publishMessage(
candidates[0].slack,
`:jiphyeonjeon: 예약 알림 :jiphyeonjeon:\n예약하신 도서 \`${candidates[0].title}\`(이)가 대출 가능합니다. 3일 내로 집현전에 방문해 대출해주세요. (방문하시기 전에 비치 여부를 확인해주세요)`,
);
}
} catch (e) {
await conn.rollback();
if (e instanceof Error) {
throw e;
}
} finally {
conn.release();
await publishMessage(slack, message);
} catch (error) {
logger.error('[scheduler error(slack)]', error);
}
};

export const notifyReservation = async () => {
const reservations: [
{
bookId: number;
bookInfoId: number;
},
] = await executeQuery(`
SELECT
reservation.bookId AS bookId,
reservation.bookInfoId AS bookInfoId
FROM
reservation
WHERE
reservation.status = 3 AND
DATE(reservation.updatedAt) = CURDATE()
`);
reservations.forEach(async (reservation) => {
if (reservation.bookId) {
succeedReservation(reservation);
}
});
};

export const notifyReservationOverdue = async () => {
const reservations: {
slack: string;
title: string;
bookId: number;
bookInfoId: number;
}[] = await executeQuery(`
SELECT
user.slack AS slack,
book_info.title AS title,
reservation.bookId AS bookId,
reservation.bookInfoId AS bookInfoId
FROM
reservation
LEFT JOIN user ON
user.id = reservation.userId
LEFT JOIN book_info ON
book_info.id = reservation.bookInfoId
WHERE
reservation.status = 3 AND
DATEDIFF(CURDATE(), DATE(reservation.endAt)) = 1
`);
reservations.forEach(async (reservation) => {
publishMessage(
reservation.slack,
`:jiphyeonjeon: 예약 만료 알림 :jiphyeonjeon:\n예약하신 도서 \`${reservation.title}\`의 예약이 만료되었습니다.`,
);
const ranks: [{ id: number; createdAt: Date }] = await executeQuery(
`
SELECT
id,
createdAt
FROM
reservation
WHERE
bookInfoId = ? AND status = 0
ORDER BY createdAt ASC
`,
[reservation.bookInfoId],
);
await executeQuery(
`
UPDATE reservation
SET
bookId = ?,
endAt = ADDDATE(CURDATE(),1)
WHERE
id = ?
`,
[reservation.bookId, ranks[0].id],
);
});
/**
* 만료된 예약을 처리하고, 다음 예약자에게 할당하고, 슬랙 메시지를 전송합니다.
* @throws 만료된 예약를 처리하고, 다음 예약자에게 할당하는 쿼리 과정에서 에러가 발생하면 에러를 던집니다. 슬랙 메시지 전송 실패시엔 로그만 남깁니다.
*/
export const notifyReservationOverdueAndNotifyReservation = async () => {
const { overDueReservations, assignedReservations } = await handleReservationOverdueAndAssignReservationToNextWaitingUser();
await Promise.allSettled(overDueReservations.map(({slack, title}) => sendSlackMessage(slack, `:jiphyeonjeon: 예약 만료 알림 :jiphyeonjeon:\n예약하신 도서 \`${title}\`의 예약이 만료되었습니다.`)));
await Promise.allSettled(assignedReservations.map((data) => sendSlackMessage(data!.slack, `:jiphyeonjeon: 예약 알림 :jiphyeonjeon:\n예약하신 도서 \`${data!.title}\`(이)가 대출 가능합니다. 3일 내로 집현전에 방문해 대출해주세요.`,)));
};

/**
Expand Down
122 changes: 122 additions & 0 deletions backend/src/v1/reservations/reservations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,128 @@ export const cancel = async (reservationId: number): Promise<void> => {
}
};

type TransactionExecuteQueryType = (queryText: string, values?: any[]) => Promise<any>;

/**
* 만료된 예약을 처리 후, 다음 예약자에게 예약을 할당하기 위한 예약 정보를 반환합니다.
* - 책이 할당 될때, status는 여전히 0이지만 endAt과 bookId가 null에서 각각 값이 할당됩니다.
* - 따라서 status가 0이면서 책이 할당 되어 bookId와 endAt을 가졌으면서 endAt이 현재 날짜보다 이전인 예약을 찾습니다.
* - status를 3을 부여하여 만료된 예약임을 표시합니다.
* @param transactionExecuteQuery 트랜잭션 처리를 위한 executeQuery
*/
const handleReservationOverdue = async (transactionExecuteQuery: TransactionExecuteQueryType) => {
const overDueReservations: {
slack: string;
title: string;
bookId: number;
bookInfoId: number;
}[] = await transactionExecuteQuery(`
SELECT
user.slack AS slack,
book_info.title AS title,
reservation.bookId AS bookId,
reservation.bookInfoId AS bookInfoId
FROM
reservation
LEFT JOIN user ON
user.id = reservation.userId
LEFT JOIN book_info ON
book_info.id = reservation.bookInfoId
WHERE
reservation.status = 0 AND
bookId IS NOT NULL AND
IFNULL(DATEDIFF(CURDATE(), DATE(reservation.endAt)), 0) >= 1;

`);
await transactionExecuteQuery(`
UPDATE
reservation
SET
reservation.status = 3
WHERE
reservation.id IN (
SELECT id
FROM (
SELECT
reservation.id
FROM
reservation
WHERE
reservation.status = 0 AND
reservation.bookId IS NOT NULL AND
IFNULL(DATEDIFF(CURDATE(), DATE(reservation.endAt)), 0) >= 1
) AS expiredReservations
);
`);
return overDueReservations;
}

/**
* 예약 취소 시, 다음 예약자에게 예약을 할당합니다.
* - 아직 책을 할당 받지 못한 예약자는 status가 0이고, bookId와 endAt이 null입니다.
* - 따라서 해당 조건을 만족하면서 가장 먼저 예약한 예약자를 찾아 해당 예약자에게 책을 할당합니다.
* @param transactionExecuteQuery 트랜잭션 처리를 위한 executeQuery
* @param bookData 책 정보, title, bookId, bookInfoId를 가지고 있습니다. 해당 책은 반드시 예약가능한 상태여야 합니다.
* @returns 인자로 받은 transactionExecuteQuery를 이용한 함수를 반환합니다. 해당 함수를 이용해 쿼리를 실행하고, 쿼리 실행 결과를 반환합니다. 반환된 값은 결과를 슬랙 메시지로 안내시 사용가능합니다.
*/
const assignReservationToNextWaitingUser = (transactionExecuteQuery: TransactionExecuteQueryType) => async (bookData: {title: string, bookId: number, bookInfoId: number}) => {

const firstWaitingReservation: { id: number, slack: string }[] = await transactionExecuteQuery(
`
SELECT
reservation.id AS id,
user.slack AS slack
FROM
reservation
LEFT JOIN user ON
user.id = reservation.userId
WHERE
bookInfoId = ? AND status = 0 AND bookId IS NULL AND endAt IS NULL
ORDER BY
reservation.createdAt ASC
LIMIT 1
FOR UPDATE;
`,
[bookData.bookInfoId],
);
if (firstWaitingReservation.length > 0) {
await transactionExecuteQuery(
`
UPDATE reservation
SET
bookId = ?,
endAt = ADDDATE(CURDATE(), 3)
WHERE
id = ?;
`,
[bookData.bookId, firstWaitingReservation[0].id],
);
return { title: bookData.title, slack: firstWaitingReservation[0].slack };
}
return null;
};

/**
* 만료된 예약을 처리하고, 다음 예약자에게 할당하고, 슬랙 메시지 전송을 위한 정보를 반환합니다.
* @returns 각각 만료된 예약, 다음 예약자에게 할당된 예약 정보를 반환합니다. 해당 정보는 슬랙 메시지 전송을 위해 사용됩니다.
*/
export const handleReservationOverdueAndAssignReservationToNextWaitingUser = async() => {
const conn = await pool.getConnection();
const transactionExecuteQuery = makeExecuteQuery(conn);
await conn.beginTransaction();
try {
const overDueReservations = await handleReservationOverdue(transactionExecuteQuery);
const assignedReservations = await Promise.all(overDueReservations.map(assignReservationToNextWaitingUser(transactionExecuteQuery)));
await conn.commit();
return { overDueReservations, assignedReservations };
} catch (error) {
await conn.rollback();
throw error;
} finally {
await conn.release();
}
}

export const userCancel = async (userId: number, reservationId: number): Promise<void> => {
const reservations = await executeQuery(
`
Expand Down
4 changes: 2 additions & 2 deletions backend/src/v1/utils/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const midnightScheduler = () => {
rule.tz = 'Asia/Seoul';
schedule.scheduleJob(rule, async () => {
await slack.updateSlackId();
await notifications.notifyReservationOverdue();
await searchKeywords.renewLastPopular();
});
};
Expand All @@ -24,7 +23,8 @@ const morningScheduler = () => {
rule.minute = 42;
rule.tz = 'Asia/Seoul';
schedule.scheduleJob(rule, async () => {
await notifications.notifyReservation();
await notifications.notifyReservationOverdueAndNotifyReservation();
await notifications.notifyReturningReminder();
await notifications.notifyOverdueManager();
await notifications.notifyOverdue();
});
Expand Down
18 changes: 17 additions & 1 deletion nginx/conf.d/default.conf
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
server {
listen 80;
listen 443 ssl;
server_name 42library.kr;

#access_log /var/log/nginx/host.access.log main;

ssl_certificate /etc/letsencrypt/live/42library.kr/fullchain.pem; # managed by Cert>
ssl_certificate_key /etc/letsencrypt/live/42library.kr/privkey.pem; # managed by Ce>

include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

location /api/ {
proxy_pass http://backend:3000;
}
Expand Down Expand Up @@ -42,3 +48,13 @@ server {
root /usr/share/nginx/html;
}
}

server {
if ($host = 42library.kr) {
return 301 https://$host$request_uri;
} # managed by Certbot

listen 80;
server_name 42library.kr;
return 404; # managed by Certbot
}
Loading