/po-fe-12th-w4

[원티드 프리온보딩 인턴십 | 프론트엔드 12차] 4주차 개인 과제 레포지토리입니다.

Primary LanguageTypeScript

원티드 프리온보딩 12th 4주차 개인 과제

원티드 프리온보딩 12th 4주차에 진행된 개인 과제입니다.

본 과제는 mock data를 이용해 시계열 차트를 구현하는 것이 목표입니다.

🧑🏻‍💻 프로젝트 정보

실행 방법

git clone https://github.com/devseop/po-fe-12th-w4
npm install && npm start

프로젝트 구조

📦 src
┣ 📂 api
┃ ┗ api.ts
┣ 📂 components
┃ ┣ ChartHeader.tsx
┃ ┣ FilterButtons.tsx
┃ ┗ TimeSeriesChart.tsx
┣ 📂 constants
┃ ┗ constants.ts
┣ 📂 hooks
┃ ┣ useData.tsx
┃ ┗ useFilter.tsx
┣ 📂 styles
┃ ┗ globalStyles.ts
┣ 📂 types
┃ ┗ types.ts
┣ 📂 utils
┃ ┣ convertChartData.ts
┃ ┣ customedChartOption.ts
┃ ┣ filteredChartStyle.ts
┃ ┣ getUniqueIds.ts
┃ ┗ registerChartJS.ts
┣ App.tsx
┣ main.tsx
┗ vite-env.d.ts

사용 라이브러리

"dependencies": {
    "@emotion/react": "^11.11.1",
    "@emotion/styled": "^11.11.0",
    "chart.js": "^4.4.0",
    "react": "^18.2.0",
    "react-chartjs-2": "^5.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.15.0"
  },

"devDependencies": {
    "babel-eslint": "^10.1.0",
    "eslint": "^8.45.0",
    "husky": "^8.0.3",
    "lint-staged": "^14.0.1",
    "prettier": "^3.0.3",
    "typescript": "^5.0.2",
    "vite": "^4.4.5"
  },

📝 구현 내용

Assignment 1, Assignment 2 Assignment 3
chart_1 chart_2

✅ Assignment 1

시계열 차트 만들기

