From 27ab63cccdda4ec06bfdf972259224374405b328 Mon Sep 17 00:00:00 2001 From: Jiyoung Jung <72294509+Jungjjeong@users.noreply.github.com> Date: Thu, 29 Aug 2024 01:08:11 +0900 Subject: [PATCH 1/5] =?UTF-8?q?refactor:=20follow=20virtual=20list,=20memb?= =?UTF-8?q?er=20type=20=EA=B3=B5=ED=86=B5=EC=9C=BC=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/molecules/index.ts | 2 +- .../{profile-list-item => profile-list}/index.ts | 1 + .../profile-list-item.tsx | 12 ++++++------ .../molecules/profile-list/profile-list.tsx | 15 +++++++-------- features/follow/components/index.ts | 1 - features/follow/sections/follower-section.tsx | 5 +++-- features/follow/sections/following-section.tsx | 5 +++-- features/follow/types/index.ts | 10 ++-------- types/index.ts | 1 + types/member-type.ts | 6 ++++++ 10 files changed, 30 insertions(+), 28 deletions(-) rename components/molecules/{profile-list-item => profile-list}/index.ts (53%) rename components/molecules/{profile-list-item => profile-list}/profile-list-item.tsx (87%) rename features/follow/components/follow-virtual-list.tsx => components/molecules/profile-list/profile-list.tsx (70%) delete mode 100644 features/follow/components/index.ts create mode 100644 types/index.ts create mode 100644 types/member-type.ts diff --git a/components/molecules/index.ts b/components/molecules/index.ts index 5c483c5f..f1f1abe8 100644 --- a/components/molecules/index.ts +++ b/components/molecules/index.ts @@ -5,7 +5,7 @@ export * from './header-bar'; export * from './infinite-scroller'; export * from './modal'; export * from './page-modal'; -export * from './profile-list-item'; +export * from './profile-list'; export * from './record-mark'; export * from './search-bar'; export * from './tab'; diff --git a/components/molecules/profile-list-item/index.ts b/components/molecules/profile-list/index.ts similarity index 53% rename from components/molecules/profile-list-item/index.ts rename to components/molecules/profile-list/index.ts index 232a412e..a764c7f8 100644 --- a/components/molecules/profile-list-item/index.ts +++ b/components/molecules/profile-list/index.ts @@ -1 +1,2 @@ +export * from './profile-list'; export * from './profile-list-item'; diff --git a/components/molecules/profile-list-item/profile-list-item.tsx b/components/molecules/profile-list/profile-list-item.tsx similarity index 87% rename from components/molecules/profile-list-item/profile-list-item.tsx rename to components/molecules/profile-list/profile-list-item.tsx index 275e91f9..b2b20086 100644 --- a/components/molecules/profile-list-item/profile-list-item.tsx +++ b/components/molecules/profile-list/profile-list-item.tsx @@ -1,18 +1,18 @@ import Link from 'next/link'; import { Button, Image } from '@/components/atoms'; -import { ProfileFollowContent } from '@/features/follow'; import { css } from '@/styled-system/css'; import { flex } from '@/styled-system/patterns'; +import { MemberProfile } from '@/types'; type FollowListItem = { isFollow: boolean; onClick?: () => void; onClickFollow?: () => void; -} & ProfileFollowContent; +} & MemberProfile; export const ProfileListItem = ({ memberId, - name, + nickname, introduction, profileImageUrl, isFollow, @@ -22,7 +22,7 @@ export const ProfileListItem = ({
profile image
-

{name}

-

{introduction}

+

{nickname}

+ {introduction &&

{introduction}

}
diff --git a/features/follow/components/follow-virtual-list.tsx b/components/molecules/profile-list/profile-list.tsx similarity index 70% rename from features/follow/components/follow-virtual-list.tsx rename to components/molecules/profile-list/profile-list.tsx index a80255b6..bfe282c7 100644 --- a/features/follow/components/follow-virtual-list.tsx +++ b/components/molecules/profile-list/profile-list.tsx @@ -1,18 +1,17 @@ +'use client'; + import React from 'react'; import { Virtuoso } from 'react-virtuoso'; -import { ProfileListItem } from '@/components/molecules'; +import { MemberProfile } from '@/types'; -import { ProfileFollowContent } from '../types'; +import { ProfileListItem } from './profile-list-item'; -type FollowVirtualList = { - data: ProfileFollowContent[]; +type ProfileList = { + data: MemberProfile[]; fetchNextData: () => void; }; -export const FollowVirtualList = ({ - data, - fetchNextData, -}: FollowVirtualList) => { +export const ProfileList = ({ data, fetchNextData }: ProfileList) => { const handleRangeChanged = (range: { endIndex: number }) => { const currentContentsLastIndex = data.length - 1; if (range.endIndex >= currentContentsLastIndex - 3) { diff --git a/features/follow/components/index.ts b/features/follow/components/index.ts deleted file mode 100644 index 49cad4d9..00000000 --- a/features/follow/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './follow-virtual-list'; diff --git a/features/follow/sections/follower-section.tsx b/features/follow/sections/follower-section.tsx index 48416091..9f8b9214 100644 --- a/features/follow/sections/follower-section.tsx +++ b/features/follow/sections/follower-section.tsx @@ -1,7 +1,8 @@ 'use client'; +import { ProfileList } from '@/components/molecules'; + import { useFollowerList } from '../apis'; -import { FollowVirtualList } from '../components'; export const FollowerSection = ({ id }: { id: number }) => { const { flattenData, hasNextPage, isFetchingNextPage, fetchNextPage } = @@ -13,5 +14,5 @@ export const FollowerSection = ({ id }: { id: number }) => { } }; - return ; + return ; }; diff --git a/features/follow/sections/following-section.tsx b/features/follow/sections/following-section.tsx index d588b31b..d4243bb4 100644 --- a/features/follow/sections/following-section.tsx +++ b/features/follow/sections/following-section.tsx @@ -1,7 +1,8 @@ 'use client'; +import { ProfileList } from '@/components/molecules'; + import { useFollowingList } from '../apis'; -import { FollowVirtualList } from '../components'; export const FollowingSection = ({ id }: { id: number }) => { const { flattenData, hasNextPage, isFetchingNextPage, fetchNextPage } = @@ -13,5 +14,5 @@ export const FollowingSection = ({ id }: { id: number }) => { } }; - return ; + return ; }; diff --git a/features/follow/types/index.ts b/features/follow/types/index.ts index 231948d5..bb8dfe1d 100644 --- a/features/follow/types/index.ts +++ b/features/follow/types/index.ts @@ -1,16 +1,10 @@ import { Response } from '@/apis'; +import { MemberProfile } from '@/types'; export type FollowTab = 'follow' | 'following'; -export type ProfileFollowContent = { - memberId: number; - name: string; - profileImageUrl: string; - introduction: string; -}; - export type ProfileFollow = Response<{ - contents: ProfileFollowContent[]; + contents: MemberProfile[]; cursorId: number; hasNext: boolean; }>; diff --git a/types/index.ts b/types/index.ts new file mode 100644 index 00000000..efb0245a --- /dev/null +++ b/types/index.ts @@ -0,0 +1 @@ +export * from './member-type'; diff --git a/types/member-type.ts b/types/member-type.ts new file mode 100644 index 00000000..0b1c9adc --- /dev/null +++ b/types/member-type.ts @@ -0,0 +1,6 @@ +export type MemberProfile = { + memberId: number; + nickname: string; + profileImageUrl?: string; + introduction?: string; +}; From 73af958c5f7e6269ffc004a351adc54ac94488fe Mon Sep 17 00:00:00 2001 From: Jiyoung Jung <72294509+Jungjjeong@users.noreply.github.com> Date: Thu, 29 Aug 2024 01:11:58 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20profile=20search=20page=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20api,=20section,=20component=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=A9=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/member/search/route.ts | 17 +++++++ features/profile-search/apis/index.ts | 1 + .../apis/use-profile-search.tsx | 38 ++++++++++++++++ .../components/empty-keyword.tsx | 18 ++++++++ .../components/empty-search-result.tsx | 31 +++++++++++++ features/profile-search/components/index.ts | 2 + features/profile-search/index.ts | 2 + features/profile-search/sections/index.ts | 2 + .../sections/search-bar-section.tsx | 45 +++++++++++++++++++ .../sections/search-result-section.tsx | 38 ++++++++++++++++ features/profile-search/types/index.ts | 8 ++++ 11 files changed, 202 insertions(+) create mode 100644 app/api/member/search/route.ts create mode 100644 features/profile-search/apis/index.ts create mode 100644 features/profile-search/apis/use-profile-search.tsx create mode 100644 features/profile-search/components/empty-keyword.tsx create mode 100644 features/profile-search/components/empty-search-result.tsx create mode 100644 features/profile-search/components/index.ts create mode 100644 features/profile-search/index.ts create mode 100644 features/profile-search/sections/index.ts create mode 100644 features/profile-search/sections/search-bar-section.tsx create mode 100644 features/profile-search/sections/search-result-section.tsx create mode 100644 features/profile-search/types/index.ts diff --git a/app/api/member/search/route.ts b/app/api/member/search/route.ts new file mode 100644 index 00000000..4e6f5d25 --- /dev/null +++ b/app/api/member/search/route.ts @@ -0,0 +1,17 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { fetchData } from '@/apis/fetch-data'; +import { ProfileSearch } from '@/features/profile-search'; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const cursorId = searchParams.get('cursorId') ?? ''; + const nameQuery = searchParams.get('nameQuery') ?? ''; + + const data = await fetchData( + `/member/search?nameQuery=${nameQuery}&cursorId=${cursorId}`, + 'GET', + ); + + return NextResponse.json(data); +} diff --git a/features/profile-search/apis/index.ts b/features/profile-search/apis/index.ts new file mode 100644 index 00000000..16495410 --- /dev/null +++ b/features/profile-search/apis/index.ts @@ -0,0 +1 @@ +export * from './use-profile-search'; diff --git a/features/profile-search/apis/use-profile-search.tsx b/features/profile-search/apis/use-profile-search.tsx new file mode 100644 index 00000000..92b2cd5d --- /dev/null +++ b/features/profile-search/apis/use-profile-search.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { useInfiniteQuery } from '@tanstack/react-query'; + +import { ProfileSearch } from '../types'; + +const fetchProfile = async (nameQuery: string, cursorId?: number) => { + const res = await fetch( + `/api/member/search?nameQuery=${nameQuery}&cursorId=${cursorId ?? ''}`, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + return res.json(); +}; + +export const useProfileSearch = (nameQuery: string) => { + const query = useInfiniteQuery({ + queryKey: ['useProfileSearch', nameQuery], + queryFn: ({ pageParam }) => fetchProfile(nameQuery, pageParam as number), + initialPageParam: undefined, + getNextPageParam: (lastPage) => + lastPage?.data?.hasNext ? lastPage?.data?.cursorId : undefined, + enabled: !!nameQuery?.length, + }); + + const flattenData = + query.data?.pages.flatMap(({ data }) => data?.memberInfoResponses ?? []) ?? + []; + + return { + ...query, + flattenData, + }; +}; diff --git a/features/profile-search/components/empty-keyword.tsx b/features/profile-search/components/empty-keyword.tsx new file mode 100644 index 00000000..933cef44 --- /dev/null +++ b/features/profile-search/components/empty-keyword.tsx @@ -0,0 +1,18 @@ +import { css } from '@/styled-system/css'; + +export const EmptyKeyword = () => { + return ( +
+ 친구를 팔로우하고 +
+ 서로의 기록에 응원을 보내보세요. +
+ ); +}; + +const containerStyle = css({ + m: '80px auto 0px', + textStyle: 'body2.normal', + color: 'text.alternative', + textAlign: 'center', +}); diff --git a/features/profile-search/components/empty-search-result.tsx b/features/profile-search/components/empty-search-result.tsx new file mode 100644 index 00000000..ffaf0618 --- /dev/null +++ b/features/profile-search/components/empty-search-result.tsx @@ -0,0 +1,31 @@ +import { css } from '@/styled-system/css'; +import { flex } from '@/styled-system/patterns'; + +export const EmptySearchResult = ({ keyword }: { keyword: string }) => { + return ( +
+

‘{keyword}‘ 유저가 없어요.

+

+ 마이페이지에서 내 프로필을 공유할 수 있어요 +

+
+ ); +}; + +const containerStyle = flex({ + direction: 'column', + gap: '4px', + m: '80px auto 0px', + align: 'center', +}); + +const titleStyle = css({ + textStyle: 'heading6', + fontWeight: 'medium', + color: 'text.normal', +}); + +const descriptionStyle = css({ + color: 'text.alternative', + fontWeight: 'regular', +}); diff --git a/features/profile-search/components/index.ts b/features/profile-search/components/index.ts new file mode 100644 index 00000000..9b7802a9 --- /dev/null +++ b/features/profile-search/components/index.ts @@ -0,0 +1,2 @@ +export * from './empty-keyword'; +export * from './empty-search-result'; diff --git a/features/profile-search/index.ts b/features/profile-search/index.ts new file mode 100644 index 00000000..d86c3a84 --- /dev/null +++ b/features/profile-search/index.ts @@ -0,0 +1,2 @@ +export * from './sections'; +export * from './types'; diff --git a/features/profile-search/sections/index.ts b/features/profile-search/sections/index.ts new file mode 100644 index 00000000..e6a839a4 --- /dev/null +++ b/features/profile-search/sections/index.ts @@ -0,0 +1,2 @@ +export * from './search-bar-section'; +export * from './search-result-section'; diff --git a/features/profile-search/sections/search-bar-section.tsx b/features/profile-search/sections/search-bar-section.tsx new file mode 100644 index 00000000..64a48f4d --- /dev/null +++ b/features/profile-search/sections/search-bar-section.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { debounce } from 'lodash'; +import { useRouter } from 'next/navigation'; +import { useCallback, useEffect, useState } from 'react'; + +import { SearchBar } from '@/components/molecules'; +import { css } from '@/styled-system/css'; + +export const SearchBarSection = ({ keyword }: { keyword: string }) => { + const router = useRouter(); + const [searchKeyword, setSearchKeyword] = useState(keyword); + + const handleChangeKeyword = debounce((keyword: string) => { + setSearchKeyword(keyword); + }, 400); + + const setKeywordParams = useCallback( + (keyword: string) => { + const params = new URL(window.location.href); + params.searchParams.set('keyword', keyword); + + router.replace(params.toString()); + }, + [router], + ); + + useEffect(() => { + setKeywordParams(searchKeyword); + }, [searchKeyword, setKeywordParams]); + + return ( +
+ +
+ ); +}; + +const containerStyle = css({ + p: '8px 20px', +}); diff --git a/features/profile-search/sections/search-result-section.tsx b/features/profile-search/sections/search-result-section.tsx new file mode 100644 index 00000000..05af9711 --- /dev/null +++ b/features/profile-search/sections/search-result-section.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { ProfileList } from '@/components/molecules'; +import { flex } from '@/styled-system/patterns'; + +import { useProfileSearch } from '../apis/use-profile-search'; +import { EmptyKeyword, EmptySearchResult } from '../components'; + +export const SearchResultSection = ({ keyword }: { keyword: string }) => { + const { + flattenData, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + isFetching, + } = useProfileSearch(keyword); + + const fetchNextData = () => { + if (hasNextPage && !isFetchingNextPage) { + void fetchNextPage(); + } + }; + + if (!keyword.length) return ; + if (flattenData.length === 0 && !isFetching) + return ; + return ( +
+ +
+ ); +}; + +const containerStyle = flex({ + direction: 'column', + gap: '12px', + p: '16px 20px', +}); diff --git a/features/profile-search/types/index.ts b/features/profile-search/types/index.ts new file mode 100644 index 00000000..9b158466 --- /dev/null +++ b/features/profile-search/types/index.ts @@ -0,0 +1,8 @@ +import { Response } from '@/apis'; +import { MemberProfile } from '@/types'; + +export type ProfileSearch = Response<{ + memberInfoResponses: MemberProfile[]; + cursorId: number; + hasNext: boolean; +}>; From 18cbe6e1b83c4343b0e8176b54aba2bcdc11a0a7 Mon Sep 17 00:00:00 2001 From: Jiyoung Jung <72294509+Jungjjeong@users.noreply.github.com> Date: Thu, 29 Aug 2024 01:16:03 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20profile=20search=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/profile/search/page.tsx | 55 +++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 app/profile/search/page.tsx diff --git a/app/profile/search/page.tsx b/app/profile/search/page.tsx new file mode 100644 index 00000000..ecaf2a0b --- /dev/null +++ b/app/profile/search/page.tsx @@ -0,0 +1,55 @@ +import dynamic from 'next/dynamic'; + +import { LeftArrowIcon } from '@/components/atoms'; +import { HeaderBar } from '@/components/molecules'; + +const DynamicBackButton = dynamic( + () => import('@/components/molecules').then(({ BackButton }) => BackButton), + { + ssr: false, + loading: () => , + }, +); + +const DynamicSearchBarSection = dynamic( + () => + import('@/features/profile-search').then( + ({ SearchBarSection }) => SearchBarSection, + ), + { + ssr: false, + }, +); + +const DynamicSearchResultSection = dynamic( + () => + import('@/features/profile-search').then( + ({ SearchResultSection }) => SearchResultSection, + ), + { + ssr: false, + }, +); + +export default function ProfileSearch({ + searchParams, +}: { + searchParams: { keyword: string }; +}) { + const { keyword = '' } = searchParams; + + return ( + <> + + + + + 친구 찾기 + +
+ + +
+ + ); +} From e3db5b74dd23c25d02153a1b876cb59ac01a2a02 Mon Sep 17 00:00:00 2001 From: Jiyoung Jung <72294509+Jungjjeong@users.noreply.github.com> Date: Thu, 29 Aug 2024 01:50:15 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20profile=20image=20component=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/molecules/index.ts | 1 + components/molecules/profile-image/index.ts | 1 + .../molecules/profile-image/profile-image.tsx | 24 ++++++++++++++++++ .../profile-list/profile-list-item.tsx | 6 +++-- .../components/cheer-modal-item.tsx | 4 +-- public/images/default-profile/blue-hat.png | Bin 20344 -> 52529 bytes public/images/default-profile/green-hat.png | Bin 20424 -> 53144 bytes public/images/default-profile/orange-hat.png | Bin 21068 -> 54810 bytes public/images/default-profile/yellow-hat.png | Bin 20102 -> 52003 bytes 9 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 components/molecules/profile-image/index.ts create mode 100644 components/molecules/profile-image/profile-image.tsx diff --git a/components/molecules/index.ts b/components/molecules/index.ts index f1f1abe8..cf445732 100644 --- a/components/molecules/index.ts +++ b/components/molecules/index.ts @@ -5,6 +5,7 @@ export * from './header-bar'; export * from './infinite-scroller'; export * from './modal'; export * from './page-modal'; +export * from './profile-image'; export * from './profile-list'; export * from './record-mark'; export * from './search-bar'; diff --git a/components/molecules/profile-image/index.ts b/components/molecules/profile-image/index.ts new file mode 100644 index 00000000..44545fa9 --- /dev/null +++ b/components/molecules/profile-image/index.ts @@ -0,0 +1 @@ +export * from './profile-image'; diff --git a/components/molecules/profile-image/profile-image.tsx b/components/molecules/profile-image/profile-image.tsx new file mode 100644 index 00000000..9cf1b23a --- /dev/null +++ b/components/molecules/profile-image/profile-image.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { ImageProps } from 'next/image'; +import React, { useMemo } from 'react'; + +import { + defaultProfileImages, + ProfileIndexType, +} from '@/public/images/default-profile'; + +import { Image } from '../../atoms'; + +export const ProfileImage = ({ + src, + alt = 'profile image', + ...props +}: ImageProps) => { + const imageSrc = useMemo(() => { + const profileImage = defaultProfileImages[Number(src) as ProfileIndexType]; + return profileImage ?? src; + }, [src]); + + return {alt}; +}; diff --git a/components/molecules/profile-list/profile-list-item.tsx b/components/molecules/profile-list/profile-list-item.tsx index b2b20086..a9ba371c 100644 --- a/components/molecules/profile-list/profile-list-item.tsx +++ b/components/molecules/profile-list/profile-list-item.tsx @@ -1,10 +1,12 @@ import Link from 'next/link'; -import { Button, Image } from '@/components/atoms'; +import { Button } from '@/components/atoms'; import { css } from '@/styled-system/css'; import { flex } from '@/styled-system/patterns'; import { MemberProfile } from '@/types'; +import { ProfileImage } from '../profile-image'; + type FollowListItem = { isFollow: boolean; onClick?: () => void; @@ -21,7 +23,7 @@ export const ProfileListItem = ({
- profile image