Skip to content

Commit bbcba7e

Browse files
committed
[PreparePage] feat: prepare 페이지 기능 추가
1 parent 2b550d8 commit bbcba7e

File tree

6 files changed

+206
-59
lines changed

6 files changed

+206
-59
lines changed

src/components/video/VideoPlayer.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import IconPlay from "@Components/icons/IconPlay";
2+
import { shareRef } from "@Utils/reactExtension";
3+
import { mergeClassName } from "@Utils/styleExtension";
4+
import { AnimatePresence, motion } from "framer-motion";
5+
import { ComponentProps, forwardRef, useEffect, useRef, useState } from "react";
6+
7+
export interface VideoPlayerProps extends ComponentProps<"video"> {}
8+
9+
// HJ TODO: default 값 더 좋게 넘길 수 있을 듯...
10+
const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(
11+
(
12+
{ src = "", muted = true, autoPlay = true, className, ...restProps },
13+
ref
14+
) => {
15+
const [isVisible, setIsVisible] = useState(false);
16+
const [isPlaying, setIsPlaying] = useState(false);
17+
const videoRef = useRef<HTMLVideoElement>(null);
18+
19+
const toggleState = () => {
20+
const videoEl = videoRef.current;
21+
if (!videoEl) return;
22+
23+
if (videoEl.paused) {
24+
videoEl.play();
25+
setIsPlaying(true);
26+
} else {
27+
videoEl.pause();
28+
setIsPlaying(false);
29+
}
30+
31+
setIsVisible(true);
32+
setTimeout(() => setIsVisible(false), 500);
33+
};
34+
35+
// HJ TODO: video manager로 분리 (촬영 페이지에서도 사용되어야됨)
36+
useEffect(() => {
37+
const videoEl = videoRef.current;
38+
if (!videoEl) return;
39+
function playHandler() {
40+
setIsPlaying(true);
41+
}
42+
function endHandler() {
43+
setIsPlaying(false);
44+
}
45+
videoEl.addEventListener("play", playHandler);
46+
videoEl.addEventListener("ended", endHandler);
47+
return () => {
48+
videoEl.removeEventListener("play", playHandler);
49+
videoEl.removeEventListener("ended", endHandler);
50+
};
51+
}, []);
52+
53+
return (
54+
<div className="relative w-full aspect-[9/16]">
55+
<video
56+
ref={shareRef(videoRef, ref)}
57+
className={mergeClassName("w-full h-full", className)}
58+
src={src}
59+
muted={muted}
60+
autoPlay={true}
61+
onClick={toggleState}
62+
{...restProps}
63+
/>
64+
65+
<div className="absolute top-0 bottom-0 left-0 right-0 m-auto w-fit h-fit">
66+
<AnimatePresence>
67+
{isVisible && (
68+
<motion.div
69+
initial={{ opacity: 0 }}
70+
animate={{ opacity: 1 }}
71+
exit={{ opacity: 0 }}
72+
transition={{ duration: 1 }}
73+
>
74+
{isPlaying ? <StartButton /> : <PauseButton />}
75+
</motion.div>
76+
)}
77+
</AnimatePresence>
78+
</div>
79+
</div>
80+
);
81+
}
82+
);
83+
84+
VideoPlayer.displayName = "VideoPlayer";
85+
86+
function PauseButton() {
87+
return (
88+
<button className="flex items-center justify-center w-[100px] h-[100px] bg-[#FFFFFF40] rounded-full gap-[8px]">
89+
<div className="w-[16px] h-[40px] rounded-[6px] bg-[#FFFFFF]" />
90+
<div className="w-[16px] h-[40px] rounded-[6px] bg-[#FFFFFF]" />
91+
</button>
92+
);
93+
}
94+
95+
function StartButton() {
96+
return (
97+
<button className="flex items-center justify-center w-[100px] h-[100px] bg-[#FFFFFF40] rounded-full gap-[8px]">
98+
<IconPlay />
99+
</button>
100+
);
101+
}
102+
103+
export default VideoPlayer;

src/hooks/useLateTemplate.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { QUERY_KEYS } from "@Constant/queryKeys";
2+
import { getTemplate } from "@Utils/templateManager";
3+
import { useQuery } from "@tanstack/react-query";
4+
5+
// HJ TODO: base interface 만들기
6+
export default function useLateTemplate() {
7+
const templateQuery = useQuery({
8+
queryKey: [QUERY_KEYS.template],
9+
queryFn: getTemplate,
10+
staleTime: Infinity,
11+
});
12+
13+
return templateQuery;
14+
}

