diff --git a/public/correct.png b/public/correct.png
new file mode 100644
index 0000000..858035d
Binary files /dev/null and b/public/correct.png differ
diff --git a/public/learningFinish.png b/public/learningFinish.png
new file mode 100644
index 0000000..d803975
Binary files /dev/null and b/public/learningFinish.png differ
diff --git a/public/onboardingFinish.png b/public/onboardingFinish.png
new file mode 100644
index 0000000..6027df3
Binary files /dev/null and b/public/onboardingFinish.png differ
diff --git a/public/wrong.png b/public/wrong.png
new file mode 100644
index 0000000..b02fd62
Binary files /dev/null and b/public/wrong.png differ
diff --git a/src/components/common/FinishScreen.tsx b/src/components/common/FinishScreen.tsx
new file mode 100644
index 0000000..68def70
--- /dev/null
+++ b/src/components/common/FinishScreen.tsx
@@ -0,0 +1,5 @@
+const FinishScreen = () => {
+ return
FinishScreen
;
+};
+
+export default FinishScreen;
diff --git a/src/components/common/NextBtn.tsx b/src/components/common/NextBtn.tsx
index 1368211..7b113bf 100644
--- a/src/components/common/NextBtn.tsx
+++ b/src/components/common/NextBtn.tsx
@@ -3,14 +3,18 @@ import { CommonBtn } from "./common.styled";
interface NextBtnProps {
isActive: boolean;
text: string;
- width: string;
+ width?: string;
handleBtn: () => void;
}
-const NextBtn = ({ width, isActive, text, handleBtn }: NextBtnProps) => {
+const NextBtn = (props: NextBtnProps) => {
return (
-
- {text}
+
+ {props.text}
);
};
diff --git a/src/components/common/progressBar/ProgressBar.styeld.ts b/src/components/common/progressBar/ProgressBar.styeld.ts
new file mode 100644
index 0000000..22c6d92
--- /dev/null
+++ b/src/components/common/progressBar/ProgressBar.styeld.ts
@@ -0,0 +1,25 @@
+import styled from "styled-components";
+
+export const ProgressBarWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: 14px;
+ border-radius: 20px;
+ background: #ededed;
+ margin-top: 2rem;
+ position: relative;
+`;
+
+export const Progress = styled.div<{ $percentage: number }>`
+ background-color: #ffd600;
+ border-top-left-radius: 20px;
+ border-bottom-left-radius: 20px;
+ border-top-right-radius: ${(props) => (props.$percentage >= 99 ? 20 : 0)}px;
+ border-bottom-right-radius: ${(props) =>
+ props.$percentage >= 99 ? 20 : 0}px;
+ height: 100%;
+ width: ${(props) => props.$percentage}%;
+ transition: width 0.5s ease-in-out;
+ position: relative;
+`;
diff --git a/src/components/common/progressBar/ProgressBar.tsx b/src/components/common/progressBar/ProgressBar.tsx
new file mode 100644
index 0000000..e0a601c
--- /dev/null
+++ b/src/components/common/progressBar/ProgressBar.tsx
@@ -0,0 +1,15 @@
+import * as S from "./ProgressBar.styeld";
+
+interface ProgressBarProps {
+ percentage: number;
+}
+
+const ProgressBar = (props: ProgressBarProps) => {
+ return (
+
+
+
+ );
+};
+
+export default ProgressBar;
diff --git a/src/components/common/selectOption/SelectBtn.tsx b/src/components/common/selectOption/SelectBtn.tsx
new file mode 100644
index 0000000..20961a2
--- /dev/null
+++ b/src/components/common/selectOption/SelectBtn.tsx
@@ -0,0 +1,16 @@
+import { SelectBtnProps } from "@type/selectList";
+import * as S from "./SelectOptionList.styled";
+import { colorSets } from "@utils/defaultData";
+
+const SelectBtn = (props: SelectBtnProps) => {
+ const colorSet = colorSets[props.colorName];
+
+ return (
+
+ {props.text}
+ {props.imgURL && }
+
+ );
+};
+
+export default SelectBtn;
diff --git a/src/components/common/selectOption/SelectOptionList.styled.ts b/src/components/common/selectOption/SelectOptionList.styled.ts
new file mode 100644
index 0000000..3762fb1
--- /dev/null
+++ b/src/components/common/selectOption/SelectOptionList.styled.ts
@@ -0,0 +1,35 @@
+import { ColorSet } from "@type/selectList";
+import styled from "styled-components";
+
+export const SelectOptionContainer = styled.div<{ width?: string }>`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+ gap: 1rem;
+ width: ${({ width }) => (width ? width : "100%")};
+`;
+
+export const SelectBtnWrapper = styled.div<{ $colorSet: ColorSet }>`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0 1.5rem;
+ box-sizing: border-box;
+ width: 100%;
+ height: 5rem;
+ font-size: 2rem;
+ font-weight: 700;
+ background-color: ${({ $colorSet }) => $colorSet.background};
+ color: ${({ $colorSet }) => $colorSet.color};
+ border: 1px solid ${({ $colorSet }) => $colorSet.border};
+ border-radius: 6px;
+`;
+
+export const SelectIcon = styled.img`
+ width: 2.3rem;
+ height: 2.3rem;
+ margin-right: 10px;
+ vertical-align: middle;
+`;
diff --git a/src/components/common/selectOption/SelectOptionList.tsx b/src/components/common/selectOption/SelectOptionList.tsx
new file mode 100644
index 0000000..525fbc0
--- /dev/null
+++ b/src/components/common/selectOption/SelectOptionList.tsx
@@ -0,0 +1,52 @@
+import { SelectListProps } from "@type/selectList";
+import SelectBtn from "./SelectBtn";
+import * as S from "./SelectOptionList.styled";
+
+const SelectOptionList = (props: SelectListProps) => {
+ const getColorName = (state: string) => {
+ switch (state) {
+ case "correct":
+ return "green";
+ case "wrong":
+ return "red";
+ case "selected":
+ return "yellow";
+ case "default":
+ default:
+ return "gray";
+ }
+ };
+
+ const getImgURL = (state: string) => {
+ switch (state) {
+ case "correct":
+ return "/correct.png";
+ case "wrong":
+ return "/wrong.png";
+ case "selected":
+ return "";
+ default:
+ return "";
+ }
+ };
+
+ const handleSelect = (value: string | number | null) => {
+ props.setter(value);
+ };
+
+ return (
+
+ {props.selectList.map((element) => (
+ handleSelect(element.value)}
+ />
+ ))}
+
+ );
+};
+
+export default SelectOptionList;
diff --git a/src/components/login/Login.tsx b/src/components/login/Login.tsx
index be19cec..9165b49 100644
--- a/src/components/login/Login.tsx
+++ b/src/components/login/Login.tsx
@@ -1,5 +1,5 @@
import { CommonWrapper } from "@common/common.styled";
-import { LoginBtn, LogoBox, Title } from "./Login.styled";
+import * as S from "./Login.styled";
import { kakaoURL } from "./kakaoLoginConfig";
const Login = () => {
@@ -9,9 +9,9 @@ const Login = () => {
return (
- {`{메인소개 멘트}`}
- {`{로고이미지}`}
-
+ {`{메인소개 멘트}`}
+ {`{로고이미지}`}
+
{
alt="카카오 로그인 버튼"
onClick={toLogin}
/>
-
+
);
};
diff --git a/src/components/onboarding/AgeInput.tsx b/src/components/onboarding/AgeInput.tsx
new file mode 100644
index 0000000..e2c08eb
--- /dev/null
+++ b/src/components/onboarding/AgeInput.tsx
@@ -0,0 +1,88 @@
+import { colorSets } from "@utils/defaultData";
+import React, { ChangeEvent, KeyboardEvent } from "react";
+import styled from "styled-components";
+
+interface AgeInputProps {
+ age: string | number | null;
+ setter: (value: number | null) => void;
+}
+
+const AgeInput = ({ age, setter }: AgeInputProps) => {
+ const [isEditing, setIsEditing] = React.useState(false);
+
+ const handleChange = (event: ChangeEvent) => {
+ const value = event.target.value;
+ const newValue = value === "" ? null : Number(value);
+ setter(newValue);
+ };
+
+ const handleBlur = () => {
+ setIsEditing(false);
+ };
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === "Enter") {
+ setIsEditing(false);
+ }
+ };
+
+ return (
+
+ {isEditing ? (
+
+ ) : (
+ setIsEditing(true)}>
+ {age !== null ? `만 ${age}세` : "만"}
+
+ )}
+
+ );
+};
+
+export default AgeInput;
+
+const Container = styled.div<{ $age?: any }>`
+ display: flex;
+ align-items: center;
+ padding: 0 1rem;
+ box-sizing: border-box;
+ width: 50%;
+ height: 4.5rem;
+ font-size: 2rem;
+ font-weight: 700;
+ border: 1px solid
+ ${({ $age }) =>
+ $age ? colorSets["yellow"].border : colorSets["gray"].border};
+ background-color: ${({ $age }) =>
+ $age ? colorSets["yellow"].background : "none"};
+ border-radius: 6px;
+ position: relative;
+`;
+
+const Text = styled.div`
+ cursor: pointer;
+ width: 100%;
+ line-height: 4.5rem;
+ padding: 0 1rem;
+`;
+
+const Input = styled.input`
+ font-size: 2rem;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 0 1.5rem;
+ border: none;
+ border-radius: 6px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ background-color: transparent;
+`;
diff --git a/src/components/onboarding/SelectAge.tsx b/src/components/onboarding/SelectAge.tsx
new file mode 100644
index 0000000..a4bca1e
--- /dev/null
+++ b/src/components/onboarding/SelectAge.tsx
@@ -0,0 +1,21 @@
+import AgeInput from "./AgeInput";
+import * as S from "./onboarding.styled";
+
+interface SelectAgeProps {
+ age: string | number | null;
+ setter: (value: string | number | null) => void;
+}
+
+const SelectAge = ({ age, setter }: SelectAgeProps) => {
+ return (
+
+ 학습자의 나이는 몇살인가요?
+
+ 만 나이
+
+
+
+ );
+};
+
+export default SelectAge;
diff --git a/src/components/onboarding/SelectLanguage.tsx b/src/components/onboarding/SelectLanguage.tsx
new file mode 100644
index 0000000..a0b08f0
--- /dev/null
+++ b/src/components/onboarding/SelectLanguage.tsx
@@ -0,0 +1,21 @@
+import Dropdown from "@components/common/dropDown/Dropdown";
+import * as S from "./onboarding.styled";
+import { nationElements } from "@utils/defaultData";
+
+interface SelectLanguageProps {
+ setter: (value: string | number | null) => void;
+}
+
+const SelectLanguage = ({ setter }: SelectLanguageProps) => {
+ return (
+
+ 어떤 언어를 배우시나요?
+
+ 언어
+
+
+
+ );
+};
+
+export default SelectLanguage;
diff --git a/src/components/onboarding/SelectLevel.tsx b/src/components/onboarding/SelectLevel.tsx
new file mode 100644
index 0000000..52a5b5e
--- /dev/null
+++ b/src/components/onboarding/SelectLevel.tsx
@@ -0,0 +1,39 @@
+import * as S from "./onboarding.styled";
+import useSelectLevel from "@hooks/useSelectLevel";
+import { nationElements } from "@utils/defaultData";
+import SelectOptionList from "@common/selectOption/SelectOptionList";
+
+interface SelectLevelProps {
+ languageId: number | string;
+ setter: (value: string | number | null) => void;
+}
+
+const SelectLevel = ({ languageId, setter }: SelectLevelProps) => {
+ const { selectedLevel, setSelectedLevel, levelOptions } = useSelectLevel();
+ const language = nationElements.find(
+ (element) => element.value === languageId
+ );
+
+ const handleOptionChange = (value: string | number | null) => {
+ setSelectedLevel(value);
+ setter(value);
+ };
+
+ return (
+
+ {language?.text}를 얼마나 알고 계신가요?
+
+ ({
+ text: option.text,
+ value: option.value,
+ state: selectedLevel === option.value ? "selected" : "default",
+ }))}
+ setter={handleOptionChange}
+ />
+
+
+ );
+};
+
+export default SelectLevel;
diff --git a/src/components/onboarding/onboarding.styled.ts b/src/components/onboarding/onboarding.styled.ts
new file mode 100644
index 0000000..14bbc11
--- /dev/null
+++ b/src/components/onboarding/onboarding.styled.ts
@@ -0,0 +1,30 @@
+import styled from "styled-components";
+
+export const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ min-height: 70%;
+ margin-top: 6rem;
+ gap: 4rem;
+`;
+
+export const Title = styled.div`
+ font-size: 2.5rem;
+ font-weight: 800;
+`;
+
+export const SubContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ gap: 2rem;
+`;
+
+export const SubTitle = styled.div`
+ font-size: 2rem;
+ font-weight: 700;
+ color: #b1b1b1;
+`;
+
+
diff --git a/src/components/tales/readTale/ReadTale.tsx b/src/components/tales/readTale/ReadTale.tsx
index d345b52..c64b07a 100644
--- a/src/components/tales/readTale/ReadTale.tsx
+++ b/src/components/tales/readTale/ReadTale.tsx
@@ -1,12 +1,12 @@
import Header from "@components/common/header/Header";
import * as S from "./ReadTale.styled";
import Dropdown from "@components/common/dropDown/Dropdown";
-import { nationElements } from "@pages/OnboardingPage";
import { useEffect, useState } from "react";
import NextBtn from "@components/common/NextBtn";
import LoadingScreen from "@components/common/spinner/LoadingScreen";
import { createTale } from "@apis/createTales";
import { useLocation } from "react-router-dom";
+import { nationElements } from "@utils/defaultData";
const ReadTale = () => {
const location = useLocation();
diff --git a/src/components/tales/taleDetail/TaleDetail.tsx b/src/components/tales/taleDetail/TaleDetail.tsx
index fbe1047..114835b 100644
--- a/src/components/tales/taleDetail/TaleDetail.tsx
+++ b/src/components/tales/taleDetail/TaleDetail.tsx
@@ -1,11 +1,12 @@
import Header from "@components/common/header/Header";
import * as S from "./TaleDetail.styled";
import Dropdown from "@components/common/dropDown/Dropdown";
-import { nationElements } from "@pages/OnboardingPage";
import { useEffect, useState } from "react";
import NextBtn from "@components/common/NextBtn";
+import { nationElements } from "@utils/defaultData";
import { useLocation, useNavigate } from "react-router-dom";
+
const TaleDetail = () => {
const location = useLocation();
const { selectKeywords } = location.state || {};
diff --git a/src/hooks/test.tsx b/src/hooks/test.tsx
deleted file mode 100644
index e69de29..0000000
diff --git a/src/hooks/useAgeInput.ts b/src/hooks/useAgeInput.ts
new file mode 100644
index 0000000..033fd68
--- /dev/null
+++ b/src/hooks/useAgeInput.ts
@@ -0,0 +1,17 @@
+import { useState } from "react";
+
+const useAgeInput = () => {
+ const [age, setAge] = useState(null);
+
+ const updateAge = (value: number | null) => {
+ setAge(value);
+ };
+
+ const getAgeText = () => {
+ return age !== null ? `만 ${age}세` : "만";
+ };
+
+ return { age, updateAge, getAgeText };
+};
+
+export default useAgeInput;
diff --git a/src/hooks/useOnboarding.ts b/src/hooks/useOnboarding.ts
new file mode 100644
index 0000000..082aff6
--- /dev/null
+++ b/src/hooks/useOnboarding.ts
@@ -0,0 +1,51 @@
+import { useState, useEffect } from "react";
+
+const useOnboarding = () => {
+ const [languageId, setLanguageId] = useState(null);
+ const [learningLevel, setLearningLevel] = useState(
+ null
+ );
+ const [age, setAge] = useState(null);
+ const [currentStep, setCurrentStep] = useState(0);
+ const [isStepCompleted, setIsStepCompleted] = useState([false, false, false]);
+
+ const handleNextStep = () => {
+ if (currentStep < 2) {
+ setCurrentStep(currentStep + 1);
+ } else {
+ console.log(languageId, learningLevel, age);
+ }
+ };
+
+ const checkStepCompletion = () => {
+ const stepsCompletion = [false, false, false];
+ if (languageId) stepsCompletion[0] = true;
+ if (learningLevel) stepsCompletion[1] = true;
+ if (age !== null) stepsCompletion[2] = true;
+
+ setIsStepCompleted(stepsCompletion);
+ };
+
+ useEffect(() => {
+ checkStepCompletion();
+ }, [languageId, learningLevel, age]);
+
+ const isLastStep = currentStep === 2;
+ const isNextBtnActive = isStepCompleted[currentStep];
+
+ return {
+ languageId,
+ setLanguageId,
+ learningLevel,
+ setLearningLevel,
+ age,
+ setAge,
+ currentStep,
+ setCurrentStep,
+ isLastStep,
+ isNextBtnActive,
+ handleNextStep,
+ };
+};
+
+export default useOnboarding;
diff --git a/src/hooks/useResult.ts b/src/hooks/useResult.ts
new file mode 100644
index 0000000..f3c5173
--- /dev/null
+++ b/src/hooks/useResult.ts
@@ -0,0 +1,11 @@
+import { useState, useCallback } from "react";
+
+export const useResult = () => {
+ const [result, setResult] = useState(null);
+
+ const updateResult = useCallback((value: string | number | null) => {
+ setResult(value);
+ }, []);
+
+ return [result, updateResult] as const;
+};
diff --git a/src/hooks/useSelectLevel.ts b/src/hooks/useSelectLevel.ts
new file mode 100644
index 0000000..2b0ea28
--- /dev/null
+++ b/src/hooks/useSelectLevel.ts
@@ -0,0 +1,23 @@
+import { useState } from "react";
+
+const useSelectLevel = () => {
+ const [selectedLevel, setSelectedLevel] = useState(
+ null
+ );
+
+ const levelOptions = [
+ { text: "처음 배워요", value: "1000" },
+ { text: "자주 사용되는 단어 몇 개를 알고 있어요", value: "2000" },
+ { text: "기본적인 대화를 할 수 있어요", value: "3000" },
+ { text: "다양한 주제에 대해 이야기할 수 있어요", value: "4000" },
+ { text: "대부분의 주제에 대해 상세하게 논의할 수 있어요", value: "5000" },
+ ];
+
+ return {
+ selectedLevel,
+ setSelectedLevel,
+ levelOptions,
+ };
+};
+
+export default useSelectLevel;
diff --git a/src/pages/OnboardingPage.tsx b/src/pages/OnboardingPage.tsx
index ce4f6fa..466b469 100644
--- a/src/pages/OnboardingPage.tsx
+++ b/src/pages/OnboardingPage.tsx
@@ -1,45 +1,40 @@
-import Dropdown from "@common/dropDown/Dropdown";
-import { DropdownElement } from "@type/dropdown";
-import { useEffect, useState } from "react";
import { CommonWrapper } from "@common/common.styled";
-
-export const nationElements: DropdownElement[] = [
- {
- text: "선택해주세요",
- value: null,
- },
- {
- imgURL: `/america.png`,
- text: "영어",
- value: "영어",
- },
- {
- imgURL: `/china.png`,
- text: "중국어",
- value: "중국어",
- },
- {
- imgURL: `/korea.png`,
- text: "한국어",
- value: "한국어",
- },
- {
- imgURL: `/japan.png`,
- text: "일본어",
- value: "일본어",
- },
-];
+import ProgressBar from "@components/common/progressBar/ProgressBar";
+import SelectLanguage from "@components/onboarding/SelectLanguage";
+import NextBtn from "@components/common/NextBtn";
+import SelectLevel from "@components/onboarding/SelectLevel";
+import SelectAge from "@components/onboarding/SelectAge";
+import useOnboarding from "@hooks/useOnboarding";
const OnboardingPage = () => {
- const [result, setResult] = useState();
-
- useEffect(() => {
- console.log(result);
- }, [result]);
+ const {
+ languageId,
+ setLanguageId,
+ learningLevel,
+ setLearningLevel,
+ age,
+ setAge,
+ currentStep,
+ isLastStep,
+ isNextBtnActive,
+ handleNextStep,
+ } = useOnboarding();
return (
-
+
+ {currentStep === 0 && }
+ {currentStep === 1 && languageId && (
+
+ )}
+ {currentStep === 2 && languageId && learningLevel && (
+
+ )}
+
);
};
diff --git a/src/stories/Dropdown.stories.ts b/src/stories/Dropdown.stories.ts
index d82cc24..a9a1c2d 100644
--- a/src/stories/Dropdown.stories.ts
+++ b/src/stories/Dropdown.stories.ts
@@ -3,7 +3,7 @@ import Dropdown from "@common/dropDown/Dropdown";
import { DropdownProps } from "@type/dropdown";
import { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
-import { nationElements } from "@pages/OnboardingPage";
+import { nationElements } from "@utils/defaultData";
const meta: Meta = {
title: "Components/Dropdown",
diff --git a/src/type/selectList.d.ts b/src/type/selectList.d.ts
new file mode 100644
index 0000000..62e5112
--- /dev/null
+++ b/src/type/selectList.d.ts
@@ -0,0 +1,24 @@
+export interface ColorSet {
+ background: string;
+ border: string;
+ color: string;
+}
+
+export interface SelectListProps {
+ selectList: SelectOptionElement[];
+ setter: (value: string | number | null) => void;
+ width?: string;
+}
+
+export interface SelectListElement {
+ text: string;
+ state: "correct" | "wrong" | "selected" | "default";
+ value: string | number;
+}
+
+export interface SelectBtnProps {
+ text: string;
+ colorName: string;
+ imgURL?: string;
+ onClick: () => void;
+}
diff --git a/src/utils/defaultData.ts b/src/utils/defaultData.ts
new file mode 100644
index 0000000..3d535b4
--- /dev/null
+++ b/src/utils/defaultData.ts
@@ -0,0 +1,52 @@
+import { DropdownElement } from "@type/dropdown";
+import { ColorSet } from "@type/selectList";
+
+export const nationElements: DropdownElement[] = [
+ {
+ text: "선택해주세요",
+ value: null,
+ },
+ {
+ imgURL: `/america.png`,
+ text: "영어",
+ value: 1,
+ },
+ {
+ imgURL: `/china.png`,
+ text: "중국어",
+ value: 2,
+ },
+ {
+ imgURL: `/korea.png`,
+ text: "한국어",
+ value: 3,
+ },
+ {
+ imgURL: `/japan.png`,
+ text: "일본어",
+ value: 4,
+ },
+];
+
+export const colorSets: { [key: string]: ColorSet } = {
+ red: {
+ background: "#FFDADA",
+ border: "#FF5757",
+ color: "black",
+ },
+ green: {
+ background: "#E6FFE5",
+ border: "#6CE368",
+ color: "black",
+ },
+ yellow: {
+ background: "#FFF9E5",
+ border: "#FFC300",
+ color: "black",
+ },
+ gray: {
+ background: "none",
+ border: "#D8D8D8",
+ color: "#909090",
+ },
+};
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 2d14fa1..7af903b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,7 +9,8 @@
"@components/*": ["src/components/*"],
"@common/*": ["src/components/common/*"],
"@utils/*": ["src/utils/*"],
- "@apis/*": ["src/apis/*"]
+ "@apis/*": ["src/apis/*"],
+ "@hooks/*": ["src/hooks/*"]
}
},
"files": [],
diff --git a/vite.config.ts b/vite.config.ts
index 791aef7..37099fa 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -22,6 +22,7 @@ export default defineConfig({
},
{ find: "@utils", replacement: path.resolve(__dirname, "src/utils") },
{ find: "@apis", replacement: path.resolve(__dirname, "src/apis") },
+ { find: "@hooks", replacement: path.resolve(__dirname, "src/hooks") },
],
},
});