Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6a7f51a
Style : 픽픽픽 메인 상단 설명글 UI 수정 (v2)
mandelina Oct 19, 2025
d05f235
Merge branch 'main' into DP-562
mandelina Oct 19, 2025
76df3ae
Style : 픽픽픽 메인 스켈레톤 컴포넌트 교체
mandelina Oct 19, 2025
f6b4089
Fix : 픽픽픽 메인 작성하기 버튼 UI 수정 , 컴포넌트 명 오타 수정
mandelina Oct 19, 2025
64c0360
Feat : 픽픽픽 메인 컴포넌트v2 추가
mandelina Oct 19, 2025
69d8ed9
Image : 테스트 이미지 (삭제필요)
mandelina Oct 19, 2025
8ac5a2c
Fix : 픽픽픽 메인에서 contents는 Parsing하지않고 텍스트로만 보여주도록 처리 (최대 6줄 말줄임)
mandelina Oct 26, 2025
b545cf1
Feat : 픽픽픽 메인 검색 input 생성
mandelina Oct 26, 2025
e795258
Refactor : 작성하기 , 드롭다운 위치변경 및 픽픽픽 메인 전체적인 컴포넌트 분리
mandelina Oct 26, 2025
27e56aa
Fix : 스켈레톤 컴포넌트 이름 변경에 따른 수정 (v1)
mandelina Oct 26, 2025
1f6d53c
Feat : v2 API 변경에 따른 데이터 구조 수정 및 필드 추가
mandelina Nov 2, 2025
6e6883c
Feat : 작성하기 버튼 컴포넌트 분리 , PickActionSection에 통합
mandelina Nov 2, 2025
6e5d54c
Feat : /main 페이지의 픽픽픽 컴포넌트 교체
mandelina Nov 2, 2025
0bcf056
Remove : 사용되지 않는 테스트 이미지 파일 삭제
mandelina Nov 2, 2025
890a1ca
Fix : PickCount 컴포넌트의 불필요한 주석 제거
mandelina Nov 2, 2025
13446c5
Feat : 픽픽픽 검색기능 api연동 , 데이터 뿌려주기 (스위칭)
mandelina Nov 9, 2025
9172e86
Feat : 검색 모드에 따라 드롭다운 표시 제어
mandelina Nov 9, 2025
e85ffbc
Fix : getPickSearchData 함수의 pageParam 처리 개선 및 초기 페이지 매개변수 수정
mandelina Nov 9, 2025
1553119
Fix : 픽픽픽메인 box 모바일일때 패딩값 수정
mandelina Nov 16, 2025
d6776a4
Feat : 픽픽픽 메인 문구 애니메이션 개발
mandelina Nov 16, 2025
8d5da48
Feat : 픽픽픽 검색어 없을때 SearchNotFound 컴포넌트 처리 추가
mandelina Nov 16, 2025
ef19c1e
Feat : 픽픽픽 헤더를 눌렀을때 초기화 되도록 처리
mandelina Nov 16, 2025
7af2132
Merge branch 'DP-568', remote-tracking branch 'origin' into DP-562
mandelina Nov 16, 2025
8d1dcab
Fix : pending상태일때도 픽픽픽메인에서 문구 , input , 버튼이 disabled 보이도록 수정
mandelina Nov 16, 2025
bf4a041
Refactor : 애니메이션 crash 나는 부분 보완코드 추가 (마운트 되기전 controls.set() 호출을 하지 …
mandelina Nov 16, 2025
024a019
refactor : 스크롤바 등장/제거에 따른 가로 시프트 방지 기능 추가
mandelina Nov 16, 2025
ad48679
fix: dropdown border 추가
minyoungg-kim Nov 16, 2025
2ce5b52
fix: 북마크 dropdown line 추가
minyoungg-kim Nov 16, 2025
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
103 changes: 90 additions & 13 deletions components/common/skeleton/pickSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { MobilePickInfo, PickInfo } from '@pages/pickpickpick/components/PickInfo';
import { MobilePickInfoV1, PickInfoV1 } from '@pages/pickpickpick/components/PickInfo';

export const PickSkeleton = () => {
import AngleRightIcon from '@components/svgs/AngleRightIcon';

// ------------------------------픽픽픽 메인 스켈레톤 v1------------------------------
export const PickSkeletonV1 = () => {
return (
<div className='px-[2.4rem] py-[3.2rem] flex flex-col gap-[3.2rem] rounded-[1.6rem] border-gray600 border-solid border'>
<div className='h-[3.7rem] w-[100%] rounded-[1.6rem] bg-gray600 relative overflow-hidden skeleton-item' />
Expand All @@ -13,56 +16,130 @@ 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 (
<div className='grid grid-cols-3 gap-8'>
{hasInfo ? (
<>
<PickInfo />
<PickInfoV1 />
{Array.from({ length: rows * itemsInRows - 1 }, (_, index) => (
<PickSkeleton key={index} />
<PickSkeletonV1 key={index} />
))}
</>
) : (
<>
{Array.from({ length: rows * itemsInRows }, (_, index) => (
<PickSkeleton key={index} />
<PickSkeletonV1 key={index} />
))}
</>
)}
</div>
);
};

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 (
<div className='grid grid-cols-1 gap-8'>
{hasInfo && <MobilePickInfo />}
{hasInfo && <MobilePickInfoV1 />}

{arr.map((_, index) => (
<PickSkeletonV1 key={index} />
))}
</div>
);
};

export const MyPickSkeletonListV1 = ({ rows, itemsInRows }: PickSkeletonListV1Props) => {
return (
<div className='grid grid-cols-2 gap-8'>
{Array.from({ length: rows * itemsInRows }, (_, index) => (
<PickSkeletonV1 key={index} />
))}
</div>
);
};
// -------------------------------------------------------------------------------

// ------------------------------픽픽픽 메인 스켈레톤 v2------------------------------

export const PickSkeletonV2 = () => {
return (
<div className='pt-[3.2rem] pb-[2.4rem] px-[3.2rem] flex flex-col gap-[3.2rem] rounded-[1.6rem] bg-gray600'>
<div className='flex flex-col gap-[2.4rem]'>
<div className='flex flex-row items-start justify-between gap-[1.6rem]'>
<div className='bg-black h-[4.3rem] w-[100%] rounded-[1.6rem] relative overflow-hidden skeleton-item' />
<AngleRightIcon color='var(--gray300)' />
</div>
<div className='bg-black h-[1rem] w-[40%] rounded-[1.6rem] relative overflow-hidden skeleton-item' />
</div>

<div className='flex flex-row gap-[1.6rem]'>
<div className='bg-black flex flex-col gap-[1rem] p-[2.4rem] w-[50%] rounded-[1.6rem] relative overflow-hidden border border-gray500'>
<div className='bg-gray500 h-[1rem] w-[50%] rounded-[1.6rem] relative overflow-hidden skeleton-item' />
<div className='bg-gray500 h-[5rem] w-[100%] rounded-[1.6rem] relative overflow-hidden skeleton-item' />
<div className='bg-gray500 h-[12rem] w-[100%] rounded-[1.6rem] relative overflow-hidden skeleton-item' />
</div>
<div className='bg-black flex flex-col gap-[1rem] p-[2.4rem] w-[50%] rounded-[1.6rem] relative overflow-hidden border border-gray500'>
<div className='bg-gray500 h-[1rem] w-[50%] rounded-[1.6rem] relative overflow-hidden skeleton-item' />
<div className='bg-gray500 h-[5rem] w-[100%] rounded-[1.6rem] relative overflow-hidden skeleton-item' />
<div className='bg-gray500 h-[12rem] w-[100%] rounded-[1.6rem] relative overflow-hidden skeleton-item' />
</div>
</div>
</div>
);
};

interface PickSkeletonListV2Props {
rows: number;
itemsInRows: number;
}

export const PickSkeletonListV2 = ({ rows, itemsInRows }: PickSkeletonListV2Props) => {
return (
<div className='grid grid-cols-2 gap-8'>
{Array.from({ length: rows * itemsInRows }, (_, index) => (
<PickSkeletonV2 key={index} />
))}
</div>
);
};

export const MobilePickSkeletonListV2 = ({ rows }: { rows: number }) => {
const arr = Array.from({ length: rows });

return (
<div className='grid grid-cols-1 gap-8'>
{arr.map((_, index) => (
<PickSkeleton key={index} />
<PickSkeletonV2 key={index} />
))}
</div>
);
};

export const MyPickSkeletonList = ({ rows, itemsInRows }: PickSkeletonListProps) => {
export const MyPickSkeletonListV2 = ({ rows, itemsInRows }: PickSkeletonListV2Props) => {
return (
<div className='grid grid-cols-2 gap-8'>
{Array.from({ length: rows * itemsInRows }, (_, index) => (
<PickSkeleton key={index} />
<PickSkeletonV2 key={index} />
))}
</div>
);
};
// -------------------------------------------------------------------------------

/** 메인페이지 픽픽픽 스켈레톤 */
export const MainPickSkeleton = () => {
Expand All @@ -86,7 +163,7 @@ export const MainPickSkeletonList = ({ itemsInRows }: MainPickSkeletonListProps)
return (
<>
{Array.from({ length: itemsInRows }, (_, index) => (
<MainPickSkeleton key={index} />
<PickSkeletonV2 key={index} />
))}
</>
);
Expand Down
8 changes: 5 additions & 3 deletions components/common/title/ArrowWithTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
},
},
});
Expand All @@ -26,8 +27,8 @@ interface ArrowWithTitleProps extends VariantProps<typeof ArrowWithTitleVariants
iconText?: string;
routeURL?: string;
className?: string;
iconSize?: string;
ArrowClassName?: string;
iconSize?: { width: number; height: number };
}

