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..f1c6254 100644
--- a/src/app/home/page.tsx
+++ b/src/app/home/page.tsx
@@ -2,14 +2,22 @@
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(() => {}, [audio]);
return (
<>
@@ -27,7 +35,13 @@ const Home = () => {
style={{ cursor: "pointer" }}
/>
-
+
+
+ {audio && 오늘 할 일을 들려드릴게요!}
+
+
+
+
@@ -85,3 +99,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..97ebe93 100644
--- a/src/components/home/Todo.tsx
+++ b/src/components/home/Todo.tsx
@@ -2,10 +2,15 @@ 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";
+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;
@@ -17,6 +22,7 @@ interface Todo {
}
const Todo = () => {
+ const dispatch = useDispatch();
const [currentDate, setCurrentDate] = useState(new Date());
const [addTodo, setAddTodo] = useState(false);
const [todoData, setTodoData] = useState([]);
@@ -112,17 +118,57 @@ 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();
+ }, []);
+
+ let date = `${todayMonth}월 ${todayDate}일 ${dayOfWeek}요일`;
+
+ const handleVoiceConversion = () => {
+ dispatch(setAudio(true));
+
+ const unassignedTodos = todoData
+ .filter((todo: Todo) => todo.status === "INCOMPLETE")
+ .map((todo: Todo) => todo.description + ".");
+
+ const text =
+ date +
+ `. ${getDateString(currentDate)} ${getDateString(currentDate) === "오늘" ? "해야할" : "했어야 할"} 일은. ` +
+ unassignedTodos +
+ "입니다.";
+
+ getSpeech(text, () => {
+ dispatch(setAudio(false));
+ });
+ };
+
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ 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);
@@ -135,84 +181,99 @@ const Todo = () => {
};
return (
-
- 오늘 할 일 잊지마세요!
-
-
- {todayMonth}월 {todayDate}일 {dayOfWeek}요일
- {getDateString(currentDate)}
-
-
-
- {todoData && todoData.length > 0 ? (
- todoData.map((data, index) => (
- category.value === data.todoType)
- ?.label || data.todoType
- }
- onClick={() => {
- changeTodo(data.todoId);
+ <>
+
+ 오늘 할 일 잊지마세요!
+
+
+
+ {todayMonth}월 {todayDate}일 {dayOfWeek}요일
+
+ {getDateString(currentDate)}
+
+ deleteTodo(data.todoId)}
/>
- ))
- ) : (
- 할 일이 없어요!
- )}
-
-
-
+
+
+
+ {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)}
+ />
+ ))
+ ) : (
+ 할 일이 없어요!
+ )}
+
+
+
+
+ 할 일 직접 추가하기
+
+
+ {addTodo && (
+
+ )}
+ {showToast && (
+ setShowToast(false)}
/>
- 할 일 직접 추가하기
-
-
- {addTodo && (
-
- )}
- {showToast && (
- setShowToast(false)}
- />
- )}
-
+ )}
+
+ >
);
};
@@ -227,9 +288,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,
},
});
diff --git a/src/utils/getSpeech.tsx b/src/utils/getSpeech.tsx
new file mode 100644
index 0000000..ed51915
--- /dev/null
+++ b/src/utils/getSpeech.tsx
@@ -0,0 +1,40 @@
+export const getSpeech = (text: string, onEndCallback?: () => void) => {
+ let voices: any[] = [];
+
+ const setVoiceList = () => {
+ voices = window.speechSynthesis.getVoices();
+ };
+
+ setVoiceList();
+
+ if (window.speechSynthesis.onvoiceschanged !== undefined) {
+ window.speechSynthesis.onvoiceschanged = setVoiceList;
+ }
+
+ window.speechSynthesis.cancel();
+
+ 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;
+ }
+
+ if (onEndCallback) {
+ utterThis.onend = onEndCallback;
+ }
+
+ window.speechSynthesis.speak(utterThis);
+ };
+
+ speech(text);
+};