diff --git a/src/LecueNote/api/getPresignedUrl.ts b/src/LecueNote/api/getPresignedUrl.ts new file mode 100644 index 00000000..e232a2e6 --- /dev/null +++ b/src/LecueNote/api/getPresignedUrl.ts @@ -0,0 +1,8 @@ +import { api } from '../../libs/api'; + +const getPresignedUrl = async () => { + const { data } = await api.get('/api/images/note'); + return data; +}; + +export default getPresignedUrl; diff --git a/src/LecueNote/api/postLecueNote.ts b/src/LecueNote/api/postLecueNote.ts new file mode 100644 index 00000000..d23b5a36 --- /dev/null +++ b/src/LecueNote/api/postLecueNote.ts @@ -0,0 +1,38 @@ +// import { useNavigate } from 'react-router-dom'; + +import { api } from '../../libs/api'; +import { postLecueNoteProps } from '../type/lecueNoteType'; + +const postLecueNote = ({ + contents, + color, + fileName, + bgColor, +}: postLecueNoteProps) => { + // const navigate = useNavigate(); + + const response = api + .post( + '/api/notes', + { + bookId: 1, + content: contents, + textColor: color, + background: fileName ? fileName : bgColor, + }, + { + headers: { + Authorization: `Bearer ${import.meta.env.VITE_APP_TOKEN}`, + }, + }, + ) + .then((res) => { + console.log(res); + // 나중에 주석코드를 활성화시킬 예정! + // navigate(`lecue-book/${res.data.data.bookUuid}`); + }); + + return response; +}; + +export default postLecueNote; diff --git a/src/LecueNote/api/putPresignedUrl.ts b/src/LecueNote/api/putPresignedUrl.ts new file mode 100644 index 00000000..f09f1c8e --- /dev/null +++ b/src/LecueNote/api/putPresignedUrl.ts @@ -0,0 +1,18 @@ +import { api } from '../../libs/api'; +import { putPresignedUrlProps } from '../type/lecueNoteType'; + +const putPresignedUrl = ({ + presignedUrl, + binaryFile, + fileType, +}: putPresignedUrlProps) => { + const response = api.put(presignedUrl, binaryFile, { + headers: { + 'Content-Type': fileType, + }, + }); + + return response; +}; + +export default putPresignedUrl; diff --git a/src/LecueNote/components/CreateNote/CreateNote.style.ts b/src/LecueNote/components/CreateNote/CreateNote.style.ts index 9fcb1774..28ce432b 100644 --- a/src/LecueNote/components/CreateNote/CreateNote.style.ts +++ b/src/LecueNote/components/CreateNote/CreateNote.style.ts @@ -2,10 +2,9 @@ import styled from '@emotion/styled'; export const Wrapper = styled.section` display: flex; + gap: 3.2rem; flex-direction: column; width: 100%; margin: 7.8rem 0 3.3rem; - - gap: 3.2rem; `; diff --git a/src/LecueNote/components/CreateNote/index.tsx b/src/LecueNote/components/CreateNote/index.tsx index 783b7711..67b42f52 100644 --- a/src/LecueNote/components/CreateNote/index.tsx +++ b/src/LecueNote/components/CreateNote/index.tsx @@ -1,43 +1,42 @@ -import { useState } from 'react'; -import { - BG_COLOR_CHART, - CATEGORY, - TEXT_COLOR_CHART, -} from '../../constants/colorChart'; +import { CreateNoteProps } from '../../type/lecueNoteType'; import SelectColor from '../SelectColor'; import WriteNote from '../WriteNote'; import * as S from './CreateNote.style'; -function CreateNote() { - const [clickedCategory, setclickedCategory] = useState(CATEGORY[0]); - const [clickedTextColor, setClickedTextColor] = useState(TEXT_COLOR_CHART[0]); - const [clickedBgColor, setclickedBgColor] = useState(BG_COLOR_CHART[0]); - - const handleClickCategory = ( - e: React.MouseEvent, - ) => { - setclickedCategory(e.currentTarget.innerHTML); - }; - - const handleClickedColorBtn = ( - e: React.MouseEvent, - ) => { - if (clickedCategory === '텍스트색') { - setClickedTextColor(e.currentTarget.id); - } else { - setclickedBgColor(e.currentTarget.id); - } - }; - +function CreateNote({ + clickedCategory, + clickedBgColor, + clickedTextColor, + isIconClicked, + contents, + setFileName, + handleChangeFn, + handleClickCategory, + handleClickedColorBtn, + handleClickedIcon, + imgFile, + uploadImage, +}: CreateNoteProps) { return ( - + ); diff --git a/src/LecueNote/components/Footer/index.tsx b/src/LecueNote/components/Footer/index.tsx index 38a8f217..e255dbd7 100644 --- a/src/LecueNote/components/Footer/index.tsx +++ b/src/LecueNote/components/Footer/index.tsx @@ -1,10 +1,27 @@ import Button from '../../../components/common/Button'; +import usePostLecueNote from '../../hooks/usePostLecueNote'; +import { FooterProps } from '../../type/lecueNoteType'; import * as S from './Footer.style'; -function Footer() { +function Footer({ contents, fileName, textColor, bgColor }: FooterProps) { + const postMutation = usePostLecueNote(); + + const handleClickBtn = () => { + postMutation.mutate({ + contents: contents, + color: textColor, + fileName: fileName, + bgColor: bgColor, + }); + }; + return ( - diff --git a/src/LecueNote/components/SelectColor/SelectColor.style.ts b/src/LecueNote/components/SelectColor/SelectColor.style.ts index e07545f1..4c0c4e56 100644 --- a/src/LecueNote/components/SelectColor/SelectColor.style.ts +++ b/src/LecueNote/components/SelectColor/SelectColor.style.ts @@ -3,14 +3,12 @@ import styled from '@emotion/styled'; export const Wrapper = styled.article` display: flex; - flex-direction: column; - gap: 1.8rem; + flex-direction: column; `; export const CategoryWrapper = styled.div` display: flex; - gap: 1.4rem; `; diff --git a/src/LecueNote/components/SelectColor/index.tsx b/src/LecueNote/components/SelectColor/index.tsx index a88032f9..8163a7e1 100644 --- a/src/LecueNote/components/SelectColor/index.tsx +++ b/src/LecueNote/components/SelectColor/index.tsx @@ -8,11 +8,15 @@ import ShowColorChart from '../ShowColorChart'; import * as S from './SelectColor.style'; function SelectColor({ + isIconClicked, clickedCategory, clickedTextColor, clickedBgColor, + setFileName, handleCategoryFn, handleColorFn, + handleIconFn, + uploadImage, }: SelectColorProps) { return ( @@ -33,15 +37,23 @@ function SelectColor({ {clickedCategory === '텍스트색' ? ( ) : ( )} diff --git a/src/LecueNote/components/ShowColorChart/ShowColorChart.style.ts b/src/LecueNote/components/ShowColorChart/ShowColorChart.style.ts index b432ee29..4a376f32 100644 --- a/src/LecueNote/components/ShowColorChart/ShowColorChart.style.ts +++ b/src/LecueNote/components/ShowColorChart/ShowColorChart.style.ts @@ -3,14 +3,31 @@ import styled from '@emotion/styled'; export const Wrapper = styled.div` display: flex; + gap: 1.4rem; justify-content: flex-start; align-items: center; padding: 0.5rem 0.1rem 0.7rem 0.3rem; overflow-x: scroll; +`; - gap: 1.4rem; +export const Input = styled.input` + display: none; +`; + +export const IconWrapper = styled.button<{ $isIconClicked: boolean }>` + ${({ theme, $isIconClicked }) => + $isIconClicked && + css` + outline: 0.1rem solid ${theme.colors.WG}; + `}; + flex-shrink: 0; + + width: 3.8rem; + height: 3.8rem; + + border-radius: 3rem; `; export const ColorWrapper = styled.div` @@ -23,7 +40,11 @@ export const ColorWrapper = styled.div` height: 3.8rem; `; -export const Color = styled.button<{ $colorCode: string; variant: boolean }>` +export const Color = styled.button<{ + $isIconClicked: boolean; + $colorCode: string; + variant: boolean; +}>` border-radius: 3rem; ${({ $colorCode, theme }) => $colorCode === '#FFF' && @@ -32,19 +53,26 @@ export const Color = styled.button<{ $colorCode: string; variant: boolean }>` `}; background-color: ${({ $colorCode }) => $colorCode}; - ${({ variant, theme }) => - variant + ${({ variant, theme, $isIconClicked }) => + $isIconClicked ? css` - width: 3.8rem; - height: 3.8rem; - - border: 0.4rem solid ${theme.colors.white}; - outline: 0.1rem solid ${theme.colors.WG}; - ` - : css` width: 3rem; height: 3rem; border: none; - `}; + ` + : variant + ? css` + width: 3.8rem; + height: 3.8rem; + + border: 0.4rem solid ${theme.colors.white}; + outline: 0.1rem solid ${theme.colors.WG}; + ` + : css` + width: 3rem; + height: 3rem; + + border: none; + `}; `; diff --git a/src/LecueNote/components/ShowColorChart/index.tsx b/src/LecueNote/components/ShowColorChart/index.tsx index 3a853cc1..4a68a0e1 100644 --- a/src/LecueNote/components/ShowColorChart/index.tsx +++ b/src/LecueNote/components/ShowColorChart/index.tsx @@ -1,15 +1,95 @@ +import { useEffect, useRef, useState } from 'react'; + +import { IcCameraSmall } from '../../../assets'; +import { BG_COLOR_CHART } from '../../constants/colorChart'; +import useGetPresignedUrl from '../../hooks/useGetPresignedUrl'; +import usePutPresignedUrl from '../../hooks/usePutPresignedUrl'; import { ShowColorChartProps } from '../../type/lecueNoteType'; import * as S from './ShowColorChart.style'; -function ShowColorChart({ colorChart, state, handleFn }: ShowColorChartProps) { +function ShowColorChart({ + isIconClicked, + colorChart, + state, + setFileName, + uploadImage, + handleFn, + handleIconFn, +}: ShowColorChartProps) { + const imgRef = useRef(null); + const [presignedUrl, setPresignedUrl] = useState(''); + const { data } = useGetPresignedUrl(); + // 함수 컴포넌트 내에서 커스텀 훅 호출 시, 에러발생 + const putMutation = usePutPresignedUrl(); + + const handleImageUpload = () => { + const fileInput = imgRef.current; + + if (fileInput && fileInput.files && fileInput.files.length > 0) { + const file = fileInput.files[0]; + + // reader1: 파일을 base64로 읽어서 업로드 + const reader1 = new FileReader(); + reader1.readAsDataURL(file); + reader1.onloadend = () => { + if (reader1.result !== null) { + uploadImage(reader1.result as string); + } + }; + + // reader2: 파일을 ArrayBuffer로 읽어서 PUT 요청 수행 + const reader2 = new FileReader(); + reader2.readAsArrayBuffer(file); + reader2.onloadend = () => { + if (reader2.result !== null && presignedUrl) { + putMutation.mutate({ + presignedUrl: presignedUrl, + binaryFile: reader2.result, + fileType: file.type, + }); + } + }; + } + }; + + useEffect(() => { + if (data !== undefined) { + setPresignedUrl(data.data.url); + setFileName(data.data.fileName); + } + }, [data]); + return ( + {colorChart === BG_COLOR_CHART && ( + <> + + + { + handleIconFn(); + imgRef.current?.click(); + }} + $isIconClicked={isIconClicked} + > + + + + )} {colorChart.map((colorCode) => ( diff --git a/src/LecueNote/components/WriteNote/WriteNote.style.ts b/src/LecueNote/components/WriteNote/WriteNote.style.ts index 63835995..b5f75df1 100644 --- a/src/LecueNote/components/WriteNote/WriteNote.style.ts +++ b/src/LecueNote/components/WriteNote/WriteNote.style.ts @@ -1,18 +1,75 @@ +import { css } from '@emotion/react'; import styled from '@emotion/styled'; export const Wrapper = styled.div` display: flex; + gap: 0.6rem; flex-direction: column; - - gap: 0.4rem; `; -export const LecueNote = styled.article<{ $bgColor: string }>` +export const LecueNote = styled.article<{ + $bgColor: string; + $isIconClicked: boolean; + $imgFile: string; +}>` + display: flex; + flex-direction: column; + width: 100%; - height: calc(100dvh - 33rem); + height: calc(100dvh - 33.2rem); border-radius: 0.6rem; - background-color: ${({ $bgColor }) => $bgColor}; + + ${({ $isIconClicked, $bgColor, $imgFile }) => + $isIconClicked + ? css` + width: 100%; + height: calc(100dvh - 33.2rem); + + background-size: 100% calc(100dvh - 33.2rem); + + background-image: url(${$imgFile}); + ` + : css` + background-color: ${$bgColor}; + `}; +`; + +export const Nickname = styled.p<{ $textColor: string }>` + margin: 2rem 0 1rem 2rem; + + color: ${({ $textColor }) => $textColor}; + ${({ theme }) => theme.fonts.Head1_B_20} +`; + +export const Contents = styled.textarea<{ $textColor: string }>` + width: calc(100% - 3rem); + height: 100%; + margin: 0 1.5rem 2rem; + + border: none; + ${({ theme }) => theme.fonts.Body1_R_16}; + background-color: transparent; + color: ${({ $textColor }) => $textColor}; +`; + +export const BottomContentsWrapper = styled.div` + display: flex; + justify-content: space-between; + + width: calc(100% - 4rem); + height: 1.7rem; + margin: 0 2rem 2rem; +`; + +export const Date = styled.p` + ${({ theme }) => theme.fonts.E_Body2_R_14}; + color: ${({ theme }) => theme.colors.DG50}; +`; + +export const Counter = styled.p` + ${({ theme }) => theme.fonts.E_Body2_R_14}; + color: ${({ theme }) => theme.colors.DG}; `; export const Notice = styled.p` diff --git a/src/LecueNote/components/WriteNote/index.tsx b/src/LecueNote/components/WriteNote/index.tsx index 89196c1e..2904f0bc 100644 --- a/src/LecueNote/components/WriteNote/index.tsx +++ b/src/LecueNote/components/WriteNote/index.tsx @@ -1,10 +1,41 @@ +import { useEffect, useState } from 'react'; + import { WriteNoteProps } from '../../type/lecueNoteType'; import * as S from './WriteNote.style'; -function WriteNote({ clickedBgColor }: WriteNoteProps) { +function WriteNote({ + imgFile, + isIconClicked, + clickedBgColor, + clickedTextColor, + contents, + handleChangeFn, +}: WriteNoteProps) { + const nickname = '와라라라랄라'; + + const today = new Date(); + const [dateArr, setDateArr] = useState([0, 0, 0]); + + useEffect(() => { + setDateArr([today.getFullYear(), today.getMonth() + 1, today.getDate()]); + }, [today.getDate()]); + return ( - + + {nickname} + + + + {dateArr[0]}.{dateArr[1]}.{dateArr[2]} + + ({contents.length}/1000) + + *욕설/비속어는 자동 필터링됩니다. ); diff --git a/src/LecueNote/hooks/useGetPresignedUrl.tsx b/src/LecueNote/hooks/useGetPresignedUrl.tsx new file mode 100644 index 00000000..29b62bc2 --- /dev/null +++ b/src/LecueNote/hooks/useGetPresignedUrl.tsx @@ -0,0 +1,14 @@ +import { useQuery } from 'react-query'; + +import getPresignedUrl from '../api/getPresignedUrl'; + +const useGetPresignedUrl = () => { + const { isLoading, error, data } = useQuery({ + queryKey: ['get-presigned-url'], + queryFn: () => getPresignedUrl(), + }); + + return { isLoading, error, data }; +}; + +export default useGetPresignedUrl; diff --git a/src/LecueNote/hooks/usePostLecueNote.ts b/src/LecueNote/hooks/usePostLecueNote.ts new file mode 100644 index 00000000..0ac36e70 --- /dev/null +++ b/src/LecueNote/hooks/usePostLecueNote.ts @@ -0,0 +1,22 @@ +import { AxiosError } from 'axios'; +import { useMutation } from 'react-query'; + +import postLecueNote from '../api/postLecueNote'; +import { postLecueNoteProps } from '../type/lecueNoteType'; + +const usePostLecueNote = () => { + const mutation = useMutation({ + mutationFn: ({ + contents, + color, + fileName, + bgColor, + }: postLecueNoteProps) => { + return postLecueNote({ contents, color, fileName, bgColor }); + }, + onError: (err: AxiosError) => console.log(err), + }); + return mutation; +}; + +export default usePostLecueNote; diff --git a/src/LecueNote/hooks/usePutPresignedUrl.ts b/src/LecueNote/hooks/usePutPresignedUrl.ts new file mode 100644 index 00000000..83903085 --- /dev/null +++ b/src/LecueNote/hooks/usePutPresignedUrl.ts @@ -0,0 +1,21 @@ +import { AxiosError } from 'axios'; +import { useMutation } from 'react-query'; + +import putPresignedUrl from '../api/putPresignedUrl'; +import { putPresignedUrlProps } from './../type/lecueNoteType'; + +const usePutPresignedUrl = () => { + const mutation = useMutation({ + mutationFn: ({ + presignedUrl, + binaryFile, + fileType, + }: putPresignedUrlProps) => { + return putPresignedUrl({ presignedUrl, binaryFile, fileType }); + }, + onError: (err: AxiosError) => console.log(err), + }); + return mutation; +}; + +export default usePutPresignedUrl; diff --git a/src/LecueNote/page/LeceuNotePage/LecueNotePage.style.ts b/src/LecueNote/page/LeceuNotePage/LecueNotePage.style.ts index ca2763b5..d65164c2 100644 --- a/src/LecueNote/page/LeceuNotePage/LecueNotePage.style.ts +++ b/src/LecueNote/page/LeceuNotePage/LecueNotePage.style.ts @@ -9,6 +9,5 @@ export const Wrapper = styled.div` width: 100vw; height: 100dvh; - padding: 0 1.7rem; `; diff --git a/src/LecueNote/page/LeceuNotePage/index.tsx b/src/LecueNote/page/LeceuNotePage/index.tsx index 603f71a6..d9ba845c 100644 --- a/src/LecueNote/page/LeceuNotePage/index.tsx +++ b/src/LecueNote/page/LeceuNotePage/index.tsx @@ -1,14 +1,77 @@ +import { useState } from 'react'; + import Header from '../../../components/common/Header'; import CreateNote from '../../components/CreateNote'; import Footer from '../../components/Footer'; +import { + BG_COLOR_CHART, + CATEGORY, + TEXT_COLOR_CHART, +} from '../../constants/colorChart'; import * as S from './LecueNotePage.style'; function LecueNotePage() { + const MAX_LENGTH = 1000; + const [contents, setContents] = useState(''); + const [imgFile, setImgFile] = useState(''); + const [clickedCategory, setclickedCategory] = useState(CATEGORY[0]); + const [clickedTextColor, setClickedTextColor] = useState(TEXT_COLOR_CHART[0]); + const [clickedBgColor, setclickedBgColor] = useState(BG_COLOR_CHART[0]); + const [isIconClicked, setIsIconClicked] = useState(false); + const [fileName, setFileName] = useState(''); + + const handleClickCategory = ( + e: React.MouseEvent, + ) => { + setclickedCategory(e.currentTarget.innerHTML); + }; + + const handleChangeContents = (e: React.ChangeEvent) => { + setContents(e.target.value); + if (e.target.value.length > MAX_LENGTH) { + setContents((e.target.value = e.target.value.substring(0, MAX_LENGTH))); + alert('1000자 내로 작성해주세요.'); + } + }; + + const handleClickedColorBtn = ( + e: React.MouseEvent, + ) => { + if (clickedCategory === '텍스트색') { + setClickedTextColor(e.currentTarget.id); + } else { + setclickedBgColor(e.currentTarget.id); + setIsIconClicked(false); + } + }; + + const handleClickedIcon = () => { + setIsIconClicked(true); + }; + return ( -
- -