const ArrowWithTitle: FC<ArrowWithTitleProps> = ({
Expand All @@ -37,6 +38,7 @@ const ArrowWithTitle: FC<ArrowWithTitleProps> = ({
routeURL,
className,
ArrowClassName,
iconSize = { width: 7, height: 14 },
}) => {
return (
<div className='grid grid-flow-col items-baseline gap-6 justify-between'>
Expand All @@ -51,8 +53,8 @@ const ArrowWithTitle: FC<ArrowWithTitleProps> = ({
<Image
src={AngleRight}
alt={'오른쪽 화살표'}
width={7}
height={14}
width={iconSize.width}
height={iconSize.height}
className={ArrowClassName}
/>
</div>
Expand Down
6 changes: 3 additions & 3 deletions components/features/main/dynamicPickComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,7 +26,7 @@ export default function DynamicPickComponent() {
const getStatusComponent = () => {
switch (status) {
case 'pending':
return <MainPickSkeletonList itemsInRows={2} />;
return <MainPickSkeletonList itemsInRows={1} />;

default:
return (
Expand All @@ -38,7 +38,7 @@ export default function DynamicPickComponent() {
<div key={index}>
{group?.data.content.map((data: PickDataProps) => (
<Link href={`${ROUTES.PICKPICKPICK.MAIN}/${data.id}`} key={data.id}>
<PickContainer pickData={data} type='main' />
<PickContainerV2 pickData={data} />
</Link>
))}
</div>
Expand Down
2 changes: 1 addition & 1 deletion components/features/techblog/BookmarkComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default function BookmarkComponent() {
<MobileDropdown type='bookmark' />
</div>
) : (
<Dropdown type='bookmark' disable={!hasData} />
<Dropdown type='bookmark' disable={!hasData} line />
)}
</div>
);
Expand Down
55 changes: 55 additions & 0 deletions hooks/useVerticalStepLoop.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLParagraphElement | null>(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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -38,9 +38,9 @@ export default function MyPickStatusComponent() {
return (
<>
{isMobile ? (
<MobilePickSkeletonList rows={3} />
<MobilePickSkeletonListV1 rows={3} />
) : (
<MyPickSkeletonList rows={3} itemsInRows={2} />
<MyPickSkeletonListV1 rows={3} itemsInRows={2} />
)}
</>
);
Expand Down Expand Up @@ -77,9 +77,9 @@ export default function MyPickStatusComponent() {
{isFetchingNextPage && hasNextPage && (
<div className='mt-[2rem]'>
{isMobile ? (
<MobilePickSkeletonList rows={3} />
<MobilePickSkeletonListV1 rows={3} />
) : (
<MyPickSkeletonList rows={3} itemsInRows={2} />
<MyPickSkeletonListV1 rows={3} itemsInRows={2} />
)}
</div>
)}
Expand Down
Loading