Skip to content

Commit

Permalink
Merge pull request #63 from softeerbootcamp4th/feature/12-commentary
Browse files Browse the repository at this point in the history
[feat] 기대평 무한 캐러셀 구현
  • Loading branch information
darkdulgi authored Aug 5, 2024
2 parents 486bca0 + 097fa70 commit 4fbaae2
Show file tree
Hide file tree
Showing 17 changed files with 392 additions and 8 deletions.
35 changes: 35 additions & 0 deletions public/icons/error.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import IntroSection from "./introSection";
import Header from "./header";
import SimpleInformation from "./simpleInformation";
import DetailInformation from "./detailInformation";
import CommentSection from "./comment";
import QnA from "./qna";
import Footer from "./footer";
import Modal from "./modal/modal.jsx";
Expand All @@ -19,6 +20,7 @@ function App() {
<Header />
<SimpleInformation />
<DetailInformation />
<CommentSection />
<QnA />
<Footer />
<Modal layer="interaction" />
Expand Down
16 changes: 16 additions & 0 deletions src/comment/assets/decoration.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions src/comment/autoScrollCarousel/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import useAutoCarousel from "./useAutoCarousel.js";

function AutoScrollCarousel({ speed = 1, gap = 0, children }) {
const { position, ref, eventListener } = useAutoCarousel(speed);

const flexStyle =
"flex [&>div]:flex-shrink-0 gap-[var(--gap,0)] items-center absolute";
return (
<div className="w-full h-full overflow-hidden" {...eventListener}>
<div
style={{
"--gap": gap + "px",
transform: `translateX(${position * -1}px)`,
}}
className="relative h-max touch-pan-y"
>
<div
className={`${flexStyle} -translate-x-[calc(100%+var(--gap,0px))]`}
>
{children}
</div>
<div className={flexStyle} ref={ref}>
{children}
</div>
<div className={`${flexStyle} translate-x-[calc(100%+var(--gap,0px))]`}>
{children}
</div>
</div>
</div>
);
}

export default AutoScrollCarousel;
105 changes: 105 additions & 0 deletions src/comment/autoScrollCarousel/useAutoCarousel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { useState, useEffect, useRef, useCallback } from "react";
import useMountDragEvent from "@/common/useMountDragEvent";

const FRICTION_RATE = 0.1;
const MOMENTUM_THRESHOLD = 0.6;
const MOMENTUM_RATE = 0.15;

function useAutoCarousel(speed = 1) {
const childRef = useRef(null);
const [position, setPosition] = useState(0);
const [isControlled, setIsControlled] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const timestamp = useRef(null);
const dragging = useRef(false);
const prevDragState = useRef({ x: 0, mouseX: 0, prevMouseX: 0 });
const momentum = useRef(speed);

useEffect(() => {
if (isControlled) return;

let progress = true;
timestamp.current = performance.now();
function animate(time) {
if (childRef.current === null) return;

// 마우스 뗐을 때 관성 재계산
const baseSpeed = isHovered ? 0 : speed;
momentum.current -= (momentum.current - baseSpeed) * FRICTION_RATE;

if (Math.abs(momentum.current, baseSpeed) < MOMENTUM_THRESHOLD)
momentum.current = baseSpeed;
const finalSpeed = momentum.current;

// 인터벌과 실제 x 포지션 계산
const interval = performance.now() - timestamp.current;
setPosition((position) => {
const newPos = position + finalSpeed * interval;
return newPos % childRef.current.clientWidth;
});

// 타임스탬프 저장
timestamp.current = time;
if (progress) requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

return () => {
progress = false;
};
}, [isControlled, isHovered, speed]);

// 드래그 도중 함수
const onDrag = useCallback(({ x: mouseX }) => {
if (!dragging.current) return;

// 새로운 포지션 계산
let newPos =
prevDragState.current.x - mouseX + prevDragState.current.mouseX;
newPos %= childRef.current.clientWidth;
setPosition(newPos);

// 관성 계산
if (Math.abs(mouseX - prevDragState.current.mouseX) > 10) {
momentum.current =
(prevDragState.current.prevMouseX - mouseX) * MOMENTUM_RATE;
} else momentum.current = 0;
prevDragState.current.prevMouseX = mouseX;
}, []);

// 드래그 종료 함수
const onDragEnd = useCallback((e) => {
if (!dragging.current) return;
dragging.current = false;
setIsControlled(false);
if (e.pointerType === "touch") setIsHovered(false);
}, []);

useMountDragEvent(onDrag, onDragEnd);

return {
position,
ref: childRef,
eventListener: {
onMouseEnter() {
setIsControlled(true);
setIsHovered(true);
},
onMouseLeave() {
setIsControlled(false);
setIsHovered(false);
},
onPointerDown(e) {
setIsControlled(true);
setIsHovered(true);
dragging.current = true;
prevDragState.current.x = position;
prevDragState.current.mouseX = e.clientX;
prevDragState.current.prevMouseX = e.clientX;
},
},
};
}

export default useAutoCarousel;
41 changes: 41 additions & 0 deletions src/comment/commentCarousel/CommentCarousel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import AutoScrollCarousel from "../autoScrollCarousel";

function mask(string) {
const len = string.length;
if (len <= 1) return "*";
if (len === 2) return string[0] + "*";
return string[0] + "*".repeat(len - 2) + string[len - 1];
}

function formatDate(dateString) {
const date = new Date(dateString);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${year}. ${month}. ${day}`;
}

function CommentCarousel({ resource }) {
const comments = resource().comments;

return (
<div className="w-full h-[29rem]">
<AutoScrollCarousel speed={0.1} gap={28}>
{comments.map(({ id, content, userName, createdAt }) => (
<div
className="w-72 h-96 mt-10 bg-neutral-50 p-8 flex flex-col justify-between gap-10 hover:scale-110 transition-transform duration-200 ease-in-out-cubic"
key={id}
>
<p className="text-neutral-800 text-body-l">{content}</p>
<div className="text-blue-400 flex flex-col gap-1">
<p className="text-body-m">{mask(userName)}</p>
<p className="text-body-s">{formatDate(createdAt)}</p>
</div>
</div>
))}
</AutoScrollCarousel>
</div>
);
}

export default CommentCarousel;
19 changes: 19 additions & 0 deletions src/comment/commentCarousel/CommentCarouselError.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
function CommentCarouselError() {
return (
<div className="w-full h-[29rem] flex justify-center items-center px-6">
<div className="w-full max-w-[1200px] h-96 bg-neutral-50 flex flex-col justify-center items-center gap-4">
<img
src="/icons/error.svg"
alt="불러오기 오류"
width="120"
height="120"
/>
<p className="text-body-l text-red-500 font-bold">
기대평 정보를 불러오지 못했어요!
</p>
</div>
</div>
);
}

export default CommentCarouselError;
33 changes: 33 additions & 0 deletions src/comment/commentCarousel/CommentCarouselSkeleton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Spinner from "@/common/Spinner.jsx";

function CommentCarouselSkeleton() {
return (
<div className="w-full h-[29rem] flex justify-center items-center gap-7 overflow-hidden">
<div className="flex justify-center items-center gap-7">
<div className="w-72 h-96 bg-neutral-50 flex justify-center items-center ">
<Spinner />
</div>
<div className="w-72 h-96 bg-neutral-50 flex justify-center items-center">
<Spinner />
</div>
<div className="w-72 h-96 bg-neutral-50 flex justify-center items-center">
<Spinner />
</div>
<div className="w-72 h-96 bg-neutral-50 flex justify-center items-center">
<Spinner />
</div>
<div className="w-72 h-96 bg-neutral-50 flex justify-center items-center">
<Spinner />
</div>
<div className="w-72 h-96 bg-neutral-50 flex justify-center items-center">
<Spinner />
</div>
<div className="w-72 h-96 bg-neutral-50 flex justify-center items-center">
<Spinner />
</div>
</div>
</div>
);
}

export default CommentCarouselSkeleton;
19 changes: 19 additions & 0 deletions src/comment/commentCarousel/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Suspense from "@/common/Suspense.jsx";
import ErrorBoundary from "@/common/ErrorBoundary.jsx";
import { fetchResource } from "@/common/fetchServer.js";
import CommentCarousel from "./CommentCarousel.jsx";
import CommentCarouselSkeleton from "./CommentCarouselSkeleton.jsx";
import CommentCarouselError from "./CommentCarouselError.jsx";

function CommentCarouselView() {
const resource = fetchResource("/api/v1/comment");
return (
<ErrorBoundary fallback={<CommentCarouselError />}>
<Suspense fallback={<CommentCarouselSkeleton />}>
<CommentCarousel resource={resource} />
</Suspense>
</ErrorBoundary>
);
}

export default CommentCarouselView;
32 changes: 32 additions & 0 deletions src/comment/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import CommentCarousel from "./commentCarousel";
import decoration from "./assets/decoration.svg";

function CommentSection() {
return (
<section className="w-full flex flex-col items-center py-24 lg:py-60 gap-40">
<div className="w-full flex flex-col items-center">
<div className="relative flex flex-col gap-3 lg:gap-9 text-center font-bold items-center">
<p className="text-body-m text-neutral-600 w-fit py-3 lg:py-5">
기대평 작성하기
</p>
<h2 className="text-head-s lg:text-head-m text-black">
UNIQUE한 <br className="hidden sm:inline" />
IONIQ 5의 <br className="inline sm:hidden" />
<span className="sketch-line">기대평</span>을 남겨주세요
</h2>
<img
src={decoration}
alt="heart"
className="size-20 lg:size-28 absolute -top-2 sm:top-4 lg:top-10 -left-2 sm:left-12"
/>
</div>
<p className="text-body-m sm:text-title-s text-neutral-800 font-medium mt-10">
기대평을 등록하면 추첨 이벤트의 당첨 확률이 올라갑니다.
</p>
<CommentCarousel />
</div>
</section>
);
}

export default CommentSection;
41 changes: 41 additions & 0 deletions src/comment/mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { http, HttpResponse } from "msw";

function randArr(arr) {
let idx = Math.floor(Math.random() * arr.length);
return arr[idx];
}

function makeLorem(min, max) {
const loremipsum =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.".split(
" ",
);

let result = [];
let cnt = Math.floor(Math.random() * (max - min)) + min;
for (let i = 0; i < cnt; i++) {
result.push(randArr(loremipsum));
}
return result.join(" ");
}

function getCommentMock() {
return Array.from({ length: 20 }, (_, i) => {
return {
id: i * 100 + Math.floor(Math.random() * 99),
content: makeLorem(5, 12),
userName: makeLorem(1, 1),
createdAt: new Date(
Date.now() - Math.floor(Math.random() * 86400 * 60 * 1000),
),
};
});
}

const handlers = [
http.get("/api/v1/comment", async () => {
return HttpResponse.json({ comments: getCommentMock() });
}),
];

export default handlers;
Loading

0 comments on commit 4fbaae2

Please sign in to comment.