Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 고객센터 약관 동의 및 context 추가 #229

Merged
merged 11 commits into from
Oct 17, 2023
Merged
3 changes: 3 additions & 0 deletions src/components/CheckBox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type Size = "lg" | "md";

export interface CheckBoxProps {
size?: Size;
id?: string;
name: string;
checked: boolean;
disabled?: boolean;
Expand All @@ -15,6 +16,7 @@ export interface CheckBoxProps {

export default function CheckBox({
disabled = false,
id = "",
name,
checked,
onClick,
Expand All @@ -25,6 +27,7 @@ export default function CheckBox({
<CheckboxContainer size={size} onClick={onClick}>
<Input
type="checkbox"
id={id}
name={name}
checked={checked}
disabled={disabled}
Expand Down
1 change: 1 addition & 0 deletions src/components/CheckBox/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const Input = styled.input`
margin: 0;
background-color: ${({ theme }) => theme.colors.neutral["30"]};
border: 1px solid ${({ theme }) => theme.colors.neutral["30"]};
cursor: pointer;

&:disabled {
background: ${({ theme }) => theme.colors.neutral["30"]};
Expand Down
1 change: 1 addition & 0 deletions src/components/Layout/Header/HeaderCenterSection.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export const HeaderCenterSectionContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
`;
6 changes: 5 additions & 1 deletion src/components/Layout/Header/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Loader/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const LoaderContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 100%;
height: 100dvh;
width: 100%;

& img {
Expand Down
120 changes: 120 additions & 0 deletions src/contexts/HelpDeskContext.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement | HTMLTextAreaElement>,
) => 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<State & FormAction>({
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<HTMLInputElement | HTMLTextAreaElement>,
) => {
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 (
<HelpDeskContext.Provider value={value}>
<Outlet />
</HelpDeskContext.Provider>
);
}
53 changes: 21 additions & 32 deletions src/features/common/hooks/useForm.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,33 @@
import { useState } from "react";
import { useContext } from "react";

import useSnackBar from "@/components/SnackBar/useSnackBar";
import { HelpDeskContext } from "@/contexts/HelpDeskContext";

export default function useForm() {
const [form, setForm] = useState({ email: "", title: "", content: "" });
const [error, setError] = useState({ email: 0, title: 0, content: 0 });
Comment on lines -4 to -5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

common/hooks/useForm이라는 이름이 나중에 겹치거나 찾을때 조금 어렵지 않을까 생각이 들어요~

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

common/hooks/useInquiryForm 이건 괜찮을까요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니답

const {
form,
agree,
error,
validateForm,
handleInputChange,
handleAgreeChange,
resetForm,
} = useContext(HelpDeskContext);
const snackbar = useSnackBar();

const errorMessage = {
email: ["", "이메일을 입력해 주세요.", "이메일 형식이 올바르지 않습니다."],
title: ["", "문의 제목을 입력해 주세요."],
content: ["", "문의 내용을 입력해 주세요."],
};

const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
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<React.SetStateAction<boolean>>) => {
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<string, string>)[key].trim().length === 0) {
setError((prev) => ({ ...prev, [key]: 1 }));
} else if (key === "email" && !emailPattern.test(form[key]))
setError((prev) => ({ ...prev, [key]: 2 }));
}
const isValidate = validateForm();

if (!agree)
snackbar.open({ message: "문의를 남기시려면 약관에 동의해주세요." });

const ok =
emailPattern.test(form.email) &&
form.title.trim().length !== 0 &&
form.content.trim().length !== 0;
const ok = isValidate && agree;

if (ok) {
// TODO: API 요청
Expand All @@ -52,9 +39,11 @@ export default function useForm() {

return {
form,
agree,
error,
errorMessage,
handleInputChange,
handleAgreeChange,
resetForm,
send,
};
Expand Down
45 changes: 33 additions & 12 deletions src/features/common/routes/HelpDesk/Form/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
import { useEffect } from "react";
import { Link } from "react-router-dom";

import Button from "@/components/Button";
import CheckBox from "@/components/CheckBox";
import useForm from "@/features/common/hooks/useForm";

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<React.SetStateAction<boolean>>;
}

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,
} = useForm();

useEffect(() => {
if (inquiryType === 0) resetForm();
}, [inquiryType, resetForm]);

return (
<FormContainer>
Expand Down Expand Up @@ -55,13 +71,18 @@ export default function Form({ setSuccess }: Props) {
required
/>
</FormItem>
<p style={{ marginTop: "30px" }}>개인 정보 처리 및 약관 동의</p>
<Button
size="lg"
style={{ width: "100%", height: "48px", marginTop: "50px" }}
name="보내기"
onClick={() => send(setSuccess)}
>
<Terms>
<CheckBox
id="agree"
name="agree"
checked={agree}
onChange={handleAgreeChange}
/>
<label htmlFor="agree">
개인정보 처리 및 <Link to="/terms/email">이용 약관 동의</Link>
</label>
</Terms>
<Button size="lg" name="보내기" onClick={() => send(setSuccess)}>
보내기
</Button>
</Content>
Expand Down
26 changes: 26 additions & 0 deletions src/features/common/routes/HelpDesk/Form/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>`
Expand Down Expand Up @@ -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;
}
`;
Loading