diff --git a/api/api.ts b/api/api.ts index 5163a332d2..96d68cb012 100644 --- a/api/api.ts +++ b/api/api.ts @@ -53,6 +53,11 @@ export const postSignIn = async (data: any) => { return fetchPOSTJson(url, data); }; +export const postSignUp = async(data: {email: string; password: string}) => { + const url = `${BASE_URL}sign-up`; + return fetchPOSTJson(url, data); +} + export const postCheckDuplicationEmail = async (data: any) => { const url = `${BASE_URL}check-email`; return fetchPOSTJson(url, data); diff --git a/components/SignInUp/EmailPwdInput.tsx b/components/SignInUp/EmailPwdInput.tsx index 3c687de02f..479ed8b9b9 100644 --- a/components/SignInUp/EmailPwdInput.tsx +++ b/components/SignInUp/EmailPwdInput.tsx @@ -1,168 +1,59 @@ -import { useState, useEffect, ChangeEvent, KeyboardEvent } from "react"; +import { useState, useEffect } from "react"; import styled from "styled-components"; import Image from "next/image"; -import eyeoff from "@/assets/icons/eye-off.png"; -import eyeon from "@/assets/icons/eye-on.png"; -import { changeInputBorderColor } from "@/utils/commonSigninupFunc"; -import { - emailInputValidationcheck, - loginPasswordInputValidationcheck, - signInPasswordInputValidationcheck, - emailDuplicationCheck, -} from "@/utils/validation"; -import { ERROR_MESSAGE } from "@/constants/errorMessage"; -import INPUT_STATUS from "@/constants/inputStatus"; +import eyeoff from "@/public/assets/icons/eye-off.png"; +import eyeon from "@/public/assets/icons/eye-on.png"; +import INPUT_ERROR_INFO from "@/type/inputErrorInfo"; + type EmailPwdInputPropsType = { title: string; type: string; - valueType: string; - isEyeIcon?: boolean; - setEmailValue?: React.Dispatch>; - emailValue?: string | ""; - setPasswordValue?: React.Dispatch>; - passwordValue?: string | undefined; - onEnterButtonClick: () => void; - loginStatus?: string; - signInStatus?: string; - setIsEmailValid?: React.Dispatch>; - setIsPasswordValid?: React.Dispatch>; - setIsPasswordConfirmValid?: React.Dispatch>; + eventFunc: { + onFocusOut: () => void; + onChange: () => void; + onKeydown: (e: React.KeyboardEvent) => void; + } + inputErrorInfo: INPUT_ERROR_INFO; + inputRef: React.RefObject }; + const EmailPwdInput = ({ title, type, - valueType, - isEyeIcon, - setEmailValue, - emailValue, - setPasswordValue, - passwordValue, - onEnterButtonClick, - loginStatus, - signInStatus, - setIsEmailValid, - setIsPasswordValid, - setIsPasswordConfirmValid, + eventFunc, + inputErrorInfo, + inputRef }: EmailPwdInputPropsType) => { - const INPUT_STATUS_VALUE = { - default: "default", - error: "error", - }; + const [inputStatus, setInputStatus] = useState("default"); const [inputErrorMessage, setInputErrorMessage] = useState(""); const [isViewPassword, setIsViewPassword] = useState(false); - const onInputChangeHandler = (e: ChangeEvent) => { - const inputValue = e.target.value; - if (valueType === "email") { - setEmailValue?.(inputValue); - } else if (valueType === "password") { - setPasswordValue?.(inputValue); - } - }; - - const setInputStatusAndErrorMessage = (status: "valid" | ErrorType) => { - status === "valid" - ? setInputStatus(INPUT_STATUS_VALUE.default) - : (setInputStatus(INPUT_STATUS_VALUE.error), - setInputErrorMessage(status.message)); - }; - - const onInputFocusOutHandler = (e: ChangeEvent) => { - const inputValue = e.target.value; - - switch (valueType) { - case "email": - emailInputFocusOutHandler(inputValue); - break; - case "password": { - passwordInputFocusOutHandler(inputValue); - break; - } - case "password2": - passwordConfirmInputFocusOutHandler(inputValue); - break; - default: - null; - } - }; - - const emailInputFocusOutHandler = async (email: string) => { - const emailInputStatus = emailInputValidationcheck(email); - setInputStatusAndErrorMessage(emailInputStatus); - if (emailInputStatus === "valid") { - setIsEmailValid?.(true); - if (signInStatus) { - const isDuplicateEmail = await emailDuplicationCheck(email); - isDuplicateEmail && - setInputStatusAndErrorMessage(INPUT_STATUS.inUseEmail); - } - } else { - setIsEmailValid?.(false); - } - }; - - const passwordInputFocusOutHandler = async (password: string) => { - let passwordInputStatus; - if (loginStatus) { - passwordInputStatus = loginPasswordInputValidationcheck(password); - } else { - passwordInputStatus = signInPasswordInputValidationcheck(password); - } - setInputStatusAndErrorMessage(passwordInputStatus); - passwordInputStatus === "valid" - ? setIsPasswordValid?.(true) - : setIsPasswordValid?.(false); - }; - - const passwordConfirmInputFocusOutHandler = async (password2: string) => { - const passwordInputStatus = signInPasswordInputValidationcheck( - passwordValue, - password2 - ); - setInputStatusAndErrorMessage(passwordInputStatus); - passwordInputStatus === "valid" - ? setIsPasswordConfirmValid?.(true) - : setIsPasswordConfirmValid?.(false); - }; useEffect(() => { - if (loginStatus === "fail") { - valueType === "email" - ? (setInputStatus(INPUT_STATUS_VALUE.error), - setInputErrorMessage(ERROR_MESSAGE.email.check)) - : (setInputStatus(INPUT_STATUS_VALUE.error), - setInputErrorMessage(ERROR_MESSAGE.password.check)); - } - }, [loginStatus]); - - const onInputFocusHandler = () => { - setInputStatus("writing"); - }; - - const KeyEventHandler = (e: KeyboardEvent) => { - const inputElement = e.target as HTMLInputElement; - if (e.key === "Enter") { - onEnterButtonClick(); - inputElement.blur(); + if(inputErrorInfo !== "valid") { + setInputErrorMessage(inputErrorInfo.message); + } else { + setInputErrorMessage(''); } - }; + + },[inputErrorInfo]) return (

{title}

- {isEyeIcon && ( + {type==="password" && ( { setIsViewPassword(!isViewPassword); @@ -171,31 +62,24 @@ const EmailPwdInput = ({ eye_off_icon )} - {inputStatus === "error" && ( + {inputErrorMessage && ( {inputErrorMessage} )}
); }; -type ErrorType = - | { - errorName: string; - type: string; - message: string; - } - | "valid"; const InputDiv = styled.div` width: 100%; margin-bottom: 24px; position: relative; `; -const EmailPasswordInput = styled.input<{ status: string }>` +const EmailPasswordInput = styled.input<{ inputErrorInfo: INPUT_ERROR_INFO }>` width: 100%; padding: 18px 15px; - border: ${({ status }) => - `1px solid ${changeInputBorderColor(status) ?? "var(--Grey_300)"}`}; + border: ${({ inputErrorInfo }) => + `1px solid ${inputErrorInfo === "valid" ? "var(--Grey_300)" : "red"}`}; border-radius: 8px; outline: none; margin-top: 12px; diff --git a/components/SignInUp/Input.tsx b/components/SignInUp/Input.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/components/common/FolderItem.tsx b/components/common/FolderItem.tsx index c28c1de704..70f3b1b4fe 100644 --- a/components/common/FolderItem.tsx +++ b/components/common/FolderItem.tsx @@ -1,35 +1,56 @@ import styled from "styled-components"; import { useState } from "react"; -import { CalcTime } from "@/utils/calculator"; -import Star from "@/assets/icons/card_star.svg"; -import Kebab from "@/assets/icons/kebab.svg"; -import logo from "@/assets/icons/logo.png"; +import { calcTime } from "@/utils/calculator"; +import Star from "@/public/assets/icons/card_star.svg"; +import Kebab from "@/public/assets/icons/kebab.svg"; +import logo from "@/public/assets/icons/logo.png"; import { PopOver } from "./modals/PopOver"; import Image from "next/image"; import styles from "@/styles/shared.module.css"; +import Link from "next/link"; import { CommonFolderInfoProps } from "@/constants/commonTypes"; +import { useModal } from "@/contexts/ModalContext"; +import { DeleteModal } from "./modals/DeleteModal"; +import { AddToFolder } from "./modals/AddToFolder"; interface FolderItemProps { item: CommonFolderInfoProps; - $isModalVisible: string; - setIsModalVisible: any; } function FolderItem({ item, - $isModalVisible, - setIsModalVisible, }: FolderItemProps) { const { imageSource, createdAt, description, url, id } = item; const { created_at, favorite, image_source } = item; const [isPopOverVisible, setIsPopOverVisible] = useState(false); const createdAtTime: string = String(createdAt ?? created_at); - const time = CalcTime(createdAtTime); + const time = calcTime(createdAtTime); const img_src = image_source || imageSource; + const modal = useModal(); + + const openDeleteModal = () => { + modal.openModal(); + }; + + const openAddToFolderModal = () => { + modal.openModal() + }; + + const POPOVER_INFO = [ + { + option: "삭제하기", + callback: openDeleteModal + }, + { + option: "폴더에 추가", + callback: openAddToFolderModal + } + ] + return ( - + {img_src ? ( @@ -49,25 +70,21 @@ function FolderItem({ e.preventDefault(); setIsPopOverVisible(!isPopOverVisible); }} - /> + /> {description} 2023. 3. 15 - {" "} - * - - + + + ); } @@ -111,12 +128,13 @@ const DefaultImage = styled.div` const TextBox = styled.div` width: 100%; - height: 135px; + height: auto; display: flex; flex-direction: column; gap: 10px; padding: 15px 20px; border-radius: 0px 0px 15px 15px; + text-decoration-line: none; `; const Description = styled.div` @@ -129,6 +147,7 @@ const Description = styled.div` line-height: 24px; margin: 0px; color: #000; + text-decoration: none; display: -webkit-box; -webkit-box-orient: vertical; diff --git a/components/common/FolderList.tsx b/components/common/FolderList.tsx index b9691f6aca..860827e8d5 100644 --- a/components/common/FolderList.tsx +++ b/components/common/FolderList.tsx @@ -4,30 +4,18 @@ import { CommonFolderInfoProps } from "@/constants/commonTypes"; type FolderPropsType = { items: CommonFolderInfoProps[]; - $isModalVisible: any; - setIsModalVisible: any; }; function FolderList({ items, - $isModalVisible, - setIsModalVisible, }: FolderPropsType) { + return ( - {items.map((item: CommonFolderInfoProps) => { - { - return ( - - ); - } - })} + {items.map((item: CommonFolderInfoProps) => ( + + ))} ); diff --git a/components/common/HeaderElement.tsx b/components/common/HeaderElement.tsx index df2852d5cc..a894e176d4 100644 --- a/components/common/HeaderElement.tsx +++ b/components/common/HeaderElement.tsx @@ -1,9 +1,10 @@ import styled from "styled-components"; -import logo from "@/assets/Linkbrary.png"; -import profile from "@/assets/icons/icon_myprofile.png"; +import logo from "@/public/assets/Linkbrary.png"; +import profile from "@/public/assets/icons/icon_myprofile.png"; import { getUserInfo } from "../../api/api"; import { useGetPromise } from "@/hooks/useGetPromise"; import Image from "next/image"; +import Link from "next/link"; interface propTypes { $positionval: string; @@ -16,7 +17,7 @@ function HeaderElement({ $positionval }: propTypes) { return ( - logo + logo {user ? ( diff --git a/components/common/Input.tsx b/components/common/Input.tsx index 64f30f5bcf..3ed0928ea1 100644 --- a/components/common/Input.tsx +++ b/components/common/Input.tsx @@ -1,7 +1,7 @@ -import react, { KeyboardEvent } from "react"; +import { KeyboardEvent } from "react"; import styled from "styled-components"; -import searchIcon from "../../assets/icons/icon_search.png"; -import Delete from "@/assets/icons/delete.svg"; +import searchIcon from "@/public/assets/icons/icon_search.png"; +import Delete from "@/public/assets/icons/delete.svg"; import Image from "next/image"; type InputPropsType = { diff --git a/components/common/Portal.tsx b/components/common/Portal.tsx new file mode 100644 index 0000000000..7daef19bd8 --- /dev/null +++ b/components/common/Portal.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { createPortal } from 'react-dom'; + +type PortalProps = { + children: React.ReactNode; +} + +const Portal = ({ + children +}: PortalProps) => { + + const modalRoot = typeof document !== 'undefined' ? document.getElementById('modal-root') : null; + + if(!modalRoot) return; + + return createPortal(children, modalRoot) +} + +export default Portal \ No newline at end of file diff --git a/components/common/Toast.tsx b/components/common/Toast.tsx new file mode 100644 index 0000000000..a7101cc880 --- /dev/null +++ b/components/common/Toast.tsx @@ -0,0 +1,74 @@ +import React, { useEffect } from 'react'; +import styled from 'styled-components'; +import closeIcon from '@/public/assets/icons/close.png'; +import Image from 'next/image'; + +type ToastType = { + errorMessage?:string | null; + isToastView: boolean, + setIsToastView: React.Dispatch>; +} +const Toast = ({errorMessage, isToastView, setIsToastView}:ToastType) => { + + const onClickCloseIconHandler = () => { + setIsToastView(false); + } + + useEffect(() => { + setTimeout(() => { + setIsToastView(false); + }, 3000); + },[isToastView]); + + return ( + + + {errorMessage} + + + closeIcon + + + ) +} + +export default Toast; + +const ToastContainer = styled.div<{ isToastView: boolean } >` + width: 50%; + padding: 10px 20px; + background-color: #dfcbff; + border: 1px solid var(--Primary); + border-radius: 10px;; + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 9999; + text-align: center; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + box-shadow: 0px 10px 29px -14px rgba(0,0,0,1); + visibility: ${({isToastView}) => isToastView ? "visible" : "hidden"}; + + @media (max-width: 767px) { + width: 90%; + font-size: 13px; + } +`; + +const ErrorMessageDiv = styled.div` + display: flex; + align-items: center; + color: #4600b8; + padding-top: 3px; +`; + +const CloseIconDiv = styled.div` + display: flex; + align-items: center; + cursor: pointer; + +`; diff --git a/components/common/ToastPortalContent.tsx b/components/common/ToastPortalContent.tsx new file mode 100644 index 0000000000..9fbcd3cfb8 --- /dev/null +++ b/components/common/ToastPortalContent.tsx @@ -0,0 +1,8 @@ + +const ToastPortalContent = ({children}: {children?:any}) => { + return ( + children + ) +} + +export default ToastPortalContent \ No newline at end of file diff --git a/components/common/modals/AddFolderModal.tsx b/components/common/modals/AddFolderModal.tsx index 0ede85b0cc..13495f4c0c 100644 --- a/components/common/modals/AddFolderModal.tsx +++ b/components/common/modals/AddFolderModal.tsx @@ -1,21 +1,26 @@ +import { useRef } from "react"; import styled from "styled-components"; import { BlueButton } from "../BlueButton"; -import closeIcon from "@/assets/icons/closeModal.png"; +import closeIcon from '@/public/assets/icons/closeModal.png'; import Image from "next/image"; -import { CommonModalProps } from "@/constants/commonTypes"; +import { useModal } from "@/contexts/ModalContext"; +import useOutSideClick from "@/hooks/useOutSideClick"; -export const AddFolderModal = ({ - isModalVisible, - setIsModalVisible, -}: CommonModalProps) => { - const handleCloseBtn = () => { - setIsModalVisible(""); - }; +export const AddFolderModal = () => { + const modal = useModal(); + const modalRef = useRef(null); + + const onCloseModal = () => { + modal.closeModal(); + } + + useOutSideClick({ ref: modalRef, callback: onCloseModal}); + return ( - - - handleCloseBtn()}> + + + 닫기 아이콘 폴더 추가 @@ -34,9 +39,7 @@ export const AddFolderModal = ({ ); }; -const Background = styled.div<{ $isVisible: string }>` - display: ${({ $isVisible }) => - $isVisible === "폴더 추가" ? "block" : "none"}; +const Background = styled.div` z-index: 9999; position: fixed; top: 0; diff --git a/components/common/modals/AddToFolder.tsx b/components/common/modals/AddToFolder.tsx index b9d3999124..303515c085 100644 --- a/components/common/modals/AddToFolder.tsx +++ b/components/common/modals/AddToFolder.tsx @@ -1,30 +1,26 @@ +import { useRef } from "react"; import styled from "styled-components"; -import closeIcon from "@/assets/icons/closeModal.png"; +import closeIcon from "@/public/assets/icons/closeModal.png"; import { BlueButton } from "../BlueButton"; import Image from "next/image"; -import { CommonModalProps } from "@/constants/commonTypes"; +import useOutSideClick from "@/hooks/useOutSideClick"; +import { useModal } from "@/contexts/ModalContext"; -export const AddToFolder = ({ - isModalVisible, - setIsModalVisible, -}: CommonModalProps) => { - const handleCloseBtn = () => { - setIsModalVisible(""); - }; +export const AddToFolder = () => { + + const modal = useModal(); + const modalRef = useRef(null); + + const onCloseModal = () => { + modal.closeModal(); + } + + useOutSideClick({ ref: modalRef, callback: onCloseModal}); return ( - { - e.preventDefault(); - }} - > - - { - handleCloseBtn(); - }} - > + + + closeIcon @@ -59,9 +55,7 @@ export const AddToFolder = ({ ); }; -const Background = styled.div<{ $isVisible: string }>` - display: ${({ $isVisible }) => - $isVisible === "폴더에 추가" ? "block" : "none"}; +const Background = styled.div` z-index: 9999; position: fixed; top: 0; diff --git a/components/common/modals/DeleteModal.tsx b/components/common/modals/DeleteModal.tsx index f9d7b10b8e..1d6593dcda 100644 --- a/components/common/modals/DeleteModal.tsx +++ b/components/common/modals/DeleteModal.tsx @@ -1,48 +1,51 @@ +import { useRef } from "react"; import styled from "styled-components"; -import closeIcon from "@/assets/icons/closeModal.png"; +import closeIcon from "@/public/assets/icons/closeModal.png"; import Image from "next/image"; -import { RedButton } from "../../../components/common/RedButton"; -import { CommonModalProps } from "@/constants/commonTypes"; +import { RedButton } from "../RedButton"; +import { useModal } from "@/contexts/ModalContext"; +import useOutSideClick from "@/hooks/useOutSideClick"; -export const DeleteModal = ({ - isModalVisible, - setIsModalVisible, -}: CommonModalProps) => { - const handleCloseBtn = () => { - setIsModalVisible(""); - }; + +export const DeleteModal = () => { + + const modal = useModal(); + const modalRef = useRef<HTMLDivElement>(null); + + const onCloseModal = () => { + modal.closeModal(); + } + + useOutSideClick({ ref: modalRef, callback: onCloseModal}); return ( - <Background $isVisible={isModalVisible}> - <Modal> - <Close - onClick={(e) => { - e.preventDefault(); - handleCloseBtn(); - }} - > - <Image src={closeIcon} alt="closeIcon" /> - </Close> - <Title> - <h3>폴더 삭제</h3> - <p>폴더명</p> - - - - + + + + closeIcon + + + <h3>폴더 삭제</h3> + <p>폴더명</p> + + + + ); }; -const Background = styled.div<{ $isVisible: string }>` - display: ${({ $isVisible }) => ($isVisible === "삭제" ? "block" : "none")}; +const Background = styled.div` + visibility: visible; z-index: 9999; position: fixed; top: 0; diff --git a/components/common/modals/EditNameModal.tsx b/components/common/modals/EditNameModal.tsx index d4f2404d65..13a928cccc 100644 --- a/components/common/modals/EditNameModal.tsx +++ b/components/common/modals/EditNameModal.tsx @@ -1,21 +1,26 @@ +import { useRef } from "react"; import { styled } from "styled-components"; -import closeIcon from "@/assets/icons/closeModal.png"; +import closeIcon from "@/public/assets/icons/closeModal.png"; import Image from "next/image"; import { BlueButton } from "../BlueButton"; -import { CommonModalProps } from "@/constants/commonTypes"; +import { useModal } from "@/contexts/ModalContext"; +import useOutSideClick from "@/hooks/useOutSideClick"; -export const EditNameModal = ({ - isModalVisible, - setIsModalVisible, -}: CommonModalProps) => { - const handleCloseBtn = () => { - setIsModalVisible(""); - }; +export const EditNameModal = () => { + + const modal = useModal(); + const modalRef = useRef(null); + + const onCloseModal = () => { + modal.closeModal(); + } + + useOutSideClick({ ref: modalRef, callback: onCloseModal}); return ( - - - handleCloseBtn()}> + + + closeIcon 폴더 이름 변경 @@ -34,9 +39,7 @@ export const EditNameModal = ({ ); }; -const Background = styled.div<{ $isVisible: string }>` - display: ${({ $isVisible }) => - $isVisible === "이름 변경" ? "block" : "none"}; +const Background = styled.div` z-index: 9999; position: fixed; top: 0; diff --git a/components/common/modals/ModalPortalContent.tsx b/components/common/modals/ModalPortalContent.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/components/common/modals/PopOver.tsx b/components/common/modals/PopOver.tsx index 23b36449a9..d5520f58a2 100644 --- a/components/common/modals/PopOver.tsx +++ b/components/common/modals/PopOver.tsx @@ -1,47 +1,56 @@ import styled from "styled-components"; import { DeleteModal } from "@/components/common/modals/DeleteModal"; import { AddToFolder } from "@/components/common/modals/AddToFolder"; +import { useEffect, useRef } from "react"; type PopOverPropTypes = { $isPopOverVisible: boolean; setIsPopOverVisible: any; - $options: string[]; - $modalType: any; + popOverInfo: { + option: string; + callback: () => void; + } []; $top: string; $right: string; - $isModalVisible: string; - setIsModalVisible: any; }; export const PopOver = ({ $isPopOverVisible, - $options, - $modalType, + setIsPopOverVisible, + popOverInfo, $top, $right, - $isModalVisible, - setIsModalVisible, }: PopOverPropTypes) => { + + const popoverRef = useRef(null); + + useEffect (() => { + const handleClick = (e: MouseEvent) => { + if(popoverRef.current && !popoverRef.current.contains(e.target as Node)) { + setIsPopOverVisible(false); + } + } + + window.addEventListener('mousedown', handleClick); + + return ()=> window.removeEventListener('mousedown', handleClick); + }) + + + return ( <> - - - - {$options.map((option, index) => ( + + {popOverInfo.map((option) => ( ))} @@ -64,6 +73,7 @@ const MenuOptions = styled.div` display: ${({ $isVisible }) => ($isVisible ? "block" : "none")}; background-color: #fff; z-index: 1; + box-shadow: 3px 8px 15px -4px rgba(0,0,0,0.38); `; const Option = styled.p` diff --git a/components/common/modals/SharedModal.tsx b/components/common/modals/SharedModal.tsx index 9fdcc85c78..0f7a707d2b 100644 --- a/components/common/modals/SharedModal.tsx +++ b/components/common/modals/SharedModal.tsx @@ -1,21 +1,26 @@ +import { useRef } from "react"; import styled from "styled-components"; -import closeIcon from "@/assets/icons/closeModal.png"; +import closeIcon from "@/public/assets/icons/closeModal.png"; import Image from "next/image"; -import { CommonModalProps } from "@/constants/commonTypes"; +import { useModal } from "@/contexts/ModalContext"; +import useOutSideClick from "@/hooks/useOutSideClick"; import { SOCIAL_ICONS } from "@/constants/socialIcon"; -export const SharedModal = ({ - isModalVisible, - setIsModalVisible, -}: CommonModalProps) => { - const handleCloseBtn = () => { - setIsModalVisible(""); - }; +export const SharedModal = () => { + + const modal = useModal(); + const modalRef = useRef(null); + + const onCloseModal = () => { + modal.closeModal(); + } + + useOutSideClick({ ref: modalRef, callback: onCloseModal}); return ( - - - handleCloseBtn()}> + + + closeIcon @@ -42,8 +47,7 @@ type BackgroundPropsType = { $backgroundColor?: string; }; -const Background = styled.div<BackgroundPropsType>` - display: ${({ $isVisible }) => ($isVisible === "공유" ? "block" : "none")}; +const Background = styled.div` z-index: 9999; position: fixed; top: 0; diff --git a/components/folder/FolderInput.tsx b/components/folder/FolderInput.tsx index f8ed3833dc..183468af43 100644 --- a/components/folder/FolderInput.tsx +++ b/components/folder/FolderInput.tsx @@ -1,5 +1,5 @@ import styled from "styled-components"; -import Link from "@/assets/icons/link.svg"; +import Link from "@/public/assets/icons/link.svg"; import { BlueButton } from "../common/BlueButton"; import { forwardRef, SetStateAction, Dispatch } from "react"; @@ -102,6 +102,7 @@ const InputBoxFixed = styled(InputBox)` const Input = styled.input` width: 100%; + height: min-content; border: none; margin-left: 12px; @@ -112,7 +113,7 @@ const Input = styled.input` &::placeholder { color: #9fa6b2; font-family: Pretendard; - font-size: 16px; + font-size: 14px; font-style: normal; font-weight: 400; line-height: 24px; diff --git a/components/folder/FolderTitle.tsx b/components/folder/FolderTitle.tsx index c574878ba0..6ca3a0dba0 100644 --- a/components/folder/FolderTitle.tsx +++ b/components/folder/FolderTitle.tsx @@ -1,26 +1,40 @@ import styled from "styled-components"; -import Share from "../../assets/icons/share.svg"; -import Pen from "../../assets/icons/pen.svg"; -import Trash from "../../assets/icons/trash.svg"; +import Share from "@/public/assets/icons/share.svg"; +import Pen from "@/public/assets/icons/pen.svg"; +import Trash from "@/public/assets/icons/trash.svg"; +import { useModal } from "@/contexts/ModalContext"; +import { SharedModal } from "../common/modals/SharedModal"; +import { EditNameModal } from "../common/modals/EditNameModal"; +import { DeleteModal } from "../common/modals/DeleteModal"; type FolderTitlePropsType = { titleName: string; - setIsModal: any; }; -const FolderTitle = ({ titleName, setIsModal }: FolderTitlePropsType) => { +const FolderTitle = ({ titleName }: FolderTitlePropsType) => { + const modal = useModal(); + const titles = [ { name: "공유", svg: <Share />, + callback: () => { + modal.openModal(<SharedModal />) + } }, { name: "이름 변경", svg: <Pen />, + callback: () => { + modal.openModal(<EditNameModal />) + } }, { name: "삭제", svg: <Trash />, + callback: () => { + modal.openModal(<DeleteModal />) + } }, ]; @@ -32,9 +46,7 @@ const FolderTitle = ({ titleName, setIsModal }: FolderTitlePropsType) => { {titles.map((title) => ( <Option key={title.name} - onClick={() => { - setIsModal(title.name); - }} + onClick={title.callback} > {title.svg} <OptionText>{title.name}</OptionText> diff --git a/components/folder/Menus.tsx b/components/folder/Menus.tsx index e01a7ce9d6..e952c5c4c6 100644 --- a/components/folder/Menus.tsx +++ b/components/folder/Menus.tsx @@ -1,20 +1,22 @@ import { useState, useEffect } from "react"; import { getFolderList } from "../../api/api"; import styled from "styled-components"; -import Union from "@/assets/icons/Union.svg"; +import Union from "@/public/assets/icons/Union.svg"; import { COLORS } from "../../constants/colors"; import { useGetPromise } from "@/hooks/useGetPromise"; +import { useModal } from "@/contexts/ModalContext"; +import { AddFolderModal } from "../common/modals/AddFolderModal"; type MenusPropsType = { changeTitle: any; changeID: any; - setIsVisible: any; }; -const Menus = ({ changeTitle, changeID, setIsVisible }: MenusPropsType) => { +const Menus = ({ changeTitle, changeID }: MenusPropsType) => { const [buttonColors, setButtonColors] = useState([]); const fetchedMenuData: any = useGetPromise(getFolderList); const menulists = fetchedMenuData?.data ?? []; + const modal = useModal(); useEffect(() => { const initialButtonColors = setAllWhiteButtonColor(); @@ -49,6 +51,10 @@ const Menus = ({ changeTitle, changeID, setIsVisible }: MenusPropsType) => { changeButtonColor(name); }; + const addFolderClickHandler = () => { + modal.openModal(<AddFolderModal />) + } + return ( <Container> <ButtonDiv> @@ -63,7 +69,7 @@ const Menus = ({ changeTitle, changeID, setIsVisible }: MenusPropsType) => { </Button> ))} </ButtonDiv> - <AddFolderDiv onClick={() => setIsVisible("폴더 추가")}> + <AddFolderDiv onClick={addFolderClickHandler}> <AddFolder>폴더 추가</AddFolder> <Union /> </AddFolderDiv> diff --git a/components/home/CardFrame.tsx b/components/home/CardFrame.tsx index 1736132d18..a9847336af 100644 --- a/components/home/CardFrame.tsx +++ b/components/home/CardFrame.tsx @@ -1,8 +1,8 @@ import styled from "styled-components"; -import card1 from "@/assets/cards/card1.png"; -import card2 from "@/assets/cards/card2.png"; -import card3 from "@/assets/cards/card3.png"; -import card4 from "@/assets/cards/card4.png"; +import card1 from "@/public/assets/cards/card1.png"; +import card2 from "@/public/assets/cards/card2.png"; +import card3 from "@/public/assets/cards/card3.png"; +import card4 from "@/public/assets/cards/card4.png"; import Image, { StaticImageData } from "next/image"; interface propTypes { diff --git a/components/shared/SharedSection.tsx b/components/shared/SharedSection.tsx index 2128e8129b..57ec4c5c33 100644 --- a/components/shared/SharedSection.tsx +++ b/components/shared/SharedSection.tsx @@ -1,4 +1,4 @@ -import icon_smile from "@/assets/icons/icon_smile.png"; +import icon_smile from "@/public/assets/icons/icon_smile.png"; import styled from "styled-components"; import { OwnerProps } from "@/constants/commonTypes"; diff --git a/constants/errorText.ts b/constants/errorText.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/constants/footerIcons.ts b/constants/footerIcons.ts index 9cf9da3280..a831ed9896 100644 --- a/constants/footerIcons.ts +++ b/constants/footerIcons.ts @@ -1,7 +1,7 @@ -import facebookIcon from "@/assets/icons/icon_facebook.png"; -import twitterIcon from "@/assets/icons/icon_twitter.png"; -import youtubeIcon from "@/assets/icons/icon_youtube.png"; -import instagramIcon from "@/assets/icons/icon_instagram.png"; +import facebookIcon from "@/public/assets/icons/icon_facebook.png"; +import twitterIcon from "@/public/assets/icons/icon_twitter.png"; +import youtubeIcon from "@/public/assets/icons/icon_youtube.png"; +import instagramIcon from "@/public/assets/icons/icon_instagram.png"; import Image, { StaticImageData } from "next/image"; type footerIconsType = { diff --git a/constants/socialIcon.ts b/constants/socialIcon.ts index 2ec482c632..c0962740da 100644 --- a/constants/socialIcon.ts +++ b/constants/socialIcon.ts @@ -1,6 +1,6 @@ -import kakao from "@/assets/icons/icon_kakao.png"; -import facebook from "@/assets/icons/icon_facebook.png"; -import link from "@/assets/icons/link.png"; +import kakao from "@/public/assets/icons/icon_kakao.png"; +import facebook from "@/public/assets/icons/icon_facebook.png"; +import link from "@/public/assets/icons/link.png"; export const SOCIAL_ICONS = [ { diff --git a/contexts/ModalContext.tsx b/contexts/ModalContext.tsx new file mode 100644 index 0000000000..bb697e9e30 --- /dev/null +++ b/contexts/ModalContext.tsx @@ -0,0 +1,44 @@ +import { createContext, useContext, useState } from "react"; + +type ModalProvider = { + isOpen: boolean; + openModal: (modal: React.ReactNode) => void; + closeModal: () => void; +} + +const ModalContext = createContext<ModalProvider | undefined>(undefined); + +export const useModal = () => { + const context = useContext(ModalContext); + if(!context) throw new Error ("modal error"); + return context; +}; + +export const ModalProvider = ({children}: {children: any}) => { + + const [isOpen, setIsOpen] = useState(false); + const [modalComponent, setModalComponent] = useState<React.ReactNode | null >(null) + const [modalStack, setModalStack] = useState<React.ReactNode[]>([]); + + const openModal = (modal: React.ReactNode) => { + setIsOpen(true); + setModalComponent(modal); + setModalStack((prevStack) => [...prevStack, modal]); + } + const closeModal = () => { + setIsOpen(false); + const prevStack = [...modalStack]; + prevStack.pop(); + const prevModal = prevStack[prevStack.length-1]; + setModalStack(prevStack); + setModalComponent(prevModal); + + } + + return ( + <ModalContext.Provider value={{isOpen, openModal, closeModal}}> + {children} + {modalComponent} + </ModalContext.Provider> + ) +} \ No newline at end of file diff --git a/contexts/ToastContext.tsx b/contexts/ToastContext.tsx new file mode 100644 index 0000000000..4431437f84 --- /dev/null +++ b/contexts/ToastContext.tsx @@ -0,0 +1,44 @@ +import { createContext, useContext, useState } from 'react'; +import Toast from '@/components/common/Toast'; +import ToastPortalContent from '@/components/common/ToastPortalContent'; + +type ToastContextType = { + setText: (message: string) => void; + setViewToast: (value: boolean) => void; +} + +const ToastContext = createContext<ToastContextType | undefined >(undefined); + +export const useToast = () =>{ + const context = useContext(ToastContext); + if(!context) throw new Error("toast error"); + return context; +} + +export const ToastProvider = ({children}: {children: any}) => { + + const [toastText, setToastText] = useState(''); + const [isToastView, setIsToastView] = useState(false); + + const setText = (message: string) => { + setToastText(message); + } + + const setViewToast = (value : boolean) => { + setIsToastView(value); + } + + const contextValue: ToastContextType = { + setText, + setViewToast + } + + return ( + <ToastContext.Provider value={contextValue}> + {children} + <ToastPortalContent > + <Toast errorMessage={toastText} isToastView={isToastView} setIsToastView={setIsToastView}/> + </ToastPortalContent> + </ToastContext.Provider> + ) +} \ No newline at end of file diff --git a/hooks/useOutSideClick.ts b/hooks/useOutSideClick.ts new file mode 100644 index 0000000000..57fe5c57a8 --- /dev/null +++ b/hooks/useOutSideClick.ts @@ -0,0 +1,22 @@ +import React, {useEffect} from 'react'; + +type UseOutSideClickType = { + ref: React.RefObject<HTMLDivElement>; + callback: Function; +} + +const useOutSideClick = ({ref, callback} : UseOutSideClickType) => { + const handleClick = (e: MouseEvent) => { + if(ref.current && !ref.current.contains(e.target as Node)) { + callback?.(); + } + }; + + useEffect(() => { + window.addEventListener('mousedown', handleClick); + + return () => window.removeEventListener('mousedown', handleClick); + }) +} + +export default useOutSideClick; \ No newline at end of file diff --git a/next.config.js b/next.config.js index ccde0b0e2d..2fa821e136 100644 --- a/next.config.js +++ b/next.config.js @@ -56,6 +56,14 @@ const nextConfig = { protocol: "https", hostname: "s.pstatic.net", }, + { + protocol: "https", + hostname: "ssl.pstatic.net", + }, + { + protocol: "https", + hostname: "data1.pokemonkorea.co.kr", + }, ], }, }; diff --git a/package-lock.json b/package-lock.json index c5622519c1..93f25758ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,9 @@ "dependencies": { "next": "13.5.6", "react": "^18", - "react-dom": "^18", - "styled-components": "^6.1.8" + "react-dom": "^18.3.1", + "styled-components": "^6.1.8", + "swr": "^2.2.5" }, "devDependencies": { "@svgr/webpack": "^8.1.0", @@ -5735,9 +5736,9 @@ ] }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -5746,15 +5747,15 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.1" } }, "node_modules/react-is": { @@ -5987,9 +5988,9 @@ } }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dependencies": { "loose-envify": "^1.1.0" } @@ -6325,6 +6326,18 @@ "url": "https://opencollective.com/svgo" } }, + "node_modules/swr": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz", + "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==", + "dependencies": { + "client-only": "^0.0.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -6592,6 +6605,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index a83f30bf73..0837b831b9 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,9 @@ "dependencies": { "next": "13.5.6", "react": "^18", - "react-dom": "^18", - "styled-components": "^6.1.8" + "react-dom": "^18.3.1", + "styled-components": "^6.1.8", + "swr": "^2.2.5" }, "devDependencies": { "@svgr/webpack": "^8.1.0", diff --git a/pages/Folder.tsx b/pages/Folder.tsx index 7cd2972f3f..69e325d317 100644 --- a/pages/Folder.tsx +++ b/pages/Folder.tsx @@ -10,7 +10,6 @@ import Menus from "../components/folder/Menus"; import FolderTitle from "../components/folder/FolderTitle"; import { SharedModal } from "../components/common/modals/SharedModal"; import { EditNameModal } from "../components/common/modals/EditNameModal"; -import { DeleteModal } from "../components/common/modals/DeleteModal"; import { AddFolderModal } from "../components/common/modals/AddFolderModal"; import { COLORS } from "../constants/colors"; import { CommonFolderInfoProps } from "@/constants/commonTypes"; @@ -27,16 +26,18 @@ const Folder = () => { const [isModalVisible, setIsModalVisible] = useState(""); const [searchInputValue, setSearchInputValue] = useState(""); + const fetchData = async () => { + try { + const response = await getAllLinkData(listId); + const result = await response.data; + setData(result); + } catch (error) { + console.error(error); + } + }; + fetchData(); + useEffect(() => { - const fetchData = async () => { - try { - const response = await getAllLinkData(listId); - const result = await response.data; - setData(result); - } catch (error) { - console.error(error); - } - }; fetchData(); }, [listId]); @@ -94,22 +95,6 @@ const Folder = () => { return ( <Container> - <SharedModal - isModalVisible={isModalVisible} - setIsModalVisible={setIsModalVisible} - /> - <EditNameModal - isModalVisible={isModalVisible} - setIsModalVisible={setIsModalVisible} - /> - <DeleteModal - isModalVisible={isModalVisible} - setIsModalVisible={setIsModalVisible} - /> - <AddFolderModal - isModalVisible={isModalVisible} - setIsModalVisible={setIsModalVisible} - /> <HeaderElement $positionval="static" /> <FolderInput setIsVisible={setIsModalVisible} @@ -124,14 +109,11 @@ const Folder = () => { <Menus changeTitle={setTitleName} changeID={setListId} - setIsVisible={setIsModalVisible} /> - <FolderTitle titleName={titleName} setIsModal={setIsModalVisible} /> + <FolderTitle titleName={titleName} /> {data[0] ? ( <FolderList items={data} - $isModalVisible={isModalVisible} - setIsModalVisible={setIsModalVisible} /> ) : ( <NoLinkMsg>저장된 링크가 없습니다.</NoLinkMsg> diff --git a/pages/ModalTest.tsx b/pages/ModalTest.tsx new file mode 100644 index 0000000000..f2791903bb --- /dev/null +++ b/pages/ModalTest.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { DeleteModal } from '@/components/common/modals/DeleteModal'; +import { useModal } from '@/contexts/ModalContext'; + + +const ModalTest = () => { + + const modal = useModal(); + + const buttonHandler = () => { + modal.openModal(<DeleteModal />); + } + + return ( + <div>ModalTest + <button onClick={buttonHandler}>모달 가져오기</button> + </div> + ) +} + +export default ModalTest; \ No newline at end of file diff --git a/pages/Shared.tsx b/pages/Shared.tsx index d79c20549e..aed5b64f73 100644 --- a/pages/Shared.tsx +++ b/pages/Shared.tsx @@ -5,13 +5,10 @@ import FooterElement from "../components/common/FooterElement"; import SharedSection from "../components/shared/SharedSection"; import Input from "../components/common/Input"; import FolderList from "../components/common/FolderList"; -import { AddFolderModal } from "../components/common/modals/AddFolderModal"; -import { DeleteModal } from "@/components/common/modals/DeleteModal"; import { OwnerProps } from "@/constants/commonTypes"; export default function Shared() { const [searchInputValue, setSearchInputValue] = useState(""); - const [isModalVisible, setIsModalVisible] = useState(""); const [data, setData] = useState([]); const [folderName, setFolderName] = useState(""); const [owner, setOwner] = useState<OwnerProps>({ @@ -37,14 +34,6 @@ export default function Shared() { return ( <> - <DeleteModal - isModalVisible={isModalVisible} - setIsModalVisible={setIsModalVisible} - /> - <AddFolderModal - isModalVisible={isModalVisible} - setIsModalVisible={setIsModalVisible} - /> <HeaderElement $positionval="" /> <SharedSection folderName={folderName} owner={owner} /> <Input @@ -55,8 +44,6 @@ export default function Shared() { {data[0] ? ( <FolderList items={data} - $isModalVisible={isModalVisible} - setIsModalVisible={setIsModalVisible} /> ) : null} <FooterElement /> diff --git a/pages/Signin.tsx b/pages/Signin.tsx index 87d3da8fe1..9fc70725af 100644 --- a/pages/Signin.tsx +++ b/pages/Signin.tsx @@ -1,41 +1,113 @@ -import React, { useState } from "react"; +"use client"; + +import React, { useState, useRef, useEffect } from "react"; import styled from "styled-components"; import Image from "next/image"; import Link from "next/link"; -import login_logo from "@/assets/login_logo.png"; -import icon_google from "@/assets/icons/icon_google.png"; -import icon_kakao from "@/assets/icons/icon_kakao.png"; +import login_logo from "@/public/assets/login_logo.png"; +import icon_google from "@/public/assets/icons/icon_google.png"; +import icon_kakao from "@/public/assets/icons/icon_kakao.png"; import { BlueButton } from "@/components/common/BlueButton"; import EmailPwdInput from "@/components/SignInUp/EmailPwdInput"; +import { postSignIn } from "@/api/api"; +import INPUT_ERROR_INFO from "@/type/inputErrorInfo"; +import { emailInputValidationcheck, loginPasswordInputValidationcheck } from "@/utils/validation"; +import { useToast } from "@/contexts/ToastContext"; +import { localStorage } from "@/utils/localStorage"; -export default function Signup() { +export default function Signin() { const [emailValue, setEmailValue] = useState(""); - const [passwordValue, setPasswordValue] = useState<string | undefined>(""); - const [isEmailValid, setIsEmailValid] = useState(false); - const [isPasswordValid, setIsPasswordValid] = useState(false); - const [isPasswordConfirmValid, setIsPasswordConfirmValid] = useState(false); - const [signInStatus, setSignInStatus] = useState("normal"); - - const trySignin = () => { - if (isEmailValid && isPasswordValid && isPasswordConfirmValid) { - console.log("회원가입 시도"); - location.assign("/Folder"); - } else { - setSignInStatus("fail"); + const [emailErrorInfo, setEmailErrorInfo] = useState<INPUT_ERROR_INFO>("valid"); + const emailInputRef = useRef<HTMLInputElement>(null); + + const [passwordValue, setPasswordValue] = useState(""); + const [passwordErrorInfo, setPasswordErrorInfo] = useState<INPUT_ERROR_INFO>("valid"); + const passwordInputRef = useRef<HTMLInputElement>(null); + + const toast = useToast(); + + + useEffect(() => { + localStorage.remove("accssToken"); + localStorage.get("accessToken") && location.assign("/Folder"); + }, []); + + + const trySignin = async () => { + let result; + if (emailErrorInfo==="valid" && passwordErrorInfo === "valid") { + const postJsonValue = {email: emailValue, password: passwordValue}; + try{ + result = await postSignIn(postJsonValue); + } catch (error){ + console.error("로그인 오류"); + } + + if(result.data) { + localStorage.save("accessToken", result.data.accessToken); + location.assign("/Folder"); + } else if (result.error) { + toast.setViewToast(true); + toast.setText("로그인에 성공하지 못햇습니다."); + console.log(result); + } + }; + } + + const emailFocusOutHandler = () => { + const emailInputStatus = emailInputValidationcheck(emailValue); + setEmailErrorInfo(emailInputStatus); + } + + const emailChangeHandler = () => { + if(emailInputRef.current) { + setEmailValue(emailInputRef.current.value) + } + } + + const passwordFocusOutHandler = () => { + const passwordStatus = loginPasswordInputValidationcheck(passwordValue) + setPasswordErrorInfo(passwordStatus); + } + + const passwordChangeHandler = () => { + if(passwordInputRef.current) { + setPasswordValue(passwordInputRef.current.value) + } + } + + const onEnterKeyHandler = (e:React.KeyboardEvent<HTMLInputElement>) => { + if(e.key === 'Enter') { + emailInputRef?.current?.blur(); + passwordInputRef?.current?.blur(); + trySignin(); + } + } + + const emailventFunc = { + onFocusOut: emailFocusOutHandler, + onChange: emailChangeHandler, + onKeydown: onEnterKeyHandler, } - }; + + const passwordEventFunc = { + onFocusOut: passwordFocusOutHandler, + onChange: passwordChangeHandler, + onKeydown: onEnterKeyHandler, + } + return ( <BackgroundDiv> <ContainerDiv> <TitleDiv> <Link href="./"> - <Image src={login_logo} alt="login log" /> + <Image src={login_logo} alt="logo" /> </Link> <p> - 이미 회원이신가요?{" "} + 회원이 아니신가요?{" "} <Link href="./Signup" id="header-link"> - <span>로그인 하기</span> + <span>회원가입 하기</span> </Link> </p> </TitleDiv> @@ -43,40 +115,16 @@ export default function Signup() { <EmailPwdInput title="이메일" type="email" - valueType="email" - setEmailValue={setEmailValue} - emailValue={emailValue} - setPasswordValue={setPasswordValue} - passwordValue={passwordValue} - onEnterButtonClick={trySignin} - setIsEmailValid={setIsEmailValid} - signInStatus={signInStatus} - /> + eventFunc = {emailventFunc} + inputErrorInfo = {emailErrorInfo} + inputRef = {emailInputRef} + /> <EmailPwdInput title="비밀번호" type="password" - valueType="password" - isEyeIcon={true} - setEmailValue={setEmailValue} - emailValue={emailValue} - setPasswordValue={setPasswordValue} - passwordValue={passwordValue} - onEnterButtonClick={trySignin} - setIsPasswordValid={setIsPasswordValid} - signInStatus={signInStatus} - /> - <EmailPwdInput - title="비밀번호 확인" - type="password" - valueType="password2" - isEyeIcon={true} - setEmailValue={setEmailValue} - emailValue={emailValue} - setPasswordValue={setPasswordValue} - passwordValue={passwordValue} - onEnterButtonClick={trySignin} - setIsPasswordConfirmValid={setIsPasswordConfirmValid} - signInStatus={signInStatus} + eventFunc={passwordEventFunc} + inputErrorInfo={passwordErrorInfo} + inputRef={passwordInputRef} /> </InputBoxDiv> <BlueButton diff --git a/pages/Signup.tsx b/pages/Signup.tsx index 22be9443b9..ade6162c98 100644 --- a/pages/Signup.tsx +++ b/pages/Signup.tsx @@ -1,48 +1,112 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import styled from "styled-components"; import Image from "next/image"; import Link from "next/link"; -import login_logo from "@/assets/login_logo.png"; -import icon_google from "@/assets/icons/icon_google.png"; -import icon_kakao from "@/assets/icons/icon_kakao.png"; +import login_logo from "@/public/assets/login_logo.png"; +import icon_google from "@/public/assets/icons/icon_google.png"; +import icon_kakao from "@/public/assets/icons/icon_kakao.png"; import { BlueButton } from "@/components/common/BlueButton"; import EmailPwdInput from "@/components/SignInUp/EmailPwdInput"; -import { postSignIn } from "@/api/api"; -import { localStorage } from "@/utils/localStorage"; +import { postSignUp } from "@/api/api"; +import { emailInputValidationcheck, signUpPasswordInputValidationcheck } from "@/utils/validation"; +import INPUT_ERROR_INFO from "@/type/inputErrorInfo"; +import { useToast } from "@/contexts/ToastContext"; -export default function LogIn() { +export default function Signup() { const [emailValue, setEmailValue] = useState(""); - const [passwordValue, setPasswordValue] = useState<string | undefined>(""); - const [loginStatus, setLoginStatus] = useState("normal"); - - useEffect(() => { - localStorage.remove("accssToken"); - // localStorage.get("accessToken") && location.assign("/Folder"); - }, []); - - const tryLogin = async () => { - const postJsonValue = { email: emailValue, password: passwordValue }; - let result; - try { - result = await postSignIn(postJsonValue); - } catch (error) { - console.error(error); - } + const [emailErrorInfo, setEmailErrorInfo] = useState<INPUT_ERROR_INFO>("valid"); + const emailInputRef = useRef<HTMLInputElement>(null); - if (result.data) successLogin(result); - else failLogin(); - }; + const [passwordValue, setPasswordValue] = useState(""); + const [passwordErrorInfo, setPasswordErrorInfo] = useState<INPUT_ERROR_INFO>("valid"); + const passwordInputRef = useRef<HTMLInputElement>(null); - const successLogin = (result: any) => { - setLoginStatus("success"); - localStorage.save("accessToken", result.data.accessToken); - location.assign("/Folder"); - }; + const [passwordCheckValue, setPasswordCheckValue] = useState(""); + const [passwordCheckErrorInfo, setPasswordCheckErrorInfo] = useState<INPUT_ERROR_INFO>("valid"); + const passwordCheckInputRef = useRef<HTMLInputElement>(null); + + const toast = useToast(); + + const trySignup = async () => { + if(emailErrorInfo === "valid" && passwordErrorInfo === "valid" && passwordCheckErrorInfo === "valid") { + const postJsonValue = { email: emailValue, password: passwordValue }; + let result; + try { + result = await postSignUp(postJsonValue); + + } catch (error) { + console.error(error); + } - const failLogin = () => { - setLoginStatus("fail"); + if(result.data) { + localStorage.save("accessToken", result.data.accessToken); + location.assign("/Folder"); + } else if(result.error) { + toast.setText("회원가입에 실패하였습니다."); + toast.setViewToast(true); + } + } }; + const onEnterKeyHandler = (e: React.KeyboardEvent<HTMLInputElement>) => { + if(e.key === "Enter") { + emailInputRef.current?.blur(); + passwordInputRef.current?.blur(); + passwordCheckInputRef.current?.blur(); + trySignup(); + } + } + + const emailChangeHandler = () => { + if(emailInputRef.current) { + setEmailValue(emailInputRef.current.value); + } + } + const emailFocusoutHandler = () => { + const emailErrorInfo = emailInputValidationcheck(emailValue); + setEmailErrorInfo(emailErrorInfo); + } + + const passwordChangeHandler = () => { + if(passwordInputRef.current) { + setPasswordValue(passwordInputRef.current.value); + } + } + + const passwordFocusoutHandler = () => { + const passwordErrorInfo = signUpPasswordInputValidationcheck(passwordValue); + setPasswordErrorInfo(passwordErrorInfo); + } + + const passwordCheckChangeHandler = () => { + if(passwordCheckInputRef.current) { + setPasswordCheckValue(passwordCheckInputRef.current.value); + } + } + + const passwordCheckFocusoutHandler = () => { + const passwordCheckErrorInfo = signUpPasswordInputValidationcheck(passwordValue, passwordCheckValue); + setPasswordCheckErrorInfo(passwordCheckErrorInfo); + } + + const emailEventFunc = { + onChange: emailChangeHandler, + onFocusOut: emailFocusoutHandler, + onKeydown: onEnterKeyHandler, + } + + const passwordEventFunc = { + onChange: passwordChangeHandler, + onFocusOut: passwordFocusoutHandler, + onKeydown: onEnterKeyHandler, + } + + const passwordCheckEventFunc = { + onChange: passwordCheckChangeHandler, + onFocusOut: passwordCheckFocusoutHandler, + onKeydown: onEnterKeyHandler, + } + return ( <BackgroundDiv> <ContainerDiv> @@ -51,9 +115,9 @@ export default function LogIn() { <Image src={login_logo} alt="login log" /> </Link> <p> - 회원이 아니신가요?{" "} + 이미 회원이신가요?{" "} <Link href="./Signin" id="header-link"> - <span>회원 가입하기</span> + <span>로그인 하기</span> </Link> </p> </TitleDiv> @@ -61,35 +125,34 @@ export default function LogIn() { <EmailPwdInput title="이메일" type="email" - valueType="email" - setEmailValue={setEmailValue} - emailValue={emailValue} - setPasswordValue={setPasswordValue} - passwordValue={passwordValue} - onEnterButtonClick={tryLogin} - loginStatus={loginStatus} + eventFunc={emailEventFunc} + inputErrorInfo={emailErrorInfo} + inputRef={emailInputRef} + /> <EmailPwdInput title="비밀번호" type="password" - valueType="password" - isEyeIcon={true} - setEmailValue={setEmailValue} - emailValue={emailValue} - setPasswordValue={setPasswordValue} - passwordValue={passwordValue} - onEnterButtonClick={tryLogin} - loginStatus={loginStatus} + eventFunc={passwordEventFunc} + inputErrorInfo={passwordErrorInfo} + inputRef={passwordInputRef} + /> + <EmailPwdInput + title="비밀번호 확인" + type="password" + eventFunc={passwordCheckEventFunc} + inputErrorInfo={passwordCheckErrorInfo} + inputRef={passwordCheckInputRef} /> </InputBoxDiv> <BlueButton - text="로그인" + text="회원가입" width="100%" margin="0px 0px 32px" padding="16px 20px" radius="8px" fontSize="18px" - onBtnHandle={tryLogin} + onBtnHandle={trySignup} /> <SocialLoginDiv> <span>소셜 로그인</span> diff --git a/pages/ToastTest.tsx b/pages/ToastTest.tsx new file mode 100644 index 0000000000..68048eed80 --- /dev/null +++ b/pages/ToastTest.tsx @@ -0,0 +1,21 @@ +import React, { useEffect, useState } from 'react'; +import { useToast } from '@/contexts/ToastContext'; + +const ToastTest = () => { + + const { setText, setViewToast } = useToast(); + + useEffect(() => { + setText('ddddd'); + }) + + + return ( + <div> + <button onClick={() => setViewToast(true)}>토스트 열기</button> + <button onClick={() => setViewToast(false)}>토스트 끄기</button> + </div> + ) +} + +export default ToastTest \ No newline at end of file diff --git a/pages/_app.tsx b/pages/_app.tsx index f0d4879e4d..1bd94c0021 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,11 +1,20 @@ import "@/styles/globals.css"; import type { AppProps } from "next/app"; import "@/styles/globals.css"; +import { ToastProvider } from "@/contexts/ToastContext"; +import Portal from "@/components/common/Portal"; +import ToastPortalContent from "@/components/common/ToastPortalContent"; +import { ModalProvider } from "@/contexts/ModalContext"; export default function App({ Component, pageProps }: AppProps) { return ( - <> - <Component {...pageProps} /> - </> + <ToastProvider> + <ModalProvider> + <Component {...pageProps}/> + <Portal> + <ToastPortalContent /> + </Portal> + </ModalProvider> + </ToastProvider> ); } diff --git a/pages/_document.tsx b/pages/_document.tsx index ae39f9b655..f5368e0779 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -5,7 +5,7 @@ export default function MyDocument(props: any) { return ( <Html> <Head>{/* Place your head elements here */}</Head> - <body> + <body id="modal-root"> <Main /> <NextScript /> </body> diff --git a/pages/index.tsx b/pages/index.tsx index e33c42573c..7805e1122d 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -3,7 +3,7 @@ import HeaderElement from "../components/common/HeaderElement"; import FooterElement from "../components/common/FooterElement"; import { Headline } from "../components/home/Headline"; import { CardFrame } from "../components/home/CardFrame"; -import cards_img from "/assets/cards/cards_img.png"; +import cards_img from "/public/assets/cards/cards_img.png"; import Image from "next/image"; export default function Home() { diff --git a/public/assets/icons/close.png b/public/assets/icons/close.png new file mode 100644 index 0000000000..de8e32570d Binary files /dev/null and b/public/assets/icons/close.png differ diff --git a/type/inputErrorInfo.ts b/type/inputErrorInfo.ts new file mode 100644 index 0000000000..ee1f466876 --- /dev/null +++ b/type/inputErrorInfo.ts @@ -0,0 +1,9 @@ +type INPUT_ERROR_INFO = + // error + { + errorName: string; + type: string; + message: string; + } + | "valid"; +export default INPUT_ERROR_INFO; \ No newline at end of file diff --git a/utils/calculator.ts b/utils/calculator.ts index 8af6d0e8e7..b843dc6611 100644 --- a/utils/calculator.ts +++ b/utils/calculator.ts @@ -31,7 +31,7 @@ function timeToString(time: number) { return `${diffYear} years ago`; } -export function CalcTime(time: string) { +export function calcTime(time: string) { const nowTime = Date.now(); const writingTime = Date.parse(time); const diffTime = nowTime - writingTime; diff --git a/utils/commonSigninupFunc.ts b/utils/commonSigninupFunc.ts deleted file mode 100644 index fefc2cee72..0000000000 --- a/utils/commonSigninupFunc.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const changeInputBorderColor = (status: string | undefined) => { - switch (status) { - case "default": - return "var(--Grey_300)"; - case "writing": - return "var(--Primary)"; - default: - return "var(--Red)"; - } -}; diff --git a/utils/validation.ts b/utils/validation.ts index 5c6b073f6c..ebecdfd16f 100644 --- a/utils/validation.ts +++ b/utils/validation.ts @@ -3,12 +3,13 @@ import { postCheckDuplicationEmail } from "@/api/api"; // 이메일 유효성 검사 function emailCheck(email_address: string) { - let email_regex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/i; + const email_regex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/i; return Boolean(email_regex.test(email_address)); } -type ErrorType = - | { +type ErrorValid = + // error + { errorName: string; type: string; message: string; @@ -16,16 +17,16 @@ type ErrorType = | "valid"; // 이메일 에러 검사 -export const emailInputValidationcheck = (email: string): ErrorType => { - let status: ErrorType; +export const emailInputValidationcheck = (email: string): ErrorValid => { + let emailValid: ErrorValid; if (!email) { - status = INPUT_STATUS.noEmail; + emailValid = INPUT_STATUS.noEmail; } else if (!emailCheck(email)) { - status = INPUT_STATUS.wrongEmail; + emailValid = INPUT_STATUS.wrongEmail; } else { - status = "valid"; + emailValid = "valid"; } - return status; + return emailValid; }; // 이메일 중복 검사 @@ -40,8 +41,8 @@ export const emailDuplicationCheck = async (email: string) => { // 로그인할 때 비밀번호 에러 검사 export const loginPasswordInputValidationcheck = ( password: string | undefined -): ErrorType => { - let status: ErrorType; +): ErrorValid => { + let status: ErrorValid; if (!password) { status = INPUT_STATUS.noPassword; } else { @@ -51,11 +52,11 @@ export const loginPasswordInputValidationcheck = ( }; // 회원가입할 때 비밀번호 에러 검사 -export const signInPasswordInputValidationcheck = ( +export const signUpPasswordInputValidationcheck = ( password: string | undefined, password2?: string | undefined -): ErrorType => { - let status: ErrorType; +): ErrorValid => { + let status: ErrorValid; if (!password) { status = INPUT_STATUS.noPassword; } else if (!passwordCheck(password)) { @@ -75,9 +76,4 @@ function passwordCheck(password: string): boolean { return password_regex.test(password); } -// 비밀번호 중복 확인 -export function isMatch(password1: string, password2: string) { - return password1 === password2; -} - export { emailCheck, passwordCheck };