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

[#44] 회원 탈퇴 api #60

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from
Open
13 changes: 9 additions & 4 deletions src/controllers/quizController.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const saveQuizResult = async (req, res, next) => {
const payload = await verifyToken(token);
const userId = payload.id;

// TODO: getUserNumId service 만들어서 사용
// TODO: getUserNumIdByToken service 만들어서 사용
const getUserIdResult = await pool.query(userQuery.getUserId, userId);
const userNumId = getUserIdResult[0][0]?.id;

Expand Down Expand Up @@ -112,16 +112,20 @@ const saveQuizResult = async (req, res, next) => {
// 이전에 맞힌 적이 없다면
if (isSolved == false) {
// 문제 풀었음을 표기 solved_quizzes
connection.query(quizQuery.recordQuizSolved, [quizId, userNumId]);
if (solvedQuizCount === 1)
await connection.query(quizQuery.recordQuizSolved, [
quizId,
userNumId,
]);
// 통계 데이터를 반영 quiz_accuracy_statistics;
connection.query(quizQuery.updateQuizStatistics, [
await connection.query(quizQuery.updateQuizStatistics, [
solvedQuizCount,
totalQuizCount,
quizId,
]);
}

connection.commit();
await connection.commit();
} catch (error) {
console.error("퀴즈 결과 저장 트렌젝션 쿼리 에러 ,", err);
await connection.rollback();
Expand All @@ -132,6 +136,7 @@ const saveQuizResult = async (req, res, next) => {

return res.status(StatusCodes.NO_CONTENT).end();
} catch (err) {
console.error("퀴즈 결과 저장 트렌젝션 쿼리 에러 ,", err);
next(err);
}
};
Expand Down
92 changes: 90 additions & 2 deletions src/controllers/userController.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ const createHttpError = require("http-errors");
const pool = require("../db/mysqldb");
const userQuery = require("../queries/userQuery.js");
const scoreQuery = require("../queries/scoreQuery.js");
const quizQuery = require("../queries/quizQuery.js");
const { COOKIE_OPTION } = require("../constant/constant.js");
const { gerRankInfo } = require("../services/rankService.js");

const {
convertHashPassword,
generateSalt,
getUserNumIdByToken,
} = require("../services/userService.js");
const { verifyToken } = require("../services/jwtService.js");

Expand Down Expand Up @@ -264,13 +266,19 @@ const resetPassword = async (req, res, next) => {
"solvedCount": 30// 지금까지 푼 문제 수
}
*/
// src/controllers/userController.js
const mypage = async (req, res, next) => {
try {
const token = req.cookies?.token;
const payload = await verifyToken(token);
const userId = payload.id;
const getUserIdResult = await pool.query(userQuery.getUserId, userId);
const userNumId = getUserIdResult[0][0]?.id;
const userIdResult = await pool.query(userQuery.getUserId, userId);
console.log("userIdResult : ", userIdResult);
console.log("userIdResult[0] : ", userIdResult[0]);
console.log("userIdResult[0][0] : ", userIdResult[0][0]);
const userNumId = userIdResult[0][0]?.id;

console.log("userNumId : ", userNumId);

if (!userNumId) {
throw createHttpError(
Expand Down Expand Up @@ -303,6 +311,85 @@ const mypage = async (req, res, next) => {
}
};

/** 회원탈퇴
* 유저 정보를 토큰에서 뽑아냄
*
* 뽑아낸 유저 정보를 가지고 아래 테이블들에서 삭제 진행
* - user
* - solved_quizzes
* - score
* TO BE
* - 무한 퀴즈 챌린지, 유저 데이터
*
* # 고려사항
* - 탈퇴한 유저의 기록은 지울 것인가?
*/
// src/controllers/userController.js
const removeUserAccount = async (req, res, next) => {
try {
const token = req.cookies?.token;
const userNumId = await getUserNumIdByToken(token);
const findUserInfoQuery = `SELECT * FROM user WHERE id = ?`;
const findUserInfoQueryResult = await pool.query(
findUserInfoQuery,
userNumId
);

console.log(
"findUserInfoQueryResult[0][0] : ",
findUserInfoQueryResult[0][0]
);

const findUserScoreInfoQuery = `SELECT * FROM score WHERE user_id = ?`;
const findUserScoreInfoQueryResult = await pool.query(
findUserScoreInfoQuery,
userNumId
);

console.log(
"findUserScoreInfoQueryResult[0][0] : ",
findUserScoreInfoQueryResult[0][0]
);

const findSolvedQuizHistoryQuery = `SELECT * FROM solved_quizzes WHERE user_id = ?`;
const findSolvedQuizHistoryQueryResult = await pool.query(
findSolvedQuizHistoryQuery,
userNumId
);

console.log(
"findSolvedQuizHistoryQueryResult[0][0] : ",
findSolvedQuizHistoryQueryResult[0][0]
);

const connection = await pool.getConnection();
try {
await connection.beginTransaction();

// user, score, solved_quizzes 테이블 속 계정 삭제
await connection.query(scoreQuery.removeUserScoreHistory, userNumId);
await connection.query(quizQuery.removeUserSolvedQuizHistory, userNumId);
// user id를 FK로 가진 테이블의 데이터들부터 삭제한 뒤에 user 테이블에서 유저 정보 삭제
await connection.query(userQuery.removeUserAccount, userNumId);

await connection.commit();
} catch (err) {
console.error("회원 탈퇴 트렌젝션 쿼리 에러 ,", err);
await connection.rollback();
next(err);
} finally {
connection.release();
}

res.clearCookie("token", COOKIE_OPTION);

return res.status(StatusCodes.NO_CONTENT).end();
} catch (err) {
console.error("removeUserAccount : ", err);
next(err);
}
};

module.exports = {
join,
checkLoginId,
Expand All @@ -312,4 +399,5 @@ module.exports = {
isAvailablePassword,
resetPassword,
mypage,
removeUserAccount,
};
5 changes: 3 additions & 2 deletions src/db/mysqldb.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// mysql 모듈 소환
const mysqldb = require("mysql2/promise");
// import { createPool } from "mysql2/promise";
const { createPool } = require("mysql2/promise");

const pool = mysqldb.createPool({
const pool = createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
Expand Down
1 change: 1 addition & 0 deletions src/queries/quizQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ exports.updateQuizStatistics = `UPDATE quiz_accuracy_statistics \
SET correct_people_count = correct_people_count + ?, \
total_attempts_count_before_correct = total_attempts_count_before_correct + ? \
WHERE quiz_id = ?`;
exports.removeUserSolvedQuizHistory = `DELETE FROM solved_quizzes WHERE user_id = ?`;
5 changes: 1 addition & 4 deletions src/queries/scoreQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,4 @@ exports.getRankingPagesInfo = `SELECT u.user_id AS id, \
ON u.id = s.user_id
LIMIT ? \
OFFSET ?`;
// UPDATE your_table_name
// SET correct_people_count = correct_people_count + 0,
// total_attempts_count_before_correct = total_attempts_count_before_correct + 1
// WHERE quiz_id = 1;
exports.removeUserScoreHistory = `DELETE FROM score WHERE user_id = ?`;
1 change: 1 addition & 0 deletions src/queries/userQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ exports.getUserInfo = `SELECT user_id, password, salt FROM user WHERE user_id =
exports.getUserPasswordInfo = `SELECT password, salt FROM user WHERE user_id = ?`;
exports.resetPassword = `UPDATE user SET password = ?, salt = ? WHERE user_id = ?`;
exports.getThreeUsersInfo = `SELECT user_id, id FROM user WHERE id IN (?, ?, ?)`;
exports.removeUserAccount = `DELETE FROM user WHERE id = ?`;

// Function to dynamically generate query for up to 3 user IDs
exports.getThreeUsersInfoQuery = (ids) => {
Expand Down
3 changes: 0 additions & 3 deletions src/routes/quiz.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,14 @@ const {
saveQuizResult,
} = require("../controllers/quizController");
const trimMiddleware = require("../middlewares/trimMiddleware");
const validationMiddleware = require("../middlewares/validationMiddleware");
const quizValidators = require("../validators/quizValidators.js");

router.get("/", generateQuiz);
router.get("/:quizId/mark", markQuizAnswer);
router.post(
"/result",
isAuthenticated,
trimMiddleware,
quizValidators.saveQuizResult,
validationMiddleware,
saveQuizResult
);

Expand Down
3 changes: 3 additions & 0 deletions src/routes/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const {
isCurrentPassword,
resetPassword,
mypage,
removeUserAccount,
} = require("../controllers/userController");

router.post(
Expand Down Expand Up @@ -83,4 +84,6 @@ router.put(
resetPassword
);

router.delete("/account", isAuthenticated, removeUserAccount);

module.exports = router;
43 changes: 0 additions & 43 deletions src/services/jwtService.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,47 +23,4 @@ const verifyToken = async (token) => {
}
};

/*
const verifyToken = async (token) => {
try {
const decoded = jwt.verify(token, process.env.JWT_PRIVATE_KEY);
return decoded;
} catch (err) {
if (err.name === "TokenExpiredError") {
throw createHttpError(StatusCodes.UNAUTHORIZED, "토큰이 만료되었습니다.");
} else {
throw createHttpError(
StatusCodes.UNAUTHORIZED,
"토큰 인증에 실패 하셨습니다."
);
}
}
};
*/

/**
const verifyToken = async (token) => {
return await new Promise((resolve, reject) => {
jwt.verify(token, process.env.JWT_PRIVATE_KEY, (err, decoded) => {
if (err) {
if (err.name === "TokenExpiredError") {
reject(
createHttpError(StatusCodes.UNAUTHORIZED, "토큰이 만료되었습니다.")
);
} else {
reject(
createHttpError(
StatusCodes.UNAUTHORIZED,
"토큰 인증에 실패 하셨습니다."
)
);
}
} else {
resolve(decoded);
}
});
});
};
*/

module.exports = { verifyToken };
14 changes: 9 additions & 5 deletions src/services/rankService.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,19 @@ const topThreeRankerInfo = async (scoreInfos) => {
};
};

// TODO: SQL로 순위도 가져오도록 수정
const nearThreeRankerInfo = async (scoreInfos, myScoreInfoIdx) => {
// myScoreInfoIdx 인근 +- 1
// 내가 1등인 경우 1,2,3등
// 내가 마지막 순위인 경우 내 위로 2단계 이전부터 시작
let nearThreeUserNumIds = [];
// 내가 1등인 경우
if (myScoreInfoIdx === 0) {
result["nearRankers"] = topUserRanks;
result["nearRankersCount"] = topUserRanks.length;
const topRankers = await topThreeRankerInfo(scoreInfos);

return {
nearRankers: topRankers["topRankers"],
};
} else {
let idx = myScoreInfoIdx - 1;

Expand All @@ -123,7 +127,7 @@ const nearThreeRankerInfo = async (scoreInfos, myScoreInfoIdx) => {
userQuery.getThreeUsersInfoQuery(nearThreeUserNumIds);
const userInfosQueryResult = await pool.query(query, params);
const userInfos = userInfosQueryResult[0];
let nearThreRanks = [];
let nearThreeRanks = [];
idx = myScoreInfoIdx - 1;

// 현재 내 순위가 마지막이고 전체 유저 수가 3명 이상일 때
Expand All @@ -135,7 +139,7 @@ const nearThreeRankerInfo = async (scoreInfos, myScoreInfoIdx) => {
const userId = scoreInfos[idx]["user_id"];
const userInfo = userInfos.find((user) => user.id === userId);
if (userInfo) {
nearThreRanks.push({
nearThreeRanks.push({
id: userInfo["user_id"],
rank: idx + 1,
score: scoreInfos[idx]["total_score"],
Expand All @@ -145,7 +149,7 @@ const nearThreeRankerInfo = async (scoreInfos, myScoreInfoIdx) => {
}
}
return {
nearRankers: nearThreRanks,
nearRankers: nearThreeRanks,
};
}
};
Expand Down
32 changes: 31 additions & 1 deletion src/services/userService.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// src/services/userService.js
const crypto = require("crypto");

const {
Expand All @@ -6,6 +7,11 @@ const {
DIGEST_ALGORITHM,
ENCODING_STYLE,
} = require("../constant/constant.js");
const { verifyToken } = require("./jwtService.js");
const pool = require("../db/mysqldb.js");
const userQuery = require("../queries/userQuery.js");
const createHttpError = require("http-errors");
const { StatusCodes } = require("http-status-codes");

const generateSalt = () => {
return crypto.randomBytes(SALT_BYTE_SEQUENCE_SIZE).toString(ENCODING_STYLE);
Expand All @@ -25,4 +31,28 @@ const convertHashPassword = (password, salt) => {
return hashPassword;
};

module.exports = { convertHashPassword, generateSalt };
const getUserNumIdByToken = async (token) => {
try {
const payload = await verifyToken(token);
const userId = payload.id;
const userIdResult = await pool.query(userQuery.getUserId, userId);
const userNumId = userIdResult[0][0]?.id;

if (!userNumId) {
throw createHttpError(
StatusCodes.NOT_FOUND,
"사용자 정보를 찾을 수 없습니다."
);
}

return userNumId;
} catch (err) {
console.error("Fatal: 유저의 아이디를 찾을 수 없습니다.");
throw createHttpError(
StatusCodes.NOT_FOUND,
"사용자 정보를 찾을 수 없습니다."
);
}
};

module.exports = { convertHashPassword, generateSalt, getUserNumIdByToken };
9 changes: 9 additions & 0 deletions src/validators/quizValidators.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ const quizValidators = {
.withMessage("맞춘 총 점수는 문자열이 아닌 정수여야 합니다.")
.isInt({ min: 0 })
.withMessage("맞춘 총 점수는 0 이상의 정수여야 합니다."),
body("quizId")
.exists()
.withMessage("퀴즈ID가 존재해야 합니다.")
.notEmpty()
.withMessage("퀴즈ID는 비어 있을 수 없습니다.")
.custom(ensureInt)
.withMessage("퀴즈ID는 문자열이 아닌 정수여야 합니다.")
.isInt({ min: 0 })
.withMessage("퀴즈ID는 0 이상의 정수여야 합니다."),
],
};

Expand Down