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',