Skip to content

Commit 174fde1

Browse files
authored
Merge pull request #28 from uchumu/dev/create-message-page
feat: 메시지 생성 페이지 추가 및 기능 구현
2 parents 0a1c167 + ce78e92 commit 174fde1

File tree

30 files changed

+882
-44
lines changed

30 files changed

+882
-44
lines changed

package-lock.json

Lines changed: 240 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"react-leaflet": "^4.2.1",
2323
"react-router": "^7.0.2",
2424
"react-router-dom": "^7.0.2",
25-
"react-transition-group": "^4.4.5"
25+
"react-transition-group": "^4.4.5",
26+
"react-use": "^17.5.1"
2627
},
2728
"devDependencies": {
2829
"@eslint/js": "^9.15.0",

src/_app/Providers/router/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const router = createBrowserRouter([
1919
element: <CapsuleDetailPage />,
2020
},
2121
{
22-
path: "/capsule/message/create",
22+
path: "/capsule/:code/message/create",
2323
element: <CreateMessagePage />,
2424
},
2525
]);

src/_app/pages/CapsuleDetailPage/index.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import { useLoadingOverlay } from "@/_app/Providers/loadingOverlay";
22
import CustomButtons from "@/components/CustomButtons";
3-
import { useNavigate } from "react-router";
3+
import { isUndefined } from "@/utils";
4+
import { useMemo } from "react";
5+
import { useNavigate, useParams } from "react-router";
46

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

913
const { setGlobalLoading } = useLoadingOverlay();
@@ -16,7 +20,9 @@ const CapsuleDetailPage = () => {
1620
></CustomButtons.CommonButton>
1721
<CustomButtons.BottomButton
1822
title="캡슐 채우기"
19-
onClick={() => navigate("/capsule")}
23+
onClick={() =>
24+
navigate(`/capsule/${encodeURIComponent(capsuleCode)}/message/create`)
25+
}
2026
/>
2127
</>
2228
);

src/_app/pages/CreateCapsulePage/Steps/NameInputStep/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import CustomInput from "@/components/CustomInput";
22
import StepHeader from "@/components/Funnel/StepHeader";
3-
import Margin from "@/components/Margin";
43

