Skip to content

Commit

Permalink
Merge pull request #20 from Murakano/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
sen2y authored Jul 2, 2024
2 parents c1ce0e9 + b1814d2 commit 5b06f68
Show file tree
Hide file tree
Showing 16 changed files with 300 additions and 13 deletions.
4 changes: 2 additions & 2 deletions src/common/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ switch (process.env.NODE_ENV) {
};
conf.cookieInRefreshTokenOptions = {
httpOnly: true,
maxAge: 60 * 60 * 1000,
maxAge: 12 * 60 * 60 * 1000,
sameSite: 'Lax',
secure: true,
};
Expand All @@ -49,7 +49,7 @@ switch (process.env.NODE_ENV) {
};
conf.cookieInRefreshTokenOptions = {
httpOnly: true,
maxAge: 60 * 60 * 1000,
maxAge: 12 * 60 * 60 * 1000,
sameSite: 'Lax',
};
1;
Expand Down
6 changes: 6 additions & 0 deletions src/common/constants/error-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ const ErrorMessage = Object.freeze({
KAKAO_LOGIN_ERROR: '카카오 로그인중 오류가 발생하였습니다.',
NO_REFRESH_TOKEN: 'refresh token이 존재하지 않습니다.',
REFRESH_TOKEN_ERROR: 'refresh token 검증중 오류가 발생하였습니다.',

// WORD
RECENT_WORDS_ERROR: '최근 검색어 조회중 오류가 발생하였습니다.',
DELETE_RECENT_WORD_ERROR: '최근 검색어 삭제중 오류가 발생하였습니다.',
SEARCH_WORDS_ERROR: '검색 결과 조회 중 오류가 발생하였습니다.',
RANK_WORDS_ERROR: '인기 검색어 조회 중 오류가 발생하였습니다.',
});

module.exports = ErrorMessage;
9 changes: 9 additions & 0 deletions src/common/constants/success-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ const SucesssMessage = Object.freeze({
REFRESH_TOKEN: 'access token 발급 성공',

GET_PROFILE_SUCCESS: '유저 정보 조회 성공',

// WORD - 최근검색어
RECENT_WORDS_SUCCESS: '최근 검색어 조회 성공',
DELETE_RECENT_WORD_SUCCESS: '최근 검색어 삭제 성공',

// WORD - 검색어
SEARCH_WORDS_SUCCESS: '검색어 조회 성공',
SEARCH_WORDS_NONE: '검색 결과가 없습니다.',
RANK_WORDS_SUCCESS: '인기 검색어 조회 성공',
});

module.exports = SucesssMessage;
12 changes: 11 additions & 1 deletion src/common/utils/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ exports.generateAccessToken = (user) => {

exports.generateRefreshToken = (user) => {
return jwt.sign({ userId: user._id, nickname: user.nickname, email: user.email }, config.jwtRefreshSecret, {
expiresIn: '24h',
expiresIn: '12h',
});
};

Expand Down Expand Up @@ -66,3 +66,13 @@ exports.isNotLoggedIn = async (req, res, next) => {
}
}
};