src/hooks/useVideo.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useCallback, useState } from "react";
2+
3+
interface UseVideoProps {
4+
defaultMuted: boolean;
5+
}
6+
7+
// HJ TODO: 범용적으로 바꾸기 가능
8+
export default function useVideo({ defaultMuted }: UseVideoProps) {
9+
const [isMuted, setIsMuted] = useState(defaultMuted);
10+
11+
const mute = useCallback(() => {
12+
setIsMuted(true);
13+
}, []);
14+
15+
const unMute = useCallback(() => {
16+
setIsMuted(false);
17+
}, []);
18+
19+
const toggleMute = useCallback(() => {
20+
setIsMuted((prev) => !prev);
21+
}, []);
22+
23+
return { isMuted, mute, unMute, toggleMute };
24+
}

src/pages/choose/Choose.page.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ import { useNavigate } from "react-router-dom";
55
import { motion } from "framer-motion";
66
import useMotion from "@Hooks/useMotion";
77
import CatImage2 from "@Assets/images/cat2.png";
8-
import { TemplateType, getTemplate } from "@Utils/templateManager";
9-
import { useQuery } from "@tanstack/react-query";
10-
import { QUERY_KEYS } from "@Constant/queryKeys";
8+
import { TemplateType } from "@Utils/templateManager";
119
import lodash from "lodash";
10+
import useLateTemplate from "@Hooks/useLateTemplate";
1211

