Stack/JavaScript

[JS] 2022 데브매칭 상반기 - 프로그래밍 언어 검색

7ingout 2022. 8. 22. 17:38

- client

언어검색

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>2022 FE 데브매칭</title>
    <link rel="stylesheet" href="./style.css">
</head>
<body>
    <div id="App">

    </div>
    <script src="./index.js" type="module"></script>
</body>
</html>

 

App.js

import { fetchLanguages } from "./api.js"
import SearchInput from "./components/SearchInput.js"
import SelectedLanguages from "./components/SelectedLanguages.js"
import Suggestion from "./components/Suggestion.js"

export default function App({ $target }) {
    this.state = {
        fetchedLanguages : [],
        selectedLanguages: []
    }
    this.setState = (nextState) => {
        this.state = {
            ...this.state,
            ...nextState
        }
        suggestion.setState({
            selectedIndex: 0,
            items: this.state.fetchedLanguages
        })
        selectedLanguages.setState(this.state.selectedLanguages)
    }
    const selectedLanguages = new SelectedLanguages({
        $target,
        initialState: []
    })
    const searchInput = new SearchInput({
        $target,
        initialState: '',
        onChange: async(keyword) => {
            // input에 입력한 키워드가 없을 경우
            if(keyword.length === 0) {
                this.setState({
                    fetchedLanguages: [],
                })
            } 
            // input에 입력했을 경우
            else {
                const languages = await fetchLanguages(keyword)
                console.log(languages);
                this.setState({
                    fetchedLanguages: languages.langArr
                })
            }
        }
    })
    // 현재 선택한 항목을 알기위하여 cursor 추가
    const suggestion = new Suggestion({
        $target,
        initialState: {
            cursor: 0,
            items: []
        },
        onSelect: (language) => {
            alert(language)
            // 이미 선택된 언어의 경우 맨 뒤로 보내버리는 처리
            const nextSelectedLanguage = [...this.state.selectedLanguages]
            // 선택된 언어 배열에 클릭한 값이 있으면 그 값의 index 값을 반환, 없으면 -1을 반환
            // ['java', 'javascript', 'java']
            const index = nextSelectedLanguage.findIndex((selected) => selected === language)
            if (index > -1) {
                // splice(index, num)
                nextSelectedLanguage.splice(index, 1)
            }
            nextSelectedLanguage.push(language)
            this.setState({
                ...this.state,
                selectedLanguages: nextSelectedLanguage
            })
        }
    })
}

 

api.js

export const API_END_POINT = 'http://localhost:3002';

const request = async(url) => {
    const res = await fetch(url)
    if(res.ok) {
        const json = await res.json();
        return json;
    }
    throw new Error('요청에 실패함')
}
export const fetchLanguages = async (keyword) => request(`${API_END_POINT}/languages?keyword=${keyword}`)

 

index.js

import App from "./App.js";

new App({ $target: document.querySelector('#App')})

 

components/SearchInput.js

export default function SearchInput({
    $target, 
    initialState,
    onChange
}){
    this.$element = document.createElement('form')
    this.$element.className = "SearchInput"
    this.state = initialState

    $target.appendChild(this.$element)

    this.render = () => {
        this.$element.innerHTML = `
        <input class="SearchInput__input" type="text"
        placeholder="프로그램 언어를 입력하세요" value="${this.state}"/>
        `
    }
    this.render();

    // 이벤트 구현
    this.$element.addEventListener('keyup', (e)=> {
        const actionIgnoreKeys = ['Enter', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
        if(!actionIgnoreKeys.includes(e.key)) {
            console.log(e.target.value)
            onChange(e.target.value)
        }
    })
    // submit 이벤트 처리하기
    this.$element.addEventListener('submit', (e) =>{
        e.preventDefault();
    })
}

 

components/Suggestion.js

export default function Suggestion({
    $target,
    initialState,
    onSelect
}) {
    this.$element = document.createElement('div');
    this.$element.className = 'Suggestion';
    $target.appendChild(this.$element)

    // this.state = initialState;
    // 현재키가 어디를 순회하는지 지정하는 selectedIndex를 추가
    this.state = {
        selectedIndex: 0,
        items: initialState.items
    }
    this.setState = (nextState) => {
        this.state = nextState;
        this.render();
    }
    this.render = () => {
        const { items = [], selectedIndex } = this.state;
        if(items.length > 0) {
            this.$element.style.display = 'block'
            this.$element.innerHTML = `
            <ul>
                ${items.map((item,index)=>`
                    <li class="${index===selectedIndex ? 'Suggestion__item--selected' : ''}" data-index='${index}'>${item}</li>    
                `).join('')}
            </ul>
            `
        } else {
            this.$element.style.display = 'none';
            this.$element.innerHTML = '';
        }
    }
    this.render();

    // 화살표 위, 아래 입력으로 selectedIndex 변경하기
    window.addEventListener('keyup', (e)=> {
        // 검색된 항목의 배열의 길이가 0보다 클 때
        if(this.state.items.length > 0) {
            // 현재 selectedIndex값을 selectedIndex에 할당
            const { selectedIndex } = this.state;
            // 검색된 배열의 마지막 인덱스를 lastIndex에 할당
            const lastIndex = this.state.items.length - 1
            const navigationKeys = ['ArrowUp', 'ArrowDown']
            let nextIndex = selectedIndex;
            // 클릭한 키 값이 navigationKeys배열에 있으면 
            if(navigationKeys.includes(e.key)) {
                if(e.key === 'ArrowUp') {
                    nextIndex = selectedIndex === 0 ? lastIndex : nextIndex - 1
                } else if (e.key ==='ArrowDown') {
                    nextIndex = selectedIndex === lastIndex ? 0 : nextIndex + 1
                }
                this.setState({
                    ...this.state,
                    selectedIndex: nextIndex
                })
            }
            // 현재 커서의 위치의 검색어를 파라메터로 전달
            else if(e.key === 'Enter') {
                onSelect(this.state.items[this.state.selectedIndex])
            }
        } 
    })

    // 마우스클릭 이벤트
    this.$element.addEventListener('click', (e)=> {
        const $li = e.target.closest('li');
        console.log(e);
        if ($li) {
            const { index } = $li.dataset;
            try {
                onSelect(this.state.items[parseInt(index)])
            } catch(e){
                alert('선택할 수 없습니다.')
            }
        }
    })
}

 

components/SelectedLanguages.js

export default function SelectedLanguages({
    $target,
    initialState
}) {
    this.$element = document.createElement('div')
    this.$element.className = 'SelectedLanguage';
    this.state = initialState;

    $target.appendChild(this.$element);

    this.setState = (nextState) => {
        this.state = nextState;
        this.render();
    }

    this.render = () => {
        this.$element.innerHTML = `
        <ul>
            ${this.state.map((item) => `
                <li>${item}</li>
            `).join("")}
        </ul>
        `
    }
    this.render();
}

 


 

- server

EXAMSERVER

npm init
npm install express
npm install cors

 

const express = require('express');
const cors = require('cors');
const app = express();
const port = 3002;

let langArr = [
    'Javascript',
    'java',
    'typescript',
    'php',
    'Asp',
    'Jsp',
    'React',
    'Python',
    'nodejs',
    'AnelScript',
    'CobolScript',
    'json',
    'jsonjava'
]
app.use(express.json());
app.use(cors());
app.get('/languages', function(req, res){
    let langArr2 = langArr.filter(r=> r.toLowerCase().includes(req.query.keyword)) 
    res.send({ langArr: langArr2 })
})
app.listen(port, ()=> {
    console.log('연습용 서버가 돌아가고 있습니다.')
})