From a0d46281387468f2f7ed3cf22526830cca10c2be Mon Sep 17 00:00:00 2001 From: withsang Date: Fri, 24 Nov 2023 01:19:48 +0900 Subject: [PATCH 1/9] feat(text): add sample templates --- .../LocalCouncilReportText/AgeText.tsx | 33 +++++++++++++++++++ .../LocalCouncilReportText/GenderText.tsx | 33 +++++++++++++++++++ .../LocalCouncilReportText/PartyText.tsx | 33 +++++++++++++++++++ .../molecules/LocalCouncilReportText/index.ts | 3 ++ 4 files changed, 102 insertions(+) create mode 100644 src/components/molecules/LocalCouncilReportText/AgeText.tsx create mode 100644 src/components/molecules/LocalCouncilReportText/GenderText.tsx create mode 100644 src/components/molecules/LocalCouncilReportText/PartyText.tsx create mode 100644 src/components/molecules/LocalCouncilReportText/index.ts diff --git a/src/components/molecules/LocalCouncilReportText/AgeText.tsx b/src/components/molecules/LocalCouncilReportText/AgeText.tsx new file mode 100644 index 0000000..edd471e --- /dev/null +++ b/src/components/molecules/LocalCouncilReportText/AgeText.tsx @@ -0,0 +1,33 @@ +import { Typography } from "antd"; + +const { Paragraph, Text } = Typography; + +export type AgeTextVariation = 1 | 2; + +export interface AgeTextData { + foo: string; + bar: string; +} + +interface Props { + /** text variation을 선택할 수 있습니다(기본값: 1). */ + textVariation?: AgeTextVariation; + /** text에 들어갈 데이터입니다. */ + textData: AgeTextData; +} + +export const AgeText = ({ textVariation = 1, textData }: Props) => { + const { foo, bar } = textData; + + if (textVariation === 1) + return ( + + foo={foo} + + ); + return ( + + bar={bar} + + ); +}; diff --git a/src/components/molecules/LocalCouncilReportText/GenderText.tsx b/src/components/molecules/LocalCouncilReportText/GenderText.tsx new file mode 100644 index 0000000..afa7ff6 --- /dev/null +++ b/src/components/molecules/LocalCouncilReportText/GenderText.tsx @@ -0,0 +1,33 @@ +import { Typography } from "antd"; + +const { Paragraph, Text } = Typography; + +export type GenderTextVariation = 1 | 2; + +export interface GenderTextData { + foo: string; + bar: string; +} + +interface Props { + /** text variation을 선택할 수 있습니다(기본값: 1). */ + textVariation?: GenderTextVariation; + /** text에 들어갈 데이터입니다. */ + textData: GenderTextData; +} + +export const GenderText = ({ textVariation = 1, textData }: Props) => { + const { foo, bar } = textData; + + if (textVariation === 1) + return ( + + foo={foo} + + ); + return ( + + bar={bar} + + ); +}; diff --git a/src/components/molecules/LocalCouncilReportText/PartyText.tsx b/src/components/molecules/LocalCouncilReportText/PartyText.tsx new file mode 100644 index 0000000..db16d1c --- /dev/null +++ b/src/components/molecules/LocalCouncilReportText/PartyText.tsx @@ -0,0 +1,33 @@ +import { Typography } from "antd"; + +const { Paragraph, Text } = Typography; + +export type PartyTextVariation = 1 | 2; + +export interface PartyTextData { + foo: string; + bar: string; +} + +interface Props { + /** text variation을 선택할 수 있습니다(기본값: 1). */ + textVariation?: PartyTextVariation; + /** text에 들어갈 데이터입니다. */ + textData: PartyTextData; +} + +export const PartyText = ({ textVariation = 1, textData }: Props) => { + const { foo, bar } = textData; + + if (textVariation === 1) + return ( + + foo={foo} + + ); + return ( + + bar={bar} + + ); +}; diff --git a/src/components/molecules/LocalCouncilReportText/index.ts b/src/components/molecules/LocalCouncilReportText/index.ts new file mode 100644 index 0000000..c5dc4e4 --- /dev/null +++ b/src/components/molecules/LocalCouncilReportText/index.ts @@ -0,0 +1,3 @@ +export * from "./AgeText"; +export * from "./GenderText"; +export * from "./PartyText"; From c8e367c9fa68aada86a1c47d85d9fc7ae553be67 Mon Sep 17 00:00:00 2001 From: withsang Date: Fri, 24 Nov 2023 02:17:01 +0900 Subject: [PATCH 2/9] feat(api): fetch text template from server, except age data --- .eslintrc.cjs | 1 + .../LocalCouncilReportText/AgeText.tsx | 23 +++---- .../LocalCouncilReportText/GenderText.tsx | 23 +++---- .../LocalCouncilReportText/PartyText.tsx | 23 +++---- ...lector.tsx => LocalCouncilMapSelector.tsx} | 20 +++--- .../organisms/LocalCouncilReport.tsx | 69 ++++++++++++++++--- src/components/organisms/index.ts | 2 +- ...{LocalCouncil.tsx => LocalCouncilPage.tsx} | 21 ++++-- src/components/pages/index.ts | 2 +- src/router.tsx | 10 +-- src/utils/sampleData.ts | 2 +- 11 files changed, 125 insertions(+), 71 deletions(-) rename src/components/organisms/{MapSelector.tsx => LocalCouncilMapSelector.tsx} (68%) rename src/components/pages/{LocalCouncil.tsx => LocalCouncilPage.tsx} (75%) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 7110900..99074d0 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -19,6 +19,7 @@ module.exports = { }, plugins: ["import", "@typescript-eslint", "react", "react-refresh"], rules: { + "no-alert": "off", "react-refresh/only-export-components": [ "warn", { allowConstantExport: true }, diff --git a/src/components/molecules/LocalCouncilReportText/AgeText.tsx b/src/components/molecules/LocalCouncilReportText/AgeText.tsx index edd471e..24e9c64 100644 --- a/src/components/molecules/LocalCouncilReportText/AgeText.tsx +++ b/src/components/molecules/LocalCouncilReportText/AgeText.tsx @@ -5,29 +5,26 @@ const { Paragraph, Text } = Typography; export type AgeTextVariation = 1 | 2; export interface AgeTextData { - foo: string; - bar: string; + ageDiversityIndex: number; } interface Props { /** text variation을 선택할 수 있습니다(기본값: 1). */ - textVariation?: AgeTextVariation; + variation?: AgeTextVariation; /** text에 들어갈 데이터입니다. */ - textData: AgeTextData; + data?: AgeTextData; } -export const AgeText = ({ textVariation = 1, textData }: Props) => { - const { foo, bar } = textData; +export const AgeText = ({ variation = 1, data = undefined }: Props) => { + if (!data) return 데이터를 불러오는 중입니다..; - if (textVariation === 1) + const { ageDiversityIndex } = data; + if (variation === 1) return ( - foo={foo} + 이 지역의 연령 다양성 지표의 값은{" "} + {ageDiversityIndex}입니다. ); - return ( - - bar={bar} - - ); + return 존재하지 않는 템플릿입니다.; }; diff --git a/src/components/molecules/LocalCouncilReportText/GenderText.tsx b/src/components/molecules/LocalCouncilReportText/GenderText.tsx index afa7ff6..88fbe40 100644 --- a/src/components/molecules/LocalCouncilReportText/GenderText.tsx +++ b/src/components/molecules/LocalCouncilReportText/GenderText.tsx @@ -5,29 +5,26 @@ const { Paragraph, Text } = Typography; export type GenderTextVariation = 1 | 2; export interface GenderTextData { - foo: string; - bar: string; + genderDiversityIndex: number; } interface Props { /** text variation을 선택할 수 있습니다(기본값: 1). */ - textVariation?: GenderTextVariation; + variation?: GenderTextVariation; /** text에 들어갈 데이터입니다. */ - textData: GenderTextData; + data?: GenderTextData; } -export const GenderText = ({ textVariation = 1, textData }: Props) => { - const { foo, bar } = textData; +export const GenderText = ({ variation = 1, data = undefined }: Props) => { + if (!data) return 데이터를 불러오는 중입니다..; - if (textVariation === 1) + const { genderDiversityIndex } = data; + if (variation === 1) return ( - foo={foo} + 이 지역의 성별 다양성 지표의 값은{" "} + {genderDiversityIndex}입니다. ); - return ( - - bar={bar} - - ); + return 존재하지 않는 템플릿입니다.; }; diff --git a/src/components/molecules/LocalCouncilReportText/PartyText.tsx b/src/components/molecules/LocalCouncilReportText/PartyText.tsx index db16d1c..8ee9d23 100644 --- a/src/components/molecules/LocalCouncilReportText/PartyText.tsx +++ b/src/components/molecules/LocalCouncilReportText/PartyText.tsx @@ -5,29 +5,26 @@ const { Paragraph, Text } = Typography; export type PartyTextVariation = 1 | 2; export interface PartyTextData { - foo: string; - bar: string; + partyDiversityIndex: number; } interface Props { /** text variation을 선택할 수 있습니다(기본값: 1). */ - textVariation?: PartyTextVariation; + variation?: PartyTextVariation; /** text에 들어갈 데이터입니다. */ - textData: PartyTextData; + data?: PartyTextData; } -export const PartyText = ({ textVariation = 1, textData }: Props) => { - const { foo, bar } = textData; +export const PartyText = ({ variation = 1, data = undefined }: Props) => { + if (!data) return 데이터를 불러오는 중입니다..; - if (textVariation === 1) + const { partyDiversityIndex } = data; + if (variation === 1) return ( - foo={foo} + 이 지역의 정당 다양성 지표의 값은{" "} + {partyDiversityIndex}입니다. ); - return ( - - bar={bar} - - ); + return 존재하지 않는 템플릿입니다.; }; diff --git a/src/components/organisms/MapSelector.tsx b/src/components/organisms/LocalCouncilMapSelector.tsx similarity index 68% rename from src/components/organisms/MapSelector.tsx rename to src/components/organisms/LocalCouncilMapSelector.tsx index afc1df7..ad69970 100644 --- a/src/components/organisms/MapSelector.tsx +++ b/src/components/organisms/LocalCouncilMapSelector.tsx @@ -9,8 +9,8 @@ interface Props { idMap: Map>; } -const LocalCouncil = ({ idMap }: Props) => { - const [metroId, setMetroId] = useState(); +const LocalCouncilMapSelector = ({ idMap }: Props) => { + const [metroName, setMetroName] = useState(); const { metroId: metroParam } = useParams(); const navigate = useNavigate(); useEffect(() => { @@ -18,7 +18,7 @@ const LocalCouncil = ({ idMap }: Props) => { idMap.forEach((value, key) => { if (value) { if (value.values().next().value[0] === metroParam) { - setMetroId(key as MetroID); + setMetroName(key as MetroID); } } }); @@ -32,19 +32,19 @@ const LocalCouncil = ({ idMap }: Props) => { margin: 40px 0 40px 0; `} > - {metroId ? ( + {metroName ? ( { - const idData = idMap.get(metroId)?.get(id); + selected={metroName} + onClick={localName => { + const idData = idMap.get(metroName)?.get(localName); if (!idData) return; - navigate(`/localCouncilReport/${idData[0]}/${idData[1]}`); + navigate(`/localCouncil/${metroName}/${localName}`); }} /> ) : ( { - setMetroId(id as MetroID); + setMetroName(id as MetroID); }} /> )} @@ -52,4 +52,4 @@ const LocalCouncil = ({ idMap }: Props) => { ); }; -export default LocalCouncil; +export default LocalCouncilMapSelector; diff --git a/src/components/organisms/LocalCouncilReport.tsx b/src/components/organisms/LocalCouncilReport.tsx index 2db6f16..a27a883 100644 --- a/src/components/organisms/LocalCouncilReport.tsx +++ b/src/components/organisms/LocalCouncilReport.tsx @@ -1,20 +1,67 @@ -import React from "react"; +import { useState, useEffect } from "react"; import { Flex, Typography } from "antd"; import { css } from "@emotion/react"; -import { useParams } from "react-router-dom"; +import { type MetroID } from "static/MapSVGData"; + +import { + AgeText, + GenderText, + PartyText, + type AgeTextData, + type GenderTextData, + type PartyTextData, +} from "@/components/molecules/LocalCouncilReportText"; import { AgeHistogram } from "@/components/organisms/Histogram"; import { PieChart } from "@/components/organisms/PieChart"; import { + axios, sampleAgeHistogramData, samplePartyPieData, - sampleSexPieData, + sampleGenderPieData, } from "@/utils"; const { Title } = Typography; -const LocalCouncilReport: React.FC = () => { - const { metroId, localId } = useParams(); +interface Props { + metroName: MetroID; + localName: string; + idMap: Map>; +} + +const LocalCouncilReport = ({ metroName, localName, idMap }: Props) => { + const [metroId, localId] = idMap.get(metroName)?.get(localName) || [1, 1]; + const [ageTextData, setAgeTextData] = useState(); + const [genderTextData, setGenderTextData] = useState(); + const [partyTextData, setPartyTextData] = useState(); + + // 백엔드로부터 텍스트 데이터를 가져옵니다. + useEffect(() => { + axios + .get(`localCouncil/template-data/${metroId}/${localId}?factor=age`) + .then(response => { + setAgeTextData(response.data as AgeTextData); + }) + .catch(() => { + alert("네트워크 문제가 발생했습니다. 다시 시도해주세요."); + }); + axios + .get(`localCouncil/template-data/${metroId}/${localId}?factor=gender`) + .then(response => { + setGenderTextData(response.data as GenderTextData); + }) + .catch(() => { + alert("네트워크 문제가 발생했습니다. 다시 시도해주세요."); + }); + axios + .get(`localCouncil/template-data/${metroId}/${localId}?factor=party`) + .then(response => { + setPartyTextData(response.data as PartyTextData); + }) + .catch(() => { + alert("네트워크 문제가 발생했습니다. 다시 시도해주세요."); + }); + }, []); return ( { > {`MetroId(${metroId}) LocalId(${localId})의 지역의회 다양성 리포트`} + >{`${metroName} ${localName}의 지역의회 다양성 리포트`} 연령 다양성 + + 성별 다양성 + + 정당 다양성 - 성별 다양성 - + ); }; diff --git a/src/components/organisms/index.ts b/src/components/organisms/index.ts index e876113..83acc49 100644 --- a/src/components/organisms/index.ts +++ b/src/components/organisms/index.ts @@ -5,4 +5,4 @@ export * from "./Histogram"; export * from "./PieChart"; export { default as TestChart } from "./TestChart"; export { default as LocalCouncilReport } from "./LocalCouncilReport"; -export { default as MapSelector } from "./MapSelector"; +export { default as LocalCouncilMapSelector } from "./LocalCouncilMapSelector"; diff --git a/src/components/pages/LocalCouncil.tsx b/src/components/pages/LocalCouncilPage.tsx similarity index 75% rename from src/components/pages/LocalCouncil.tsx rename to src/components/pages/LocalCouncilPage.tsx index 0842e92..7158f29 100644 --- a/src/components/pages/LocalCouncil.tsx +++ b/src/components/pages/LocalCouncilPage.tsx @@ -1,5 +1,8 @@ import { useEffect, useState } from "react"; -import { MapSelector, LocalCouncilReport } from "@/components/organisms"; +import { + LocalCouncilMapSelector, + LocalCouncilReport, +} from "@/components/organisms"; import { useParams } from "react-router-dom"; import { Element, scroller } from "react-scroll"; import { Layout } from "@/components/templates"; @@ -18,17 +21,17 @@ type LocalInfo = { }; const LocalCouncil = () => { - const { metroId, localId } = useParams(); + const { metroName, localName } = useParams(); const [metroLocalMap, setMetroLocalMap] = useState>>(); useEffect(() => { - if (!metroId || !localId) return; + if (!metroName || !localName) return; scroller.scrollTo("Report", { duration: 1000, delay: 50, smooth: "easeInOutQuart", }); - }, [metroId, localId]); + }, [metroName, localName]); useEffect(() => { const idMap = new Map>(); axios.get("/localCouncil/regionInfo").then(response => { @@ -51,9 +54,15 @@ const LocalCouncil = () => { }, []); return metroLocalMap ? ( - + - {metroId && localId ? : null} + {metroName && localName ? ( + + ) : null} ) : null; diff --git a/src/components/pages/index.ts b/src/components/pages/index.ts index ec466e4..b65453d 100644 --- a/src/components/pages/index.ts +++ b/src/components/pages/index.ts @@ -1,2 +1,2 @@ // page 컴포넌트를 모아놓은 파일 -export { default as LocalCouncil } from "./LocalCouncil"; +export { default as LocalCouncilPage } from "./LocalCouncilPage"; diff --git a/src/router.tsx b/src/router.tsx index 424c87d..8a9e854 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -4,15 +4,15 @@ import { Route, Navigate, } from "react-router-dom"; -import { LocalCouncil } from "@/components/pages"; +import { LocalCouncilPage } from "@/components/pages"; const router = createBrowserRouter( createRoutesFromElements([ - } />, - } />, + } />, + } />, } + path="/localCouncil/:metroName/:localName" + element={} />, } />, ]), diff --git a/src/utils/sampleData.ts b/src/utils/sampleData.ts index 9b6444b..850eb81 100644 --- a/src/utils/sampleData.ts +++ b/src/utils/sampleData.ts @@ -45,7 +45,7 @@ export const samplePartyPieData = { ], }; -export const sampleSexPieData = { +export const sampleGenderPieData = { colors: ["#FF0000", "#0000FF"], data: [ { From f219c87e42571ad6ec5081191ed310e468330d1f Mon Sep 17 00:00:00 2001 From: withsang Date: Fri, 24 Nov 2023 02:31:20 +0900 Subject: [PATCH 3/9] fix(router): fix useParam of MetroSelector --- .../LocalCouncilReportText/AgeText.tsx | 47 +++++++++++++++++-- .../organisms/LocalCouncilMapSelector.tsx | 23 +++------ src/components/pages/LocalCouncilPage.tsx | 2 +- 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/src/components/molecules/LocalCouncilReportText/AgeText.tsx b/src/components/molecules/LocalCouncilReportText/AgeText.tsx index 24e9c64..0ca487f 100644 --- a/src/components/molecules/LocalCouncilReportText/AgeText.tsx +++ b/src/components/molecules/LocalCouncilReportText/AgeText.tsx @@ -5,7 +5,40 @@ const { Paragraph, Text } = Typography; export type AgeTextVariation = 1 | 2; export interface AgeTextData { - ageDiversityIndex: number; + metroId: number; + localId: number; + rankingParagraph: { + ageDiversityIndex: number; + allIndices: { localId: number; rank: number; ageDiversityIndex: number }[]; + }; + indexHistoryParagraph: { + mostRecentYear: number; + history: { + year: number; + unit: number; + candidateCount: number; + candidateDiversityIndex: number; + electedDiversityIndex: number; + electedDiversityRank: number; + }[]; + }; + ageHistogramParagraph: { + year: number; + candidateCount: number; + electedCount: number; + firstQuintile: number; + lastQuintile: number; + divArea: { + localId: number; + firstQuintile: number; + lastQuintile: number; + }; + uniArea: { + localId: number; + firstQuintile: number; + lastQuintile: number; + }; + }; } interface Props { @@ -18,12 +51,18 @@ interface Props { export const AgeText = ({ variation = 1, data = undefined }: Props) => { if (!data) return 데이터를 불러오는 중입니다..; - const { ageDiversityIndex } = data; + const { + metroId, + localId, + // rankingParagraph, + // indexHistoryParagraph, + // ageHistogramParagraph, + } = data; if (variation === 1) return ( - 이 지역의 연령 다양성 지표의 값은{" "} - {ageDiversityIndex}입니다. + 이 지역의 metroId는 {metroId}, localId는{" "} + {localId}입니다. ); return 존재하지 않는 템플릿입니다.; diff --git a/src/components/organisms/LocalCouncilMapSelector.tsx b/src/components/organisms/LocalCouncilMapSelector.tsx index ad69970..1cf5802 100644 --- a/src/components/organisms/LocalCouncilMapSelector.tsx +++ b/src/components/organisms/LocalCouncilMapSelector.tsx @@ -10,20 +10,9 @@ interface Props { } const LocalCouncilMapSelector = ({ idMap }: Props) => { - const [metroName, setMetroName] = useState(); - const { metroId: metroParam } = useParams(); + const { metroName } = useParams(); const navigate = useNavigate(); - useEffect(() => { - if (metroParam) { - idMap.forEach((value, key) => { - if (value) { - if (value.values().next().value[0] === metroParam) { - setMetroName(key as MetroID); - } - } - }); - } - }, [metroParam]); + return ( { > {metroName ? ( { - const idData = idMap.get(metroName)?.get(localName); + const idData = idMap.get(metroName as MetroID)?.get(localName); if (!idData) return; navigate(`/localCouncil/${metroName}/${localName}`); }} /> ) : ( { - setMetroName(id as MetroID); + onClick={newMetroName => { + navigate(`/localCouncil/${newMetroName}`); }} /> )} diff --git a/src/components/pages/LocalCouncilPage.tsx b/src/components/pages/LocalCouncilPage.tsx index 7158f29..eb962f3 100644 --- a/src/components/pages/LocalCouncilPage.tsx +++ b/src/components/pages/LocalCouncilPage.tsx @@ -58,7 +58,7 @@ const LocalCouncil = () => { {metroName && localName ? ( From f7a08f1a196655d30577d2ee91324468ed28e037 Mon Sep 17 00:00:00 2001 From: withsang Date: Fri, 24 Nov 2023 03:21:08 +0900 Subject: [PATCH 4/9] feat(api): fetch age text template from server --- .../LocalCouncilReportText/AgeText.tsx | 41 ++++++++++++++++--- .../organisms/LocalCouncilMapSelector.tsx | 1 - .../organisms/LocalCouncilReport.tsx | 13 ++++-- src/components/templates/Layout.tsx | 4 +- src/utils/index.ts | 1 + src/utils/useGetNameFromId.ts | 24 +++++++++++ 6 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 src/utils/useGetNameFromId.ts diff --git a/src/components/molecules/LocalCouncilReportText/AgeText.tsx b/src/components/molecules/LocalCouncilReportText/AgeText.tsx index 0ca487f..abc5a0e 100644 --- a/src/components/molecules/LocalCouncilReportText/AgeText.tsx +++ b/src/components/molecules/LocalCouncilReportText/AgeText.tsx @@ -46,23 +46,54 @@ interface Props { variation?: AgeTextVariation; /** text에 들어갈 데이터입니다. */ data?: AgeTextData; + getNameFromId: (id: number) => [string, string] | undefined; } -export const AgeText = ({ variation = 1, data = undefined }: Props) => { +export const AgeText = ({ + variation = 1, + data = undefined, + getNameFromId, +}: Props) => { if (!data) return 데이터를 불러오는 중입니다..; const { - metroId, localId, // rankingParagraph, // indexHistoryParagraph, - // ageHistogramParagraph, + ageHistogramParagraph, } = data; if (variation === 1) return ( - 이 지역의 metroId는 {metroId}, localId는{" "} - {localId}입니다. + {ageHistogramParagraph.year}년 전국동시지방선거에서{" "} + {getNameFromId(localId)?.join(" ")} + 에는 {ageHistogramParagraph.candidateCount}명이{" "} + 후보로 나와 {ageHistogramParagraph.electedCount}명이{" "} + 당선됐어요. 당선자의 20%가{" "} + {ageHistogramParagraph.firstQuintile}세 미만, 20%가{" "} + {ageHistogramParagraph.lastQuintile}세 이상이에요. +
+
+ 참고로 다양성 지표 전국 1위는 전체 인원의 20%가{" "} + {ageHistogramParagraph.divArea.firstQuintile}세{" "} + 미만, 20%가{" "} + {ageHistogramParagraph.divArea.lastQuintile}세{" "} + 이상인{" "} + + {getNameFromId(ageHistogramParagraph.divArea.localId)?.join(" ")} + + , 전국 뒤에서 1위는 전체 인원의 20%가{" "} + {ageHistogramParagraph.uniArea.firstQuintile}세 + 미만, 20%가{" "} + {ageHistogramParagraph.uniArea.lastQuintile}세 + 이상인{" "} + + {getNameFromId(ageHistogramParagraph.uniArea.localId)?.join(" ")} + + 예요. +
+
+ 이전 정보를 확인하려면 아래의 슬라이더를 밀어 보세요.
); return 존재하지 않는 템플릿입니다.; diff --git a/src/components/organisms/LocalCouncilMapSelector.tsx b/src/components/organisms/LocalCouncilMapSelector.tsx index 1cf5802..e3f47fa 100644 --- a/src/components/organisms/LocalCouncilMapSelector.tsx +++ b/src/components/organisms/LocalCouncilMapSelector.tsx @@ -1,4 +1,3 @@ -import { useState, useEffect } from "react"; import { Flex } from "antd"; import { css } from "@emotion/react"; import { LocalSelector, MetroSelector } from "@/components/molecules"; diff --git a/src/components/organisms/LocalCouncilReport.tsx b/src/components/organisms/LocalCouncilReport.tsx index a27a883..dda36df 100644 --- a/src/components/organisms/LocalCouncilReport.tsx +++ b/src/components/organisms/LocalCouncilReport.tsx @@ -19,6 +19,7 @@ import { sampleAgeHistogramData, samplePartyPieData, sampleGenderPieData, + useGetNameFromId, } from "@/utils"; const { Title } = Typography; @@ -35,8 +36,10 @@ const LocalCouncilReport = ({ metroName, localName, idMap }: Props) => { const [genderTextData, setGenderTextData] = useState(); const [partyTextData, setPartyTextData] = useState(); + const getNameFromId = useGetNameFromId(idMap); + // 백엔드로부터 텍스트 데이터를 가져옵니다. - useEffect(() => { + const fetchText = () => { axios .get(`localCouncil/template-data/${metroId}/${localId}?factor=age`) .then(response => { @@ -61,7 +64,11 @@ const LocalCouncilReport = ({ metroName, localName, idMap }: Props) => { .catch(() => { alert("네트워크 문제가 발생했습니다. 다시 시도해주세요."); }); - }, []); + }; + + useEffect(() => { + fetchText(); + }, [metroName, localName]); return ( { >{`${metroName} ${localName}의 지역의회 다양성 리포트`} 연령 다양성 - + 성별 다양성 ( // 기본 폰트는 Pretendard로 설정합니다. fontFamily: "Pretendard, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'", + fontSize: 16, }, components: { Layout: { // Header, Content, Footer의 배경색을 설정합니다. headerBg: "#F0F0F0", - bodyBg: "#00E9A1", + // bodyBg: "#00E9A1", + bodyBg: "#F1F1F1", footerBg: "#F0F0F0", }, }, diff --git a/src/utils/index.ts b/src/utils/index.ts index 35d64b4..fc823e9 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,3 @@ export * from "./sampleData"; export { default as axios } from "./axios"; +export { default as useGetNameFromId } from "./useGetNameFromId"; diff --git a/src/utils/useGetNameFromId.ts b/src/utils/useGetNameFromId.ts new file mode 100644 index 0000000..418e163 --- /dev/null +++ b/src/utils/useGetNameFromId.ts @@ -0,0 +1,24 @@ +import { type MetroID } from "static/MapSVGData"; + +/** + * + * @param idMap 백엔드로부터 가져온 ID 맵 + * @returns localId(Number)를 받아 metroName과 localName을 반환하는 함수 + */ +const useGetNameFromId = ( + idMap: Map>, +) => { + // localId를 key로, [metroName, localName]을 value로 가지는 Map + const reverseMap = new Map(); + + idMap.forEach((localMap, metroName) => { + localMap.forEach((localIds, localName) => { + reverseMap.set(localIds[1], [metroName, localName]); + }); + }); + + const getNameFromId = (localId: number) => reverseMap.get(localId); + return getNameFromId; +}; + +export default useGetNameFromId; From 365b7f88656293872c08b5e257e83341cbe8da0d Mon Sep 17 00:00:00 2001 From: withsang Date: Fri, 24 Nov 2023 03:24:23 +0900 Subject: [PATCH 5/9] fix(color): remove main color --- src/components/templates/Layout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/templates/Layout.tsx b/src/components/templates/Layout.tsx index becad87..999e0ae 100644 --- a/src/components/templates/Layout.tsx +++ b/src/components/templates/Layout.tsx @@ -22,8 +22,8 @@ const NewwaysLayout = ({ children }: Props) => ( Layout: { // Header, Content, Footer의 배경색을 설정합니다. headerBg: "#F0F0F0", - // bodyBg: "#00E9A1", - bodyBg: "#F1F1F1", + // bodyBg: "#00E9A1", // ugly... + bodyBg: "#F1F1F1", // much better footerBg: "#F0F0F0", }, }, From 110727d090b69eee1a15385b2fb29b4524f952cb Mon Sep 17 00:00:00 2001 From: withsang Date: Fri, 24 Nov 2023 03:29:27 +0900 Subject: [PATCH 6/9] fix(cors): allow cors --- netlify.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/netlify.toml b/netlify.toml index ff1c050..0c2cd5d 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,4 +1,9 @@ [[redirects]] from = "/*" to = "/index.html" - status = 200 \ No newline at end of file + status = 200 + +[[headers]] + for = "/*" + [headers.values] + Access-Control-Allow-Origin = "https://diversity.tech4impact.kr/" \ No newline at end of file From a315da369a65cd4de7087b8f8f25383db71efea9 Mon Sep 17 00:00:00 2001 From: withsang Date: Fri, 24 Nov 2023 03:31:42 +0900 Subject: [PATCH 7/9] fix(cors): allow cors --- netlify.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netlify.toml b/netlify.toml index 0c2cd5d..5de5db4 100644 --- a/netlify.toml +++ b/netlify.toml @@ -6,4 +6,4 @@ [[headers]] for = "/*" [headers.values] - Access-Control-Allow-Origin = "https://diversity.tech4impact.kr/" \ No newline at end of file + Access-Control-Allow-Origin = "https://diversity-api.tech4impact.kr/" \ No newline at end of file From 72b8153e4a0509ec58c992b97de7a1190edbc32d Mon Sep 17 00:00:00 2001 From: withsang Date: Fri, 24 Nov 2023 03:32:15 +0900 Subject: [PATCH 8/9] fix(cors): allow cors --- netlify.toml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/netlify.toml b/netlify.toml index 5de5db4..a179c12 100644 --- a/netlify.toml +++ b/netlify.toml @@ -2,8 +2,4 @@ from = "/*" to = "/index.html" status = 200 - -[[headers]] - for = "/*" - [headers.values] - Access-Control-Allow-Origin = "https://diversity-api.tech4impact.kr/" \ No newline at end of file + \ No newline at end of file From 9666e3920328272c5c3fabda5c77a763eadf3281 Mon Sep 17 00:00:00 2001 From: withsang Date: Fri, 24 Nov 2023 13:18:50 +0900 Subject: [PATCH 9/9] chart(age, gender): add pie chart --- .../organisms/LocalCouncilReport.tsx | 124 +++++++++++++++--- src/components/organisms/PieChart.tsx | 6 +- src/utils/sampleData.ts | 2 +- 3 files changed, 113 insertions(+), 19 deletions(-) diff --git a/src/components/organisms/LocalCouncilReport.tsx b/src/components/organisms/LocalCouncilReport.tsx index dda36df..b0d0bd4 100644 --- a/src/components/organisms/LocalCouncilReport.tsx +++ b/src/components/organisms/LocalCouncilReport.tsx @@ -12,18 +12,34 @@ import { type PartyTextData, } from "@/components/molecules/LocalCouncilReportText"; import { AgeHistogram } from "@/components/organisms/Histogram"; -import { PieChart } from "@/components/organisms/PieChart"; +import { PieChart, type PieChartData } from "@/components/organisms/PieChart"; import { axios, sampleAgeHistogramData, samplePartyPieData, - sampleGenderPieData, useGetNameFromId, } from "@/utils"; const { Title } = Typography; +type Gender = "남" | "여"; + +type GenderPieChartDataAPIResponse = { + gender: Gender; + count: number; +}[]; + +type PartyPieChartColorAPIResponse = { + name: string; + color: string; +}[]; + +type PartyPieChartDataAPIResponse = { + party: string; + count: number; +}[]; + interface Props { metroName: MetroID; localName: string; @@ -34,19 +50,26 @@ const LocalCouncilReport = ({ metroName, localName, idMap }: Props) => { const [metroId, localId] = idMap.get(metroName)?.get(localName) || [1, 1]; const [ageTextData, setAgeTextData] = useState(); const [genderTextData, setGenderTextData] = useState(); + const [genderPieChartData, setGenderPieChartData] = + useState(); + const [genderPieChartColorMap, setGenderPieChartColorMap] = + useState>(); + const [partyPieChartData, setPartyPieChartData] = useState(); + const [partyPieChartColorMap, setPartyPieChartColorMap] = + useState>(); const [partyTextData, setPartyTextData] = useState(); const getNameFromId = useGetNameFromId(idMap); // 백엔드로부터 텍스트 데이터를 가져옵니다. - const fetchText = () => { + const fetchTextData = () => { axios .get(`localCouncil/template-data/${metroId}/${localId}?factor=age`) .then(response => { setAgeTextData(response.data as AgeTextData); }) .catch(() => { - alert("네트워크 문제가 발생했습니다. 다시 시도해주세요."); + throw new Error("네트워크 문제가 발생했습니다. 다시 시도해주세요."); }); axios .get(`localCouncil/template-data/${metroId}/${localId}?factor=gender`) @@ -54,7 +77,7 @@ const LocalCouncilReport = ({ metroName, localName, idMap }: Props) => { setGenderTextData(response.data as GenderTextData); }) .catch(() => { - alert("네트워크 문제가 발생했습니다. 다시 시도해주세요."); + throw new Error("네트워크 문제가 발생했습니다. 다시 시도해주세요."); }); axios .get(`localCouncil/template-data/${metroId}/${localId}?factor=party`) @@ -62,12 +85,85 @@ const LocalCouncilReport = ({ metroName, localName, idMap }: Props) => { setPartyTextData(response.data as PartyTextData); }) .catch(() => { - alert("네트워크 문제가 발생했습니다. 다시 시도해주세요."); + throw new Error("네트워크 문제가 발생했습니다. 다시 시도해주세요."); + }); + }; + + // 백엔드로부터 그래프 색상들을 가져옵니다. + const fetchGraphColors = () => { + const newGenderPieChartColorMap = new Map(); + const genderColors = [ + { + type: "남", + color: "#289FD4", + }, + { + type: "여", + color: "#AE2D6C", + }, + ]; + genderColors.forEach(({ type, color }) => { + newGenderPieChartColorMap.set(type, color); + }); + setGenderPieChartColorMap(newGenderPieChartColorMap); + + axios + .get("localCouncil/partyInfo") + .then(response => { + const data = response.data as PartyPieChartColorAPIResponse; + const newPartyPieChartColorMap = new Map(); + data.forEach(({ name, color }) => { + newPartyPieChartColorMap.set(name, color); + }); + setPartyPieChartColorMap(newPartyPieChartColorMap); + }) + .catch(() => { + throw new Error("네트워크 문제가 발생했습니다. 다시 시도해주세요."); + }); + }; + + // 백엔드로부터 그래프 데이터를 가져옵니다. + const fetchGraphData = () => { + axios + .get(`localCouncil/chart-data/${metroId}/${localId}?factor=gender`) + .then(response => { + const data = response.data.data as GenderPieChartDataAPIResponse; + const newGenderPieChartData: PieChartData[] = []; + data.forEach(({ gender, count }) => { + newGenderPieChartData.push({ + type: gender, + value: count, + }); + }); + setGenderPieChartData(newGenderPieChartData); + }) + .catch(() => { + throw new Error("네트워크 문제가 발생했습니다. 다시 시도해주세요."); + }); + + axios + .get(`localCouncil/chart-data/${metroId}/${localId}?factor=party`) + .then(response => { + const data = response.data.data as PartyPieChartDataAPIResponse; + const newPartyPieChartData: PieChartData[] = []; + data.forEach(({ party, count }) => { + newPartyPieChartData.push({ + type: party, + value: count, + }); + }); + setPartyPieChartData(newPartyPieChartData); + }) + .catch(() => { + throw new Error("네트워크 문제가 발생했습니다. 다시 시도해주세요."); }); }; + useEffect(fetchGraphColors, []); + useEffect(() => { - fetchText(); + fetchTextData(); + fetchGraphData(); }, [metroName, localName]); return ( @@ -85,16 +181,14 @@ const LocalCouncilReport = ({ metroName, localName, idMap }: Props) => { 성별 다양성 - + {genderPieChartData && genderPieChartColorMap ? ( + + ) : null} 정당 다양성 - + {partyPieChartData && partyPieChartColorMap ? ( + + ) : null} ); diff --git a/src/components/organisms/PieChart.tsx b/src/components/organisms/PieChart.tsx index e43e4b5..7c6d429 100644 --- a/src/components/organisms/PieChart.tsx +++ b/src/components/organisms/PieChart.tsx @@ -8,10 +8,10 @@ export interface PieChartData { export interface Props { data: PieChartData[]; - colors: string[]; + colorMap: Map; } -export const PieChart = ({ data, colors }: Props) => { +export const PieChart = ({ data, colorMap }: Props) => { const config: PieConfig = { appendPadding: 10, data, @@ -22,7 +22,7 @@ export const PieChart = ({ data, colors }: Props) => { content: "{name} {percentage}", }, colorField: "type", - color: colors, + color: data.map(({ type }) => colorMap.get(type) || "#888888"), interactions: [ { type: "pie-legend-active", diff --git a/src/utils/sampleData.ts b/src/utils/sampleData.ts index 850eb81..aa7b696 100644 --- a/src/utils/sampleData.ts +++ b/src/utils/sampleData.ts @@ -46,7 +46,7 @@ export const samplePartyPieData = { }; export const sampleGenderPieData = { - colors: ["#FF0000", "#0000FF"], + colors: ["#289FD4", "#AE2D6C"], data: [ { type: "남성",