1312
export default function ChoosePage() {
1413
const motionContent = useMotion({ id: "choosepage-content" });
@@ -90,10 +89,7 @@ function Middle() {
9089

9190
function Content() {
9291
const navigate = useNavigate();
93-
const templateQuery = useQuery({
94-
queryKey: [QUERY_KEYS.template],
95-
queryFn: getTemplate,
96-
});
92+
const templateQuery = useLateTemplate();
9793

9894
// HJ TODO: loading에 기능...
9995
if (templateQuery.status === "pending") {

src/pages/prepare/Prepare.page.tsx

Lines changed: 44 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,79 @@
11
import Button from "@Components/button/Button";
2-
import Templates from "@Constant/templates";
2+
import IconCat from "@Components/icons/IconCat";
3+
import IconSound from "@Components/icons/IconSound";
4+
import VideoPlayer from "@Components/video/VideoPlayer";
5+
import useLateTemplate from "@Hooks/useLateTemplate";
6+
import useVideo from "@Hooks/useVideo";
7+
import { mergeClassName } from "@Utils/styleExtension";
38
import { useNavigate, useParams } from "react-router-dom";
49

510
export default function PreparePage() {
6-
const { templateId } = useParams();
711
const navigate = useNavigate();
812

9-
// @ts-expect-error
10-
const templateInfo: (typeof Templates)["id1"] = Templates?.[templateId];
13+
const { templateId } = useParams();
14+
const templateQuery = useLateTemplate();
15+
16+
const { isMuted, toggleMute } = useVideo({ defaultMuted: false });
1117

12-
if (!templateInfo) {
13-
throw new Error("invalid template id");
14-
}
18+
if (templateQuery.status === "pending") return <div>loading</div>;
19+
if (templateQuery.status === "error") return <div>error</div>;
20+
21+
const template = templateQuery.data.find(
22+
(template) => template.id === templateId
23+
);
24+
25+
if (!template || template.state !== "ready") return <div>error</div>;
1526

1627
const goBack = () => navigate(-1);
1728
const goShooting = () => navigate(`/shooting/${templateId}`);
1829

19-
const { label, title, description, src } = templateInfo;
20-
2130
return (
2231
<div className="w-full h-full flex flex-col justify-between">
2332
<div className="relative w-full overflow-hidden max-h-full">
24-
<img className="w-full h-full" src={src} />
25-
<div className="absolute top-0 flex flex-col w-full h-full py-[24px] px-[16px] justify-between">
33+
<VideoPlayer src={template.previewVideoSrc} muted={isMuted} />
34+
35+
<div className="absolute top-0 flex flex-col w-full h-full py-[24px] px-[16px] justify-between pointer-events-none">
2636
<div className="flex flex-row justify-between">
2737
<button
28-
className="flex flex-row items-center gap-[8px]"
38+
className="flex flex-row items-center gap-[8px] pointer-events-auto"
2939
onClick={goBack}
3040
>
3141
<div>
3242
<BackIcon />
3343
</div>
3444
<div className="font-yClover text-white font-bold">뒤로가기</div>
3545
</button>
36-
<button className="flex justify-center items-center w-[40px] h-[40px] bg-[rgba(255,255,255,0.25)] rounded-[8px]">
46+
<button
47+
className={mergeClassName(
48+
"flex justify-center items-center w-[40px] h-[40px] bg-[rgba(255,255,255,0.25)] rounded-[8px] pointer-events-auto"
49+
)}
50+
onClick={toggleMute}
51+
>
3752
<IconSound />
3853
</button>
3954
</div>
40-
<div className="flex flex-col gap-[20px]">
41-
<div className="flex flex-col gap-[8px]">
42-
<div className="py-[4px] px-[8px] w-fit rounded-full bg-[#FCD55F] font-yClover font-bold text-[#191919]">
43-
{label}
44-
</div>
45-
<div className="font-yClover font-bold text-white leading-5">
46-
{title}
47-
</div>
48-
</div>
49-
<div className="flex flex-col gap-[8px] font-yClover font-regular text-white">
50-
{description.map((item) => (
51-
<div key={item}>{item}</div>
52-
))}
55+
<div className="flex flex-col gap-[8px]">
56+
<p className="font-yClover font-bold text-[24px] text-white leading-[120%]">
57+
{template.title}
58+
</p>
59+
<div className="flex flex-1 gap-[8px]">
60+
<IconCat />
61+
<p className="flex flex-col w-full gap-[8px] font-yClover font-normal text-[14px] leading-[140%] text-white whitespace-pre-line">
62+
{template.description}
63+
</p>
5364
</div>
5465
</div>
5566
</div>
5667
</div>
57-
<div className="flex flex-row items-center pt-[52px] pb-[40px] px-[16px] gap-[16px] justify-center">
58-
<Button className="w-full" variant={"primary"} onClick={goShooting}>
68+
<div className="absolute w-full bottom-[25px] flex flex-row items-center pt-[52px] pb-[40px] px-[16px] gap-[16px] justify-center">
69+
<Button
70+
className="w-full h-[56px]"
71+
variant={"primary"}
72+
onClick={goShooting}
73+
>
5974
직접 촬영할래
6075
</Button>
61-
<Button className="w-full" variant={"primary"}>
76+
<Button className="w-full h-[56px] bg-[#9CA3AF]" variant={"primary"}>
6277
업로르할래
6378
</Button>
6479
</div>
@@ -82,26 +97,3 @@ function BackIcon() {
8297
</svg>
8398
);
8499
}
85-
86-
function IconSound() {
87-
return (
88-
<svg
89-
xmlns="http://www.w3.org/2000/svg"
90-
width="25"
91-
height="24"
92-
viewBox="0 0 25 24"
93-
fill="none"
94-
>
95-
<path
96-
d="M2.04617 14.4299C1.60587 13.6959 1.37329 12.856 1.37329 12C1.37329 11.1441 1.60587 10.3042 2.04617 9.57016C2.18156 9.34419 2.36273 9.14905 2.57804 8.99728C2.79335 8.8455 3.03803 8.74044 3.29635 8.68886L5.42784 8.26206C5.55495 8.23689 5.6696 8.16891 5.75266 8.06944L8.35626 4.94335C9.8444 3.15557 10.5897 2.26294 11.2532 2.50341C11.9192 2.74388 11.9192 3.90719 11.9192 6.23382V17.7688C11.9192 20.0941 11.9192 21.2562 11.2545 21.4979C10.591 21.7371 9.84566 20.8445 8.35752 19.058L5.75014 15.9306C5.6674 15.8314 5.55324 15.7634 5.42658 15.738L3.29509 15.3112C3.03677 15.2596 2.79209 15.1546 2.57678 15.0028C2.36147 14.851 2.18156 14.6559 2.04617 14.4299Z"
97-
fill="white"
98-
/>
99-
<path
100-
d="M15.1121 7.54826C16.2867 8.7229 16.9495 10.3142 16.9559 11.9755C16.9622 13.6367 16.3117 15.233 15.1461 16.4166M20.3004 4.87793C22.1799 6.75719 23.2404 9.30317 23.2507 11.961C23.2611 14.6188 22.2205 17.173 20.3558 19.0668"
101-
stroke="white"
102-
strokeWidth="2.518"
103-
strokeLinecap="round"
104-
/>
105-
</svg>
106-
);
107-
}

src/utils/reactExtension.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { MutableRefObject, RefCallback } from "react";
2+
3+
type RefType<T> = MutableRefObject<T> | RefCallback<T> | null;
4+
5+
export const shareRef =
6+
<T>(localRef: RefType<T>, forwardRef: RefType<T>): RefCallback<T> =>
7+
(instance) => {
8+
if (typeof localRef === "function") {
9+
localRef(instance);
10+
} else if (localRef && instance) {
11+
localRef.current = instance;
12+
}
13+
if (typeof forwardRef === "function") {
14+
forwardRef(instance);
15+
} else if (forwardRef && instance) {
16+
forwardRef.current = instance;
17+
}
18+
};

0 commit comments

Comments
 (0)