Skip to content

feat: 메시지 생성 페이지 추가 및 기능 구현 #28

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

Merged
merged 2 commits into from
Dec 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 240 additions & 7 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"react-leaflet": "^4.2.1",
"react-router": "^7.0.2",
"react-router-dom": "^7.0.2",
"react-transition-group": "^4.4.5"
"react-transition-group": "^4.4.5",
"react-use": "^17.5.1"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
Expand Down
2 changes: 1 addition & 1 deletion src/_app/Providers/router/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const router = createBrowserRouter([
element: <CapsuleDetailPage />,
},
{
path: "/capsule/message/create",
path: "/capsule/:code/message/create",
element: <CreateMessagePage />,
},
]);
Expand Down
10 changes: 8 additions & 2 deletions src/_app/pages/CapsuleDetailPage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { useLoadingOverlay } from "@/_app/Providers/loadingOverlay";
import CustomButtons from "@/components/CustomButtons";
import { useNavigate } from "react-router";
import { isUndefined } from "@/utils";
import { useMemo } from "react";
import { useNavigate, useParams } from "react-router";

// 진입점 설정을 위해 임시로 작성되었습니다.
const CapsuleDetailPage = () => {
const { code } = useParams();
const capsuleCode = useMemo(() => (isUndefined(code) ? "" : code), [code]);
const navigate = useNavigate();

const { setGlobalLoading } = useLoadingOverlay();
Expand All @@ -16,7 +20,9 @@ const CapsuleDetailPage = () => {
></CustomButtons.CommonButton>
<CustomButtons.BottomButton
title="캡슐 채우기"
onClick={() => navigate("/capsule")}
onClick={() =>
navigate(`/capsule/${encodeURIComponent(capsuleCode)}/message/create`)
}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import CustomInput from "@/components/CustomInput";
import StepHeader from "@/components/Funnel/StepHeader";
import Margin from "@/components/Margin";

interface Props {
name: string;
Expand All @@ -9,9 +8,9 @@ interface Props {
const NameInputStep = ({ name, setName }: Props) => {
return (
<div className="flex-col px-[22px]">
<Margin H="54px" />
<div className="h-[54px]" />
<StepHeader text={`함께할 캡슐에\n$이름$을 붙여보세요.`} />
<Margin H="54px" />
<div className="h-[54px]" />
<CustomInput
label="캡슐 이름"
value={name}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import CustomInput from "@/components/CustomInput";
import StepHeader from "@/components/Funnel/StepHeader";
import Margin from "@/components/Margin";

interface Props {
password: string;
Expand All @@ -9,9 +8,9 @@ interface Props {
const PasswordInputStep = ({ password, setPassword }: Props) => {
return (
<div className="flex-col px-[22px]">
<Margin H="54px" />
<div className="h-[54px]" />
<StepHeader text={`캡슐 잠금을 위한\n$비밀번호$를 설정해 주세요.`} />
<Margin H="54px" />
<div className="h-[54px]" />
<CustomInput
label="비밀번호"
value={password}
Expand Down
7 changes: 4 additions & 3 deletions src/_app/pages/CreateCapsulePage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useLoadingOverlay } from "@/_app/Providers/loadingOverlay";
import Funnel, { Step } from "@/components/Funnel";
import CapsuleCreateFunnel from "@/components/Funnel/CapsuleCreateFunnel";
import CapsuleCreateCompleteModal from "@/components/Modals/CapsuleCreateCompleteModal";
import CapsuleCreateConfirmModal from "@/components/Modals/CapsuleCreateConfirmModal";
import { useCapsuleMutate } from "@/queries/Capsule/useCapsuleService";
import { Step } from "@/types/client";
import { getTimeStampByDate } from "@/utils/formatTime";
import { useState } from "react";
import { useNavigate } from "react-router";
Expand Down Expand Up @@ -72,7 +73,7 @@ const CreateCapsulePage = () => {

// 양쪽 끝 인덱스 콜백 함수
const firstBackCallback = () => navigate("/");
const lastNextCallback = async () => setIsCreateConfirmModalOpen(true);
const lastNextCallback = () => setIsCreateConfirmModalOpen(true);

const [createdCapsuleCode, setCreatedCapsuleCode] = useState<string>();
// 생성 확인 모달 관련
Expand Down Expand Up @@ -115,7 +116,7 @@ const CreateCapsulePage = () => {

return (
<>
<Funnel
<CapsuleCreateFunnel
steps={steps}
firstBackCallback={firstBackCallback}
lastNextCallback={lastNextCallback}
Expand Down
25 changes: 25 additions & 0 deletions src/_app/pages/CreateMessagePage/Steps/NameInputStep/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import CustomInput from "@/components/CustomInput";
import StepHeader from "@/components/Funnel/StepHeader";

interface Props {
name: string;
setName: (newName: string) => void;
}
const NameInputStep = ({ name, setName }: Props) => {
return (
<div className="flex-col px-[22px]">
<div className="h-[10px]" />
<StepHeader text={`캡슐에 입력될\n$이름$을 작성해 주세요.`} />
<div className="h-[42px]" />
<CustomInput
label="캡슐 이름"
value={name}
setValue={setName}
placeholder="캡슐에 이름을 붙여주세요."
mountFocus
/>
</div>
);
};

export default NameInputStep;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { getInputCapsuleNameValid } from "@/utils/validations";
import { useEffect, useMemo, useState } from "react";

const useNameInputStep = () => {
const [inputName, setInputName] = useState<string>("");
const [errorMessage, setErrorMessage] = useState<string>("");
const buttonDisabled = useMemo(() => inputName.length === 0, [inputName]);

const onClickButton = () => {
const isValid = getInputCapsuleNameValid(inputName);

if (!isValid) setErrorMessage("캡슐 이름이 유효하지 않습니다.");

return isValid;
};

const stepProps = {
BottomButton: {
onClick: onClickButton,
disabled: buttonDisabled,
},
errorMessage: errorMessage,
};

useEffect(() => setErrorMessage(""), [inputName]);

return { inputName, setInputName, stepProps };
};

export default useNameInputStep;
101 changes: 101 additions & 0 deletions src/_app/pages/CreateMessagePage/Steps/PhotoInputStep/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { useLoadingOverlay } from "@/_app/Providers/loadingOverlay";
import IconPlus from "@/assets/icons/plus-icon.svg?react";
import StepHeader from "@/components/Funnel/StepHeader";
import { Photo } from "@/types/client";
import { isNill, isUndefined } from "@/utils";
import clsx from "clsx";
import { ChangeEvent, useRef } from "react";
import { useWindowSize } from "react-use";

interface Props {
photo: Photo | undefined;
setPhoto: (newPhoto: Photo) => void;
}
const PhotoInputStep = ({ photo, setPhoto }: Props) => {
const { setGlobalLoading } = useLoadingOverlay();
const inputRef = useRef<HTMLInputElement>(null);
const onClickUpload = () => {
if (isNill(inputRef.current)) {
return;
}

inputRef.current.click();
};
const onChange = async (event: ChangeEvent<HTMLInputElement>) => {
if (isNill(event.target) || isNill(event.target.files)) {
return;
}

const file = event.target.files[0];
setGlobalLoading(true);

const maxSize = 5 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
setGlobalLoading(false);

return;
}

const fileUrl = URL.createObjectURL(file);

const img = new window.Image();
img.src = fileUrl;
img.onload = () =>
setPhoto({
file,
url: fileUrl,
});
setGlobalLoading(false);
};

const { height } = useWindowSize();

return (
<>
<div className="h-full flex-col px-[22px]">
<div className="h-[10px]" />
<StepHeader text={`캡슐에 함께 묻을\n$사진$을 선택해 주세요.`} />
<div className="h-[42px]" />
<div
style={{
height:
height - 54 - 18 - 32 - 10 - 64 - 42 - 18 - 20 - 18 - 56 - 30,
}}
className={clsx(
"relative p-[14px] flex flex-col gap-[10px] bg-white rounded-[15px] border-[1px] border-solid border-border-grey justify-center items-center"
)}
>
{isUndefined(photo) ? (
<div className="w-full h-full p-[14px] rounded-[15px] border-[1px] border-dashed border-[#A1A1A1] flex justify-center items-center">
<div className="flex flex-col gap-[6px] text-center">
<IconPlus onClick={onClickUpload} className="cursor-pointer" />
<p>
<span className="text-primary-main">0</span>
{` / 1`}
</p>
</div>
</div>
) : (
<img
src={photo.url}
className={clsx("w-full h-full rounded-[15px] object-contain")}
/>
)}
</div>
<div className="h-[18px]" />
<p className="text-[#A1A1A1] text-[14px] text-center">
사진은 최대 1장, 5mb까지 업로드 가능해요.
</p>
</div>
<input
type="file"
accept="image/jpeg, image/png, image/gif"
ref={inputRef}
style={{ display: "none" }}
onChange={onChange}
/>
</>
);
};

export default PhotoInputStep;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Photo } from "@/types/client";
import { useEffect, useMemo, useState } from "react";

const usePhotoInputStep = () => {
const [inputPhoto, setInputPhoto] = useState<Photo>();
const [errorMessage, setErrorMessage] = useState<string>("");
const buttonDisabled = useMemo(() => false, []);

const onClickButton = () => {
return true;
};

const stepProps = {
BottomButton: {
onClick: onClickButton,
disabled: buttonDisabled,
},
errorMessage: errorMessage,
};

useEffect(() => setErrorMessage(""), [inputPhoto]);

return { inputPhoto, setInputPhoto, stepProps };
};

export default usePhotoInputStep;
24 changes: 24 additions & 0 deletions src/_app/pages/CreateMessagePage/Steps/TextInputStep/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import CustomTextArea from "@/components/CustomTextArea";
import StepHeader from "@/components/Funnel/StepHeader";

interface Props {
text: string;
setText: (newText: string) => void;
}
const TextInputStep = ({ text, setText }: Props) => {
return (
<div className="h-full flex-col px-[22px]">
<div className="h-[10px]" />
<StepHeader text={`캡슐에 함께 묻을\n$이야기$를 작성해 주세요.`} />
<div className="h-[42px]" />
<CustomTextArea
value={text}
setValue={setText}
placeholder="캡슐에 이름을 붙여주세요."
mountFocus
/>
</div>
);
};

export default TextInputStep;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { getInputCapsuleTextValid } from "@/utils/validations";
import { useEffect, useMemo, useState } from "react";

const useTextInputStep = () => {
const [inputText, setInputText] = useState<string>("");
const [errorMessage, setErrorMessage] = useState<string>("");
const buttonDisabled = useMemo(() => inputText.length === 0, [inputText]);

const onClickButton = () => {
const isValid = getInputCapsuleTextValid(inputText);

if (!isValid) setErrorMessage("입력이 유효하지 않습니다.");

return isValid;
};

const stepProps = {
BottomButton: {
onClick: onClickButton,
disabled: buttonDisabled,
},
errorMessage: errorMessage,
};

useEffect(() => setErrorMessage(""), [inputText]);

return { inputText, setInputText, stepProps };
};

export default useTextInputStep;
Loading
Loading