최근 검색한 단어
2023년 3월 13일
intro
input 태그에 focus가 되면 최근 검색한 단어가 최대 5개까지 보이는 것을 구현
구현할 기능:
- 최근 검색한 단어를 클릭하면 그 단어를 검색
- 최근 검색한 단어 목록에 단어를 지우는 기능
- 최근 검색한 단어 목록에 이미 있는 단어를 입력하면 지운 뒤 저장
- 최근에 검색한 단어 일수록 위에 위치
구현할 기능 미리 보기
code
stores/Main.js
stores/Main.js
import { ref } from 'vue';
import { defineStore } from 'pinia';
import { useStorage } from '@vueuse/core';
import { useModalStore } from './Modal';
export const useMainStore = defineStore('main', () => {
const modalStore = useModalStore();
// 휴지통, 메인화면 전환 0: 메인, 1: 휴지통
const screenTransition = ref(0);
const localWords = useStorage('mapWords', new Map(), localStorage);
const localTrashCan = useStorage('trashCan', new Map(), localStorage);
const localRecentSearchWords = useStorage('recentWords', [], localStorage);
const wordArr = ref([]);
function getTimeAndTimestamp() {
const date = new Date();
const nowTime = `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`;
const timestamp = date.getTime();
return { date, nowTime, timestamp };
}
function getTimeAndTimestampAfterDay(day) {
const { date, nowTime, timestamp} = getTimeAndTimestamp();
const afterDate = new Date(date.setDate(date.getDate() + day));
const afterTimestamp = afterDate.getTime();
const afterTime = `${afterDate.getFullYear()}-${afterDate.getMonth()+1}-${afterDate.getDate()} ${afterDate.getHours()}:${afterDate.getMinutes()}`;
return { nowTime, timestamp, afterTime, afterTimestamp };
}
function wordArrUpdate() {
const localWordsToArr = Array.from(localWords.value, (item) => {
return { ...item[1] };
} );
wordArr.value = [...localWordsToArr].reverse();
}
// init: localStorage에 있는 단어들 set
function setWordDic() {
wordArrUpdate();
// 휴지통 갱신
const { timestamp } = getTimeAndTimestamp();
for (const [key, value] of localTrashCan.value) {
if (value.afterTimestamp <= timestamp) {
localTrashCan.value.delete(key);
}
}
}
// word add
function wordAdd(word, means) {
const { nowTime, timestamp } = getTimeAndTimestamp();
const item = { word, means: means.toString(), timestamp, time: nowTime, check: false };
// 이미 단어가 존재하면 지웠다가 저장
if (localWords.value.has(word)) {
localWords.value.delete(word);
}
localWords.value.set(word, item);
wordArrUpdate();
modalStore.modalExit();
}
// word check
function wordCheck(targetWord, ch, index) {
wordArr.value[index].check = ch;
const checkedWord = localWords.value.get(targetWord);
checkedWord.check = ch
localWords.value.set(targetWord, checkedWord);
}
// word delete
function wordDelete(targetWord) {
const { nowTime, timestamp, afterTime, afterTimestamp} = getTimeAndTimestampAfterDay(14);
const deleteWord = { word: targetWord, means: localWords.value.get(targetWord).means, time: nowTime, timestamp, afterTimestamp, afterTime };
localWords.value.delete(targetWord);
wordArrUpdate();
// 휴지통에 이미 존재하는 단어면 지운뒤 저장
if (localTrashCan.value.has(targetWord)) {
localTrashCan.value.delete(targetWord);
}
localTrashCan.value.set(targetWord, deleteWord);
modalStore.modalExit();
}
// 화면간 단어 <-> 휴지통 교체
function contentChange() {
screenTransition.value = screenTransition.value === 0 ? 1 : 0;
}
// word detail
function wordDetail(targetWord) {
const { word, means, time } = localWords.value.get(targetWord);
const detailWord = { word, means: means.split(','), time };
return detailWord;
}
// trashCan word detail
function trashCanWordDetail(targetWord) {
const { word, means, afterTime, time } = localTrashCan.value.get(targetWord);
const detailTrashCanWord = { word, means: means.split(','), time, afterTime };
return detailTrashCanWord;
}
// trashCan word kill
function trashCanWordKill(targetWord) {
localTrashCan.value.delete(targetWord);
modalStore.modalExit();
}
// trashCan word restore
function trashCanWordRestore(targetWord) {
const { word, means } = localTrashCan.value.get(targetWord);
wordAdd(word, means);
trashCanWordKill(word);
}
// recent words update
function recentWordUpdate(searchWord) {
// 중복 확인
const repeatCheckIndex = localRecentSearchWords.value.findIndex((word) => word === searchWord);
const recentWords = repeatCheckIndex !== -1 ? [...localRecentSearchWords.value.slice(0, repeatCheckIndex), ...localRecentSearchWords.value.slice(repeatCheckIndex+1)] : [...localRecentSearchWords.value];
recentWords.length >= 5 ? recentWords.pop() : '';
localRecentSearchWords.value = [searchWord, ...recentWords];
}
// recent words delete
function recentWordDelete(index) {
const recentWords = [...localRecentSearchWords.value.slice(0, index), ...localRecentSearchWords.value.slice(index+1)];
localRecentSearchWords.value = [...recentWords];
}
return {
wordArr,
localTrashCan,
localRecentSearchWords,
screenTransition,
setWordDic,
wordAdd,
wordCheck,
wordDelete,
contentChange,
wordDetail,
trashCanWordDetail,
trashCanWordKill,
trashCanWordRestore,
recentWordUpdate,
recentWordDelete,
};
})
localRecentSearchWords
: 로컬 스토리지에 최근 검색한 단어를 배열 형태로 저장recentWordUpdate()
: 최근 입력한 단어를 맨 뒤에 저장 하는 함수, 이미 입력한 단어가 있으면 삭제 후 맨 뒤로 저장하며 길이는 최대 5recentWordDelete()
:WordInput.vue
에서 삭제 버튼을 누르면 호출되는 함수, 타켓 단어의 인덱스를 이용해 삭제
stores/InputWord.js
stores/InputWord.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { useModalStore } from './Modal';
import { fetchWordSearch } from '../api';
import { useMainStore } from './Main';
export const useInputWordStore = defineStore('inputWord', () => {
const modalStore = useModalStore();
const mainStore = useMainStore();
const modalWord = ref('');
const modalMeans = ref('');
const modalSimilarWords = ref('');
function dataInitialization() {
modalWord.value ='';
modalMeans.value = [];
modalSimilarWords.value = [];
}
function createSimilarWords(wordAndMean) {
const MAX = wordAndMean.length >= 3 ? 3 : wordAndMean.length;
return wordAndMean.slice(0, MAX).map(item => item[1]);
}
function checkWordSame(word1, word2) {
const w1 = word1.toLowerCase();
const w2 = word2.toLowerCase();
if (w1 === w2) {
return true;
}
return false;
}
function caseNotExistenceWord(targetWord) {
dataInitialization();
modalWord.value = targetWord;
modalMeans.value = ['없는 단어입니다.', '다른 단어를 입력해 주세요.'];
modalStore.inputNotExistModal = true;
}
function caseSimilarWord(targetWord, wordAndMean) {
dataInitialization();
modalWord.value = `${targetWord}와(과) 유사한 단어들`;
modalSimilarWords.value = [...createSimilarWords(wordAndMean)];
modalStore.inputSimilarModal = true;
}
function caseNomalWord(wordAndMean) {
dataInitialization();
modalWord.value = wordAndMean[0][1];
modalMeans.value = wordAndMean[0][2].split(',');
mainStore.recentWordUpdate(modalWord.value);
modalStore.inputModal = true;
}
function wordAndMeanSplit(data) {
const searchWordMean = [];
data.data.items.lan.forEach(item => {
searchWordMean.push(item.item.split('|'));
});
return searchWordMean;
}
async function wordSearch(searchWord) {
const data = await fetchWordSearch(searchWord);
if (data.data.items.lan.length === 0) {
caseNotExistenceWord(searchWord);
}
else {
const wordAndMean = [...wordAndMeanSplit(data)];
checkWordSame(searchWord, wordAndMean[0][1]) ? caseNomalWord(wordAndMean) : caseSimilarWord(searchWord, wordAndMean);
}
}
function caseNomalCreate() {
return [modalWord.value, modalMeans.value];
}
function caseNotExistCreate() {
return [modalWord.value, modalMeans.value];
}
function caseSimilarCreate() {
return [modalWord.value, modalSimilarWords.value];
}
return {
caseNotExistenceWord,
caseSimilarWord,
caseNomalWord,
wordSearch,
caseNomalCreate,
caseNotExistCreate,
caseSimilarCreate,
}
});
caseNomalWord()
에서 Main 스토어의recentWordUpdate()
하여 검색한 단어를 저장
components/WordInput.vue
components/WordInput.vue
<script setup>
import { Icon } from '@iconify/vue';
import { useInputWordStore } from '../stores/InputWord';
import { useMainStore } from '../stores/Main';
import { ref, onMounted, onUnmounted } from 'vue';
const inputWordStore = useInputWordStore();
const mainStore = useMainStore();
const recentWordFocus = ref(false);
const wordSearch_input = ref(null);
const inputWord = ref('');
const outFocusInput = (e) => {
if (e.target.tagName !== 'svg' && e.target.tagName !== 'path' && !e.target.classList.contains('inputTag')) {
recentWordFocus.value = false;
}
}
onMounted(() => {
console.log("onMounted:")
window.addEventListener('click', outFocusInput);
});
onUnmounted(() => {
console.log("onUnmounted:")
window.removeEventListener('click', outFocusInput);
});
function inputWordClear() {
inputWord.value = '';
}
function caseEmptyWord() {
window.alert("단어를 입력해 주세요.");
}
function wordSearch(searchWord) {
if (searchWord === '') {
caseEmptyWord();
return;
}
inputWordStore.wordSearch(searchWord);
inputWord.value = '';
recentWordFocus.value = false;
}
function recentWordDeleteClick(index) {
mainStore.recentWordDelete(index);
}
</script>
<template>
<div class="">
<div class="h-9 px-3 border flex items-center bg-white border-black inputTag" ref="input_container">
<button v-show="inputWord.length>0" @click="inputWordClear"><Icon icon="ph:x-bold"></Icon></button>
<input ref=wordSearch_input placeholder="단어를 입력해주세요" :value="inputWord" @keyup.enter="wordSearch(inputWord)" @focus="recentWordFocus = true"
@input="event => inputWord = event.target.value" class="w-full px-2 focus:outline-0 inputTag"/>
<button ref="search_btn" class="inputTag" @click="wordSearch(inputWord)"><Icon icon="ion:search" width="24" height="24"/></button>
</div>
<!-- 최근 검색한 단어 -->
<div v-show="recentWordFocus" class="absolute w-full bg-white border-[1.5px] border-black inputTag">
<div class="text-[12px] text-slate-500 font-semibold px-2 py-0.5 inputTag">최근 검색한 단어</div>
<div v-for="(word, index) in mainStore.localRecentSearchWords" :key="index" class="flex items-center justify-between px-2 inputTag">
<button><span @click="wordSearch(word)" class="text-sm inputTag">{{ word }}</span></button>
<button @click="recentWordDeleteClick(index)" class="flex inputTag"><Icon icon="ph:x"></Icon></button>
</div>
</div>
</div>
</template>
recentWordFocus
: 값이 true면 최근 검색한 단어를 화면에 보여줌outFoucsInput
: 클릭 하는 곳이 svg, path, inputTag를 포함하지 않는 태그면recentWordFocus
를 false로 바꿔 최근 검색한 단어를 끔, onMounted를 이용해 window에 이벤트 리스너를 추가해줌wordSearch()
의 동작이 끝나면recentWordFocus
를 false로 변경해 최근 검색한 단어를 꺼줌function recentWordDeleteClick()
: Main 스토어에recenWordDelete()
를 호출