견적서 요청 확인 페이지


프리뷰

😎 프로젝트 소개


제조업 어드민 페이지를 가정하여, 각 필터별 견적요청서를 한 눈에 확인할 수 있는 웹페이지를 제작해봤다.

🪄 링크


깃헙 주소
https://github.com/hoonjoo-park/estimate-board

배포 주소
https://estimate-board-page.herokuapp.com/


🔮 프로젝트 주요 기능


json-server를 통한 API fetch

기본적인 REST-API 중, GET요청을 활용하기 위해 json-server를 직접 사용하여 데이터를 요청받아 fetch 후 활용했다.

  1. 설치

    1
    $ npm install json-server
  2. server 디렉토리 생성

    디렉토리 구조

  3. index.js 구성

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // server/index.js
    const jsonServer = require('json-server');
    const path = require('path');

    const server = jsonServer.create();
    const router = jsonServer.router(path.resolve(__dirname + '/db.json'));
    const middlewares = jsonServer.defaults({
    static: path.resolve(__dirname + '/../build/'),
    });

    const port = process.env.PORT || 3001;

    server.use(middlewares);

    server.use(jsonServer.bodyParser);

    server.use(router);
    server.listen(port, () => {
    console.log('JSON Server is running');
    });
  4. db.json 구성

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // server/db.json

    {
    "requests": [
    {
    "id": 1,
    "title": "자동차 시제품 제작",
    "client": "A 고객사",
    "due": "2020.12.14",
    "count": 2,
    "amount": 100,
    "method": ["밀링", "선반"],
    "material": ["알루미늄"],
    "status": "대기중"
    }
    // ... 이하 생략
    ]
    }
  5. fetcher 유틸함수를 통한 데이터 fetch

    1
    2
    // fetcher.tsx
    export const fetcher = await (await fetch('/requests')).json();
    1
    2
    3
    4
    5
    // mainPage.tsx에서 useEffect를 통해 초기 렌더링때마다 데이터를 받아 저장할 수 있도록 구현.
    useEffect(() => {
    const fetchData = fetcher();
    setApiData(fetchData);
    }, []);

필터 카테고리 중복 제거

카테고리에 해당하는 값들이 업데이트 될 수도 있다는 것을 가정해야 한다고 생각했다.
따라서 견적서에 존재하는 가공방식 및 재료를 모두 불러온 뒤 Set()을 활용해 중복을 제거했다.

  1. map을 통한 각 필드에 해당하는 Array 반환

    1
    2
    // 받아온 apiData를 map 돌려서 모든 값들을 배열에 담는다.
    const methodArr = apiData.map((data) => data.method);
  2. flat()을 활용하여 겹배열 해제

    1
    2
    3
    // 하지만 위의 방식만 활용할 경우 [[],[],[]]과 같이 겹배열 형식이 반환된다.
    // 따라서 아래와 같이 flat()을 활용하여 겹배열을 하나의 배열로 풀어줬다.
    const methodArr = apiData.map(data => data.method);.flat(Infinity)
  3. Set을 활용하여 중복 제거

    1
    2
    3
    4
    // 이제 하나의 배열이 만들어졌으므로, 중복을 제거해야 한다.
    // 가장 간편한 방법을 고안해 보았는데, Set을 활용하기로 결정했다.

    const methodSet = Array.from(new Set(methodArr));

onBlur 이벤트를 통한 Select박스 동적 여닫힘 기능

가장 헤맨 파트였다…

