From ebdaa668cb79a9fa1c920a151578dd1361db4094 Mon Sep 17 00:00:00 2001 From: seoyeon Moon <66253833+seoyeon08@users.noreply.github.com> Date: Thu, 4 Jul 2024 16:35:46 +0900 Subject: [PATCH] =?UTF-8?q?[Feat/#41]=20=EC=A0=84=EC=B2=B4=20=EB=A7=A4?= =?UTF-8?q?=EC=B6=9C=EC=95=A1,=20=EC=9B=94=EB=B3=84=20=EB=A7=A4=EC=B6=9C?= =?UTF-8?q?=EC=95=A1,=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=B3=84=20=EC=97=B0?= =?UTF-8?q?=EA=B0=84=20=EB=A7=A4=EC=B6=9C=EC=95=A1,=20=EC=A7=80=EC=B6=9C?= =?UTF-8?q?=EC=95=A1=20=EC=88=98=EC=A0=95=20api=20=EC=97=B0=EA=B2=B0=20(#4?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 전체 매출액 api 연결 * feat: 월별 매출액 api 연결 * feat: 클래스별 연간 매출액 api 연결 * feat: 지출액 수정 api 연결 --- src/apis/apiClient.ts | 46 ++++++-- src/apis/interfaces/revenueApi.ts | 15 +++ src/components/molecules/Calculator.tsx | 95 ++++++++++----- src/components/molecules/EditPrice.tsx | 74 ++++++------ src/components/molecules/LineChart.tsx | 117 +------------------ src/components/molecules/PieChart.tsx | 2 +- src/components/molecules/TotalSales.tsx | 12 +- src/components/molecules/TotalSalesCard.tsx | 31 +++-- src/components/organisms/LessonSalesList.tsx | 4 +- src/pages/main/HanaFunMain.tsx | 2 +- src/pages/mypage/Sales.tsx | 16 +-- src/pages/mypage/SalesYear.tsx | 90 +++++++++++++- src/types/revenue.d.ts | 35 ++++++ 13 files changed, 328 insertions(+), 211 deletions(-) create mode 100644 src/apis/interfaces/revenueApi.ts create mode 100644 src/types/revenue.d.ts diff --git a/src/apis/apiClient.ts b/src/apis/apiClient.ts index 88ed96f..a6794c2 100644 --- a/src/apis/apiClient.ts +++ b/src/apis/apiClient.ts @@ -297,7 +297,6 @@ export class ApiClient return response.data; } - //---------revenue--------- // 예약자 정보 async peopleList(lessondateId: PeopleListReqType) { console.log('전달된 lessondate_id: ', lessondateId); @@ -314,12 +313,45 @@ export class ApiClient //---------revenue--------- - // 임의 데이터. 클래스 년/월 별 매출액 - public static async getMonthSales(): Promise { - const apiUrl = '/data/monthRevenue.json'; - const response = await fetch(apiUrl); - const data = await response.json(); - return data; + async getTotal() { + const response = await this.axiosInstance.request< + BaseResponseType + >({ + method: 'get', + url: '/revenue/total', + }); + return response.data; + } + + async getMonthRevenue(year: number, month: number) { + const response = await this.axiosInstance.request< + BaseResponseType + >({ + method: 'get', + url: `/revenue/${year}/${month}`, + }); + return response.data; + } + + async getLessonRevenue(year: number, lessonId: number) { + const response = await this.axiosInstance.request< + BaseResponseType + >({ + method: 'get', + url: `/revenue/lesson/${year}/${lessonId}`, + }); + return response.data; + } + + async updatePrice(reqData: PriceReqType) { + const response = await this.axiosInstance.request< + BaseResponseType + >({ + method: 'put', + url: '/revenue/update', + data: reqData, + }); + return response.data; } static getInstance(): ApiClient { diff --git a/src/apis/interfaces/revenueApi.ts b/src/apis/interfaces/revenueApi.ts new file mode 100644 index 0000000..15ed048 --- /dev/null +++ b/src/apis/interfaces/revenueApi.ts @@ -0,0 +1,15 @@ +export interface revenueApi { + getTotal(): Promise>; + + getMonthRevenue( + year: number, + month: number + ): Promise>; + + getLessonRevenue( + year: number, + lessonId: number + ): Promise>; + + updatePrice(reqData: PriceReqType): Promise>; +} diff --git a/src/components/molecules/Calculator.tsx b/src/components/molecules/Calculator.tsx index 0ae5194..209595f 100644 --- a/src/components/molecules/Calculator.tsx +++ b/src/components/molecules/Calculator.tsx @@ -1,30 +1,42 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { BsPencil } from 'react-icons/bs'; import { GrFormNext, GrFormPrevious } from 'react-icons/gr'; -import Keypad from '../common/Keypad'; import { EditPrice } from './EditPrice'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { ApiClient } from '../../apis/apiClient'; +interface IProps { + data: any; + lessonId: number; + year: number; +} const formatNumber = (value: number) => { return new Intl.NumberFormat('ko-KR').format(value); }; -export const Calculator = () => { +export const Calculator = ({ data, lessonId, year }: IProps) => { const currentMonth = new Date().getMonth() + 1; - const [month, setMonth] = useState(6); - + const [month, setMonth] = useState(currentMonth); const [editPriceVisible, setEditPriceVisible] = useState(false); - const [keypadVisible, setKeypadVisible] = useState(false); - const [valueSetter, setValueSetter] = useState< - ((value: string) => void) | null - >(null); - const [totalSales, setTotalSales] = useState(762000); - const [totalPrice, setTotalPrice] = useState(250000); - const [materialPrice, setMaterialPrice] = useState(100000); - const [rentalPrice, setRentalPrice] = useState(100000); - const [etcPrice, setEtcPrice] = useState(50000); + const [totalSales, setTotalSales] = useState(0); + const [totalPrice, setTotalPrice] = useState(0); + const [materialPrice, setMaterialPrice] = useState(0); + const [rentalPrice, setRentalPrice] = useState(0); + const [etcPrice, setEtcPrice] = useState(0); const netProfit = totalSales - totalPrice; + useEffect(() => { + const currentMonthData = data.find((d: any) => d.month === month); + if (currentMonthData) { + setTotalSales(currentMonthData.totalRevenue); + setMaterialPrice(currentMonthData.materialPrice); + setRentalPrice(currentMonthData.rentalPrice); + setEtcPrice(currentMonthData.etcPrice); + setTotalPrice(currentMonthData.totalSales); + } + }, [month, data]); + const handlePreviousMonth = () => { setMonth((prevMonth) => (prevMonth > 1 ? prevMonth - 1 : 1)); }; @@ -37,19 +49,47 @@ export const Calculator = () => { const openEditPrice = () => { setEditPriceVisible(true); - setKeypadVisible(true); }; const closeEditPrice = () => { setEditPriceVisible(false); - setKeypadVisible(false); }; - const saveEditPrice = (material: string, rental: string, etc: string) => { - setMaterialPrice(Number(material)); - setRentalPrice(Number(rental)); - setEtcPrice(Number(etc)); - setTotalPrice(Number(material) + Number(rental) + Number(etc)); + const reqData: PriceReqType = { + lessonId: lessonId, + year: year, + month: month, + materialPrice: materialPrice, + rentalPrice: rentalPrice, + etcPrice: etcPrice, + }; + const queryClient = useQueryClient(); + const { mutate: updatePrice } = useMutation({ + mutationFn: async () => { + const response = await ApiClient.getInstance().updatePrice(reqData); + return response; + }, + onSuccess: async (response) => { + console.log('성공'); + if (response.data) { + setMaterialPrice(response.data.materialPrice); + setRentalPrice(response.data.rentalPrice); + setEtcPrice(response.data.etcPrice); + setTotalPrice( + response.data.materialPrice + + response.data.rentalPrice + + response.data.etcPrice + ); + queryClient.invalidateQueries({ queryKey: ['monthRevenue'] }); + } + }, + onError: async () => { + console.log('에러'); + }, + }); + + const saveEditPrice = () => { + updatePrice(); }; return ( @@ -106,15 +146,14 @@ export const Calculator = () => { )} - {/* {keypadVisible && ( - valueSetter && valueSetter(value)} - closeKeypad={() => setKeypadVisible(false)} - /> - )} */} ); }; diff --git a/src/components/molecules/EditPrice.tsx b/src/components/molecules/EditPrice.tsx index 514a3ed..a6d4727 100644 --- a/src/components/molecules/EditPrice.tsx +++ b/src/components/molecules/EditPrice.tsx @@ -1,48 +1,59 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { CgClose } from 'react-icons/cg'; interface IProps { closeEditPrice: () => void; - saveEditPrice: ( - materialPrice: string, - rentalPrice: string, - etcPrice: string - ) => void; - setValue: (setter: (value: string) => void) => void; + saveEditPrice: () => void; + initialMaterialPrice: number; + initialRentalPrice: number; + initialEtcPrice: number; + setMaterialPrice: (materialPrice: number) => void; + setRentalPrice: (rentalPrice: number) => void; + setEtcPrice: (etcPrice: number) => void; } -const formatNumber = (value: string) => { - return value.replace(/\B(?=(\d{3})+(?!\d))/g, ','); -}; - -const unformatNumber = (value: string) => { - return value.replace(/,/g, ''); +const formatNumber = (value: number) => { + return value.toLocaleString('ko-KR'); }; export const EditPrice = ({ closeEditPrice, saveEditPrice, - setValue, + initialMaterialPrice, + initialRentalPrice, + initialEtcPrice, + setMaterialPrice, + setRentalPrice, + setEtcPrice, }: IProps) => { - const [materialPrice, setMaterialPrice] = useState(''); - const [rentalPrice, setRentalPrice] = useState(''); - const [etcPrice, setEtcPrice] = useState(''); + const [materialPrice, setInputMaterialPrice] = useState(0); + const [rentalPrice, setInputRentalPrice] = useState(0); + const [etcPrice, setInputEtcPrice] = useState(0); + + useEffect(() => { + setInputMaterialPrice(initialMaterialPrice); + setInputRentalPrice(initialRentalPrice); + setInputEtcPrice(initialEtcPrice); + }, [initialMaterialPrice, initialRentalPrice, initialEtcPrice]); const handleSave = () => { - saveEditPrice( - unformatNumber(materialPrice), - unformatNumber(rentalPrice), - unformatNumber(etcPrice) - ); + setMaterialPrice(materialPrice); + setRentalPrice(rentalPrice); + setEtcPrice(etcPrice); + saveEditPrice(); closeEditPrice(); }; + const unformatNumber = (value: string) => { + return value.replace(/,/g, ''); + }; + const handleChange = - (setter: (value: string) => void) => + (setter: (value: number) => void) => (e: React.ChangeEvent) => { const { value } = e.target; if (/^\d*\.?\d*$/.test(unformatNumber(value))) { - setter(formatNumber(unformatNumber(value))); + setter(Number(unformatNumber(value))); } }; @@ -61,9 +72,8 @@ export const EditPrice = ({ setValue(setMaterialPrice)} + value={formatNumber(materialPrice)} + onChange={handleChange(setInputMaterialPrice)} />{' '} 원 @@ -74,9 +84,8 @@ export const EditPrice = ({ setValue(setRentalPrice)} + value={formatNumber(rentalPrice)} + onChange={handleChange(setInputRentalPrice)} />{' '} 원 @@ -87,9 +96,8 @@ export const EditPrice = ({ setValue(setEtcPrice)} + value={formatNumber(etcPrice)} + onChange={handleChange(setInputEtcPrice)} />{' '} 원 diff --git a/src/components/molecules/LineChart.tsx b/src/components/molecules/LineChart.tsx index 55dab74..756a7ec 100644 --- a/src/components/molecules/LineChart.tsx +++ b/src/components/molecules/LineChart.tsx @@ -1,127 +1,20 @@ import { ResponsiveLine } from '@nivo/line'; -export const LineChart = () => { - const data = [ - { - id: '매출액', - data: [ - { - x: 1, - y: 187, - }, - { - x: 2, - y: 199, - }, - { - x: 3, - y: 258, - }, - { - x: 4, - y: 155, - }, - { - x: 5, - y: 167, - }, - { - x: 6, - y: 139, - }, - { - x: 7, - y: 100, - }, - { - x: 8, - y: 211, - }, - { - x: 9, - y: 23, - }, - { - x: 10, - y: 0, - }, - { - x: 11, - y: 283, - }, - { - x: 12, - y: 152, - }, - ], - }, - { - id: '순수익', - data: [ - { - x: 1, - y: 218, - }, - { - x: 2, - y: 89, - }, - { - x: 3, - y: 276, - }, - { - x: 4, - y: 101, - }, - { - x: 5, - y: 40, - }, - { - x: 6, - y: 280, - }, - { - x: 7, - y: 40, - }, - { - x: 8, - y: 124, - }, - { - x: 9, - y: 250, - }, - { - x: 10, - y: 23, - }, - { - x: 11, - y: 295, - }, - { - x: 12, - y: 124, - }, - ], - }, - ]; +export const LineChart = ({ data }: any) => { return ( { diff --git a/src/components/molecules/TotalSales.tsx b/src/components/molecules/TotalSales.tsx index 3d0b5ea..e543af1 100644 --- a/src/components/molecules/TotalSales.tsx +++ b/src/components/molecules/TotalSales.tsx @@ -1,13 +1,19 @@ -export const TotalSales = () => { +interface IProps { + data: TotalType | undefined; +} + +export const TotalSales = ({ data }: IProps) => { const formatNumber = (value: number) => { return new Intl.NumberFormat('ko-KR').format(value); }; - const totalSales = 3000000; + + const totalRevenue = data?.totalRevenue ?? 0; + return (

전체 매출액

- {formatNumber(totalSales)} 원 + {formatNumber(totalRevenue)}

); diff --git a/src/components/molecules/TotalSalesCard.tsx b/src/components/molecules/TotalSalesCard.tsx index 634d759..964a747 100644 --- a/src/components/molecules/TotalSalesCard.tsx +++ b/src/components/molecules/TotalSalesCard.tsx @@ -2,26 +2,37 @@ import { useState } from 'react'; import { GrFormNext, GrFormPrevious } from 'react-icons/gr'; import { LessonSalesList } from '../organisms/LessonSalesList'; import { PieChart } from './PieChart'; - -interface Iprops { - initYear: number; - initMonth: number; - data: MonthSalesType[] | undefined; -} +import { useQuery } from '@tanstack/react-query'; +import { ApiClient } from '../../apis/apiClient'; const formatNumber = (value: number) => { return new Intl.NumberFormat('ko-KR').format(value); }; -export const TotalSalesCard = ({ initYear, initMonth, data }: Iprops) => { +export const TotalSalesCard = () => { + const date = new Date(); + const initYear: number = date.getFullYear(); + const initMonth: number = date.getMonth() + 1; + const [year, setYear] = useState(initYear); const [month, setMonth] = useState(initMonth); const currentYear = new Date().getFullYear(); const currentMonth = new Date().getMonth() + 1; + const { data: monthRevenue } = useQuery({ + queryKey: ['monthRevenue', year, month], + queryFn: async () => { + const response = await ApiClient.getInstance().getMonthRevenue( + year, + month + ); + return response.data; + }, + }); + const monthTotal = - data?.reduce((total, item) => total + item.revenue, 0) || 0; + monthRevenue?.reduce((total, item) => total + item.revenue, 0) || 0; const handlePreviousMonth = () => { if (month === 1) { @@ -89,9 +100,9 @@ export const TotalSalesCard = ({ initYear, initMonth, data }: Iprops) => { {/* chart */}
- +
- + ); }; diff --git a/src/components/organisms/LessonSalesList.tsx b/src/components/organisms/LessonSalesList.tsx index 1677179..78a8e34 100644 --- a/src/components/organisms/LessonSalesList.tsx +++ b/src/components/organisms/LessonSalesList.tsx @@ -2,7 +2,7 @@ import { LessonSalesTotal } from '../molecules/LessonSalesTotal'; interface IProps { year: number; - data: MonthSalesType[] | undefined; + data: MonthRevenueType[] | undefined; } export const LessonSalesList = ({ year, data }: IProps) => { @@ -11,7 +11,7 @@ export const LessonSalesList = ({ year, data }: IProps) => { {data?.map((item, index) => ( { setCookie( 'token', - 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLrrLjshJzsl7AiLCJ1c2VySWQiOjMsImlhdCI6MTcxOTk5MjE3NiwiZXhwIjoxNzE5OTk1Nzc2fQ.pN20QiVk3gAB-5N3a1ffWe7WeKc-ay3Yz7T1ecx7TFY' + 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLsnbTrr7zsp4AiLCJ1c2VySWQiOjEsImlhdCI6MTcyMDA1MjI0MywiZXhwIjoxNzIwMTM4NjQzfQ.BJSSTGaXMZQo3F_66Xtso9k8LRVeBgNUnZif94vgLnU' ); const [selectedAccount, setSelectedAccount] = useState({ diff --git a/src/pages/mypage/Sales.tsx b/src/pages/mypage/Sales.tsx index 274c2a2..bf0aff2 100644 --- a/src/pages/mypage/Sales.tsx +++ b/src/pages/mypage/Sales.tsx @@ -8,26 +8,22 @@ import { ApiClient } from '../../apis/apiClient'; // 매출 관리 페이지 export const Sales = () => { const navigate = useNavigate(); - const date = new Date(); - const year: number = date.getFullYear(); - const month: number = date.getMonth() + 1; - const { data: monthRevenue } = useQuery({ - queryKey: ['monthRevenue'], + const { data: totalRevenue } = useQuery({ + queryKey: ['totalRevenue'], queryFn: async () => { - const response = await ApiClient.getMonthSales(); - return response; + const response = await ApiClient.getInstance().getTotal(); + return response.data; }, }); - console.log(monthRevenue); return (
navigate('/mypage/host')} />

매출 관리

- - + +
); diff --git a/src/pages/mypage/SalesYear.tsx b/src/pages/mypage/SalesYear.tsx index 4f29321..aed9a8c 100644 --- a/src/pages/mypage/SalesYear.tsx +++ b/src/pages/mypage/SalesYear.tsx @@ -2,11 +2,89 @@ import { useNavigate, useParams } from 'react-router-dom'; import { Topbar } from '../../components/common/Topbar'; import { LineChart } from '../../components/molecules/LineChart'; import { Calculator } from '../../components/molecules/Calculator'; +import { useQuery } from '@tanstack/react-query'; +import { ApiClient } from '../../apis/apiClient'; +import { useMemo } from 'react'; export const SalesYear = () => { const { year, lesson_id } = useParams<{ year: string; lesson_id: string }>(); const navigate = useNavigate(); - console.log(year, lesson_id); + + // api 호출 + const { data: lessonRevenue } = useQuery({ + queryKey: ['monthRevenue'], + queryFn: async () => { + const response = await ApiClient.getInstance().getLessonRevenue( + Number(year), + Number(lesson_id) + ); + return response.data; + }, + }); + + // calculator 데이터 가공 + const calculatorData = useMemo(() => { + if (!lessonRevenue) return []; + + const monthlyStats = Array.from({ length: 12 }, (_, index) => ({ + month: index + 1, + totalRevenue: 0, + totalSales: 0, + materialPrice: 0, + rentalPrice: 0, + etcPrice: 0, + netProfit: 0, + })); + + lessonRevenue.forEach((lesson: any) => { + const monthIndex = lesson.month - 1; + monthlyStats[monthIndex].totalRevenue += lesson.revenue; + monthlyStats[monthIndex].materialPrice += lesson.materialPrice; + monthlyStats[monthIndex].rentalPrice += lesson.rentalPrice; + monthlyStats[monthIndex].etcPrice += lesson.etcPrice; + monthlyStats[monthIndex].totalSales += + lesson.materialPrice + lesson.rentalPrice + lesson.etcPrice; + }); + + monthlyStats.forEach((stat) => { + stat.netProfit = stat.totalRevenue - stat.totalSales; + }); + + return monthlyStats; + }, [lessonRevenue]); + + // chart 데이터 가공 + const chartData = useMemo(() => { + if (!lessonRevenue) return []; + + const monthlyRevenue = Array(12).fill(0); + const monthlyNetProfit = Array(12).fill(0); + + lessonRevenue.forEach((lesson: any) => { + const monthIndex = lesson.month - 1; + monthlyRevenue[monthIndex] += lesson.revenue; + }); + + calculatorData.forEach((data) => { + monthlyNetProfit[data.month - 1] = data.netProfit; + }); + + // chartData 형식으로 변환 + const data1 = monthlyRevenue.map((revenue, index) => ({ + x: index + 1, + y: revenue, + })); + + const data2 = monthlyNetProfit.map((netProfit, index) => ({ + x: index + 1, + y: netProfit, + })); + + return [ + { id: '매출액', data: data1 }, + { id: '순수익', data: data2 }, + ]; + }, [lessonRevenue, calculatorData]); return (
@@ -15,7 +93,7 @@ export const SalesYear = () => { onClick={() => navigate('/mypage/host/sales')} />
-

{} 매출액

+

{lessonRevenue?.[0]?.title || '클래스명'} 매출액

{year}년

@@ -23,12 +101,16 @@ export const SalesYear = () => {

단위 : 원

- +

순수익 계산기

- +
); diff --git a/src/types/revenue.d.ts b/src/types/revenue.d.ts new file mode 100644 index 0000000..d673191 --- /dev/null +++ b/src/types/revenue.d.ts @@ -0,0 +1,35 @@ +interface TotalType { + totalRevenue: number; +} + +interface MonthRevenueType { + lessonId: number; + title: string; + revenue: number; +} + +interface LessonRevenue { + month: number; + lessonId: number; + title: string; + revenue: number; + materialPrice: number; + rentalPrice: number; + etcPrice: number; +} + +interface PriceReqType { + lessonId: number; + year: number; + month: number; + materialPrice: number; + rentalPrice: number; + etcPrice: number; +} + +interface PriceType { + lessonId: number; + materialPrice: number; + rentalPrice: number; + etcPrice: number; +}