- 주어진 JSON 데이터의 `key`값(시간)을 기반으로 시계열 차트를 만들어주세요
- 하나의 차트안에 Area 형태의 그래프와 Bar 형태의 그래프가 모두 존재하는 복합 그래프로 만들어주세요
- Area 그래프의 기준값은 `value_area` 값을 이용해주세요
- Bar 그래프의 기준값은 `value_bar` 값을 이용해주세요
- 차트의 Y축에 대략적인 수치를 표현해주세요
코드 보기
  • 데이터 호출
  • useEffect(() => {
    const laodData = async () => {
    try {
    setIsLoading(true);
    const res = await fetchData();
    setchartData(convertChartData(res) as IChartDataState);
    setUniqueIds(getUniqueIds(res));
    setIsLoading(false);
    } catch (err) {
    console.error(err);
    }
    };
    laodData();
    }, []);
  • key값(시간)을 기준으로 데이터를 재분류
  • export const convertChartData = (data: IResponseData) => {
    if (!data) return [];
    const dataList = Object.entries(data).map(([time, data]) => ({
    [X_AXIS_KEY]: time,
    [Y_AXIS_KEY]: data,
    }));
    return {
    datasets: [returnToAreaDataset(dataList), returnToBarDataset(dataList)],
    };
    };
  • Area, Bar 형태가 동시에 존재하는 그래프로 데이터를 컴포넌트에 전달, 렌더링
  • const returnToAreaDataset = (dataList: ITimeSeriesData[]) => {
    return {
    type: 'line' as const,
    label: VALUE_AREA_KEY,
    yAxisID: 'area',
    data: dataList,
    parsing: {
    xAxisKey: X_AXIS_KEY,
    yAxisKey: `${Y_AXIS_KEY}.${VALUE_AREA_KEY}`,
    },
    tension: 0.2,
    borderWidth: 2,
    borderColor: COLORS.green.base,
    backgroundColor: COLORS.green.dimmed,
    pointBorderWidth: 0,
    pointHoverBorderWidth: 2,
    pointHoverBorderColor: COLORS.green.base,
    pointHoverBackgroundColor: COLORS.green.base,
    fill: true,
    order: 1,
    };
    };
    const returnToBarDataset = (dataList: ITimeSeriesData[]) => {
    return {
    type: 'bar' as const,
    label: VALUE_BAR_KEY,
    yAxisID: 'bar',
    data: dataList,
    parsing: {
    xAxisKey: X_AXIS_KEY,
    yAxisKey: `${Y_AXIS_KEY}.${VALUE_BAR_KEY}`,
    },
    borderColor: COLORS.blue.base,
    backgroundColor: COLORS.blue.dimmed,
    hoverBorderColor: COLORS.blue.base,
    hoverBackgroundColor: COLORS.blue.base,
    order: 2,
    };
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    export const TimeSeriesChart = forwardRef<any, ITimeSeriesChart>(
    ({ chartData, chartOptions, onClick }, ref) => {
    return (
    <Figure>
    <Chart type='bar' data={chartData} options={chartOptions} onClick={onClick} ref={ref} />
    </Figure>
    );
    },
    );
    TimeSeriesChart.displayName = 'TimeSeriesChart';

✅ Assignment 2

호버 기능 구현

- 특정 데이터 구역에 마우스 호버시 `id, value_area, value_bar` 데이터를 툴팁 형태로 제공해주세요

Chart.js에서는 기본적으로 마우스 호버시 타이틀(시간), 값(value)을 볼 수 있습니다.
세부옵션인 tooltipafterTitle 옵션을 이용하여 타이틀 바로 다음에 id가 제공되도록 했고,
인자로 전달 받은 tooltipItems를 구조분해하여 원하는 값인 id를 반환하도록 했습니다.
타입은 Chart.jsTooltipItem 타입을 확장하여 사용했습니다. (types/index.d.ts:2893)

코드 보기

✅ Assignment 3

필터링 기능 구현

- 필터링 기능을 구현해주세요, 필터링은 특정 데이터 구역을 하이라이트 하는 방식으로 구현해주세요
- 필터링 기능은 버튼 형태로 ID값(지역이름)을 이용해주세요
- 필터링 시 버튼에서 선택한 ID값과 동일한 ID값을 가진 데이터 구역만 하이라이트 처리를 해주세요
- 특정 데이터 구역을 클릭 시에도 필터링 기능과 동일한 형태로 동일한 ID값을 가진 데이터 구역을 하이라이트해주세요
  • useFilter.ts는 URL 쿼리 매개변수를 사용하여 데이터를 필터링하고 URL을 업데이트합니다.
  • filteredChartStyle.ts는 데이터와 필터링된 ID를 사용하여 차트 스타일을 계산하고 반환합니다.
  • useData.ts에서는 데이더와 필터링 기능을 위한 다양한 상태를 관리하고 이를 이용한 이벤트 처리 및 데이터 호출을 담당합니다.
    • highlightedById함수는 필터링된 ID에 따라 차트 스타일을 변경합니다. 현재 차트 데이터와 참조된 차트 인스턴스를 기반으로 스타일을 업데이트합니다.
    • getIdFromChartElement함수는 차트에서 클릭된 요소로부터 해당 ID를 추출합니다.
    • clickBlankCanvasHandler는 빈 캔버스 영역을 클릭할 때 실행되는 이벤트 핸들러입니다. 빈 캔버스 영역을 클릭시 선택된 값에 대한 스타일이 초기화됩니다.
코드 보기
  • useFilter.ts
  • export const useFilter = (key: string = 'id') => {
    const [searchParams, setSearchParams] = useSearchParams();
    const filteringTheKey = searchParams.get(key);
    const setFilteringTheKey = (value: string | undefined) => {
    const newSearchParams = new URLSearchParams(searchParams.toString());
    if (value) {
    newSearchParams.set(key, value);
    } else {
    newSearchParams.delete(key);
    }
    setSearchParams(newSearchParams);
    };
    return { filteringTheKey, setFilteringTheKey };
    };
  • filteredChartStyle.ts
  • interface IFilteredChartStyle {
    pointBorderWidthForArea: number[];
    backgroundColorForBar: string[];
    }
    export const filteredChartStyle = (
    data: ITimeSeriesData[],
    filteredId: ILocationData['id'] | null,
    ): IFilteredChartStyle => {
    return data.reduce<IFilteredChartStyle>(
    (obj, current) => {
    const currentId = current[Y_AXIS_KEY].id;
    const isMatched = currentId === filteredId;
    const newAreaBorderWidth = isMatched ? 2 : 0;
    const newBarColor = isMatched ? COLORS.blue.base : COLORS.blue.dimmed;
    return {
    pointBorderWidthForArea: [...obj.pointBorderWidthForArea, newAreaBorderWidth],
    backgroundColorForBar: [...obj.backgroundColorForBar, newBarColor],
    };
    },
    { pointBorderWidthForArea: [], backgroundColorForBar: [] },
    );
    };
  • highlightedById
  • const highlightedById = (filteredId: string | null) => {
    if (!chartRef.current || !chartData.datasets.length) return;
    const [areaDataset] = chartData.datasets;
    const { pointBorderWidthForArea, backgroundColorForBar } = filteredChartStyle(
    areaDataset.data,
    filteredId,
    );
    setchartData((prevChartData) => {
    const [prevAreaDataset, prevBarDataset] = prevChartData.datasets;
    const newAreaDataset = { ...prevAreaDataset, pointBorderWidth: pointBorderWidthForArea };
    const newBarDataset = { ...prevBarDataset, backgroundColor: backgroundColorForBar };
    const isMatchedInAreaDataset =
    JSON.stringify(newAreaDataset) === JSON.stringify(prevAreaDataset);
    const isMatchedInBarDataset =
    JSON.stringify(newBarDataset) === JSON.stringify(prevBarDataset);
    if (isMatchedInAreaDataset && isMatchedInBarDataset) {
    return prevChartData;
    }
    return {
    datasets: [newAreaDataset, newBarDataset],
    };
    });
    };
  • getIdFromChartElement
  • const getIdFromChartElement = (element: InteractionItem[]) => {
    if (!element.length || !chartData.datasets.length) return;
    const { datasetIndex, index } = element[0];
    const clickedItemId = chartData.datasets[datasetIndex].data[index].y.id;
    return clickedItemId;
    };
  • clickBlankCanvasHandler
  • const clickBlankCanvasHandler: React.MouseEventHandler<HTMLCanvasElement> = useCallback(
    (event) => {
    if (!chartRef.current) return;
    const clickedElementInCanvas = getElementsAtEvent(chartRef.current, event);
    const clickedId = getIdFromChartElement(clickedElementInCanvas);
    setFilteringTheKey(clickedId);
    },
    [chartRef.current],
    );

🫱🏻‍🫲🏿 Commit Convention

커밋 컨벤션과 브랜치 전략은 1주차 팀 과제 진행시 결정된 팀 컨벤션을 따랐습니다.

e.g. FEAT: 로그인 유효성 검증 기능 구현

태그 설명 (한국어로만 작성하기)
FEAT: 새로운 기능 추가 (변수명 변경 포함)
FIX: 버그 해결
DESIGN: CSS 등 사용자 UI 디자인 변경
STYLE: 코드 포맷 변경, 세미 콜론 누락, 코드 수정이 없는 경우
REFACTOR: 프로덕션 코드 리팩토링
COMMENT: 필요한 주석 추가 및 변경
DOCS: 문서를 수정한 경우
CHORE: 빌드 테스크 업데이트, 패키지 매니저 설정(프로덕션 코드 변경 X)
RENAME: 파일 혹은 폴더명을 수정하거나 옮기는 작업
REMOVE: 파일을 삭제하는 작업만 수행한 경우
INIT: 초기 커밋을 진행한 경우