From b73ac88be1b72b750642f3f458c878f71aa7cc7d Mon Sep 17 00:00:00 2001 From: kanglocal Date: Wed, 18 Sep 2024 20:55:27 +0900 Subject: [PATCH 01/13] =?UTF-8?q?feature:=20=EC=83=89=EC=83=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/theme.css.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/styles/theme.css.ts b/src/styles/theme.css.ts index 16da28c0..0ef41b73 100644 --- a/src/styles/theme.css.ts +++ b/src/styles/theme.css.ts @@ -4,6 +4,7 @@ export const vars = createThemeContract({ color: { white: 'color-white', lightblue: 'color-lightblue', + lightblue2: 'color-lightblue2', skyblue: 'color-skyblue', blue: 'color-blue', deepblue8: 'colpor-deepblue8', @@ -31,6 +32,7 @@ createGlobalTheme(':root', vars, { color: { white: '#FFFFFF', lightblue: '#E5EEFE', + lightblue2: '#E3EEFF', skyblue: '#C5DFFF', blue: '#3D95FF', deepblue8: '#6A7DA1', From 9498c9b68251af393c3908bc2a8e434db32f1ef7 Mon Sep 17 00:00:00 2001 From: kanglocal Date: Wed, 18 Sep 2024 20:55:43 +0900 Subject: [PATCH 02/13] =?UTF-8?q?fix:=20dropdown=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SelectComponent/SelectComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SelectComponent/SelectComponent.tsx b/src/components/SelectComponent/SelectComponent.tsx index a45d182a..34d16fe1 100644 --- a/src/components/SelectComponent/SelectComponent.tsx +++ b/src/components/SelectComponent/SelectComponent.tsx @@ -18,7 +18,7 @@ const selectStyles = { control: (provided: object, state: { isFocused: boolean }) => ({ ...provided, maxWidth: '320px', - backgroundColor: 'white', + backgroundColor: 'transparent', boxShadow: 'none', border: 0, borderRadius: '8px', From 3193acbf85dd2641de365fd532bbb318c21a1177 Mon Sep 17 00:00:00 2001 From: kanglocal Date: Wed, 18 Sep 2024 20:56:27 +0900 Subject: [PATCH 03/13] =?UTF-8?q?feature:=20BackIcon=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/icons/new/back_icon.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 public/icons/new/back_icon.svg diff --git a/public/icons/new/back_icon.svg b/public/icons/new/back_icon.svg new file mode 100644 index 00000000..914a9994 --- /dev/null +++ b/public/icons/new/back_icon.svg @@ -0,0 +1,3 @@ + + + From 4a6e5175d50663586b252e18729464b1abb128f2 Mon Sep 17 00:00:00 2001 From: kanglocal Date: Tue, 29 Oct 2024 15:13:40 +0900 Subject: [PATCH 04/13] =?UTF-8?q?Feat:=20=EA=B2=80=EC=83=89=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20UI=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/search/Search.css.ts | 21 +- .../search/_components/CategoryArea.css.ts | 231 ++++++++++-------- src/app/search/_components/CategoryArea.tsx | 37 +-- src/app/search/_components/Header.css.ts | 35 +++ src/app/search/_components/Header.tsx | 32 +++ src/app/search/_components/KeywordArea.css.ts | 6 +- .../search/_components/NoDataContainer.css.ts | 41 ++++ .../search/_components/NoDataContainer.tsx | 27 ++ .../_components/SearchListResult.css.ts | 33 ++- .../search/_components/SearchListResult.tsx | 39 +-- src/app/search/_components/SearchResult.tsx | 7 +- .../_components/SearchUserProfile.css.ts | 57 ++++- .../search/_components/SearchUserProfile.tsx | 16 +- .../_components/SearchUserResult.css.ts | 26 +- .../search/_components/SearchUserResult.tsx | 23 +- src/app/search/_components/Top3Card.css.ts | 223 +++++++++-------- src/app/search/_components/Top3Card.tsx | 78 ++---- src/app/search/locale.ts | 2 +- src/app/search/page.tsx | 31 +-- .../ShapeSimpleList/ShapeSimpleList.css.ts | 2 + 20 files changed, 603 insertions(+), 364 deletions(-) create mode 100644 src/app/search/_components/Header.css.ts create mode 100644 src/app/search/_components/Header.tsx create mode 100644 src/app/search/_components/NoDataContainer.css.ts create mode 100644 src/app/search/_components/NoDataContainer.tsx diff --git a/src/app/search/Search.css.ts b/src/app/search/Search.css.ts index c924ae1f..027a809e 100644 --- a/src/app/search/Search.css.ts +++ b/src/app/search/Search.css.ts @@ -2,12 +2,22 @@ import { style } from '@vanilla-extract/css'; export const container = style({ width: '100%', - // padding: '1.6rem', + height: '100%', + minHeight: '100vh', + + display: 'flex', + alignItems: 'flex-start', + + background: '#F5F6FA', +}); + +export const contents = style({ + width: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center', - gap: '3rem', + gap: '1.7rem', }); export const searchArea = style({ @@ -21,11 +31,12 @@ export const searchArea = style({ export const keywordWrapper = style({ width: '100%', - padding: '1.6rem 1.6rem 0 1.6rem', + padding: '0 1.6rem', display: 'flex', flexDirection: 'row', alignItems: 'center', + justifyContent: 'center', gap: '1.3rem', // 검색창이 화면밖으로 나오는 이슈로 추가 @@ -44,8 +55,8 @@ export const logoWrapper = style({ }); export const backButton = style({ - width: '16px', - height: '28px', + width: '24px', + height: '24px', display: 'flex', justifyContent: 'center', diff --git a/src/app/search/_components/CategoryArea.css.ts b/src/app/search/_components/CategoryArea.css.ts index 2b4208bb..a2dcb6c3 100644 --- a/src/app/search/_components/CategoryArea.css.ts +++ b/src/app/search/_components/CategoryArea.css.ts @@ -1,5 +1,5 @@ import { keyframes, style } from '@vanilla-extract/css'; -import { vars } from '@/styles/__theme.css'; +import { vars } from '@/styles/theme.css'; export const categoryWrapper = style({ paddingLeft: '1.6rem', @@ -7,6 +7,7 @@ export const categoryWrapper = style({ position: 'relative', display: 'flex', + flexWrap: 'wrap', gap: '1rem', overflow: 'auto', @@ -17,15 +18,29 @@ export const categoryWrapper = style({ }); export const category = style({ - position: 'relative', - - display: 'flex', - flexDirection: 'column', - alignItems: 'center', + // position: 'relative', + + // display: 'flex', + // flexDirection: 'column', + // alignItems: 'center', + // justifyContent: 'center', + // gap: '1.5rem', + padding: '6px 12px', justifyContent: 'center', - gap: '1.5rem', + alignItems: 'center', + gap: '8px', + borderRadius: '20px', + background: vars.color.white, + color: vars.color.bluegray8, cursor: 'pointer', + + selectors: { + '&.selected': { + background: vars.color.lightblue2, + color: vars.color.blue, + }, + }, }); export const skeletonCategory = style([ @@ -35,103 +50,109 @@ export const skeletonCategory = style([ }, ]); -export const categoryImage = style({ - width: '6rem', - height: '6rem', - - borderRadius: '8px', - - ':hover': { filter: 'opacity(50%)' }, -}); - -export const selectedCategoryImage = style([categoryImage, { filter: 'opacity(50%)' }]); - -export const categoryText = style({}); - -const slide = keyframes({ - '0%': { - transform: 'translateY(10px)', - }, - '50%': { - transform: 'translateY(-10px)', - }, - '100%': { - transform: 'translateY(0)', - }, -}); - -export const selectedIconWrapper = style({ - width: '2.5rem', - height: '2.5rem', - - position: 'absolute', - top: '2rem', - zIndex: '2', - - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - - backgroundColor: 'rgba(0,0,0, 0.5)', - borderRadius: '20px', - - animation: `${slide} 0.2s ease-in-out`, -}); - -const slideInRight = keyframes({ - '0%': { - opacity: 0, - right: '-100%', - }, - '100%': { - opacity: 1, - right: '10px', - }, -}); - -const moveLeftRight = keyframes({ - '0%': { - right: '10px', - }, - '25%': { - right: '0', - }, - '50%': { - right: '10px', - }, - '75%': { - right: '0', - }, - '100%': { - right: '10px', - }, -}); - -const slideOutRight = keyframes({ - '0%': { - right: '10px', - }, - '100%': { - right: '-100%', - display: 'none', - }, -}); - -export const scrollMessage = style({ - position: 'absolute', - top: '25%', - right: '-10px', - - color: vars.color.white, - - borderRadius: '5px', - animationName: `${slideInRight}, ${moveLeftRight}, ${slideOutRight}`, - animationDuration: '3s, 4s, 1s', - animationTimingFunction: 'ease-in-out, linear, ease-in-out', - animationFillMode: 'forwards', - animationDelay: '0s, 0s, 4s', +// export const categoryImage = style({ +// width: '6rem', +// height: '6rem', +// +// borderRadius: '8px', +// +// ':hover': { filter: 'opacity(50%)' }, +// }); + +// export const selectedCategoryImage = style([categoryImage, { filter: 'opacity(50%)' }]); + +export const categoryText = style({ + fontSize: '1.4rem', + fontStyle: 'normal', + fontWeight: 500, + lineHeight: 'normal', + letterSpacing: '-0.42px', }); -export const selectedIcon = style({ - fill: vars.color.white, -}); +// const slide = keyframes({ +// '0%': { +// transform: 'translateY(10px)', +// }, +// '50%': { +// transform: 'translateY(-10px)', +// }, +// '100%': { +// transform: 'translateY(0)', +// }, +// }); +// +// export const selectedIconWrapper = style({ +// width: '2.5rem', +// height: '2.5rem', +// +// position: 'absolute', +// top: '2rem', +// zIndex: '2', +// +// display: 'flex', +// alignItems: 'center', +// justifyContent: 'center', +// +// backgroundColor: 'rgba(0,0,0, 0.5)', +// borderRadius: '20px', +// +// animation: `${slide} 0.2s ease-in-out`, +// }); +// +// const slideInRight = keyframes({ +// '0%': { +// opacity: 0, +// right: '-100%', +// }, +// '100%': { +// opacity: 1, +// right: '10px', +// }, +// }); +// +// const moveLeftRight = keyframes({ +// '0%': { +// right: '10px', +// }, +// '25%': { +// right: '0', +// }, +// '50%': { +// right: '10px', +// }, +// '75%': { +// right: '0', +// }, +// '100%': { +// right: '10px', +// }, +// }); +// +// const slideOutRight = keyframes({ +// '0%': { +// right: '10px', +// }, +// '100%': { +// right: '-100%', +// display: 'none', +// }, +// }); + +// export const scrollMessage = style({ +// position: 'absolute', +// top: '25%', +// right: '-10px', +// +// color: vars.color.white, +// +// borderRadius: '5px', +// animationName: `${slideInRight}, ${moveLeftRight}, ${slideOutRight}`, +// animationDuration: '3s, 4s, 1s', +// animationTimingFunction: 'ease-in-out, linear, ease-in-out', +// animationFillMode: 'forwards', +// animationDelay: '0s, 0s, 4s', +// }); +// +// export const selectedIcon = style({ +// fill: vars.color.white, +// }); diff --git a/src/app/search/_components/CategoryArea.tsx b/src/app/search/_components/CategoryArea.tsx index ce49b658..b094d38a 100644 --- a/src/app/search/_components/CategoryArea.tsx +++ b/src/app/search/_components/CategoryArea.tsx @@ -32,26 +32,31 @@ function CategoryArea({ onClick }: { onClick: MouseEventHandler }) { ? Array.from({ length: 6 }).map((_, index) => ) : data && data.map((category) => ( -
- {category.korName} +
+ {/**/}
{language === 'ko' ? category.korName : category.engName}
- {categoryValue === category.engName && ( -
- -
- )} + {/*{categoryValue === category.engName && (*/} + {/*
*/} + {/* */} + {/*
*/} + {/*)}*/}
))} {/*
{searchLocale[language].rightScrollMessage}
*/} -
- -
+ {/*
*/} + {/* */} + {/*
*/}
); } diff --git a/src/app/search/_components/Header.css.ts b/src/app/search/_components/Header.css.ts new file mode 100644 index 00000000..a6783589 --- /dev/null +++ b/src/app/search/_components/Header.css.ts @@ -0,0 +1,35 @@ +import { style } from '@vanilla-extract/css'; +import { Subtitle } from '@/styles/font.css'; + +export const container = style({ + width: '100%', +}); +export const contents = style({ + width: '100%', + padding: '20px 10px 10px 10px', + + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + + position: 'relative', +}); +export const backButtonWrapper = style({ + position: 'absolute', + left: '6px', + padding: '10px', + + cursor: 'pointer', +}); +export const backButtonText = style({ + fontSize: '1.6rem', + fontWeight: 400, + lineHeight: '1.6rem', + letterSpacing: '-0.48px', +}); +export const title = style([ + Subtitle, + { + letterSpacing: '-0.36px', + }, +]); diff --git a/src/app/search/_components/Header.tsx b/src/app/search/_components/Header.tsx new file mode 100644 index 00000000..19b4ecfa --- /dev/null +++ b/src/app/search/_components/Header.tsx @@ -0,0 +1,32 @@ +import * as styles from './Header.css'; +import { useRouter } from 'next/navigation'; + +type HeaderpropsType = { + title: string; + canGoBack?: boolean; +}; + +const Header = ({ title, canGoBack = false }: HeaderpropsType) => { + const router = useRouter(); + + const handleBackClick = () => { + router.push('/'); + }; + + return ( +
+
+ {canGoBack && ( +
+
+ 뒤로가기 +
+
+ )} +
{title}
+
+
+ ); +}; + +export default Header; diff --git a/src/app/search/_components/KeywordArea.css.ts b/src/app/search/_components/KeywordArea.css.ts index 24f24941..0be82775 100644 --- a/src/app/search/_components/KeywordArea.css.ts +++ b/src/app/search/_components/KeywordArea.css.ts @@ -10,8 +10,8 @@ export const keywordWrapper = style({ flexDirection: 'column', gap: '1.6rem', - backgroundColor: vars.color.gray5, - borderRadius: '50px', + backgroundColor: vars.color.white, + borderRadius: '12px', }); const moveInputRight = keyframes({ @@ -35,7 +35,7 @@ const moveIconLeft = keyframes({ }); export const keywordInput = style({ - padding: '0.5rem 1.5rem 0.5rem 4rem', + padding: '8px 16px', backgroundColor: 'transparent', diff --git a/src/app/search/_components/NoDataContainer.css.ts b/src/app/search/_components/NoDataContainer.css.ts new file mode 100644 index 00000000..cb8bed6c --- /dev/null +++ b/src/app/search/_components/NoDataContainer.css.ts @@ -0,0 +1,41 @@ +import { style } from '@vanilla-extract/css'; +import { BodyBold } from '@/styles/font.css'; +import { vars } from '@/styles/theme.css'; + +export const container = style({ + width: '100%', +}); + +export const contents = style({ + width: '100%', + padding: '74px', + + display: 'flex', + flexDirection: 'column', + gap: '1rem', + alignItems: 'center', + justifyContent: 'center', + + background: vars.color.white, + borderRadius: '20px', +}); + +export const text = style([ + BodyBold, + { + letterSpacing: '-0.48px', + color: vars.color.deepblue10, + }, +]); + +export const button = style({ + padding: '7px 14px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + + borderRadius: '20px', + border: `1px solid rgba(61, 149, 255, 0.30);`, + background: vars.color.white, + color: vars.color.blue, +}); diff --git a/src/app/search/_components/NoDataContainer.tsx b/src/app/search/_components/NoDataContainer.tsx new file mode 100644 index 00000000..8b831675 --- /dev/null +++ b/src/app/search/_components/NoDataContainer.tsx @@ -0,0 +1,27 @@ +'use client'; + +import * as styles from './NoDataContainer.css'; +import Link from 'next/link'; +import { useSearchParams } from 'next/navigation'; + +const NoDataContainer = ({ type, category }: { type: 'list' | 'lister'; category?: string }) => { + const searchParams = useSearchParams(); + const keyword = searchParams?.get('keyword'); + + return ( +
+
+ + {type === 'list' ? '일치하는 리스트가 없어요' : '일치하는 리스터가 없어요'} 💦 + + {category !== 'entire' && type === 'list' && ( + + 전체 카테고리에서 검색하기 + + )} +
+
+ ); +}; + +export default NoDataContainer; diff --git a/src/app/search/_components/SearchListResult.css.ts b/src/app/search/_components/SearchListResult.css.ts index 86d47674..5302f069 100644 --- a/src/app/search/_components/SearchListResult.css.ts +++ b/src/app/search/_components/SearchListResult.css.ts @@ -1,26 +1,41 @@ -import { style } from '@vanilla-extract/css'; +import { globalStyle, style } from '@vanilla-extract/css'; +import { vars } from '@/styles/theme.css'; export const container = style({ paddingBottom: '9rem', + display: 'flex', + flexDirection: 'column', }); export const header = style({ display: 'flex', + alignItems: 'center', justifyContent: 'space-between', + gap: '12px', + + color: vars.color.deepblue10, +}); + +export const countWrapper = style({ + display: 'flex', alignItems: 'center', + gap: '12px', }); export const countText = style({ - minWidth: '5rem', - width: '20rem', + fontWeight: '400', + fontSize: '1.6rem', + letterSpacing: '-0.48px', +}); +export const titleText = style({ fontWeight: '600', - fontSize: '18px', + fontSize: '1.8rem', + letterSpacing: '-0.54px', }); export const sort = style({ width: '12rem', - marginBottom: '1.2rem', display: 'flex', justifyContent: 'flex-end', @@ -40,9 +55,9 @@ export const cards = style({ width: '100%', display: 'grid', - gridTemplateColumns: 'repeat(2, 49%)', + gridTemplateColumns: 'repeat(2, 48%)', gridAutoRows: 'auto', - gap: '1.6rem 0.8rem', + gap: '1.6rem 1.5rem', '@media': { 'screen and (max-width: 380px)': { @@ -50,3 +65,7 @@ export const cards = style({ }, }, }); + +globalStyle(`${cards} > *:nth-child(odd)`, { + justifySelf: 'end', +}); diff --git a/src/app/search/_components/SearchListResult.tsx b/src/app/search/_components/SearchListResult.tsx index 557c548e..a7f01107 100644 --- a/src/app/search/_components/SearchListResult.tsx +++ b/src/app/search/_components/SearchListResult.tsx @@ -12,10 +12,10 @@ import Top3Card from '@/app/search/_components/Top3Card'; import Top3CardSkeleton from '@/app/search/_components/Top3CardSkeleton'; import SelectComponent from '@/components/SelectComponent/SelectComponent'; import getSearchListResult from '@/app/_api/search/getSearchListResult'; -import NoData from '@/app/search/_components/NoData'; import makeSearhUrl from '@/app/search/util/makeSearchUrl'; import { searchLocale } from '@/app/search/locale'; import { useLanguage } from '@/store/useLanguage'; +import NoDataContainer from '@/app/search/_components/NoDataContainer'; interface OptionsProps { value: string; @@ -113,21 +113,11 @@ function SearchListResult() { }, [keyword]); const Result = () => { - const { language } = useLanguage(); - return ( -
-
-
{`${searchLocale[language].listCountFirst} ${result.totalCount} ${searchLocale[language].listCountLast}`}
- -
-
-
- {result?.resultList?.map((list: SearchListType) => )} - {isFetchingNextPage && result?.resultList?.map((_, index) => )} -
+
+
+ {result?.resultList?.map((list: SearchListType) => )} + {isFetchingNextPage && result?.resultList?.map((_, index) => )}
); @@ -143,11 +133,22 @@ function SearchListResult() { ))}
- ) : result.totalCount > 0 ? ( // 데이터가 있는 경우 - ) : ( - // 데이터가 없는 경우 - +
+
+
+

{searchLocale[language].listCountFirst}

+
{`${result.totalCount} ${searchLocale[language].listCountLast}`}
+
+ +
+ {result.totalCount > 0 ? ( // 데이터가 있는 경우 + + ) : ( + // 데이터가 없는 경우 + + )} +
)} {hasNextPage &&
} diff --git a/src/app/search/_components/SearchResult.tsx b/src/app/search/_components/SearchResult.tsx index a9c7f160..21aeaa19 100644 --- a/src/app/search/_components/SearchResult.tsx +++ b/src/app/search/_components/SearchResult.tsx @@ -3,11 +3,16 @@ import * as styles from './SearchResult.css'; import SearchUserResult from '@/app/search/_components/SearchUserResult'; import SearchListResult from '@/app/search/_components/SearchListResult'; +import { useSearchParams } from 'next/navigation'; function SearchResult() { + const searchParams = useSearchParams(); + const keyword = searchParams?.get('keyword'); + const category = searchParams?.get('category'); + return (
- + {category === 'entire' && keyword && }
); diff --git a/src/app/search/_components/SearchUserProfile.css.ts b/src/app/search/_components/SearchUserProfile.css.ts index f1cc64a0..e4a0aaee 100644 --- a/src/app/search/_components/SearchUserProfile.css.ts +++ b/src/app/search/_components/SearchUserProfile.css.ts @@ -1,19 +1,19 @@ import { style } from '@vanilla-extract/css'; -import { vars } from '@/styles/__theme.css'; +import { vars } from '@/styles/theme.css'; export const container = style({ + // width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', - cursor: 'pointer', gap: '1rem', - ':hover': { - transform: 'translateY(-10px)', - transition: 'transform 0.3s ease', - }, + // ':hover': { + // transform: 'translateY(-10px)', + // transition: 'transform 0.3s ease', + // }, }); export const skeletonContainer = style([ @@ -28,21 +28,54 @@ export const skeletonContainer = style([ ]); export const profileImageWrapper = style({ - width: '4.2rem', - height: '4.2rem', + width: '12.2rem', + height: '12.2rem', - border: `1px solid ${vars.color.gray5}`, - borderRadius: '50px', + border: `1px solid ${vars.color.gray}`, + borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', + + cursor: 'pointer', }); export const nicknameText = style({ - width: '6rem', + width: '100%', - fontSize: '1.1rem', + fontSize: '1.4rem', fontWeight: '500', textAlign: 'center', + + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + + cursor: 'pointer', }); + +const commonFollowButton = style({ + padding: '4px 6px', + borderRadius: '20px', + + fontSize: '1.2rem', + fontWeight: 400, + textAlign: 'center', + + cursor: 'pointer', +}); +export const followButton = style([ + commonFollowButton, + { + background: vars.color.white, + color: vars.color.bluegray8, + }, +]); +export const followingButton = style([ + commonFollowButton, + { + background: vars.color.blue, + color: vars.color.white, + }, +]); diff --git a/src/app/search/_components/SearchUserProfile.tsx b/src/app/search/_components/SearchUserProfile.tsx index ee61cfcf..8b99dd99 100644 --- a/src/app/search/_components/SearchUserProfile.tsx +++ b/src/app/search/_components/SearchUserProfile.tsx @@ -11,17 +11,25 @@ interface UserListProps { } function SearchUserProfile({ user }: { user: UserListProps }) { + const isFollowing = true; // TODO: API 호출로 isFollowing 값 받아오기 const router = useRouter(); const handleProfileClick = () => { router.push(`/user/${user.id}/mylist`); }; return ( -
-
- +
+
+
-
{user.nickname}
+
+ {user.nickname} +
+ {isFollowing ? ( +
팔로잉
+ ) : ( +
팔로우
+ )}
); } diff --git a/src/app/search/_components/SearchUserResult.css.ts b/src/app/search/_components/SearchUserResult.css.ts index 12e78e95..ef9c923d 100644 --- a/src/app/search/_components/SearchUserResult.css.ts +++ b/src/app/search/_components/SearchUserResult.css.ts @@ -1,23 +1,37 @@ import { style } from '@vanilla-extract/css'; +import { vars } from '@/styles/theme.css'; + +export const container = style({ + display: 'flex', + flexDirection: 'column', + gap: '12px', +}); export const header = style({ display: 'flex', - justifyContent: 'space-between', alignItems: 'center', -}); + gap: '12px', -export const countText = style({ - width: '20rem', + color: vars.color.deepblue10, +}); +export const titleText = style({ fontWeight: '600', - fontSize: '18px', + fontSize: '1.8rem', + letterSpacing: '-0.54px', +}); + +export const countText = style({ + fontWeight: '400', + fontSize: '1.6rem', + letterSpacing: '-0.48px', }); export const userProfiles = style({ padding: '1.5rem 0', display: 'flex', - gap: '1rem', + gap: '3rem', alignItems: 'flex-start', overflow: 'auto', diff --git a/src/app/search/_components/SearchUserResult.tsx b/src/app/search/_components/SearchUserResult.tsx index 77f70586..f18eb334 100644 --- a/src/app/search/_components/SearchUserResult.tsx +++ b/src/app/search/_components/SearchUserResult.tsx @@ -11,6 +11,7 @@ import getSearchUserResult from '@/app/_api/search/getSearchUserResult'; import SearchUserProfileSkeleton from '@/app/search/_components/SearchUserProfileSkeleton'; import { searchLocale } from '@/app/search/locale'; import { useLanguage } from '@/store/useLanguage'; +import NoDataContainer from '@/app/search/_components/NoDataContainer'; function SearchUserResult() { const { language } = useLanguage(); @@ -39,19 +40,21 @@ function SearchUserResult() {
) : ( - searchUserData?.users && - searchUserData?.users.length > 0 && ( - <> -
-
{`${searchLocale[language].userCountFirst} ${searchUserData?.totalCount} ${searchLocale[language].userCountLast}`}
-
+
+
+

{searchLocale[language].userCountFirst}

+
{`${searchUserData?.totalCount} ${searchLocale[language].userCountLast}`}
+
+ {searchUserData?.users && searchUserData?.users.length > 0 ? (
{searchUserData?.users.map((user) => )}
- - ) + ) : ( + + )} +
)}
); diff --git a/src/app/search/_components/Top3Card.css.ts b/src/app/search/_components/Top3Card.css.ts index 9ceea20e..cd7e0a00 100644 --- a/src/app/search/_components/Top3Card.css.ts +++ b/src/app/search/_components/Top3Card.css.ts @@ -1,138 +1,161 @@ -import { style, createVar } from '@vanilla-extract/css'; +import { style, createVar, styleVariants, globalStyle } from '@vanilla-extract/css'; import { vars } from '@/styles/__theme.css'; +import { Label, LabelSmall } from '@/styles/font.css'; -export const listColor = createVar(); -export const listBackgroundImage = createVar(); - -export const container = style({ - minWidth: '17rem', +export const imageUrl = createVar(); +const content = style({ + width: 173, + height: 173, display: 'flex', flexDirection: 'column', - justifyContent: 'flex-start', + justifyContent: 'center', alignItems: 'center', -}); - -export const card = style({ - width: '100%', - - display: 'flex', - flexDirection: 'column', - rowGap: '1rem', - columnGap: '1.6rem', -}); - -export const listWrapper = style({ - width: '100%', - height: '26rem', - padding: '3rem 1.8rem', position: 'relative', - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - - border: `1px solid ${vars.color.gray5}`, - borderRadius: '10px', - - backgroundColor: listColor, - backgroundImage: `linear-gradient(to top, rgba(0, 0, 0, 0.5), transparent), ${listBackgroundImage}`, - backgroundSize: 'cover', + backgroundImage: imageUrl, backgroundPosition: 'center', - cursor: 'pointer', + backgroundColor: vars.color.white, - ':hover': { - boxShadow: 'rgba(0, 0, 0, 0.3) 3px 3px 2px;', - borderWidth: '1.5px', - }, + zIndex: 2, }); -export const skeletonListWrapper = style([ - listWrapper, - { - cursor: 'default', - ':hover': { - boxShadow: 'none', - borderWidth: '1px', +export const contentVariant = styleVariants({ + round: [ + content, + { + borderRadius: '100%', + '::before': { + content: '""', + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + zIndex: -10, + borderRadius: '50%', + }, }, + ], + square: [content, { borderRadius: 20 }], +}); + +export const category = style([ + LabelSmall, + { + width: 'fit-content', + padding: '2px 6px', + backgroundColor: vars.color.blue, + borderRadius: 20, + color: vars.color.white, }, ]); -export const userProfiles = style({ - width: '80%', - - position: 'absolute', - bottom: '1rem', - +export const info = style({ + width: '100%', + paddingTop: '0.6rem', + paddingBottom: '0.5rem', display: 'flex', + flexDirection: 'column', alignItems: 'center', - gap: '.8rem', -}); - -export const userImageWrapper = style({ - width: '3rem', - height: '3rem', + gap: 4, - border: `1px solid ${vars.color.gray5}`, - borderRadius: '50px', + textAlign: 'center', +}); - flexShrink: 0, +// 배경이미지 유무에 따른 스타일 variants +const fontColor = { + white: vars.color.white, + black: vars.color.black, +}; +const textOneLine = style({ + width: '100%', + whiteSpace: 'nowrap', overflow: 'hidden', + textOverflow: 'ellipsis', }); +export const title = styleVariants(fontColor, (color) => [ + Label, + textOneLine, + { + color, + fontWeight: 600, + }, +]); -export const userImage = style({ - width: '100%', - height: '100%', +export const owner = styleVariants(fontColor, (color) => [ + LabelSmall, + textOneLine, + { + color, + fontWeight: 400, + // textAlign: 'center', + }, +]); - borderRadius: '50px', - backgroundColor: vars.color.gray7, - objectFit: 'cover', -}); +export const items = style([ + { + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: 5, + }, +]); -export const userTextWrapper = style({ - width: '100%', +const item = style([ + { + width: 'fit-content', + maxWidth: '100%', + padding: '0.45rem 0.62rem', + borderRadius: 18, + display: 'flex', + gap: 2, + alignItems: 'center', + }, + textOneLine, +]); - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', +globalStyle(`${item} span`, { + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', }); -export const nameText = style({ - width: '100%', - - fontSize: '1.2rem', - fontWeight: '400', - color: vars.color.white, - - wordWrap: 'break-word', +export const itemVariant = styleVariants({ + white: [ + item, + { + backgroundColor: '#F5FAFF', + color: vars.color.blue, + }, + ], + blue: [ + item, + { + backgroundColor: 'rgba(245, 250, 255, 0.30)', + color: vars.color.white, + }, + ], }); -export const updatedDateText = style({ - fontSize: '1.1rem', - color: vars.color.white, -}); +export const date = styleVariants(fontColor, (color) => ({ + paddingTop: '0.8rem', + fontSize: '0.9rem', + color, +})); -export const title = style({ - fontSize: '1.8rem', - fontWeight: '600', - color: 'var(--text-text-grey-dark, #202020)', - textAlign: 'left', - letterSpacing: '0.14px', - wordWrap: 'break-word', -}); +export const itemWrapper = style({ + width: '100%', + padding: '32px', -export const list = style({ - padding: '1rem 0', + position: 'relative', display: 'flex', flexDirection: 'column', - gap: '.8rem', - - fontSize: '1.2rem', - fontWeight: '400', - color: vars.color.white, - lineHeight: '2.5rem', - letterSpacing: '-0.36px', + justifyContent: 'center', + alignItems: 'center', }); diff --git a/src/app/search/_components/Top3Card.tsx b/src/app/search/_components/Top3Card.tsx index 1a27f1bb..82403d44 100644 --- a/src/app/search/_components/Top3Card.tsx +++ b/src/app/search/_components/Top3Card.tsx @@ -1,70 +1,46 @@ -import Image from 'next/image'; import { useMemo } from 'react'; -import { useRouter } from 'next/navigation'; import { assignInlineVars } from '@vanilla-extract/dynamic'; import * as styles from './Top3Card.css'; import { SearchListType } from '@/lib/types/listType'; import formatDate from '@/lib/utils/dateFormat'; -import Top3CardItem from './Top3CardItem'; -import DefaultProfile from '/public/icons/default_profile.svg'; -import { searchLocale } from '@/app/search/locale'; -import { useLanguage } from '@/store/useLanguage'; -import { BACKGROUND_COLOR_READ } from '@/styles/Color'; +import Link from 'next/link'; export default function Top3Card({ list }: { list: SearchListType }) { - const { language } = useLanguage(); - const router = useRouter(); - const handleCardClick = () => { - router.push(`/list/${list.id}`); - }; - const Top3CardComponent = useMemo(() => { + const hasImage = !!list.representImageUrl; return ( -
-
-
-
    -
      - {list.items.map((item, index) => { - if (index > 2) return; - return ; - })} -
    -
- -
-
- {list.ownerProfileImageUrl ? ( - {searchLocale[language].profileImageAlt} - ) : ( - - )} -
-
-
{list.ownerNickname}
-
{formatDate(list.updatedDate)}
-
-
+
+ {/* TODO: category를 추가로 받아야함. 백엔드에 요청필요*/} + {/*
{list.category}
*/} +
음악
+
+

{list.title}

+

{list.ownerNickname}

-

{list.title}

+
    + {list.items.map((item, index) => ( +
  • + + {index + 1} + {`.`} + + {item.title} +
  • + ))} +
+

{formatDate(list.updatedDate)}

-
+ ); - }, [list, handleCardClick]); + }, [list]); return Top3CardComponent; } diff --git a/src/app/search/locale.ts b/src/app/search/locale.ts index 99dc1ea7..4eb2a304 100644 --- a/src/app/search/locale.ts +++ b/src/app/search/locale.ts @@ -5,7 +5,7 @@ export const searchLocale: Record = { goToExplore: '다른 리스트 보러가기', listCountFirst: '리스트', listCountLast: '건', - userCountFirst: '사용자', + userCountFirst: '리스터', userCountLast: '건', profileImageAlt: '프로필 이미지', backButtonAlt: '뒤로 가기 버튼', diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index c7b35639..be671bcf 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -4,32 +4,21 @@ import CategoryArea from '@/app/search/_components/CategoryArea'; import SearchResult from '@/app/search/_components/SearchResult'; import * as styles from './Search.css'; import KeywordArea from '@/app/search/_components/KeywordArea'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { useEffect, useState, KeyboardEvent, MouseEvent, ChangeEvent } from 'react'; -import BackButton from '/public/icons/back.svg'; +import { useRouter } from 'next/navigation'; +import { useState, KeyboardEvent, MouseEvent, ChangeEvent } from 'react'; import PlusButton from '@/components/floatingButton/PlusOptionFloatingButton'; import ArrowUpButton from '@/components/floatingButton/ArrowUpFloatingButton'; import FloatingContainer from '@/components/floatingButton/FloatingContainer'; import makeSearchUrl from '@/app/search/util/makeSearchUrl'; -import { searchLocale } from '@/app/search/locale'; -import { useLanguage } from '@/store/useLanguage'; +import Header from '@/app/search/_components/Header'; export default function Search() { - const { language } = useLanguage(); const router = useRouter(); - const searchParams = useSearchParams(); const [keyword, setKeyword] = useState(''); const [category, setCategory] = useState(''); const [sort, setSort] = useState(''); - // useEffect(() => { - // // 페이지 첫 로드시 검색어와 카테고리 설정 - // setKeyword(searchParams?.get('keyword') ?? ''); - // setCategory(searchParams?.get('category') ?? 'entire'); - // setSort(searchParams?.get('sort') ?? 'new'); - // }, [searchParams]); - const handleKeywordChange = (e: ChangeEvent) => { setKeyword(e.target.value); }; @@ -50,18 +39,12 @@ export default function Search() { router.push(makeSearchUrl({ keyword, category: newCategory, sort })); }; - const handleBackClick = () => { - router.push('/'); - }; - return ( - <> -
+
+
+
-
@@ -73,6 +56,6 @@ export default function Search() {
- +
); } diff --git a/src/components/ShapeSimpleList/ShapeSimpleList.css.ts b/src/components/ShapeSimpleList/ShapeSimpleList.css.ts index 1ad25384..26c75cdb 100644 --- a/src/components/ShapeSimpleList/ShapeSimpleList.css.ts +++ b/src/components/ShapeSimpleList/ShapeSimpleList.css.ts @@ -18,6 +18,8 @@ export const container = style({ const content = style({ width: 173, height: 173, + padding: 10, + display: 'flex', flexDirection: 'column', justifyContent: 'center', From 9c45fdb1e9ebdf9b95ebb052f57820fe10bf3a71 Mon Sep 17 00:00:00 2001 From: kanglocal Date: Tue, 4 Feb 2025 02:00:17 +0900 Subject: [PATCH 05/13] =?UTF-8?q?feature:=20API=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/icons/new/history.svg | 3 + public/icons/new/vertical_kebab_button.svg | 12 ++ public/icons/ver3/bookmark.svg | 10 + public/icons/ver3/detail_share.svg | 10 + public/icons/ver3/lock.svg | 3 + public/icons/ver3/more.svg | 14 ++ public/icons/ver3/reaction_agree.svg | 9 + public/icons/ver3/reaction_good.svg | 9 + public/icons/ver3/reaction_thanks.svg | 9 + public/icons/ver3/share.svg | 4 +- public/icons/ver3/visibility.svg | 3 + src/app/_api/reaction/Reaction.ts | 8 + src/app/_api/search/getSearchListResult.ts | 6 +- src/app/_api/search/getSearchUserResult.ts | 9 +- .../ListDetailInner/CollectButton.css.ts | 8 + .../ListDetailInner/CollectButton.tsx | 19 +- .../_components/ListDetailInner/Footer.css.ts | 38 +++- .../_components/ListDetailInner/Footer.tsx | 136 +++++++++++- .../_components/ListDetailInner/Header.css.ts | 14 +- .../_components/ListDetailInner/Header.tsx | 14 +- .../ListDetailInner/RankList.css.ts | 64 ++++-- .../_components/ListDetailInner/RankList.tsx | 25 +-- .../_components/ListDetailInner/index.css.ts | 1 - .../_components/ListDetailInner/index.tsx | 8 +- .../ListDetailOuter/Comments.css.ts | 53 ++++- .../_components/ListDetailOuter/Comments.tsx | 11 +- .../ListDetailOuter/HeaderRight.css.ts | 1 + .../ListDetailOuter/HeaderRight.tsx | 17 +- .../ListDetailOuter/ListInformation.css.ts | 132 ++++++++++-- .../ListDetailOuter/ListInformation.tsx | 200 +++++++++++++----- .../ListDetailOuter/OpenBottomSheetButton.tsx | 32 +-- src/app/list/[listId]/locale.ts | 13 +- .../_components/NotificationList.css.ts | 36 ++++ .../_components/NotificationList.tsx | 95 ++++++--- src/app/notification/locale.ts | 6 +- src/app/search/_components/CategoryArea.tsx | 6 +- src/app/search/_components/Header.css.ts | 35 --- src/app/search/_components/Header.tsx | 32 --- .../search/_components/NoDataContainer.tsx | 2 +- .../_components/SearchListResult.css.ts | 1 + .../search/_components/SearchListResult.tsx | 10 +- src/app/search/_components/SearchResult.tsx | 4 +- .../search/_components/SearchUserProfile.tsx | 27 ++- .../search/_components/SearchUserResult.tsx | 85 ++++++-- src/app/search/_components/Top3Card.css.ts | 4 +- src/app/search/_components/Top3Card.tsx | 4 +- src/app/search/locale.ts | 2 + src/app/search/page.tsx | 12 +- src/app/search/util/makeSearchUrl.ts | 6 +- src/components/BottomSheet/BottomSheet.css.ts | 8 +- src/components/BottomSheet/BottomSheet.tsx | 19 +- .../BottomSheet/ConfirmBottomSheet.tsx | 44 ++++ .../BottomSheet/confirmBottomSheet.css.ts | 78 +++++++ src/components/Header/Header.css.ts | 3 +- src/components/Label/Label.css.ts | 15 +- src/components/Label/Label.tsx | 8 +- src/components/LinkPreview/LinkPreview.css.ts | 12 ++ src/components/LinkPreview/LinkPreview.tsx | 19 +- src/components/NoData/NoData.css.ts | 3 + src/components/NoData/NoDataComponent.tsx | 4 +- .../SelectComponent/SelectComponent.tsx | 6 +- src/lib/constants/placeholder.ts | 2 +- src/lib/constants/queryKeys.ts | 3 + src/lib/types/listType.ts | 23 ++ src/lib/types/notificationType.ts | 59 +++++- src/lib/types/reactionType.ts | 1 + src/lib/utils/dateTimeFormat.ts | 11 + src/styles/theme.css.ts | 10 + 68 files changed, 1238 insertions(+), 352 deletions(-) create mode 100644 public/icons/new/history.svg create mode 100644 public/icons/new/vertical_kebab_button.svg create mode 100644 public/icons/ver3/bookmark.svg create mode 100644 public/icons/ver3/detail_share.svg create mode 100644 public/icons/ver3/lock.svg create mode 100644 public/icons/ver3/more.svg create mode 100644 public/icons/ver3/reaction_agree.svg create mode 100644 public/icons/ver3/reaction_good.svg create mode 100644 public/icons/ver3/reaction_thanks.svg create mode 100644 public/icons/ver3/visibility.svg create mode 100644 src/app/_api/reaction/Reaction.ts delete mode 100644 src/app/search/_components/Header.css.ts delete mode 100644 src/app/search/_components/Header.tsx create mode 100644 src/components/BottomSheet/ConfirmBottomSheet.tsx create mode 100644 src/components/BottomSheet/confirmBottomSheet.css.ts create mode 100644 src/lib/types/reactionType.ts create mode 100644 src/lib/utils/dateTimeFormat.ts diff --git a/public/icons/new/history.svg b/public/icons/new/history.svg new file mode 100644 index 00000000..8bde5006 --- /dev/null +++ b/public/icons/new/history.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/icons/new/vertical_kebab_button.svg b/public/icons/new/vertical_kebab_button.svg new file mode 100644 index 00000000..ac53dc62 --- /dev/null +++ b/public/icons/new/vertical_kebab_button.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/public/icons/ver3/bookmark.svg b/public/icons/ver3/bookmark.svg new file mode 100644 index 00000000..97d7a5c0 --- /dev/null +++ b/public/icons/ver3/bookmark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/ver3/detail_share.svg b/public/icons/ver3/detail_share.svg new file mode 100644 index 00000000..5ae52f15 --- /dev/null +++ b/public/icons/ver3/detail_share.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/ver3/lock.svg b/public/icons/ver3/lock.svg new file mode 100644 index 00000000..a1930d24 --- /dev/null +++ b/public/icons/ver3/lock.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/ver3/more.svg b/public/icons/ver3/more.svg new file mode 100644 index 00000000..35abcbc1 --- /dev/null +++ b/public/icons/ver3/more.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/icons/ver3/reaction_agree.svg b/public/icons/ver3/reaction_agree.svg new file mode 100644 index 00000000..7e2c2a9c --- /dev/null +++ b/public/icons/ver3/reaction_agree.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/ver3/reaction_good.svg b/public/icons/ver3/reaction_good.svg new file mode 100644 index 00000000..645d1daf --- /dev/null +++ b/public/icons/ver3/reaction_good.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/ver3/reaction_thanks.svg b/public/icons/ver3/reaction_thanks.svg new file mode 100644 index 00000000..4ccc66f4 --- /dev/null +++ b/public/icons/ver3/reaction_thanks.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/ver3/share.svg b/public/icons/ver3/share.svg index eb4d401e..2f20fb26 100644 --- a/public/icons/ver3/share.svg +++ b/public/icons/ver3/share.svg @@ -1,3 +1,3 @@ - - + + diff --git a/public/icons/ver3/visibility.svg b/public/icons/ver3/visibility.svg new file mode 100644 index 00000000..8ef339ac --- /dev/null +++ b/public/icons/ver3/visibility.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/_api/reaction/Reaction.ts b/src/app/_api/reaction/Reaction.ts new file mode 100644 index 00000000..c0b19f9f --- /dev/null +++ b/src/app/_api/reaction/Reaction.ts @@ -0,0 +1,8 @@ +import axiosInstance from '@/lib/axios/axiosInstance'; +import { ReactionType } from '@/lib/types/reactionType'; + +const reaction = async (listId: number, type: ReactionType) => { + return await axiosInstance.post(`/lists/${listId}/reaction`, { reaction: type }); +}; + +export default reaction; diff --git a/src/app/_api/search/getSearchListResult.ts b/src/app/_api/search/getSearchListResult.ts index 8dda406e..4694ee53 100644 --- a/src/app/_api/search/getSearchListResult.ts +++ b/src/app/_api/search/getSearchListResult.ts @@ -4,10 +4,10 @@ interface GetSearchListResultType { cursorId: number | undefined | null; sort: string; keyword: string; - category: string; + categoryCode: string; } -async function getSearchListResult({ sort, keyword, category, cursorId }: GetSearchListResultType) { +async function getSearchListResult({ sort, keyword, categoryCode, cursorId }: GetSearchListResultType) { const params = new URLSearchParams({ size: '6', }); @@ -17,7 +17,7 @@ async function getSearchListResult({ sort, keyword, category, cursorId }: GetSea } const response = await axiosInstance.get( - `/lists/search?keyword=${keyword}&sort=${sort}&category=${category}&${params.toString()}` + `/lists/search?keyword=${keyword}&sort=${sort}&categoryCode=${categoryCode}&${params.toString()}` ); return response.data; diff --git a/src/app/_api/search/getSearchUserResult.ts b/src/app/_api/search/getSearchUserResult.ts index bd0e593f..603d98b7 100644 --- a/src/app/_api/search/getSearchUserResult.ts +++ b/src/app/_api/search/getSearchUserResult.ts @@ -1,14 +1,21 @@ import axiosInstance from '@/lib/axios/axiosInstance'; interface GetSearchUserResultType { + // cursorId: number | undefined | null; + page: number | undefined | null; keyword: string; } -async function getSearchUserResult({ keyword }: GetSearchUserResultType) { +async function getSearchUserResult({ keyword, page }: GetSearchUserResultType) { + console.log('page:::', page); const params = new URLSearchParams({ size: '3', }); + if (page) { + params.append('page', page.toString()); + } + const response = await axiosInstance.get(`/users?search=${keyword}&${params.toString()}`); return response.data; diff --git a/src/app/list/[listId]/_components/ListDetailInner/CollectButton.css.ts b/src/app/list/[listId]/_components/ListDetailInner/CollectButton.css.ts index 9878d52c..f2ef5af4 100644 --- a/src/app/list/[listId]/_components/ListDetailInner/CollectButton.css.ts +++ b/src/app/list/[listId]/_components/ListDetailInner/CollectButton.css.ts @@ -5,6 +5,7 @@ export const myCollectWrapper = style({ flexDirection: 'column', justifyContent: 'center', alignItems: 'center', + gap: '6px', }); export const collectWrapper = style([ @@ -13,3 +14,10 @@ export const collectWrapper = style([ cursor: 'pointer', }, ]); + +export const collectTextWrapper = style({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', +}); diff --git a/src/app/list/[listId]/_components/ListDetailInner/CollectButton.tsx b/src/app/list/[listId]/_components/ListDetailInner/CollectButton.tsx index d5453896..32709b4e 100644 --- a/src/app/list/[listId]/_components/ListDetailInner/CollectButton.tsx +++ b/src/app/list/[listId]/_components/ListDetailInner/CollectButton.tsx @@ -8,8 +8,8 @@ import { useUser } from '@/store/useUser'; import numberFormatter from '@/lib/utils/numberFormatter'; import collectList from '@/app/_api/collect/__collectList'; import toasting from '@/lib/utils/toasting'; -import CollectIcon from '/public/icons/collect.svg'; -import CollectedIcon from '/public/icons/collected.svg'; +import BookmarkIcon from '/public/icons/ver3/bookmark.svg'; +import BookmarkedIcon from '/public/icons/collected.svg'; import Modal from '@/components/Modal/Modal'; import LoginModal from '@/components/login/LoginModal'; import useBooleanOutput from '@/hooks/useBooleanOutput'; @@ -74,7 +74,7 @@ const CollectButton = ({ data }: { data: CollectProps }) => { return ( <>
- +
{isOn && ( @@ -89,15 +89,22 @@ const CollectButton = ({ data }: { data: CollectProps }) => { if (loginUser?.id === data.ownerId) { return (
- -
{numberFormatter(data.collectCount, 'ko') ?? 0}
+ + +
+

콜렉트

+ {loginUser?.id === data.ownerId &&

({numberFormatter(data.collectCount, 'ko') ?? 0})

} +
); } return (
- {data.isCollected ? : } + {data.isCollected ? : } +
+

콜렉트

+
); }; diff --git a/src/app/list/[listId]/_components/ListDetailInner/Footer.css.ts b/src/app/list/[listId]/_components/ListDetailInner/Footer.css.ts index 9a9bf820..ea95b225 100644 --- a/src/app/list/[listId]/_components/ListDetailInner/Footer.css.ts +++ b/src/app/list/[listId]/_components/ListDetailInner/Footer.css.ts @@ -1,4 +1,5 @@ import { style } from '@vanilla-extract/css'; +import { vars } from '@/styles/theme.css'; export const container = style({ width: '100%', @@ -7,10 +8,11 @@ export const container = style({ justifyContent: 'space-between', alignItems: 'center', - padding: '1rem 4rem 2rem 4rem', + padding: '1rem 0', }); export const collectAndView = style({ + width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center', @@ -23,12 +25,17 @@ export const shareAndOthers = style({ display: 'flex', flexDirection: 'row', justifyContent: 'right', - alignItems: 'center', + alignItems: 'flex-start', gap: '20px', }); export const buttonComponent = style({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', cursor: 'pointer', + gap: '6px', }); // TODO: 조회수 증가 기능이 완료되면 display: 'flex' 로 수정 예정 @@ -41,3 +48,30 @@ export const viewCountWrapper = style({ cursor: 'pointer', }); + +export const reactionContainer = style({ + padding: '8px 12px', + display: 'flex', + gap: '8px', + background: vars.color.bggray, + borderRadius: '16px', +}); + +export const reactionText = style({ + minWidth: '40px', + textAlign: 'center', +}); + +export const reactionIcon = style({ + transition: 'filter 0.3s ease', +}); + +export const reactionIconInactive = style({ + filter: 'grayscale(100%)', +}); + +export const reactionIconHover = style({ + ':hover': { + filter: 'grayscale(0%)', + }, +}); diff --git a/src/app/list/[listId]/_components/ListDetailInner/Footer.tsx b/src/app/list/[listId]/_components/ListDetailInner/Footer.tsx index 76f20fcb..ec776445 100644 --- a/src/app/list/[listId]/_components/ListDetailInner/Footer.tsx +++ b/src/app/list/[listId]/_components/ListDetailInner/Footer.tsx @@ -1,13 +1,13 @@ 'use client'; -import { MouseEvent, useState } from 'react'; +import { MouseEvent, useMemo, useState } from 'react'; import { usePathname, useRouter } from 'next/navigation'; import Script from 'next/script'; import * as styles from './Footer.css'; import { useUser } from '@/store/useUser'; import { useLanguage } from '@/store/useLanguage'; -import { ItemType } from '@/lib/types/listType'; +import { ItemType, Reaction } from '@/lib/types/listType'; import { UserProfileType } from '@/lib/types/userProfileType'; import toasting from '@/lib/utils/toasting'; import BottomSheet from '@/components/BottomSheet/BottomSheet'; @@ -15,12 +15,20 @@ import ModalPortal from '@/components/modal-portal'; import { listLocale } from '@/app/list/[listId]/locale'; import CollectButton from '@/app/list/[listId]/_components/ListDetailInner/CollectButton'; import getBottomSheetOptionList from '@/app/list/[listId]/_components/ListDetailInner/getBottomSheetOptionList'; -import ShareIcon from '/public/icons/share.svg'; -import EtcIcon from '/public/icons/etc.svg'; +import ShareIcon from '/public/icons/ver3/share.svg'; +import MoreIcon from '/public/icons/ver3/more.svg'; import EyeIcon from '/public/icons/eye.svg'; import Modal from '@/components/Modal/Modal'; import LoginModal from '@/components/login/LoginModal'; import useBooleanOutput from '@/hooks/useBooleanOutput'; +import ReactionAgreeIcon from '/public/icons/ver3/reaction_agree.svg'; +import ReactionGoodIcon from '/public/icons/ver3/reaction_good.svg'; +import ReactionThanksIcon from '/public/icons/ver3/reaction_thanks.svg'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { QUERY_KEYS } from '@/lib/constants/queryKeys'; +import { AxiosError } from 'axios'; +import reaction from '@/app/_api/reaction/Reaction'; +import { ReactionType } from '@/lib/types/reactionType'; interface BottomSheetOptionsProps { key: string; @@ -41,6 +49,7 @@ interface FooterProps { viewCount: number; collectCount: number; isPublic: boolean; + reactions: Reaction[]; } declare global { @@ -49,6 +58,32 @@ declare global { } } +const ReactionIcon = ({ type, ...props }: { type: ReactionType } & React.SVGProps) => { + switch (type) { + case 'COOL': + return ; + case 'AGREE': + return ; + case 'THANKS': + return ; + default: + return null; + } +}; + +const getReactionText = (type: ReactionType, language: 'ko' | 'en') => { + switch (type) { + case 'COOL': + return language === 'ko' ? '멋져요' : 'Cool'; + case 'AGREE': + return language === 'ko' ? '동의해요' : 'Agree'; + case 'THANKS': + return language === 'ko' ? '고마워요' : 'Thanks'; + default: + return ''; + } +}; + function Footer({ data }: { data: FooterProps }) { const { language } = useLanguage(); const router = useRouter(); @@ -58,6 +93,8 @@ function Footer({ data }: { data: FooterProps }) { const [isSheetActive, setSheetActive] = useState(false); const [sheetOptionList, setSheetOptionList] = useState([]); const listUrl = `https://listywave.com${path}`; + const queryClient = useQueryClient(); + // const [localReactions, setLocalReactions] = useState(data.reactions); function kakaoInit() { if (!window.Kakao.isInitialized()) { @@ -68,6 +105,60 @@ function Footer({ data }: { data: FooterProps }) { let goToCreateList: () => void; + const reactions = useMemo(() => { + return { + COOL: data.reactions.find((r) => r.reaction === 'COOL') || { count: 0, isReacted: false }, + AGREE: data.reactions.find((r) => r.reaction === 'AGREE') || { count: 0, isReacted: false }, + THANKS: data.reactions.find((r) => r.reaction === 'THANKS') || { count: 0, isReacted: false }, + }; + }, [data.reactions]); + + // TODO: 현재 새로굄해야만 반영되는 버그 있음. 낙관적업데이트도 되지않고있음. 수정필요. + const reactMutation = useMutation({ + mutationKey: [QUERY_KEYS.reaction, data.listId], + mutationFn: ({ type }: { type: ReactionType }) => reaction(data.listId, type), + onMutate: async ({ type }) => { + await queryClient.cancelQueries({ queryKey: [QUERY_KEYS.getListDetail, data.listId] }); + const previousList = queryClient.getQueryData([QUERY_KEYS.getListDetail, data.listId]); + + if (!previousList) return { previousList: null }; + + const updatedReactions = previousList.reactions.map((reaction) => { + if (reaction.reaction === type) { + return { + ...reaction, + count: reaction.isReacted ? Math.max((reaction.count || 1) - 1, 0) : (reaction.count || 0) + 1, + isReacted: !reaction.isReacted, + }; + } + return reaction; + }); + + const updatedList = { + ...previousList, + reactions: updatedReactions, + }; + + queryClient.setQueryData([QUERY_KEYS.getListDetail, data.listId], updatedList); + + return { previousList }; + }, + onError: (error: AxiosError, variables, context) => { + if (error.response?.status === 401) { + handleSetOn(); + } + if (context?.previousList) { + queryClient.setQueryData([QUERY_KEYS.getListDetail, data.listId], context.previousList); + } + }, + onSettled: () => { + console.log('reaction settled'); + queryClient.invalidateQueries({ + queryKey: [QUERY_KEYS.getListDetail, data.listId], + }); + }, + }); + if (loginUser.id === null) { goToCreateList = () => { toasting({ type: 'default', txt: listLocale[language].loginRequired }); @@ -110,6 +201,14 @@ function Footer({ data }: { data: FooterProps }) { ); }; + const handleReaction = (type: 'COOL' | 'AGREE' | 'THANKS') => { + if (!loginUser.id) { + handleSetOn(); // 로그인 모달 표시 + return; + } + reactMutation.mutate({ type }); + }; + return ( <>