사실 굉장히 사소한 기능이라고 생각할 수 있으나, 사용자 입장에서는 일일이 필터 드롭다운을 닫아줘야 한다는 부분이 피곤하게 느껴질 것 같았다. 따라서 반드시 해당 기능을 구현하여 불편점을 개선해보고 싶었다.

  1. tabIndex 부여와 onBlur 이벤트 활용

    1
    2
    3
    4
    5
    // ul태그는 기본적으로 input태그가 아니기 때문에 focus가 되지 않는 요소다.
    // 따라서 아래와 같이 tabIndex를 수기로 부여해주어야 onBlur이벤트를 활용할 수 있다.
    <ul onBlur={(e) => handleBlur(e)} tabIndex={0}>
    드롭다운
    </ul>
  2. e.relatedTarget()의 활용

    onBlurfocusOut과 같다고 생각하면 된다. 즉, 포커싱이 된 요소의 포커스가 해제됐을 때의 이벤트를 리스닝하는 것이다.

    🎫 Event
    <a> (단, href 포함됐을 때)
    <link> (단, href 포함됐을 때)
    <button>
    <input> (단, hidden이 설정된 경우는 제외)
    <select>
    <textarea>
    draggable이 적용된 모든 엘리먼트

    e.relatedTarget은 이벤트를 리스닝하고 있는 엘리먼트에 focus/onBlur 됐을 때 함께 포커싱되거나 블러된 엘리먼트를 담아준다. 즉, 함께 포커싱 되거나 포커스 아웃된 엘리먼트를 표시해준다고 생각하면 된다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // handleBlur 함수

    const handleBlur = (e: React.FocusEvent<HTMLUListElement>, type: string) => {
    if (e.relatedTarget !== null) {
    return;
    }
    if (type === 'method') return setIsMethodOpen(false);
    if (type === 'material') return setIsMaterialOpen(false);
    };

선택 카테고리에 따른 견적서 필터링

팀원과 함께 필터링 알고리즘을 고민하는 과정이 굉장히 유익했다.
나 또한 시간복잡도가 너무 비효율적으로 변하는 것을 선호하지 않는데, 팀원 또한 같은 생각을 갖고 있어 같은 방향성으로 알고리즘을 구현하려 노력했다.

물론 프로젝트 완성 기한에 대한 고려 때문에… 결과적으론 시간복잡도를 O(n) 또는 O(nlogn)으로 구현해 내는 데에는 실패했지만, 과정 자체가 매우 유의미했다고 생각한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// getFilter.ts

export const getFilter = (apiData: Estimate[], categories: Category) => {
const newData = [];
for (let i = 0; i < apiData.length; i++) {
// target Array에서 필터링 되어야 하는 카테고리가 있다면 해당 값들을 모두 배열화
const methodFiltered = apiData[i].method.filter((data: string) =>
categories.method.includes(data)
);
const materialFiltered = apiData[i].material.filter((data: string) =>
categories.material.includes(data)
);
// Array.filter를 통해 일치되는 카테고리들만 배열화 되었다.
// 따라서 target Array(선택된 카테고리들)를 각 데이터들이 모두 포함하는지 확인하는 절차가 필요하다.
if (
// 만약 필터링된 각 데이터들의 카테고리 모음(array)의 길이가 기준 배열의 길이보다 길다면,
// 이는 기준 배열이 target Arrat에 포섭된다는 뜻이다. 따라서 target Array는 기준 배열의 모든 요소들을 포함하고 있다는 뜻이 된다.
methodFiltered.length >= categories.method.length &&
materialFiltered.length >= categories.material.length
) {
newData.push(apiData[i]);
}
}
return newData;
};

🤔 회고

프로젝트 실행 초기에 Typescript + React에서의 절대경로 설정에서 꽤나 애를 먹었다.

하지만 tsconfig 파일에서 아래 두 코드만 추가해주니 쉽게 해결됐다.

1
2
3
4
5
6
7
8
{
"compileOptions" : {
// 생략
"jsx": "react-jsx",
"baseUrl": "src"
},
"include": ['src']
}

팀원들과 페어 프로그래밍을 하며 협업에 대한 안목과 방법론 등을 더 단련할 수 있는 기회였다고 생각한다. 굉장히 훌륭한 팀원들과 함께 프로젝트를 진행할 수 있어서 배울 점이 너무나도 많다. 이를 동기부여 삼아 꾸준히 성장하는 개발자가 되도록 노력해야겠다…!!


Author

Hoonjoo

Posted on

2022-02-10

Updated on

2022-02-10

Licensed under

Comments