Skip to content

Commit

Permalink
Merge pull request #173 from naya-h2/design/post-v2
Browse files Browse the repository at this point in the history
✨ feat: post 자동저장 기능 추가
  • Loading branch information
naya-h2 authored Mar 14, 2024
2 parents 187c00b + 6eabdc4 commit db5a5ba
Show file tree
Hide file tree
Showing 19 changed files with 318 additions and 82 deletions.
8 changes: 6 additions & 2 deletions app/(route)/post/_components/DetailInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import FunnelTitle from "./FunnelTitle";
import PostFrame from "./PostFrame";
import DetailInput from "./_inputs/DetailInput";

const DetailInfo = () => {
interface Props {
onPrevStep?: () => void;
}

const DetailInfo = ({ onPrevStep }: Props) => {
const { isCheck, postLoading } = useStore((state) => ({ isCheck: state.isWarningCheck, postLoading: state.postLoading }));
const { watch } = useFormContext<PostType>();
const { description, eventImages } = watch();
Expand All @@ -22,7 +26,7 @@ const DetailInfo = () => {
</div>
<DetailInput />
</PostFrame>
<BottomButton isSubmit isDisabled={!isCheck || description.length > 300 || eventImages.length > 5 || postLoading}>
<BottomButton hasBack onBackClick={onPrevStep} isSubmit isDisabled={!isCheck || description.length > 300 || eventImages.length > 5 || postLoading}>
{postLoading ? <LoadingDot /> : "작성 완료"}
</BottomButton>
</div>
Expand Down
6 changes: 3 additions & 3 deletions app/(route)/post/_components/FunnelTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ interface Props {

const FunnelTitle = ({ step, isRequired = false }: Props) => {
return (
<div className="flex flex-col gap-8">
<div className="text-20 font-700">{POST_FUNNEL_TITLE[step]}</div>
{isRequired ? <div className="text-blue text-14 font-500">*필수 입력 사항입니다.</div> : <div className="text-14 font-500 text-gray-400">*선택 입력 사항입니다.</div>}
<div className="flex flex-col gap-8 pc:gap-12">
<div className="pc:text-24 text-20 font-700 text-gray-900">{POST_FUNNEL_TITLE[step]}</div>
{isRequired ? <div className="text-14 font-500 text-blue">*필수 입력 사항입니다.</div> : <div className="text-14 font-500 text-gray-400">*선택 입력 사항입니다.</div>}
</div>
);
};
Expand Down
5 changes: 3 additions & 2 deletions app/(route)/post/_components/MainInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import MainInput from "./_inputs/MainInput";

interface Props {
onNextStep?: () => void;
onPrevStep?: () => void;
}

const MainInfo = ({ onNextStep }: Props) => {
const MainInfo = ({ onNextStep, onPrevStep }: Props) => {
const {
watch,
formState: { isValid },
Expand All @@ -31,7 +32,7 @@ const MainInfo = ({ onNextStep }: Props) => {
</div>
<MainInput />
</PostFrame>
<BottomButton onClick={onNextStep} isDisabled={isDisabled}>
<BottomButton onClick={onNextStep} isDisabled={isDisabled} hasBack onBackClick={onPrevStep}>
다음으로
</BottomButton>
</div>
Expand Down
22 changes: 22 additions & 0 deletions app/(route)/post/_components/PostContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useEffect, useState } from "react";
import { useFunnel } from "@/hooks/useFunnel";
import useGetWindowWidth from "@/hooks/useGetWindowWidth";
import { PostStepNameType } from "@/types/index";
import PostFunnel from "./PostFunnel";
import PostPc from "./PostPc";

const POST_STEPS: PostStepNameType[] = ["행사 대상", "행사 정보", "특전 정보", "상세 설명"];

const PostContent = () => {
const { Funnel, Step, setStep, currentStep } = useFunnel<PostStepNameType>(POST_STEPS);
const [isPc, setIsPc] = useState(false);
const { isPc: isPcSize } = useGetWindowWidth();

useEffect(() => {
setIsPc(isPcSize);
}, [isPcSize]);

return <>{isPc ? <PostPc /> : <PostFunnel Funnel={Funnel} Step={Step} setStep={setStep} currentStep={currentStep} />}</>;
};

export default PostContent;
49 changes: 49 additions & 0 deletions app/(route)/post/_components/PostFunnel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Dispatch, SetStateAction } from "react";
import { useFormContext } from "react-hook-form";
import { PostStepNameType } from "@/types/index";
import DetailInfo from "./DetailInfo";
import MainInfo from "./MainInfo";
import StarInfo from "./StarInfo";
import SubInfo from "./SubInfo";

const POST_STEPS: PostStepNameType[] = ["행사 대상", "행사 정보", "특전 정보", "상세 설명"];

interface Props {
Funnel: any;
Step: any;
setStep: Dispatch<SetStateAction<PostStepNameType>>;
currentStep: PostStepNameType;
}

const PostFunnel = ({ Funnel, Step, setStep, currentStep }: Props) => {
const { getValues } = useFormContext();

const handlePrevClick = () => {
currentStep === POST_STEPS[0] ? window.history.back() : setStep(POST_STEPS[POST_STEPS.indexOf(currentStep) - 1]);
};

const handleNextClick = () => {
const userInput = getValues();
localStorage.setItem("post", JSON.stringify(userInput));
return setStep(POST_STEPS[POST_STEPS.indexOf(currentStep) + 1]);
};

return (
<Funnel>
<Step name={POST_STEPS[0]}>
<StarInfo onNextStep={handleNextClick} />
</Step>
<Step name={POST_STEPS[1]}>
<MainInfo onNextStep={handleNextClick} onPrevStep={handlePrevClick} />
</Step>
<Step name={POST_STEPS[2]}>
<SubInfo onNextStep={handleNextClick} onPrevStep={handlePrevClick} />
</Step>
<Step name={POST_STEPS[3]}>
<DetailInfo onPrevStep={handlePrevClick} />
</Step>
</Funnel>
);
};

export default PostFunnel;
55 changes: 55 additions & 0 deletions app/(route)/post/_components/PostPc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import classNames from "classnames";
import { ReactNode } from "react";
import { useFormContext } from "react-hook-form";
import Button from "@/components/button";
import { useStore } from "@/store/index";
import FunnelTitle from "./FunnelTitle";
import DetailInput from "./_inputs/DetailInput";
import MainInput from "./_inputs/MainInput";
import StarInput from "./_inputs/StarInput";
import SubInput from "./_inputs/SubInput";

const PostPc = () => {
const { watch } = useFormContext();
const { isCheck } = useStore((state) => ({ isCheck: state.isWarningCheck }));
const { address, artists, groupId, eventType, placeName, startDate, endDate } = watch();
const isDisabled = !(artists.length > 0 && address && artists && groupId && eventType && placeName && startDate && endDate) || !isCheck;

return (
<div className="flex max-w-[68.8rem] flex-col gap-32 pb-120">
<p className="text-14 font-600 text-gray-500">행사 등록하기</p>
<div className="flex flex-col gap-56">
<TitleLayout>
<FunnelTitle step="행사 대상" isRequired />
<StarInput />
</TitleLayout>
<TitleLayout>
<FunnelTitle step="행사 정보" isRequired />
<MainInput />
</TitleLayout>
<TitleLayout>
<FunnelTitle step="특전 정보" />
<SubInput />
</TitleLayout>
<TitleLayout isLast>
<FunnelTitle step="상세 설명" />
<DetailInput />
</TitleLayout>
<Button size="xl" isSubmit isDisabled={isDisabled}>
작성완료
</Button>
</div>
</div>
);
};

export default PostPc;

interface Props {
children: ReactNode;
isLast?: boolean;
}

const TitleLayout = ({ children, isLast = false }: Props) => {
return <section className={classNames("flex flex-col gap-24 border-b border-gray-50 pb-32", { "!border-none !pb-0": isLast })}>{children}</section>;
};
7 changes: 5 additions & 2 deletions app/(route)/post/_components/SubInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import SubInput from "./_inputs/SubInput";

interface Props {
onNextStep?: () => void;
onPrevStep?: () => void;
}

const SubInfo = ({ onNextStep }: Props) => {
const SubInfo = ({ onNextStep, onPrevStep }: Props) => {
return (
<div className="flex h-full flex-col justify-between">
<PostFrame>
Expand All @@ -20,7 +21,9 @@ const SubInfo = ({ onNextStep }: Props) => {
</div>
<SubInput />
</PostFrame>
<BottomButton onClick={onNextStep}>다음으로</BottomButton>
<BottomButton onClick={onNextStep} hasBack onBackClick={onPrevStep}>
다음으로
</BottomButton>
</div>
);
};
Expand Down
34 changes: 19 additions & 15 deletions app/(route)/post/_components/_inputs/StarInput.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import InitButton from "@/(route)/event/[eventId]/edit/_components/InitButton";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useFormContext } from "react-hook-form";
import EventTypeList from "@/components/bottom-sheet/content/EventTypeList";
import InputText from "@/components/input/InputText";
import { useBottomSheet } from "@/hooks/useBottomSheet";
import useGetWindowWidth from "@/hooks/useGetWindowWidth";
Expand Down Expand Up @@ -36,7 +37,7 @@ const StarInput = () => {
};

useEffect(() => {
if (isPc && bottomSheet && bottomSheet !== "event") {
if (isPc && bottomSheet) {
openModal(bottomSheet);
closeBottomSheet();
}
Expand Down Expand Up @@ -75,19 +76,22 @@ const StarInput = () => {
</div>
{isNotMember && <div className="pt-4 text-12 font-500 text-red">그룹 선택 시, 멤버 선택이 필수입니다.</div>}
</div>
<InputText
name="eventType"
readOnly
placeholder="행사 유형을 선택하세요."
tabIndex={0}
onClick={() => openBottomSheet("event")}
onKeyDown={(event) => handleEnterDown(event, () => openBottomSheet("event"))}
isEdit={validateEdit(defaultValues?.eventType !== eventType)}
onInit={() => setValue("eventType", defaultValues?.eventType || "카페")}
noButton
>
행사 유형
</InputText>
<div className="flex flex-col gap-[0.9rem]">
<InputText
name="eventType"
readOnly
placeholder="행사 유형을 선택하세요."
tabIndex={0}
onClick={() => (isPc ? openModal("event") : openBottomSheet("event"))}
onKeyDown={(event) => handleEnterDown(event, () => openBottomSheet("event"))}
isEdit={validateEdit(defaultValues?.eventType !== eventType)}
onInit={() => setValue("eventType", defaultValues?.eventType || "카페")}
noButton
>
행사 유형
</InputText>
{modal === "event" && <EventTypeList handleClickFunc={closeModal} />}
</div>
<InputText name="groupId" hidden />
<InputText name="artists" hidden />
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/(route)/post/_components/_inputs/SubInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import ChipButton from "@/components/chip/ChipButton";
import InputText from "@/components/input/InputText";
import { checkArrUpdate } from "@/utils/checkArrUpdate";
import { validateEdit } from "@/utils/editValidate";
import { GIFT_LIST, TAG } from "@/constants/post";
import { PostType } from "../../page";

const SNS_TYPE_LIST = ["트위터", "인스타그램", "유튜브", "기타"];
const GIFT_LIST = ["컵/컵홀더", "포스터", "포토카드", "포토굿즈", "엽서", "스티커", "키링", "기타"];

const SubInput = () => {
const {
Expand Down Expand Up @@ -65,7 +65,7 @@ const SubInput = () => {
/>
)}
</label>
<div className="flex gap-16">
<div className="flex gap-16 text-16">
{SNS_TYPE_LIST.map((type) => (
<label key={type} className={classNames("flex cursor-pointer items-center gap-4", { "!text-gray-400 hover:cursor-not-allowed": hasNotOrganizer })}>
<input
Expand Down
68 changes: 34 additions & 34 deletions app/(route)/post/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
"use client";

import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import GenericFormProvider from "@/components/GenericFormProvider";
import MetaTag from "@/components/MetaTag";
import PinkLayout from "@/components/layout/PinkLayout";
import { useFunnel } from "@/hooks/useFunnel";
import { PostStepNameType } from "@/types/index";
import DottedLayout from "@/components/layout/DottedLayout";
import AlertModal from "@/components/modal/AlertModal";
import { useModal } from "@/hooks/useModal";
import { META_TAG } from "@/constants/metaTag";
import LoadingDot from "../signin/_components/LoadingDot";
import DetailInfo from "./_components/DetailInfo";
import MainInfo from "./_components/MainInfo";
import StarInfo from "./_components/StarInfo";
import SubInfo from "./_components/SubInfo";
import PostContent from "./_components/PostContent";

const DEFAULT_INPUT_VALUES = {
placeName: "",
Expand All @@ -33,8 +31,6 @@ const DEFAULT_INPUT_VALUES = {
tags: [],
};

const POST_STEPS: PostStepNameType[] = ["행사 대상", "행사 정보", "특전 정보", "상세 설명"];

export type PostType = Omit<typeof DEFAULT_INPUT_VALUES, "artists" | "artistNames" | "eventImages" | "tags"> & {
artists: string[];
artistNames: string[];
Expand All @@ -43,42 +39,39 @@ export type PostType = Omit<typeof DEFAULT_INPUT_VALUES, "artists" | "artistName
};

const Post = () => {
const { Funnel, Step, setStep, currentStep } = useFunnel<PostStepNameType>(POST_STEPS);
const [defaultValue, setDefaultValue] = useState(DEFAULT_INPUT_VALUES);
const [isInit, setIsInit] = useState(false);
const { modal, openModal, closeModal } = useModal();
const _ = require("lodash");

useEffect(() => {
if (sessionStorage.getItem("post")) {
setDefaultValue(JSON.parse(sessionStorage.getItem("post") as string));
}
const importAutoSave = () => {
toast("저장 내용을 불러옵니다.", { className: "text-16 font-500" });
setDefaultValue(JSON.parse(localStorage.getItem("post") as string));
setIsInit(true);
}, []);
closeModal();
};

const handlePrevClick = () => {
currentStep === POST_STEPS[0] ? window.history.back() : setStep(POST_STEPS[POST_STEPS.indexOf(currentStep) - 1]);
const clearAutoSave = () => {
localStorage.clear();
setIsInit(true);
closeModal();
};

useEffect(() => {
if (localStorage.getItem("post") && !_.isEqual(JSON.parse(localStorage.getItem("post") as string), DEFAULT_INPUT_VALUES)) {
openModal("autoSave");
} else setIsInit(true);
}, []);

return (
<>
<MetaTag title={META_TAG.post["title"]} description={META_TAG.post["description"]} />
<PinkLayout size="narrow">
<DottedLayout size="narrow">
<div className="flex h-full flex-col">
<div className="h-full p-20 pb-116 pt-36 text-16 pc:relative pc:min-h-[59.5vh] pc:px-0 pc:pb-0">
<div className="h-full p-20 pb-116 pt-36 text-16 tablet:px-40 pc:relative pc:min-h-[59.5vh] pc:px-0 pc:pb-0 pc:pt-56 pc:text-20 pc:font-500">
{isInit ? (
<GenericFormProvider formOptions={{ mode: "onBlur", defaultValues: defaultValue, shouldFocusError: true }}>
<Funnel>
<Step name={POST_STEPS[0]}>
<StarInfo onNextStep={() => setStep(POST_STEPS[1])} />
</Step>
<Step name={POST_STEPS[1]}>
<MainInfo onNextStep={() => setStep(POST_STEPS[2])} />
</Step>
<Step name={POST_STEPS[2]}>
<SubInfo onNextStep={() => setStep(POST_STEPS[3])} />
</Step>
<Step name={POST_STEPS[3]}>
<DetailInfo />
</Step>
</Funnel>
<PostContent />
</GenericFormProvider>
) : (
<div className="flex h-[10vh] w-full items-center justify-center">
Expand All @@ -87,7 +80,14 @@ const Post = () => {
)}
</div>
</div>
</PinkLayout>
</DottedLayout>
{modal === "autoSave" && (
<AlertModal closeModal={closeModal} hasCancelBtn handleBtnClick={importAutoSave} handleCancelClick={clearAutoSave}>
자동 저장된 내용이 있습니다.
<br />
이어서 작성할까요?
</AlertModal>
)}
</>
);
};
Expand Down
Loading

0 comments on commit db5a5ba

Please sign in to comment.