From 66bb39dd925c94031e542bb10221cf987d0407fd Mon Sep 17 00:00:00 2001 From: yyypearl Date: Mon, 26 Aug 2024 20:53:19 +0900 Subject: [PATCH 1/5] =?UTF-8?q?design(#134):=20=EC=9D=8C=EC=84=B1=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=EC=8B=9C=20=EB=B7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/assets/icons/ic_volumn.svg | 3 + src/app/home/page.tsx | 53 +++++++- src/components/home/Notification.tsx | 1 + src/components/home/Ready.tsx | 4 +- src/components/home/Todo.tsx | 191 ++++++++++++++++----------- src/redux/slices/audioSlice.ts | 16 +++ src/redux/store.ts | 2 + 7 files changed, 190 insertions(+), 80 deletions(-) create mode 100644 public/assets/icons/ic_volumn.svg create mode 100644 src/redux/slices/audioSlice.ts diff --git a/public/assets/icons/ic_volumn.svg b/public/assets/icons/ic_volumn.svg new file mode 100644 index 0000000..9309d69 --- /dev/null +++ b/public/assets/icons/ic_volumn.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/home/page.tsx b/src/app/home/page.tsx index 910006a..9a3b77c 100644 --- a/src/app/home/page.tsx +++ b/src/app/home/page.tsx @@ -2,14 +2,28 @@ import Tabbar from "@/components/common/Tabbar"; import Meal from "@/components/home/Meal"; +import Notification from "@/components/home/Notification"; import Ready from "@/components/home/Ready"; +import Todo from "@/components/home/Todo"; +import { RootState } from "@/redux/store"; import { theme } from "@/styles/theme"; import Image from "next/image"; import { useRouter } from "next/navigation"; +import { useEffect } from "react"; +import { useSelector } from "react-redux"; import styled from "styled-components"; const Home = () => { const router = useRouter(); + const audio = useSelector((state: RootState) => state.audio.audio); + + useEffect(() => { + if (audio) { + console.log("Audio is enabled"); + } else { + console.log("Audio is disabled"); + } + }, [audio]); return ( <> @@ -27,7 +41,13 @@ const Home = () => { style={{ cursor: "pointer" }} /> - + + + {audio && 오늘 할 일을 들려드릴게요!} + + + + @@ -85,3 +105,34 @@ const Header = styled.div` color: ${theme.colors.white}; ${(props) => props.theme.fonts.body1_b}; `; + +const ReadyContainer = styled.div` + width: 100%; + display: flex; + flex-direction: column; + padding: 9px 0px; + gap: 31px; + z-index: 1000; + position: relative; +`; + +const TodoOverlay = styled.div<{ $audio: boolean }>` + position: fixed; + background-color: ${({ $audio }) => ($audio ? "#00000078" : "transparent")}; + width: 100%; + height: 100vh; + top: 0; + left: 0; + z-index: 2000; + pointer-events: none; +`; + +const AudioText = styled.div` + position: absolute; + top: 130px; + left: 50%; + transform: translateX(-50%); + color: ${theme.colors.white}; + ${(props) => props.theme.fonts.heading2_b}; + z-index: 2000; +`; diff --git a/src/components/home/Notification.tsx b/src/components/home/Notification.tsx index f808e89..8c1d152 100644 --- a/src/components/home/Notification.tsx +++ b/src/components/home/Notification.tsx @@ -63,6 +63,7 @@ const NotiContainer = styled.div` ${(props) => props.theme.fonts.body2_b}; color: ${theme.colors.b700}; letter-spacing: -0.28px; + z-index: -10; `; const Title = styled.div` diff --git a/src/components/home/Ready.tsx b/src/components/home/Ready.tsx index 6eec4d6..8a40e15 100644 --- a/src/components/home/Ready.tsx +++ b/src/components/home/Ready.tsx @@ -59,8 +59,8 @@ const Ready = () => { height={230} /> - - + {/* + */} ); }; diff --git a/src/components/home/Todo.tsx b/src/components/home/Todo.tsx index 95f1726..65295cd 100644 --- a/src/components/home/Todo.tsx +++ b/src/components/home/Todo.tsx @@ -6,6 +6,10 @@ import { useEffect, useState } from "react"; import AddTodoPopup, { categories } from "./AddTodoPopup"; import Toast from "../common/Toast"; import Axios from "@/apis/axios"; +import { setAudio } from "@/redux/slices/audioSlice"; +import { useDispatch } from "react-redux"; +import { RootState } from "@/redux/store"; +import { useSelector } from "react-redux"; interface Todo { todoId: number; @@ -17,6 +21,7 @@ interface Todo { } const Todo = () => { + const dispatch = useDispatch(); const [currentDate, setCurrentDate] = useState(new Date()); const [addTodo, setAddTodo] = useState(false); const [todoData, setTodoData] = useState([]); @@ -123,6 +128,20 @@ const Todo = () => { } }; + const [voice, setVoice] = useState(false); + + const test = useSelector((state: RootState) => state.audio.audio); + + const handleVoiceConversion = () => { + setVoice(!voice); + console.log("audio", voice); + dispatch(setAudio(voice)); + }; + + useEffect(() => { + console.log("test", test); + }, [voice]); + /* Toast 메세지 유무 */ const [showToast, setShowToast] = useState(false); @@ -135,90 +154,103 @@ const Todo = () => { }; return ( - - 오늘 할 일 잊지마세요! - - left - {todayMonth}월 {todayDate}일 {dayOfWeek}요일 - {getDateString(currentDate)} - right - - - {todoData && todoData.length > 0 ? ( - todoData.map((data, index) => ( - category.value === data.todoType) - ?.label || data.todoType - } - onClick={() => { - changeTodo(data.todoId); + <> + + 오늘 할 일 잊지마세요! + + + left + {todayMonth}월 {todayDate}일 {dayOfWeek}요일 + {getDateString(currentDate)} + right deleteTodo(data.todoId)} /> - )) - ) : ( - 할 일이 없어요! - )} - - - + add - 할 일 직접 추가하기 - - - {addTodo && ( - - )} - {showToast && ( - setShowToast(false)} - /> - )} - + + + {todoData && todoData.length > 0 ? ( + todoData.map((data, index) => ( + category.value === data.todoType + )?.label || data.todoType + } + onClick={() => { + changeTodo(data.todoId); + }} + text={data.description} + time={`${getDayOfWeek(data.deadline)}까지`} + checked={data.status === "COMPLETE"} + onDelete={() => deleteTodo(data.todoId)} + /> + )) + ) : ( + 할 일이 없어요! + )} + + + + add + 할 일 직접 추가하기 + + + {addTodo && ( + + )} + {showToast && ( + setShowToast(false)} + /> + )} + + ); }; export default Todo; -const TodoContainer = styled.div` +const TodoContainer = styled.div<{ $overlay: boolean }>` width: 100%; padding: 16px; gap: 10px; @@ -227,9 +259,14 @@ const TodoContainer = styled.div` box-shadow: 0px 0px 64px 0px rgba(30, 41, 59, 0.1); ${(props) => props.theme.fonts.body2_b}; color: ${theme.colors.b700}; - z-index: 10; + z-index: 2000; letter-spacing: -0.28px; - position: relative; +`; + +const Row = styled.div` + display: flex; + justify-content: space-between; + align-items: center; `; const DateLine = styled.div` diff --git a/src/redux/slices/audioSlice.ts b/src/redux/slices/audioSlice.ts new file mode 100644 index 0000000..50cb192 --- /dev/null +++ b/src/redux/slices/audioSlice.ts @@ -0,0 +1,16 @@ +import { createSlice } from "@reduxjs/toolkit"; + +export const audioSlice = createSlice({ + name: "audio", + initialState: { + audio: false, + }, + reducers: { + setAudio: (state, action) => { + state.audio = action.payload; + }, + }, +}); + +export const { setAudio } = audioSlice.actions; +export default audioSlice.reducer; diff --git a/src/redux/store.ts b/src/redux/store.ts index de59d3e..cedad87 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -2,12 +2,14 @@ import { configureStore } from "@reduxjs/toolkit"; import categoryReducer from "./slices/categorySlice"; import authReducer from "./slices/authSlice"; import userReducer from "./slices/userSlice"; +import audioReducer from "./slices/audioSlice"; export const store = configureStore({ reducer: { category: categoryReducer, auth: authReducer, user: userReducer, + audio: audioReducer, }, }); From 090eb302f2bdf82c6c3d7905662e4e74330ddd55 Mon Sep 17 00:00:00 2001 From: yyypearl Date: Mon, 26 Aug 2024 21:26:23 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat(#134):=20Todo=20=EC=9D=8C=EC=84=B1?= =?UTF-8?q?=EB=B3=80=ED=99=98=20=EA=B8=B0=EB=8A=A5=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/home/Todo.tsx | 33 ++++++++++++++++++++++++++------- src/utils/getSpeech.tsx | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 src/utils/getSpeech.tsx diff --git a/src/components/home/Todo.tsx b/src/components/home/Todo.tsx index 65295cd..896b11a 100644 --- a/src/components/home/Todo.tsx +++ b/src/components/home/Todo.tsx @@ -10,6 +10,7 @@ import { setAudio } from "@/redux/slices/audioSlice"; import { useDispatch } from "react-redux"; import { RootState } from "@/redux/store"; import { useSelector } from "react-redux"; +import { getSpeech } from "@/utils/getSpeech"; interface Todo { todoId: number; @@ -40,6 +41,7 @@ const Todo = () => { .then((response) => { const todoData: Todo[] = response.data.todoList; setTodoData(todoData); + console.log("조회", response); }) .catch(() => {}); }, [currentDate, render]); @@ -48,6 +50,7 @@ const Todo = () => { const changeTodo = (todoId: number) => { Axios.put(`/api/v1/todos/${todoId}`).then((response) => { setRenderData(!render); + console.log("수정", response); }); }; @@ -117,25 +120,39 @@ const Todo = () => { /* 날짜(일수) 차이 문자열 */ const getDateString = (date: any) => { if (isToday(date)) { - return 오늘; + return "오늘"; } else { const diff = getDayDifference(date); - return ( - - {diff > 0 ? `${diff}일 전` : `${-diff}일 후`} - - ); + return diff > 0 ? `${diff}일 전` : `${-diff}일 후`; } }; + /* 음성 변환 목소리 preload */ + useEffect(() => { + window.speechSynthesis.getVoices(); + }, []); + const [voice, setVoice] = useState(false); const test = useSelector((state: RootState) => state.audio.audio); + let date = `${todayMonth}월 ${todayDate}일 ${dayOfWeek}요일`; + const handleVoiceConversion = () => { setVoice(!voice); console.log("audio", voice); dispatch(setAudio(voice)); + const unassignedTodos = todoData + .filter((todo: Todo) => todo.status === "INCOMPLETE") + .map((todo: Todo) => todo.description + "."); + console.log("unassignedTodos", unassignedTodos); + const text = + date + + `. ${getDateString(currentDate)} ${getDateString(currentDate) === "오늘" ? "해야할" : "했어야 할"} 일은. ` + + unassignedTodos + + "입니다."; + + getSpeech(text); }; useEffect(() => { @@ -168,7 +185,9 @@ const Todo = () => { style={{ cursor: "pointer" }} /> {todayMonth}월 {todayDate}일 {dayOfWeek}요일 - {getDateString(currentDate)} + + {getDateString(currentDate)} + right { + let voices: any[] = []; + + const setVoiceList = () => { + voices = window.speechSynthesis.getVoices(); + }; + + setVoiceList(); + + if (window.speechSynthesis.onvoiceschanged !== undefined) { + window.speechSynthesis.onvoiceschanged = setVoiceList; + } + + const speech = (txt: string) => { + const lang = "ko-KR"; + const utterThis = new SpeechSynthesisUtterance(txt); + + utterThis.lang = lang; + + const kor_voice = voices.find( + (elem) => elem.lang === lang || elem.lang === lang.replace("-", "_") + ); + + if (kor_voice) { + utterThis.voice = kor_voice; + } else { + return; + } + + window.speechSynthesis.speak(utterThis); + }; + + speech(text); +}; From b2062a59907ec2895a34d41ca3de4cd965053ee4 Mon Sep 17 00:00:00 2001 From: yyypearl Date: Mon, 26 Aug 2024 21:37:43 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat(#134):=20=EC=9D=8C=EC=84=B1=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=20=EC=A2=85=EB=A3=8C=20=EC=8B=9C=20=EC=BD=9C?= =?UTF-8?q?=EB=B0=B1=20=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/home/Todo.tsx | 11 ++++++++--- src/utils/getSpeech.tsx | 8 +++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/home/Todo.tsx b/src/components/home/Todo.tsx index 896b11a..3802f76 100644 --- a/src/components/home/Todo.tsx +++ b/src/components/home/Todo.tsx @@ -139,20 +139,25 @@ const Todo = () => { let date = `${todayMonth}월 ${todayDate}일 ${dayOfWeek}요일`; const handleVoiceConversion = () => { - setVoice(!voice); + setVoice(true); console.log("audio", voice); - dispatch(setAudio(voice)); + dispatch(setAudio(true)); + const unassignedTodos = todoData .filter((todo: Todo) => todo.status === "INCOMPLETE") .map((todo: Todo) => todo.description + "."); console.log("unassignedTodos", unassignedTodos); + const text = date + `. ${getDateString(currentDate)} ${getDateString(currentDate) === "오늘" ? "해야할" : "했어야 할"} 일은. ` + unassignedTodos + "입니다."; - getSpeech(text); + getSpeech(text, () => { + setVoice(false); + dispatch(setAudio(false)); + }); }; useEffect(() => { diff --git a/src/utils/getSpeech.tsx b/src/utils/getSpeech.tsx index f55bbf5..ed51915 100644 --- a/src/utils/getSpeech.tsx +++ b/src/utils/getSpeech.tsx @@ -1,4 +1,4 @@ -export const getSpeech = (text: string) => { +export const getSpeech = (text: string, onEndCallback?: () => void) => { let voices: any[] = []; const setVoiceList = () => { @@ -11,6 +11,8 @@ export const getSpeech = (text: string) => { window.speechSynthesis.onvoiceschanged = setVoiceList; } + window.speechSynthesis.cancel(); + const speech = (txt: string) => { const lang = "ko-KR"; const utterThis = new SpeechSynthesisUtterance(txt); @@ -27,6 +29,10 @@ export const getSpeech = (text: string) => { return; } + if (onEndCallback) { + utterThis.onend = onEndCallback; + } + window.speechSynthesis.speak(utterThis); }; From 0294ed2326312d6bf51427997ef571810bd0b3bd Mon Sep 17 00:00:00 2001 From: yyypearl Date: Mon, 26 Aug 2024 21:48:39 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat(#134):=20Todo=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=99=B8=EB=B6=80=20=ED=81=B4=EB=A6=AD?= =?UTF-8?q?=EC=8B=9C=20=EC=9D=8C=EC=84=B1=EB=B3=80=ED=99=98=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/home/Todo.tsx | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/components/home/Todo.tsx b/src/components/home/Todo.tsx index 3802f76..19c8b5c 100644 --- a/src/components/home/Todo.tsx +++ b/src/components/home/Todo.tsx @@ -2,7 +2,7 @@ import styled from "styled-components"; import { theme } from "@/styles/theme"; import Image from "next/image"; import ListBox from "../common/ListBox"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import AddTodoPopup, { categories } from "./AddTodoPopup"; import Toast from "../common/Toast"; import Axios from "@/apis/axios"; @@ -132,21 +132,14 @@ const Todo = () => { window.speechSynthesis.getVoices(); }, []); - const [voice, setVoice] = useState(false); - - const test = useSelector((state: RootState) => state.audio.audio); - let date = `${todayMonth}월 ${todayDate}일 ${dayOfWeek}요일`; const handleVoiceConversion = () => { - setVoice(true); - console.log("audio", voice); dispatch(setAudio(true)); const unassignedTodos = todoData .filter((todo: Todo) => todo.status === "INCOMPLETE") .map((todo: Todo) => todo.description + "."); - console.log("unassignedTodos", unassignedTodos); const text = date + @@ -155,14 +148,28 @@ const Todo = () => { "입니다."; getSpeech(text, () => { - setVoice(false); dispatch(setAudio(false)); }); }; + const containerRef = useRef(null); + useEffect(() => { - console.log("test", test); - }, [voice]); + const handleClickOutside = (event: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(event.target as Node) + ) { + window.speechSynthesis.cancel(); + dispatch(setAudio(false)); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [dispatch]); /* Toast 메세지 유무 */ const [showToast, setShowToast] = useState(false); @@ -177,7 +184,7 @@ const Todo = () => { return ( <> - + 오늘 할 일 잊지마세요! @@ -274,7 +281,7 @@ const Todo = () => { export default Todo; -const TodoContainer = styled.div<{ $overlay: boolean }>` +const TodoContainer = styled.div` width: 100%; padding: 16px; gap: 10px; From c14869f1995fe8b4891f243838f446dec56731bd Mon Sep 17 00:00:00 2001 From: yyypearl Date: Mon, 26 Aug 2024 21:56:47 +0900 Subject: [PATCH 5/5] =?UTF-8?q?style(#134):=20console.log=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/home/page.tsx | 8 +------- src/components/home/Todo.tsx | 2 -- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/app/home/page.tsx b/src/app/home/page.tsx index 9a3b77c..f1c6254 100644 --- a/src/app/home/page.tsx +++ b/src/app/home/page.tsx @@ -17,13 +17,7 @@ const Home = () => { const router = useRouter(); const audio = useSelector((state: RootState) => state.audio.audio); - useEffect(() => { - if (audio) { - console.log("Audio is enabled"); - } else { - console.log("Audio is disabled"); - } - }, [audio]); + useEffect(() => {}, [audio]); return ( <> diff --git a/src/components/home/Todo.tsx b/src/components/home/Todo.tsx index 19c8b5c..97ebe93 100644 --- a/src/components/home/Todo.tsx +++ b/src/components/home/Todo.tsx @@ -41,7 +41,6 @@ const Todo = () => { .then((response) => { const todoData: Todo[] = response.data.todoList; setTodoData(todoData); - console.log("조회", response); }) .catch(() => {}); }, [currentDate, render]); @@ -50,7 +49,6 @@ const Todo = () => { const changeTodo = (todoId: number) => { Axios.put(`/api/v1/todos/${todoId}`).then((response) => { setRenderData(!render); - console.log("수정", response); }); };