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

Features/header 검색결과 조회, 최근검색어 조회 및 삭제 api 로직 구현 #16

Merged
merged 7 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions src/common/constants/error-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ 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: '검색 결과 조회 중 오류가 발생하였습니다.',
});

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

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

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

// WORD - 검색어
SEARCH_WORDS_SUCCESS: '검색어 조회 성공',
});

module.exports = SucesssMessage;
28 changes: 28 additions & 0 deletions src/routes/user/user.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,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);
}
};
4 changes: 2 additions & 2 deletions src/routes/user/user.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ 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, enum: ['USER', 'ADMIN'], default: 'USER' },
role: { type: String, enum: ['user', 'ADMIN'], default: 'user' },
snsId: { type: String, default: null },
provider: { type: String, enum: ['GOOGLE', 'KAKAO', 'NAVER'], default: null },
provider: { type: String, enum: ['kakao'], default: null },
recentSearches: [
{
searchTerm: { type: String, required: true },
Expand Down
58 changes: 58 additions & 0 deletions src/routes/user/user.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,61 @@ 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;
9 changes: 9 additions & 0 deletions src/routes/user/user.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,12 @@ 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);
};
28 changes: 28 additions & 0 deletions src/routes/word/word.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const wordService = require('./word.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) => {};

exports.getSearchWords = async (req, res) => {
try {
const { _id } = req.user;
const validData = validateRequest(searchTermSchema, req.params); // 검색어 검증
const searchTerm = validData.searchTerm; // 요청 파라미터에서 검색어 추출
const data = await wordService.getSearchWords(_id, searchTerm); // 검색어 조회

sendResponse.ok(res, {
message: SucesssMessage.SEARCH_WORDS_SUCCESS,
data,
});
} catch (error) {
console.log(error);
if (error?.type) {
return sendResponse.badRequest(res, error.message);
}
sendResponse.fail(req, res, ErrorMessage.SEARCH_WORDS_ERROR);
}
};
6 changes: 6 additions & 0 deletions src/routes/word/word.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,10 @@ const wordSchema = new mongoose.Schema(
{ 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);
16 changes: 16 additions & 0 deletions src/routes/word/word.repository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const Word = require('./word.model');

exports.getSearchWords = async (searchTerm, user) => {
try {
// 검색 결과
const searchWords = await Word.findOne({ word: searchTerm });

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

return searchWords;
} catch (error) {
console.log('Error while getting search words:', error);
}
};
8 changes: 8 additions & 0 deletions src/routes/word/word.route.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
const express = require('express');
const wordRouter = express.Router();

const { getRankWords, getSearchWords, getRelatedWords } = require('./word.controller');
const { isLoggedIn } = require('../../common/utils/auth');

// 메인 검색
wordRouter.get('/rank', getRankWords); // 인기 검색어 조회

// wordRouter.get('/:searchTerm', getRelatedWords); // params로 전달받은 검색어 searchTerm을 포함하는 단어 조회
wordRouter.get('/search/:searchTerm', isLoggedIn, getSearchWords);

module.exports = wordRouter;
13 changes: 13 additions & 0 deletions src/routes/word/word.schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const searchTermSchema = {
type: 'object',
properties: {
searchTerm: {
type: 'string',
maxLength: 50,
},
},
required: ['searchTerm'],
additionalProperties: false,
};

module.exports = { searchTermSchema };
16 changes: 16 additions & 0 deletions src/routes/word/word.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const wordRepository = require('./word.repository');
const userRepository = require('../user/user.repository');

exports.getSearchWords = async (_id, searchTerm) => {
// 최근 검색어 저장 로직
const saveRecentSearch = userRepository.updateRecentSearch(_id, searchTerm);

// 검색 결과
const searchWords = wordRepository.getSearchWords(_id, searchTerm);

// 모든 비동기 작업이 완료될 때까지 기다린 후 결과를 반환
await Promise.all([saveRecentSearch, searchWords]);

// 검색 결과 return
return searchWords;
};