From d02b17ba120ac53add4212148200719a97ab143c Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 4 Jul 2025 17:30:50 +0900 Subject: [PATCH 1/3] =?UTF-8?q?dc-service=20=EC=A0=91=EC=86=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/.env.local | 5 +++-- .../components/popupComponent/chart/aplcnYms.ts | 16 ++++++++++++---- .../popupComponent/chart/infilChart.tsx | 6 +++--- .../popupComponent/chart/rdiiChart.tsx | 2 +- apps/sn/app/components/lib/api/index.ts | 14 ++++++++++++++ apps/sn/middleware.ts | 15 +++++++++++++++ 6 files changed, 48 insertions(+), 10 deletions(-) diff --git a/apps/sn/.env.local b/apps/sn/.env.local index 26a9a0490..3354fcc4c 100644 --- a/apps/sn/.env.local +++ b/apps/sn/.env.local @@ -2,9 +2,10 @@ NEXT_PUBLIC_SITE_ID = SN NEXT_PUBLIC_PORT = 3002 NEXT_PUBLIC_PROXY_HOST = http://localhost:3002 #NEXT_PUBLIC_PROXY_HOST = http://sn.hasu.or.kr:3002 -NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 -NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver +NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver #TODO 운영에서 바꿔야 함 +NEXT_PUBLIC_RSONE_DC_URL = https://apigateway.rsonesoft.com/dc-service #자료관 데이터 NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts index 7f4acf616..1cd0d6ded 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts @@ -1,5 +1,5 @@ import {useState, useEffect} from 'react'; -import { getData } from '@/components/lib/api'; +import { getData, getDcData } from '@/components/lib/api'; import moment from "moment"; export type TInftwtAnls = { @@ -64,6 +64,13 @@ export type TOilqData = { mtrngDt: string; // 계측일시 oilq: number; // 유량 } + +export const getDcTree = async () => { + const queryString = ''; + const result = await getDcData(`/api/v1/dc/tree${queryString ? `?${queryString}` : ''}`); + return result; +}; + const getOptionsAplcnYm = (yyyymms: string[]): { text: string; val: string }[] => { let yyyymms1 = yyyymms.map(dateStr => { const year = dateStr.slice(0, 4); @@ -88,6 +95,7 @@ const selectAplcnYms = async(gubun:string) => { // 상수사용월 console.error(error); } }; + export const useAplcnYms = (gubun:string) => { const [data, setData] = useState<{ text: string; val: string }[]>([]); @@ -124,7 +132,7 @@ export const selectInftwtAnls = async(aplcnYm:String) => { // 침입수분석 }; export const getChartDataInftwtAnlsRslt = (arr: TInftwtAnlsRslt[]): { label: string; val: number }[] => { const options = arr.map(anlsRslt => ({ - label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("DD HH:mm"), + label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("HH시"), val: anlsRslt.predcOilq, })); return options; @@ -223,14 +231,14 @@ export const selectRdiiAnls = async(aplcnYm:String) => { // 침입수분석 }; export const getChartDataRdiiAnlsRslt = (arr: TRdiiAnlsRslt[]): { label: string; val: number }[] => { const options = arr.map(anlsRslt => ({ - label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("DD HH:mm"), + label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("MM-DD HH시"), val: anlsRslt.predcOilq, })); return options; }; export const getChartDataRdiiAnlsRsltMinq = (arr: TRdiiAnlsRslt[]): { label: string; val: number }[] => { const options = arr.map(anlsRslt => ({ - label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("DD HH:mm"), + label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("MM-DD HH시"), val: anlsRslt.minq, })); return options; diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx index 090e02f64..8ad2a4362 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx @@ -30,8 +30,8 @@ const InfilChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo }); const [inftwtQty, setInftwtQty] = useState(0); // 침입수량 const aplcnYms = fn.useAplcnYms('inftwtAnlsRslt'); // 상수사용월 데이터 가져오기 - pplMngNo = "L"+pplMngNo; // "L13101003"; - console.log("pplMngNo", pplMngNo); + const dcData = fn.getDcTree(); // 자료관 데이터 가져오기 + pplMngNo = "L"+pplMngNo; // inp파일에 하수관로와 맨홀의 번호가 같으면 분석 오류가 발행하여 const openPopup = () => { if (buttonRef.current) { const rect = buttonRef.current.getBoundingClientRect(); // 버튼 위치 계산 @@ -51,7 +51,7 @@ const InfilChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo useEffect(() => { if (isModalOpen === false) { - setPopupOpen(false); // 모달이 닫힐 때 조회조건팝업은 열어두기 + setPopupOpen(true); // 모달이 닫힐 때 조회조건팝업은 열어두기 setAplcnYm(""); // 상수사용월 초기화 setInftwtAnlss([]); // 침입수분석 초기화 setInftwtAnlsInfos([{ val: 0, text: '선택' }]); // 침입수분석정보 초기화 diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx index 930f83a6d..ddaffcaf3 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx @@ -52,7 +52,7 @@ const RdiiChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo } useEffect(() => { if (isModalOpen === false) { - setPopupOpen(false); // 모달이 닫힐 때 팝업은 + setPopupOpen(true); // 모달이 닫힐 때 팝업은 setAplcnYm(""); // 상수사용월 초기화 setRdiiAnlss([]); // 강우유입수분석 초기화 setRdiiAnlsInfos([{ val: 0, text: '선택' }]); // 강우유입수분석정보 초기화 diff --git a/apps/sn/app/components/lib/api/index.ts b/apps/sn/app/components/lib/api/index.ts index beb527c65..1d711b18a 100644 --- a/apps/sn/app/components/lib/api/index.ts +++ b/apps/sn/app/components/lib/api/index.ts @@ -1,4 +1,5 @@ const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ''; +const DC_URL = process.env.NEXT_PUBLIC_RSONE_DC_URL || ''; export const postData = async (url: string, data: object): Promise => { const options: RequestInit = { @@ -54,6 +55,19 @@ export const getData = async (url: string, data?: object): Promise => { throw new Error('Failed to fetch data'); }; +export const getDcData = async (url: string, data?: object): Promise => { + console.log(url); + console.log(`${DC_URL}${url}`); + const res = await fetch(`${DC_URL}${url}`, { + method: "GET", + }); + if (res.ok) { + const data = await res.json(); + return data; + } + throw new Error('Failed to fetch data'); +}; + export const deleteData = async (url: string, data?: object): Promise => { const options: RequestInit = { method: "DELETE", diff --git a/apps/sn/middleware.ts b/apps/sn/middleware.ts index 3ad5d5129..546516261 100644 --- a/apps/sn/middleware.ts +++ b/apps/sn/middleware.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import path from "path"; const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; @@ -6,6 +7,7 @@ const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL || ""; const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ""; const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || ""; const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || ""; +const RSONE_DC_URL = process.env.NEXT_PUBLIC_RSONE_DC_URL || ""; export function middleware(req: NextRequest) { console.log("Middleware 진입:", req.nextUrl.pathname); @@ -80,6 +82,17 @@ export function middleware(req: NextRequest) { }); } + if (pathname.startsWith("/dc-service")) { + const requestHeaders = new Headers(req.headers); + const newUrl = `${RSONE_DC_URL}${pathname}`; + + console.log( + `[Middleware] Rewriting from '/dc-service' path to: ${newUrl}`, + ); + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } return NextResponse.next(); } @@ -91,5 +104,7 @@ export const config = { "/user-service/:path*", "/cms-service/:path*", "/sewerage-service/api/v1/map/geometry/:path*", + "/dc-service/:path*", ], + }; From 970bfc6c8211ae3d515fda837829f3dc4305c0c6 Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 4 Jul 2025 19:36:05 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=EC=9E=90=EB=A3=8C=EA=B4=80=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/.env.local | 2 +- .../map/components/popupComponent/chart/infilChart.tsx | 4 ++-- apps/sn/app/components/lib/api/index.ts | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/sn/.env.local b/apps/sn/.env.local index 3354fcc4c..109b519ea 100644 --- a/apps/sn/.env.local +++ b/apps/sn/.env.local @@ -5,7 +5,7 @@ NEXT_PUBLIC_PROXY_HOST = http://localhost:3002 NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver #TODO 운영에서 바꿔야 함 -NEXT_PUBLIC_RSONE_DC_URL = https://apigateway.rsonesoft.com/dc-service #자료관 데이터 +NEXT_PUBLIC_RSONE_DC_URL = https://apigateway.rsonesoft.com #자료관 데이터 NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx index 8ad2a4362..46c94eaf6 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx @@ -30,7 +30,7 @@ const InfilChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo }); const [inftwtQty, setInftwtQty] = useState(0); // 침입수량 const aplcnYms = fn.useAplcnYms('inftwtAnlsRslt'); // 상수사용월 데이터 가져오기 - const dcData = fn.getDcTree(); // 자료관 데이터 가져오기 + pplMngNo = "L"+pplMngNo; // inp파일에 하수관로와 맨홀의 번호가 같으면 분석 오류가 발행하여 const openPopup = () => { if (buttonRef.current) { @@ -70,7 +70,7 @@ const InfilChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo setInftwtAnlsInfos([{ val: 0, text: '선택' }]); return; } - + const dcData = fn.getDcTree(); // 자료관 데이터 가져오기 fn.selectInftwtAnls(aplcnYm) .then((data) => { setInftwtAnlss(data ?? []); diff --git a/apps/sn/app/components/lib/api/index.ts b/apps/sn/app/components/lib/api/index.ts index 1d711b18a..11267a573 100644 --- a/apps/sn/app/components/lib/api/index.ts +++ b/apps/sn/app/components/lib/api/index.ts @@ -1,5 +1,4 @@ const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ''; -const DC_URL = process.env.NEXT_PUBLIC_RSONE_DC_URL || ''; export const postData = async (url: string, data: object): Promise => { const options: RequestInit = { @@ -57,8 +56,7 @@ export const getData = async (url: string, data?: object): Promise => { export const getDcData = async (url: string, data?: object): Promise => { console.log(url); - console.log(`${DC_URL}${url}`); - const res = await fetch(`${DC_URL}${url}`, { + const res = await fetch('/dc-service'+url, { method: "GET", }); if (res.ok) { From 80b356f66b304ab9946fe77635607ce245a959fa Mon Sep 17 00:00:00 2001 From: hsson Date: Sat, 5 Jul 2025 22:37:03 +0900 Subject: [PATCH 3/3] =?UTF-8?q?React.memo=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/popupComponent/MsSwagePpl.tsx | 1 - .../popupComponent/chart/infilChart.tsx | 14 +++--- .../popupComponent/chart/rdiiChart.tsx | 14 +++--- .../popupComponent/chartModalStore.ts | 16 +++++++ .../map/featureClickEventPopup.tsx | 47 +++---------------- 5 files changed, 35 insertions(+), 57 deletions(-) create mode 100644 apps/sn/app/(map-layout)/map/components/popupComponent/chartModalStore.ts diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx index bf018340b..46537a7dd 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx @@ -4,7 +4,6 @@ import React, { useState } from 'react' import InputSection from '../InputSection'; import { handleInputChange } from '../common'; - // 하수관로 const inputColumn = [ { id: "title", title: "설치주제", type: "select", options: ["BTL", "재정사업"] }, diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx index 46c94eaf6..ac89a9403 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/infilChart.tsx @@ -7,16 +7,14 @@ import moment from "moment"; import type { TInftwtAnls, TOilqData } from './aplcnYms'; import * as fn from './aplcnYms'; +import { useInfilChartModalStore } from '../chartModalStore'; // 모달 상태 관리 스토어 type ModalOpenProp = { - isModalOpen: boolean, - setIsModalOpen: (open: boolean) => void, - closeModal: () => void, - btnTxt?: string, pplMngNo?: string, // 관로관리번호 } type DataType = {label: string; val: number}; -const InfilChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo }: ModalOpenProp) => { +const InfilChart = ({ pplMngNo }: ModalOpenProp) => { + const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useInfilChartModalStore(); // 모달 상태 관리 스토어 const [popupOpen, setPopupOpen] = React.useState(false); // 조회조건팝업 const buttonRef = React.useRef(null); // 버튼 참조 const [popupPosition, setPopupPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 }); // 팝업 위치 @@ -130,7 +128,7 @@ const InfilChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo <>
침입수분석결과
-
+
{/* 차트 */} -
+
@@ -271,4 +269,4 @@ const InfilChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo ) } -export default InfilChart \ No newline at end of file +export default React.memo(InfilChart); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx index ddaffcaf3..653b568b5 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx @@ -7,16 +7,14 @@ import moment from "moment"; import type { TRdiiAnls, TRdiiAnlsRslt, TOilqData } from './aplcnYms'; import * as fn from './aplcnYms'; +import { useRdiiChartModalStore } from '../chartModalStore'; // 모달 상태 관리 스토어 type ModalOpenProp = { - isModalOpen: boolean, - setIsModalOpen: (open: boolean) => void, - closeModal: () => void, - btnTxt?: string, pplMngNo: string, // 관로관리번호 } type DataType = {label: string; val: number}; -const RdiiChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo }: ModalOpenProp) => { +const RdiiChart = ({ pplMngNo }: ModalOpenProp) => { + const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useRdiiChartModalStore(); // 모달 상태 관리 스토어 const [popupOpen, setPopupOpen] = React.useState(false); // 조회조건팝업 const buttonRef = React.useRef(null); // 버튼 참조 const [popupPosition, setPopupPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 }); // 팝업 위치 @@ -117,7 +115,7 @@ const RdiiChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo } <>
RDII분석결과
-
+
{/* 차트 */} -
+
@@ -263,4 +261,4 @@ const RdiiChart = ({ btnTxt, isModalOpen, setIsModalOpen, closeModal, pplMngNo } ) } -export default RdiiChart \ No newline at end of file +export default React.memo(RdiiChart); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chartModalStore.ts b/apps/sn/app/(map-layout)/map/components/popupComponent/chartModalStore.ts new file mode 100644 index 000000000..c84fa48a7 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chartModalStore.ts @@ -0,0 +1,16 @@ +import { create } from 'zustand'; + +interface ModalStore { + isOpen: boolean; + setIsOpen: (open: boolean) => void; +} + +export const useInfilChartModalStore = create((set) => ({ + isOpen: false, + setIsOpen: (open: boolean) => set({ isOpen: open }), +})); + +export const useRdiiChartModalStore = create((set) => ({ + isOpen: false, + setIsOpen: (open: boolean) => set({ isOpen: open }), +})); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/featureClickEventPopup.tsx b/apps/sn/app/(map-layout)/map/featureClickEventPopup.tsx index a6dac412d..16c4f99a8 100644 --- a/apps/sn/app/(map-layout)/map/featureClickEventPopup.tsx +++ b/apps/sn/app/(map-layout)/map/featureClickEventPopup.tsx @@ -9,6 +9,7 @@ import { getLayerContentComponent } from "./components/common"; import { SwtrmtFcltCol, SwtrmtPrcsZoneCol, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, SewagePump, MsSwageMhlCol, stofcbrCol } from "./components/common/table"; import InfilChart from './components/popupComponent/chart/infilChart'; import RdiiChart from './components/popupComponent/chart/rdiiChart'; +import { useInfilChartModalStore, useRdiiChartModalStore } from "./components/popupComponent/chartModalStore"; // TODO: const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => { @@ -28,9 +29,8 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, const [isAdditionalDataLoading, setIsAdditionalDataLoading] = useState(false); const [additionalDataError, setAdditionalDataError] = useState(null); // 🎯 InfilChart 및 RdiiChart 관련 상태 추가 - const [isModalInfil, setIsModalInfil] = useState(false); - const [isModalRdii, setIsModalRdii] = useState(false); - const [btnTxt, setBtnTxt] = useState(""); + const { setIsOpen:setIsModalInfil } = useInfilChartModalStore(); + const { setIsOpen:setIsModalRdii } = useRdiiChartModalStore(); // 🎯 관로관리번호 추출 const pplMngNo = featureData?.values_?.fclt_mng_no || ""; @@ -237,25 +237,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, setIsModalOpen(false); // 모달 닫기 }; - // 🎯 모달 열기/닫기 함수들 - const openModalInfil = () => { - setBtnTxt("Infil"); - setIsModalInfil(true); - }; - - const closeModalInfil = () => { - setIsModalInfil(false); - }; - - const openModalRdii = () => { - setBtnTxt("RDII"); - setIsModalRdii(true); - }; - - const closeModalRdii = () => { - setIsModalRdii(false); - }; - const btnClickEvent = async () => { const bodyData: Record = {} if (buttonChk && contentRef.current) { @@ -305,17 +286,15 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, {/* 🎯 하수관로일 때만 Infil/RDII 버튼 표시 */} {layerNm === '하수관로' && ( <> - - @@ -363,20 +342,8 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
{/* 🎯 InfilChart 및 RdiiChart 컴포넌트 추가 */} - - + +
) }