@@ -15,7 +15,7 @@ export const PickInfo = () => {
);
};
-export const MobilePickInfo = () => {
+export const MobilePickInfoV1 = () => {
return (
diff --git a/pages/pickpickpick/components/PickInfoV2.tsx b/pages/pickpickpick/components/PickInfoV2.tsx
new file mode 100644
index 00000000..fb3ffc80
--- /dev/null
+++ b/pages/pickpickpick/components/PickInfoV2.tsx
@@ -0,0 +1,115 @@
+'use client';
+
+import { motion, useReducedMotion } from 'framer-motion';
+
+import { useMemo } from 'react';
+
+import {
+ PICK_INFO_ITEMS,
+ PICK_INFO_DWELL_MS,
+ PICK_INFO_SLIDE_MS,
+} from '@pages/pickpickpick/[id]/constants/pickInfoConstants';
+
+import { useVerticalStepLoop } from '@hooks/useVerticalStepLoop';
+
+export const PickInfoV2 = () => {
+ // 루프 애니메이션을 자연스럽게 만들기 위해 첫 항목을 배열 끝에 한 번 더 붙입니다.
+ // [A, B, C] -> [A, B, C, A'] 로 만들어 C→A'도 '한 칸 위로' 슬라이드가 되게 합니다.
+ // 슬라이드 직후 y=0 으로 스냅(리셋)하므로 A'와 A가 동일해 화면상 깜빡임/중복 대기는 생기지 않습니다.
+ const loopItems = useMemo(() => [...PICK_INFO_ITEMS, PICK_INFO_ITEMS[0]], []);
+ const reduceMotion = useReducedMotion();
+
+ const dwell = PICK_INFO_DWELL_MS;
+ const slide = PICK_INFO_SLIDE_MS;
+
+ const { controls, firstItemRef } = useVerticalStepLoop({
+ itemCount: PICK_INFO_ITEMS.length,
+ dwellMs: dwell,
+ slideMs: slide,
+ reduceMotion,
+ });
+
+ return (
+
+
+ 개발고민 혼자 끙끙 앓지말고, 픽픽픽 💘에서 함께 나눠요!
+
+
+ {reduceMotion ? (
+
+ {PICK_INFO_ITEMS[0].icon}
+ {PICK_INFO_ITEMS[0].text}
+
+ ) : (
+
+ {loopItems.map((item, i) => (
+
+ {item.icon}
+ {item.text}
+
+ ))}
+
+ )}
+
+
+ );
+};
+
+export const MobilePickInfoV2 = () => {
+ // 데스크톱과 동일한 이유로 첫 항목을 끝에 복제하여 부드러운 루프를 구성합니다.
+ const loopItems = useMemo(() => [...PICK_INFO_ITEMS, PICK_INFO_ITEMS[0]], []);
+ const reduceMotion = useReducedMotion();
+
+ const dwell = PICK_INFO_DWELL_MS;
+ const slide = PICK_INFO_SLIDE_MS;
+
+ const { controls, firstItemRef } = useVerticalStepLoop({
+ itemCount: PICK_INFO_ITEMS.length,
+ dwellMs: dwell,
+ slideMs: slide,
+ reduceMotion,
+ });
+
+ return (
+
+
+ 개발고민 혼자 끙끙 앓지말고,
+
+ 픽픽픽 💘에서 함께 나눠요!
+
+
+ {reduceMotion ? (
+
+ {PICK_INFO_ITEMS[0].icon}
+ {PICK_INFO_ITEMS[0].text}
+
+ ) : (
+
+ {loopItems.map((item, i) => (
+
+ {item.icon}
+ {item.text}
+
+ ))}
+
+ )}
+
+
+ );
+};
diff --git a/pages/pickpickpick/components/PickWriteButton.tsx b/pages/pickpickpick/components/PickWriteButton.tsx
new file mode 100644
index 00000000..9d451c3b
--- /dev/null
+++ b/pages/pickpickpick/components/PickWriteButton.tsx
@@ -0,0 +1,83 @@
+import Link from 'next/link';
+
+import { useLoginStatusStore } from '@stores/loginStore';
+import { useLoginModalStore } from '@stores/modalStore';
+
+import { MainButtonV2 } from '@components/common/buttons/mainButtonsV2';
+import MobileMainButton from '@components/common/buttons/mobileMainButton';
+
+import { ROUTES } from '@/constants/routes';
+import { useMediaQueryContext } from '@/contexts/MediaQueryContext';
+
+// 픽픽픽 작성하기 버튼 컴포넌트 모바일,웹
+
+export const MobileWriteButton = () => {
+ const { isMobile } = useMediaQueryContext();
+ const { loginStatus } = useLoginStatusStore();
+ const { openLoginModal, setDescription } = useLoginModalStore();
+ const { POSTING } = ROUTES.PICKPICKPICK;
+
+ const handleWriteClick = () => {
+ openLoginModal();
+ setDescription('댑댑이가 되면 픽픽픽을 작성할 수 있어요 🥳');
+ };
+
+ if (!isMobile) return null;
+
+ return (
+ <>
+ {loginStatus === 'login' ? (
+
+
+
+ ) : (
+
+ )}
+ >
+ );
+};
+
+export const WebWriteButton = ({ disabled = false }: { disabled?: boolean }) => {
+ const { loginStatus } = useLoginStatusStore();
+ const { openLoginModal, setDescription } = useLoginModalStore();
+ const { POSTING } = ROUTES.PICKPICKPICK;
+ const { isMobile } = useMediaQueryContext();
+
+ const handleWriteClick = () => {
+ openLoginModal();
+ setDescription('댑댑이가 되면 픽픽픽을 작성할 수 있어요 🥳');
+ };
+
+ return (
+ <>
+ {!isMobile && (
+
+ {loginStatus === 'login' && !disabled ? (
+
+
+
+ ) : (
+
+ )}
+
+ )}
+ >
+ );
+};
diff --git a/pages/pickpickpick/constants/pickConstants.ts b/pages/pickpickpick/constants/pickConstants.ts
index 9159a460..0b7d2c7a 100644
--- a/pages/pickpickpick/constants/pickConstants.ts
+++ b/pages/pickpickpick/constants/pickConstants.ts
@@ -2,3 +2,7 @@ export const PICK_VIEW_SIZE = 9;
export const MOBILE_MAIN_PICK_VIEW_SIZE = 3;
export const PICK_COMMENT_VIEW_SIZE = 10;
+
+// 픽픽픽 검색시 최소 입력 글자 수
+export const PICK_SEARCH_MIN_LENGTH = 1;
+export const isPickSearchEnabled = (term: string) => term.trim().length >= PICK_SEARCH_MIN_LENGTH;
diff --git a/pages/pickpickpick/index.page.tsx b/pages/pickpickpick/index.page.tsx
index 7ffc280d..eaa547f9 100644
--- a/pages/pickpickpick/index.page.tsx
+++ b/pages/pickpickpick/index.page.tsx
@@ -1,161 +1,132 @@
-import React, { useRef } from 'react';
+import React, { useRef, useState } from 'react';
import dynamic from 'next/dynamic';
-import Image from 'next/image';
import Link from 'next/link';
-import { useInfinitePickData } from '@pages/pickpickpick/api/useInfinitePickData';
+import { useUnifiedPickFeed } from '@pages/pickpickpick/api/useUnifiedPickFeed';
+import SearchNotFound from '@pages/techblog/components/searchNotFound';
-import { useLoginStatusStore } from '@stores/loginStore';
import { useLoginModalStore } from '@stores/modalStore';
import { useObserver } from '@hooks/useObserver';
-import { MainButton } from '@components/common/buttons/mainButtons';
-import MobileMainButton from '@components/common/buttons/mobileMainButton';
-import { Dropdown } from '@components/common/dropdowns/dropdown';
-import MobileDropdown from '@components/common/dropdowns/mobileDropdown';
import { LoginModal } from '@components/common/modals/modal';
-import { MobilePickSkeletonList, PickSkeletonList } from '@components/common/skeleton/pickSkeleton';
-
-import IconPencil from '@public/image/pencil-alt.svg';
+import {
+ MobilePickSkeletonListV2,
+ PickSkeletonListV2,
+} from '@components/common/skeleton/pickSkeleton';
import { META } from '@/constants/metaData';
import { ROUTES } from '@/constants/routes';
import { useMediaQueryContext } from '@/contexts/MediaQueryContext';
import { PickDropdownProps, usePickDropdownStore } from '@/stores/dropdownStore';
-import { MobilePickInfo, PickInfo } from './components/PickInfo';
+import { PickSearchInput } from './[id]/components/pickSearchInput';
+import { PickActionSection } from './components/PickActionSection';
+import { PickHeader } from './components/PickHeader';
+import { MobilePickInfoV2, PickInfoV2 } from './components/PickInfoV2';
+import { MobileWriteButton } from './components/PickWriteButton';
import { PickDataProps } from './types/pick';
-const DynamicComponent = dynamic(() => import('@/pages/pickpickpick/components/PickContainer'));
+const DynamicComponent = dynamic(() => import('@/pages/pickpickpick/components/PickContainerV2'));
export default function Index() {
const bottom = useRef(null);
+ const [editingKeyword, setEditingKeyword] = useState('');
+ const [submittedKeyword, setSubmittedKeyword] = useState('');
- const { MAIN, POSTING } = ROUTES.PICKPICKPICK;
-
- const { loginStatus } = useLoginStatusStore();
- const { openLoginModal, isLoginModalOpen, setDescription } = useLoginModalStore();
+ const { MAIN } = ROUTES.PICKPICKPICK;
+ const { isLoginModalOpen } = useLoginModalStore();
const { sortOption } = usePickDropdownStore();
const { isMobile } = useMediaQueryContext();
- const { pickData, isFetchingNextPage, hasNextPage, status, onIntersect } = useInfinitePickData(
- sortOption as PickDropdownProps,
- );
+ const { pages, status, isFetchingNextPage, hasNextPage, onIntersect, totalCount, isSearchMode } =
+ useUnifiedPickFeed(sortOption as PickDropdownProps, submittedKeyword);
+
+ useObserver({ target: bottom, onIntersect });
+
+ const renderContent = () => (
+ <>
+
setSubmittedKeyword(value)}
+ onClear={() => setSubmittedKeyword('')}
+ />
+
+
+ {totalCount === 0 && (
+
+ )}
+
+
+ {pages?.map((group, index) => (
+
+ {group?.data.content.map((data: PickDataProps) => (
+
+
+
+ ))}
+
+ ))}
+
- useObserver({
- target: bottom,
- onIntersect,
- });
+ {isFetchingNextPage && hasNextPage && (
+
+ {isMobile ? (
+
+ ) : (
+
+ )}
+
+ )}
+ >
+ );
const getStatusComponent = () => {
switch (status) {
case 'pending':
return (
<>
+ setSubmittedKeyword(value)}
+ onClear={() => setSubmittedKeyword('')}
+ disabled
+ />
+
+
{isMobile ? (
-
+
) : (
-
+
)}
>
);
-
default:
- return (
- <>
-
- {isMobile ?
:
}
- {isMobile ? (
-
-
-
- ) : (
- <>>
- )}
-
- {pickData?.pages.map((group, index) => (
-
- {group?.data.content.map((data: PickDataProps) => (
-
-
-
- ))}
-
- ))}
-
-
- {isFetchingNextPage && hasNextPage && (
-
- {isMobile ? (
-
- ) : (
-
- )}
-
- )}
- >
- );
+ return renderContent();
}
};
return (
-
-
- 픽픽픽 💘
-
-
- {!isMobile && (
-
-
-
- {loginStatus === 'login' ? (
-
- }
- type='button'
- />
-
- ) : (
- }
- onClick={() => {
- openLoginModal();
- setDescription('댑댑이가 되면 픽픽픽을 작성할 수 있어요 🥳');
- }}
- type='button'
- />
- )}
-
- )}
-
+
{
+ setEditingKeyword('');
+ setSubmittedKeyword('');
+ }}
+ />
+ {isMobile ? : }
{getStatusComponent()}
- {isMobile &&
- (loginStatus === 'login' ? (
-
-
-
- ) : (
- {
- openLoginModal();
- setDescription('댑댑이가 되면 픽픽픽을 작성할 수 있어요 🥳');
- }}
- />
- ))}
- {isLoginModalOpen && loginStatus !== 'login' && }
+
+ {isLoginModalOpen && }
);
}
@@ -166,4 +137,4 @@ export function getStaticProps() {
meta: META.PICK,
},
};
-}
\ No newline at end of file
+}
diff --git a/pages/pickpickpick/types/pick.ts b/pages/pickpickpick/types/pick.ts
index c2242dec..836cd8f7 100644
--- a/pages/pickpickpick/types/pick.ts
+++ b/pages/pickpickpick/types/pick.ts
@@ -12,12 +12,16 @@ export interface PickDataProps {
id: number;
title: string;
isVoted?: boolean;
+ isNew: boolean; // v2 추가 필드
pickOptions: {
id: string;
title: string;
picked?: boolean | null;
percent?: number;
isPicked?: boolean;
+ // ---v2 추가 필드---
+ thumbnailImageUrl: string;
+ content: string;
}[];
voteTotalCount: number;
commentTotalCount: number;
diff --git a/pages/techblog/components/searchNotFound.tsx b/pages/techblog/components/searchNotFound.tsx
index 163c0bb1..1ea87b3d 100644
--- a/pages/techblog/components/searchNotFound.tsx
+++ b/pages/techblog/components/searchNotFound.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import Image from 'next/image';
-import { useCompanyInfoStore, useSearchKeywordStore } from '@stores/techBlogStore';
+import { useCompanyInfoStore } from '@stores/techBlogStore';
import { MainButton } from '@components/common/buttons/mainButtons';
@@ -13,12 +13,17 @@ import { useMediaQueryContext } from '@/contexts/MediaQueryContext';
type SearchNotFoundProps = {
type: 'company' | 'keyword';
+ searchKeyword: string;
+ setSearchKeyword: (keyword: string) => void;
};
-export default function SearchNotFound({ type }: SearchNotFoundProps) {
+export default function SearchNotFound({
+ type,
+ searchKeyword,
+ setSearchKeyword,
+}: SearchNotFoundProps) {
const { isMobile } = useMediaQueryContext();
const { companyName, resetCompanyInfo } = useCompanyInfoStore();
- const { searchKeyword, setSearchKeyword } = useSearchKeywordStore();
const KeyType = type.toUpperCase() as keyof typeof NO_TECHBLOG_DATA;
const handleOnClick = () => {
diff --git a/pages/techblog/index.page.tsx b/pages/techblog/index.page.tsx
index 05e00dcc..46ee9a5b 100644
--- a/pages/techblog/index.page.tsx
+++ b/pages/techblog/index.page.tsx
@@ -110,7 +110,13 @@ export default function Index() {
+ )}
>
);
}
@@ -150,7 +156,7 @@ export default function Index() {