diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 1fa92c9..63ad6ba 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -45,8 +45,6 @@ jobs: run: aws s3 sync ./dist s3://$BUCKET_NAME --delete - name: CloudFront Invalidation - env: - CLOUD_FRONT_ID: ${{ secrets.AWS_CLOUDFRONT_ID}} run: | aws cloudfront create-invalidation \ - --distribution-id $CLOUD_FRONT_ID --paths /* + --distribution-id ${{ secrets.AWS_CLOUDFRONT_ID}} --paths /* diff --git a/package-lock.json b/package-lock.json index 01e94fa..3fb130a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dongbti", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dongbti", - "version": "1.0.1", + "version": "1.0.2", "dependencies": { "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", diff --git a/package.json b/package.json index 00e822c..b48c670 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dongbti", "private": true, - "version": "1.0.1", + "version": "1.0.2", "type": "module", "scripts": { "dev": "vite", diff --git a/serverless/dongbti/handler.js b/serverless/dongbti/handler.js index 5ec2783..27c4e76 100644 --- a/serverless/dongbti/handler.js +++ b/serverless/dongbti/handler.js @@ -38,14 +38,46 @@ const departmentTypes = [ "undeclared", ]; +const resultTypes = [ + "intenseSportsman", + "coatSportsman", + "compatitionSportsman", + "fontSportsman", + "natureSportsman", + "matSportsman", + "fightSportsman", + "uniqueSportsman", + "10cm", + "tchaikovsky", + "stageMusician", + "mozart", + "parkHyoShin", + "newJeans", + "macGyver", + "cutieArtist", + "leedongjin", + "multiArtist", + "heatDebater", + "siliconValley", + "creator", + "winner", + "scholar", + "god", + "budda", + "hyunwoojin", + "philanthropist", + "teresa", + "american", +]; + // 사용자의 MBTI와 학과를 저장하고 통계 업데이트 app.post("/stats", async (req, res) => { const { department, mbti } = req.body; if (!departmentTypes.includes(department)) return res.status(400).json({ status: 400, message: "department 의 값이 유효하지 않습니다" }); - if (typeof mbti !== "string") - return res.status(400).json({ status: 400, message: "mbti 는 string 값이어야 합니다" }); + if (!resultTypes.includes(mbti)) + return res.status(400).json({ status: 400, message: "mbti 의 값이 유효하지 않습니다" }); const userId = uuid(); @@ -57,13 +89,33 @@ app.post("/stats", async (req, res) => { }), ); - const stats = await db.send( + const totalStats = await db.send( new GetCommand({ TableName: STATS_TABLE, Key: { id: "total_count" }, }), ); - const total_count = stats.Item.total_count || 0; + const total_count = totalStats.Item.total_count || 0; + + const mbtiStats = await db.send( + new GetCommand({ + TableName: STATS_TABLE, + Key: { id: "mbti" }, + }), + ); + const mbtiCount = mbtiStats.Item[mbti]; + + await db.send( + new UpdateCommand({ + TableName: STATS_TABLE, + Key: { id: "mbti" }, + UpdateExpression: `set ${mbti} = :updated_count`, + ExpressionAttributeValues: { + ":updated_count": mbtiCount + 1, + }, + }), + ); + await db.send( new UpdateCommand({ TableName: STATS_TABLE, @@ -82,6 +134,31 @@ app.post("/stats", async (req, res) => { } }); +app.get("/stats", async (req, res) => { + const { type } = req.query; + + if (!resultTypes.includes(type)) + return res.status(400).json({ status: 400, message: "type 값은 string 이어야 합니다" }); + + const params = { + TableName: STATS_TABLE, + Key: { id: "mbti" }, + }; + try { + const data = await db.send(new GetCommand(params)); + const totalCount = await db.send( + new GetCommand({ + TableName: STATS_TABLE, + Key: { id: "total_count" }, + }), + ); + return res.status(200).json({ status: 200, count: data.Item[type], total_count: totalCount.Item.total_count }); + } catch (e) { + console.error(e); + return res.status(500).json({ status: 500, message: "서버 내부 오류 발생" }); + } +}); + // 총 사용자 수 조회 app.get("/stats/total", async (req, res) => { const params = { diff --git a/src/components/form/DropDown.tsx b/src/components/form/DropDown.tsx index f369354..8dc6e79 100644 --- a/src/components/form/DropDown.tsx +++ b/src/components/form/DropDown.tsx @@ -6,11 +6,17 @@ export interface DropDownProps { color: string; width: string; height: string; - selectedMajor?: string; - setSelectedMajor?: React.Dispatch>; + selectedDepartment?: string; + setSelectedDepartment?: React.Dispatch>; } -export default function DropDown({ color, width, height, selectedMajor, setSelectedMajor }: DropDownProps) { +export default function DropDown({ + color, + width, + height, + selectedDepartment: selectedMajor, + setSelectedDepartment: setSelectedMajor, +}: DropDownProps) { const majors = [ { label: "📖 인문대학", value: "humanities" }, { label: "📋 사회과학대학", value: "social-sciences" }, diff --git a/src/hooks/useTop10.tsx b/src/hooks/useTop10.tsx new file mode 100644 index 0000000..7481277 --- /dev/null +++ b/src/hooks/useTop10.tsx @@ -0,0 +1,30 @@ +import { useEffect, useState } from "react"; + +import { top10Response } from "@/pages/AnalyticsPage"; + +import { api } from "@/config/axios"; + +export const useTop10 = () => { + const [top10, setTop10] = useState(null); + const [isPending, setIsPending] = useState(false); + const [isError, setIsError] = useState(false); + + useEffect(() => { + setIsPending(true); + setIsError(false); + + api.get(`/stats/top/mbti`) + .then((data) => { + setTop10({ top: data.data.top }); + console.log(data.data.top); + }) + .catch(() => { + setIsError(true); + }) + .finally(() => { + setIsPending(false); + }); + }, []); + + return { isPending, isError, top10 }; +}; diff --git a/src/hooks/useTop10ByDepartment.tsx b/src/hooks/useTop10ByDepartment.tsx new file mode 100644 index 0000000..6ec590d --- /dev/null +++ b/src/hooks/useTop10ByDepartment.tsx @@ -0,0 +1,34 @@ +import { useState, useEffect } from "react"; + +import { top10Response } from "@/pages/AnalyticsPage"; + +import { api } from "@/config/axios"; + +interface top10ByDepartmentProps { + department: string; +} + +export const useTop10ByDepartment = (department: top10ByDepartmentProps) => { + const [top10ByDepartment, setTop10ByDepartment] = useState(null); + const [isPending, setIsPending] = useState(false); + const [isError, setIsError] = useState(false); + + useEffect(() => { + setIsPending(true); + setIsError(false); + + api.get(`/stats/top/department?key=${department.department}`) + .then((data) => { + setTop10ByDepartment({ top: data.data.top }); + console.log(data.data.top); + }) + .catch(() => { + setIsError(true); + }) + .finally(() => { + setIsPending(false); + }); + }, [department.department]); + + return { isPending, isError, top10ByDepartment }; +}; diff --git a/src/hooks/useTotalStats.tsx b/src/hooks/useTotalCount.tsx similarity index 71% rename from src/hooks/useTotalStats.tsx rename to src/hooks/useTotalCount.tsx index c1a7310..6a01c19 100644 --- a/src/hooks/useTotalStats.tsx +++ b/src/hooks/useTotalCount.tsx @@ -2,18 +2,21 @@ import { useEffect, useState } from "react"; import { api } from "@/config/axios"; -interface TotalStatsResponse { +interface TotalCountResponse { + status: number; total_count: number; } -export const useTotalStats = () => { +export const useTotalCount = () => { + const [totalCount, setTotalCount] = useState(null); const [isPending, setIsPending] = useState(false); const [isError, setIsError] = useState(false); - const [totalCount, setTotalCount] = useState(0); useEffect(() => { setIsPending(true); - api.get(`/stats/total`) + setIsError(false); + + api.get(`/stats/total`) .then((data) => { setTotalCount(data.data.total_count); }) diff --git a/src/main.tsx b/src/main.tsx index 443f40f..e0a4c24 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,5 @@ -import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; -ReactDOM.createRoot(document.getElementById("root")!).render( - - - , -); +ReactDOM.createRoot(document.getElementById("root")!).render(); diff --git a/src/pages/AnalyticsPage.style.ts b/src/pages/AnalyticsPage.style.ts index 5773bc5..de24ea4 100644 --- a/src/pages/AnalyticsPage.style.ts +++ b/src/pages/AnalyticsPage.style.ts @@ -11,9 +11,9 @@ export const TitleContainer = styled.div` export const TitleTop = styled.div` display: flex; - width: 60%; align-items: center; - justify-content: flex-start; + justify-content: center; + flex-direction: row; `; export const Main = styled.div``; diff --git a/src/pages/AnalyticsPage.tsx b/src/pages/AnalyticsPage.tsx index 5242fb7..9c41d81 100644 --- a/src/pages/AnalyticsPage.tsx +++ b/src/pages/AnalyticsPage.tsx @@ -1,37 +1,42 @@ -import React from "react"; +import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; +import Footer from "@/components/display/Footer"; import DropDown from "@/components/form/DropDown"; import TopBar from "@/components/layout/TopBar"; import { Text } from "@/components/typography"; -import useAxios from "@/hooks/useAxios"; +import { useTop10 } from "@/hooks/useTop10"; +import { useTop10ByDepartment } from "@/hooks/useTop10ByDepartment"; import backIcon from "@/assets/back.svg"; +import { results } from "@/constants/results"; + import { TitleContainer, TitleTop, Main, MiddleSection, TableContainer, Card, Rank, Type } from "./AnalyticsPage.style"; -interface axiosProps { - top: Array<[number, string]>; +export interface top10Response { + top: [string, number][]; } -const renderData = (data: axiosProps | null, loading: boolean, error: any) => { - if (loading) return Loading...; - if (error) return Error: {error.message}; - if (data?.top.length == 0) { +const renderData = (data: top10Response | null, isPending: boolean, isError: any) => { + if (isPending) return 로딩중...; + if (isError) return 오류가 발생했습니다.; + if (!data) { return ( 데이터가 존재하지 않습니다. ); } + if (data && data.top) { return ( {data.top.map((value, index) => ( {index + 1}위 - {value[0]} + {results.find((result) => result.resultType === value[0])?.typeName} ))} @@ -41,36 +46,25 @@ const renderData = (data: axiosProps | null, loading: boolean, error: any) => { }; export default function AnalyticsPage() { - const [selectedMajor, setSelectedMajor] = React.useState("humanities"); + const [selectedDepartment, setSelectedDepartment] = useState(""); const navigate = useNavigate(); const handleGoBack = () => { navigate("/"); }; + const { top10, isPending: top10Loading, isError: top10Error } = useTop10(); const { - data: allData, - loading: allDataLoading, - error: allDataError, - } = useAxios( - { - url: `/stats/top/mbti`, - method: "GET", - }, - [], - ); + top10ByDepartment, + isPending: top10ByDepartmentLoading, + isError: top10ByDepartmentError, + } = useTop10ByDepartment({ department: selectedDepartment }); - const { - data: majorData, - loading: majorDataLoading, - error: majorDataError, - } = useAxios( - { - url: `/stats/top/department?key=${selectedMajor}`, - method: "GET", - }, - [selectedMajor], - ); + useEffect(() => { + if (selectedDepartment) { + setSelectedDepartment(selectedDepartment); + } + }, [selectedDepartment]); return ( <> @@ -78,10 +72,12 @@ export default function AnalyticsPage() { - 어떤 유형이 가장 많을까요? + + 어떤 유형이 가장 많을까요? + -
{renderData(allData, allDataLoading, allDataError)}
+
{renderData(top10, top10Loading, top10Error)}
@@ -89,17 +85,24 @@ export default function AnalyticsPage() { - 에서 + + 에서 + - 어떤 유형이 가장 많을까요? + + 어떤 유형이 가장 많을까요? + -
{renderData(majorData, majorDataLoading, majorDataError)}
+
+ {selectedDepartment && renderData(top10ByDepartment, top10ByDepartmentLoading, top10ByDepartmentError)} +
+