From d7b77ad32c4ea306ddf78b850cb9d43ac7aebce7 Mon Sep 17 00:00:00 2001 From: Whale2200d Date: Wed, 22 Nov 2023 23:49:05 +0900 Subject: [PATCH 01/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20[IMPROVE]=20=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=20=EA=B8=B0=EB=8A=A5=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=EC=99=80=20websocket=20=ED=86=B5=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - send 함수까지 구현 완료 - useEffect로 subscribe 함수로 데이터 가져오는 부분은 미완성 --- package.json | 5 +- src/App.tsx | 149 +++++------------------ src/assets/util.ts | 17 +-- src/components/Chatting/index.tsx | 92 +++++++++++++- src/components/ChattingContent/index.tsx | 33 ++--- src/core/api/instance.ts | 1 + 6 files changed, 141 insertions(+), 156 deletions(-) diff --git a/package.json b/package.json index 189e808..d5f3a88 100644 --- a/package.json +++ b/package.json @@ -21,16 +21,19 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.17.0", - "recoil": "^0.7.7" + "recoil": "^0.7.7", + "stompjs": "^2.3.3" }, "devDependencies": { "@types/node": "^20.9.4", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", + "@types/stompjs": "^2.3.9", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react-swc": "^3.3.2", "eslint": "^8.45.0", + "eslint-plugin-import": "^2.29.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", "json-server": "^0.17.4", diff --git a/src/App.tsx b/src/App.tsx index 7dc815b..cce375f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,15 +15,9 @@ import Layout from "./components/Common/Layout"; import ChattingPage from "./pages/ChattingPage"; import ChallengeDetailPage from "./pages/ChallengeDetailPage"; import CreateChallengeGroupPage from "./pages/CreateChallengeGroupPage"; -// import { isHaveToken } from "./assets/util"; - -interface ProtectedRouteProps { - isLoggedIn: boolean; - children: React.ReactNode; -} function App() { - // const isLoggedIn = isHaveToken(); + const isLoggedIn = isHaveToken(); const isLoginPage = window.location.pathname === "/login"; const isSignUpPage = window.location.pathname === "/signup"; @@ -34,118 +28,35 @@ function App() { {/* COMPLETED: 처음 화면에 진입했을 때 */} - } /> - {/* COMPLETED: 로그인하지 않아서, Token이 없을 때 */} - - - } - /> - - } - /> - - - } + path="/" + element={isLoggedIn ? : } /> + {/* COMPLETED: 로그인하지 않아서, Token이 없을 때 */} + } /> + } /> + } /> {/* COMPLETED: 로그인하여, Token이 주어졌을 때 */} - - - - } - /> - - - - } - /> + } /> + } /> - - - } + element={} /> - - - } - /> - - - - } - /> - - - - } + element={} /> + } /> + } /> - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } + element={} /> + } /> + } /> + } /> + } /> {!isLoginPage && !isSignUpPage && @@ -155,15 +66,21 @@ function App() { ); } -const ProtectedRoute = ({ isLoggedIn, children }: ProtectedRouteProps) => { - if (!isLoggedIn) { - return ( - - } /> - - ); - } +/** COMPLETED: Cookie에 Token이 존재하는지 확인하기 */ +const isHaveToken = () => { + const COOKIE_VALUE = `${document.cookie}`; + console.log("COOKIE_VALUE: ", COOKIE_VALUE); - return children; + if (!COOKIE_VALUE) return false; + + const PARTS = COOKIE_VALUE.split(";"); + const ACCESS_TOKEN = PARTS[5].split("=")[1]; + + if (ACCESS_TOKEN) { + return true; + } else { + return false; + } }; + export default App; diff --git a/src/assets/util.ts b/src/assets/util.ts index 1de329f..7a4900b 100644 --- a/src/assets/util.ts +++ b/src/assets/util.ts @@ -49,7 +49,7 @@ export const CalculatePercentage = (numerator: number, denominator: number) => { }; /** COMPLETED: '오전/오후 00:00' 형태로 시간 구성하기 */ -export const ChangeTime = (time: string) => { +export const ChangeTime = (time: string | Date) => { /* 1. Date 객체로 변환 */ const date = new Date(time); @@ -146,23 +146,10 @@ export const ChangeMonth = (time: string) => { return formattedMonth; }; -/** COMPLETED: Cookie에 Token이 존재하는지 확인하기 */ -export const isHaveToken = () => { - const COOKIE_VALUE = `${document.cookie}`; - const PARTS = COOKIE_VALUE.split(";"); - const ACCESS_TOKEN = PARTS[5].split("=")[1]; - - if (ACCESS_TOKEN) { - return true; - } else { - return false; - } -}; - /** COMPLETED: Cookie에 Token 가져오기 */ export const getAccessToken = () => { const COOKIE_VALUE = `${document.cookie}`; - console.log("COOKIE_VALUE: ", COOKIE_VALUE); + console.log("COOKIE_VALUE2: ", COOKIE_VALUE); // const PARTS = COOKIE_VALUE.split(";"); // const ACCESS_TOKEN = PARTS[5].split("=")[1]; diff --git a/src/components/Chatting/index.tsx b/src/components/Chatting/index.tsx index 956e40b..7de4829 100644 --- a/src/components/Chatting/index.tsx +++ b/src/components/Chatting/index.tsx @@ -3,8 +3,24 @@ import BarGraphItemDetailsTopContainer from "../BarGraphItemDetailsTopContainer" import { ChattingUI } from "./style"; // import { ChangeTime } from "@/src/assets/util"; import ChattingContent from "../ChattingContent"; +import { API_WEBSOCKET_URL } from "@/src/core/api/instance"; +import { useEffect, useState } from "react"; +import Stomp from "stompjs"; +import { ChangeTime } from "@/src/assets/util"; + +export interface Message { + groupId: number; + senderId: number; + message: string; + messageType: "TALK" | "LEAVE" | "ENTER"; + date: string; +} function Chatting() { + const [messages, setMessages] = useState([]); + const [newMessage, setNewMessage] = useState(""); + const [stompClient, setStompClient] = useState(); + const array1 = { groupId: 2, groupName: "testGroup2", @@ -71,18 +87,86 @@ function Chatting() { access: "enter", }, ]; + + const socket = new WebSocket(`${API_WEBSOCKET_URL}/ws`); + const stomp = Stomp.over(socket); + + useEffect(() => { + /* COMPLETED: Websocket 연결하기 */ + stomp.connect({}, () => { + /** COMPLETED: 연결 성공 시 실행되는 함수 */ + setStompClient(stomp); + + console.log("Connected to WebSocket"); + /* COMPLETED: 채팅 메시지를 구독하기 */ + stomp.subscribe( + `/chat/groups/${array1.groupId}`, + (message: Stomp.Message) => { + const newMessages: Message[] = [ + ...messages, + JSON.parse(message.body), + ]; + console.log("Received message: ", newMessages); + setMessages(newMessages); + } + ); + }); + + /** COMPLETED: Websocket 연결 해제하기 */ + return () => { + if (stompClient) { + stompClient.disconnect(() => + console.log("Websocket 연결이 해제되었습니다.") + ); + } + }; + }, []); + + /* Click 이벤트 등 전송하기 관련 이벤트를 통한 채팅 메시지 전송하기 */ + const sendMessage = (message: string) => { + if (stompClient && message !== "") { + stompClient.send( + "/app/send", + {}, + JSON.stringify({ + groupId: 2, + senderId: 9, + message: message, + messageType: "TALK", + }) + ); + // /* Messages 배열 state에 추가 */ + const newMessage: Message = { + groupId: 2, + senderId: 9, + message: message, + messageType: "TALK", + date: ChangeTime(new Date()), + }; + const updatedMessages = [...messages, newMessage]; + setMessages(updatedMessages); + /* input창 초기화 */ + setNewMessage(""); + } + }; + return (
- {chattingArray.map(content => { - return ; + {messages.map((content, index) => { + return ; })} - - + setNewMessage(e.target.value)} + /> + sendMessage(newMessage)}> Send PNG diff --git a/src/components/ChattingContent/index.tsx b/src/components/ChattingContent/index.tsx index ebbc4cb..1551bc8 100644 --- a/src/components/ChattingContent/index.tsx +++ b/src/components/ChattingContent/index.tsx @@ -1,42 +1,35 @@ import { ChangeTime } from "@/src/assets/util"; import { ChattingContentUI } from "./style"; +import { Message } from "../Chatting"; interface ChattingContentData { - memberName: string; - sendTime: string; - messageContent: string; - access: string; + // memberName: string; + // sendTime: string; + // messageContent: string; + // access: string; } interface ChattingContentProps { - content: ChattingContentData; + // content: ChattingContentData; + content: Message; } function ChattingContent({ content }: ChattingContentProps) { return ( <> - {content.access === "enter" ? ( + {content.messageType === "ENTER" ? ( - {`${content.memberName}님이 입장하셨습니다.`} + {`${content.senderId}님이 입장하셨습니다.`} - ) : content.access === "exit" ? ( + ) : content.messageType === "LEAVE" ? ( - {`${content.memberName}님이 퇴장하셨습니다.`} + {`${content.senderId}님이 퇴장하셨습니다.`} - ) : content.access === "others" ? ( - -
-
{content.memberName}
-
{`${ChangeTime(content.sendTime)}`}
-
-
{content.messageContent}
-
) : ( -
{content.messageContent}
+
{content.message}
-
{content.memberName}
-
{`${ChangeTime(content.sendTime)}`}
+
{content.senderId}
)} diff --git a/src/core/api/instance.ts b/src/core/api/instance.ts index c54af7b..85719d0 100644 --- a/src/core/api/instance.ts +++ b/src/core/api/instance.ts @@ -2,6 +2,7 @@ import axios, { AxiosRequestConfig } from "axios"; /** TODO: GET 요청 */ const API_BASE_URL = "https://asts.cozybinarybase.com:8443"; +export const API_WEBSOCKET_URL = "ws://asts.cozybinarybase.com:8443"; /** COMPLETED: 1. instance 만들기 */ const createAPIInstance = (config: AxiosRequestConfig) => { From a67ed776c180579649d3668d719a7a0696ea3a1d Mon Sep 17 00:00:00 2001 From: Whale2200d Date: Thu, 23 Nov 2023 12:19:52 +0900 Subject: [PATCH 02/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20[IMPROVE]=20=20debug?= =?UTF-8?q?ging=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Chatting/index.tsx | 63 +++++++++++++++---------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/src/components/Chatting/index.tsx b/src/components/Chatting/index.tsx index 7de4829..c35436c 100644 --- a/src/components/Chatting/index.tsx +++ b/src/components/Chatting/index.tsx @@ -88,10 +88,12 @@ function Chatting() { }, ]; - const socket = new WebSocket(`${API_WEBSOCKET_URL}/ws`); - const stomp = Stomp.over(socket); + const connect = async () => { + const socket = new WebSocket(`${API_WEBSOCKET_URL}/ws`); + console.log("socket.onmessage: ", socket.onmessage); - useEffect(() => { + const stomp = Stomp.over(socket); + // stomp.debug = msg => console.log("debug message: ", msg); /* COMPLETED: Websocket 연결하기 */ stomp.connect({}, () => { /** COMPLETED: 연결 성공 시 실행되는 함수 */ @@ -99,29 +101,36 @@ function Chatting() { console.log("Connected to WebSocket"); /* COMPLETED: 채팅 메시지를 구독하기 */ - stomp.subscribe( - `/chat/groups/${array1.groupId}`, - (message: Stomp.Message) => { - const newMessages: Message[] = [ - ...messages, - JSON.parse(message.body), - ]; - console.log("Received message: ", newMessages); - setMessages(newMessages); - } + const receiveMessage = stomp.subscribe( + `/chat/groups/3`, + onReceiveMessage, + { id: 1 } ); + + console.log("receiveMessage.id: ", receiveMessage.id); + console.log("receiveMessage.unsubscribe: ", receiveMessage.unsubscribe); }); + }; - /** COMPLETED: Websocket 연결 해제하기 */ - return () => { - if (stompClient) { - stompClient.disconnect(() => - console.log("Websocket 연결이 해제되었습니다.") - ); - } - }; + const onReceiveMessage = (message: Stomp.Message) => { + const newMessages: Message[] = [...messages, JSON.parse(message.body)]; + console.log("Received message: ", newMessages); + setMessages(newMessages); + }; + + useEffect(() => { + connect(); }, []); + /** COMPLETED: Websocket 연결 해제하기 */ + // return () => { + // if (stompClient) { + // stompClient.disconnect(() => + // console.log("Websocket 연결이 해제되었습니다.") + // ); + // } + // }; + /* Click 이벤트 등 전송하기 관련 이벤트를 통한 채팅 메시지 전송하기 */ const sendMessage = (message: string) => { if (stompClient && message !== "") { @@ -129,22 +138,12 @@ function Chatting() { "/app/send", {}, JSON.stringify({ - groupId: 2, + groupId: 3, senderId: 9, message: message, messageType: "TALK", }) ); - // /* Messages 배열 state에 추가 */ - const newMessage: Message = { - groupId: 2, - senderId: 9, - message: message, - messageType: "TALK", - date: ChangeTime(new Date()), - }; - const updatedMessages = [...messages, newMessage]; - setMessages(updatedMessages); /* input창 초기화 */ setNewMessage(""); } From b05f8bb96427051f6372eedf53409cc47a2f5f17 Mon Sep 17 00:00:00 2001 From: Sojung Park Date: Thu, 23 Nov 2023 12:29:00 +0900 Subject: [PATCH 03/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20[IMPROVE]=20?= =?UTF-8?q?=EC=88=98=EC=9E=85=20=EB=B0=8F=20=EC=A7=80=EC=B6=9C=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=20=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=20=EC=9C=84=EC=B9=98=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Common/LabelInput/index.tsx | 133 +++++++++- src/components/Common/LabelInput/style.ts | 39 ++- .../Common/SeparatedCategory/index.tsx | 56 +++-- .../Common/SeparatedCategory/style.ts | 6 +- src/components/Common/ShortButton/index.tsx | 18 +- src/components/Common/ShortButton/style.ts | 10 +- .../IncomeExpenseButton/idnex.tsx | 15 +- .../IncomeExpenseButton/style.ts | 2 +- src/components/InputArea/index.tsx | 233 ++++++++++++++++++ .../InputArea/style.ts | 30 +-- .../RecordAccountBook/InputArea/index.tsx | 212 ---------------- .../RecordAccountBook/SelectedImage/index.tsx | 78 ------ src/components/Recurring/index.tsx | 0 .../RecurringIcon/index.tsx | 0 .../RecurringInstallmentButtons/index.tsx | 10 +- .../RecurringInstallmentButtons/style.ts | 2 +- src/components/SelectedImage/index.tsx | 95 +++++++ .../SelectedImage/style.ts | 4 +- src/core/api/image.ts | 5 +- src/hooks/recoil/btnLabelState.ts | 6 + src/hooks/recoil/useSaveAccountBook.ts | 6 +- src/hooks/recoil/useUploadImageFile.ts | 16 ++ src/pages/InstallmentPage/index.tsx | 47 ++-- src/pages/InstallmentPage/style.ts | 26 +- src/pages/RecordAccountBookPage/index.tsx | 10 +- src/pages/RecurringPage/index.tsx | 21 +- .../RecurringPage}/style.ts | 2 +- 27 files changed, 670 insertions(+), 412 deletions(-) rename src/components/{RecordAccountBook => }/IncomeExpenseButton/idnex.tsx (79%) rename src/components/{RecordAccountBook => }/IncomeExpenseButton/style.ts (89%) create mode 100644 src/components/InputArea/index.tsx rename src/components/{RecordAccountBook => }/InputArea/style.ts (60%) delete mode 100644 src/components/RecordAccountBook/InputArea/index.tsx delete mode 100644 src/components/RecordAccountBook/SelectedImage/index.tsx delete mode 100644 src/components/Recurring/index.tsx rename src/components/{RecordAccountBook => }/RecurringIcon/index.tsx (100%) rename src/components/{RecordAccountBook => }/RecurringInstallmentButtons/index.tsx (79%) rename src/components/{RecordAccountBook => }/RecurringInstallmentButtons/style.ts (94%) create mode 100644 src/components/SelectedImage/index.tsx rename src/components/{RecordAccountBook => }/SelectedImage/style.ts (95%) create mode 100644 src/hooks/recoil/btnLabelState.ts create mode 100644 src/hooks/recoil/useUploadImageFile.ts rename src/{components/Recurring => pages/RecurringPage}/style.ts (82%) diff --git a/src/components/Common/LabelInput/index.tsx b/src/components/Common/LabelInput/index.tsx index 577875e..e3b4506 100644 --- a/src/components/Common/LabelInput/index.tsx +++ b/src/components/Common/LabelInput/index.tsx @@ -1,38 +1,149 @@ -import { ChangeEvent } from "react"; +import { ChangeEvent, useEffect, useRef, useState } from "react"; import { LabelInputUI } from "./style"; +import RecurringICon from "../../RecurringIcon"; +import { useRecoilState } from "recoil"; +import { btnLabelStateAtom } from "@/src/hooks/recoil/btnLabelState"; +import { useNavigate } from "react-router-dom"; +import { theme } from "@/src/assets/theme"; +import RecurringInstallmentButtons from "../../RecurringInstallmentButtons"; +import ImageUploadSVG from "@/public/icon/ImageUpload.svg"; +import { uploadImageFileAtom } from "@/src/hooks/recoil/useUploadImageFile"; export interface LabelInputProps { type: string; label: string; inputId: string; - inputName: string; + value: string | number; placeholder: string; - onClick?: (e: ChangeEvent) => void; - readonly?: boolean | undefined; + addContent?: "button" | "won" | "imageUpload"; + onClick?: () => void; + onChange?: undefined | ((e: ChangeEvent) => void); + readonly?: boolean; } +export const checkGreenColor = "#3cb043"; + function LabelInput({ type = "text", label, inputId, - inputName, + value, placeholder, + addContent, onClick, - readonly, + onChange, + readonly = false, }: LabelInputProps) { + const navigate = useNavigate(); + const [btnLabel, setBtnLabel] = useRecoilState(btnLabelStateAtom); + const fileInputRef = useRef(null); + const recurringInstallmentBtnRef = useRef(null); + const [showRecurringInstallmentBtns, setShowRecurringInstallmentBtns] = + useState(false); + const [, setSelectedImageFile] = useRecoilState(uploadImageFileAtom); + + // handleRecurringInstallmentBtn 외부 클릭시 버튼 사라짐 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + recurringInstallmentBtnRef.current && + !recurringInstallmentBtnRef.current.contains(event.target as Node) + ) { + setShowRecurringInstallmentBtns(false); + } + }; + + document.addEventListener("click", handleClickOutside); + + return () => { + document.removeEventListener("click", handleClickOutside); + }; + }, [recurringInstallmentBtnRef]); + + const handleRecurringButton = () => { + setBtnLabel("반복"); + navigate("/recordAccountBook/recurring"); + }; + + const handleInstallmentButton = () => { + setBtnLabel("할부"); + navigate("/recordAccountBook/installment"); + }; + + // 이미지 표시 + const handleImageChange = (e: ChangeEvent) => { + const FILE = e.target.files?.[0]; + if (FILE) { + const imageUrl = URL.createObjectURL(FILE); + setSelectedImageFile({ + isUploadImage: true, + selectedImage: imageUrl, + selectedImageFile: FILE, + }); + } + }; + return ( - - {label} + + {label} - + {addContent === "button" ? ( + + setShowRecurringInstallmentBtns(!showRecurringInstallmentBtns) + } + ref={recurringInstallmentBtnRef} + style={{ + color: + btnLabel === "반복/할부" + ? theme.font_color.gray3 + : theme.font_color.black, + }}> + + {btnLabel} + + ) : addContent === "won" ? ( + + ) : addContent === "imageUpload" ? ( + <> + + plus svg + + + + ) : ( + <> + )} + {showRecurringInstallmentBtns && ( + + )} + ); } diff --git a/src/components/Common/LabelInput/style.ts b/src/components/Common/LabelInput/style.ts index 2f7abbb..680e9ff 100644 --- a/src/components/Common/LabelInput/style.ts +++ b/src/components/Common/LabelInput/style.ts @@ -3,10 +3,12 @@ import { theme } from "../../../assets/theme"; const inputWidth = "100% - 73px"; -const Container = styled.div` +const InputContainer = styled.div` display: flex; align-items: center; height: 45px; + + position: relative; `; const Label = styled.label` @@ -28,8 +30,41 @@ const Input = styled.input` } `; +const RecurringInstallmentButton = styled.button` + display: flex; + align-items: center; + + ${theme.font_style.regular_small}; + color: ${theme.font_color.gray3}; + position: absolute; + top: 25%; + right: 0; + z-index: 1; +`; + +const WonUnit = styled.span` + ${theme.font_style.regular_medium} + position: absolute; + top: 25%; + right: 0; +`; + +const AddImageButtonLabel = styled.label` + position: absolute; + top: 25%; + right: 0; + + img { + width: 20px; + height: 20px; + } +`; + export const LabelInputUI = { - Container, + InputContainer, Label, Input, + RecurringInstallmentButton, + WonUnit, + AddImageButtonLabel, } as const; diff --git a/src/components/Common/SeparatedCategory/index.tsx b/src/components/Common/SeparatedCategory/index.tsx index 7e8617a..4c96cbb 100644 --- a/src/components/Common/SeparatedCategory/index.tsx +++ b/src/components/Common/SeparatedCategory/index.tsx @@ -3,6 +3,11 @@ import WritingPNG from "../../../../public/icon/Writing.png"; import { Button } from "@mui/material"; import { theme } from "../../../assets/theme"; import { SeperatedCategoryUI } from "./style"; +import { useNavigate } from "react-router-dom"; + +interface SeparatedCategoryProps { + title?: string; +} const buttonStyles = { height: "75px", @@ -10,30 +15,39 @@ const buttonStyles = { borderRadius: "0", }; -function SeparatedCategory() { +function SeparatedCategory({ title }: SeparatedCategoryProps) { + const navigate = useNavigate(); + + const handleWritingButton = () => { + navigate("/setting"); + }; + const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; return ( - -
카테고리 분류
-
- - -
-
- - {array.map(item => ( - - ))} - +
+ +
{title}
+
+ + +
+
+ + {array.map(item => ( + + ))} + +
); } diff --git a/src/components/Common/SeparatedCategory/style.ts b/src/components/Common/SeparatedCategory/style.ts index d37dd0e..8869827 100644 --- a/src/components/Common/SeparatedCategory/style.ts +++ b/src/components/Common/SeparatedCategory/style.ts @@ -3,10 +3,12 @@ import { theme } from "@/src/assets/theme"; const Container = styled.div` width: 100%; - background-color: ${theme.font_color.gray2}; + height: 450px; + background-color: ${theme.font_color.gray1}; position: absolute; left: 0; - z-index: 1; + bottom: 0; + z-index: 2; `; const CategoryHeader = styled.div` diff --git a/src/components/Common/ShortButton/index.tsx b/src/components/Common/ShortButton/index.tsx index 9f0cbf8..bdaf02b 100644 --- a/src/components/Common/ShortButton/index.tsx +++ b/src/components/Common/ShortButton/index.tsx @@ -27,14 +27,16 @@ function ShortButton({ return ( - - 취소 - - - {isSaveButton ? "저장" : "탈퇴"} - + + + 취소 + + + {isSaveButton ? "저장" : "탈퇴"} + + ); } diff --git a/src/components/Common/ShortButton/style.ts b/src/components/Common/ShortButton/style.ts index 3fff51a..de6deb5 100644 --- a/src/components/Common/ShortButton/style.ts +++ b/src/components/Common/ShortButton/style.ts @@ -3,10 +3,17 @@ import { theme } from "../../../assets/theme"; import { css } from "@emotion/react"; const Container = styled.div` + width: 100%; + position: absolute; + bottom: 0; + padding: 0 20px; + margin-bottom: 20px; +`; + +const ButtonWrapper = styled.div` display: flex; justify-content: space-between; align-items: center; - padding: 0 20px 20px; `; const buttonStyle = css` @@ -29,6 +36,7 @@ const GreenButton = styled.button` export const ShortButtonUI = { Container, + ButtonWrapper, CancelButton, GreenButton, } as const; diff --git a/src/components/RecordAccountBook/IncomeExpenseButton/idnex.tsx b/src/components/IncomeExpenseButton/idnex.tsx similarity index 79% rename from src/components/RecordAccountBook/IncomeExpenseButton/idnex.tsx rename to src/components/IncomeExpenseButton/idnex.tsx index 4cc8155..63746e8 100644 --- a/src/components/RecordAccountBook/IncomeExpenseButton/idnex.tsx +++ b/src/components/IncomeExpenseButton/idnex.tsx @@ -1,20 +1,33 @@ import { IncomeExpenseButtonUI } from "./style"; -import { theme } from "../../../assets/theme"; +import { theme } from "../../assets/theme"; import { useState } from "react"; +import { useRecoilState } from "recoil"; +import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; function IncomeExpenseButton() { const [isIncomeClicked, setIsIncomeClicked] = useState(true); const [isExpenseClicked, setIsExpenseClicked] = useState(false); + const [, setPostSaveAccountBook] = useRecoilState(saveAccountBookAtom); const handleIncomeClick = () => { setIsIncomeClicked(true); setIsExpenseClicked(false); + setPostSaveAccountBook(prev => ({ + ...prev, + transactionType: "수입", + })); + console.log("수입 버튼"); }; const handleExpenseClick = () => { setIsExpenseClicked(true); setIsIncomeClicked(false); + setPostSaveAccountBook(prev => ({ + ...prev, + transactionType: "지출", + })); + console.log("지출 버튼"); }; diff --git a/src/components/RecordAccountBook/IncomeExpenseButton/style.ts b/src/components/IncomeExpenseButton/style.ts similarity index 89% rename from src/components/RecordAccountBook/IncomeExpenseButton/style.ts rename to src/components/IncomeExpenseButton/style.ts index 8dc9c91..c8d44f0 100644 --- a/src/components/RecordAccountBook/IncomeExpenseButton/style.ts +++ b/src/components/IncomeExpenseButton/style.ts @@ -1,5 +1,5 @@ import styled from "@emotion/styled"; -import { theme } from "../../../assets/theme"; +import { theme } from "../../assets/theme"; const Container = styled.div` width: 100%; diff --git a/src/components/InputArea/index.tsx b/src/components/InputArea/index.tsx new file mode 100644 index 0000000..5bea303 --- /dev/null +++ b/src/components/InputArea/index.tsx @@ -0,0 +1,233 @@ +import { ChangeEvent, useState } from "react"; +import LabelInput from "../Common/LabelInput"; +import { InputAreaUI } from "./style"; + +import SelectedImage from "../SelectedImage"; +import SeparatedCategory from "../Common/SeparatedCategory"; +import { useRecoilState } from "recoil"; +import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; + +// interface InputAreaData { +// type: string; +// label: string; +// inputId: string; +// value: string | number; +// placeholder: string; +// addContent: string; +// onClick: undefined | (() => void); +// handleChange: undefined | (() => void); +// readonly: boolean; +// } + +function InputArea() { + // const InputArray = [ + // { + // type: "text", + // label: "날짜", + // inputId: "transactedAt", + // value: "", + // placeholder: "날짜를 입력해주세요.", + // addContent: "button", + // onClick: undefined, + // handleChange: handleDate, + // readonly: false, + // }, + // { + // type: "number", + // label: "금액", + // inputId: "amount", + // value: 0, + // placeholder: "금액을 입력해주세요.", + // addContent: "won", + // onClick: undefined, + // handleChange: handleAmount, + // readonly: false, + // }, + // { + // type: "text", + // label: "분류", + // inputId: "categoryName", + // value: "", + // placeholder: "카테고리를 선택해주세요.", + // addContent: "nothing", + // onClick: () => setShowSeparatedCategory(!showSeparatedCategory), + // handleChange: handleCategoryName, + // readonly: true, + // }, + // { + // type: "text", + // label: "자산", + // inputId: "asset", + // value: "", + // placeholder: "결제 수단을 선택해주세요.", + // addContent: "nothing", + // onClick: () => setShowSeparatedCategory(!showSeparatedCategory), + // handleChange: handleAsset, + // readonly: true, + // }, + // { + // type: "text", + // label: "사용처", + // inputId: "transactionDetail", + // value: "", + // placeholder: "사용처를 입력해주세요.", + // addContent: "nothing", + // onClick: undefined, + // handleChange: handleTransactionDetail, + // readonly: false, + // }, + // { + // type: "text", + // label: "주소", + // inputId: "address", + // value: "", + // placeholder: "주소를 입력해주세요.", + // addContent: "nothing", + // onClick: undefined, + // handleChange: handleAddress, + // readonly: false, + // }, + // ]; + const [postSaveAccountBook, setPostSaveAccountBook] = + useRecoilState(saveAccountBookAtom); + const [showSeparatedCategory, setShowSeparatedCategory] = + useState(false); + + /** COMPLETED: recoil saveAccountBookAtom에 값 추가하기 */ + const handleDate = (e: ChangeEvent) => { + const { value } = e.target; + + setPostSaveAccountBook(prev => ({ + ...prev, + transactedAt: value, + })); + }; + const handleAmount = (e: ChangeEvent) => { + const { value } = e.target; + + setPostSaveAccountBook(prev => ({ + ...prev, + amount: Number(value), + })); + }; + const handleCategoryName = (e: ChangeEvent) => { + const { value } = e.target; + + setPostSaveAccountBook(prev => ({ + ...prev, + categoryName: value, + })); + }; + const handleAssetName = (e: ChangeEvent) => { + const { value } = e.target; + + setPostSaveAccountBook(prev => ({ + ...prev, + assetName: value, + })); + }; + const handleTransactionDetail = (e: ChangeEvent) => { + const { value } = e.target; + + setPostSaveAccountBook(prev => ({ + ...prev, + transactionDetail: value, + })); + }; + const handleAddress = (e: ChangeEvent) => { + const { value } = e.target; + + setPostSaveAccountBook(prev => ({ + ...prev, + address: value, + })); + }; + const handleMemo = (e: ChangeEvent) => { + const { value } = e.target; + + setPostSaveAccountBook(prev => ({ + ...prev, + memo: value, + })); + }; + + return ( + + +
+ + + setShowSeparatedCategory(!showSeparatedCategory)} + onChange={handleCategoryName} + readonly={true} + /> + setShowSeparatedCategory(!showSeparatedCategory)} + onChange={handleAssetName} + readonly={true} + /> + + +
+ {showSeparatedCategory && } +
+ + + + {/** 이미지 미리보기 */} + + +
+ ); +} + +export default InputArea; diff --git a/src/components/RecordAccountBook/InputArea/style.ts b/src/components/InputArea/style.ts similarity index 60% rename from src/components/RecordAccountBook/InputArea/style.ts rename to src/components/InputArea/style.ts index a1e8d54..2530105 100644 --- a/src/components/RecordAccountBook/InputArea/style.ts +++ b/src/components/InputArea/style.ts @@ -1,5 +1,5 @@ import styled from "@emotion/styled"; -import { theme } from "../../../assets/theme"; +import { theme } from "../../assets/theme"; const barWidth = "100% - 40px"; @@ -18,33 +18,10 @@ const Wrapper = styled.div` width: calc(${barWidth}); height: 1px; background-color: ${theme.font_color.gray2}; - top: 58%; + top: 53%; } `; -const LabelInputWrapper = styled.div` - position: relative; -`; - -const RecurringInstallmentBtn = styled.button` - display: flex; - align-items: center; - - ${theme.font_style.regular_small}; - color: ${theme.font_color.gray3}; - position: absolute; - top: 25%; - right: 0; - z-index: 1; -`; - -const WonUnit = styled.span` - ${theme.font_style.regular_medium} - position: absolute; - top: 25%; - right: 0; -`; - const MemoArea = styled.div` margin-bottom: 25px; `; @@ -67,9 +44,6 @@ const AddImageButtonLabel = styled.label` export const InputAreaUI = { Container, Wrapper, - LabelInputWrapper, - RecurringInstallmentBtn, - WonUnit, MemoArea, MemoInputWrapper, AddImageButtonLabel, diff --git a/src/components/RecordAccountBook/InputArea/index.tsx b/src/components/RecordAccountBook/InputArea/index.tsx deleted file mode 100644 index b02a776..0000000 --- a/src/components/RecordAccountBook/InputArea/index.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { ChangeEvent, useEffect, useRef, useState } from "react"; -import LabelInput from "../../Common/LabelInput"; -import { InputAreaUI } from "./style"; -import { theme } from "@/src/assets/theme"; -import RecurringInstallmentButtons from "../RecurringInstallmentButtons"; -import ImageUploadSVG from "@/public/icon/ImageUpload.svg"; -import { useNavigate } from "react-router-dom"; -import RecurringICon from "../RecurringIcon"; -import SelectedImage from "../SelectedImage"; -import SeparatedCategory from "../../Common/SeparatedCategory"; -import { useRecoilState } from "recoil"; -import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; - -export const checkGreenColor = "#3cb043"; - -function InputArea() { - const labelInputItems = [ - { - label: "날짜", - inputId: "date", - inputName: "transactedAt", - placeholder: "날짜를 입력해주세요.", - addContent: "button", - }, - { - label: "금액", - inputId: "amount", - inputName: "amount", - placeholder: "금액을 입력해주세요.", - addContent: "won", - }, - { - label: "분류", - inputId: "categoryName", - inputName: "categoryName", - placeholder: "카테고리를 선택해주세요.", - onClick: () => { - setShowSeparatedCategory(true); - }, - readonly: true, - }, - { - label: "자산", - inputId: "categoryName", - inputName: "categoryName", - placeholder: "결제수단", - }, - { - label: "사용처", - inputId: "transactionDetail", - inputName: "transactionDetail", - placeholder: "결제수단", - }, - { - label: "주소", - inputId: "categoryName", - inputName: "categoryName", - placeholder: "주소를 입력해주세요.", - }, - { - label: "주소", - inputId: "categoryName", - inputName: "categoryName", - placeholder: "주소를 입력해주세요.", - }, - ]; - - const [showRecurringInstallmentBtns, setShowRecurringInstallmentBtns] = - useState(false); - const [btnLabel, setBtnLabel] = useState("반복/할부"); - const [showSeparatedCategory, setShowSeparatedCategory] = - useState(false); - const [selectedImage, setSelectedImage] = useState(null); - const [selectedImageFile, setSelectedImageFile] = useState(null); - - const naviagte = useNavigate(); - - const recurringInstallmentBtnRef = useRef(null); - const fileInputRef = useRef(null); - - // handleRecurringInstallmentBtn 외부 클릭시 버튼 사라짐 - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - recurringInstallmentBtnRef.current && - !recurringInstallmentBtnRef.current.contains(event.target as Node) - ) { - setShowRecurringInstallmentBtns(false); - } - }; - - document.addEventListener("click", handleClickOutside); - - return () => { - document.removeEventListener("click", handleClickOutside); - }; - }, [recurringInstallmentBtnRef]); - - const handleRecurringButton = () => { - setBtnLabel("반복"); - naviagte("/recordAccountBook/recurring"); - }; - - const handleInstallmentButton = () => { - setBtnLabel("할부"); - naviagte("/recordAccountBook/installment"); - }; - - // 이미지 표시 - const handleImageChange = (event: React.ChangeEvent) => { - const selectedImageFile = event.target.files?.[0]; - if (selectedImageFile) { - const imageUrl = URL.createObjectURL(selectedImageFile); - setSelectedImage(imageUrl); - setSelectedImageFile(selectedImageFile); - } - }; - - /** TODO: recoil에 값 추가하기 */ - const handleMemo = (e: ChangeEvent) => { - const { value } = e.target; - setPostAccountBook(prev => ({ - ...prev, - memo: value, - })); - }; - const [, setPostAccountBook] = useRecoilState(saveAccountBookAtom); - return ( - - - {labelInputItems.map((item, index) => ( - - - {item.addContent === "button" && ( - - setShowRecurringInstallmentBtns(!showRecurringInstallmentBtns) - } - ref={recurringInstallmentBtnRef} - style={{ - color: - btnLabel === "반복/할부" - ? theme.font_color.gray3 - : theme.font_color.black, - }}> - - {btnLabel} - - )} - {item.addContent === "button" && showRecurringInstallmentBtns && ( - - )} - {item.addContent === "won" && ( - - )} - - ))} - - {showSeparatedCategory && } - - - - {/** 이미지 업로드 */} - - plus svg - - - - {/** 이미지 미리보기 */} - - - - ); -} - -export default InputArea; diff --git a/src/components/RecordAccountBook/SelectedImage/index.tsx b/src/components/RecordAccountBook/SelectedImage/index.tsx deleted file mode 100644 index a4eb62f..0000000 --- a/src/components/RecordAccountBook/SelectedImage/index.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { theme } from "@/src/assets/theme"; -import { useState } from "react"; -import { checkGreenColor } from "../InputArea"; -import RecurringICon from "../RecurringIcon"; -import { SelecteImageUI } from "./style"; -import CloseSVG from "@/public/icon/Close.svg"; -import { imageAPI } from "@/src/core/api/image"; - -interface SelectedImageProps { - selectedImages: string | null; - setSelectedImages: (imageUrl: string | null) => void; - selectedImageFile: File | null; -} - -function SelectedImage({ - selectedImages, - setSelectedImages, - selectedImageFile, -}: SelectedImageProps) { - const [isReceipt, setIsReceipt] = useState(false); - - const handleIsReceiptButton = () => { - setIsReceipt(!isReceipt); - handlUploadImage(); - }; - - const handleCloseButton = () => { - setSelectedImages(null); - }; - - // 이미지 서버로 전송 - /** TODO: 전송시 302 에러 처리하기 */ - const handlUploadImage = () => { - if (selectedImageFile) { - const jsonData = JSON.stringify({ isReceipt }); - const imageFile = selectedImageFile; - - imageAPI - .imageUpload(jsonData, imageFile) - .then(response => { - console.log("이미지 전송 성공: ", response); - }) - .catch(error => { - console.error("이미지 전송 실패:", error); - }); - } - }; - - return ( - - {selectedImages ? null : 이미지 미리보기} - {selectedImages && ( -
- -
- - 영수증 -
-
- - - 닫기 버튼 - -
- )} -
- ); -} - -export default SelectedImage; diff --git a/src/components/Recurring/index.tsx b/src/components/Recurring/index.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/RecordAccountBook/RecurringIcon/index.tsx b/src/components/RecurringIcon/index.tsx similarity index 100% rename from src/components/RecordAccountBook/RecurringIcon/index.tsx rename to src/components/RecurringIcon/index.tsx diff --git a/src/components/RecordAccountBook/RecurringInstallmentButtons/index.tsx b/src/components/RecurringInstallmentButtons/index.tsx similarity index 79% rename from src/components/RecordAccountBook/RecurringInstallmentButtons/index.tsx rename to src/components/RecurringInstallmentButtons/index.tsx index 2b6a766..ab172c5 100644 --- a/src/components/RecordAccountBook/RecurringInstallmentButtons/index.tsx +++ b/src/components/RecurringInstallmentButtons/index.tsx @@ -1,6 +1,6 @@ import { RecurringInstallmentButtonsUI } from "./style"; -interface RecurringInstallmentButtonsPRops { +interface RecurringInstallmentButtonsProps { onRecurringClick: React.MouseEventHandler; onInstallmentClick: React.MouseEventHandler; } @@ -8,19 +8,17 @@ interface RecurringInstallmentButtonsPRops { function RecurringInstallmentButtons({ onRecurringClick, onInstallmentClick, -}: RecurringInstallmentButtonsPRops) { +}: RecurringInstallmentButtonsProps) { return ( + onClick={onRecurringClick}> 반복 + onClick={onInstallmentClick}> 할부 diff --git a/src/components/RecordAccountBook/RecurringInstallmentButtons/style.ts b/src/components/RecurringInstallmentButtons/style.ts similarity index 94% rename from src/components/RecordAccountBook/RecurringInstallmentButtons/style.ts rename to src/components/RecurringInstallmentButtons/style.ts index 2f443b7..a215e97 100644 --- a/src/components/RecordAccountBook/RecurringInstallmentButtons/style.ts +++ b/src/components/RecurringInstallmentButtons/style.ts @@ -1,5 +1,5 @@ import styled from "@emotion/styled"; -import { theme } from "../../../assets/theme"; +import { theme } from "../../assets/theme"; import { css } from "@emotion/react"; const Wrapper = styled.div` diff --git a/src/components/SelectedImage/index.tsx b/src/components/SelectedImage/index.tsx new file mode 100644 index 0000000..b7810f9 --- /dev/null +++ b/src/components/SelectedImage/index.tsx @@ -0,0 +1,95 @@ +import { theme } from "@/src/assets/theme"; +import { useEffect, useState } from "react"; +// import { checkGreenColor } from "../InputArea"; +import RecurringICon from "../RecurringIcon"; +import { SelecteImageUI } from "./style"; +import CloseSVG from "@/public/icon/Close.svg"; +import { imageAPI } from "@/src/core/api/image"; +import { useRecoilState } from "recoil"; +import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; +import { uploadImageFileAtom } from "@/src/hooks/recoil/useUploadImageFile"; + +function SelectedImage() { + const [isReceipt, setIsReceipt] = useState(false); + const [, setPostSaveAccountBook] = useRecoilState(saveAccountBookAtom); + const [selectedImageFile, setSelectedImageFile] = + useRecoilState(uploadImageFileAtom); + + const handleIsReceiptButton = () => { + setIsReceipt(!isReceipt); + handleUploadImage(); + }; + + const handleCloseButton = () => { + setSelectedImageFile(prev => ({ + ...prev, + isUploadImage: false, + selectedImageFile: null, + })); + }; + + /** COMPLETED: 이미지 전송하기 */ + const handleUploadImage = () => { + if (selectedImageFile) { + const jsonData = JSON.stringify({ isReceipt }); + const imageFile = selectedImageFile.selectedImageFile; + + if (imageFile === null) return; + + imageAPI + .imageUpload(jsonData, imageFile) + .then(response => { + console.log("imageUpload response: ", response); + + /** COMPLETED: image OCR 결과를 input에 사용되고 있는 recoil state에 값으로 대체하기 */ + setPostSaveAccountBook(prev => ({ + ...prev, + address: response.data.ocrResult.address, + amount: response.data.ocrResult.amount, + transactedAt: response.data.ocrResult.date, + transactionDetail: response.data.ocrResult.vendor, + })); + }) + .catch(error => { + console.error("이미지 전송 실패:", error); + }); + } + }; + + useEffect(() => { + // isReceipt가 변경될 때마다 handlUploadImage 호출 + handleUploadImage(); + console.log("isReceipt: ", isReceipt); + }, [isReceipt]); + + return ( + + {selectedImageFile.isUploadImage ? ( +
+ +
+ + 영수증 +
+
+ + + 닫기 버튼 + +
+ ) : ( + 영수증 이미지 미리보기 + )} +
+ ); +} + +export default SelectedImage; diff --git a/src/components/RecordAccountBook/SelectedImage/style.ts b/src/components/SelectedImage/style.ts similarity index 95% rename from src/components/RecordAccountBook/SelectedImage/style.ts rename to src/components/SelectedImage/style.ts index e09a845..e91cfe8 100644 --- a/src/components/RecordAccountBook/SelectedImage/style.ts +++ b/src/components/SelectedImage/style.ts @@ -3,7 +3,7 @@ import { theme } from "@/src/assets/theme"; const Container = styled.div` width: 100%; - min-height: 180px; + height: 220px; position: relative; border: 1px dashed ${theme.font_color.gray2}; margin-top: 15px; @@ -20,7 +20,7 @@ const Container = styled.div` const SelectedImageArea = styled.img` width: 100%; - min-height: 180px; + height: 220px; object-fit: fill; `; diff --git a/src/core/api/image.ts b/src/core/api/image.ts index 17e24e9..a6b4e8f 100644 --- a/src/core/api/image.ts +++ b/src/core/api/image.ts @@ -1,3 +1,4 @@ +import { imageUploadResponseAtomProps } from "@/src/hooks/recoil/useOcrResult"; import { APIInstance } from "./instance"; export const imageAPI = { @@ -11,9 +12,7 @@ export const imageAPI = { formData.append("json-data", jsonData); formData.append("image-file", imageFile); - console.log("formData: ", formData); - - return APIInstance.post( + return APIInstance.post( "/images", formData, { diff --git a/src/hooks/recoil/btnLabelState.ts b/src/hooks/recoil/btnLabelState.ts new file mode 100644 index 0000000..de9fea4 --- /dev/null +++ b/src/hooks/recoil/btnLabelState.ts @@ -0,0 +1,6 @@ +import { atom } from "recoil"; + +export const btnLabelStateAtom = atom({ + key: "btnLabelStateAtom", + default: "반복/할부", +}); diff --git a/src/hooks/recoil/useSaveAccountBook.ts b/src/hooks/recoil/useSaveAccountBook.ts index 06b9501..ed9552d 100644 --- a/src/hooks/recoil/useSaveAccountBook.ts +++ b/src/hooks/recoil/useSaveAccountBook.ts @@ -6,7 +6,8 @@ export interface SaveAccountBookAtomProps { assetName: string; categoryName: string; imageIds: number[]; - isInstallment: number; + isInstallment: boolean; + installmentMonth: number; memo: string; recurringType: string; transactedAt: string; @@ -22,7 +23,8 @@ export const saveAccountBookAtom = atom({ assetName: "", categoryName: "", imageIds: [], - isInstallment: 0, + isInstallment: false, + installmentMonth: 0, memo: "", recurringType: "", transactedAt: "", diff --git a/src/hooks/recoil/useUploadImageFile.ts b/src/hooks/recoil/useUploadImageFile.ts new file mode 100644 index 0000000..d341f7e --- /dev/null +++ b/src/hooks/recoil/useUploadImageFile.ts @@ -0,0 +1,16 @@ +import { atom } from "recoil"; + +export interface UploadImageFileAtomProps { + isUploadImage: boolean; + selectedImage: string | undefined; + selectedImageFile: File | null; +} + +export const uploadImageFileAtom = atom({ + key: "uploadImageFileAtom", + default: { + isUploadImage: false, + selectedImage: undefined, + selectedImageFile: null, + }, +}); diff --git a/src/pages/InstallmentPage/index.tsx b/src/pages/InstallmentPage/index.tsx index ae76432..cc2cb02 100644 --- a/src/pages/InstallmentPage/index.tsx +++ b/src/pages/InstallmentPage/index.tsx @@ -2,23 +2,36 @@ import Header from "@/src/components/Common/Header"; import LongButton from "@/src/components/Common/LongButton"; import { InstallmentPageUI } from "./style"; import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useRecoilState } from "recoil"; +import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; +import { btnLabelStateAtom } from "@/src/hooks/recoil/btnLabelState"; function InstallmentPage() { const [inputValue, setInputValue] = useState(""); - console.log("inputValue: ", inputValue); - const handleBackButton = () => { - /** 이전 페이지로 이동 */ - console.log("뒤로가기"); + const [, setPostSaveAccountBook] = useRecoilState(saveAccountBookAtom); + const [, setBtnLabel] = useRecoilState(btnLabelStateAtom); + + const navigate = useNavigate(); + + const inputChange = (event: React.ChangeEvent) => { + const newValue = event.target.value; + + setInputValue(newValue); + console.log(newValue); }; const handleSaveButton = () => { - console.log("저장"); - }; + setPostSaveAccountBook(prev => ({ + ...prev, + installmentMonth: parseInt(inputValue, 10), + isInstallment: true, + })); + setBtnLabel("할부"); + navigate("/recordAccountBook"); - const InputChange = (event: React.ChangeEvent) => { - setInputValue(event.target.value); - console.log(event.target.value); + console.log("저장"); }; return ( @@ -28,22 +41,20 @@ function InstallmentPage() { isBackButton={true} isFilterButton={false} isMoreButton={false} - onBackClick={handleBackButton} isSearchButton={false} isAddButton={false} /> - - - +
+ + + 개월 + +
diff --git a/src/pages/InstallmentPage/style.ts b/src/pages/InstallmentPage/style.ts index 8b337b3..dcc9ce9 100644 --- a/src/pages/InstallmentPage/style.ts +++ b/src/pages/InstallmentPage/style.ts @@ -1,26 +1,36 @@ import styled from "@emotion/styled"; import { theme } from "../../assets/theme"; -const inputWidth = "100% - 72px"; +const inputWrapperWidth = "100% - 72px"; const Container = styled.div` margin-top: 75px; + + div: first-of-type { + display: flex; + justify-content: center; + } `; -const Form = styled.form` +const InputWrapper = styled.div` display: flex; justify-content: center; + align-items: center; + width: calc(${inputWrapperWidth}); + border-bottom: 1px solid ${theme.font_color.gray2}; + margin-bottom: 25px; + + span { + ${theme.font_style.regular_medium} + } `; const Input = styled.input` border: 0; - border-bottom: 1px solid ${theme.font_color.gray2}; - width: calc(${inputWidth}); + width: 40px; height: 40px; ${theme.font_style.regular_medium}; - - padding-left: 38%; - margin-bottom: 25px; + padding: 0 5px; `; -export const InstallmentPageUI = { Container, Form, Input } as const; +export const InstallmentPageUI = { Container, InputWrapper, Input } as const; diff --git a/src/pages/RecordAccountBookPage/index.tsx b/src/pages/RecordAccountBookPage/index.tsx index dc4136e..f8510db 100644 --- a/src/pages/RecordAccountBookPage/index.tsx +++ b/src/pages/RecordAccountBookPage/index.tsx @@ -1,18 +1,22 @@ import { useRecoilState } from "recoil"; import Header from "../../components/Common/Header"; import ShortButton from "../../components/Common/ShortButton"; -import IncomeExpenseButton from "../../components/RecordAccountBook/IncomeExpenseButton/idnex"; -import InputArea from "../../components/RecordAccountBook/InputArea"; +import IncomeExpenseButton from "../../components/IncomeExpenseButton/idnex"; +import InputArea from "../../components/InputArea"; import { AccountBookAPI } from "@/src/core/api/accountBook"; import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; +import { useNavigate } from "react-router-dom"; function RecordAccountBookPage() { + const navigate = useNavigate(); + const [postAccountBook] = useRecoilState(saveAccountBookAtom); + const handleCancelButton = () => { console.log("저장 안하고 가계부 페이지로 이동"); + navigate("/account"); }; /** TODO: recoil에 값 사용하기 */ - const [postAccountBook] = useRecoilState(saveAccountBookAtom); const handleSaveButton = () => { console.log("저장하고 가계부 페이지로 이동"); diff --git a/src/pages/RecurringPage/index.tsx b/src/pages/RecurringPage/index.tsx index 2cf21e2..faa4836 100644 --- a/src/pages/RecurringPage/index.tsx +++ b/src/pages/RecurringPage/index.tsx @@ -1,6 +1,10 @@ import CommonButton from "@/src/components/Common/CommonButton"; import Header from "@/src/components/Common/Header"; -import { RecurringUI } from "@/src/components/Recurring/style"; +import { RecurringPageUI } from "@/src/pages/RecurringPage/style"; +import { useRecoilState } from "recoil"; +import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; +import { useNavigate } from "react-router-dom"; +import { btnLabelStateAtom } from "@/src/hooks/recoil/btnLabelState"; function RecurringPage() { const recurringItems = [ @@ -19,7 +23,18 @@ function RecurringPage() { "1년마다", ]; + const [, setPostSaveAccountBook] = useRecoilState(saveAccountBookAtom); + const [, setBtnLabel] = useRecoilState(btnLabelStateAtom); + const navigate = useNavigate(); + const handleButtonClick = (item: string) => { + setPostSaveAccountBook(prev => ({ + ...prev, + recurringType: item, + })); + setBtnLabel("반복"); + navigate("/recordAccountBook"); + console.log(`${item} 클릭`); }; @@ -33,7 +48,7 @@ function RecurringPage() { isAddButton={false} isMoreButton={false} /> - +
{recurringItems.map((item, index) => ( ))}
-
+ ); } diff --git a/src/components/Recurring/style.ts b/src/pages/RecurringPage/style.ts similarity index 82% rename from src/components/Recurring/style.ts rename to src/pages/RecurringPage/style.ts index f39fb30..2f76680 100644 --- a/src/components/Recurring/style.ts +++ b/src/pages/RecurringPage/style.ts @@ -9,4 +9,4 @@ const Background = styled.div` background-color: ${theme.font_color.gray1}; `; -export const RecurringUI = { Background } as const; +export const RecurringPageUI = { Background } as const; From cfad7eea95519f6b3b04e5fda333e8976244a905 Mon Sep 17 00:00:00 2001 From: Whale2200d Date: Thu, 23 Nov 2023 17:56:40 +0900 Subject: [PATCH 04/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20[IMPROVE]=20=20Categ?= =?UTF-8?q?ory=20=EB=B0=8F=20NavigationBar=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 46 ++++---- src/@types/models/categoryTypeSymbol.ts | 3 + src/@types/models/createCategory.ts | 10 ++ src/@types/models/deleteCategory.ts | 6 ++ src/@types/models/getCategories.ts | 12 +++ src/@types/models/updateCategory.ts | 9 ++ src/App.tsx | 101 ++++++++---------- src/assets/height.ts | 3 + src/components/Chatting/index.tsx | 71 ++++-------- src/components/Chatting/style.ts | 1 + src/components/ChattingContent/index.tsx | 9 -- .../Common/SeparatedCategory/index.tsx | 46 +++++--- .../Common/SeparatedCategory/style.ts | 22 +++- src/core/api/category.ts | 12 ++- src/hooks/recoil/useGetCategories.ts | 12 +++ src/hooks/recoil/useOpenSeparatedCategory.ts | 12 +++ 16 files changed, 217 insertions(+), 158 deletions(-) create mode 100644 src/@types/models/categoryTypeSymbol.ts create mode 100644 src/@types/models/createCategory.ts create mode 100644 src/@types/models/deleteCategory.ts create mode 100644 src/@types/models/getCategories.ts create mode 100644 src/@types/models/updateCategory.ts create mode 100644 src/hooks/recoil/useGetCategories.ts create mode 100644 src/hooks/recoil/useOpenSeparatedCategory.ts diff --git a/README.md b/README.md index 1ebe379..13972e9 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,35 @@ -# React + TypeScript + Vite +# 가게그만가계(AccountStopTheStore) -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +D:\Whale\Etc\Whale Reference\02. Programming\04. Zerobase_Frontend\01. Lesson_Reference\Mission\Team-project\AccountStopTheStore\public\main_icon.png -Currently, two official plugins are available: +## SUMMARY -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +## 개발 기간 -## Expanding the ESLint configuration +## 기획 배경 -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: +## 해결 컨셉 -- Configure the top-level `parserOptions` property like this: +## 기대 효과 -```js - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], - tsconfigRootDir: __dirname, - }, -``` +## 프로젝트 소개 -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list +## 서비스 아키텍쳐 + +## ERD + +## 주요 개발 스택 + +> 상세 스택은 package.json 파일을 참고해주세요. + +--- + +# 프로젝트 상세 + +## 미리보기 + +## 팀원 소개 + +## REFERENCE + +https://whale2200d.notion.site/6c862beb10dc4880a987bb6b3a3ea508?pvs=4 diff --git a/src/@types/models/categoryTypeSymbol.ts b/src/@types/models/categoryTypeSymbol.ts new file mode 100644 index 0000000..30ddfe6 --- /dev/null +++ b/src/@types/models/categoryTypeSymbol.ts @@ -0,0 +1,3 @@ +type CategoryTypeSymbol = "수입" | "지출"; + +export type { CategoryTypeSymbol }; diff --git a/src/@types/models/createCategory.ts b/src/@types/models/createCategory.ts new file mode 100644 index 0000000..a17485b --- /dev/null +++ b/src/@types/models/createCategory.ts @@ -0,0 +1,10 @@ +import { CategoryTypeSymbol } from "./categoryTypeSymbol"; + +/** + * 카테고리 추가 Response Interface + */ +export type CreateCategory = { + categoryId: number; + categoryType: CategoryTypeSymbol; + categoryName: string; +}; diff --git a/src/@types/models/deleteCategory.ts b/src/@types/models/deleteCategory.ts new file mode 100644 index 0000000..b1db1ba --- /dev/null +++ b/src/@types/models/deleteCategory.ts @@ -0,0 +1,6 @@ +/** + * 카테고리 삭제 Response Interface + */ +export type DeleteCategory = { + message: string; +}; diff --git a/src/@types/models/getCategories.ts b/src/@types/models/getCategories.ts new file mode 100644 index 0000000..118be60 --- /dev/null +++ b/src/@types/models/getCategories.ts @@ -0,0 +1,12 @@ +import { CategoryTypeSymbol } from "./categoryTypeSymbol"; + +/** + * 카테고리 조회 Response Interface + */ +export type GetCategories = GetCategory[]; + +export type GetCategory = { + categoryId: number; + categoryType: CategoryTypeSymbol; + categoryName: string; +}; diff --git a/src/@types/models/updateCategory.ts b/src/@types/models/updateCategory.ts new file mode 100644 index 0000000..70ddf6a --- /dev/null +++ b/src/@types/models/updateCategory.ts @@ -0,0 +1,9 @@ +import { CategoryTypeSymbol } from "./categoryTypeSymbol"; + +/** + * 카테고리 수정 Response Interface + */ +export type UpdateCategory = { + categoryType: CategoryTypeSymbol; + categoryName: string; +}; diff --git a/src/App.tsx b/src/App.tsx index cce375f..f89bce4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { Routes, Route } from "react-router-dom"; +import { Routes, Route, useRoutes } from "react-router-dom"; import LoginPage from "./pages/LoginPage"; import SignUpPage from "./pages/SignUpPage"; import PasswordResetPage from "./pages/PasswordResetPage"; @@ -17,70 +17,57 @@ import ChallengeDetailPage from "./pages/ChallengeDetailPage"; import CreateChallengeGroupPage from "./pages/CreateChallengeGroupPage"; function App() { - const isLoggedIn = isHaveToken(); - - const isLoginPage = window.location.pathname === "/login"; + const isLoginPage = window.location.pathname === "/"; const isSignUpPage = window.location.pathname === "/signup"; const isPasswordReset = window.location.pathname === "/passwordreset"; const isRecordAccountBook = window.location.pathname === "/recordAccountBook"; return ( - - {/* COMPLETED: 처음 화면에 진입했을 때 */} - : } - /> - {/* COMPLETED: 로그인하지 않아서, Token이 없을 때 */} - } /> - } /> - } /> - {/* COMPLETED: 로그인하여, Token이 주어졌을 때 */} - } /> - } /> - } - /> - } - /> - } /> - } /> - } - /> - } /> - } /> - } /> - } /> - - {!isLoginPage && - !isSignUpPage && - !isPasswordReset && - !isRecordAccountBook && } + {useRoutes([ + { path: "/", element: }, + { path: "/signUp", element: }, + { path: "/passwordReset", element: }, + { path: "/recordAccountBook/recurring", element: }, + { + path: "/*", + element: ( + <> + + {/* COMPLETED: 로그인하여, Token이 주어졌을 때 */} + } /> + } + /> + } + /> + } /> + } + /> + } + /> + } /> + } /> + } /> + } /> + + {!isLoginPage && + !isSignUpPage && + !isPasswordReset && + !isRecordAccountBook && } + + ), + }, + ])} ); } -/** COMPLETED: Cookie에 Token이 존재하는지 확인하기 */ -const isHaveToken = () => { - const COOKIE_VALUE = `${document.cookie}`; - console.log("COOKIE_VALUE: ", COOKIE_VALUE); - - if (!COOKIE_VALUE) return false; - - const PARTS = COOKIE_VALUE.split(";"); - const ACCESS_TOKEN = PARTS[5].split("=")[1]; - - if (ACCESS_TOKEN) { - return true; - } else { - return false; - } -}; - export default App; diff --git a/src/assets/height.ts b/src/assets/height.ts index 027fe02..0511f7f 100644 --- a/src/assets/height.ts +++ b/src/assets/height.ts @@ -7,6 +7,7 @@ export const CDMSTabItemsHeight = 50; export const NavigationItemsHeight = 70; export const ChallengeTopContainerHeight = 92.75; export const BottomDateModalHeight = 470; +export const ShortButtonHeight = 50; export const AccountHeight = ApplicationHeight - @@ -16,6 +17,8 @@ export const AccountHeight = CDMSTabItemsHeight + NavigationItemsHeight); +export const recordAccountBookHeight = ApplicationHeight - HeaderHeight; + export const ChallengeHeight = ApplicationHeight - (HeaderHeight + NavigationItemsHeight); export const ChallengeBottomListHeight = diff --git a/src/components/Chatting/index.tsx b/src/components/Chatting/index.tsx index c35436c..065d674 100644 --- a/src/components/Chatting/index.tsx +++ b/src/components/Chatting/index.tsx @@ -10,10 +10,9 @@ import { ChangeTime } from "@/src/assets/util"; export interface Message { groupId: number; - senderId: number; message: string; messageType: "TALK" | "LEAVE" | "ENTER"; - date: string; + senderId: number; } function Chatting() { @@ -59,35 +58,6 @@ function Chatting() { ], }; - const chattingArray = [ - { - memberName: "김나나", - sendTime: "2023-01-01T12:00:00", - messageContent: "", - access: "exit", - }, - { - memberName: "Aiden", - sendTime: "2023-01-01T12:00:00", - messageContent: - "메시지 내용 1메시지 내용 1메시지 내용 1메시지 내용 1메시지 내용 1메시지 내용 1메시지 내용 1메시지 내용 1메시지 내용 1메시지 내용 1", - access: "others", - }, - { - memberName: "Aiden", - sendTime: "2023-01-01T12:00:00", - messageContent: - "메시지 내용 1메시지 내용 1메시지 내용 1메시지 내용 1메시지 내용 1메시지 내용 1메시지 내용 1메시지 내용 1메시지 내용 1메시지 내용 1", - access: "me", - }, - { - memberName: "임경락", - sendTime: "2023-01-01T12:00:00", - messageContent: "메시지 내용 1", - access: "enter", - }, - ]; - const connect = async () => { const socket = new WebSocket(`${API_WEBSOCKET_URL}/ws`); console.log("socket.onmessage: ", socket.onmessage); @@ -101,35 +71,33 @@ function Chatting() { console.log("Connected to WebSocket"); /* COMPLETED: 채팅 메시지를 구독하기 */ - const receiveMessage = stomp.subscribe( - `/chat/groups/3`, - onReceiveMessage, - { id: 1 } - ); + stomp.subscribe(`/chat/groups/3`, onReceiveMessage); + }); + }; - console.log("receiveMessage.id: ", receiveMessage.id); - console.log("receiveMessage.unsubscribe: ", receiveMessage.unsubscribe); + const onReceiveMessage = async (message: Stomp.Message) => { + setMessages(prev => { + const newMessage = JSON.parse(message.body); + return [...prev, newMessage]; }); }; - const onReceiveMessage = (message: Stomp.Message) => { - const newMessages: Message[] = [...messages, JSON.parse(message.body)]; - console.log("Received message: ", newMessages); - setMessages(newMessages); + /** COMPLETED: Websocket 연결 해제하기 */ + const disconnect = () => { + if (stompClient) { + stompClient.disconnect(() => + console.log("Websocket 연결이 해제되었습니다.") + ); + } }; useEffect(() => { connect(); - }, []); - /** COMPLETED: Websocket 연결 해제하기 */ - // return () => { - // if (stompClient) { - // stompClient.disconnect(() => - // console.log("Websocket 연결이 해제되었습니다.") - // ); - // } - // }; + return () => { + disconnect(); + }; + }, []); /* Click 이벤트 등 전송하기 관련 이벤트를 통한 채팅 메시지 전송하기 */ const sendMessage = (message: string) => { @@ -139,7 +107,6 @@ function Chatting() { {}, JSON.stringify({ groupId: 3, - senderId: 9, message: message, messageType: "TALK", }) diff --git a/src/components/Chatting/style.ts b/src/components/Chatting/style.ts index fe9f0a2..b9e1d9d 100644 --- a/src/components/Chatting/style.ts +++ b/src/components/Chatting/style.ts @@ -11,6 +11,7 @@ const BottomContainer = styled.div` `; const ChattingList = styled.ul` + overflow: scroll; flex: 1; background-color: ${theme.font_color.primary_green}; diff --git a/src/components/ChattingContent/index.tsx b/src/components/ChattingContent/index.tsx index 1551bc8..d755977 100644 --- a/src/components/ChattingContent/index.tsx +++ b/src/components/ChattingContent/index.tsx @@ -1,16 +1,7 @@ -import { ChangeTime } from "@/src/assets/util"; import { ChattingContentUI } from "./style"; import { Message } from "../Chatting"; -interface ChattingContentData { - // memberName: string; - // sendTime: string; - // messageContent: string; - // access: string; -} - interface ChattingContentProps { - // content: ChattingContentData; content: Message; } diff --git a/src/components/Common/SeparatedCategory/index.tsx b/src/components/Common/SeparatedCategory/index.tsx index 4c96cbb..45c630e 100644 --- a/src/components/Common/SeparatedCategory/index.tsx +++ b/src/components/Common/SeparatedCategory/index.tsx @@ -2,8 +2,13 @@ import CancelPNG from "../../../../public/icon/Cancel.png"; import WritingPNG from "../../../../public/icon/Writing.png"; import { Button } from "@mui/material"; import { theme } from "../../../assets/theme"; -import { SeperatedCategoryUI } from "./style"; +import { SeparatedCategoryUI } from "./style"; import { useNavigate } from "react-router-dom"; +import { useRecoilState } from "recoil"; +import { openSeparatedCategoryAtom } from "@/src/hooks/recoil/useOpenSeparatedCategory"; +import { getCategoriesAtom } from "@/src/hooks/recoil/useGetCategories"; +import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; +import { GetCategory } from "@/src/@types/models/getCategories"; interface SeparatedCategoryProps { title?: string; @@ -17,16 +22,30 @@ const buttonStyles = { function SeparatedCategory({ title }: SeparatedCategoryProps) { const navigate = useNavigate(); + const [, setIsOpenSeparatedCategory] = useRecoilState( + openSeparatedCategoryAtom + ); + const [categories] = useRecoilState(getCategoriesAtom); + const [, setPostSaveAccountBook] = useRecoilState(saveAccountBookAtom); const handleWritingButton = () => { navigate("/setting"); }; - const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + const handleCancelButton = () => { + setIsOpenSeparatedCategory({ isOpen: false }); + }; + + const handleCategoryButton = (item: GetCategory) => { + setPostSaveAccountBook(prev => ({ + ...prev, + categoryName: item.categoryName, + })); + }; return ( - +
- +
{title}
-
-
- - {array.map(item => ( - ))} - +
-
+ ); } diff --git a/src/components/Common/SeparatedCategory/style.ts b/src/components/Common/SeparatedCategory/style.ts index 8869827..dff2dd0 100644 --- a/src/components/Common/SeparatedCategory/style.ts +++ b/src/components/Common/SeparatedCategory/style.ts @@ -1,14 +1,26 @@ import styled from "@emotion/styled"; import { theme } from "@/src/assets/theme"; +import { recordAccountBookHeight } from "@/src/assets/height"; const Container = styled.div` width: 100%; - height: 450px; - background-color: ${theme.font_color.gray1}; + height: ${recordAccountBookHeight}px; + background-color: rgba(0, 0, 0, 0.5); + position: absolute; + top: 77px; left: 0; - bottom: 0; z-index: 2; + + > div { + width: 100%; + height: 450px; + background-color: ${theme.font_color.gray1}; + position: absolute; + left: 0; + bottom: 0; + z-index: 3; + } `; const CategoryHeader = styled.div` @@ -18,7 +30,7 @@ const CategoryHeader = styled.div` padding-left: 20px; padding-right: 20px; - display: fles; + display: flex; justify-content: space-between; align-items: center; `; @@ -29,7 +41,7 @@ const GridContainer = styled.div` background-color: ${theme.font_color.white}; `; -export const SeperatedCategoryUI = { +export const SeparatedCategoryUI = { Container, CategoryHeader, GridContainer, diff --git a/src/core/api/category.ts b/src/core/api/category.ts index ed833ad..4fe8064 100644 --- a/src/core/api/category.ts +++ b/src/core/api/category.ts @@ -1,14 +1,18 @@ +import { CreateCategory } from "@/src/@types/models/createCategory"; import { APIInstance } from "./instance"; +import { GetCategories } from "@/src/@types/models/getCategories"; +import { UpdateCategory } from "@/src/@types/models/updateCategory"; +import { DeleteCategory } from "@/src/@types/models/deleteCategory"; const CATEGORIES = "/categories"; export const categoryAPI = { /** COMPLETED: getCategory GET 요청하기 */ getCategory: () => { - return APIInstance.get(CATEGORIES); + return APIInstance.get(CATEGORIES); }, /** COMPLETED: createCategory POST 요청하기 */ createCategory: (categoryName: string, categoryType: string) => { - return APIInstance.post(CATEGORIES, { + return APIInstance.post(CATEGORIES, { categoryName: categoryName, categoryType: categoryType, }); @@ -19,13 +23,13 @@ export const categoryAPI = { categoryName: string, categoryType: string ) => { - return APIInstance.put(CATEGORIES + `/${categoryId}`, { + return APIInstance.put(CATEGORIES + `/${categoryId}`, { categoryName: categoryName, categoryType: categoryType, }); }, /** COMPLETED: deleteCategory DELETE 요청하기 */ deleteCategory: (categoryId: number) => { - return APIInstance.delete(CATEGORIES + `/${categoryId}`); + return APIInstance.delete(CATEGORIES + `/${categoryId}`); }, }; diff --git a/src/hooks/recoil/useGetCategories.ts b/src/hooks/recoil/useGetCategories.ts new file mode 100644 index 0000000..bdfdeab --- /dev/null +++ b/src/hooks/recoil/useGetCategories.ts @@ -0,0 +1,12 @@ +import { GetCategories } from "@/src/@types/models/getCategories"; +import { categoryAPI } from "@/src/core/api/category"; +import { atom } from "recoil"; + +const fetchResult = categoryAPI.getCategory(); + +export const getCategoriesAtom = atom({ + key: "getCategoriesAtom", + default: fetchResult.then(response => { + return response.data; + }), +}); diff --git a/src/hooks/recoil/useOpenSeparatedCategory.ts b/src/hooks/recoil/useOpenSeparatedCategory.ts new file mode 100644 index 0000000..228157a --- /dev/null +++ b/src/hooks/recoil/useOpenSeparatedCategory.ts @@ -0,0 +1,12 @@ +import { atom } from "recoil"; + +export interface OpenSeparatedCategoryAtomProps { + isOpen: boolean; +} + +export const openSeparatedCategoryAtom = atom({ + key: "openSeparatedCategoryAtom", + default: { + isOpen: false, + }, +}); From 5715e227fce20d9e28d050d7b9b52eb1c1f6e20a Mon Sep 17 00:00:00 2001 From: Sojung Park Date: Thu, 23 Nov 2023 18:10:09 +0900 Subject: [PATCH 05/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20[IMPROVE]=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=EC=A7=80=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 1169 ++++++++++++++++- src/@types/models/getNearbyAccountBooks.ts | 2 +- .../CalendarModal/{idnex.tsx => index.tsx} | 0 .../Common/DayIncomeExpenseInfo/index.tsx | 4 +- .../Common/DayIncomeExpenseInfo/style.ts | 8 + .../Common/DayIncomeExpenseInfos/index.tsx | 6 +- src/components/DisplayMarker/index.tsx | 6 +- .../{idnex.tsx => index.tsx} | 0 src/components/InputArea/index.tsx | 81 -- src/components/KakaoMap/index.tsx | 31 +- src/components/RecordInfos/index.tsx | 42 +- src/components/RecordInfos/style.ts | 5 +- src/core/api/accountBook.ts | 59 +- src/hooks/recoil/clickedMarkerData.ts | 25 + src/pages/RecordAccountBookPage/index.tsx | 8 +- src/pages/RecordPage/index.tsx | 79 +- 16 files changed, 1348 insertions(+), 177 deletions(-) rename src/components/Common/CalendarModal/{idnex.tsx => index.tsx} (100%) rename src/components/IncomeExpenseButton/{idnex.tsx => index.tsx} (100%) create mode 100644 src/hooks/recoil/clickedMarkerData.ts diff --git a/package-lock.json b/package-lock.json index b05342b..5cc275d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,16 +19,19 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.17.0", - "recoil": "^0.7.7" + "recoil": "^0.7.7", + "stompjs": "^2.3.3" }, "devDependencies": { "@types/node": "^20.9.4", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", + "@types/stompjs": "^2.3.9", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react-swc": "^3.3.2", "eslint": "^8.45.0", + "eslint-plugin-import": "^2.29.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", "json-server": "^0.17.4", @@ -2055,6 +2058,12 @@ "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", "dev": true }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, "node_modules/@types/node": { "version": "20.9.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz", @@ -2131,6 +2140,15 @@ "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", "dev": true }, + "node_modules/@types/stompjs": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@types/stompjs/-/stompjs-2.3.9.tgz", + "integrity": "sha512-fu/GgkRdxwyEJ+JeUsGhDxGwmZQi+xeNElradGQ4ehWiG2z/o89gsi5Y7Gv0KC6VK1v78Cjh8zj3VF+RvqCGSA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz", @@ -2418,12 +2436,44 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true }, + "node_modules/array-includes": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -2433,11 +2483,99 @@ "node": ">=8" } }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/axios": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", @@ -2573,6 +2711,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bufferutil": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2880,6 +3031,16 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "node_modules/d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "optional": true, + "dependencies": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, "node_modules/dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", @@ -2922,6 +3083,23 @@ "node": ">= 0.4" } }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3052,6 +3230,135 @@ "node": ">= 0.8" } }, + "node_modules/es-abstract": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es5-ext": { + "version": "0.10.62", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", + "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "optional": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "optional": true, + "dependencies": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, "node_modules/esbuild": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", @@ -3170,6 +3477,113 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", + "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-plugin-react-hooks": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", @@ -3428,6 +3842,21 @@ } ] }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "optional": true, + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/ext/node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", + "optional": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3600,6 +4029,15 @@ } } }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -3659,6 +4097,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3692,6 +4157,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3739,10 +4220,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "dependencies": { "array-union": "^2.1.0", @@ -3794,6 +4290,15 @@ "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==" }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3839,6 +4344,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", @@ -3935,6 +4455,20 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3944,11 +4478,65 @@ "node": ">= 0.10" } }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -3960,6 +4548,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3990,6 +4593,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3999,6 +4614,21 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -4014,6 +4644,97 @@ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", "dev": true }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "optional": true + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -4374,6 +5095,15 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/morgan": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", @@ -4456,6 +5186,12 @@ "node": ">= 0.6" } }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "optional": true + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -4466,6 +5202,17 @@ "tslib": "^2.0.3" } }, + "node_modules/node-gyp-build": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.7.0.tgz", + "integrity": "sha512-PbZERfeFdrHQOOXiAKOY0VPbykZy90ndPKk0d+CFDegTKmWp1VgOTz2xACVbr1BjCWxrQp68CXtvNsveFhqDJg==", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-releases": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", @@ -4489,6 +5236,79 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -4924,6 +5744,23 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5021,12 +5858,50 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -5143,6 +6018,20 @@ "node": ">= 0.4" } }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -5238,6 +6127,14 @@ "graceful-fs": "^4.1.3" } }, + "node_modules/stompjs": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/stompjs/-/stompjs-2.3.3.tgz", + "integrity": "sha512-5l/Ogz0DTFW7TrpHF0LAETGqM/so8UxNJvYZjJKqcX31EVprSQgnGkO80tZctPC/lFBDUrSFiTG3xd0R27XAIA==", + "optionalDependencies": { + "websocket": "latest" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5252,6 +6149,51 @@ "node": ">=8" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5264,6 +6206,15 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5377,12 +6328,42 @@ } } }, + "node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, + "node_modules/type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "optional": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5420,6 +6401,80 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "optional": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -5433,6 +6488,21 @@ "node": ">=14.17" } }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -5487,6 +6557,19 @@ "punycode": "^2.1.0" } }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -5593,6 +6676,38 @@ } } }, + "node_modules/websocket": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz", + "integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==", + "optional": true, + "dependencies": { + "bufferutil": "^4.0.1", + "debug": "^2.2.0", + "es5-ext": "^0.10.50", + "typedarray-to-buffer": "^3.1.5", + "utf-8-validate": "^5.0.2", + "yaeti": "^0.0.6" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/websocket/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/websocket/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5608,6 +6723,41 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -5640,6 +6790,15 @@ "node": ">=10" } }, + "node_modules/yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", + "optional": true, + "engines": { + "node": ">=0.10.32" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/src/@types/models/getNearbyAccountBooks.ts b/src/@types/models/getNearbyAccountBooks.ts index 75ef757..224647c 100644 --- a/src/@types/models/getNearbyAccountBooks.ts +++ b/src/@types/models/getNearbyAccountBooks.ts @@ -5,7 +5,7 @@ export type GetNearbyAccountBooks = { accountId: number; address: string; amount: number; - assetType: string; + assetName: string; categoryName: string; createdAt: string; imageIds: number[]; diff --git a/src/components/Common/CalendarModal/idnex.tsx b/src/components/Common/CalendarModal/index.tsx similarity index 100% rename from src/components/Common/CalendarModal/idnex.tsx rename to src/components/Common/CalendarModal/index.tsx diff --git a/src/components/Common/DayIncomeExpenseInfo/index.tsx b/src/components/Common/DayIncomeExpenseInfo/index.tsx index 60e08d6..682f4bb 100644 --- a/src/components/Common/DayIncomeExpenseInfo/index.tsx +++ b/src/components/Common/DayIncomeExpenseInfo/index.tsx @@ -20,14 +20,14 @@ function DayIncomeExpenseInfo({ onClick, item }: DayIncomeExpenseInfoProps) {
{item.transactionDetail}
{ChangeTime(item.createdAt)} - {item.assetType} + {item.assetName}
- {ChangeNumberForAccounting(item.amount)} + {ChangeNumberForAccounting(item.amount)}원
diff --git a/src/components/Common/DayIncomeExpenseInfo/style.ts b/src/components/Common/DayIncomeExpenseInfo/style.ts index 09b1f7b..2e2b4d5 100644 --- a/src/components/Common/DayIncomeExpenseInfo/style.ts +++ b/src/components/Common/DayIncomeExpenseInfo/style.ts @@ -10,6 +10,7 @@ const DayIncomeExpenseInfoButton = styled(Button)` color: ${theme.font_color.black}; font-weight: 300; fontsize: 13px; + background-color: ${theme.font_color.white}; `; const ContentContainer = styled.div` @@ -21,6 +22,7 @@ const ContentContainer = styled.div` > div:nth-of-type(1) { margin-right: 46px; color: ${theme.font_color.gray3}; + ${theme.font_style.regular_small}; } > div:nth-of-type(2) { flex: 1; @@ -40,10 +42,16 @@ const ContentContainer = styled.div` > div:nth-of-type(2) { color: ${theme.font_color.gray3}; + > span { + ${theme.font_style.regular_small}; + } > span:nth-of-type(1) { margin-right: 32px; } } + > div:nth-of-type(2) { + ${theme.font_style.bold_small}; + } } `; diff --git a/src/components/Common/DayIncomeExpenseInfos/index.tsx b/src/components/Common/DayIncomeExpenseInfos/index.tsx index 1d1e1c1..a00f9d4 100644 --- a/src/components/Common/DayIncomeExpenseInfos/index.tsx +++ b/src/components/Common/DayIncomeExpenseInfos/index.tsx @@ -4,7 +4,7 @@ export interface AccountsData { accountId: number; address: string; amount: number; - assetType: string; + assetName: string; categoryName: string; createdAt: string; imageIds: number[]; @@ -24,7 +24,7 @@ function DayIncomeExpenseInfos() { accountId: 1, address: "서울특별시 은평구 은평터널로 150", categoryName: "간식", - assetType: "현금", + assetName: "현금", amount: 2000000, transactionType: "지출", transactionDetail: "감자칩, 서신초등학교", @@ -41,7 +41,7 @@ function DayIncomeExpenseInfos() { accountId: 2, address: "서울특별시 은평구 증산로 403-1", categoryName: "간식", - assetType: "현금", + assetName: "현금", amount: 1500, transactionType: "수입", transactionDetail: "몽소", diff --git a/src/components/DisplayMarker/index.tsx b/src/components/DisplayMarker/index.tsx index 115e1ae..337cf72 100644 --- a/src/components/DisplayMarker/index.tsx +++ b/src/components/DisplayMarker/index.tsx @@ -7,6 +7,7 @@ interface DisplayMarkerProps { width: number; height: number; }; + markerClick?: () => void; } function DisplayMarker({ @@ -15,6 +16,7 @@ function DisplayMarker({ message, markerImageURL, markerImageSize, + markerClick, }: DisplayMarkerProps) { // 마커 이미지 사이즈 const markerSize = new window.kakao.maps.Size( @@ -44,7 +46,9 @@ function DisplayMarker({ // 마커 이벤트 등록 const hanldeMarkerClick = () => { - console.log("마커 클릭"); + if (markerClick) { + markerClick(); + } }; kakao.maps.event.addListener(marker, "mouseover", function () { diff --git a/src/components/IncomeExpenseButton/idnex.tsx b/src/components/IncomeExpenseButton/index.tsx similarity index 100% rename from src/components/IncomeExpenseButton/idnex.tsx rename to src/components/IncomeExpenseButton/index.tsx diff --git a/src/components/InputArea/index.tsx b/src/components/InputArea/index.tsx index 5bea303..8d5d042 100644 --- a/src/components/InputArea/index.tsx +++ b/src/components/InputArea/index.tsx @@ -1,93 +1,12 @@ import { ChangeEvent, useState } from "react"; import LabelInput from "../Common/LabelInput"; import { InputAreaUI } from "./style"; - import SelectedImage from "../SelectedImage"; import SeparatedCategory from "../Common/SeparatedCategory"; import { useRecoilState } from "recoil"; import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; -// interface InputAreaData { -// type: string; -// label: string; -// inputId: string; -// value: string | number; -// placeholder: string; -// addContent: string; -// onClick: undefined | (() => void); -// handleChange: undefined | (() => void); -// readonly: boolean; -// } - function InputArea() { - // const InputArray = [ - // { - // type: "text", - // label: "날짜", - // inputId: "transactedAt", - // value: "", - // placeholder: "날짜를 입력해주세요.", - // addContent: "button", - // onClick: undefined, - // handleChange: handleDate, - // readonly: false, - // }, - // { - // type: "number", - // label: "금액", - // inputId: "amount", - // value: 0, - // placeholder: "금액을 입력해주세요.", - // addContent: "won", - // onClick: undefined, - // handleChange: handleAmount, - // readonly: false, - // }, - // { - // type: "text", - // label: "분류", - // inputId: "categoryName", - // value: "", - // placeholder: "카테고리를 선택해주세요.", - // addContent: "nothing", - // onClick: () => setShowSeparatedCategory(!showSeparatedCategory), - // handleChange: handleCategoryName, - // readonly: true, - // }, - // { - // type: "text", - // label: "자산", - // inputId: "asset", - // value: "", - // placeholder: "결제 수단을 선택해주세요.", - // addContent: "nothing", - // onClick: () => setShowSeparatedCategory(!showSeparatedCategory), - // handleChange: handleAsset, - // readonly: true, - // }, - // { - // type: "text", - // label: "사용처", - // inputId: "transactionDetail", - // value: "", - // placeholder: "사용처를 입력해주세요.", - // addContent: "nothing", - // onClick: undefined, - // handleChange: handleTransactionDetail, - // readonly: false, - // }, - // { - // type: "text", - // label: "주소", - // inputId: "address", - // value: "", - // placeholder: "주소를 입력해주세요.", - // addContent: "nothing", - // onClick: undefined, - // handleChange: handleAddress, - // readonly: false, - // }, - // ]; const [postSaveAccountBook, setPostSaveAccountBook] = useRecoilState(saveAccountBookAtom); const [showSeparatedCategory, setShowSeparatedCategory] = diff --git a/src/components/KakaoMap/index.tsx b/src/components/KakaoMap/index.tsx index a6aa05c..a59efae 100644 --- a/src/components/KakaoMap/index.tsx +++ b/src/components/KakaoMap/index.tsx @@ -3,6 +3,8 @@ import { MapUI } from "./style"; import DisplayMarker from "../DisplayMarker"; import BackToCenterIcon from "@/public/icon/BackToCenter.svg"; import { GetNearbyAccountBooks } from "@/src/@types/models/getNearbyAccountBooks"; +import { useRecoilState } from "recoil"; +import { clickedMarkerDataAtom } from "@/src/hooks/recoil/clickedMarkerData"; declare global { interface Window { @@ -13,17 +15,40 @@ declare global { interface onLocationChangeCallBack { onLocationChange: (lat: number, lng: number) => void; data: GetNearbyAccountBooks[]; + toggleRecordInfosVisibility: () => void; } -function KakaoMap({ onLocationChange, data }: onLocationChangeCallBack) { +function KakaoMap({ + onLocationChange, + data, + toggleRecordInfosVisibility, +}: onLocationChangeCallBack) { const { kakao } = window; const mapRef = useRef(null); + const [map, setMap] = useState(null); const [currentLocation, setCurrentLocation] = useState(null); + const [datas, setDatas] = useState([]); + useEffect(() => { + setDatas(data); + }, []); const [message, setMessage] = useState(""); - console.log("message: ", message); + + const [, setClickedMarkerData] = useRecoilState(clickedMarkerDataAtom); + + // 마커 클릭 이벤트 + const handleMarkerClick = (markerData: GetNearbyAccountBooks) => { + const markerAddress = markerData.address; + + const selectedAddressData = data.filter(data => { + return data.address === markerAddress; + }); + + setClickedMarkerData(selectedAddressData); + toggleRecordInfosVisibility(); + }; /** 카카오 맵 불러오기 */ useEffect(() => { @@ -50,6 +75,7 @@ function KakaoMap({ onLocationChange, data }: onLocationChangeCallBack) { setCurrentLocation(locPosition); onLocationChange(lat, lng); + console.log("위경도", lat, lng); }); } else { // HTML5의 GeoLocation을 사용할 수 없을 때 마커 표시 위치와 인포윈도우 내용 설정 @@ -88,6 +114,7 @@ function KakaoMap({ onLocationChange, data }: onLocationChangeCallBack) { markerImageURL: "https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/markerStar.png", markerImageSize: { width: 24, height: 35 }, + markerClick: () => handleMarkerClick(item), }); }); } diff --git a/src/components/RecordInfos/index.tsx b/src/components/RecordInfos/index.tsx index 05074c6..535f481 100644 --- a/src/components/RecordInfos/index.tsx +++ b/src/components/RecordInfos/index.tsx @@ -1,29 +1,41 @@ import { RecordInfosUI } from "./style"; import DayIncomeExpenseInfo from "../Common/DayIncomeExpenseInfo"; import { GetNearbyAccountBooks } from "@/src/@types/models/getNearbyAccountBooks"; +import { useRecoilValue } from "recoil"; +import { clickedMarkerDataAtom } from "@/src/hooks/recoil/clickedMarkerData"; -export interface DataType { - data: GetNearbyAccountBooks[]; +interface RecordInfosProps { + isVisible: boolean; } -function RecordInfos({ data }: DataType) { +function RecordInfos({ isVisible }: RecordInfosProps) { + const clickedMarkerData = useRecoilValue(clickedMarkerDataAtom); + const eventHandler = (item: GetNearbyAccountBooks) => { console.log("item: ", item); }; return ( - - 주소 - - {data.map(item => ( - eventHandler(item)} - item={item} - /> - ))} - - + <> + {isVisible && ( + + + {clickedMarkerData.length > 0 + ? clickedMarkerData[0].address + : "주소 없음"} + + + {clickedMarkerData.map(item => ( + eventHandler(item)} + item={item} + /> + ))} + + + )} + ); } diff --git a/src/components/RecordInfos/style.ts b/src/components/RecordInfos/style.ts index c331447..820b3f6 100644 --- a/src/components/RecordInfos/style.ts +++ b/src/components/RecordInfos/style.ts @@ -8,18 +8,19 @@ const Container = styled.div` width: 100%; position: absolute; bottom: ${NavigationItemsHeight}px; - background-color: ${theme.font_color.white}; + background-color: ${theme.font_color.gray1}; border-top: 1px solid ${theme.font_color.gray2}; z-index: 1; `; const Address = styled.div` widht: inherit; - height: 84px; padding: 20px; border-bottom: 1px solid ${theme.font_color.gray2}; + background-color: ${theme.font_color.white}; ${theme.font_style.bold_large}; color: ${theme.font_color.black}; + white-space: normal; `; const ListContainerUl = styled.ul` diff --git a/src/core/api/accountBook.ts b/src/core/api/accountBook.ts index 2eefc25..6190915 100644 --- a/src/core/api/accountBook.ts +++ b/src/core/api/accountBook.ts @@ -32,7 +32,7 @@ export const AccountBookAPI = { assetName: string; categoryName: string; imageIds: number[]; - isInstallment: number; + isInstallment: boolean; memo: string; recurringType: string; transactedAt: string; @@ -54,33 +54,36 @@ export const AccountBookAPI = { }); }, /** COMPLETED: updateAccountBook PUT 요청하기 */ - updateAccountBook: ( - accountId: number, - address: string, - amount: number, - assetName: string, - categoryName: string, - imageIds: number[], - isInstallment: false, - memo: string, - recurringType: string, - transactedAt: string, - transactionDetail: string, - transactionType: string - ) => { - return APIInstance.put(ACCOUNTS + `/${accountId}`, { - address: address, - amount: amount, - assetName: assetName, - categoryName: categoryName, - imageIds: imageIds, - isInstallment: isInstallment, - memo: memo, - recurringType: recurringType, - transactedAt: transactedAt, - transactionDetail: transactionDetail, - transactionType: transactionType, - }); + updateAccountBook: (PutAccountBook: { + accountId: number; + address: string; + amount: number; + assetName: string; + categoryName: string; + imageIds: number[]; + isInstallment: boolean; + memo: string; + recurringType: string; + transactedAt: string; + transactionDetail: string; + transactionType: string; + }) => { + return APIInstance.put( + ACCOUNTS + `/${PutAccountBook.accountId}`, + { + address: PutAccountBook.address, + amount: PutAccountBook.amount, + assetName: PutAccountBook.assetName, + categoryName: PutAccountBook.categoryName, + imageIds: PutAccountBook.imageIds, + isInstallment: PutAccountBook.isInstallment, + memo: PutAccountBook.memo, + recurringType: PutAccountBook.recurringType, + transactedAt: PutAccountBook.transactedAt, + transactionDetail: PutAccountBook.transactionDetail, + transactionType: PutAccountBook.transactionType, + } + ); }, /** COMPLETED: deleteAccountBook DELETE 요청하기 */ deleteAccountBook: (accountId: number) => { diff --git a/src/hooks/recoil/clickedMarkerData.ts b/src/hooks/recoil/clickedMarkerData.ts new file mode 100644 index 0000000..f3d5227 --- /dev/null +++ b/src/hooks/recoil/clickedMarkerData.ts @@ -0,0 +1,25 @@ +import { atom } from "recoil"; + +export interface clickedMarkerDataAtomProps { + accountId: number; + address: string; + amount: number; + assetName: string; + categoryName: string; + createdAt: string; + imageIds: number[]; + installment: boolean; + latitude: number; + longitude: number; + memo: string; + recurringType: string; + transactedAt: string; + transactionDetail: string; + transactionType: string; + updatedAt: string; +} + +export const clickedMarkerDataAtom = atom({ + key: "clickedMarkerDataAtom", + default: [], +}); diff --git a/src/pages/RecordAccountBookPage/index.tsx b/src/pages/RecordAccountBookPage/index.tsx index f8510db..0a4de5e 100644 --- a/src/pages/RecordAccountBookPage/index.tsx +++ b/src/pages/RecordAccountBookPage/index.tsx @@ -1,7 +1,7 @@ import { useRecoilState } from "recoil"; import Header from "../../components/Common/Header"; import ShortButton from "../../components/Common/ShortButton"; -import IncomeExpenseButton from "../../components/IncomeExpenseButton/idnex"; +import IncomeExpenseButton from "../../components/IncomeExpenseButton"; import InputArea from "../../components/InputArea"; import { AccountBookAPI } from "@/src/core/api/accountBook"; import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; @@ -9,7 +9,7 @@ import { useNavigate } from "react-router-dom"; function RecordAccountBookPage() { const navigate = useNavigate(); - const [postAccountBook] = useRecoilState(saveAccountBookAtom); + const [postSaveAccountBook] = useRecoilState(saveAccountBookAtom); const handleCancelButton = () => { console.log("저장 안하고 가계부 페이지로 이동"); @@ -18,9 +18,9 @@ function RecordAccountBookPage() { /** TODO: recoil에 값 사용하기 */ const handleSaveButton = () => { - console.log("저장하고 가계부 페이지로 이동"); + AccountBookAPI.saveAccountBook(postSaveAccountBook); - AccountBookAPI.saveAccountBook(postAccountBook); + console.log("저장하고 가계부 페이지로 이동"); }; return ( diff --git a/src/pages/RecordPage/index.tsx b/src/pages/RecordPage/index.tsx index 2f7ca3c..0dfa387 100644 --- a/src/pages/RecordPage/index.tsx +++ b/src/pages/RecordPage/index.tsx @@ -2,17 +2,17 @@ import { GetNearbyAccountBooks } from "@/src/@types/models/getNearbyAccountBooks import Header from "@/src/components/Common/Header"; import NavigationItems from "@/src/components/Common/NavigationItems"; import KakaoMap from "@/src/components/KakaoMap"; -// import RecordInfos from "@/src/components/RecordInfos"; +import RecordInfos from "@/src/components/RecordInfos"; import { useEffect, useState } from "react"; -// import { AccountBookAPI } from "@/src/core/api/accountBook"; +import { AccountBookAPI } from "@/src/core/api/accountBook"; function RecordPage() { const exampleNearby = [ { accountId: 1, - address: "서울특별시 은평구 은평터널로 150", + address: "서울특별시 은평구 은평터널로 150 ", categoryName: "간식", - assetType: "현금", + assetName: "현금", amount: 2000000, transactionType: "지출", transactionDetail: "감자칩, 서신초등학교", @@ -30,7 +30,7 @@ function RecordPage() { accountId: 2, address: "서울특별시 은평구 증산로 403-1", categoryName: "간식", - assetType: "현금", + assetName: "현금", amount: 1500, transactionType: "수입", transactionDetail: "몽소", @@ -46,9 +46,9 @@ function RecordPage() { }, { accountId: 3, - address: "서울특별시 은평구 가좌로 275-2", + address: "서울특별시 은평구 신사동 증산로 413", categoryName: "간식", - assetType: "현금", + assetName: "현금", amount: 1500, transactionType: "수입", transactionDetail: "신사초등학교", @@ -57,8 +57,8 @@ function RecordPage() { recurringType: "", createdAt: "2023-11-17 13:00:00", updatedAt: "", - latitude: 37.5928348327068, - longitude: 126.911983196588, + latitude: 37.5931409230026, + longitude: 126.913846721415, installment: false, transactedAt: "", }, @@ -66,7 +66,7 @@ function RecordPage() { accountId: 4, address: "서울특별시 은평구 신사동 증산로 413", categoryName: "간식", - assetType: "현금", + assetName: "현금", amount: 1500, transactionType: "지출", transactionDetail: "설렁탕", @@ -85,33 +85,42 @@ function RecordPage() { const [currentLatLng, setCurrentLatLng] = useState<{ lat: number; lng: number; - } | null>(null); - console.log("currentLatLng: ", currentLatLng); - - /** getNearbyAccountBooks로 가져온 데이터 */ - // const [nearbyAccountBooksData, setNearbyAccountBooksData] = useState< - // GetNearbyAccountBooks[] - // >([]); + }>({ lat: 0, lng: 0 }); - const [exampleData, setExampleData] = useState([]); - useEffect(() => { - setExampleData(exampleNearby); - }, []); + // const [exampleData, setExampleData] = useState([]); + // useEffect(() => { + // setExampleData(exampleNearby); + // }, []); + const [isRecordInfosVisible, setIsRecordInfosVisible] = + useState(false); const handleLocationChange = (lat: number, lng: number) => { setCurrentLatLng({ lat, lng }); }; - // const getNearbyAccountBooks = (lat: number, lng: number) => { - // AccountBookAPI.getNearbyAccountBooks(lat, lng) - // .then(response => { - // setNearbyAccountBooksData(response.data); - // console.log("기록지 수신 성공: ", response.data); - // }) - // .catch(error => { - // console.error("기록지 수신 실패:", error); - // }); - // }; + const toggleRecordInfosVisibility = () => { + setIsRecordInfosVisible(!isRecordInfosVisible); + }; + + /** getNearbyAccountBooks로 가져온 데이터 */ + const [nearbyAccountBooksData, setNearbyAccountBooksData] = useState< + GetNearbyAccountBooks[] + >([]); + + const getNearbyAccountBook = (lat: number, lng: number) => { + AccountBookAPI.getNearbyAccountBooks(lat, lng) + .then(response => { + setNearbyAccountBooksData(response.data); + console.log("기록지 수신 성공: ", response.data); + }) + .catch(error => { + console.error("기록지 수신 실패:", error); + }); + }; + + useEffect(() => { + getNearbyAccountBook(37.59305502696753, 126.91361917024199); + }, []); return ( <> @@ -123,8 +132,12 @@ function RecordPage() { isAddButton={false} isMoreButton={false} /> - - {/* */} + + ); From 6f76e2593df7c5a65ca77acdca1bd0843d0d9a22 Mon Sep 17 00:00:00 2001 From: Sojung Park Date: Mon, 27 Nov 2023 15:45:12 +0900 Subject: [PATCH 06/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20[IMPROVE]=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=EC=A7=80=20=ED=98=84=EC=9E=AC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20recoil=20=EC=82=AC=EC=9A=A9=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A1=9C=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - recoil > useCurrentLocation.ts : 현재 위치 좌표 추가 --- src/components/KakaoMap/index.tsx | 17 ++++++++++++----- src/hooks/recoil/useCurrentLocation.ts | 14 ++++++++++++++ src/pages/RecordPage/index.tsx | 26 ++++++++++---------------- 3 files changed, 36 insertions(+), 21 deletions(-) create mode 100644 src/hooks/recoil/useCurrentLocation.ts diff --git a/src/components/KakaoMap/index.tsx b/src/components/KakaoMap/index.tsx index a59efae..116c94a 100644 --- a/src/components/KakaoMap/index.tsx +++ b/src/components/KakaoMap/index.tsx @@ -5,6 +5,10 @@ import BackToCenterIcon from "@/public/icon/BackToCenter.svg"; import { GetNearbyAccountBooks } from "@/src/@types/models/getNearbyAccountBooks"; import { useRecoilState } from "recoil"; import { clickedMarkerDataAtom } from "@/src/hooks/recoil/clickedMarkerData"; +import { + currentLocationAtom, + currentLocationDataProps, +} from "@/src/hooks/recoil/useCurrentLocation"; declare global { interface Window { @@ -13,13 +17,11 @@ declare global { } interface onLocationChangeCallBack { - onLocationChange: (lat: number, lng: number) => void; data: GetNearbyAccountBooks[]; toggleRecordInfosVisibility: () => void; } function KakaoMap({ - onLocationChange, data, toggleRecordInfosVisibility, }: onLocationChangeCallBack) { @@ -37,6 +39,8 @@ function KakaoMap({ const [message, setMessage] = useState(""); const [, setClickedMarkerData] = useRecoilState(clickedMarkerDataAtom); + const [currentLatLng, setCurrentLatLng] = + useRecoilState(currentLocationAtom); // 마커 클릭 이벤트 const handleMarkerClick = (markerData: GetNearbyAccountBooks) => { @@ -55,7 +59,7 @@ function KakaoMap({ const container = mapRef.current; const options = { center: new kakao.maps.LatLng(33.450701, 126.570667), - level: 4, //100m + level: 3, }; // 지도 객체 생성 const mapContainer = new kakao.maps.Map(container, options); @@ -74,8 +78,11 @@ function KakaoMap({ const locPosition = new kakao.maps.LatLng(lat, lng); setCurrentLocation(locPosition); - onLocationChange(lat, lng); - console.log("위경도", lat, lng); + setCurrentLatLng(prev => ({ + ...prev, + lat: lat, + lng: lng, + })); }); } else { // HTML5의 GeoLocation을 사용할 수 없을 때 마커 표시 위치와 인포윈도우 내용 설정 diff --git a/src/hooks/recoil/useCurrentLocation.ts b/src/hooks/recoil/useCurrentLocation.ts new file mode 100644 index 0000000..20efbb6 --- /dev/null +++ b/src/hooks/recoil/useCurrentLocation.ts @@ -0,0 +1,14 @@ +import { atom } from "recoil"; + +export interface currentLocationDataProps { + lat: number; + lng: number; +} + +export const currentLocationAtom = atom({ + key: "currentLocationAtom", + default: { + lat: 33.450701, + lng: 126.570667, + }, +}); diff --git a/src/pages/RecordPage/index.tsx b/src/pages/RecordPage/index.tsx index 0dfa387..c794038 100644 --- a/src/pages/RecordPage/index.tsx +++ b/src/pages/RecordPage/index.tsx @@ -5,6 +5,11 @@ import KakaoMap from "@/src/components/KakaoMap"; import RecordInfos from "@/src/components/RecordInfos"; import { useEffect, useState } from "react"; import { AccountBookAPI } from "@/src/core/api/accountBook"; +import { useRecoilValue } from "recoil"; +import { + currentLocationAtom, + currentLocationDataProps, +} from "@/src/hooks/recoil/useCurrentLocation"; function RecordPage() { const exampleNearby = [ @@ -82,21 +87,10 @@ function RecordPage() { }, ]; - const [currentLatLng, setCurrentLatLng] = useState<{ - lat: number; - lng: number; - }>({ lat: 0, lng: 0 }); - - // const [exampleData, setExampleData] = useState([]); - // useEffect(() => { - // setExampleData(exampleNearby); - // }, []); const [isRecordInfosVisible, setIsRecordInfosVisible] = useState(false); - - const handleLocationChange = (lat: number, lng: number) => { - setCurrentLatLng({ lat, lng }); - }; + const currentLatLng = + useRecoilValue(currentLocationAtom); const toggleRecordInfosVisibility = () => { setIsRecordInfosVisible(!isRecordInfosVisible); @@ -118,9 +112,10 @@ function RecordPage() { }); }; + /** COMPLETED: 위도, 경도 현재 위치로 바꾸기 */ useEffect(() => { - getNearbyAccountBook(37.59305502696753, 126.91361917024199); - }, []); + getNearbyAccountBook(currentLatLng.lat, currentLatLng.lng); + }, [currentLatLng]); return ( <> @@ -133,7 +128,6 @@ function RecordPage() { isMoreButton={false} /> From 19a52d68be48ff33e93cb527c33665e0a4504767 Mon Sep 17 00:00:00 2001 From: Sojung Park Date: Thu, 30 Nov 2023 22:12:39 +0900 Subject: [PATCH 07/29] =?UTF-8?q?=E2=9C=A8=20[FEATURE]=20=ED=86=B5?= =?UTF-8?q?=EA=B3=84=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=99=B8=207?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CalendarModal : 상태에 따라 화면 렌더링 조건 추가 및 css 수정 - StatisticsTabs : 코드 수정 및 style.ts 삭제 - StatisticsHeader : 통계 Header 컴포넌트 생성 - StatisticsYearMonthOptions : 통계 월간/연간 버튼 구분 컴포넌트 생성 - calendarModalState.ts : CalendarModal 컴포넌트 표시 상태 관리 생성 - useCalendarDate.ts : date 옵셔널 체이닝 추가 - App.tsx : StatisticsPage 라우트 경로 "/statistics"로 수정 --- public/icon/DownArrow.svg | 4 + public/icon/LeftChevron.svg | 4 + public/icon/RightChevron.svg | 4 + src/App.tsx | 2 +- src/components/Common/CalendarModal/index.tsx | 162 ++++++++++-------- src/components/Common/CalendarModal/style.ts | 8 +- .../Common/StatisticsTabs/index.tsx | 14 +- src/components/Common/StatisticsTabs/style.ts | 1 - src/components/StatisticsHeader/index.tsx | 71 ++++++++ src/components/StatisticsHeader/style.ts | 66 +++++++ .../StatisticsYearMonthOptions/index.tsx | 9 + src/hooks/recoil/calendarModalState.ts | 6 + src/hooks/recoil/useCalendarDate.ts | 2 +- src/pages/StatisticPage/index.tsx | 6 +- 14 files changed, 273 insertions(+), 86 deletions(-) create mode 100644 public/icon/DownArrow.svg create mode 100644 public/icon/LeftChevron.svg create mode 100644 public/icon/RightChevron.svg delete mode 100644 src/components/Common/StatisticsTabs/style.ts create mode 100644 src/components/StatisticsHeader/index.tsx create mode 100644 src/components/StatisticsHeader/style.ts create mode 100644 src/components/StatisticsYearMonthOptions/index.tsx create mode 100644 src/hooks/recoil/calendarModalState.ts diff --git a/public/icon/DownArrow.svg b/public/icon/DownArrow.svg new file mode 100644 index 0000000..cd6daa9 --- /dev/null +++ b/public/icon/DownArrow.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/icon/LeftChevron.svg b/public/icon/LeftChevron.svg new file mode 100644 index 0000000..9fae8ea --- /dev/null +++ b/public/icon/LeftChevron.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/icon/RightChevron.svg b/public/icon/RightChevron.svg new file mode 100644 index 0000000..496c099 --- /dev/null +++ b/public/icon/RightChevron.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index f89bce4..b7132b7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -53,7 +53,7 @@ function App() { path="/challenge/create" element={} /> - } /> + } /> } /> } /> } /> diff --git a/src/components/Common/CalendarModal/index.tsx b/src/components/Common/CalendarModal/index.tsx index 5ba591a..54076c3 100644 --- a/src/components/Common/CalendarModal/index.tsx +++ b/src/components/Common/CalendarModal/index.tsx @@ -6,87 +6,103 @@ import { CalendarModalUI } from "./style"; // import dayjs from "dayjs"; import "dayjs/locale/ko"; import { koKR } from "@mui/x-date-pickers/locales"; +import { useRecoilState } from "recoil"; +import { modalOpenStateAtom } from "@/src/hooks/recoil/calendarModalState"; function CalendarModal() { + const [isCalendarModalOpen, setIsCalendarModalOpen] = + useRecoilState(modalOpenStateAtom); + + const handleModalOutsideClick = () => { + setIsCalendarModalOpen(false); + }; + return ( - - - - - + {isCalendarModalOpen && ( + + ) => + e.stopPropagation() + }> + + + - - - - + }} + /> + + + + + )} + ); } diff --git a/src/components/Common/CalendarModal/style.ts b/src/components/Common/CalendarModal/style.ts index 51be75c..c3fedd6 100644 --- a/src/components/Common/CalendarModal/style.ts +++ b/src/components/Common/CalendarModal/style.ts @@ -1,26 +1,28 @@ import styled from "@emotion/styled"; import { theme } from "../../../assets/theme"; +import { NavigationItemsHeight } from "@/src/assets/height"; const modalWidth = "100% - 80px"; const Backgorund = styled.div` width: 100%; - height: 744px; + height: calc(100% - ${NavigationItemsHeight}px); background-color: rgba(73, 80, 87, 0.74); position: absolute; top: 0; + left: 0; display: flex; justify-content: center; align-items: center; + z-index: 1; `; const ModalContainer = styled.div` width: calc(${modalWidth}); height: 190px; ${theme.border_radius}; - border: 1px solid; background-color: ${theme.font_color.white}; display: flex; justify-content: center; @@ -29,7 +31,7 @@ const ModalContainer = styled.div` position: absolute; top: 35%; - z-index: 1; + z-index: 3; `; export const CalendarModalUI = { Backgorund, ModalContainer } as const; diff --git a/src/components/Common/StatisticsTabs/index.tsx b/src/components/Common/StatisticsTabs/index.tsx index e48ca08..eb8314f 100644 --- a/src/components/Common/StatisticsTabs/index.tsx +++ b/src/components/Common/StatisticsTabs/index.tsx @@ -47,8 +47,7 @@ export default function StatisticsTabs() { }; const tabNames = ["수입", "지출", "합계"]; - - const tabWidth = "(100% - (20px * 2)) / 3"; // 100%를 390px로 지정하면 정확하게 3등분 됨 + const tabWidth = "(100% - (20px * 2)) / 3"; const StyledTab = styled(Tab)({ "&.Mui-selected": { @@ -61,7 +60,7 @@ export default function StatisticsTabs() { {tabNames.map((name, index) => ( { + let newYear = getYearMonthDate.year; + let newMonth = getYearMonthDate.month; + + newMonth += plusMinusOne; + + if (newMonth === 0) { + newYear += plusMinusOne; + newMonth = 12; + } else if (newMonth === 13) { + newYear += plusMinusOne; + newMonth = 1; + } + + setGetYearMonthDate({ + year: newYear, + month: newMonth, + }); + }; + + const handleYearMonthClick = () => { + console.log("YearMonth Clicked"); + setIsCalendarModalOpen(true); + }; + + return ( + + + + 왼쪽 방향 changeMonth(-1)} + /> + + + {`${getYearMonthDate.year}년 ${getYearMonthDate.month}월`} + + {isCalendarModalOpen && } + + 오른쪽 방향 changeMonth(+1)} + /> + + + + 월별 +  + + + ); +} + +export default StatisticsHeader; diff --git a/src/components/StatisticsHeader/style.ts b/src/components/StatisticsHeader/style.ts new file mode 100644 index 0000000..46e5073 --- /dev/null +++ b/src/components/StatisticsHeader/style.ts @@ -0,0 +1,66 @@ +import styled from "@emotion/styled"; +import { theme } from "@/src/assets/theme"; + +const HeaderContainer = styled.div` + width: 100%; + height: 70px; + background-color: ${theme.font_color.gray1}; + padding: 20px 20px; + display: flex; + justify-content: space-between; + align-items: center; +`; + +const YearMonthContainer = styled.div` + display: flex; + align-items: center; + height: 35px; + + img { + width: 20px; + height: 20xpx; + } +`; + +const LeftButton = styled.button` + width: 25px; + height: 25px; +`; + +const YearMonth = styled.button` + ${theme.font_style.regular_medium}; + color: ${theme.font_color.black}; + margin: 0 15px; +`; + +const RightButton = styled.button` + width: 25px; + height: 25px; +`; + +const YearMonthButton = styled.button` + display: flex; + align-items: center; + width: 66px; + height: 28px; + padding: 0 10px; + ${theme.border_radius}; + border: 1px solid ${theme.font_color.gray3}; + background-color: ${theme.font_color.white}; + color: ${theme.font_color.black}; + ${theme.font_style.regular_small}; + + img { + width: 18px; + height: 18px; + } +`; + +export const StatisticsHeaderUI = { + HeaderContainer, + YearMonthContainer, + LeftButton, + YearMonth, + RightButton, + YearMonthButton, +} as const; diff --git a/src/components/StatisticsYearMonthOptions/index.tsx b/src/components/StatisticsYearMonthOptions/index.tsx new file mode 100644 index 0000000..b01ea7c --- /dev/null +++ b/src/components/StatisticsYearMonthOptions/index.tsx @@ -0,0 +1,9 @@ +function StatisticsYearMonthOptions() { + return ( +
+
+
+ ); +} + +export default StatisticsYearMonthOptions; diff --git a/src/hooks/recoil/calendarModalState.ts b/src/hooks/recoil/calendarModalState.ts new file mode 100644 index 0000000..ee3b4fc --- /dev/null +++ b/src/hooks/recoil/calendarModalState.ts @@ -0,0 +1,6 @@ +import { atom } from "recoil"; + +export const modalOpenStateAtom = atom({ + key: "modalOpenStateAtom", + default: false, +}); diff --git a/src/hooks/recoil/useCalendarDate.ts b/src/hooks/recoil/useCalendarDate.ts index 71191a1..316ad04 100644 --- a/src/hooks/recoil/useCalendarDate.ts +++ b/src/hooks/recoil/useCalendarDate.ts @@ -7,7 +7,7 @@ const currentDate = new Date().getDate(); export interface CalendarDateAtomProps { year: number; month: number; - date: number; + date?: number; } export const calendarDateAtom = atom({ diff --git a/src/pages/StatisticPage/index.tsx b/src/pages/StatisticPage/index.tsx index 75a6697..a0b053e 100644 --- a/src/pages/StatisticPage/index.tsx +++ b/src/pages/StatisticPage/index.tsx @@ -1,13 +1,15 @@ // import StatisticsItems from "@/src/components/Common/StatisticsItems"; +import StatisticsHeader from "@/src/components/StatisticsHeader"; import StatisticsTabs from "@/src/components/Common/StatisticsTabs"; -function StatisticPage() { +function StatisticsPage() { return ( <> + {/* */} ); } -export default StatisticPage; +export default StatisticsPage; From d094fc747fe5ba4df6384785cd60ecaedeced1bf Mon Sep 17 00:00:00 2001 From: Whale2200d Date: Thu, 7 Dec 2023 18:44:08 +0900 Subject: [PATCH 08/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20[IMPROVE]=20=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EB=A1=9C=EC=A7=81=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 2 +- src/assets/util.ts | 4 +- src/components/Chatting/index.tsx | 6 +- src/components/Common/Input/index.tsx | 9 +- src/components/Common/Input/style.ts | 2 +- .../Common/SeparatedCategory/index.tsx | 1 + src/components/InputArea/index.tsx | 14 +-- src/components/Login/index.tsx | 85 ++++++++++++++++--- src/components/Login/style.ts | 7 ++ src/components/PasswordReset/index.tsx | 24 ++++-- src/components/PasswordReset/style.ts | 2 +- src/components/SignUp/index.tsx | 42 +++++---- 12 files changed, 145 insertions(+), 53 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b7132b7..180f809 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,6 +24,7 @@ function App() { return ( + {/* COMPLETED: NavigationItems 컴포넌트 조건에 따라 렌더링 */} {useRoutes([ { path: "/", element: }, { path: "/signUp", element: }, @@ -34,7 +35,6 @@ function App() { element: ( <> - {/* COMPLETED: 로그인하여, Token이 주어졌을 때 */} } /> { }; /** COMPLETED: 이메일 유효성 검사하기 */ -export const isValidEmail = (email: string): boolean => { +export const isEmailValid = (email: string): boolean => { /** 이메일 형식을 정의한 정규표현식 */ - const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; + const emailRegex = /^\S+@\S+\.\S+$/; // 정규표현식을 사용하여 이메일 형식을 검증 return emailRegex.test(email); diff --git a/src/components/Chatting/index.tsx b/src/components/Chatting/index.tsx index 065d674..e1d3d87 100644 --- a/src/components/Chatting/index.tsx +++ b/src/components/Chatting/index.tsx @@ -1,12 +1,10 @@ import SendPNG from "@/public/icon/Send.png"; import BarGraphItemDetailsTopContainer from "../BarGraphItemDetailsTopContainer"; import { ChattingUI } from "./style"; -// import { ChangeTime } from "@/src/assets/util"; import ChattingContent from "../ChattingContent"; import { API_WEBSOCKET_URL } from "@/src/core/api/instance"; import { useEffect, useState } from "react"; import Stomp from "stompjs"; -import { ChangeTime } from "@/src/assets/util"; export interface Message { groupId: number; @@ -71,7 +69,7 @@ function Chatting() { console.log("Connected to WebSocket"); /* COMPLETED: 채팅 메시지를 구독하기 */ - stomp.subscribe(`/chat/groups/3`, onReceiveMessage); + stomp.subscribe(`/chat/groups/4`, onReceiveMessage); }); }; @@ -106,7 +104,7 @@ function Chatting() { "/app/send", {}, JSON.stringify({ - groupId: 3, + groupId: 4, message: message, messageType: "TALK", }) diff --git a/src/components/Common/Input/index.tsx b/src/components/Common/Input/index.tsx index 0aa7b0e..0b493d1 100644 --- a/src/components/Common/Input/index.tsx +++ b/src/components/Common/Input/index.tsx @@ -2,10 +2,10 @@ import { ChangeEvent } from "react"; import { InputUI } from "./style"; export interface InputProps { - type: "text" | "password" | "number"; + type: "text" | "password" | "number" | "email"; value?: string | number; placeholder: string; - handleChange?: (e: ChangeEvent) => void; + onChange?: (e: ChangeEvent) => void; handleBlur?: () => void; content?: string; isOpenButton?: boolean; @@ -16,7 +16,7 @@ function Input({ type, value, placeholder, - handleChange, + onChange, handleBlur, content, isOpenButton, @@ -28,8 +28,9 @@ function Input({ type={type} value={value} placeholder={placeholder} - onChange={handleChange} + onChange={onChange} onBlur={handleBlur} + size={100} /> {isOpenButton && ( diff --git a/src/components/Common/Input/style.ts b/src/components/Common/Input/style.ts index d712e10..b881bb3 100644 --- a/src/components/Common/Input/style.ts +++ b/src/components/Common/Input/style.ts @@ -2,7 +2,7 @@ import styled from "@emotion/styled"; import { theme } from "../../../assets/theme"; import { Button } from "@mui/material"; -const deleteMarginLength = "100% - 64px"; +export const deleteMarginLength = "100% - 64px"; const Input = styled.input` width: calc(${deleteMarginLength}); diff --git a/src/components/Common/SeparatedCategory/index.tsx b/src/components/Common/SeparatedCategory/index.tsx index 45c630e..e0b0dd9 100644 --- a/src/components/Common/SeparatedCategory/index.tsx +++ b/src/components/Common/SeparatedCategory/index.tsx @@ -42,6 +42,7 @@ function SeparatedCategory({ title }: SeparatedCategoryProps) { categoryName: item.categoryName, })); }; + return (
diff --git a/src/components/InputArea/index.tsx b/src/components/InputArea/index.tsx index 8d5d042..4ef76a4 100644 --- a/src/components/InputArea/index.tsx +++ b/src/components/InputArea/index.tsx @@ -1,16 +1,18 @@ -import { ChangeEvent, useState } from "react"; +import { ChangeEvent } from "react"; import LabelInput from "../Common/LabelInput"; import { InputAreaUI } from "./style"; import SelectedImage from "../SelectedImage"; import SeparatedCategory from "../Common/SeparatedCategory"; import { useRecoilState } from "recoil"; import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; +import { openSeparatedCategoryAtom } from "@/src/hooks/recoil/useOpenSeparatedCategory"; function InputArea() { const [postSaveAccountBook, setPostSaveAccountBook] = useRecoilState(saveAccountBookAtom); - const [showSeparatedCategory, setShowSeparatedCategory] = - useState(false); + const [isOpenSeparatedCategory, setIsOpenSeparatedCategory] = useRecoilState( + openSeparatedCategoryAtom + ); /** COMPLETED: recoil saveAccountBookAtom에 값 추가하기 */ const handleDate = (e: ChangeEvent) => { @@ -98,7 +100,7 @@ function InputArea() { inputId={"categoryName"} value={postSaveAccountBook.categoryName} placeholder={"카테고리를 선택해주세요."} - onClick={() => setShowSeparatedCategory(!showSeparatedCategory)} + onClick={() => setIsOpenSeparatedCategory({ isOpen: true })} onChange={handleCategoryName} readonly={true} /> @@ -108,7 +110,7 @@ function InputArea() { inputId={"assetName"} value={postSaveAccountBook.assetName} placeholder={"결제 수단을 선택해주세요."} - onClick={() => setShowSeparatedCategory(!showSeparatedCategory)} + onClick={() => setIsOpenSeparatedCategory({ isOpen: true })} onChange={handleAssetName} readonly={true} /> @@ -129,7 +131,7 @@ function InputArea() { onChange={handleAddress} />
- {showSeparatedCategory && } + {isOpenSeparatedCategory.isOpen && } { /** COMPLETED: 1. 서버로부터 set-Cookie를 통해 JWT 토큰 받기 */ - const response = memberAPI.signInWithEmail( - "skatewang@naver.com", - "12345678" + const response = await memberAPI.signInWithEmail( + login.email, + login.password ); - console.log("response: "); if ((await response).status === 200) { /** COMPLETED: 2. 메인 페이지(가계부)로 이동하기 */ navigate("/account"); } else if ((await response).status === 400) { - navigate("/signup"); + navigate("/signUp"); + } + }; + + const handleSignUp = () => { + navigate("/signUp"); + }; + const handlePasswordReset = () => { + navigate("/passwordReset"); + }; + + const handleEmailInput = (e: ChangeEvent) => { + const { value } = e.target; + + setLogin(prev => ({ + ...prev, + email: value, + })); + + if (!isEmailValid(value)) { + setEmailError("이메일 양식이 올바르지 않습니다. 다시 확인해주세요."); + } else { + setEmailError(""); + } + }; + + const handlePasswordInput = (e: ChangeEvent) => { + const { value } = e.target; + + setLogin(prev => ({ + ...prev, + password: value, + })); + + if (!isPasswordValid(value)) { + setPasswordError("비밀번호 양식이 올바르지 않습니다. 다시 확인해주세요."); + } else { + setPasswordError(""); } }; @@ -28,22 +72,39 @@ function Login() { 로그인
- {/*
*/} - - + + {emailError && ( + + {emailError} + + )} + + {passwordError && ( + + {passwordError} + + )} - {/* */}
- - + + 또는 - {/* */} { - console.log("exampleFunction"); + const [email, setEmail] = useState(""); + const navigate = useNavigate(); + const handlePasswordReset = () => { + memberAPI.sendResetPasswordLink(email).then().catch(); }; return ( 비밀번호 재설정 -
- +
+ setEmail(e.target.value)} + /> - +
); } diff --git a/src/components/PasswordReset/style.ts b/src/components/PasswordReset/style.ts index 1c85b0d..ce4d932 100644 --- a/src/components/PasswordReset/style.ts +++ b/src/components/PasswordReset/style.ts @@ -8,7 +8,7 @@ const SectionContainer = styled.section` justify-content: center; align-items: center; - > form { + > div { width: 100%; > * { display: block; diff --git a/src/components/SignUp/index.tsx b/src/components/SignUp/index.tsx index 36c87e5..894aabe 100644 --- a/src/components/SignUp/index.tsx +++ b/src/components/SignUp/index.tsx @@ -3,7 +3,7 @@ import Input from "../Common/Input"; import LongButton from "../Common/LongButton"; import { SignUpUI } from "./style"; import { memberAPI } from "@/src/core/api/member"; -import { isValidEmail } from "@/src/assets/util"; +import { isEmailValid, isPasswordValid } from "@/src/assets/util"; import { useNavigate } from "react-router-dom"; interface SignUpObject { @@ -37,18 +37,18 @@ function SignUp() { const handleChangeEmail = (e: ChangeEvent) => { const email = e.target.value; - if (!isValidEmail(email)) { + setSignUpObject(prev => ({ + ...prev, + email: email, + })); + + if (!isEmailValid(email)) { setErrorMessage( "유효하지 않은 이메일 형식입니다. 올바른 이메일을 입력하세요." ); } else { setErrorMessage(""); } - - setSignUpObject(prev => ({ - ...prev, - email: email, - })); }; const handleChangeCheck = (e: ChangeEvent) => { const { value } = e.target; @@ -59,12 +59,20 @@ function SignUp() { })); }; const handleChangePassword = (e: ChangeEvent) => { - const { value } = e.target; + const password = e.target.value; setSignUpObject(prev => ({ ...prev, - password: value, + password: password, })); + + if (!isPasswordValid(password)) { + setErrorMessage( + "유효하지 않은 비밀번호 형식입니다. 올바른 이메일을 입력하세요." + ); + } else { + setErrorMessage(""); + } }; const handleChangePasswordCheck = (e: ChangeEvent) => { const { value } = e.target; @@ -144,6 +152,7 @@ function SignUp() { !signUpObject.password || !signUpObject.passwordCheck ) { + console.log("1"); setErrorMessage("* 빈 곳이 존재합니다. 빈 곳을 채워주세요."); return; } @@ -152,6 +161,7 @@ function SignUp() { /* COMPLETED: 비밀번호가 같은지 확인 */ signUpObject.password !== signUpObject.passwordCheck ) { + console.log("2"); setErrorMessage("* 비밀번호가 비밀번호 확인과 다릅니다."); return; } @@ -164,10 +174,12 @@ function SignUp() { ); if ((await response).status === 200) { + console.log("3"); setErrorMessage(""); - navigate("/login"); + navigate("/"); } } catch (error) { + console.log("4"); console.log("Submit error: ", error); setErrorMessage( "회원가입 전송에 실패했습니다. 확인 후 다시 시도해주세요." @@ -183,7 +195,7 @@ function SignUp() { type={"text"} value={signUpObject.name} placeholder={"이름"} - handleChange={handleChangeName} + onChange={handleChangeName} // handleBlur={handleInputBlur} /> {openCheckInput && ( @@ -204,7 +216,7 @@ function SignUp() { content={"확인"} isOpenButton={true} onClickButton={handleCheckEmail} - handleChange={handleChangeCheck} + onChange={handleChangeCheck} // handleBlur={handleInputBlur} /> )} @@ -212,14 +224,14 @@ function SignUp() { type={"password"} value={signUpObject.password} placeholder={"비밀번호"} - handleChange={handleChangePassword} + onChange={handleChangePassword} // handleBlur={handleInputBlur} /> From ab3458fc19b911ad1f15bda57ece000278337132 Mon Sep 17 00:00:00 2001 From: Whale2200d Date: Fri, 8 Dec 2023 22:11:59 +0900 Subject: [PATCH 09/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20[IMPROVE]=20=20Chall?= =?UTF-8?q?engePage=20=EC=B4=88=EC=95=88=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - (기타) PasswordResetPage 내 취소버튼 추가 - ChallengePage 내 서버와 연결 및 화면에 렌더링 로직 추가 --- src/@types/models/JoinChallengeGroup.ts | 12 ++ src/@types/models/challengeMember.ts | 15 +++ src/@types/models/createChallengeGroup.ts | 16 +++ src/@types/models/createInviteLink.ts | 7 ++ src/@types/models/deleteChallengeGroup.ts | 6 + src/@types/models/getChallengeGroups.ts | 28 +++++ src/@types/models/messageTypeSymbol.ts | 3 + src/@types/models/saveMoney.ts | 8 ++ src/@types/models/updateChallengeGroup.ts | 16 +++ src/App.tsx | 10 +- src/components/BarGraphItem/index.tsx | 45 ++++--- src/components/BarGraphItem/style.ts | 6 +- src/components/BarGraphItemDetails/index.tsx | 65 ++-------- src/components/BarGraphItemDetails/style.ts | 2 + .../BarGraphItemDetailsTopContainer/index.tsx | 14 ++- src/components/BarGraphList/index.tsx | 116 +++++------------- src/components/BarGraphList/style.ts | 2 +- src/components/Chatting/index.tsx | 47 +------ .../Common/BudgetAccountBarGraph/style.ts | 1 + src/components/Common/Header/index.tsx | 4 +- src/components/Common/LongButton/index.tsx | 17 ++- src/components/CreateChallengeGroup/index.tsx | 39 +++--- src/components/CreateChallengeGroup/style.ts | 5 + src/components/PasswordReset/index.tsx | 12 +- src/core/api/challengeGroup.ts | 30 +++-- src/hooks/recoil/useGetChallengeGroup.ts | 22 ++++ src/hooks/recoil/useGetChallengeGroups.ts | 7 ++ src/pages/ChallengeDetailPage/index.tsx | 5 +- src/pages/CreateChallengeGroupPage/index.tsx | 4 +- 29 files changed, 314 insertions(+), 250 deletions(-) create mode 100644 src/@types/models/JoinChallengeGroup.ts create mode 100644 src/@types/models/challengeMember.ts create mode 100644 src/@types/models/createChallengeGroup.ts create mode 100644 src/@types/models/createInviteLink.ts create mode 100644 src/@types/models/deleteChallengeGroup.ts create mode 100644 src/@types/models/getChallengeGroups.ts create mode 100644 src/@types/models/messageTypeSymbol.ts create mode 100644 src/@types/models/saveMoney.ts create mode 100644 src/@types/models/updateChallengeGroup.ts create mode 100644 src/hooks/recoil/useGetChallengeGroup.ts create mode 100644 src/hooks/recoil/useGetChallengeGroups.ts diff --git a/src/@types/models/JoinChallengeGroup.ts b/src/@types/models/JoinChallengeGroup.ts new file mode 100644 index 0000000..2c83dda --- /dev/null +++ b/src/@types/models/JoinChallengeGroup.ts @@ -0,0 +1,12 @@ +/** + * 챌린지 그룹 조인 생성 Response Interface + */ +export type JoinChallengeGroup = { + id: number; + groupName: string; + description: string; + targetAmount: number; + maxMembers: number; + startAt: string; + endAt: string; +}; diff --git a/src/@types/models/challengeMember.ts b/src/@types/models/challengeMember.ts new file mode 100644 index 0000000..51dd1d1 --- /dev/null +++ b/src/@types/models/challengeMember.ts @@ -0,0 +1,15 @@ +import { MessageTypeSymbol } from "./messageTypeSymbol"; + +/** + * 챌린지 메시지 조회 Response Interface + */ +export type GetMessages = GetMessage[]; + +export type GetMessage = { + id: number; + groupId: number; + senderId: number; + message: string; + messageType: MessageTypeSymbol; + createdAt: string; +}; diff --git a/src/@types/models/createChallengeGroup.ts b/src/@types/models/createChallengeGroup.ts new file mode 100644 index 0000000..3950b84 --- /dev/null +++ b/src/@types/models/createChallengeGroup.ts @@ -0,0 +1,16 @@ +/** + * 챌린지 그룹 생성 Response Interface + */ +export type CreateChallengeGroup = { + id: number; + name: string; + description: string; + targetAmount: number; + maxMembers: number; + startAt: string; + endAt: string; + adminId: number; + viewerId: null; + viewerName: null; + viewerEmail: null; +}; diff --git a/src/@types/models/createInviteLink.ts b/src/@types/models/createInviteLink.ts new file mode 100644 index 0000000..b2219ca --- /dev/null +++ b/src/@types/models/createInviteLink.ts @@ -0,0 +1,7 @@ +/** + * 챌린지 그룹 초대 링크 생성 Response Interface + */ +export type CreateInviteLink = { + groupId: number; + inviteLink: string; +}; diff --git a/src/@types/models/deleteChallengeGroup.ts b/src/@types/models/deleteChallengeGroup.ts new file mode 100644 index 0000000..7fac9ed --- /dev/null +++ b/src/@types/models/deleteChallengeGroup.ts @@ -0,0 +1,6 @@ +/** + * 챌린지 그룹 삭제 Response Interface + */ +export type DeleteChallengeGroup = { + id: number; +}; diff --git a/src/@types/models/getChallengeGroups.ts b/src/@types/models/getChallengeGroups.ts new file mode 100644 index 0000000..3433253 --- /dev/null +++ b/src/@types/models/getChallengeGroups.ts @@ -0,0 +1,28 @@ +/** + * 챌린지 그룹 조회 Response Interface + */ +export type GetChallengeGroups = GetChallengeGroup[]; + +export type GetChallengeGroup = { + id: number; + name: string; + description: string; + targetAmount: number; + maxMembers: number; + currentMembers: number; // 추가 + startAt: string; + endAt: string; + inviteLink: string; // 추가 + adminId: number; + viewerId: number; + viewerName: string; + viewerEmail: string; + groupMembers: GroupMember[]; // 추가 +}; + +export type GroupMember = { + memberId: number; + memberEmail: string; + memberName: string; + totalSavingAmount: number; +}; diff --git a/src/@types/models/messageTypeSymbol.ts b/src/@types/models/messageTypeSymbol.ts new file mode 100644 index 0000000..997436c --- /dev/null +++ b/src/@types/models/messageTypeSymbol.ts @@ -0,0 +1,3 @@ +type MessageTypeSymbol = "TALK" | "LEAVE" | "ENTER"; + +export type { MessageTypeSymbol }; diff --git a/src/@types/models/saveMoney.ts b/src/@types/models/saveMoney.ts new file mode 100644 index 0000000..91e5d38 --- /dev/null +++ b/src/@types/models/saveMoney.ts @@ -0,0 +1,8 @@ +/** + * 챌린지 저축 금액 생성 Response Interface + */ +export type SaveMoney = { + groupName: string; + memberName: string; + savedAmount: number; +}; diff --git a/src/@types/models/updateChallengeGroup.ts b/src/@types/models/updateChallengeGroup.ts new file mode 100644 index 0000000..ae9d303 --- /dev/null +++ b/src/@types/models/updateChallengeGroup.ts @@ -0,0 +1,16 @@ +/** + * 챌린지 그룹 수정 Response Interface + */ +export type UpdateChallengeGroup = { + id: number; + name: string; + description: string; + targetAmount: number; + maxMembers: number; + startAt: string; + endAt: string; + adminId: number; + viewerId: null; + viewerName: null; + viewerEmail: null; +}; diff --git a/src/App.tsx b/src/App.tsx index 180f809..c053313 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,6 +21,8 @@ function App() { const isSignUpPage = window.location.pathname === "/signup"; const isPasswordReset = window.location.pathname === "/passwordreset"; const isRecordAccountBook = window.location.pathname === "/recordAccountBook"; + const isCreateChallengeGroups = + window.location.pathname === "/challenge/create"; return ( @@ -30,6 +32,7 @@ function App() { { path: "/signUp", element: }, { path: "/passwordReset", element: }, { path: "/recordAccountBook/recurring", element: }, + { path: "/challenge/create", element: }, { path: "/*", element: ( @@ -49,10 +52,6 @@ function App() { path="/challenge/:slug" element={} /> - } - /> } /> } /> } /> @@ -61,7 +60,8 @@ function App() { {!isLoginPage && !isSignUpPage && !isPasswordReset && - !isRecordAccountBook && } + !isRecordAccountBook && + !isCreateChallengeGroups && } ), }, diff --git a/src/components/BarGraphItem/index.tsx b/src/components/BarGraphItem/index.tsx index 93acd2c..8dc868c 100644 --- a/src/components/BarGraphItem/index.tsx +++ b/src/components/BarGraphItem/index.tsx @@ -1,10 +1,12 @@ +import { GetChallengeGroup } from "@/src/@types/models/getChallengeGroups"; import BudgetAccountBarGraph from "../Common/BudgetAccountBarGraph"; -import { ChallengeData } from "../BarGraphList"; import { BarGraphItemUI } from "./style"; import { ChangeChallengePeriod } from "@/src/assets/util"; +import { useRecoilState } from "recoil"; +import { getChallengeGroupAtom } from "@/src/hooks/recoil/useGetChallengeGroup"; interface BarGraphItemProps { - item: ChallengeData; + item: GetChallengeGroup; path: string; } @@ -14,23 +16,30 @@ function BarGraphItem({ item, path }: BarGraphItemProps) { deposit = deposit + item.groupMembers[i].totalSavingAmount; } + const [, setChallengeGroup] = useRecoilState(getChallengeGroupAtom); + const handleItemLink = () => { + setChallengeGroup(item); + }; + return ( - -
- {item.groupName} - - {`${item.currentMembers}명 ${ChangeChallengePeriod( - item.startedAt, - item.finishedAt - )}`} - -
- -
+ + +
+ {item.name} + + {`${item.currentMembers}명 ${ChangeChallengePeriod( + item.startAt, + item.endAt + )}`} + +
+ +
+
); } diff --git a/src/components/BarGraphItem/style.ts b/src/components/BarGraphItem/style.ts index 4453595..851a50e 100644 --- a/src/components/BarGraphItem/style.ts +++ b/src/components/BarGraphItem/style.ts @@ -2,10 +2,13 @@ import { theme } from "@/src/assets/theme"; import styled from "@emotion/styled"; import { Link } from "react-router-dom"; +const Item = styled.li` + background-color: ${theme.font_color.white}; +`; + const ItemLink = styled(Link)` ${theme.font_style.light_medium}; color: ${theme.font_color.black}; - background-color: ${theme.font_color.white}; border-radius: 0; padding: 0; @@ -28,6 +31,7 @@ const ItemLink = styled(Link)` const ChallengePeriod = styled.div``; export const BarGraphItemUI = { + Item, ItemLink, ChallengePeriod, }; diff --git a/src/components/BarGraphItemDetails/index.tsx b/src/components/BarGraphItemDetails/index.tsx index a95072e..6f40f1f 100644 --- a/src/components/BarGraphItemDetails/index.tsx +++ b/src/components/BarGraphItemDetails/index.tsx @@ -1,73 +1,22 @@ import BudgetAccountBarGraph from "../Common/BudgetAccountBarGraph"; import { BarGraphItemDetailsUI } from "./style"; import BarGraphItemDetailsTopContainer from "../BarGraphItemDetailsTopContainer"; - -interface groupMembersData { - memberId: number; - totalSavingAmount: number; -} - -export interface array1Data { - groupId: number; - groupName: string; - targetAmount: number; - maxMembers: number; - currentMembers: number; - startedAt: string; - finishedAt: string; - createdAt: string; - inviteToken: string; - groupMembers: groupMembersData[]; -} +import { useRecoilState } from "recoil"; +import { getChallengeGroupAtom } from "@/src/hooks/recoil/useGetChallengeGroup"; function BarGraphItemDetails() { - const array1 = { - groupId: 2, - groupName: "testGroup2", - targetAmount: 200000, - maxMembers: 5, - currentMembers: 3, - startedAt: "2023-10-16", - finishedAt: "2023-12-25", - createdAt: "2023-01-01T12:00:00", - inviteToken: "TokenToInvite", - groupMembers: [ - { - memberId: 1, - totalSavingAmount: 1000000, - }, - { - memberId: 2, - totalSavingAmount: 50000, - }, - { - memberId: 3, - totalSavingAmount: 300000, - }, - { - memberId: 3, - totalSavingAmount: 300000, - }, - { - memberId: 3, - totalSavingAmount: 300000, - }, - { - memberId: 3, - totalSavingAmount: 300000, - }, - ], - }; + const [challengeGroup] = useRecoilState(getChallengeGroupAtom); return (
- + - {array1.groupMembers.map((member) => { + {challengeGroup.groupMembers.map((member, id) => { return ( ); diff --git a/src/components/BarGraphItemDetails/style.ts b/src/components/BarGraphItemDetails/style.ts index 6e62c28..af0ebdb 100644 --- a/src/components/BarGraphItemDetails/style.ts +++ b/src/components/BarGraphItemDetails/style.ts @@ -1,8 +1,10 @@ import styled from "@emotion/styled"; import { ChallengeBottomListHeight } from "@/src/assets/height"; +import { theme } from "@/src/assets/theme"; const BottomList = styled.ul` height: calc(${ChallengeBottomListHeight}px); + background-color: ${theme.font_color.gray1}; overflow: scroll; > div { padding: 20px 0; diff --git a/src/components/BarGraphItemDetailsTopContainer/index.tsx b/src/components/BarGraphItemDetailsTopContainer/index.tsx index 683b459..8ed11c9 100644 --- a/src/components/BarGraphItemDetailsTopContainer/index.tsx +++ b/src/components/BarGraphItemDetailsTopContainer/index.tsx @@ -1,24 +1,26 @@ import { ChangeChallengePeriod } from "@/src/assets/util"; -import { array1Data } from "../BarGraphItemDetails"; import { BarGraphItemDetailsTopContainerUI } from "./style"; +import { GetChallengeGroup } from "@/src/@types/models/getChallengeGroups"; interface BarGraphItemDetailsTopContainerProps { - array1: array1Data; + challengeGroup: GetChallengeGroup; } function BarGraphItemDetailsTopContainer({ - array1, + challengeGroup, }: BarGraphItemDetailsTopContainerProps) { return (
- {array1.groupName} - {array1.currentMembers}명 + {challengeGroup.name} + {challengeGroup.currentMembers}명 저금하기
-
{ChangeChallengePeriod(array1.startedAt, array1.finishedAt)}
+
+ {ChangeChallengePeriod(challengeGroup.startAt, challengeGroup.endAt)} +
); } diff --git a/src/components/BarGraphList/index.tsx b/src/components/BarGraphList/index.tsx index 64cbd33..5bbc54c 100644 --- a/src/components/BarGraphList/index.tsx +++ b/src/components/BarGraphList/index.tsx @@ -1,92 +1,42 @@ +import { challengeGroupAPI } from "@/src/core/api/challengeGroup"; import BarGraphItem from "../BarGraphItem"; import { BarGraphListUI } from "./style"; - -export interface ChallengeData { - groupId: number; - groupName: string; - targetAmount: number; - maxMembers: number; - currentMembers: number; - startedAt: string; - finishedAt: string; - createdAt: string; - inviteToken: string; - groupMembers: [ - { - memberId: number; - totalSavingAmount: number; - }, - { - memberId: number; - totalSavingAmount: number; - }, - { - memberId: number; - totalSavingAmount: number; - } - ]; -} +import { useEffect } from "react"; +import { useRecoilState } from "recoil"; +import { getChallengeGroupsAtom } from "@/src/hooks/recoil/useGetChallengeGroups"; function BarGraphList() { - const array: ChallengeData[] = [ - { - groupId: 1, - groupName: "testGroup", - targetAmount: 100000, - maxMembers: 5, - currentMembers: 3, - startedAt: "2023-10-10", - finishedAt: "2023-12-25", - createdAt: "2023-01-01T12:00:00", - inviteToken: "TokenToInvite", - groupMembers: [ - { - memberId: 1, - totalSavingAmount: 1000000, - }, - { - memberId: 2, - totalSavingAmount: 50000, - }, - { - memberId: 3, - totalSavingAmount: 300000, - }, - ], - }, - { - groupId: 2, - groupName: "testGroup2", - targetAmount: 200000, - maxMembers: 5, - currentMembers: 3, - startedAt: "2023-10-16", - finishedAt: "2023-12-25", - createdAt: "2023-01-01T12:00:00", - inviteToken: "TokenToInvite", - groupMembers: [ - { - memberId: 1, - totalSavingAmount: 1000000, - }, - { - memberId: 2, - totalSavingAmount: 50000, - }, - { - memberId: 3, - totalSavingAmount: 300000, - }, - ], - }, - ]; + const [challengeGroups, setChallengeGroups] = useRecoilState( + getChallengeGroupsAtom + ); + + useEffect(() => { + const getChallengeGroups = async () => { + try { + const response = await challengeGroupAPI.getGroups(); + setChallengeGroups(response.data); + } catch (error) { + console.log("error: ", error); + } + }; + getChallengeGroups(); + }, []); return ( - - {array.map((item) => { - return ; - })} - + <> + + {challengeGroups && + challengeGroups.map((item, id) => { + return ( + + ); + })} + + ); } diff --git a/src/components/BarGraphList/style.ts b/src/components/BarGraphList/style.ts index aadb03c..80519fe 100644 --- a/src/components/BarGraphList/style.ts +++ b/src/components/BarGraphList/style.ts @@ -2,7 +2,7 @@ import { ChallengeHeight } from "@/src/assets/height"; import { theme } from "@/src/assets/theme"; import styled from "@emotion/styled"; -const List = styled.li` +const List = styled.ul` height: calc(${ChallengeHeight}px); background-color: ${theme.font_color.gray1}; diff --git a/src/components/Chatting/index.tsx b/src/components/Chatting/index.tsx index e1d3d87..12f9303 100644 --- a/src/components/Chatting/index.tsx +++ b/src/components/Chatting/index.tsx @@ -5,6 +5,8 @@ import ChattingContent from "../ChattingContent"; import { API_WEBSOCKET_URL } from "@/src/core/api/instance"; import { useEffect, useState } from "react"; import Stomp from "stompjs"; +import { useRecoilState } from "recoil"; +import { getChallengeGroupAtom } from "@/src/hooks/recoil/useGetChallengeGroup"; export interface Message { groupId: number; @@ -17,44 +19,7 @@ function Chatting() { const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(""); const [stompClient, setStompClient] = useState(); - - const array1 = { - groupId: 2, - groupName: "testGroup2", - targetAmount: 200000, - maxMembers: 5, - currentMembers: 3, - startedAt: "2023-10-16", - finishedAt: "2023-12-25", - createdAt: "2023-01-01T12:00:00", - inviteToken: "TokenToInvite", - groupMembers: [ - { - memberId: 1, - totalSavingAmount: 1000000, - }, - { - memberId: 2, - totalSavingAmount: 50000, - }, - { - memberId: 3, - totalSavingAmount: 300000, - }, - { - memberId: 3, - totalSavingAmount: 300000, - }, - { - memberId: 3, - totalSavingAmount: 300000, - }, - { - memberId: 3, - totalSavingAmount: 300000, - }, - ], - }; + const [challengeGroup] = useRecoilState(getChallengeGroupAtom); const connect = async () => { const socket = new WebSocket(`${API_WEBSOCKET_URL}/ws`); @@ -69,7 +34,7 @@ function Chatting() { console.log("Connected to WebSocket"); /* COMPLETED: 채팅 메시지를 구독하기 */ - stomp.subscribe(`/chat/groups/4`, onReceiveMessage); + stomp.subscribe(`/chat/groups/${challengeGroup.id}`, onReceiveMessage); }); }; @@ -104,7 +69,7 @@ function Chatting() { "/app/send", {}, JSON.stringify({ - groupId: 4, + groupId: challengeGroup.id, message: message, messageType: "TALK", }) @@ -116,7 +81,7 @@ function Chatting() { return (
- + {messages.map((content, index) => { diff --git a/src/components/Common/BudgetAccountBarGraph/style.ts b/src/components/Common/BudgetAccountBarGraph/style.ts index 75bcdb4..41f3b17 100644 --- a/src/components/Common/BudgetAccountBarGraph/style.ts +++ b/src/components/Common/BudgetAccountBarGraph/style.ts @@ -18,6 +18,7 @@ const BudgetAccountBarGraphContainer = styled.div` justify-content: space-around; border-bottom: 1px solid ${theme.font_color.gray2}; + background-color: ${theme.font_color.white}; `; const TotalBudget = styled.div` ${theme.font_style.regular_medium}; diff --git a/src/components/Common/Header/index.tsx b/src/components/Common/Header/index.tsx index 9b7a92a..e156a69 100644 --- a/src/components/Common/Header/index.tsx +++ b/src/components/Common/Header/index.tsx @@ -20,7 +20,7 @@ interface HeaderProps { onMoreClick?: () => void; } -interface RederButtonProps { +interface RenderButtonProps { onClick?: () => void; src: string; alt: string; @@ -44,7 +44,7 @@ function Header({ navigate(-1); }; - const renderButton = ({ onClick, src, alt }: RederButtonProps) => ( + const renderButton = ({ onClick, src, alt }: RenderButtonProps) => ( {alt} diff --git a/src/components/Common/LongButton/index.tsx b/src/components/Common/LongButton/index.tsx index 3c913dd..3d4ae1e 100644 --- a/src/components/Common/LongButton/index.tsx +++ b/src/components/Common/LongButton/index.tsx @@ -1,17 +1,30 @@ +import { theme } from "@/src/assets/theme"; import { LongButtonUI } from "./style"; interface LongButtonProps { type: "button" | "submit" | "reset" | undefined; + cancel?: boolean; buttonName: string; onClick?: () => void; } -function LongButton({ type, buttonName, onClick }: LongButtonProps) { +function LongButton({ type, cancel, buttonName, onClick }: LongButtonProps) { return ( - + {buttonName} ); } +const buttonStyle = { + backgroundColor: `${theme.font_color.primary_green}`, +}; +const cancelStyle = { + backgroundColor: `${theme.font_color.white}`, + border: `1px solid ${theme.font_color.gray2}`, +}; + export default LongButton; diff --git a/src/components/CreateChallengeGroup/index.tsx b/src/components/CreateChallengeGroup/index.tsx index f77932f..5eab480 100644 --- a/src/components/CreateChallengeGroup/index.tsx +++ b/src/components/CreateChallengeGroup/index.tsx @@ -1,6 +1,7 @@ import { CreateChallengeGroupUI } from "./style"; import LabelInput from "../Common/LabelInput"; import ShortButton from "../Common/ShortButton"; +import { useNavigate } from "react-router-dom"; function CreateChallengeGroup() { const array = [ @@ -45,31 +46,37 @@ function CreateChallengeGroup() { const handleSavingButton = () => { console.log("saving"); }; + + const navigate = useNavigate(); const handleCancelButton = () => { console.log("cancel"); + + navigate("/challenge"); }; return ( - -
- {array.map(object => { - return ( - - ); - })} -
+ <> + +
+ {array.map(object => { + return ( + + ); + })} +
+
-
+ ); } diff --git a/src/components/CreateChallengeGroup/style.ts b/src/components/CreateChallengeGroup/style.ts index bb3e70b..3a14f7d 100644 --- a/src/components/CreateChallengeGroup/style.ts +++ b/src/components/CreateChallengeGroup/style.ts @@ -21,6 +21,11 @@ const Container = styled.div` border-bottom: 1px solid ${theme.font_color.gray2}; } } + + > div:last-of-type { + width: 100%; + position: relative; + } `; export const CreateChallengeGroupUI = { diff --git a/src/components/PasswordReset/index.tsx b/src/components/PasswordReset/index.tsx index 12774bb..9558957 100644 --- a/src/components/PasswordReset/index.tsx +++ b/src/components/PasswordReset/index.tsx @@ -7,11 +7,15 @@ import { useNavigate } from "react-router-dom"; function PasswordReset() { const [email, setEmail] = useState(""); - const navigate = useNavigate(); const handlePasswordReset = () => { memberAPI.sendResetPasswordLink(email).then().catch(); }; + const navigate = useNavigate(); + const handleCancelButton = () => { + navigate("/"); + }; + return ( 비밀번호 재설정 @@ -27,6 +31,12 @@ function PasswordReset() { buttonName={"발송"} onClick={handlePasswordReset} /> +
); diff --git a/src/core/api/challengeGroup.ts b/src/core/api/challengeGroup.ts index 9995bf5..517b53a 100644 --- a/src/core/api/challengeGroup.ts +++ b/src/core/api/challengeGroup.ts @@ -1,11 +1,19 @@ +import { GetChallengeGroups } from "@/src/@types/models/getChallengeGroups"; import { APIInstance } from "./instance"; +import { CreateChallengeGroup } from "@/src/@types/models/createChallengeGroup"; +import { UpdateChallengeGroup } from "@/src/@types/models/updateChallengeGroup"; +import { CreateInviteLink } from "@/src/@types/models/createInviteLink"; +import { DeleteChallengeGroup } from "@/src/@types/models/deleteChallengeGroup"; +import { SaveMoney } from "@/src/@types/models/saveMoney"; +import { JoinChallengeGroup } from "@/src/@types/models/JoinChallengeGroup"; +import { GetMessages } from "@/src/@types/models/challengeMember"; const GROUPS = "/groups"; export const challengeGroupAPI = { /** COMPLETED: getGroups GET 요청하기 */ getGroups: () => { - return APIInstance.get(GROUPS); + return APIInstance.get(GROUPS); }, /** COMPLETED: createGroups POST 요청하기 */ createGroup: ( @@ -16,7 +24,7 @@ export const challengeGroupAPI = { startAt: string, targetAmount: number ) => { - return APIInstance.post(GROUPS, { + return APIInstance.post(GROUPS, { description: description, endAt: endAt, maxMembers: maxMembers, @@ -35,7 +43,7 @@ export const challengeGroupAPI = { startAt: string, targetAmount: number ) => { - return APIInstance.put(GROUPS + `/${groupId}`, { + return APIInstance.put(GROUPS + `/${groupId}`, { description: description, endAt: endAt, maxMembers: maxMembers, @@ -46,28 +54,30 @@ export const challengeGroupAPI = { }, /** COMPLETED: deleteGroup DELETE 요청하기 */ deleteGroup: (groupId: number) => { - return APIInstance.delete(GROUPS + `/${groupId}`); + return APIInstance.delete(GROUPS + `/${groupId}`); }, /** COMPLETED: createInviteLink GET 요청하기 */ createInviteLink: (groupId: number) => { - return APIInstance.get(GROUPS + `/${groupId}/invite-link`); + return APIInstance.get( + GROUPS + `/${groupId}/invite-link` + ); }, /** COMPLETED: leaveGroup DELETE 요청하기 */ leaveGroup: (groupId: number, memberId: number) => { return APIInstance.delete(GROUPS + `/${groupId}/member/${memberId}`); }, - /** COMPLETED: getMessage GET 요청하기 */ - getMessage: (groupId: number) => { - return APIInstance.delete(GROUPS + `/${groupId}/messages`); + /** COMPLETED: getMessages GET 요청하기 */ + getMessages: (groupId: number) => { + return APIInstance.get(GROUPS + `/${groupId}/messages`); }, /** COMPLETED: saveMoney POST 요청하기 */ saveMoney: (groupId: number, savingAmount: number) => { - return APIInstance.post(GROUPS + `/${groupId}/saving`, { + return APIInstance.post(GROUPS + `/${groupId}/saving`, { savingAmount: savingAmount, }); }, /** COMPLETED: joinGroup POST 요청하기 */ joinGroup: (inviteLink: number) => { - return APIInstance.post(GROUPS + `/join/${inviteLink}`); + return APIInstance.post(GROUPS + `/join/${inviteLink}`); }, }; diff --git a/src/hooks/recoil/useGetChallengeGroup.ts b/src/hooks/recoil/useGetChallengeGroup.ts new file mode 100644 index 0000000..4385b7f --- /dev/null +++ b/src/hooks/recoil/useGetChallengeGroup.ts @@ -0,0 +1,22 @@ +import { GetChallengeGroup } from "@/src/@types/models/getChallengeGroups"; +import { atom } from "recoil"; + +export const getChallengeGroupAtom = atom({ + key: "getChallengeGroupAtom", + default: { + id: -1, + name: "", + description: "", + targetAmount: -1, + maxMembers: -1, + currentMembers: -1, + startAt: "", + endAt: "", + inviteLink: "", + adminId: -1, + viewerId: -1, + viewerName: "", + viewerEmail: "", + groupMembers: [], + }, +}); diff --git a/src/hooks/recoil/useGetChallengeGroups.ts b/src/hooks/recoil/useGetChallengeGroups.ts new file mode 100644 index 0000000..53465bc --- /dev/null +++ b/src/hooks/recoil/useGetChallengeGroups.ts @@ -0,0 +1,7 @@ +import { GetChallengeGroups } from "@/src/@types/models/getChallengeGroups"; +import { atom } from "recoil"; + +export const getChallengeGroupsAtom = atom({ + key: "getChallengeGroupsAtom", + default: [], +}); diff --git a/src/pages/ChallengeDetailPage/index.tsx b/src/pages/ChallengeDetailPage/index.tsx index b82e857..9981c0e 100644 --- a/src/pages/ChallengeDetailPage/index.tsx +++ b/src/pages/ChallengeDetailPage/index.tsx @@ -7,12 +7,9 @@ import { useParams } from "react-router-dom"; function ChallengeDetailPage() { const params = useParams(); - console.log("params: ", typeof params.slug); const [openEditButton, setOpenEditButton] = useState(false); - const handleFilterClick = () => { - console.log("hello World"); - }; + const handleFilterClick = () => {}; return ( <>
Date: Wed, 13 Dec 2023 17:22:10 +0900 Subject: [PATCH 10/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20[IMPROVE]=20=20Chall?= =?UTF-8?q?enge=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 수정 페이지 쪽 인원 수 Modal로 변경 --- src/App.tsx | 7 + src/assets/util.ts | 66 +++-- .../BarGraphItemDetailsTopContainer/index.tsx | 11 +- src/components/Common/CenterInput/index.tsx | 51 ++++ src/components/Common/CenterInput/style.ts | 27 ++ .../Common/FakeInputButton/index.tsx | 17 ++ .../Common/FakeInputButton/style.ts | 20 ++ src/components/CreateChallengeGroup/index.tsx | 201 ++++++++++---- src/components/SavingChallenge/index.tsx | 44 ++++ src/components/SavingChallenge/style.ts | 0 src/components/UpdateChallengeGroup/index.tsx | 246 ++++++++++++++++++ src/components/UpdateChallengeGroup/style.ts | 80 ++++++ src/core/api/challengeGroup.ts | 73 +++--- src/hooks/recoil/useCreateCenterInputValue.ts | 6 + src/hooks/recoil/useCreateChallengeGroup.ts | 24 ++ src/hooks/recoil/useGetChallengeGroup.ts | 16 +- src/pages/ChallengeDetailPage/index.tsx | 9 +- src/pages/RecurringPage/index.tsx | 2 +- src/pages/SavingPage/index.tsx | 20 ++ src/pages/UpdateChallengeGroupPage/index.tsx | 20 ++ 20 files changed, 811 insertions(+), 129 deletions(-) create mode 100644 src/components/Common/CenterInput/index.tsx create mode 100644 src/components/Common/CenterInput/style.ts create mode 100644 src/components/Common/FakeInputButton/index.tsx create mode 100644 src/components/Common/FakeInputButton/style.ts create mode 100644 src/components/SavingChallenge/index.tsx create mode 100644 src/components/SavingChallenge/style.ts create mode 100644 src/components/UpdateChallengeGroup/index.tsx create mode 100644 src/components/UpdateChallengeGroup/style.ts create mode 100644 src/hooks/recoil/useCreateCenterInputValue.ts create mode 100644 src/hooks/recoil/useCreateChallengeGroup.ts create mode 100644 src/pages/SavingPage/index.tsx create mode 100644 src/pages/UpdateChallengeGroupPage/index.tsx diff --git a/src/App.tsx b/src/App.tsx index c053313..fb7a431 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,8 @@ import Layout from "./components/Common/Layout"; import ChattingPage from "./pages/ChattingPage"; import ChallengeDetailPage from "./pages/ChallengeDetailPage"; import CreateChallengeGroupPage from "./pages/CreateChallengeGroupPage"; +import UpdateChallengeGroupPage from "./pages/UpdateChallengeGroupPage"; +import SavingPage from "./pages/SavingPage"; function App() { const isLoginPage = window.location.pathname === "/"; @@ -33,6 +35,10 @@ function App() { { path: "/passwordReset", element: }, { path: "/recordAccountBook/recurring", element: }, { path: "/challenge/create", element: }, + { + path: "/challenge/update/:slug", + element: , + }, { path: "/*", element: ( @@ -52,6 +58,7 @@ function App() { path="/challenge/:slug" element={} /> + } /> } /> } /> } /> diff --git a/src/assets/util.ts b/src/assets/util.ts index 24c8862..47e0f7f 100644 --- a/src/assets/util.ts +++ b/src/assets/util.ts @@ -2,34 +2,50 @@ * 코드 작성 시 자주 사용하게 될 함수를 따로 정리한 파일 */ /** COMPLETED: (index.tsx) 회계식으로 숫자 표현하기 - * 세 자리씩 끊어서 쉼표 사용하는 방식 - * 음수까지 표현 - * 소수점 두 번째자리까지 표현 - * index.tsx에 사용하는 경우를 생각하고 제작 */ -export const ChangeNumberForAccounting = (number: number) => { - /* 1. 숫자를 문자열로 변환 */ - const numStr = String(number); - - /* 2. 소수점 확인 */ - const hasDecimal = numStr.includes("."); - - /* 3. 소주점 이하 숫자와 소수점 이상 숫자를 분리 */ - let integerPart = numStr; - let decimalPart = ""; - - if (hasDecimal) { - [integerPart, decimalPart] = numStr.split("."); - } + * 1. 세 자리씩 끊어서 쉼표 사용하는 방식 + * 2. 음수까지 표현 + * 3. 소수점 두 번째자리까지 표현 + * 4. index.tsx에 사용하는 경우를 생각하고 제작 + * 5. Input으로 값을 변경해도 표현 가능 */ +export const ChangeNumberForAccounting = (number: number | string) => { + // /* 1. 숫자를 문자열로 변환 */ + // let numStr = ""; + // if (typeof number === "number") { + // numStr = String(number); + // } else if (typeof number === "string") { + // const newNumber = number.split(",").join(""); + // numStr = newNumber; + // } + // /* 2. 소수점 확인 */ + // const hasDecimal = numStr.includes("."); + + // /* 3. 소주점 이하 숫자와 소수점 이상 숫자를 분리 */ + // let integerPart = numStr; + // let decimalPart = ""; + + // if (hasDecimal) { + // [integerPart, decimalPart] = numStr.split("."); + // } + + // /* 4. 정수 부분에 ',' 추가 */ + // integerPart = integerPart.replace(/\B(? { + navigate(`/saving/${param.slug}`); + }; + return (
{challengeGroup.name} {challengeGroup.currentMembers}명 - + 저금하기
diff --git a/src/components/Common/CenterInput/index.tsx b/src/components/Common/CenterInput/index.tsx new file mode 100644 index 0000000..b10bb38 --- /dev/null +++ b/src/components/Common/CenterInput/index.tsx @@ -0,0 +1,51 @@ +import { ChangeEvent } from "react"; +import { CenterInputUI } from "./style"; +import { ChangeNumberForAccounting } from "@/src/assets/util"; +import { useRecoilState } from "recoil"; +import { createCenterInputValueAtom } from "@/src/hooks/recoil/useCreateCenterInputValue"; + +interface CenterInputProps { + type: "text" | "number" | "submit"; + unit: "개월" | "원"; +} + +function CenterInput({ type, unit }: CenterInputProps) { + const [centerInputValue, setCenterInputValue] = useRecoilState( + createCenterInputValueAtom + ); + + const handleCenterInputValue = (e: ChangeEvent) => { + const { value } = e.target; + + if (unit === "원") { + const result = ChangeNumberForAccounting(value); + setCenterInputValue(result || ""); + } + }; + + const adjustCenterInputSize = () => { + const input = document.getElementById("dynamicInput"); + if (input) { + const minWidth = 10; + input.style.width = "auto"; + const newWidth = Math.max(minWidth, input.scrollWidth); + input.style.width = `${newWidth}px`; + } + }; + + return ( + + + {unit} + + ); +} + +export default CenterInput; diff --git a/src/components/Common/CenterInput/style.ts b/src/components/Common/CenterInput/style.ts new file mode 100644 index 0000000..81b928f --- /dev/null +++ b/src/components/Common/CenterInput/style.ts @@ -0,0 +1,27 @@ +import { theme } from "@/src/assets/theme"; +import styled from "@emotion/styled"; + +const CenterInputContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + + margin: 85px 36px 20px; + + border-bottom: 1px solid ${theme.font_color.gray2}; +`; + +const CenterInput = styled.input` + text-align: center; + ${theme.font_style.regular_medium}; + + border: none; +`; + +const Unit = styled.span``; + +export const CenterInputUI = { + CenterInputContainer, + CenterInput, + Unit, +}; diff --git a/src/components/Common/FakeInputButton/index.tsx b/src/components/Common/FakeInputButton/index.tsx new file mode 100644 index 0000000..100a279 --- /dev/null +++ b/src/components/Common/FakeInputButton/index.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from "react"; +import { FakeInputButtonUI } from "./style"; + +interface FakeInputButtonProps { + children: ReactNode; + onClick: () => void; +} + +function FakeInputButton({ children, onClick }: FakeInputButtonProps) { + return ( + + {children} + + ); +} + +export default FakeInputButton; diff --git a/src/components/Common/FakeInputButton/style.ts b/src/components/Common/FakeInputButton/style.ts new file mode 100644 index 0000000..4cf6f8e --- /dev/null +++ b/src/components/Common/FakeInputButton/style.ts @@ -0,0 +1,20 @@ +import { theme } from "@/src/assets/theme"; +import styled from "@emotion/styled"; +import { Button } from "@mui/material"; + +const inputWidth = "100% - 73px"; + +const InputButton = styled(Button)` + padding: 0 0 6px 10px; + border: none; + border-bottom: 1px solid ${theme.font_color.gray2}; + ${theme.font_style.regular_medium} + width: calc(${inputWidth}); + + justify-content: flex-start; + color: ${theme.font_color.black}; +`; + +export const FakeInputButtonUI = { + InputButton, +}; diff --git a/src/components/CreateChallengeGroup/index.tsx b/src/components/CreateChallengeGroup/index.tsx index 5eab480..2a356de 100644 --- a/src/components/CreateChallengeGroup/index.tsx +++ b/src/components/CreateChallengeGroup/index.tsx @@ -2,73 +2,125 @@ import { CreateChallengeGroupUI } from "./style"; import LabelInput from "../Common/LabelInput"; import ShortButton from "../Common/ShortButton"; import { useNavigate } from "react-router-dom"; +import { challengeGroupAPI } from "@/src/core/api/challengeGroup"; +import { useRecoilState } from "recoil"; +import { createChallengeGroupAtom } from "@/src/hooks/recoil/useCreateChallengeGroup"; +import { ChangeEvent, useState } from "react"; +import { LabelInputUI } from "../Common/LabelInput/style"; +import FakeInputButton from "../Common/FakeInputButton"; +import { UpdateChallengeGroupUI } from "../UpdateChallengeGroup/style"; function CreateChallengeGroup() { - const array = [ - { - label: "그룹명", - inputId: "groupName", - inputName: "", - placeholder: "그룹명 입력", - }, - { - label: "인원", - inputId: "members", - inputName: "", - placeholder: "초대 인원수 선택", - }, - { - label: "목표액", - inputId: "totalAmount", - inputName: "", - placeholder: "목표액 입력", - }, - { - label: "시작일", - inputId: "startedAt", - inputName: "", - placeholder: "시작일 입력", - }, - { - label: "종료일", - inputId: "finishedAt", - inputName: "", - placeholder: "종료일 입력", - }, - { - label: "초대링크", - inputId: "inviteLink", - inputName: "", - placeholder: "초대링크 출력", - }, - ]; + const navigate = useNavigate(); - const handleSavingButton = () => { - console.log("saving"); + const [createChallengeGroup, setCreateChallengeGroup] = useRecoilState( + createChallengeGroupAtom + ); + const handleSavingButton = async () => { + try { + const response = await challengeGroupAPI.createGroup( + createChallengeGroup + ); + if (response.status === 200) { + navigate("/challenge"); + } + } catch (error) { + console.log("챌린지 생성 error: ", error); + } }; - const navigate = useNavigate(); const handleCancelButton = () => { - console.log("cancel"); - navigate("/challenge"); }; + + const handleGroupName = (e: ChangeEvent) => { + const { value } = e.target; + setCreateChallengeGroup(prev => ({ + ...prev, + name: value, + })); + }; + // const handleMaxMembers = (e: ChangeEvent) => { + // const { value } = e.target; + // setCreateChallengeGroup(prev => ({ + // ...prev, + // maxMembers: Number(value), + // })); + // }; + const handleTargetAmount = (e: ChangeEvent) => { + const { value } = e.target; + setCreateChallengeGroup(prev => ({ + ...prev, + targetAmount: Number(value), + })); + }; + const handleStartAt = (e: ChangeEvent) => { + const { value } = e.target; + setCreateChallengeGroup(prev => ({ + ...prev, + startAt: value, + })); + }; + const handleEndAt = (e: ChangeEvent) => { + const { value } = e.target; + setCreateChallengeGroup(prev => ({ + ...prev, + endAt: value, + })); + }; + + /* 인원 수 선택 창 */ + const [isOpenModal, setIsOpenModal] = useState(false); + const handleModalItem = (members: number) => { + setIsOpenModal(false); + setCreateChallengeGroup(prev => ({ + ...prev, + maxMembers: members, + })); + }; + return ( <>
- {array.map(object => { - return ( - - ); - })} + + + 인원 + setIsOpenModal(true)}> + {createChallengeGroup.maxMembers} + + + + +
+ {isOpenModal && ( + + +
초대 인원 수
+
    +
  • + handleModalItem(1)}> + 1 + +
  • +
  • + handleModalItem(2)}> + 2 + +
  • +
  • + handleModalItem(3)}> + 3 + +
  • +
  • + handleModalItem(4)}> + 4 + +
  • +
  • + handleModalItem(5)}> + 5 + +
  • +
+
+
+ )} ); } diff --git a/src/components/SavingChallenge/index.tsx b/src/components/SavingChallenge/index.tsx new file mode 100644 index 0000000..c6d5f2e --- /dev/null +++ b/src/components/SavingChallenge/index.tsx @@ -0,0 +1,44 @@ +import { challengeGroupAPI } from "@/src/core/api/challengeGroup"; +import CenterInput from "../Common/CenterInput"; +import LongButton from "../Common/LongButton"; +import { useNavigate, useParams } from "react-router-dom"; +import { useRecoilState } from "recoil"; +import { createCenterInputValueAtom } from "@/src/hooks/recoil/useCreateCenterInputValue"; + +function SavingChallenge() { + const param = useParams(); + const navigate = useNavigate(); + + const [centerInputValue] = useRecoilState(createCenterInputValueAtom); + + const handleSavingButton = async () => { + const newInputValue = centerInputValue.split(",").join(""); + const saveMoneySaving = { + groupId: Number(param.slug), + savingAmount: Number(newInputValue), + }; + + try { + const response = await challengeGroupAPI.saveMoney(saveMoneySaving); + + if (response.status === 200) { + navigate(`/challenge/${param.slug}`); + } + } catch (error) { + console.error("saving Error: ", error); + } + }; + + return ( + <> + + + + ); +} + +export default SavingChallenge; diff --git a/src/components/SavingChallenge/style.ts b/src/components/SavingChallenge/style.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/components/UpdateChallengeGroup/index.tsx b/src/components/UpdateChallengeGroup/index.tsx new file mode 100644 index 0000000..245d101 --- /dev/null +++ b/src/components/UpdateChallengeGroup/index.tsx @@ -0,0 +1,246 @@ +import { getChallengeGroupAtom } from "@/src/hooks/recoil/useGetChallengeGroup"; +import LabelInput from "../Common/LabelInput"; +import { UpdateChallengeGroupUI } from "./style"; +import { useRecoilState } from "recoil"; +import { ChangeEvent, useEffect, useState } from "react"; +import ShortButton from "../Common/ShortButton"; +import { useNavigate, useParams } from "react-router-dom"; +import { challengeGroupAPI } from "@/src/core/api/challengeGroup"; +import { LabelInputUI } from "../Common/LabelInput/style"; +import FakeInputButton from "../Common/FakeInputButton"; +import { DateCalendar, LocalizationProvider } from "@mui/x-date-pickers"; +import { DemoContainer, DemoItem } from "@mui/x-date-pickers/internals/demo"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; + +function UpdateChallengeGroup() { + const param = useParams(); + const navigate = useNavigate(); + /* COMPLETED: Recoil(getChallengeGroupAtom) 값 가져오기 */ + const [challengeGroup, setChallengeGroup] = useRecoilState( + getChallengeGroupAtom + ); + + /* COMPLETED: Recoil(getChallengeGroupAtom) 데이터 수정하기 */ + const handleGroupName = (e: ChangeEvent) => { + const { value } = e.target; + + setChallengeGroup(prev => ({ + ...prev, + name: value, + })); + }; + const handleTargetAmount = (e: ChangeEvent) => { + const { value } = e.target; + + setChallengeGroup(prev => ({ + ...prev, + targetAmount: Number(value), + })); + }; + const handleStartAt = (e: ChangeEvent) => { + const { value } = e.target; + setChallengeGroup(prev => ({ + ...prev, + startAt: value, + })); + }; + const handleEndAt = (e: ChangeEvent) => { + const { value } = e.target; + setChallengeGroup(prev => ({ + ...prev, + endAt: value, + })); + }; + + /* 초대링크 값 가져오기 */ + useEffect(() => { + const fetchInviteLinkData = async () => { + try { + const response = await challengeGroupAPI.createInviteLink( + challengeGroup.id + ); + + if (response.status === 200) { + setChallengeGroup(prev => ({ + ...prev, + inviteLink: response.data.inviteLink, + })); + } + } catch (error) { + console.error("InviteLink error: ", error); + } + }; + fetchInviteLinkData(); + }, []); + + /* COMPLETED: Server로 수정한 데이터 변경하기(PUT) */ + // 수정 내용 저장하기 + const handleSavingButton = async () => { + try { + const response = await challengeGroupAPI.updateGroup(challengeGroup); + if (response.status === 200) { + navigate(`/challenge/${param.slug}`); + } + } catch (error) { + console.error("error: ", error); + } + }; + // 수정 내용 취소하기 + const handleCancelButton = () => { + navigate(`/challenge/${param.slug}`); + }; + + /* 인원 수 선택 창 */ + const [isOpenMaxMembersModal, setIsOpenMaxMembersModal] = useState(false); + const handleMaxMembersModalItem = (members: number) => { + setIsOpenMaxMembersModal(false); + setChallengeGroup(prev => ({ + ...prev, + maxMembers: members, + })); + }; + const [isOpenStartAtModal, setIsOpenStartAtModal] = useState(false); + const handleStartAtModalItem = (date: string) => { + setIsOpenStartAtModal(false); + setChallengeGroup(prev => ({ + ...prev, + startAt: date, + })); + }; + const today = new Date().toDateString(); + + // const [isOpenMaxMembersModal, setIsOpenMaxMembersModal] = useState(false); + // const handleModalItem = (members: number) => { + // setIsOpenMaxMembersModal(false); + // setChallengeGroup(prev => ({ + // ...prev, + // maxMembers: members, + // })); + // }; + + return ( + <> + +
+ + + 인원 + setIsOpenMaxMembersModal(true)}> + {challengeGroup.maxMembers} + + + + + 시작일 + setIsOpenStartAtModal(true)}> + {challengeGroup.startAt} + + + + + {/* 초대링크 Input (ReadOnly) */} + +
+
+ + {isOpenMaxMembersModal && ( + + +
초대 인원 수
+
    +
  • + handleMaxMembersModalItem(1)}> + 1 + +
  • +
  • + handleMaxMembersModalItem(2)}> + 2 + +
  • +
  • + handleMaxMembersModalItem(3)}> + 3 + +
  • +
  • + handleMaxMembersModalItem(4)}> + 4 + +
  • +
  • + handleMaxMembersModalItem(5)}> + 5 + +
  • +
+
+
+ )} + {isOpenStartAtModal && ( + <> + + + + + + handleStartAtModalItem(today)} + /> + + + + + + + )} + + ); +} + +export default UpdateChallengeGroup; diff --git a/src/components/UpdateChallengeGroup/style.ts b/src/components/UpdateChallengeGroup/style.ts new file mode 100644 index 0000000..cc028ff --- /dev/null +++ b/src/components/UpdateChallengeGroup/style.ts @@ -0,0 +1,80 @@ +import { ApplicationHeight, ChallengeHeight } from "@/src/assets/height"; +import { theme } from "@/src/assets/theme"; +import styled from "@emotion/styled"; +import { Button } from "@mui/material"; + +const Container = styled.div` + height: calc(${ChallengeHeight}px); + + display: flex; + flex-direction: column; + justify-content: space-between; + + > div { + > div { + padding: 30px 0; + margin: 0 20px; + } + > div:nth-of-type(2) { + border-bottom: 1px solid ${theme.font_color.gray2}; + } + > div:nth-of-type(5) { + border-bottom: 1px solid ${theme.font_color.gray2}; + } + } + + > div:last-of-type { + width: 100%; + position: relative; + } +`; + +const ModalBackgroundContainer = styled.div` + width: 100%; + height: calc(${ApplicationHeight}px); + background-color: rgba(134, 142, 150, 0.7); + + position: absolute; + top: 0; +`; + +const ModalContainer = styled.div` + width: 350px; + background-color: ${theme.font_color.white}; + + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + + > h6 { + height: 69px; + ${theme.font_style.bold_large}; + border-bottom: 1px solid ${theme.font_color.gray2}; + display: flex; + align-items: center; + padding-left: 20px; + } + > ul li { + border-bottom: 1px solid ${theme.font_color.gray2}; + } +`; + +const ModalButton = styled(Button)` + width: 100%; + ${theme.font_style.light_medium}; + color: ${theme.font_color.black}; + + display: flex; + justify-content: flex-start; + padding-left: 20px; +`; + +export const UpdateChallengeGroupUI = { + Container, + ModalBackgroundContainer, + ModalContainer, + ModalButton, +}; diff --git a/src/core/api/challengeGroup.ts b/src/core/api/challengeGroup.ts index 517b53a..716e9c2 100644 --- a/src/core/api/challengeGroup.ts +++ b/src/core/api/challengeGroup.ts @@ -1,4 +1,7 @@ -import { GetChallengeGroups } from "@/src/@types/models/getChallengeGroups"; +import { + GetChallengeGroup, + GetChallengeGroups, +} from "@/src/@types/models/getChallengeGroups"; import { APIInstance } from "./instance"; import { CreateChallengeGroup } from "@/src/@types/models/createChallengeGroup"; import { UpdateChallengeGroup } from "@/src/@types/models/updateChallengeGroup"; @@ -16,41 +19,36 @@ export const challengeGroupAPI = { return APIInstance.get(GROUPS); }, /** COMPLETED: createGroups POST 요청하기 */ - createGroup: ( - description: string, - endAt: string, - maxMembers: number, - name: string, - startAt: string, - targetAmount: number - ) => { + createGroup: (createGroupObject: { + description: string; + endAt: string; + maxMembers: number; + name: string; + startAt: string; + targetAmount: number; + }) => { return APIInstance.post(GROUPS, { - description: description, - endAt: endAt, - maxMembers: maxMembers, - name: name, - startAt: startAt, - targetAmount: targetAmount, + description: createGroupObject.description, + endAt: createGroupObject.endAt, + maxMembers: createGroupObject.maxMembers, + name: createGroupObject.name, + startAt: createGroupObject.startAt, + targetAmount: createGroupObject.targetAmount, }); }, /** COMPLETED: updateGroup PUT 요청하기 */ - updateGroup: ( - groupId: number, - description: string, - endAt: string, - maxMembers: number, - name: string, - startAt: string, - targetAmount: number - ) => { - return APIInstance.put(GROUPS + `/${groupId}`, { - description: description, - endAt: endAt, - maxMembers: maxMembers, - name: name, - startAt: startAt, - targetAmount: targetAmount, - }); + updateGroup: (challengeGroup: GetChallengeGroup) => { + return APIInstance.put( + GROUPS + `/${challengeGroup.id}`, + { + description: challengeGroup.description, + endAt: challengeGroup.endAt, + maxMembers: challengeGroup.maxMembers, + name: challengeGroup.name, + startAt: challengeGroup.startAt, + targetAmount: challengeGroup.targetAmount, + } + ); }, /** COMPLETED: deleteGroup DELETE 요청하기 */ deleteGroup: (groupId: number) => { @@ -71,10 +69,13 @@ export const challengeGroupAPI = { return APIInstance.get(GROUPS + `/${groupId}/messages`); }, /** COMPLETED: saveMoney POST 요청하기 */ - saveMoney: (groupId: number, savingAmount: number) => { - return APIInstance.post(GROUPS + `/${groupId}/saving`, { - savingAmount: savingAmount, - }); + saveMoney: (saveMoneyObject: { groupId: number; savingAmount: number }) => { + return APIInstance.post( + GROUPS + `/${saveMoneyObject.groupId}/saving`, + { + savingAmount: saveMoneyObject.savingAmount, + } + ); }, /** COMPLETED: joinGroup POST 요청하기 */ joinGroup: (inviteLink: number) => { diff --git a/src/hooks/recoil/useCreateCenterInputValue.ts b/src/hooks/recoil/useCreateCenterInputValue.ts new file mode 100644 index 0000000..7253f71 --- /dev/null +++ b/src/hooks/recoil/useCreateCenterInputValue.ts @@ -0,0 +1,6 @@ +import { atom } from "recoil"; + +export const createCenterInputValueAtom = atom({ + key: "createCenterInputValueAtom", + default: "", +}); diff --git a/src/hooks/recoil/useCreateChallengeGroup.ts b/src/hooks/recoil/useCreateChallengeGroup.ts new file mode 100644 index 0000000..11a5213 --- /dev/null +++ b/src/hooks/recoil/useCreateChallengeGroup.ts @@ -0,0 +1,24 @@ +import { atom } from "recoil"; + +export type CreateChallengeGroupAtomProps = { + name: string; + description: string; + targetAmount: number; + maxMembers: number; + startAt: string; + endAt: string; + inviteLink: string; +}; + +export const createChallengeGroupAtom = atom({ + key: "createChallengeGroupAtom", + default: { + name: "", + description: "", + targetAmount: 0, + maxMembers: 0, + startAt: "", + endAt: "", + inviteLink: "", + }, +}); diff --git a/src/hooks/recoil/useGetChallengeGroup.ts b/src/hooks/recoil/useGetChallengeGroup.ts index 4385b7f..329ea6b 100644 --- a/src/hooks/recoil/useGetChallengeGroup.ts +++ b/src/hooks/recoil/useGetChallengeGroup.ts @@ -4,17 +4,17 @@ import { atom } from "recoil"; export const getChallengeGroupAtom = atom({ key: "getChallengeGroupAtom", default: { - id: -1, + id: 0, name: "", description: "", - targetAmount: -1, - maxMembers: -1, - currentMembers: -1, - startAt: "", - endAt: "", + targetAmount: 0, + maxMembers: 0, + currentMembers: 0, + startAt: "시작일 입력", + endAt: "종료일 입력", inviteLink: "", - adminId: -1, - viewerId: -1, + adminId: 0, + viewerId: 0, viewerName: "", viewerEmail: "", groupMembers: [], diff --git a/src/pages/ChallengeDetailPage/index.tsx b/src/pages/ChallengeDetailPage/index.tsx index 9981c0e..e1726f6 100644 --- a/src/pages/ChallengeDetailPage/index.tsx +++ b/src/pages/ChallengeDetailPage/index.tsx @@ -3,17 +3,20 @@ import FixedCircleButton from "@/src/components/Common/FixedCircleButton"; import Header from "@/src/components/Common/Header"; import EditChallengeButton from "@/src/components/EditChallengeButton"; import { useState } from "react"; -import { useParams } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; function ChallengeDetailPage() { const params = useParams(); + const navigate = useNavigate(); const [openEditButton, setOpenEditButton] = useState(false); - const handleFilterClick = () => {}; + const handleFilterClick = () => { + navigate(`/challenge/update/${params.slug}`); + }; return ( <>
+
+ + + ); +} + +export default SavingPage; diff --git a/src/pages/UpdateChallengeGroupPage/index.tsx b/src/pages/UpdateChallengeGroupPage/index.tsx new file mode 100644 index 0000000..44390ca --- /dev/null +++ b/src/pages/UpdateChallengeGroupPage/index.tsx @@ -0,0 +1,20 @@ +import Header from "@/src/components/Common/Header"; +import UpdateChallengeGroup from "@/src/components/UpdateChallengeGroup"; + +function UpdateChallengeGroupPage() { + return ( + <> +
+ + + ); +} + +export default UpdateChallengeGroupPage; From 0b742cd4fce3add51442715c513ffc1e9da6ecd0 Mon Sep 17 00:00:00 2001 From: Whale2200d Date: Thu, 14 Dec 2023 16:14:03 +0900 Subject: [PATCH 11/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20[IMPROVE]=20=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=88=98=EC=A0=95=20=EC=AA=BD?= =?UTF-8?q?=20Modal=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/CreateChallengeGroup/index.tsx | 125 +++++++++++----- src/components/UpdateChallengeGroup/index.tsx | 135 ++++++++++-------- src/hooks/recoil/useCreateChallengeGroup.ts | 20 +-- src/hooks/recoil/useGetChallengeGroup.ts | 34 ++--- 4 files changed, 194 insertions(+), 120 deletions(-) diff --git a/src/components/CreateChallengeGroup/index.tsx b/src/components/CreateChallengeGroup/index.tsx index 2a356de..30bfc28 100644 --- a/src/components/CreateChallengeGroup/index.tsx +++ b/src/components/CreateChallengeGroup/index.tsx @@ -4,11 +4,16 @@ import ShortButton from "../Common/ShortButton"; import { useNavigate } from "react-router-dom"; import { challengeGroupAPI } from "@/src/core/api/challengeGroup"; import { useRecoilState } from "recoil"; -import { createChallengeGroupAtom } from "@/src/hooks/recoil/useCreateChallengeGroup"; +import { + createChallengeGroupAtom, + createChallengeGroupInitial, +} from "@/src/hooks/recoil/useCreateChallengeGroup"; import { ChangeEvent, useState } from "react"; import { LabelInputUI } from "../Common/LabelInput/style"; import FakeInputButton from "../Common/FakeInputButton"; import { UpdateChallengeGroupUI } from "../UpdateChallengeGroup/style"; +import { DateCalendar, LocalizationProvider } from "@mui/x-date-pickers"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; function CreateChallengeGroup() { const navigate = useNavigate(); @@ -22,14 +27,15 @@ function CreateChallengeGroup() { createChallengeGroup ); if (response.status === 200) { + setCreateChallengeGroup(createChallengeGroupInitial); navigate("/challenge"); } } catch (error) { console.log("챌린지 생성 error: ", error); } }; - const handleCancelButton = () => { + setCreateChallengeGroup(createChallengeGroupInitial); navigate("/challenge"); }; @@ -40,13 +46,15 @@ function CreateChallengeGroup() { name: value, })); }; - // const handleMaxMembers = (e: ChangeEvent) => { - // const { value } = e.target; - // setCreateChallengeGroup(prev => ({ - // ...prev, - // maxMembers: Number(value), - // })); - // }; + /* 인원 수 선택 창 */ + const [isOpenMaxMembersModal, setIsMaxMembersOpenModal] = useState(false); + const handleModalItem = (members: number) => { + setIsMaxMembersOpenModal(false); + setCreateChallengeGroup(prev => ({ + ...prev, + maxMembers: members, + })); + }; const handleTargetAmount = (e: ChangeEvent) => { const { value } = e.target; setCreateChallengeGroup(prev => ({ @@ -54,28 +62,47 @@ function CreateChallengeGroup() { targetAmount: Number(value), })); }; - const handleStartAt = (e: ChangeEvent) => { - const { value } = e.target; + const [isOpenStartAtModal, setIsOpenStartAtModal] = useState(false); + const handleStartAtModalItem = (date: unknown) => { + if (date === null) return; + + /** 'YYYY-MM-DD' 형태로 startAt 데이터 저장하기 */ + const options = { year: "numeric", month: "2-digit", day: "2-digit" }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const stringDate = (date as any).$d + .toLocaleDateString("ko-KR", options) + .replace(/\./g, "") + .replace(/ /g, "-"); + + setIsOpenStartAtModal(false); setCreateChallengeGroup(prev => ({ ...prev, - startAt: value, + startAt: stringDate, })); }; - const handleEndAt = (e: ChangeEvent) => { - const { value } = e.target; + const [isOpenEndAtModal, setIsOpenEndAtModal] = useState(false); + const handleEndAtModalItem = (date: unknown) => { + if (date === null) return; + + /** 'YYYY-MM-DD' 형태로 endAt 데이터 저장하기 */ + const options = { year: "numeric", month: "2-digit", day: "2-digit" }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const stringDate = (date as any).$d + .toLocaleDateString("ko-KR", options) + .replace(/\./g, "") + .replace(/ /g, "-"); + + setIsOpenEndAtModal(false); setCreateChallengeGroup(prev => ({ ...prev, - endAt: value, + endAt: stringDate, })); }; - - /* 인원 수 선택 창 */ - const [isOpenModal, setIsOpenModal] = useState(false); - const handleModalItem = (members: number) => { - setIsOpenModal(false); + const handleDescription = (e: ChangeEvent) => { + const { value } = e.target; setCreateChallengeGroup(prev => ({ ...prev, - maxMembers: members, + description: value, })); }; @@ -93,7 +120,7 @@ function CreateChallengeGroup() { /> 인원 - setIsOpenModal(true)}> + setIsMaxMembersOpenModal(true)}> {createChallengeGroup.maxMembers} @@ -105,21 +132,25 @@ function CreateChallengeGroup() { value={createChallengeGroup.targetAmount} onChange={handleTargetAmount} /> + + 시작일 + setIsOpenStartAtModal(true)}> + {createChallengeGroup.startAt} + + + + 종료일 + setIsOpenEndAtModal(true)}> + {createChallengeGroup.endAt} + + -
@@ -128,7 +159,7 @@ function CreateChallengeGroup() { onSaveClick={handleSavingButton} onCancelClick={handleCancelButton} /> - {isOpenModal && ( + {isOpenMaxMembersModal && (
초대 인원 수
@@ -167,6 +198,28 @@ function CreateChallengeGroup() {
)} + {isOpenStartAtModal && ( + <> + + + + handleStartAtModalItem(date)} /> + + + + + )} + {isOpenEndAtModal && ( + <> + + + + handleEndAtModalItem(date)} /> + + + + + )} ); } diff --git a/src/components/UpdateChallengeGroup/index.tsx b/src/components/UpdateChallengeGroup/index.tsx index 245d101..dd966b8 100644 --- a/src/components/UpdateChallengeGroup/index.tsx +++ b/src/components/UpdateChallengeGroup/index.tsx @@ -1,4 +1,7 @@ -import { getChallengeGroupAtom } from "@/src/hooks/recoil/useGetChallengeGroup"; +import { + getChallengeGroupAtom, + getChallengeGroupInitial, +} from "@/src/hooks/recoil/useGetChallengeGroup"; import LabelInput from "../Common/LabelInput"; import { UpdateChallengeGroupUI } from "./style"; import { useRecoilState } from "recoil"; @@ -9,7 +12,6 @@ import { challengeGroupAPI } from "@/src/core/api/challengeGroup"; import { LabelInputUI } from "../Common/LabelInput/style"; import FakeInputButton from "../Common/FakeInputButton"; import { DateCalendar, LocalizationProvider } from "@mui/x-date-pickers"; -import { DemoContainer, DemoItem } from "@mui/x-date-pickers/internals/demo"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; function UpdateChallengeGroup() { @@ -29,6 +31,15 @@ function UpdateChallengeGroup() { name: value, })); }; + /* 인원 수 선택 창 */ + const [isOpenMaxMembersModal, setIsOpenMaxMembersModal] = useState(false); + const handleMaxMembersModalItem = (members: number) => { + setIsOpenMaxMembersModal(false); + setChallengeGroup(prev => ({ + ...prev, + maxMembers: members, + })); + }; const handleTargetAmount = (e: ChangeEvent) => { const { value } = e.target; @@ -37,18 +48,50 @@ function UpdateChallengeGroup() { targetAmount: Number(value), })); }; - const handleStartAt = (e: ChangeEvent) => { - const { value } = e.target; + + const [isOpenStartAtModal, setIsOpenStartAtModal] = useState(false); + const handleStartAtModalItem = (date: unknown) => { + if (date === null) return; + + /** 'YYYY-MM-DD' 형태로 startAt 데이터 저장하기 */ + const options = { year: "numeric", month: "2-digit", day: "2-digit" }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const stringDate = (date as any).$d + .toLocaleDateString("ko-KR", options) + .replace(/\./g, "") + .replace(/ /g, "-"); + + setIsOpenStartAtModal(false); + setChallengeGroup(prev => ({ + ...prev, + startAt: stringDate, + })); + }; + + const [isOpenEndAtModal, setIsOpenEndAtModal] = useState(false); + const handleEndAtModalItem = (date: unknown) => { + if (date === null) return; + + /** 'YYYY-MM-DD' 형태로 endAt 데이터 저장하기 */ + const options = { year: "numeric", month: "2-digit", day: "2-digit" }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const stringDate = (date as any).$d + .toLocaleDateString("ko-KR", options) + .replace(/\./g, "") + .replace(/ /g, "-"); + + setIsOpenEndAtModal(false); setChallengeGroup(prev => ({ ...prev, - startAt: value, + endAt: stringDate, })); }; - const handleEndAt = (e: ChangeEvent) => { + const handleDescription = (e: ChangeEvent) => { const { value } = e.target; + setChallengeGroup(prev => ({ ...prev, - endAt: value, + description: value, })); }; @@ -79,7 +122,8 @@ function UpdateChallengeGroup() { try { const response = await challengeGroupAPI.updateGroup(challengeGroup); if (response.status === 200) { - navigate(`/challenge/${param.slug}`); + setChallengeGroup(getChallengeGroupInitial); + navigate(-1); } } catch (error) { console.error("error: ", error); @@ -87,37 +131,9 @@ function UpdateChallengeGroup() { }; // 수정 내용 취소하기 const handleCancelButton = () => { - navigate(`/challenge/${param.slug}`); + navigate(-1); }; - /* 인원 수 선택 창 */ - const [isOpenMaxMembersModal, setIsOpenMaxMembersModal] = useState(false); - const handleMaxMembersModalItem = (members: number) => { - setIsOpenMaxMembersModal(false); - setChallengeGroup(prev => ({ - ...prev, - maxMembers: members, - })); - }; - const [isOpenStartAtModal, setIsOpenStartAtModal] = useState(false); - const handleStartAtModalItem = (date: string) => { - setIsOpenStartAtModal(false); - setChallengeGroup(prev => ({ - ...prev, - startAt: date, - })); - }; - const today = new Date().toDateString(); - - // const [isOpenMaxMembersModal, setIsOpenMaxMembersModal] = useState(false); - // const handleModalItem = (members: number) => { - // setIsOpenMaxMembersModal(false); - // setChallengeGroup(prev => ({ - // ...prev, - // maxMembers: members, - // })); - // }; - return ( <> @@ -150,23 +166,20 @@ function UpdateChallengeGroup() { {challengeGroup.startAt} + + 종료일 + setIsOpenEndAtModal(true)}> + {challengeGroup.endAt} + + - - {/* 초대링크 Input (ReadOnly) */} - - - handleStartAtModalItem(today)} - /> - - + handleStartAtModalItem(date)} /> + + + + + )} + {isOpenEndAtModal && ( + <> + + + + handleEndAtModalItem(date)} /> diff --git a/src/hooks/recoil/useCreateChallengeGroup.ts b/src/hooks/recoil/useCreateChallengeGroup.ts index 11a5213..fc29dd3 100644 --- a/src/hooks/recoil/useCreateChallengeGroup.ts +++ b/src/hooks/recoil/useCreateChallengeGroup.ts @@ -10,15 +10,17 @@ export type CreateChallengeGroupAtomProps = { inviteLink: string; }; +export const createChallengeGroupInitial = { + name: "", + description: "", + targetAmount: 0, + maxMembers: 0, + startAt: "시작일 입력", + endAt: "종료일 입력", + inviteLink: "", +}; + export const createChallengeGroupAtom = atom({ key: "createChallengeGroupAtom", - default: { - name: "", - description: "", - targetAmount: 0, - maxMembers: 0, - startAt: "", - endAt: "", - inviteLink: "", - }, + default: createChallengeGroupInitial, }); diff --git a/src/hooks/recoil/useGetChallengeGroup.ts b/src/hooks/recoil/useGetChallengeGroup.ts index 329ea6b..85024cd 100644 --- a/src/hooks/recoil/useGetChallengeGroup.ts +++ b/src/hooks/recoil/useGetChallengeGroup.ts @@ -1,22 +1,24 @@ import { GetChallengeGroup } from "@/src/@types/models/getChallengeGroups"; import { atom } from "recoil"; +export const getChallengeGroupInitial = { + id: 0, + name: "", + description: "", + targetAmount: 0, + maxMembers: 0, + currentMembers: 0, + startAt: "시작일 입력", + endAt: "종료일 입력", + inviteLink: "", + adminId: 0, + viewerId: 0, + viewerName: "", + viewerEmail: "", + groupMembers: [], +}; + export const getChallengeGroupAtom = atom({ key: "getChallengeGroupAtom", - default: { - id: 0, - name: "", - description: "", - targetAmount: 0, - maxMembers: 0, - currentMembers: 0, - startAt: "시작일 입력", - endAt: "종료일 입력", - inviteLink: "", - adminId: 0, - viewerId: 0, - viewerName: "", - viewerEmail: "", - groupMembers: [], - }, + default: getChallengeGroupInitial, }); From a3388709ff744f936cb8b54505ce562ca8a3311d Mon Sep 17 00:00:00 2001 From: Sojung Park Date: Thu, 14 Dec 2023 17:10:20 +0900 Subject: [PATCH 12/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20[IMPROVE]=20?= =?UTF-8?q?=EC=88=98=EC=9E=85=20=EB=B0=8F=20=EC=A7=80=EC=B6=9C=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - react-daum-postcode: 주소 검색 라이브러리 추가 --- package-lock.json | 9 + package.json | 1 + src/@types/models/createAssets.ts | 13 + src/@types/models/getAssets.ts | 18 ++ src/@types/models/updateAssets.ts | 15 ++ src/App.tsx | 8 +- src/components/Common/CalendarModal/index.tsx | 229 ++++++++++++++---- src/components/Common/LabelInput/index.tsx | 34 ++- src/components/Common/LabelInput/style.ts | 3 +- .../Common/SeparatedCategory/index.tsx | 134 +++++++--- .../Common/SeparatedCategory/style.ts | 1 + src/components/Common/ShortButton/style.ts | 2 +- src/components/DaumPost/index.tsx | 65 +++++ src/components/DaumPost/style.ts | 12 + src/components/IncomeExpenseButton/index.tsx | 4 - src/components/IncomeExpenseButton/style.ts | 2 +- src/components/InputArea/index.tsx | 88 ++++--- src/components/InputArea/style.ts | 9 +- src/components/SelectedImage/index.tsx | 72 +++--- src/components/SelectedImage/style.ts | 2 +- src/core/api/accountBook.ts | 8 + src/core/api/assets.ts | 22 +- src/hooks/recoil/useGetAssets.ts | 12 + src/hooks/recoil/useSaveAccountBook.ts | 6 +- src/hooks/recoil/useUploadImageFile.ts | 5 + src/pages/InstallmentPage/index.tsx | 37 +-- src/pages/InstallmentPage/style.ts | 21 +- src/pages/RecordAccountBookPage/index.tsx | 14 +- src/pages/RecurringPage/index.tsx | 4 +- 29 files changed, 650 insertions(+), 200 deletions(-) create mode 100644 src/@types/models/createAssets.ts create mode 100644 src/@types/models/getAssets.ts create mode 100644 src/@types/models/updateAssets.ts create mode 100644 src/components/DaumPost/index.tsx create mode 100644 src/components/DaumPost/style.ts create mode 100644 src/hooks/recoil/useGetAssets.ts diff --git a/package-lock.json b/package-lock.json index 5cc275d..7750444 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "axios": "^1.5.1", "dayjs": "^1.11.10", "react": "^18.2.0", + "react-daum-postcode": "^3.1.3", "react-dom": "^18.2.0", "react-router-dom": "^6.17.0", "recoil": "^0.7.7", @@ -5658,6 +5659,14 @@ "node": ">=0.10.0" } }, + "node_modules/react-daum-postcode": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/react-daum-postcode/-/react-daum-postcode-3.1.3.tgz", + "integrity": "sha512-qTyzUb1BeszPFO4FXSj6p83Wrn5Zpo6YqI2EZ46XSVRZT+du9CrKg9p3KshBRFKYxXmFE1Mv7wEynzXdRFNlmQ==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", diff --git a/package.json b/package.json index d5f3a88..5387db1 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "axios": "^1.5.1", "dayjs": "^1.11.10", "react": "^18.2.0", + "react-daum-postcode": "^3.1.3", "react-dom": "^18.2.0", "react-router-dom": "^6.17.0", "recoil": "^0.7.7", diff --git a/src/@types/models/createAssets.ts b/src/@types/models/createAssets.ts new file mode 100644 index 0000000..37689f1 --- /dev/null +++ b/src/@types/models/createAssets.ts @@ -0,0 +1,13 @@ +/** + * 자산 추가 Response Interface + */ + +export type CreateAssets = { + amount: number; + assetGroup: string; + assetName: string; + assetType: string; + dueDay: number; + memo: string; + statementDay: number; +}; diff --git a/src/@types/models/getAssets.ts b/src/@types/models/getAssets.ts new file mode 100644 index 0000000..32ba54d --- /dev/null +++ b/src/@types/models/getAssets.ts @@ -0,0 +1,18 @@ +/** + * 자산 목록 조회 Response Interface + */ + +export type GetAssets = GetAsset[]; + +export type GetAsset = { + amount: number; + assetGroup: string; + assetId: number; + assetName: string; + assetType: string; + createdAt: string; + dueDay: string; + memo: string; + statementDay: number; + updatedAt: string; +}; diff --git a/src/@types/models/updateAssets.ts b/src/@types/models/updateAssets.ts new file mode 100644 index 0000000..d98df5e --- /dev/null +++ b/src/@types/models/updateAssets.ts @@ -0,0 +1,15 @@ +/** + * 자산 수정 Response Interface + */ + +export type UpdateAssets = { + amount: number; + assetGroup: string; + assetName: string; + assetType: string; + createdAt: string; + dueDay: number; + memo: string; + statementDay: number; + updatedAt: string; +}; diff --git a/src/App.tsx b/src/App.tsx index b7132b7..45c6af4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,7 +28,11 @@ function App() { { path: "/", element: }, { path: "/signUp", element: }, { path: "/passwordReset", element: }, - { path: "/recordAccountBook/recurring", element: }, + { + path: "/recordAccountBook/recurring", + element: , + caseSensitive: true, + }, { path: "/*", element: ( @@ -39,10 +43,12 @@ function App() { } + caseSensitive={true} /> } + caseSensitive={true} /> } /> { setIsCalendarModalOpen(false); }; + const handleChangeRecordAccountBookDate = (value: Date | null) => { + if (value !== null) { + const formattedDate = dayjs(value).format("YYYY-MM-DDTHH:mm:ss"); // 형식에 맞게 날짜를 문자열로 변환 + setPostSaveAccountBook(prev => ({ + ...prev, + transactedAt: formattedDate, + })); + console.log("Selected Date:", formattedDate); + } + }; + return ( <> {isCalendarModalOpen && ( @@ -32,71 +50,200 @@ function CalendarModal() { koKR.components.MuiLocalizationProvider.defaultProps.localeText } // 달력 날짜 포맷 - dateFormats={{ monthAndYear: "YYYY년 MM월" }}> + dateFormats={ + page === "recordAccountBook" + ? { monthAndYear: "YYYY년 MM월" } + : { monthAndYear: "YYYY년 MM월" } + }> .MuiTextField-root": { + minWidth: "0!important", + }, }}> - button": { + fontSize: "13px", + }, }, }, - }, - // .MuiPaper-root (달력) - desktopPaper: { - sx: { - position: "absolute", - left: "-36px", + // MuiPickersLayout-root + layout: { + sx: { + // 요일 + " & .MuiDayCalendar-header > span": { + fontSize: "13px", + }, - "& .MuiPickersCalendarHeader-label": { - fontWeight: "600", - fontSize: "16px", + // 시, 분, 초 + "& .MuiMenuItem-root": { + fontSize: "13px", + }, }, - "& .MuiPickersCalendarHeader-switchViewIcon": { + }, + + leftArrowIcon: { + sx: { + width: "24px", + height: "24px", + }, + }, + + rightArrowIcon: { + sx: { width: "24px", height: "24px", }, - "& .MuiPickersMonth-monthButton": { - fontSize: "16px", + }, + }} + /> + ) : ( + + }} + /> + )} diff --git a/src/components/Common/LabelInput/index.tsx b/src/components/Common/LabelInput/index.tsx index e3b4506..ad4d473 100644 --- a/src/components/Common/LabelInput/index.tsx +++ b/src/components/Common/LabelInput/index.tsx @@ -7,7 +7,12 @@ import { useNavigate } from "react-router-dom"; import { theme } from "@/src/assets/theme"; import RecurringInstallmentButtons from "../../RecurringInstallmentButtons"; import ImageUploadSVG from "@/public/icon/ImageUpload.svg"; -import { uploadImageFileAtom } from "@/src/hooks/recoil/useUploadImageFile"; +import { + isReceiptAtom, + uploadImageFileAtom, +} from "@/src/hooks/recoil/useUploadImageFile"; +import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; +import { imageAPI } from "@/src/core/api/image"; export interface LabelInputProps { type: string; @@ -35,12 +40,17 @@ function LabelInput({ readonly = false, }: LabelInputProps) { const navigate = useNavigate(); + const [btnLabel, setBtnLabel] = useRecoilState(btnLabelStateAtom); - const fileInputRef = useRef(null); + const recurringInstallmentBtnRef = useRef(null); const [showRecurringInstallmentBtns, setShowRecurringInstallmentBtns] = useState(false); + + const fileInputRef = useRef(null); const [, setSelectedImageFile] = useRecoilState(uploadImageFileAtom); + const [isReceipt] = useRecoilState(isReceiptAtom); + const [, setPostSaveAccountBook] = useRecoilState(saveAccountBookAtom); // handleRecurringInstallmentBtn 외부 클릭시 버튼 사라짐 useEffect(() => { @@ -70,9 +80,10 @@ function LabelInput({ navigate("/recordAccountBook/installment"); }; - // 이미지 표시 + // 이미지 표시 및 영수증 아닌 이미지 전송 const handleImageChange = (e: ChangeEvent) => { const FILE = e.target.files?.[0]; + if (FILE) { const imageUrl = URL.createObjectURL(FILE); setSelectedImageFile({ @@ -80,6 +91,23 @@ function LabelInput({ selectedImage: imageUrl, selectedImageFile: FILE, }); + + if (isReceipt === false) { + const jsonData = JSON.stringify({ isReceipt }); + imageAPI + .imageUpload(jsonData, FILE) + .then(response => { + console.log("이미지 전송 성공: ", response.data.imageId); + + setPostSaveAccountBook(prev => ({ + ...prev, + imageIds: [response.data.imageId], + })); + }) + .catch(error => { + console.error("이미지 전송 실패:", error); + }); + } } }; diff --git a/src/components/Common/LabelInput/style.ts b/src/components/Common/LabelInput/style.ts index 680e9ff..795f9a5 100644 --- a/src/components/Common/LabelInput/style.ts +++ b/src/components/Common/LabelInput/style.ts @@ -21,8 +21,9 @@ const Input = styled.input` padding: 0 0 6px 10px; border: none; border-bottom: 1px solid ${theme.font_color.gray2}; - ${theme.font_style.regular_medium} + ${theme.font_style.regular_medium}; width: calc(${inputWidth}); + text-overflow: ellipsis; &::placeholder { ${theme.font_style.regular_small} diff --git a/src/components/Common/SeparatedCategory/index.tsx b/src/components/Common/SeparatedCategory/index.tsx index 45c630e..ebca6e3 100644 --- a/src/components/Common/SeparatedCategory/index.tsx +++ b/src/components/Common/SeparatedCategory/index.tsx @@ -9,24 +9,39 @@ import { openSeparatedCategoryAtom } from "@/src/hooks/recoil/useOpenSeparatedCa import { getCategoriesAtom } from "@/src/hooks/recoil/useGetCategories"; import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; import { GetCategory } from "@/src/@types/models/getCategories"; +import { GetAsset } from "@/src/@types/models/getAssets"; +import { getAssetsAtom } from "@/src/hooks/recoil/useGetAssets"; interface SeparatedCategoryProps { - title?: string; + title: string; } const buttonStyles = { height: "75px", border: `1px solid ${theme.font_color.gray2}`, borderRadius: "0", + fontSize: "13px", + textWrap: "balance", }; function SeparatedCategory({ title }: SeparatedCategoryProps) { const navigate = useNavigate(); - const [, setIsOpenSeparatedCategory] = useRecoilState( + + const [isOpenSeparatedCategory, setIsOpenSeparatedCategory] = useRecoilState( openSeparatedCategoryAtom ); + const [categories] = useRecoilState(getCategoriesAtom); - const [, setPostSaveAccountBook] = useRecoilState(saveAccountBookAtom); + const incomeCategories = categories.filter( + category => category.categoryType === "수입" + ); + const expenseCategories = categories.filter( + category => category.categoryType === "지출" + ); + + const [assets] = useRecoilState(getAssetsAtom); + const [postSaveAccountBook, setPostSaveAccountBook] = + useRecoilState(saveAccountBookAtom); const handleWritingButton = () => { navigate("/setting"); @@ -36,41 +51,92 @@ function SeparatedCategory({ title }: SeparatedCategoryProps) { setIsOpenSeparatedCategory({ isOpen: false }); }; - const handleCategoryButton = (item: GetCategory) => { - setPostSaveAccountBook(prev => ({ - ...prev, - categoryName: item.categoryName, - })); + const handleCategoryButton = (item: GetCategory | GetAsset) => { + if ( + postSaveAccountBook.transactionType === "수입" || + postSaveAccountBook.transactionType === "지출" + ) { + setPostSaveAccountBook(prev => ({ + ...prev, + categoryName: (item as GetCategory).categoryName, + })); + } + + if (title === "결제수단 자산" && (item as GetAsset).assetName === "") { + setPostSaveAccountBook(prev => ({ + ...prev, + assetType: (item as GetAsset).assetType, + })); + } else { + setPostSaveAccountBook(prev => ({ + ...prev, + assetName: (item as GetAsset).assetName, + })); + } + + setIsOpenSeparatedCategory({ isOpen: false }); }; + return ( - -
- -
{title}
+ <> + {isOpenSeparatedCategory.isOpen && ( +
- - + +
{title}
+
+ + +
+
+ {postSaveAccountBook.transactionType === "수입" && ( + + {incomeCategories.map(item => ( + + ))} + + )} + {postSaveAccountBook.transactionType === "지출" && ( + + {expenseCategories.map(item => ( + + ))} + + )} + {/* TODO: 지출일때 자산 카테고리에 지출 카테고리도 같이 표시되는 문제 해결하기 */} + {title === "결제수단 자산" && ( + + {assets.map(item => ( + + ))} + + )}
-
- - {categories.map(item => ( - - ))} - -
-
+
+ )} + ); } diff --git a/src/components/Common/SeparatedCategory/style.ts b/src/components/Common/SeparatedCategory/style.ts index dff2dd0..c58c996 100644 --- a/src/components/Common/SeparatedCategory/style.ts +++ b/src/components/Common/SeparatedCategory/style.ts @@ -39,6 +39,7 @@ const GridContainer = styled.div` display: grid; grid-template-columns: repeat(4, 1fr); background-color: ${theme.font_color.white}; + overflow-y: auto; `; export const SeparatedCategoryUI = { diff --git a/src/components/Common/ShortButton/style.ts b/src/components/Common/ShortButton/style.ts index de6deb5..51fabca 100644 --- a/src/components/Common/ShortButton/style.ts +++ b/src/components/Common/ShortButton/style.ts @@ -7,7 +7,7 @@ const Container = styled.div` position: absolute; bottom: 0; padding: 0 20px; - margin-bottom: 20px; + margin: 20px 0; `; const ButtonWrapper = styled.div` diff --git a/src/components/DaumPost/index.tsx b/src/components/DaumPost/index.tsx new file mode 100644 index 0000000..e1c5911 --- /dev/null +++ b/src/components/DaumPost/index.tsx @@ -0,0 +1,65 @@ +import { useDaumPostcodePopup } from "react-daum-postcode"; +import { DaumPostUI } from "./style"; +import { useRecoilState } from "recoil"; +import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; + +interface DaumPostcodeData { + address: string; + roadAddress: string; + jibunAddress: string; + bname: string; + buildingName: string; + apartment: "Y" | "N"; + userSelectedType: "R" | "J"; +} + +function DaumPost() { + const [, setPostSaveAccountBook] = useRecoilState(saveAccountBookAtom); + + const scriptURL = + "https://t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"; + const open = useDaumPostcodePopup(scriptURL); + + const handleComplete = (data: DaumPostcodeData) => { + let fullAddress = ""; // 주소 변수 + let extraAddress = ""; // 추가될 주소 + + //사용자가 선택한 주소 타입에 따라 해당 주소 값을 가져온다. + // 사용자가 도로명 주소를 선택했을 경우 + if (data.userSelectedType === "R") { + fullAddress = data.roadAddress; + } else { + fullAddress = data.jibunAddress; + } + + // 사용자가 선택한 주소가 도로명 타입일때 참고항목을 조합한다. + if (data.userSelectedType === "R") { + if (data.bname !== "") { + extraAddress += data.bname; + } + // 건물명이 있고, 공동주택일 경우 추가한다. + if (data.buildingName !== "" && data.apartment === "Y") { + extraAddress += + extraAddress !== "" ? `, ${data.buildingName}` : data.buildingName; + } + } + fullAddress += extraAddress !== "" ? ` (${extraAddress})` : ""; + + setPostSaveAccountBook(prev => ({ + ...prev, + address: fullAddress, + })); + }; + + const handleClick = () => { + open({ onComplete: handleComplete }); + }; + + return ( + + 주소 찾기 + + ); +} + +export default DaumPost; diff --git a/src/components/DaumPost/style.ts b/src/components/DaumPost/style.ts new file mode 100644 index 0000000..311a553 --- /dev/null +++ b/src/components/DaumPost/style.ts @@ -0,0 +1,12 @@ +import { theme } from "@/src/assets/theme"; +import styled from "@emotion/styled"; + +const AddressButton = styled.button` + padding: 6px 10px; + background-color: ${theme.font_color.primary_green}; + color: ${theme.font_color.black}; + ${theme.font_style.regular_small}; + ${theme.border_radius}; +`; + +export const DaumPostUI = { AddressButton } as const; diff --git a/src/components/IncomeExpenseButton/index.tsx b/src/components/IncomeExpenseButton/index.tsx index 63746e8..d591cbc 100644 --- a/src/components/IncomeExpenseButton/index.tsx +++ b/src/components/IncomeExpenseButton/index.tsx @@ -16,8 +16,6 @@ function IncomeExpenseButton() { ...prev, transactionType: "수입", })); - - console.log("수입 버튼"); }; const handleExpenseClick = () => { @@ -27,8 +25,6 @@ function IncomeExpenseButton() { ...prev, transactionType: "지출", })); - - console.log("지출 버튼"); }; const incomeButtonStyle = { diff --git a/src/components/IncomeExpenseButton/style.ts b/src/components/IncomeExpenseButton/style.ts index c8d44f0..12ea69d 100644 --- a/src/components/IncomeExpenseButton/style.ts +++ b/src/components/IncomeExpenseButton/style.ts @@ -3,7 +3,7 @@ import { theme } from "../../assets/theme"; const Container = styled.div` width: 100%; - padding: 10px 20px; + padding: 15px 20px; display: flex; justify-content: space-between; align-items: center; diff --git a/src/components/InputArea/index.tsx b/src/components/InputArea/index.tsx index 8d5d042..6404aef 100644 --- a/src/components/InputArea/index.tsx +++ b/src/components/InputArea/index.tsx @@ -5,22 +5,24 @@ import SelectedImage from "../SelectedImage"; import SeparatedCategory from "../Common/SeparatedCategory"; import { useRecoilState } from "recoil"; import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; +import { openSeparatedCategoryAtom } from "@/src/hooks/recoil/useOpenSeparatedCategory"; +import DaumPost from "../DaumPost"; +import { modalOpenStateAtom } from "@/src/hooks/recoil/calendarModalState"; +import CalendarModal from "../Common/CalendarModal"; function InputArea() { const [postSaveAccountBook, setPostSaveAccountBook] = useRecoilState(saveAccountBookAtom); - const [showSeparatedCategory, setShowSeparatedCategory] = - useState(false); + const [isOpenSeparatedCategory, setIsOpenSeparatedCategory] = useRecoilState( + openSeparatedCategoryAtom + ); + const [isCalendarModalOpen, setIsCalendarModalOpen] = + useRecoilState(modalOpenStateAtom); - /** COMPLETED: recoil saveAccountBookAtom에 값 추가하기 */ - const handleDate = (e: ChangeEvent) => { - const { value } = e.target; + const [serperatedCategoryTitle, setSerperatedCategoryTitle] = + useState(""); - setPostSaveAccountBook(prev => ({ - ...prev, - transactedAt: value, - })); - }; + /** input value 관리 */ const handleAmount = (e: ChangeEvent) => { const { value } = e.target; @@ -29,22 +31,6 @@ function InputArea() { amount: Number(value), })); }; - const handleCategoryName = (e: ChangeEvent) => { - const { value } = e.target; - - setPostSaveAccountBook(prev => ({ - ...prev, - categoryName: value, - })); - }; - const handleAssetName = (e: ChangeEvent) => { - const { value } = e.target; - - setPostSaveAccountBook(prev => ({ - ...prev, - assetName: value, - })); - }; const handleTransactionDetail = (e: ChangeEvent) => { const { value } = e.target; @@ -70,6 +56,21 @@ function InputArea() { })); }; + /** 클릭 이벤트 */ + const handleDateClick = () => { + setIsCalendarModalOpen(true); + }; + + const handleCategoryClick = () => { + setIsOpenSeparatedCategory({ isOpen: true }); + setSerperatedCategoryTitle("카테고리 분류"); + }; + + const handleAssetClick = () => { + setIsOpenSeparatedCategory({ isOpen: true }); + setSerperatedCategoryTitle("결제수단 자산"); + }; + return ( @@ -81,7 +82,8 @@ function InputArea() { value={postSaveAccountBook.transactedAt} placeholder={"날짜를 입력해주세요."} addContent={"button"} - onChange={handleDate} + onClick={handleDateClick} + readonly={true} /> setShowSeparatedCategory(!showSeparatedCategory)} - onChange={handleCategoryName} + onClick={handleCategoryClick} readonly={true} /> setShowSeparatedCategory(!showSeparatedCategory)} - onChange={handleAssetName} + onClick={handleAssetClick} readonly={true} /> - + + + + - {showSeparatedCategory && } + {isOpenSeparatedCategory.isOpen && ( + + )} + {isCalendarModalOpen && } + (false); + const [isReceipt, setIsReceipt] = useRecoilState(isReceiptAtom); + const [, setPostSaveAccountBook] = useRecoilState(saveAccountBookAtom); const [selectedImageFile, setSelectedImageFile] = useRecoilState(uploadImageFileAtom); const handleIsReceiptButton = () => { setIsReceipt(!isReceipt); - handleUploadImage(); + handleSendReceiptImage(); }; const handleCloseButton = () => { @@ -26,42 +29,45 @@ function SelectedImage() { isUploadImage: false, selectedImageFile: null, })); + setPostSaveAccountBook(prev => ({ + ...prev, + imageIds: [], + })); }; - /** COMPLETED: 이미지 전송하기 */ - const handleUploadImage = () => { + /** COMPLETED: 이미지 전송후 OCR 결과 보여주기 */ + const handleSendReceiptImage = () => { if (selectedImageFile) { const jsonData = JSON.stringify({ isReceipt }); const imageFile = selectedImageFile.selectedImageFile; - if (imageFile === null) return; + if (imageFile === null) { + return; + } else { + if (isReceipt === true) { + imageAPI + .imageUpload(jsonData, imageFile) + .then(response => { + console.log("영수증 이미지 전송 성공: ", response); - imageAPI - .imageUpload(jsonData, imageFile) - .then(response => { - console.log("imageUpload response: ", response); - - /** COMPLETED: image OCR 결과를 input에 사용되고 있는 recoil state에 값으로 대체하기 */ - setPostSaveAccountBook(prev => ({ - ...prev, - address: response.data.ocrResult.address, - amount: response.data.ocrResult.amount, - transactedAt: response.data.ocrResult.date, - transactionDetail: response.data.ocrResult.vendor, - })); - }) - .catch(error => { - console.error("이미지 전송 실패:", error); - }); + /** COMPLETED: image OCR 결과를 input에 사용되고 있는 recoil state에 값으로 대체하기 */ + setPostSaveAccountBook(prev => ({ + ...prev, + address: response.data.ocrResult.address, + amount: response.data.ocrResult.amount, + imageIds: [response.data.ocrResult.imageId], + transactedAt: response.data.ocrResult.date, + transactionDetail: response.data.ocrResult.vendor, + })); + }) + .catch(error => { + console.error("영수증 이미지 전송 실패:", error); + }); + } + } } }; - useEffect(() => { - // isReceipt가 변경될 때마다 handlUploadImage 호출 - handleUploadImage(); - console.log("isReceipt: ", isReceipt); - }, [isReceipt]); - return ( {selectedImageFile.isUploadImage ? ( @@ -71,14 +77,14 @@ function SelectedImage() { onClick={handleIsReceiptButton}>
영수증
diff --git a/src/components/SelectedImage/style.ts b/src/components/SelectedImage/style.ts index e91cfe8..1638ca3 100644 --- a/src/components/SelectedImage/style.ts +++ b/src/components/SelectedImage/style.ts @@ -3,7 +3,7 @@ import { theme } from "@/src/assets/theme"; const Container = styled.div` width: 100%; - height: 220px; + min-height: 220px; position: relative; border: 1px dashed ${theme.font_color.gray2}; margin-top: 15px; diff --git a/src/core/api/accountBook.ts b/src/core/api/accountBook.ts index 6190915..ecc3230 100644 --- a/src/core/api/accountBook.ts +++ b/src/core/api/accountBook.ts @@ -29,6 +29,8 @@ export const AccountBookAPI = { saveAccountBook: (postAccountBook: { address: string; amount: number; + assetGroup: string; + assetType: string; assetName: string; categoryName: string; imageIds: number[]; @@ -42,6 +44,8 @@ export const AccountBookAPI = { return APIInstance.post(ACCOUNTS, { address: postAccountBook.address, amount: postAccountBook.amount, + assetGroup: postAccountBook.assetGroup, + assetType: postAccountBook.assetType, assetName: postAccountBook.assetName, categoryName: postAccountBook.categoryName, imageIds: postAccountBook.imageIds, @@ -58,6 +62,8 @@ export const AccountBookAPI = { accountId: number; address: string; amount: number; + assetGroup: string; + assetType: string; assetName: string; categoryName: string; imageIds: number[]; @@ -73,6 +79,8 @@ export const AccountBookAPI = { { address: PutAccountBook.address, amount: PutAccountBook.amount, + assetGroup: PutAccountBook.assetGroup, + assetType: PutAccountBook.assetType, assetName: PutAccountBook.assetName, categoryName: PutAccountBook.categoryName, imageIds: PutAccountBook.imageIds, diff --git a/src/core/api/assets.ts b/src/core/api/assets.ts index 602859e..de6a6d1 100644 --- a/src/core/api/assets.ts +++ b/src/core/api/assets.ts @@ -1,22 +1,28 @@ +import { CreateAssets } from "@/src/@types/models/createAssets"; +import { GetAssets } from "@/src/@types/models/getAssets"; import { APIInstance } from "./instance"; +import { UpdateAssets } from "@/src/@types/models/updateAssets"; const ASSETS = "/assets"; export const assetsAPI = { /** COMPLETED: getAssets GET 조회하기 */ getAssets: () => { - return APIInstance.get(ASSETS); + return APIInstance.get(ASSETS); }, + /** COMPLETED: createAssets POST 조회하기 */ createAssets: ( amount: number, + assetGroup: string, assetName: string, assetType: string, dueDay: number, memo: string, statementDay: number ) => { - return APIInstance.post(ASSETS, { + return APIInstance.post(ASSETS, { amount: amount, + assetGroup: assetGroup, assetName: assetName, assetType: assetType, dueDay: dueDay, @@ -24,25 +30,33 @@ export const assetsAPI = { statementDay: statementDay, }); }, + /** COMPLETED: updateAssets PUT 조회하기 */ updateAssets: ( assetId: number, amount: number, + assetGroup: string, assetName: string, assetType: string, + createdAt: string, dueDay: number, memo: string, - statementDay: number + statementDay: number, + updatedAt: string ) => { - return APIInstance.post(ASSETS + `/${assetId}`, { + return APIInstance.post(ASSETS + `/${assetId}`, { amount: amount, + assetGroup: assetGroup, assetName: assetName, assetType: assetType, + createdAt: createdAt, dueDay: dueDay, memo: memo, statementDay: statementDay, + updatedAt: updatedAt, }); }, + /** COMPLETED: deleteAssets DELETE 조회하기 */ deleteAssets: (assetId: number) => { return APIInstance.delete(ASSETS + `/${assetId}`); diff --git a/src/hooks/recoil/useGetAssets.ts b/src/hooks/recoil/useGetAssets.ts new file mode 100644 index 0000000..cc4d4b4 --- /dev/null +++ b/src/hooks/recoil/useGetAssets.ts @@ -0,0 +1,12 @@ +import { GetAssets } from "@/src/@types/models/getAssets"; +import { assetsAPI } from "@/src/core/api/assets"; +import { atom } from "recoil"; + +const fetchResult = assetsAPI.getAssets(); + +export const getAssetsAtom = atom({ + key: "getAssetsAtom", + default: fetchResult.then(response => { + return response.data; + }), +}); diff --git a/src/hooks/recoil/useSaveAccountBook.ts b/src/hooks/recoil/useSaveAccountBook.ts index ed9552d..5a57e68 100644 --- a/src/hooks/recoil/useSaveAccountBook.ts +++ b/src/hooks/recoil/useSaveAccountBook.ts @@ -3,11 +3,13 @@ import { atom } from "recoil"; export interface SaveAccountBookAtomProps { address: string; amount: number; + assetGroup: string; + assetType: string; assetName: string; categoryName: string; imageIds: number[]; isInstallment: boolean; - installmentMonth: number; + installmentMonth: number | null; memo: string; recurringType: string; transactedAt: string; @@ -20,6 +22,8 @@ export const saveAccountBookAtom = atom({ default: { address: "", amount: 0, + assetGroup: "", + assetType: "", assetName: "", categoryName: "", imageIds: [], diff --git a/src/hooks/recoil/useUploadImageFile.ts b/src/hooks/recoil/useUploadImageFile.ts index d341f7e..3b3c171 100644 --- a/src/hooks/recoil/useUploadImageFile.ts +++ b/src/hooks/recoil/useUploadImageFile.ts @@ -14,3 +14,8 @@ export const uploadImageFileAtom = atom({ selectedImageFile: null, }, }); + +export const isReceiptAtom = atom({ + key: "isReceiptAtom", + default: false, +}); diff --git a/src/pages/InstallmentPage/index.tsx b/src/pages/InstallmentPage/index.tsx index cc2cb02..f3b28bc 100644 --- a/src/pages/InstallmentPage/index.tsx +++ b/src/pages/InstallmentPage/index.tsx @@ -8,27 +8,29 @@ import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; import { btnLabelStateAtom } from "@/src/hooks/recoil/btnLabelState"; function InstallmentPage() { - const [inputValue, setInputValue] = useState(""); + const [inputValue, setInputValue] = useState(null); const [, setPostSaveAccountBook] = useRecoilState(saveAccountBookAtom); const [, setBtnLabel] = useRecoilState(btnLabelStateAtom); const navigate = useNavigate(); - const inputChange = (event: React.ChangeEvent) => { - const newValue = event.target.value; + const inputChange = (e: React.ChangeEvent) => { + const newValue = parseInt(e.target.value, 10); - setInputValue(newValue); + setInputValue(isNaN(newValue) ? null : newValue); console.log(newValue); }; const handleSaveButton = () => { - setPostSaveAccountBook(prev => ({ - ...prev, - installmentMonth: parseInt(inputValue, 10), - isInstallment: true, - })); - setBtnLabel("할부"); + if (inputValue !== 0 && inputValue !== null) { + setPostSaveAccountBook(prev => ({ + ...prev, + installmentMonth: inputValue, + isInstallment: true, + })); + setBtnLabel(`${inputValue}개월 할부`); + } navigate("/recordAccountBook"); console.log("저장"); @@ -45,12 +47,15 @@ function InstallmentPage() { isAddButton={false} /> -
- - - 개월 - -
+
할부 입력 (개월 수)
+ + + div:first-of-type { + margin: 55px 0 45px 0; + ${theme.font_style.regular_medium}; + color: ${theme.font_color.gray3}; } `; @@ -18,19 +22,14 @@ const InputWrapper = styled.div` align-items: center; width: calc(${inputWrapperWidth}); border-bottom: 1px solid ${theme.font_color.gray2}; - margin-bottom: 25px; - - span { - ${theme.font_style.regular_medium} - } + margin-bottom: 30px; `; const Input = styled.input` border: 0; + margin-bottom: 3px; width: 40px; - height: 40px; ${theme.font_style.regular_medium}; - padding: 0 5px; `; export const InstallmentPageUI = { Container, InputWrapper, Input } as const; diff --git a/src/pages/RecordAccountBookPage/index.tsx b/src/pages/RecordAccountBookPage/index.tsx index 0a4de5e..d8eb834 100644 --- a/src/pages/RecordAccountBookPage/index.tsx +++ b/src/pages/RecordAccountBookPage/index.tsx @@ -16,11 +16,17 @@ function RecordAccountBookPage() { navigate("/account"); }; - /** TODO: recoil에 값 사용하기 */ const handleSaveButton = () => { - AccountBookAPI.saveAccountBook(postSaveAccountBook); - - console.log("저장하고 가계부 페이지로 이동"); + AccountBookAPI.saveAccountBook(postSaveAccountBook) + .then(response => { + if (response.status === 200) { + console.log("가계부 저장 성공: ", response.data); + navigate("/account"); + } + }) + .catch(error => { + console.log("가계부 저장 실패: ", error); + }); }; return ( diff --git a/src/pages/RecurringPage/index.tsx b/src/pages/RecurringPage/index.tsx index faa4836..5e1990b 100644 --- a/src/pages/RecurringPage/index.tsx +++ b/src/pages/RecurringPage/index.tsx @@ -32,10 +32,8 @@ function RecurringPage() { ...prev, recurringType: item, })); - setBtnLabel("반복"); + setBtnLabel(item); navigate("/recordAccountBook"); - - console.log(`${item} 클릭`); }; return ( From dd3996cf0f960f02f89a8f3d1c6110331c3e93c3 Mon Sep 17 00:00:00 2001 From: Whale2200d Date: Fri, 15 Dec 2023 22:47:52 +0900 Subject: [PATCH 13/29] =?UTF-8?q?=E2=9C=A8=20[FEATURE]=20=20SettingPage=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=99=B8=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SettingPage 기본 마크업 제작 - SettingPage 내 createAsset 페이지 마크업 제작 --- public/icon/BlackWriting.png | Bin 0 -> 302 bytes public/icon/Settings.png | Bin 0 -> 649 bytes public/icon/TrashCan.png | Bin 0 -> 682 bytes public/icon/{Writing.png => WhiteWriting.png} | Bin src/@types/models/assetSymbol.ts | 20 ++ src/@types/models/createAsset.ts | 14 + src/@types/models/getAsset.ts | 19 + src/@types/models/updateAsset.ts | 16 + src/App.tsx | 7 +- src/assets/height.ts | 3 + src/components/Common/LabelInput/index.tsx | 2 +- .../Common/NavigationItems/index.tsx | 8 +- .../Common/SeparatedCategory/index.tsx | 6 +- .../CreateSettingAssetManagement/index.tsx | 330 ++++++++++++++++++ .../CreateSettingAssetManagement/style.ts | 80 +++++ src/components/SettingList/index.tsx | 327 +++++++++++++++++ src/components/SettingList/style.ts | 50 +++ src/core/api/assets.ts | 80 +++-- src/hooks/recoil/useCreateAsset.ts | 15 + .../SettingAssetManagementPage/index.tsx | 20 ++ src/pages/SettingPage/index.tsx | 2 + 21 files changed, 957 insertions(+), 42 deletions(-) create mode 100644 public/icon/BlackWriting.png create mode 100644 public/icon/Settings.png create mode 100644 public/icon/TrashCan.png rename public/icon/{Writing.png => WhiteWriting.png} (100%) create mode 100644 src/@types/models/assetSymbol.ts create mode 100644 src/@types/models/createAsset.ts create mode 100644 src/@types/models/getAsset.ts create mode 100644 src/@types/models/updateAsset.ts create mode 100644 src/components/CreateSettingAssetManagement/index.tsx create mode 100644 src/components/CreateSettingAssetManagement/style.ts create mode 100644 src/components/SettingList/index.tsx create mode 100644 src/components/SettingList/style.ts create mode 100644 src/hooks/recoil/useCreateAsset.ts create mode 100644 src/pages/SettingAssetManagementPage/index.tsx diff --git a/public/icon/BlackWriting.png b/public/icon/BlackWriting.png new file mode 100644 index 0000000000000000000000000000000000000000..e534cf75a17e259539e68c206c9f963eb7af1abc GIT binary patch literal 302 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`j)FbFd;%$g$s6l5$8 za(7}_cTVOdki(Mh=X zImi45_Z$u~zu?`ndDG_Y8#V~)Zs~|vvPt+0e@!8$~K8KFDzCY46+}KMXSyzc`)R+I)PD=i-P% u%WM97er0PKC%sZ3yC65#69R^QVKbLh*2~7Zrc5{6I literal 0 HcmV?d00001 diff --git a/public/icon/Settings.png b/public/icon/Settings.png new file mode 100644 index 0000000000000000000000000000000000000000..6c55b2534f26333948ab3e0006093d45e589e707 GIT binary patch literal 649 zcmV;40(Sk0P)Mz*DZkHGpiT(i8Pq4HNI#f(5(n_7vo;UAl z``*5{typ-HCwcF^=REhlALrZy{KJgCns$PC<73k{wuwdJinPaxCE=L#t%}W-jbIn- zFhJ}io)R~Sb875?cuSlYn|`!`A!3vmBK8u`;I7b*Y}-Q|Chm%sHKHPtC8nF0)L~kD zOQL0skaE=IGdv)Mi9uml#P2-pv!Z2}*O0;~zjxki9DGMzV z*QNb}#^<(JGdXJ=!u-SKKP#YqyLOe$mGm83IA&2vn>OK;M0GfscBUNMMVJ#v2k{!MClJ;)77AV|lk+sE%nfNjqjAby5+9v1 zS<|XO;P*78oIhkjzei#RmXCy~0^y1ysM;LQe4D?dpU?ME#wLgh#5Gh>szy+>@~}j^ zg+K}|76OBCOCR3$R!X0cXbhqa#NdVn$E42ZoE$FThG7gp2)soO literal 0 HcmV?d00001 diff --git a/public/icon/TrashCan.png b/public/icon/TrashCan.png new file mode 100644 index 0000000000000000000000000000000000000000..1e67d3ed99f69d64be39c0a9dbdb51790e2d5e32 GIT binary patch literal 682 zcmV;b0#*HqP)4~n?er?9tz^6TJa=;UJ4#WEj_6h#UAwLM=y#9 zJ$N)#4=tFWM35XrG_?jH&XWDZGBfYJ`Mp20vr7ojk9a(KuqQUc z;L`y0%TTYI$`SBW0Mi|*^vj^Vu29wo#5YSj^2WK%r_@k47wa9+*_;^aoH^tUYUW z1?$5@%{8!BLv&aWiStUeQM8A~S=ATj!Sb&QENOkZJ&Juj5c?0< z^GN?0r>121Iw0SbO1~2HHdob{$2`dm=i2NTyX}Azr=T5A*z@SsYfeqE=miqS$@%~r zYa@B$zVvd-&UNpGrk_MRnb~jP!n|vaCw8VX_Z)7Eg8n$+;wQ0G=4NB%xregL-8%CX z>pa3aSdjN*q!p$(Y}SH`1NvnFx+p8Gw-I+!dAT7g$M^Ej@^06stn#Y20L4kxO4CpP Q4FCWD07*qoM6N<$f_N=5i~s-t literal 0 HcmV?d00001 diff --git a/public/icon/Writing.png b/public/icon/WhiteWriting.png similarity index 100% rename from public/icon/Writing.png rename to public/icon/WhiteWriting.png diff --git a/src/@types/models/assetSymbol.ts b/src/@types/models/assetSymbol.ts new file mode 100644 index 0000000..01449af --- /dev/null +++ b/src/@types/models/assetSymbol.ts @@ -0,0 +1,20 @@ +type AssetTypeSymbol = + | "현금" + | "신한은행" + | "국민은행" + | "우리은행" + | "하나은행" + | "기업은행" + | "농협은행" + | "신한카드" + | "현대카드" + | "삼성카드" + | "KB국민카드" + | "NH농협카드" + | "우리카드" + | "롯데카드" + | "비씨카드" + | "하나카드"; +type AssetGroupSymbol = "현금" | "은행" | "카드"; + +export type { AssetGroupSymbol, AssetTypeSymbol }; diff --git a/src/@types/models/createAsset.ts b/src/@types/models/createAsset.ts new file mode 100644 index 0000000..8e3bb95 --- /dev/null +++ b/src/@types/models/createAsset.ts @@ -0,0 +1,14 @@ +import { AssetGroupSymbol, AssetTypeSymbol } from "./assetSymbol"; + +/** + * 자산 추가 Response Interface + */ +export type CreateAsset = { + amount: number; + assetGroup: AssetGroupSymbol; + assetType: AssetTypeSymbol; + assetName: string; + statementDay: number | null; // 정산일. '카드'에만 사용합니다. + dueDay: number | null; // 결제일. '카드'에만 사용합니다. + memo: string; +}; diff --git a/src/@types/models/getAsset.ts b/src/@types/models/getAsset.ts new file mode 100644 index 0000000..d2f9172 --- /dev/null +++ b/src/@types/models/getAsset.ts @@ -0,0 +1,19 @@ +import { AssetGroupSymbol, AssetTypeSymbol } from "./assetSymbol"; + +/** + * 자산 조회 Response Interface + */ +export type GetAssets = GetAsset[]; + +export type GetAsset = { + amountId: number; + assetGroup: AssetGroupSymbol; + assetType: AssetTypeSymbol; + assetName: string; + amount: number; + statementDay: number | null; // 정산일. '카드'에만 사용합니다. + dueDay: number | null; // 결제일. '카드'에만 사용합니다. + memo: string; + createAt: string; + updatedAt: string; +}; diff --git a/src/@types/models/updateAsset.ts b/src/@types/models/updateAsset.ts new file mode 100644 index 0000000..52563d9 --- /dev/null +++ b/src/@types/models/updateAsset.ts @@ -0,0 +1,16 @@ +import { AssetGroupSymbol, AssetTypeSymbol } from "./assetSymbol"; + +/** + * 자산 수정 Response Interface + */ +export type UpdateAsset = { + assetGroup: AssetGroupSymbol; + assetType: AssetTypeSymbol; + assetName: string; + amount: number; + statementDay: number | null; + dueDay: number | null; + memo: string; + createdAt: string; + updatedAt: string; +}; diff --git a/src/App.tsx b/src/App.tsx index fb7a431..af49e03 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ import ChallengeDetailPage from "./pages/ChallengeDetailPage"; import CreateChallengeGroupPage from "./pages/CreateChallengeGroupPage"; import UpdateChallengeGroupPage from "./pages/UpdateChallengeGroupPage"; import SavingPage from "./pages/SavingPage"; +import SettingAssetManagementPage from "./pages/SettingAssetManagementPage"; function App() { const isLoginPage = window.location.pathname === "/"; @@ -39,6 +40,10 @@ function App() { path: "/challenge/update/:slug", element: , }, + { + path: "/setting/createasset", + element: , + }, { path: "/*", element: ( @@ -61,8 +66,8 @@ function App() { } /> } /> } /> - } /> } /> + } />
{!isLoginPage && !isSignUpPage && diff --git a/src/assets/height.ts b/src/assets/height.ts index 0511f7f..aa2136b 100644 --- a/src/assets/height.ts +++ b/src/assets/height.ts @@ -9,6 +9,9 @@ export const ChallengeTopContainerHeight = 92.75; export const BottomDateModalHeight = 470; export const ShortButtonHeight = 50; +export const BasicHeight = + ApplicationHeight - (HeaderHeight + NavigationItemsHeight); + export const AccountHeight = ApplicationHeight - (HeaderHeight + diff --git a/src/components/Common/LabelInput/index.tsx b/src/components/Common/LabelInput/index.tsx index e3b4506..cda042f 100644 --- a/src/components/Common/LabelInput/index.tsx +++ b/src/components/Common/LabelInput/index.tsx @@ -13,7 +13,7 @@ export interface LabelInputProps { type: string; label: string; inputId: string; - value: string | number; + value: string | number | null; placeholder: string; addContent?: "button" | "won" | "imageUpload"; onClick?: () => void; diff --git a/src/components/Common/NavigationItems/index.tsx b/src/components/Common/NavigationItems/index.tsx index 8a663ca..bfa2447 100644 --- a/src/components/Common/NavigationItems/index.tsx +++ b/src/components/Common/NavigationItems/index.tsx @@ -2,7 +2,7 @@ import AccountPNG from "@/public/icon/Account.png"; import StatsPNG from "@/public/icon/Stats.png"; import WroteMapPNG from "@/public/icon/WroteMap.png"; import ChallengePNG from "@/public/icon/Challenge.png"; -import MorePNG from "@/public/icon/More.png"; +import SettingsPNG from "@/public/icon/Settings.png"; import { NavigationItemsUI } from "./style"; import { useState } from "react"; import NavigationItem from "../NavigationItem"; @@ -45,7 +45,7 @@ function NavigationItems() { active: false, }, { - image: MorePNG, + image: SettingsPNG, categoryName: "더보기", address: "/setting", active: false, @@ -55,7 +55,7 @@ function NavigationItems() { const eventHandler = (item: bottomNavigationType) => { // console.log("item.name : ", item.categoryName); /** bottomNavigation에 활성화 버튼 변경하기 */ - const updatedNavigation = bottomNavigation.map((navItem) => { + const updatedNavigation = bottomNavigation.map(navItem => { // 클릭한 항목만 active를 true로, 나머지는 false로 설정 return { ...navItem, @@ -76,7 +76,7 @@ function NavigationItems() { return ( <> - {bottomNavigation.map((item) => ( + {bottomNavigation.map(item => ( eventHandler(item)} diff --git a/src/components/Common/SeparatedCategory/index.tsx b/src/components/Common/SeparatedCategory/index.tsx index e0b0dd9..1e35db1 100644 --- a/src/components/Common/SeparatedCategory/index.tsx +++ b/src/components/Common/SeparatedCategory/index.tsx @@ -1,5 +1,5 @@ -import CancelPNG from "../../../../public/icon/Cancel.png"; -import WritingPNG from "../../../../public/icon/Writing.png"; +import CancelPNG from "@/public/icon/Cancel.png"; +import WhiteWriting from "@/public/icon/WhiteWriting.png"; import { Button } from "@mui/material"; import { theme } from "../../../assets/theme"; import { SeparatedCategoryUI } from "./style"; @@ -53,7 +53,7 @@ function SeparatedCategory({ title }: SeparatedCategoryProps) { type="button" onClick={handleWritingButton} style={{ display: "inline-block", marginRight: "20px" }}> - WritingPNG + WritingPNG - {postSaveAccountBook.transactionType === "수입" && ( - - {incomeCategories.map(item => ( - - ))} - - )} - {postSaveAccountBook.transactionType === "지출" && ( - - {expenseCategories.map(item => ( - - ))} - - )} - {/* TODO: 지출일때 자산 카테고리에 지출 카테고리도 같이 표시되는 문제 해결하기 */} - {title === "결제수단 자산" && ( - - {assets.map(item => ( - - ))} - - )} + + {title === "카테고리 분류" && + postSaveAccountBook.transactionType === "수입" && ( + + {incomeCategories.map(item => ( + + ))} + + )} + {title === "카테고리 분류" && + postSaveAccountBook.transactionType === "지출" && ( + + {expenseCategories.map(item => ( + + ))} + + )} + {title === "결제수단 자산" && ( + + {assets.map(item => ( + + ))} + + )} + - + )} ); diff --git a/src/components/Common/SeparatedCategory/style.ts b/src/components/Common/SeparatedCategory/style.ts index c58c996..5db46aa 100644 --- a/src/components/Common/SeparatedCategory/style.ts +++ b/src/components/Common/SeparatedCategory/style.ts @@ -2,7 +2,7 @@ import styled from "@emotion/styled"; import { theme } from "@/src/assets/theme"; import { recordAccountBookHeight } from "@/src/assets/height"; -const Container = styled.div` +const Background = styled.div` width: 100%; height: ${recordAccountBookHeight}px; background-color: rgba(0, 0, 0, 0.5); @@ -35,15 +35,21 @@ const CategoryHeader = styled.div` align-items: center; `; +const GridWrapper = styled.div` + height: 400px; + overflow: auto; +`; + const GridContainer = styled.div` display: grid; grid-template-columns: repeat(4, 1fr); background-color: ${theme.font_color.white}; - overflow-y: auto; + overflow: auto; `; export const SeparatedCategoryUI = { - Container, + Background, CategoryHeader, + GridWrapper, GridContainer, } as const; diff --git a/src/components/InputArea/index.tsx b/src/components/InputArea/index.tsx index 881b9b9..4c5eb1d 100644 --- a/src/components/InputArea/index.tsx +++ b/src/components/InputArea/index.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent } from "react"; +import { ChangeEvent, useState } from "react"; import LabelInput from "../Common/LabelInput"; import { InputAreaUI } from "./style"; import SelectedImage from "../SelectedImage"; @@ -101,7 +101,6 @@ function InputArea() { inputId={"categoryName"} value={postSaveAccountBook.categoryName} placeholder={"카테고리를 선택해주세요."} - onClick={handleCategoryClick} readonly={true} /> diff --git a/src/core/api/accountBook.ts b/src/core/api/accountBook.ts index ecc3230..bc38ca4 100644 --- a/src/core/api/accountBook.ts +++ b/src/core/api/accountBook.ts @@ -3,6 +3,7 @@ import { APIInstance } from "./instance"; import { PostAccount } from "@/src/@types/models/postAccount"; import { PutAccount } from "@/src/@types/models/putAccount"; import { DeleteAccount } from "@/src/@types/models/deleteAccount"; +import { CategoryTypeSymbol } from "@/src/@types/models/categoryTypeSymbol"; const ACCOUNTS = "/accounts"; export const AccountBookAPI = { @@ -12,7 +13,7 @@ export const AccountBookAPI = { limit: number, page: number, startDate: string, - transactionType: string + transactionType: CategoryTypeSymbol ) => { return APIInstance.get( ACCOUNTS + @@ -29,23 +30,19 @@ export const AccountBookAPI = { saveAccountBook: (postAccountBook: { address: string; amount: number; - assetGroup: string; - assetType: string; assetName: string; categoryName: string; imageIds: number[]; isInstallment: boolean; memo: string; - recurringType: string; + recurringType: string | null; transactedAt: string; transactionDetail: string; - transactionType: string; + transactionType: CategoryTypeSymbol; }) => { return APIInstance.post(ACCOUNTS, { address: postAccountBook.address, amount: postAccountBook.amount, - assetGroup: postAccountBook.assetGroup, - assetType: postAccountBook.assetType, assetName: postAccountBook.assetName, categoryName: postAccountBook.categoryName, imageIds: postAccountBook.imageIds, @@ -72,7 +69,7 @@ export const AccountBookAPI = { recurringType: string; transactedAt: string; transactionDetail: string; - transactionType: string; + transactionType: CategoryTypeSymbol; }) => { return APIInstance.put( ACCOUNTS + `/${PutAccountBook.accountId}`, @@ -132,7 +129,7 @@ export const AccountBookAPI = { getTransactionStatistics: ( endDate: string, startDate: string, - transactionType: string + transactionType: CategoryTypeSymbol ) => { return APIInstance.get( ACCOUNTS + diff --git a/src/core/api/assets.ts b/src/core/api/assets.ts index de6a6d1..f8ffcc5 100644 --- a/src/core/api/assets.ts +++ b/src/core/api/assets.ts @@ -1,7 +1,7 @@ -import { CreateAssets } from "@/src/@types/models/createAssets"; -import { GetAssets } from "@/src/@types/models/getAssets"; +import { CreateAsset } from "@/src/@types/models/createAsset"; +import { GetAssets } from "@/src/@types/models/getAsset"; import { APIInstance } from "./instance"; -import { UpdateAssets } from "@/src/@types/models/updateAssets"; +import { UpdateAsset } from "@/src/@types/models/updateAsset"; const ASSETS = "/assets"; export const assetsAPI = { @@ -16,11 +16,11 @@ export const assetsAPI = { assetGroup: string, assetName: string, assetType: string, - dueDay: number, + dueDay: number | null, memo: string, - statementDay: number + statementDay: number | null ) => { - return APIInstance.post(ASSETS, { + return APIInstance.post(ASSETS, { amount: amount, assetGroup: assetGroup, assetName: assetName, @@ -39,12 +39,12 @@ export const assetsAPI = { assetName: string, assetType: string, createdAt: string, - dueDay: number, + dueDay: number | null, memo: string, - statementDay: number, + statementDay: number | null, updatedAt: string ) => { - return APIInstance.post(ASSETS + `/${assetId}`, { + return APIInstance.post(ASSETS + `/${assetId}`, { amount: amount, assetGroup: assetGroup, assetName: assetName, diff --git a/src/hooks/recoil/useGetAssets.ts b/src/hooks/recoil/useGetAssets.ts index cc4d4b4..48a767d 100644 --- a/src/hooks/recoil/useGetAssets.ts +++ b/src/hooks/recoil/useGetAssets.ts @@ -1,4 +1,4 @@ -import { GetAssets } from "@/src/@types/models/getAssets"; +import { GetAssets } from "@/src/@types/models/getAsset"; import { assetsAPI } from "@/src/core/api/assets"; import { atom } from "recoil"; diff --git a/src/hooks/recoil/useSaveAccountBook.ts b/src/hooks/recoil/useSaveAccountBook.ts index 5a57e68..8cfbb71 100644 --- a/src/hooks/recoil/useSaveAccountBook.ts +++ b/src/hooks/recoil/useSaveAccountBook.ts @@ -1,3 +1,4 @@ +import { CategoryTypeSymbol } from "@/src/@types/models/categoryTypeSymbol"; import { atom } from "recoil"; export interface SaveAccountBookAtomProps { @@ -9,30 +10,32 @@ export interface SaveAccountBookAtomProps { categoryName: string; imageIds: number[]; isInstallment: boolean; - installmentMonth: number | null; + installmentMonth: number; memo: string; - recurringType: string; + recurringType: string | null; transactedAt: string; transactionDetail: string; - transactionType: string; + transactionType: CategoryTypeSymbol; } +export const saveAccountBookInitial = { + address: "", + amount: 0, + assetGroup: "", + assetType: "", + assetName: "", + categoryName: "", + imageIds: [], + isInstallment: false, + installmentMonth: 0, + memo: "", + recurringType: null, + transactedAt: "", + transactionDetail: "", + transactionType: "수입" as CategoryTypeSymbol, +}; + export const saveAccountBookAtom = atom({ key: "saveAccountBookAtom", - default: { - address: "", - amount: 0, - assetGroup: "", - assetType: "", - assetName: "", - categoryName: "", - imageIds: [], - isInstallment: false, - installmentMonth: 0, - memo: "", - recurringType: "", - transactedAt: "", - transactionDetail: "", - transactionType: "", - }, + default: saveAccountBookInitial, }); diff --git a/src/pages/RecordAccountBookPage/index.tsx b/src/pages/RecordAccountBookPage/index.tsx index d8eb834..6ccfc8c 100644 --- a/src/pages/RecordAccountBookPage/index.tsx +++ b/src/pages/RecordAccountBookPage/index.tsx @@ -4,25 +4,28 @@ import ShortButton from "../../components/Common/ShortButton"; import IncomeExpenseButton from "../../components/IncomeExpenseButton"; import InputArea from "../../components/InputArea"; import { AccountBookAPI } from "@/src/core/api/accountBook"; -import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; +import { + saveAccountBookAtom, + saveAccountBookInitial, +} from "@/src/hooks/recoil/useSaveAccountBook"; import { useNavigate } from "react-router-dom"; function RecordAccountBookPage() { const navigate = useNavigate(); - const [postSaveAccountBook] = useRecoilState(saveAccountBookAtom); + const [postSaveAccountBook, setPostSaveAccountBook] = + useRecoilState(saveAccountBookAtom); const handleCancelButton = () => { console.log("저장 안하고 가계부 페이지로 이동"); + setPostSaveAccountBook(saveAccountBookInitial); navigate("/account"); }; const handleSaveButton = () => { AccountBookAPI.saveAccountBook(postSaveAccountBook) .then(response => { - if (response.status === 200) { - console.log("가계부 저장 성공: ", response.data); - navigate("/account"); - } + console.log("가계부 저장 성공: ", response.data); + navigate("/account"); }) .catch(error => { console.log("가계부 저장 실패: ", error); @@ -39,7 +42,7 @@ function RecordAccountBookPage() { isAddButton={false} isMoreButton={false} /> -
+
- +
); } From 2fa155f1d6e61d896e45ef98de42f25ae2be2f64 Mon Sep 17 00:00:00 2001 From: Whale2200d Date: Sun, 17 Dec 2023 19:33:01 +0900 Subject: [PATCH 15/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20[IMPROVE]=20?= =?UTF-8?q?=EC=B7=A8=EC=86=8C=EB=B2=84=ED=8A=BC=20=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EC=8B=9C,=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreateSettingAssetManagement/index.tsx | 32 ++++++++++--------- src/hooks/recoil/useCreateAsset.ts | 26 +++++++++------ 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/components/CreateSettingAssetManagement/index.tsx b/src/components/CreateSettingAssetManagement/index.tsx index 595539b..4560f94 100644 --- a/src/components/CreateSettingAssetManagement/index.tsx +++ b/src/components/CreateSettingAssetManagement/index.tsx @@ -4,7 +4,10 @@ import LabelInput from "../Common/LabelInput"; import { LabelInputUI } from "../Common/LabelInput/style"; import { UpdateChallengeGroupUI } from "../UpdateChallengeGroup/style"; import { useRecoilState } from "recoil"; -import { createAssetAtom } from "@/src/hooks/recoil/useCreateAsset"; +import { + createAssetAtom, + createAssetInitial, +} from "@/src/hooks/recoil/useCreateAsset"; import { AssetGroupSymbol, AssetTypeSymbol, @@ -25,6 +28,18 @@ function CreateSettingAssetManagement() { ...prev, assetGroup: assetGroup, })); + if (assetGroup === "은행") { + setCreateAsset(prev => ({ + ...prev, + assetType: "신한은행", + })); + } + if (assetGroup === "카드") { + setCreateAsset(prev => ({ + ...prev, + assetType: "KB국민카드", + })); + } setIsOpenAssetGroupModal(false); }; /* COMPLETED: assetType 데이터 추가 */ @@ -88,6 +103,7 @@ function CreateSettingAssetManagement() { }; const handleCancelButton = () => { + setCreateAsset(createAssetInitial); navigate(-1); }; @@ -129,20 +145,6 @@ function CreateSettingAssetManagement() { {createAsset.dueDay} - {/* */} - {/* */} )} ({ +export const createAssetInitial = { + amount: 0, + assetGroup: "현금", + assetType: "현금", + assetName: "", + statementDay: 0, + dueDay: 0, + memo: "", +}; + +export const createAssetAtom = atom({ key: "createAssetAtom", - default: { - amount: 0, - assetGroup: "현금", - assetType: "현금", - assetName: "", - statementDay: 0, - dueDay: 0, - memo: "", - }, + default: createAssetInitial, }); From dd73f441491b71a264688f57779e389978f8dd4e Mon Sep 17 00:00:00 2001 From: Whale2200d Date: Sun, 17 Dec 2023 19:41:55 +0900 Subject: [PATCH 16/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20[IMPROVE]=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20type=20models=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/@types/models/createAssets.ts | 13 ------------- src/@types/models/getAssets.ts | 18 ------------------ src/@types/models/updateAssets.ts | 15 --------------- 3 files changed, 46 deletions(-) delete mode 100644 src/@types/models/createAssets.ts delete mode 100644 src/@types/models/getAssets.ts delete mode 100644 src/@types/models/updateAssets.ts diff --git a/src/@types/models/createAssets.ts b/src/@types/models/createAssets.ts deleted file mode 100644 index 37689f1..0000000 --- a/src/@types/models/createAssets.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * 자산 추가 Response Interface - */ - -export type CreateAssets = { - amount: number; - assetGroup: string; - assetName: string; - assetType: string; - dueDay: number; - memo: string; - statementDay: number; -}; diff --git a/src/@types/models/getAssets.ts b/src/@types/models/getAssets.ts deleted file mode 100644 index 32ba54d..0000000 --- a/src/@types/models/getAssets.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * 자산 목록 조회 Response Interface - */ - -export type GetAssets = GetAsset[]; - -export type GetAsset = { - amount: number; - assetGroup: string; - assetId: number; - assetName: string; - assetType: string; - createdAt: string; - dueDay: string; - memo: string; - statementDay: number; - updatedAt: string; -}; diff --git a/src/@types/models/updateAssets.ts b/src/@types/models/updateAssets.ts deleted file mode 100644 index d98df5e..0000000 --- a/src/@types/models/updateAssets.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * 자산 수정 Response Interface - */ - -export type UpdateAssets = { - amount: number; - assetGroup: string; - assetName: string; - assetType: string; - createdAt: string; - dueDay: number; - memo: string; - statementDay: number; - updatedAt: string; -}; From faa74f44e9a1298fe3ca5ce89ee0af7477756e04 Mon Sep 17 00:00:00 2001 From: Sojung Park Date: Sun, 17 Dec 2023 19:57:16 +0900 Subject: [PATCH 17/29] =?UTF-8?q?=F0=9F=9A=9A=20[RENAME]=20getAsset.ts=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/@types/models/{getAsset.ts => getAssets.ts} | 0 src/components/Common/SeparatedCategory/index.tsx | 2 +- src/core/api/assets.ts | 2 +- src/hooks/recoil/useGetAssets.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/@types/models/{getAsset.ts => getAssets.ts} (100%) diff --git a/src/@types/models/getAsset.ts b/src/@types/models/getAssets.ts similarity index 100% rename from src/@types/models/getAsset.ts rename to src/@types/models/getAssets.ts diff --git a/src/components/Common/SeparatedCategory/index.tsx b/src/components/Common/SeparatedCategory/index.tsx index a6a4df8..93a2b23 100644 --- a/src/components/Common/SeparatedCategory/index.tsx +++ b/src/components/Common/SeparatedCategory/index.tsx @@ -9,7 +9,7 @@ import { openSeparatedCategoryAtom } from "@/src/hooks/recoil/useOpenSeparatedCa import { getCategoriesAtom } from "@/src/hooks/recoil/useGetCategories"; import { saveAccountBookAtom } from "@/src/hooks/recoil/useSaveAccountBook"; import { GetCategory } from "@/src/@types/models/getCategories"; -import { GetAsset } from "@/src/@types/models/getAsset"; +import { GetAsset } from "@/src/@types/models/getAssets"; import { getAssetsAtom } from "@/src/hooks/recoil/useGetAssets"; interface SeparatedCategoryProps { diff --git a/src/core/api/assets.ts b/src/core/api/assets.ts index f8ffcc5..44d3a78 100644 --- a/src/core/api/assets.ts +++ b/src/core/api/assets.ts @@ -1,5 +1,5 @@ import { CreateAsset } from "@/src/@types/models/createAsset"; -import { GetAssets } from "@/src/@types/models/getAsset"; +import { GetAssets } from "@/src/@types/models/getAssets"; import { APIInstance } from "./instance"; import { UpdateAsset } from "@/src/@types/models/updateAsset"; diff --git a/src/hooks/recoil/useGetAssets.ts b/src/hooks/recoil/useGetAssets.ts index 48a767d..cc4d4b4 100644 --- a/src/hooks/recoil/useGetAssets.ts +++ b/src/hooks/recoil/useGetAssets.ts @@ -1,4 +1,4 @@ -import { GetAssets } from "@/src/@types/models/getAsset"; +import { GetAssets } from "@/src/@types/models/getAssets"; import { assetsAPI } from "@/src/core/api/assets"; import { atom } from "recoil"; From 701d804c47bea1b5ab04dbe12c8d79989a1acb94 Mon Sep 17 00:00:00 2001 From: Sojung Park Date: Sun, 17 Dec 2023 20:06:01 +0900 Subject: [PATCH 18/29] =?UTF-8?q?=F0=9F=9A=9A=20[RENAME]=20getAsset.ts=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/@types/models/getAsset.ts | 19 ------------------- src/@types/models/getAssets.ts | 19 ++++++++++--------- 2 files changed, 10 insertions(+), 28 deletions(-) delete mode 100644 src/@types/models/getAsset.ts diff --git a/src/@types/models/getAsset.ts b/src/@types/models/getAsset.ts deleted file mode 100644 index d2f9172..0000000 --- a/src/@types/models/getAsset.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { AssetGroupSymbol, AssetTypeSymbol } from "./assetSymbol"; - -/** - * 자산 조회 Response Interface - */ -export type GetAssets = GetAsset[]; - -export type GetAsset = { - amountId: number; - assetGroup: AssetGroupSymbol; - assetType: AssetTypeSymbol; - assetName: string; - amount: number; - statementDay: number | null; // 정산일. '카드'에만 사용합니다. - dueDay: number | null; // 결제일. '카드'에만 사용합니다. - memo: string; - createAt: string; - updatedAt: string; -}; diff --git a/src/@types/models/getAssets.ts b/src/@types/models/getAssets.ts index a75e3a1..d2f9172 100644 --- a/src/@types/models/getAssets.ts +++ b/src/@types/models/getAssets.ts @@ -1,18 +1,19 @@ +import { AssetGroupSymbol, AssetTypeSymbol } from "./assetSymbol"; + /** - * 자산 목록 조회 Response Interface + * 자산 조회 Response Interface */ - export type GetAssets = GetAsset[]; export type GetAsset = { - amount: number; - assetGroup: string; - assetId: number; + amountId: number; + assetGroup: AssetGroupSymbol; + assetType: AssetTypeSymbol; assetName: string; - assetType: string; - createdAt: string; - dueDay: string | null; + amount: number; + statementDay: number | null; // 정산일. '카드'에만 사용합니다. + dueDay: number | null; // 결제일. '카드'에만 사용합니다. memo: string; - statementDay: number | null; + createAt: string; updatedAt: string; }; From 2a04239ee9097717359927a68d37329362d6abb6 Mon Sep 17 00:00:00 2001 From: Whale2200d Date: Sun, 17 Dec 2023 20:15:11 +0900 Subject: [PATCH 19/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20[IMPROVE]=20=20Separ?= =?UTF-8?q?etedCategory=20Type=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Common/SeparatedCategory/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Common/SeparatedCategory/index.tsx b/src/components/Common/SeparatedCategory/index.tsx index 03245eb..6bb5d5b 100644 --- a/src/components/Common/SeparatedCategory/index.tsx +++ b/src/components/Common/SeparatedCategory/index.tsx @@ -86,7 +86,7 @@ function SeparatedCategory({ title }: SeparatedCategoryProps) { type="button" onClick={handleWritingButton} style={{ display: "inline-block", marginRight: "20px" }}> - WritingPNG + WritingPNG