Skip to content

Commit

Permalink
Merge pull request #137 from Kusitms-29th-ASAP/feat/#134
Browse files Browse the repository at this point in the history
[Feat] 투두리스트 음성변환
  • Loading branch information
yyypearl authored Aug 31, 2024
2 parents 6a9dc1c + c14869f commit 4985035
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 86 deletions.
3 changes: 3 additions & 0 deletions public/assets/icons/ic_volumn.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 46 additions & 1 deletion src/app/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
Expand All @@ -27,7 +35,13 @@ const Home = () => {
style={{ cursor: "pointer" }}
/>
</Header>
<Ready />
<ReadyContainer>
<TodoOverlay $audio={audio} />
{audio && <AudioText>오늘 할 일을 들려드릴게요!</AudioText>}
<Ready />
<Todo />
<Notification />
</ReadyContainer>
<Meal />
</Container>
</HomeLayout>
Expand Down Expand Up @@ -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;
`;
1 change: 1 addition & 0 deletions src/components/home/Notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
4 changes: 2 additions & 2 deletions src/components/home/Ready.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ const Ready = () => {
height={230}
/>
</Top>
<Todo />
<Notification />
{/* <Todo />
<Notification /> */}
</Box>
);
};
Expand Down
232 changes: 149 additions & 83 deletions src/components/home/Todo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Todo[]>([]);
Expand Down Expand Up @@ -112,17 +118,57 @@ const Todo = () => {
/* 날짜(일수) 차이 문자열 */
const getDateString = (date: any) => {
if (isToday(date)) {
return <span style={{ color: theme.colors.primary500 }}>오늘</span>;
return "오늘";
} else {
const diff = getDayDifference(date);
return (
<span style={{ color: theme.colors.primary500 }}>
{diff > 0 ? `${diff}일 전` : `${-diff}일 후`}
</span>
);
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<HTMLDivElement>(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);

Expand All @@ -135,84 +181,99 @@ const Todo = () => {
};

return (
<TodoContainer>
오늘 할 일 잊지마세요!
<DateLine>
<Image
src="/assets/icons/ic_chevron_left.svg"
alt="left"
width={20}
height={20}
onClick={handleBackDay}
style={{ cursor: "pointer" }}
/>
{todayMonth}{todayDate}{dayOfWeek}요일
{getDateString(currentDate)}
<Image
src="/assets/icons/ic_chevron_right.svg"
alt="right"
width={20}
height={20}
onClick={handleForwardDay}
style={{
opacity: isToday(currentDate) ? 0.5 : 1,
cursor: isToday(currentDate) ? "" : "pointer",
}}
/>
</DateLine>
<TodoLists>
{todoData && todoData.length > 0 ? (
todoData.map((data, index) => (
<ListBox
key={index}
id={data.todoId}
listboxType={data.isAssigned ? "check" : "direct"}
color={data.deadline === "내일까지" ? "orange" : "mint"}
type={
categories.find((category) => category.value === data.todoType)
?.label || data.todoType
}
onClick={() => {
changeTodo(data.todoId);
<>
<TodoContainer ref={containerRef}>
오늘 할 일 잊지마세요!
<Row>
<DateLine>
<Image
src="/assets/icons/ic_chevron_left.svg"
alt="left"
width={20}
height={20}
onClick={handleBackDay}
style={{ cursor: "pointer" }}
/>
{todayMonth}{todayDate}{dayOfWeek}요일
<span style={{ color: theme.colors.primary500 }}>
{getDateString(currentDate)}
</span>
<Image
src="/assets/icons/ic_chevron_right.svg"
alt="right"
width={20}
height={20}
onClick={handleForwardDay}
style={{
opacity: isToday(currentDate) ? 0.5 : 1,
cursor: isToday(currentDate) ? "" : "pointer",
}}
text={data.description}
time={`${getDayOfWeek(data.deadline)}까지`}
checked={data.status === "COMPLETE"}
onDelete={() => deleteTodo(data.todoId)}
/>
))
) : (
<NoData>할 일이 없어요!</NoData>
)}
</TodoLists>
<Plus>
<PlusButton onClick={handleOpenAddTodo}>
</DateLine>
<Image
src="/assets/icons/ic_plus.svg"
alt="add"
width={20}
height={20}
src="/assets/icons/ic_volumn.svg"
alt="sound"
width={21}
height={21}
onClick={handleVoiceConversion}
style={{ cursor: "pointer" }}
/>
</Row>
<TodoLists>
{todoData && todoData.length > 0 ? (
todoData.map((data, index) => (
<ListBox
key={index}
id={data.todoId}
listboxType={data.isAssigned ? "check" : "direct"}
color={data.deadline === "내일까지" ? "orange" : "mint"}
type={
categories.find(
(category) => 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)}
/>
))
) : (
<NoData>할 일이 없어요!</NoData>
)}
</TodoLists>
<Plus>
<PlusButton onClick={handleOpenAddTodo}>
<Image
src="/assets/icons/ic_plus.svg"
alt="add"
width={20}
height={20}
/>
할 일 직접 추가하기
</PlusButton>
</Plus>
{addTodo && (
<AddTodoPopup
onClose={handleCloseAddTodo}
setShowToast={setShowToast}
render={render}
setRenderData={setRenderData}
/>
)}
{showToast && (
<Toast
message="할 일이 추가되었어요!"
type="basic"
duration={2000}
onClose={() => setShowToast(false)}
/>
할 일 직접 추가하기
</PlusButton>
</Plus>
{addTodo && (
<AddTodoPopup
onClose={handleCloseAddTodo}
setShowToast={setShowToast}
render={render}
setRenderData={setRenderData}
/>
)}
{showToast && (
<Toast
message="할 일이 추가되었어요!"
type="basic"
duration={2000}
onClose={() => setShowToast(false)}
/>
)}
</TodoContainer>
)}
</TodoContainer>
</>
);
};

Expand All @@ -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`
Expand Down
16 changes: 16 additions & 0 deletions src/redux/slices/audioSlice.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 4985035

Please sign in to comment.