exports.isUser = async (req, res, next) => {
try {
const user = await authenticateJWT(req, res);
req.user = user;
next();
} catch (err) {
next();
}
};
2 changes: 1 addition & 1 deletion src/common/utils/response-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const sendResponse = {
badRequest: (res, dto) => {
if (dto) {
res.type('application/json');
return res.status(HTTP_STATUS_CODE.BAD_REQUEST).json({ message: dto });
return res.status(HTTP_STATUS_CODE.BAD_REQUEST).json(dto);
} else {
return res.status(HTTP_STATUS_CODE.BAD_REQUEST).json({ message: ErrorMessage.BAD_REQUEST });
}
Expand Down
35 changes: 31 additions & 4 deletions src/routes/user/user.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,11 @@ exports.register = async (req, res) => {
exports.isNicknameExist = async (req, res) => {
try {
const { nickname } = validateRequest(nicknameCheckReqQuerySchema, req.query);

const isUserExist = await userService.isNicknameExist(nickname);
data = { isUserExist };

if (isUserExist) {
return sendResponse.badRequest(res, {
return sendResponse.ok(res, {
message: ErrorMessage.EXIST_NICKNAME,
data,
});
Expand All @@ -55,7 +54,7 @@ exports.isNicknameExist = async (req, res) => {
});
} catch (err) {
if (err?.type) {
return sendResponse.badRequest(res, err.message);
return sendResponse.badRequest(res, err);
}
sendResponse.fail(req, res, ErrorMessage.NICKNAME_CHECK_ERROR);
}
Expand All @@ -69,7 +68,7 @@ exports.isEmailExist = async (req, res) => {
const data = { isUserExist };

if (isUserExist) {
return sendResponse.badRequest(res, {
return sendResponse.ok(res, {
message: ErrorMessage.EXIST_EMAIL,
data,
});
Expand Down Expand Up @@ -188,3 +187,31 @@ exports.logout = (_, res) => {
message: SucesssMessage.LOGOUT_SUCCESS,
});
};

exports.recentSearches = async (req, res) => {
try {
const { _id } = req.user;
const recentSearches = await userService.getRecentSearches(_id);
sendResponse.ok(res, {
message: SucesssMessage.RECENT_WORDS_SUCCESS,
data: { recentSearches },
});
} catch (err) {
console.log(err);
sendResponse.fail(req, res, ErrorMessage.RECENT_WORDS_ERROR);
}
};

exports.delRecentSearch = async (req, res) => {
try {
const { _id } = req.user;
const { searchTerm } = req.params;
await userService.delRecentSearch(_id, searchTerm);
sendResponse.ok(res, {
message: SucesssMessage.DELETE_RECENT_WORD_SUCCESS,
});
} catch (err) {
console.log(err);
sendResponse.fail(req, res, ErrorMessage.DELETE_RECENT_WORD_ERROR);
}
};
36 changes: 31 additions & 5 deletions src/routes/user/user.model.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');

const requestSchema = new mongoose.Schema(
{
word: { type: String, required: true },
awkPron: [{ type: String }],
comPron: [
{
type: String,
required: function () {
return this.type === 'mod';
},
},
],
info: { type: String },
type: { type: String, enum: ['add', 'mod'], required: true },
status: { type: String, enum: ['ped', 'rej', 'app'], default: 'pend' },
deletedAt: { type: Date, default: null },
},
{ timestamps: true }
);

const userSchema = new mongoose.Schema(
{
deletedAt: { type: Date, default: null },
Expand All @@ -13,13 +33,19 @@ const userSchema = new mongoose.Schema(
match: /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/,
},
password: { type: String, minLength: 8, maxLength: 20 },
role: { type: String, default: 'user' },
role: { type: String, enum: ['user', 'admin'], default: 'user' },
snsId: { type: String, default: null },
provider: { type: String, default: null },
provider: { type: String, enum: ['kakao'], default: null },
recentSearches: [
{
searchTerm: { type: String, required: true },
updatedAt: { type: Date, default: Date.now },
deletedAt: { type: Date, default: null },
},
],
requests: [requestSchema],
},
{
timestamps: { currentTime: () => Date.now() },
}
{ timestamps: true }
);

userSchema.pre('save', async function (next) {
Expand Down
57 changes: 57 additions & 0 deletions src/routes/user/user.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,60 @@ exports.getUserBySnsId = async (snsId) => {
const user = await User.findOne({ snsId, provider: 'kakao' });
return user;
};

exports.getRecentSearches = async (_id) => {
try {
// 사용자 문서를 찾고 recentSearches 배열만 선택합니다.
const user = await User.findById(_id).select('recentSearches').exec();

// 최근 검색어 배열을 필터링, 정렬하고 상위 10개를 선택합니다.
// recentSearches가 정의되지 않았을 경우 빈 배열로 처리합니다.
const recentSearches = (user.recentSearches || [])
.filter((search) => !search.deletedAt) // 삭제되지 않은 검색어만 선택
.sort((a, b) => b.updatedAt - a.updatedAt) // 최근 검색순으로 정렬
.slice(0, 10); // 상위 10개 선택

// 검색어만 반환
return recentSearches.map((search) => search.searchTerm);
} catch (err) {
console.error(err);
return []; // 오류 발생 시 빈 배열 반환
}
};

exports.delRecentSearch = async (_id, searchTerm) => {
try {
await User.findOneAndUpdate(
{ _id, 'recentSearches.searchTerm': searchTerm, 'recentSearches.deletedAt': null },
{ $set: { 'recentSearches.$.deletedAt': Date.now() } }
);
} catch (err) {
console.error(err);
}
};

exports.updateRecentSearch = async (_id, searchTerm) => {
try {
const user = await User.findById(_id).exec();
if (!user) {
console.log('User not found');
}
const recentSearch = user.recentSearches.find((search) => search.searchTerm === searchTerm);
if (recentSearch) {
// 검색어가 이미 존재하는 경우
if (recentSearch.deletedAt) {
// deletedAt이 null이 아닌 경우, deletedAt을 null로 바꾸고 updatedAt 수정
recentSearch.deletedAt = null;
}
// updatedAt만 수정
recentSearch.updatedAt = Date.now();
} else {
// 검색어가 존재하지 않는 경우, updatedAt과 용어 추가
user.recentSearches.push({ searchTerm, updatedAt: Date.now() });
}

await user.save();
} catch (err) {
console.error(err);
}
};
6 changes: 6 additions & 0 deletions src/routes/user/user.route.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const {
getProfile,
refreshToken,
logout,
recentSearches,
delRecentSearch,
} = require('./user.controller');
const { isLoggedIn, isNotLoggedIn } = require('../../common/utils/auth');
const userRouter = express.Router();
Expand All @@ -25,4 +27,8 @@ userRouter.post('/logout', logout);

userRouter.get('/profile', isLoggedIn, getProfile);

// 최근 검색어
userRouter.get('/recent', isLoggedIn, recentSearches); // 최근 검색어 조회
userRouter.delete('/:searchTerm', isLoggedIn, delRecentSearch); // 최근 검색어 삭제

module.exports = userRouter;
16 changes: 16 additions & 0 deletions src/routes/user/user.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,19 @@ exports.isKaKaoUserExist = async (snsId) => {
const user = await userRepository.getUserBySnsId(snsId);
return user;
};

exports.getRecentSearches = async (userId) => {
const recentSearches = await userRepository.getRecentSearches(userId);
return recentSearches;
};

exports.delRecentSearch = async (userId, searchTerm) => {
return await userRepository.delRecentSearch(userId, searchTerm);
};

// 최근 검색어 저장
exports.updateRecentSearch = async (userID, searchTerm) => {
if (userID) {
await userRepository.updateRecentSearch(userID, searchTerm);
}
};
47 changes: 47 additions & 0 deletions src/routes/word/word.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const wordService = require('./word.service');
const userService = require('../user/user.service');
const sendResponse = require('../../common/utils/response-handler');
const ErrorMessage = require('../../common/constants/error-message');
const SucesssMessage = require('../../common/constants/success-message');
const { validateRequest } = require('../../common/utils/request.validator');
const { rankWordsSchema, searchTermSchema } = require('./word.schema');

exports.getRankWords = async (req, res) => {
try {
const data = await wordService.getRankWords();
sendResponse.ok(res, {
message: SucesssMessage.RANK_WORDS_SUCCESS,
data,
});
} catch (error) {
sendResponse.fail(req, res, ErrorMessage.RANK_WORDS_ERROR);
}
};

exports.getSearchWords = async (req, res) => {
try {
const _id = req?.user ? req.user._id : null;

// 검색어 검증
const validData = validateRequest(searchTermSchema, req.params);
// 요청 파라미터에서 검색어 추출
const searchTerm = validData.searchTerm;
// 검색어 조회
const data = await wordService.getSearchWords(searchTerm);

if (_id) {
await userService.updateRecentSearch(_id, searchTerm);
}
const message = data ? SucesssMessage.SEARCH_WORDS_SUCCESS : SucesssMessage.SEARCH_WORDS_NONE;
sendResponse.ok(res, {
message,
data,
});
} catch (error) {
console.log(error);
if (error?.type) {
return sendResponse.badRequest(res, error.message);
}
sendResponse.fail(req, res, ErrorMessage.SEARCH_WORDS_ERROR);
}
};
21 changes: 21 additions & 0 deletions src/routes/word/word.model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const mongoose = require('mongoose');

const wordSchema = new mongoose.Schema(
{
word: { type: String, required: true, unique: true }, //콜렉션 이름과 겹치는데 상관없나..
awkPron: { type: String }, // 어색한 발음
comPron: { type: String, required: true }, // 일반적인 발음
info: { type: String }, //추가정보
suggestedBy: { type: String }, // 제안한 사용자의 닉네임
freq: { type: Number, default: 0 }, // 인기검색어
},
{ timestamps: true }
);

// 검색 시마다 검색어의 freq를 1씩 증가시키는 미들웨어
wordSchema.pre(/^findOne/, async function (next) {
await this.model.updateOne(this.getQuery(), { $inc: { freq: 1 } });
next();
});

module.exports = mongoose.model('Word', wordSchema);
28 changes: 28 additions & 0 deletions src/routes/word/word.repository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const Word = require('./word.model');

exports.getSearchWords = async (searchTerm) => {
try {
// 대소문자 구분 없이 검색어를 찾기 위한 정규 표현식 사용
const searchWords = await Word.findOne({ word: new RegExp(`^${searchTerm}$`, 'i') });

if (!searchWords) {
console.log('Search term not found in Word collection');
}

return searchWords;
} catch (error) {
console.log('Error while getting search words:', error);
return null;
}
};

exports.getRankWords = async () => {
try {
const words = await Word.find().sort({ freq: -1 }).limit(10);
const wordNames = words.map((word) => word.word);
return wordNames;
} catch (error) {
console.log('Error while getting rank words:', error);
return null;
}
};
Loading

0 comments on commit 5b06f68

Please sign in to comment.