54
interface Props {
65
name: string;
@@ -9,9 +8,9 @@ interface Props {
98
const NameInputStep = ({ name, setName }: Props) => {
109
return (
1110
<div className="flex-col px-[22px]">
12-
<Margin H="54px" />
11+
<div className="h-[54px]" />
1312
<StepHeader text={`함께할 캡슐에\n$이름$을 붙여보세요.`} />
14-
<Margin H="54px" />
13+
<div className="h-[54px]" />
1514
<CustomInput
1615
label="캡슐 이름"
1716
value={name}

src/_app/pages/CreateCapsulePage/Steps/PasswordInputStep/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import CustomInput from "@/components/CustomInput";
22
import StepHeader from "@/components/Funnel/StepHeader";
3-
import Margin from "@/components/Margin";
43

54
interface Props {
65
password: string;
@@ -9,9 +8,9 @@ interface Props {
98
const PasswordInputStep = ({ password, setPassword }: Props) => {
109
return (
1110
<div className="flex-col px-[22px]">
12-
<Margin H="54px" />
11+
<div className="h-[54px]" />
1312
<StepHeader text={`캡슐 잠금을 위한\n$비밀번호$를 설정해 주세요.`} />
14-
<Margin H="54px" />
13+
<div className="h-[54px]" />
1514
<CustomInput
1615
label="비밀번호"
1716
value={password}

src/_app/pages/CreateCapsulePage/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { useLoadingOverlay } from "@/_app/Providers/loadingOverlay";
2-
import Funnel, { Step } from "@/components/Funnel";
2+
import CapsuleCreateFunnel from "@/components/Funnel/CapsuleCreateFunnel";
33
import CapsuleCreateCompleteModal from "@/components/Modals/CapsuleCreateCompleteModal";
44
import CapsuleCreateConfirmModal from "@/components/Modals/CapsuleCreateConfirmModal";
55
import { useCapsuleMutate } from "@/queries/Capsule/useCapsuleService";
6+
import { Step } from "@/types/client";
67
import { getTimeStampByDate } from "@/utils/formatTime";
78
import { useState } from "react";
89
import { useNavigate } from "react-router";
@@ -72,7 +73,7 @@ const CreateCapsulePage = () => {
7273

7374
// 양쪽 끝 인덱스 콜백 함수
7475
const firstBackCallback = () => navigate("/");
75-
const lastNextCallback = async () => setIsCreateConfirmModalOpen(true);
76+
const lastNextCallback = () => setIsCreateConfirmModalOpen(true);
7677

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

116117
return (
117118
<>
118-
<Funnel
119+
<CapsuleCreateFunnel
119120
steps={steps}
120121
firstBackCallback={firstBackCallback}
121122
lastNextCallback={lastNextCallback}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import CustomInput from "@/components/CustomInput";
2+
import StepHeader from "@/components/Funnel/StepHeader";
3+
4+
interface Props {
5+
name: string;
6+
setName: (newName: string) => void;
7+
}
8+
const NameInputStep = ({ name, setName }: Props) => {
9+
return (
10+
<div className="flex-col px-[22px]">
11+
<div className="h-[10px]" />
12+
<StepHeader text={`캡슐에 입력될\n$이름$을 작성해 주세요.`} />
13+
<div className="h-[42px]" />
14+
<CustomInput
15+
label="캡슐 이름"
16+
value={name}
17+
setValue={setName}
18+
placeholder="캡슐에 이름을 붙여주세요."
19+
mountFocus
20+
/>
21+
</div>
22+
);
23+
};
24+
25+
export default NameInputStep;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { getInputCapsuleNameValid } from "@/utils/validations";
2+
import { useEffect, useMemo, useState } from "react";
3+
4+
const useNameInputStep = () => {
5+
const [inputName, setInputName] = useState<string>("");
6+
const [errorMessage, setErrorMessage] = useState<string>("");
7+
const buttonDisabled = useMemo(() => inputName.length === 0, [inputName]);
8+
9+
const onClickButton = () => {
10+
const isValid = getInputCapsuleNameValid(inputName);
11+
12+
if (!isValid) setErrorMessage("캡슐 이름이 유효하지 않습니다.");
13+
14+
return isValid;
15+
};
16+
17+
const stepProps = {
18+
BottomButton: {
19+
onClick: onClickButton,
20+
disabled: buttonDisabled,
21+
},
22+
errorMessage: errorMessage,
23+
};
24+
25+
useEffect(() => setErrorMessage(""), [inputName]);
26+
27+
return { inputName, setInputName, stepProps };
28+
};
29+
30+
export default useNameInputStep;
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { useLoadingOverlay } from "@/_app/Providers/loadingOverlay";
2+
import IconPlus from "@/assets/icons/plus-icon.svg?react";
3+
import StepHeader from "@/components/Funnel/StepHeader";
4+
import { Photo } from "@/types/client";
5+
import { isNill, isUndefined } from "@/utils";
6+
import clsx from "clsx";
7+
import { ChangeEvent, useRef } from "react";
8+
import { useWindowSize } from "react-use";
9+
10+
interface Props {
11+
photo: Photo | undefined;
12+
setPhoto: (newPhoto: Photo) => void;
13+
}
14+
const PhotoInputStep = ({ photo, setPhoto }: Props) => {
15+
const { setGlobalLoading } = useLoadingOverlay();
16+
const inputRef = useRef<HTMLInputElement>(null);
17+
const onClickUpload = () => {
18+
if (isNill(inputRef.current)) {
19+
return;
20+
}
21+
22+
inputRef.current.click();
23+
};
24+
const onChange = async (event: ChangeEvent<HTMLInputElement>) => {
25+
if (isNill(event.target) || isNill(event.target.files)) {
26+
return;
27+
}
28+
29+
const file = event.target.files[0];
30+
setGlobalLoading(true);
31+
32+
const maxSize = 5 * 1024 * 1024; // 10MB
33+
if (file.size > maxSize) {
34+
setGlobalLoading(false);
35+
36+
return;
37+
}
38+
39+
const fileUrl = URL.createObjectURL(file);
40+
41+
const img = new window.Image();
42+
img.src = fileUrl;
43+
img.onload = () =>
44+
setPhoto({
45+
file,
46+
url: fileUrl,
47+
});
48+
setGlobalLoading(false);
49+
};
50+
51+
const { height } = useWindowSize();
52+
53+
return (
54+
<>
55+
<div className="h-full flex-col px-[22px]">
56+
<div className="h-[10px]" />
57+
<StepHeader text={`캡슐에 함께 묻을\n$사진$을 선택해 주세요.`} />
58+
<div className="h-[42px]" />
59+
<div
60+
style={{
61+
height:
62+
height - 54 - 18 - 32 - 10 - 64 - 42 - 18 - 20 - 18 - 56 - 30,
63+
}}
64+
className={clsx(
65+
"relative p-[14px] flex flex-col gap-[10px] bg-white rounded-[15px] border-[1px] border-solid border-border-grey justify-center items-center"
66+
)}
67+
>
68+
{isUndefined(photo) ? (
69+
<div className="w-full h-full p-[14px] rounded-[15px] border-[1px] border-dashed border-[#A1A1A1] flex justify-center items-center">
70+
<div className="flex flex-col gap-[6px] text-center">
71+
<IconPlus onClick={onClickUpload} className="cursor-pointer" />
72+
<p>
73+
<span className="text-primary-main">0</span>
74+
{` / 1`}
75+
</p>
76+
</div>
77+
</div>
78+
) : (
79+
<img
80+
src={photo.url}
81+
className={clsx("w-full h-full rounded-[15px] object-contain")}
82+
/>
83+
)}
84+
</div>
85+
<div className="h-[18px]" />
86+
<p className="text-[#A1A1A1] text-[14px] text-center">
87+
사진은 최대 1장, 5mb까지 업로드 가능해요.
88+
</p>
89+
</div>
90+
<input
91+
type="file"
92+
accept="image/jpeg, image/png, image/gif"
93+
ref={inputRef}
94+
style={{ display: "none" }}
95+
onChange={onChange}
96+
/>
97+
</>
98+
);
99+
};
100+
101+
export default PhotoInputStep;

0 commit comments

Comments
 (0)