최근 검색한 단어

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(): 최근 입력한 단어를 맨 뒤에 저장 하는 함수, 이미 입력한 단어가 있으면 삭제 후 맨 뒤로 저장하며 길이는 최대 5
  • recentWordDelete(): 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()를 호출


댓글