@@ -13,26 +16,26 @@ export const PickSkeleton = () => {
);
};
-interface PickSkeletonListProps {
+interface PickSkeletonListV1Props {
rows: number;
itemsInRows: number;
hasInfo?: boolean;
}
-export const PickSkeletonList = ({ rows, itemsInRows, hasInfo }: PickSkeletonListProps) => {
+export const PickSkeletonListV1 = ({ rows, itemsInRows, hasInfo }: PickSkeletonListV1Props) => {
return (
{hasInfo ? (
<>
-
+
{Array.from({ length: rows * itemsInRows - 1 }, (_, index) => (
-
+
))}
>
) : (
<>
{Array.from({ length: rows * itemsInRows }, (_, index) => (
-
+
))}
>
)}
@@ -40,29 +43,103 @@ export const PickSkeletonList = ({ rows, itemsInRows, hasInfo }: PickSkeletonLis
);
};
-export const MobilePickSkeletonList = ({ rows, hasInfo }: { rows: number; hasInfo?: boolean }) => {
+export const MobilePickSkeletonListV1 = ({
+ rows,
+ hasInfo,
+}: {
+ rows: number;
+ hasInfo?: boolean;
+}) => {
const arr = Array.from({ length: rows });
return (
- {hasInfo &&
}
+ {hasInfo &&
}
+
+ {arr.map((_, index) => (
+
+ ))}
+
+ );
+};
+
+export const MyPickSkeletonListV1 = ({ rows, itemsInRows }: PickSkeletonListV1Props) => {
+ return (
+
+ {Array.from({ length: rows * itemsInRows }, (_, index) => (
+
+ ))}
+
+ );
+};
+// -------------------------------------------------------------------------------
+
+// ------------------------------픽픽픽 메인 스켈레톤 v2------------------------------
+
+export const PickSkeletonV2 = () => {
+ return (
+
+ );
+};
+
+interface PickSkeletonListV2Props {
+ rows: number;
+ itemsInRows: number;
+}
+
+export const PickSkeletonListV2 = ({ rows, itemsInRows }: PickSkeletonListV2Props) => {
+ return (
+
+ {Array.from({ length: rows * itemsInRows }, (_, index) => (
+
+ ))}
+
+ );
+};
+
+export const MobilePickSkeletonListV2 = ({ rows }: { rows: number }) => {
+ const arr = Array.from({ length: rows });
+
+ return (
+
{arr.map((_, index) => (
-
+
))}
);
};
-export const MyPickSkeletonList = ({ rows, itemsInRows }: PickSkeletonListProps) => {
+export const MyPickSkeletonListV2 = ({ rows, itemsInRows }: PickSkeletonListV2Props) => {
return (
{Array.from({ length: rows * itemsInRows }, (_, index) => (
-
+
))}
);
};
+// -------------------------------------------------------------------------------
/** 메인페이지 픽픽픽 스켈레톤 */
export const MainPickSkeleton = () => {
@@ -86,7 +163,7 @@ export const MainPickSkeletonList = ({ itemsInRows }: MainPickSkeletonListProps)
return (
<>
{Array.from({ length: itemsInRows }, (_, index) => (
-
+
))}
>
);
diff --git a/components/common/title/ArrowWithTitle.tsx b/components/common/title/ArrowWithTitle.tsx
index 81db1beb..2bd1dc2c 100644
--- a/components/common/title/ArrowWithTitle.tsx
+++ b/components/common/title/ArrowWithTitle.tsx
@@ -17,6 +17,7 @@ export const ArrowWithTitleVariants = cva(ARROW_TITLE_CLASSES, {
mainTitle: ['st2', 'text-gray200'],
similarPick: ['st2', 'text-white'],
defaultPick: ['p1', 'text-gray100'],
+ defaultPickV2: ['st2', 'text-white'],
},
},
});
@@ -26,8 +27,8 @@ interface ArrowWithTitleProps extends VariantProps
= ({
@@ -37,9 +38,10 @@ const ArrowWithTitle: FC = ({
routeURL,
className,
ArrowClassName,
+ iconSize = { width: 7, height: 14 },
}) => {
return (
-
+
{title}
@@ -51,8 +53,8 @@ const ArrowWithTitle: FC
= ({
diff --git a/components/features/main/dynamicPickComponent.tsx b/components/features/main/dynamicPickComponent.tsx
index 953b2b67..e2dc87ff 100644
--- a/components/features/main/dynamicPickComponent.tsx
+++ b/components/features/main/dynamicPickComponent.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import Link from 'next/link';
import { useInfinitePickData } from '@pages/pickpickpick/api/useInfinitePickData';
-import PickContainer from '@pages/pickpickpick/components/PickContainer';
+import PickContainerV2 from '@pages/pickpickpick/components/PickContainerV2';
import {
PICK_VIEW_SIZE,
MOBILE_MAIN_PICK_VIEW_SIZE,
@@ -26,7 +26,7 @@ export default function DynamicPickComponent() {
const getStatusComponent = () => {
switch (status) {
case 'pending':
- return
;
+ return
;
default:
return (
@@ -38,7 +38,7 @@ export default function DynamicPickComponent() {
{group?.data.content.map((data: PickDataProps) => (
-
+
))}
diff --git a/components/features/main/dynamicTechBlogComponent.tsx b/components/features/main/dynamicTechBlogComponent.tsx
index 2b3cc418..2cf95602 100644
--- a/components/features/main/dynamicTechBlogComponent.tsx
+++ b/components/features/main/dynamicTechBlogComponent.tsx
@@ -38,7 +38,7 @@ export default function DynamicTechBlogComponent({
const { techBlogData, isFetchingNextPage, hasNextPage, status, onIntersect } = data;
- const SCROLL_CLASS = isMobile ? '' : 'relative overflow-y-scroll scrollbar-hide max-h-[50rem]';
+ const SCROLL_CLASS = isMobile ? '' : 'relative overflow-y-scroll scrollbar-hide max-h-[47rem]';
useObserver({
target: bottomDiv,
diff --git a/components/features/techblog/BookmarkComponent.tsx b/components/features/techblog/BookmarkComponent.tsx
index 7e2d00e8..53f1ff23 100644
--- a/components/features/techblog/BookmarkComponent.tsx
+++ b/components/features/techblog/BookmarkComponent.tsx
@@ -28,7 +28,7 @@ export default function BookmarkComponent() {
) : (
-
+
)}
);
diff --git a/hooks/useVerticalStepLoop.ts b/hooks/useVerticalStepLoop.ts
new file mode 100644
index 00000000..919709a9
--- /dev/null
+++ b/hooks/useVerticalStepLoop.ts
@@ -0,0 +1,55 @@
+import { useAnimationControls } from 'framer-motion';
+
+import { useEffect, useRef, useState } from 'react';
+
+type Options = {
+ itemCount: number;
+ dwellMs: number;
+ slideMs: number;
+ reduceMotion: boolean | null;
+};
+
+export function useVerticalStepLoop({ itemCount, dwellMs, slideMs, reduceMotion }: Options) {
+ const controls = useAnimationControls();
+ const firstItemRef = useRef(null);
+ const [rowHeight, setRowHeight] = useState(0);
+
+ useEffect(() => {
+ if (firstItemRef.current) setRowHeight(firstItemRef.current.offsetHeight);
+ }, []);
+
+ useEffect(() => {
+ if (reduceMotion === true || rowHeight === 0) return;
+
+ let i = 0;
+ let mounted = true;
+ // 한 칸씩 위로 이동 후, 마지막(복제된 첫 항목)까지 도달하면 즉시 y=0으로 스냅하여
+ // 시각적 깜빡임 없이 처음 상태로 되돌립니다. (복제된 첫 항목과 실제 첫 항목은 동일 내용)
+ const tick = async () => {
+ try {
+ i += 1;
+ await controls.start({
+ y: -(i * rowHeight),
+ transition: { duration: slideMs / 1000, ease: 'easeInOut' },
+ });
+ if (!mounted) return;
+ if (i === itemCount) {
+ // set()은 마운트 이후에만 안전합니다. 언마운트/미바인딩 크래시 방지를 위해
+ // start() + duration: 0으로 즉시 스냅 처리합니다.
+ await controls.start({ y: 0, transition: { duration: 0 } });
+ i = 0;
+ }
+ } catch (e) {
+ // 언마운트 타이밍 등으로 발생 가능한 경합 에러 무시
+ }
+ };
+
+ const id = setInterval(tick, dwellMs + slideMs);
+ return () => {
+ mounted = false;
+ clearInterval(id);
+ };
+ }, [controls, reduceMotion, rowHeight, itemCount, dwellMs, slideMs]);
+
+ return { controls, firstItemRef } as const;
+}
diff --git a/pages/main/index.page.tsx b/pages/main/index.page.tsx
index 0dc662c4..df9c75a5 100644
--- a/pages/main/index.page.tsx
+++ b/pages/main/index.page.tsx
@@ -55,7 +55,6 @@ export default function Index() {
variant='mainTitle'
iconText='바로가기'
routeURL={PICK_PATH}
- className='pb-[2.45rem]'
/>
(
@@ -98,4 +97,4 @@ export function getStaticProps() {
meta: META.MAIN,
},
};
-}
\ No newline at end of file
+}
diff --git a/pages/myinfo/mywriting/mypick/components/MyPickStatusComponent.tsx b/pages/myinfo/mywriting/mypick/components/MyPickStatusComponent.tsx
index aec1ea97..bd43023a 100644
--- a/pages/myinfo/mywriting/mypick/components/MyPickStatusComponent.tsx
+++ b/pages/myinfo/mywriting/mypick/components/MyPickStatusComponent.tsx
@@ -9,8 +9,8 @@ import { PickDataProps } from '@pages/pickpickpick/types/pick';
import { useObserver } from '@hooks/useObserver';
import {
- MobilePickSkeletonList,
- MyPickSkeletonList,
+ MobilePickSkeletonListV1,
+ MyPickSkeletonListV1,
} from '@components/common/skeleton/pickSkeleton';
import { ROUTES } from '@/constants/routes';
@@ -38,9 +38,9 @@ export default function MyPickStatusComponent() {
return (
<>
{isMobile ? (
-
+
) : (
-
+
)}
>
);
@@ -77,9 +77,9 @@ export default function MyPickStatusComponent() {
{isFetchingNextPage && hasNextPage && (
{isMobile ? (
-
+
) : (
-
+
)}
)}
diff --git a/pages/pickpickpick/[id]/apiHooks/comment/usePostPickComment.tsx b/pages/pickpickpick/[id]/apiHooks/comment/usePostPickComment.tsx
index 53235294..e5d767c9 100644
--- a/pages/pickpickpick/[id]/apiHooks/comment/usePostPickComment.tsx
+++ b/pages/pickpickpick/[id]/apiHooks/comment/usePostPickComment.tsx
@@ -24,7 +24,7 @@ const postPickComment = async ({ pickId, contents, isPickVotePublic }: PostPickC
return res.data;
};
-export const usePostPickComment = () => {
+export const usePostPickComment = ({ pickId }: { pickId: string }) => {
const queryClient = useQueryClient();
const { setToastVisible } = useToastVisibleStore();
@@ -33,6 +33,7 @@ export const usePostPickComment = () => {
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['pickCommentData'] });
await queryClient.invalidateQueries({ queryKey: ['getBestComments'] });
+ await queryClient.invalidateQueries({ queryKey: ['getDetailPickData', pickId] });
setToastVisible({ message: '댓글을 성공적으로 작성했어요!', type: 'success' });
},
onError: (error: ErrorRespone) => {
diff --git a/pages/pickpickpick/[id]/apiHooks/usePickDetailData.ts b/pages/pickpickpick/[id]/apiHooks/usePickDetailData.ts
index d3802e61..0dcdfdc4 100644
--- a/pages/pickpickpick/[id]/apiHooks/usePickDetailData.ts
+++ b/pages/pickpickpick/[id]/apiHooks/usePickDetailData.ts
@@ -5,7 +5,7 @@ import { UseQueryResult, useQuery } from '@tanstack/react-query';
import { PickDetailData } from '../types/pickDetailData';
export const getPickDetailData = async (pickId: string) => {
- const res = await axios.get(`/devdevdev/api/v1/picks/${pickId}`);
+ const res = await axios.get(`/devdevdev/api/v2/picks/${pickId}`);
return res;
};
diff --git a/pages/pickpickpick/[id]/components/Comment.tsx b/pages/pickpickpick/[id]/components/Comment.tsx
index c179848f..03519bb4 100644
--- a/pages/pickpickpick/[id]/components/Comment.tsx
+++ b/pages/pickpickpick/[id]/components/Comment.tsx
@@ -272,7 +272,8 @@ export default function Comment({