diff --git a/src/components/CheckBox/index.tsx b/src/components/CheckBox/index.tsx index 724fd042..4f92d41e 100644 --- a/src/components/CheckBox/index.tsx +++ b/src/components/CheckBox/index.tsx @@ -6,6 +6,7 @@ export type Size = "lg" | "md"; export interface CheckBoxProps { size?: Size; + id?: string; name: string; checked: boolean; disabled?: boolean; @@ -15,6 +16,7 @@ export interface CheckBoxProps { export default function CheckBox({ disabled = false, + id = "", name, checked, onClick, @@ -25,6 +27,7 @@ export default function CheckBox({ theme.colors.neutral["30"]}; border: 1px solid ${({ theme }) => theme.colors.neutral["30"]}; + cursor: pointer; &:disabled { background: ${({ theme }) => theme.colors.neutral["30"]}; diff --git a/src/components/Layout/Header/HeaderCenterSection.style.ts b/src/components/Layout/Header/HeaderCenterSection.style.ts index 1c863c50..f6902255 100644 --- a/src/components/Layout/Header/HeaderCenterSection.style.ts +++ b/src/components/Layout/Header/HeaderCenterSection.style.ts @@ -4,4 +4,5 @@ export const HeaderCenterSectionContainer = styled.div` display: flex; align-items: center; justify-content: center; + flex-shrink: 0; `; diff --git a/src/components/Layout/Header/style.ts b/src/components/Layout/Header/style.ts index 3df90a95..9001a091 100644 --- a/src/components/Layout/Header/style.ts +++ b/src/components/Layout/Header/style.ts @@ -20,10 +20,14 @@ export const Contents = styled.div` padding: 0 16px; margin: 0 auto; - & > div { + & > div:not(:nth-of-type(2)) { width: calc(100% / 3); } + & > div:nth-of-type(2) { + min-width: calc(100% / 3); + } + svg { cursor: pointer; } diff --git a/src/components/Loader/style.ts b/src/components/Loader/style.ts index b74bbdd0..1cca9753 100644 --- a/src/components/Loader/style.ts +++ b/src/components/Loader/style.ts @@ -17,7 +17,7 @@ export const LoaderContainer = styled.div` display: flex; align-items: center; justify-content: center; - height: 100%; + height: 100dvh; width: 100%; & img { diff --git a/src/contexts/HelpDeskContext.tsx b/src/contexts/HelpDeskContext.tsx new file mode 100644 index 00000000..47c93e83 --- /dev/null +++ b/src/contexts/HelpDeskContext.tsx @@ -0,0 +1,120 @@ +import { createContext, useCallback, useState } from "react"; +import { Outlet } from "react-router-dom"; + +export interface Form { + email: string; + title: string; + content: string; +} + +type FormError = { + [K in keyof Form]: number; +}; + +interface State { + inquiryType: number; + form: Form; + error: FormError; + agree: boolean; +} + +interface FormAction { + updateInquiryType: (i: number) => void; + handleInputChange: ( + e: React.ChangeEvent, + ) => void; + handleAgreeChange: () => void; + resetForm: () => void; + validateForm: () => boolean; +} + +const INIT_FORM: Form = { + email: "", + title: "", + content: "", +}; + +const INIT_ERROR: FormError = { + email: 0, + title: 0, + content: 0, +}; + +export const HelpDeskContext = createContext({ + inquiryType: 0, + form: INIT_FORM, + agree: false, + error: INIT_ERROR, + updateInquiryType: () => {}, + handleInputChange: () => {}, + handleAgreeChange: () => {}, + resetForm: () => {}, + validateForm: () => false, +}); + +export function HelpDeskProvider() { + const [inquiryType, setInquiryType] = useState(0); + const [form, setForm] = useState(INIT_FORM); + const [agree, setAgree] = useState(false); + const [error, setError] = useState(INIT_ERROR); + + const updateInquiryType = (i: number) => { + setInquiryType(i); + }; + + const handleInputChange = ( + e: React.ChangeEvent, + ) => { + const { name, value } = e.target; + + if (name === "title" && value.length > 50) return; + if (name === "content" && value.length > 1000) return; + + setForm((prev) => ({ ...prev, [name]: value })); + }; + + const handleAgreeChange = () => { + setAgree((prev) => !prev); + }; + + const resetForm = useCallback(() => { + setForm(INIT_FORM); + setAgree(false); + setError(INIT_ERROR); + }, []); + + const validateForm = () => { + const emailPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + let result = true; + + for (const key in form) { + setError((prev) => ({ ...prev, [key]: 0 })); + if (form[key as keyof Form].trim().length === 0) { + setError((prev) => ({ ...prev, [key]: 1 })); + result = false; + } else if (key === "email" && !emailPattern.test(form[key])) { + setError((prev) => ({ ...prev, [key]: 2 })); + result = false; + } + } + return result; + }; + + const value = { + inquiryType, + form, + agree, + error, + updateInquiryType, + handleInputChange, + handleAgreeChange, + validateForm, + resetForm, + }; + + return ( + + + + ); +} diff --git a/src/features/common/hooks/useForm.ts b/src/features/common/hooks/useForm.ts deleted file mode 100644 index 761233e8..00000000 --- a/src/features/common/hooks/useForm.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { useState } from "react"; - -export default function useForm() { - const [form, setForm] = useState({ email: "", title: "", content: "" }); - const [error, setError] = useState({ email: 0, title: 0, content: 0 }); - - const errorMessage = { - email: ["", "이메일을 입력해 주세요.", "이메일 형식이 올바르지 않습니다."], - title: ["", "문의 제목을 입력해 주세요."], - content: ["", "문의 내용을 입력해 주세요."], - }; - - const handleInputChange = ( - e: React.ChangeEvent, - ) => { - const { name, value } = e.target; - - if (name === "title" && value.length > 50) return; - if (name === "content" && value.length > 1000) return; - - setForm((prev) => ({ ...prev, [name]: value })); - }; - - const resetForm = () => { - setForm({ email: "", title: "", content: "" }); - setError({ email: 0, title: 0, content: 0 }); - }; - - const send = (setSuccess: React.Dispatch>) => { - const emailPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; - - for (const key in form) { - setError((prev) => ({ ...prev, [key]: 0 })); - if ((form as Record)[key].trim().length === 0) { - setError((prev) => ({ ...prev, [key]: 1 })); - } else if (key === "email" && !emailPattern.test(form[key])) - setError((prev) => ({ ...prev, [key]: 2 })); - } - - const ok = - emailPattern.test(form.email) && - form.title.trim().length !== 0 && - form.content.trim().length !== 0; - - if (ok) { - // TODO: API 요청 - // POST 성공 시 - resetForm(); - setSuccess(true); - } - }; - - return { - form, - error, - errorMessage, - handleInputChange, - resetForm, - send, - }; -} diff --git a/src/features/common/hooks/useInquiryForm.ts b/src/features/common/hooks/useInquiryForm.ts new file mode 100644 index 00000000..486ecc26 --- /dev/null +++ b/src/features/common/hooks/useInquiryForm.ts @@ -0,0 +1,50 @@ +import { useContext } from "react"; + +import useSnackBar from "@/components/SnackBar/useSnackBar"; +import { HelpDeskContext } from "@/contexts/HelpDeskContext"; + +export default function useInquiryForm() { + const { + form, + agree, + error, + validateForm, + handleInputChange, + handleAgreeChange, + resetForm, + } = useContext(HelpDeskContext); + const snackbar = useSnackBar(); + + const errorMessage = { + email: ["", "이메일을 입력해 주세요.", "이메일 형식이 올바르지 않습니다."], + title: ["", "문의 제목을 입력해 주세요."], + content: ["", "문의 내용을 입력해 주세요."], + }; + + const send = (setSuccess: React.Dispatch>) => { + const isValidate = validateForm(); + + if (!agree) + snackbar.open({ message: "문의를 남기시려면 약관에 동의해주세요." }); + + const ok = isValidate && agree; + + if (ok) { + // TODO: API 요청 + // POST 성공 시 + resetForm(); + setSuccess(true); + } + }; + + return { + form, + agree, + error, + errorMessage, + handleInputChange, + handleAgreeChange, + resetForm, + send, + }; +} diff --git a/src/features/common/routes/HelpDesk/Form/index.tsx b/src/features/common/routes/HelpDesk/Form/index.tsx index e4a5234a..c78c72a5 100644 --- a/src/features/common/routes/HelpDesk/Form/index.tsx +++ b/src/features/common/routes/HelpDesk/Form/index.tsx @@ -1,18 +1,34 @@ +import { useEffect } from "react"; +import { Link } from "react-router-dom"; + import Button from "@/components/Button"; -import useForm from "@/features/common/hooks/useForm"; +import CheckBox from "@/components/CheckBox"; +import useInquiryForm from "@/features/common/hooks/useInquiryForm"; import { SelectContainer as FormContainer } from "../Select/style"; -import { Content, FormItem, FormTextInput, FormTextarea } from "./style"; +import { Content, FormItem, FormTextInput, FormTextarea, Terms } from "./style"; interface Props { - goPrev: () => void; - inquiryTypeName: string; + inquiryType: number; setSuccess: React.Dispatch>; } -export default function Form({ setSuccess }: Props) { - const { form, error, handleInputChange, errorMessage, send } = useForm(); +export default function Form({ inquiryType, setSuccess }: Props) { + const { + form, + agree, + error, + handleInputChange, + handleAgreeChange, + errorMessage, + send, + resetForm, + } = useInquiryForm(); + + useEffect(() => { + if (inquiryType === 0) resetForm(); + }, [inquiryType, resetForm]); return ( @@ -55,13 +71,18 @@ export default function Form({ setSuccess }: Props) { required /> -

개인 정보 처리 및 약관 동의

- diff --git a/src/features/common/routes/HelpDesk/Form/style.ts b/src/features/common/routes/HelpDesk/Form/style.ts index 6b4fed5b..2fb09cc1 100644 --- a/src/features/common/routes/HelpDesk/Form/style.ts +++ b/src/features/common/routes/HelpDesk/Form/style.ts @@ -19,6 +19,17 @@ export const Content = styled.div` flex-shrink: 0; margin-bottom: -8px; } + + a { + text-decoration: underline; + text-decoration-thickness: 1px; + } + + button { + width: 100%; + height: 48px; + margin-top: 50px; + } `; export const FormItem = styled.div<{ textarea?: boolean }>` @@ -47,3 +58,18 @@ export const FormTextarea = styled(Textarea)` border-radius: 12px; } `; + +export const Terms = styled.div` + width: 100%; + display: flex; + justify-content: flex-start; + align-items: center; + gap: 6px; + margin-top: 30px; + padding-left: 6px; + + label { + ${({ theme }) => theme.typo["body-2-r"]}; + cursor: pointer; + } +`; diff --git a/src/features/common/routes/HelpDesk/index.tsx b/src/features/common/routes/HelpDesk/index.tsx index 5b51fbe6..94ba8be9 100644 --- a/src/features/common/routes/HelpDesk/index.tsx +++ b/src/features/common/routes/HelpDesk/index.tsx @@ -1,34 +1,38 @@ -import styled from "@emotion/styled"; import { CaretLeft } from "@phosphor-icons/react"; -import { useState } from "react"; +import { useContext, useState } from "react"; import { useNavigate } from "react-router"; import Head from "@/components/Head"; import Header from "@/components/Layout/Header"; +import { HelpDeskContext } from "@/contexts/HelpDeskContext"; import Form from "./Form"; import Select from "./Select"; +import { HelpDeskContainer, Slide } from "./style"; import Success from "./Success"; export default function HelpDesk() { - const [inquiryType, setInquiryType] = useState(0); const inquiryTypeName = ["기능 추가 건의", "버그 신고", "기타 문의"]; - const [headerTitle, setHeaderTitle] = useState("고객센터"); - const [translateX, setTranslateX] = useState(0); + const CHANGE_HEADER_TITLE_DELAY = 200; + + const { inquiryType, updateInquiryType } = useContext(HelpDeskContext); + const [headerTitle, setHeaderTitle] = useState( + inquiryType === 0 ? "고객센터" : inquiryTypeName[inquiryType - 1], + ); + + const [translateX, setTranslateX] = useState(inquiryType === 0 ? 0 : -50); const [success, setSuccess] = useState(false); const navigate = useNavigate(); - const CHANGE_HEADER_TITLE_DELAY = 200; - const goPrev = () => { - setInquiryType(0); + updateInquiryType(0); setTranslateX(0); setTimeout(() => setHeaderTitle("고객센터"), CHANGE_HEADER_TITLE_DELAY); }; const handleItemClick = (i: number) => { - setInquiryType(i + 1); + updateInquiryType(i + 1); setTranslateX(-50); setTimeout( () => setHeaderTitle(inquiryTypeName[i]), @@ -54,29 +58,9 @@ export default function HelpDesk() {