diff --git a/package.json b/package.json index 5b1bad47..44840dcc 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "axios": "^1.6.5", "eslint-plugin-react": "^7.33.2", "grapheme-splitter": "^1.0.4", + "heic2any": "^0.0.4", "lottie-react": "^2.4.0", "postcss": "^8.4.33", "postcss-styled-syntax": "^0.6.3", diff --git a/src/LecueNote/components/SelectColor/index.tsx b/src/LecueNote/components/SelectColor/index.tsx index eb2ccbc9..ee6cf943 100644 --- a/src/LecueNote/components/SelectColor/index.tsx +++ b/src/LecueNote/components/SelectColor/index.tsx @@ -18,8 +18,9 @@ function SelectColor({ handleColorFn, handleIconFn, selectedFile, + handleIsLoading, }: SelectColorProps) { - const { textColor, background, category } = lecueNoteState; + const { textColor, background, category, contents } = lecueNoteState; return ( @@ -43,11 +44,13 @@ function SelectColor({ isIconClicked={isIconClicked} colorChart={category === '텍스트색' ? TEXT_COLOR_CHART : BG_COLOR_CHART} state={category === '텍스트색' ? textColor : background} + contents={contents} handleTransformImgFile={handleTransformImgFile} presignedUrlDispatch={presignedUrlDispatch} selectedFile={selectedFile} handleFn={handleColorFn} handleIconFn={handleIconFn} + handleIsLoading={handleIsLoading} /> ); diff --git a/src/LecueNote/components/ShowColorChart/index.tsx b/src/LecueNote/components/ShowColorChart/index.tsx index 452bb58e..d0785aff 100644 --- a/src/LecueNote/components/ShowColorChart/index.tsx +++ b/src/LecueNote/components/ShowColorChart/index.tsx @@ -4,44 +4,63 @@ import { IcCameraSmall } from '../../../assets'; import { BG_COLOR_CHART } from '../../constants/colorChart'; import useGetPresignedUrl from '../../hooks/useGetPresignedUrl'; import { ShowColorChartProps } from '../../type/lecueNoteType'; +import handleClickFiletoBinary from '../../util/handleClickFiletoBinary'; +import handleClickFiletoString from '../../util/handleClickFiletoString'; +import handleClickHeicToJpg from '../../util/handleClickHeicToJpg'; import * as S from './ShowColorChart.style'; function ShowColorChart({ isIconClicked, colorChart, state, + contents, handleTransformImgFile, presignedUrlDispatch, selectedFile, handleFn, handleIconFn, + handleIsLoading, }: ShowColorChartProps) { const imgRef = useRef(null); useGetPresignedUrl({ presignedUrlDispatch }); + const handleChangeContents = () => { + sessionStorage.setItem('noteContents', contents ? contents : ''); + }; + + const handleReaderOnloadend = (reader: FileReader, file: File) => { + handleTransformImgFile(reader); + selectedFile(file); + }; + 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) { - handleTransformImgFile(reader1.result as string); - } - }; + if (file.name.split('.')[1].toUpperCase() === 'HEIC') { + handleClickHeicToJpg({ + file: file, + handleTransformImgFile, + handleReaderOnloadend, + handleIsLoading, + }); + } else { + const reader1 = new FileReader(); + handleClickFiletoString({ + file: file, + reader: reader1, + handleTransformImgFile, + }); - // reader2: 파일을 ArrayBuffer로 읽어서 PUT 요청 수행 - const reader2 = new FileReader(); - reader2.readAsArrayBuffer(file); - // reader1의 비동기 작업이 완료된 후 수행(onloadend() 활용) - reader2.onloadend = () => { - handleTransformImgFile(reader2); - selectedFile(file); - }; + const reader2 = new FileReader(); + handleClickFiletoBinary({ + file: file, + reader: reader2, + handleReaderOnloadend, + }); + } } }; @@ -51,7 +70,7 @@ function ShowColorChart({ <> @@ -59,6 +78,7 @@ function ShowColorChart({ { handleIconFn(); + handleChangeContents(); imgRef.current?.click(); }} $isIconClicked={isIconClicked} diff --git a/src/LecueNote/components/WriteNote/NoteLoading/NoteLoading.style.ts b/src/LecueNote/components/WriteNote/NoteLoading/NoteLoading.style.ts new file mode 100644 index 00000000..30277927 --- /dev/null +++ b/src/LecueNote/components/WriteNote/NoteLoading/NoteLoading.style.ts @@ -0,0 +1,21 @@ +import styled from '@emotion/styled'; + +export const LoadingWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + position: absolute; + top: 0; + left: 0; + + width: 100%; + height: 100%; + + border: 0.6rem; + background-color: ${({ theme }) => theme.colors.background}; +`; + +export const LottieWrapper = styled.div` + width: 10rem; + height: 10rem; +`; diff --git a/src/LecueNote/components/WriteNote/NoteLoading/index.tsx b/src/LecueNote/components/WriteNote/NoteLoading/index.tsx new file mode 100644 index 00000000..e56ccde4 --- /dev/null +++ b/src/LecueNote/components/WriteNote/NoteLoading/index.tsx @@ -0,0 +1,24 @@ +import Lottie from 'lottie-react'; +import { useEffect } from 'react'; + +import animationData from '../../../../../src/assets/lottie/spiner 120.json'; +import * as S from './NoteLoading.style'; + +interface NoteLoadingProps { + handleResetPrevImg: () => void; +} +const NoteLoading = ({ handleResetPrevImg }: NoteLoadingProps) => { + useEffect(() => { + handleResetPrevImg(); + }, []); + + return ( + + + + + + ); +}; + +export default NoteLoading; diff --git a/src/LecueNote/components/WriteNote/WriteNote.style.ts b/src/LecueNote/components/WriteNote/WriteNote.style.ts index 8c6e0882..844138f2 100644 --- a/src/LecueNote/components/WriteNote/WriteNote.style.ts +++ b/src/LecueNote/components/WriteNote/WriteNote.style.ts @@ -14,6 +14,7 @@ export const LecueNote = styled.article<{ }>` display: flex; flex-direction: column; + position: relative; width: 100%; height: calc(100dvh - 33.2rem); diff --git a/src/LecueNote/components/WriteNote/index.tsx b/src/LecueNote/components/WriteNote/index.tsx index 774285cf..d2c95141 100644 --- a/src/LecueNote/components/WriteNote/index.tsx +++ b/src/LecueNote/components/WriteNote/index.tsx @@ -2,14 +2,17 @@ import GraphemeSplitter from 'grapheme-splitter'; import { useEffect, useState } from 'react'; import { WriteNoteProps } from '../../type/lecueNoteType'; +import NoteLoading from './NoteLoading'; import * as S from './WriteNote.style'; function WriteNote({ + isLoading, lecueNoteState, imgFile, isIconClicked, contents, handleChangeFn, + handleResetPrevImg, }: WriteNoteProps) { const nickname = localStorage.getItem('nickname'); const { textColor, background } = lecueNoteState; @@ -30,6 +33,8 @@ function WriteNote({ $isIconClicked={isIconClicked} $imgFile={imgFile} > + {isLoading && } + {nickname} { + setIsLoading(booleanStatus); + }; + + const handleResetPrevImg = () => { + dispatch({ type: 'RESET_PREV_IMG' }); + }; + const handleChangeContents = (e: React.ChangeEvent) => { dispatch({ type: 'SET_CONTENTS', contents: e.target.value }); if (e.target.value.length > MAX_LENGTH) { @@ -89,6 +99,8 @@ function LecueNotePage() { isIconClicked: lecueNoteState.isIconClicked, bookId: bookId, }); + + sessionStorage.setItem('noteContents', ''); }; return putMutation.isLoading || postMutation.isLoading ? ( @@ -105,7 +117,10 @@ function LecueNotePage() { {escapeModal && ( navigate(-1)} + handleFn={() => { + navigate(-1); + sessionStorage.setItem('noteContents', ''); + }} category="note_escape" setModalOn={setEscapeModal} /> @@ -117,11 +132,13 @@ function LecueNotePage() { dispatch({ type: 'CLICKED_IMG_ICON' })} + handleIsLoading={handleIsLoading} /> diff --git a/src/LecueNote/reducer/lecueNoteReducer.ts b/src/LecueNote/reducer/lecueNoteReducer.ts index b7616fed..b4becec3 100644 --- a/src/LecueNote/reducer/lecueNoteReducer.ts +++ b/src/LecueNote/reducer/lecueNoteReducer.ts @@ -46,6 +46,9 @@ export const reducer = (state: State, action: Action): State => { case 'IMG_TO_BINARY': return { ...state, imgToBinary: action.imgFile }; + case 'RESET_PREV_IMG': + return { ...state, imgToStr: '' }; + default: throw new Error('Unhandled action'); } diff --git a/src/LecueNote/type/lecueNoteType.ts b/src/LecueNote/type/lecueNoteType.ts index 787f67d1..0e9bb78d 100644 --- a/src/LecueNote/type/lecueNoteType.ts +++ b/src/LecueNote/type/lecueNoteType.ts @@ -4,6 +4,7 @@ export interface SelectColorProps { textColor: string; background: string; category?: string; + contents?: string; }; selectedFile: (file: File) => void; presignedUrlDispatch: React.Dispatch<{ @@ -17,12 +18,14 @@ export interface SelectColorProps { handleColorFn: (e: React.MouseEvent) => void; handleIconFn: () => void; handleTransformImgFile: (file: string | FileReader) => void; + handleIsLoading: (status: boolean) => void; } export interface ShowColorChartProps { isIconClicked: boolean; colorChart: string[]; state: string; + contents?: string; handleTransformImgFile: (file: string | FileReader) => void; selectedFile: (file: File) => void; handleFn: (e: React.MouseEvent) => void; @@ -32,9 +35,11 @@ export interface ShowColorChartProps { presignedUrl: string; filename: string; }>; + handleIsLoading: (status: boolean) => void; } export interface WriteNoteProps { + isLoading: boolean; imgFile: string; isIconClicked: boolean; lecueNoteState: { @@ -44,6 +49,7 @@ export interface WriteNoteProps { }; contents: string; handleChangeFn: (e: React.ChangeEvent) => void; + handleResetPrevImg: () => void; } export interface FooterProps { diff --git a/src/LecueNote/type/reducerType.ts b/src/LecueNote/type/reducerType.ts index bd8a244d..e8e257f9 100644 --- a/src/LecueNote/type/reducerType.ts +++ b/src/LecueNote/type/reducerType.ts @@ -21,4 +21,5 @@ export type Action = | { type: 'CLICKED_IMG_ICON' } | { type: 'NOT_CLICKED_IMG_ICON' } | { type: 'IMG_TO_STR'; imgFile: string } - | { type: 'IMG_TO_BINARY'; imgFile: FileReader }; + | { type: 'IMG_TO_BINARY'; imgFile: FileReader } + | { type: 'RESET_PREV_IMG'; }; diff --git a/src/LecueNote/util/handleClickFiletoBinary.ts b/src/LecueNote/util/handleClickFiletoBinary.ts new file mode 100644 index 00000000..0db442be --- /dev/null +++ b/src/LecueNote/util/handleClickFiletoBinary.ts @@ -0,0 +1,19 @@ +interface handleClickFiletoBinaryProps { + file: File; + reader: FileReader; + handleReaderOnloadend: (reader: FileReader, file: File) => void; +} + +const handleClickFiletoBinary = ({ + file, + reader, + handleReaderOnloadend, +}: handleClickFiletoBinaryProps) => { + reader.readAsArrayBuffer(file); + // reader1의 비동기 작업이 완료된 후 수행(onloadend() 활용) + reader.onloadend = () => { + handleReaderOnloadend(reader, file); + }; +}; + +export default handleClickFiletoBinary; diff --git a/src/LecueNote/util/handleClickFiletoString.ts b/src/LecueNote/util/handleClickFiletoString.ts new file mode 100644 index 00000000..68d02ca5 --- /dev/null +++ b/src/LecueNote/util/handleClickFiletoString.ts @@ -0,0 +1,20 @@ +interface handleClickFiletoStringProps { + file: File; + reader: FileReader; + handleTransformImgFile: (file: string | FileReader) => void; +} + +const handleClickFiletoString = ({ + file, + reader, + handleTransformImgFile, +}: handleClickFiletoStringProps) => { + reader.readAsDataURL(file); + reader.onloadend = () => { + if (reader.result !== null) { + handleTransformImgFile(reader.result as string); + } + }; +}; + +export default handleClickFiletoString; diff --git a/src/LecueNote/util/handleClickHeicToJpg.ts b/src/LecueNote/util/handleClickHeicToJpg.ts new file mode 100644 index 00000000..27799294 --- /dev/null +++ b/src/LecueNote/util/handleClickHeicToJpg.ts @@ -0,0 +1,49 @@ +import heic2any from 'heic2any'; + +import handleClickFiletoBinary from './handleClickFiletoBinary'; +import handleClickFiletoString from './handleClickFiletoString'; + +interface handleClickHeicToJpgProps { + file: File; + handleTransformImgFile: (file: string | FileReader) => void; + handleReaderOnloadend: (reader: FileReader, file: File) => void; + handleIsLoading: (status: boolean) => void; +} + +const handleClickHeicToJpg = ({ + file, + handleTransformImgFile, + handleReaderOnloadend, + handleIsLoading, +}: handleClickHeicToJpgProps) => { + handleIsLoading(true); + + heic2any({ blob: file, toType: 'image/jpeg' }) + .then(function (resultBlob) { + // 변환된 Blob을 사용하여 새로운 File 생성 + const jpg = new File( + Array.isArray(resultBlob) ? resultBlob : [resultBlob], + file.name.split('.')[0] + '.jpg', + { type: 'image/jpeg', lastModified: new Date().getTime() }, + ); + + // reader1: 파일을 base64로 읽어서 업로드 + const reader1 = new FileReader(); + handleClickFiletoString({ + file: jpg, + reader: reader1, + handleTransformImgFile, + }); + + // reader2: 파일을 ArrayBuffer로 읽어서 PUT 요청 수행 + const reader2 = new FileReader(); + handleClickFiletoBinary({ + file: jpg, + reader: reader2, + handleReaderOnloadend, + }); + }) + .finally(() => handleIsLoading(false)); +}; + +export default handleClickHeicToJpg; diff --git a/yarn.lock b/yarn.lock index 7ca484db..327d7b5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2228,6 +2228,11 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" +heic2any@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/heic2any/-/heic2any-0.0.4.tgz#eddb8e6fec53c8583a6e18b65069bb5e8d19028a" + integrity sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA== + hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"