diff --git a/frontend/public/assets/back-button.svg b/frontend/public/assets/back-button.svg new file mode 100644 index 0000000..53ccca3 --- /dev/null +++ b/frontend/public/assets/back-button.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/Header/index.jsx b/frontend/src/components/Header/index.jsx index 94339f4..a807fa1 100644 --- a/frontend/src/components/Header/index.jsx +++ b/frontend/src/components/Header/index.jsx @@ -22,13 +22,13 @@ const Header = ({ current }) => { - + 내역 - + 달력 - + 통계 diff --git a/frontend/src/components/Header/style.js b/frontend/src/components/Header/style.js index 1278cce..be94aa7 100644 --- a/frontend/src/components/Header/style.js +++ b/frontend/src/components/Header/style.js @@ -37,7 +37,7 @@ export const Title = styled.h1` `; export const DateBox = styled.div` - margin-top: 7px; + margin: 7px 0px 0px 0px; display: flex; flex-direction: row; @@ -48,10 +48,12 @@ export const DateBox = styled.div` `; export const ArrowButton = styled.button` - margin-top: 3px; + margin: 3px 0px 0px 15px; `; export const Date = styled.strong` + margin-left: 15px; + font-weight: bold; color: ${({ theme }) => theme.color.white}; `; @@ -77,7 +79,7 @@ export const PageTarget = styled(Link)` color: ${({ theme }) => (props) => - props.isSelected ? theme.color.white : theme.color.black}; + props.selected ? theme.color.white : theme.color.black}; &:hover { cursor: pointer; diff --git a/frontend/src/components/Transaction/index.jsx b/frontend/src/components/Transaction/index.jsx index 5c09edd..c3c7b94 100644 --- a/frontend/src/components/Transaction/index.jsx +++ b/frontend/src/components/Transaction/index.jsx @@ -1,11 +1,23 @@ import React from 'react'; +import { useSetRecoilState } from 'recoil'; import PropTypes from 'prop-types'; +import { modalState } from '../../recoil/modal/atom'; import { Wrapper, OuterBox, Category, InnerBox, Content, Method, Cost } from './style'; const Transaction = ({ transaction }) => { + const setCurrentModal = useSetRecoilState(modalState); + + const changeModalState = () => { + const state = { + current: 'transaction', + props: transaction, + }; + setCurrentModal(state); + }; + return ( - + {transaction.category} diff --git a/frontend/src/components/TransactionModal/hooks.js b/frontend/src/components/TransactionModal/hooks.js new file mode 100644 index 0000000..50df277 --- /dev/null +++ b/frontend/src/components/TransactionModal/hooks.js @@ -0,0 +1,13 @@ +import { useRecoilState } from 'recoil'; + +import { modalState } from '../../recoil/modal/atom'; + +export const useModal = () => { + const [modal, setModal] = useRecoilState(modalState); + + const closeModal = () => { + setModal({ current: null, props: null }); + }; + + return [modal, closeModal]; +}; diff --git a/frontend/src/components/TransactionModal/index.jsx b/frontend/src/components/TransactionModal/index.jsx new file mode 100644 index 0000000..512fa52 --- /dev/null +++ b/frontend/src/components/TransactionModal/index.jsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import TransactionUpdateForm from '../TransactionUpdateForm'; +import { useModal } from './hooks'; +import { Wrapper, BackgroundDim } from './style'; + +const TransactionModal = () => { + const [modal, closeModal] = useModal(); + + if (modal.props == null) return null; + return ( + + + null} + onDelete={() => null} + onCancle={closeModal} + /> + + ); +}; + +export default TransactionModal; diff --git a/frontend/src/components/TransactionModal/style.js b/frontend/src/components/TransactionModal/style.js new file mode 100644 index 0000000..9e1113b --- /dev/null +++ b/frontend/src/components/TransactionModal/style.js @@ -0,0 +1,18 @@ +import styled from 'styled-components'; + +export const Wrapper = styled.div` + display: ${(props) => (props.active ? 'block' : 'none')}; +`; + +export const BackgroundDim = styled.div` + position: fixed; + z-index: 1; + + top: 0; + left: 0; + width: 100vw; + height: 100vh; + + background-color: ${({ theme }) => theme.color.black}; + opacity: 0.57; +`; diff --git a/frontend/src/components/TransactionModal/test.js b/frontend/src/components/TransactionModal/test.js new file mode 100644 index 0000000..7c36869 --- /dev/null +++ b/frontend/src/components/TransactionModal/test.js @@ -0,0 +1,49 @@ +import React from 'react'; +import { RecoilRoot } from 'recoil'; +import { ThemeProvider } from 'styled-components'; +import { render, screen } from '../../test-utils'; +import '@testing-library/jest-dom'; + +import TransactionModal from '.'; +import { modalState } from '../../recoil/modal/atom'; +import theme from '../../styles/theme'; + +const TEST_DATA = { + category: '카페/간식', + color: '#D092E2', + content: '녹차 스무디', + method: '현금', + sign: '-', + cost: '5700', + date: '2022-01-28', +}; + +describe('TransactionModal 컴포넌트 테스트', () => { + const initializeState = ({ set }) => { + set(modalState, { + current: 'transaction', + props: TEST_DATA, + }); + }; + + it('배경 Dim, 입력 폼이 표시된다.', () => { + render( + + + + + , + ); + + const BackgroundDim = screen.getByTestId('dim'); + const transactionForm = screen.getByRole('form', { name: /transaction/i }); + expect(BackgroundDim).toBeInTheDocument(); + expect(transactionForm).toBeInTheDocument(); + }); + + it('현재 선택된 modal의 props 속성이 없으면 모달이 표시되지 않는다.', () => { + render(); + const modalDiv = screen.queryByTestId('modal'); + expect(modalDiv).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/TransactionUpdateForm/index.jsx b/frontend/src/components/TransactionUpdateForm/index.jsx new file mode 100644 index 0000000..1b78411 --- /dev/null +++ b/frontend/src/components/TransactionUpdateForm/index.jsx @@ -0,0 +1,94 @@ +import React from 'react'; + +import back from '../../../public/assets/back-button.svg'; +import { Wrapper, Element, BackImg, Input, ButtonContainer, DecisionButton } from './style'; + +const TransactionUpdateForm = ({ transaction, onUpdate, onDelete, onCancle }) => { + const shapedForm = (elements) => { + const data = {}; + Array.from(elements).forEach((element) => { + if (element.value) { + data[element.id] = element.value; + } + }); + return data; + }; + + const handleUpdateSubmit = (e) => { + e.preventDefault(); + + const form = e.target; + const data = shapedForm(form.elements); + onUpdate(data); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + 삭제 + + + 수정 + + + + ); +}; + +export default TransactionUpdateForm; diff --git a/frontend/src/components/TransactionUpdateForm/style.js b/frontend/src/components/TransactionUpdateForm/style.js new file mode 100644 index 0000000..4bf5047 --- /dev/null +++ b/frontend/src/components/TransactionUpdateForm/style.js @@ -0,0 +1,88 @@ +import styled from 'styled-components'; + +import { MAX_MOBILE_DEVICE } from '../../utils/constant/device-size'; + +export const Wrapper = styled.form` + position: fixed; + z-index: 2; + + width: 700px; + height: 500px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + background-color: ${({ theme }) => theme.color.white}; + box-shadow: ${({ theme }) => theme.shadow.thick}; + + @media screen and (max-width: ${MAX_MOBILE_DEVICE}px) { + width: 100vw; + height: 100vh; + top: 0; + left: 0; + transform: none; + + justify-content: space-evenly; + + box-shadow: none; + } +`; + +export const Element = styled.div` + margin-bottom: 10px; + width: 250px; + + display: flex; + flex-direction: column; + + font-weight: bold; + + @media screen and (max-width: ${MAX_MOBILE_DEVICE}px) { + width: 80vw; + height: 12vh; + + justify-content: center; + } +`; + +export const BackImg = styled.img` + float: left; +`; + +export const Input = styled.input` + margin-top: 5px; + padding: 10px; + + border: 1px solid ${({ theme }) => theme.color.brigtenL1Gray}; + border-radius: 10px; + background: ${({ theme }) => theme.color.whiteSmoke}; + + @media screen and (max-width: ${MAX_MOBILE_DEVICE}px) { + height: 80%; + } +`; + +export const ButtonContainer = styled.div` + padding-left: 0px; + width: 250px; + + display: flex; + flex-direction: row; + justify-content: space-between; + + @media screen and (max-width: ${MAX_MOBILE_DEVICE}px) { + width: 80vw; + height: 12vh; + } +`; + +export const DecisionButton = styled.button` + font-weight: bold; + font-size: ${({ theme }) => theme.fontSize.default}; + color: ${(props) => (props.action === 'update' ? '#0990d6' : '#ff000f')}; +`; diff --git a/frontend/src/components/TransactionUpdateForm/test.js b/frontend/src/components/TransactionUpdateForm/test.js new file mode 100644 index 0000000..7da6213 --- /dev/null +++ b/frontend/src/components/TransactionUpdateForm/test.js @@ -0,0 +1,91 @@ +import React from 'react'; +import { render, screen, fireEvent } from '../../test-utils'; + +import TransactionUpdateForm from '.'; + +const TEST_DATA = { + category: '카페/간식', + content: '녹차 스무디', + method: '현금', + cost: '5700', + date: '2022-01-28', +}; + +describe('TransactionUpateForm 테스트', () => { + it('기본 요소들이 표시된다.', () => { + render( + , + ); + + const backButton = screen.getByRole('button', { name: 'back' }); + const dateInput = screen.getByLabelText('날짜'); + const categoryInput = screen.getByLabelText('카테고리'); + const contentInput = screen.getByLabelText('내용'); + const methodInput = screen.getByLabelText('결제수단'); + const costInput = screen.getByLabelText('금액'); + const deleteButton = screen.getByRole('button', { name: '삭제' }); + const updateButton = screen.getByRole('button', { name: '수정' }); + + expect(backButton).toBeInTheDocument(); + expect(dateInput.value).toEqual(TEST_DATA['date']); + expect(categoryInput.value).toEqual(TEST_DATA['category']); + expect(contentInput.value).toEqual(TEST_DATA['content']); + expect(methodInput.value).toEqual(TEST_DATA['method']); + expect(costInput.value).toEqual(TEST_DATA['cost']); + expect(deleteButton).toBeInTheDocument(); + expect(updateButton).toBeInTheDocument(); + }); + + it('뒤로가기 버튼을 누르면 onCancle 함수가 호출된다.', () => { + const onCancle = jest.fn(); + render( + , + ); + + const backButton = screen.getByRole('button', { name: 'back' }); + fireEvent.click(backButton); + expect(onCancle).toHaveBeenCalled(); + }); + + it('삭제 버튼을 누르면 onDelete 함수가 호출된다.', () => { + const onDelete = jest.fn(); + render( + , + ); + + const deleteButton = screen.getByRole('button', { name: '삭제' }); + fireEvent.click(deleteButton); + expect(onDelete).toHaveBeenCalled(); + }); + + it('수정 버튼을 누르면 form 데이터를 기반으로 onUpdate 함수가 호출된다.', () => { + const onUpdate = jest.fn(); + render( + , + ); + + const updateButton = screen.getByRole('button', { name: '수정' }); + fireEvent.click(updateButton); + expect(onUpdate).toHaveBeenCalledWith(TEST_DATA); + }); +}); diff --git a/frontend/src/components/Transactions/index.jsx b/frontend/src/components/Transactions/index.jsx index d21c3e0..1631bd4 100644 --- a/frontend/src/components/Transactions/index.jsx +++ b/frontend/src/components/Transactions/index.jsx @@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil'; import { nanoid } from 'nanoid'; import DailyTransaction from '../DailyTransaction'; +import TransactionModal from '../TransactionModal'; import { useFilterdTransactions } from './hooks'; import { checkState } from '../../recoil/check/atom'; import { TransactionsContainer } from './style'; @@ -30,7 +31,12 @@ const Transactions = () => { /> )); - return {shapedTransactions}; + return ( + <> + {shapedTransactions} + + + ); }; export default Transactions; diff --git a/frontend/src/recoil/modal/atom.js b/frontend/src/recoil/modal/atom.js new file mode 100644 index 0000000..2be3fbf --- /dev/null +++ b/frontend/src/recoil/modal/atom.js @@ -0,0 +1,9 @@ +import { atom } from 'recoil'; + +export const modalState = atom({ + key: 'modalState', + default: { + current: null, + props: null, + }, +}); diff --git a/frontend/src/styles/GlobalStyle.js b/frontend/src/styles/GlobalStyle.js index a27d6ac..2183007 100644 --- a/frontend/src/styles/GlobalStyle.js +++ b/frontend/src/styles/GlobalStyle.js @@ -134,6 +134,7 @@ const GlobalStyle = createGlobalStyle` } button { + padding: 0px; border: none; background: none; cursor: pointer; diff --git a/frontend/src/styles/theme.js b/frontend/src/styles/theme.js index 1a8ddc7..c8b04e4 100644 --- a/frontend/src/styles/theme.js +++ b/frontend/src/styles/theme.js @@ -9,9 +9,11 @@ const fontSize = { const color = { mint: '#06d6a0', blue: '#0990d6', - white: '#FFFFFF', black: '#000000', + white: '#FFFFFF', + whiteSmoke: '#F5F5F5', + gray: '#808080', brigtenL1Gray: '#d7d7d7', brigtenL2Gray: '#f5f5f5',