diff --git a/src/apis/follow.ts b/src/apis/follow.ts index 2a7440e8..1b5334db 100644 --- a/src/apis/follow.ts +++ b/src/apis/follow.ts @@ -1,8 +1,9 @@ import getQueryKey from '@/apis/getQueryKey'; import apiInstance from '@/apis/instance.api'; -import { type FollowMemberType } from '@/apis/schema/member'; +import { type FollowerMemberWithStatusType, type FollowMemberType, type FollowStatus } from '@/apis/schema/member'; import { type MissionItemTypeWithRecordId } from '@/apis/schema/mission'; -import { useQuery, type UseQueryOptions } from '@tanstack/react-query'; +import useMutationHandleError from '@/hooks/query/useMutationHandleError'; +import { type UseMutationOptions, useQuery, type UseQueryOptions } from '@tanstack/react-query'; type GetFollowMembersResponse = FollowMemberType[]; @@ -16,10 +17,15 @@ interface FollowsResponse { followerCount: number; followStatus: FollowStatus; } -export enum FollowStatus { - FOLLOWING = 'FOLLOWING', - FOLLOWED_BY_ME = 'FOLLOWED_BY_ME', - NOT_FOLLOWING = 'NOT_FOLLOWING', + +interface FollowListResponse { + targetNickname: string; + followingList: FollowerMemberWithStatusType[]; + followerList: FollowerMemberWithStatusType[]; +} + +interface DeleteFollowResponse { + followStatus: FollowStatus; } export const FOLLOW_API = { @@ -36,7 +42,7 @@ export const FOLLOW_API = { const { data } = await apiInstance.post(`/follows`, { targetId }); return data; }, - deleteFollow: async (targetId: number) => { + deleteFollow: async (targetId: number): Promise => { const { data } = await apiInstance.delete(`/follows`, { data: { targetId } }); return data; }, @@ -48,6 +54,10 @@ export const FOLLOW_API = { const { data } = await apiInstance.get(`/follows/${followId}`); return data; }, + getFollowList: async (targetId: number): Promise => { + const { data } = await apiInstance.get(`/follows/${targetId}/list`); + return data; + }, }; export const useFollowMembers = (options?: UseQueryOptions) => { @@ -81,3 +91,27 @@ export const useFollowsCountTargetId = (followId: number, option?: UseQueryOptio ...option, }); }; + +export const useFetFollowList = (targetId: number, option?: UseQueryOptions) => { + return useQuery({ + queryKey: getQueryKey('followList', { targetId }), + queryFn: () => FOLLOW_API.getFollowList(targetId), + ...option, + }); +}; + +export const useAddFollow = (options?: UseMutationOptions) => + useMutationHandleError( + { mutationFn: FOLLOW_API.addFollow, ...options }, + { + offset: 'default', + }, + ); + +export const useDeleteFollow = (options?: UseMutationOptions) => + useMutationHandleError( + { mutationFn: FOLLOW_API.deleteFollow, ...options }, + { + offset: 'default', + }, + ); diff --git a/src/apis/getQueryKey.ts b/src/apis/getQueryKey.ts index 6f060538..221e77ec 100644 --- a/src/apis/getQueryKey.ts +++ b/src/apis/getQueryKey.ts @@ -19,11 +19,16 @@ type QueryList = { me?: undefined; id?: number; }; + // follow followMembers: undefined; followsCountMe: undefined; followsCountTargetId: { followId: number; }; + followList: { + targetId: number; + }; + memberSocial: undefined; searchNickname: { nickname: string; diff --git a/src/apis/member.ts b/src/apis/member.ts index da90cac5..59ac47f9 100644 --- a/src/apis/member.ts +++ b/src/apis/member.ts @@ -1,6 +1,6 @@ import getQueryKey from '@/apis/getQueryKey'; import apiInstance from '@/apis/instance.api'; -import { type FollowStatusType, type MemberType } from '@/apis/schema/member'; +import { type FollowerMemberWithStatusType, type MemberType } from '@/apis/schema/member'; import { type UploadBaseRequest, type UploadUrlBaseResponse } from '@/apis/schema/upload'; import { useMutation, @@ -33,12 +33,7 @@ interface SocialLoginInfoResponse { email: 'string'; } -type SearchNicknameResponse = { - memberId: number; - nickname: string; - profileImageUrl: string; - followStatus: FollowStatusType; -}[]; +type SearchNicknameResponse = FollowerMemberWithStatusType[]; enum AUTH_PROVIDER { KAKAO = 'KAKAO', diff --git a/src/apis/schema/member.ts b/src/apis/schema/member.ts index 95161b11..99eecb26 100644 --- a/src/apis/schema/member.ts +++ b/src/apis/schema/member.ts @@ -23,10 +23,21 @@ export interface FollowMemberType { profileImageUrl: string; } -export type FollowStatusType = 'FOLLOWING' | 'FOLLOWED_BY_ME' | 'NOT_FOLLOWING'; - export enum FileExtension { JPEG = 'JPEG', JPG = 'JPG', PNG = 'PNG', } + +export interface FollowerMemberWithStatusType { + memberId: number; + nickname: string; + profileImageUrl: string; + followStatus: FollowStatus; +} + +export enum FollowStatus { + FOLLOWING = 'FOLLOWING', + FOLLOWED_BY_ME = 'FOLLOWED_BY_ME', + NOT_FOLLOWING = 'NOT_FOLLOWING', +} diff --git a/src/app/mypage/MyProfile.tsx b/src/app/mypage/MyProfile.tsx index 9b01c6a6..baba9ac7 100644 --- a/src/app/mypage/MyProfile.tsx +++ b/src/app/mypage/MyProfile.tsx @@ -20,13 +20,14 @@ const tabs = [ export default function MyProfile() { const { data } = useGetMembersMe(); - const missionId = data?.memberId ?? 0; - const { data: symbolStackData } = useGetMissionStack(missionId.toString()); + const memberId = data?.memberId ?? 0; + const { data: symbolStackData } = useGetMissionStack(memberId.toString()); const symbolStack = symbolStackData?.symbolStack ?? 0; const { data: followCountData } = useFollowsCountMembers(); return ( ) { return (
@@ -33,7 +35,8 @@ function ProfileContent({

{nickname}

- 팔로잉 {followingCount}   팔로워 {followerCount} + 팔로잉 {followingCount}   + 팔로워 {followerCount}
{rightElement} diff --git a/src/app/profile/[id]/follows/FollowingList.tsx b/src/app/profile/[id]/follows/FollowingList.tsx new file mode 100644 index 00000000..9316a9fe --- /dev/null +++ b/src/app/profile/[id]/follows/FollowingList.tsx @@ -0,0 +1,67 @@ +import Link from 'next/link'; +import { type FollowerMemberWithStatusType, FollowStatus } from '@/apis/schema/member'; +import { useGetMeId, useViewList } from '@/app/profile/[id]/follows/index.hooks'; +import { + FollowingMember, + type MemberItemProps, + MineMemberItem, + NotFollowingMember, +} from '@/components/ListItem/Follow/MemberItem'; +import { stagger } from '@/components/Motion/Motion.constants'; +import StaggerWrapper from '@/components/Motion/StaggerWrapper'; +import { ROUTER } from '@/constants/router'; +import { css } from '@/styled-system/css'; + +interface Props { + list: FollowerMemberWithStatusType[]; + refetch: () => void; +} + +function FollowingList(props: Props) { + const { list, onUpdateItem } = useViewList(props.list); + + return ( + + {list.map((item) => ( + + { + onUpdateItem(_item); + props.refetch(); + }} + /> + + ))} + + ); +} + +export default FollowingList; + +interface ItemProps extends Omit { + onUpdateList: (item: FollowerMemberWithStatusType) => void; +} + +function Item({ onUpdateList, ...props }: ItemProps) { + const myId = useGetMeId(); + + if (props.memberId === myId) { + return ; + } + + switch (props.followStatus) { + case FollowStatus.FOLLOWING: + return ; + case FollowStatus.NOT_FOLLOWING: + case FollowStatus.FOLLOWED_BY_ME: + return ; + default: + return null; + } +} + +const containerCss = css({ + padding: '16px', + width: '100%', +}); diff --git a/src/app/profile/[id]/follows/MyFollowerList.tsx b/src/app/profile/[id]/follows/MyFollowerList.tsx new file mode 100644 index 00000000..2f02ccb0 --- /dev/null +++ b/src/app/profile/[id]/follows/MyFollowerList.tsx @@ -0,0 +1,80 @@ +import Link from 'next/link'; +import { useAddFollow } from '@/apis/follow'; +import { type FollowerMemberWithStatusType, FollowStatus } from '@/apis/schema/member'; +import { useViewList } from '@/app/profile/[id]/follows/index.hooks'; +import { ProfileListItem } from '@/components/ListItem'; +import { stagger } from '@/components/Motion/Motion.constants'; +import StaggerWrapper from '@/components/Motion/StaggerWrapper'; +import { ROUTER } from '@/constants/router'; +import { css } from '@/styled-system/css'; + +interface Props { + list: FollowerMemberWithStatusType[]; + refetch: () => void; +} + +function MyFollowerList(props: Props) { + const { list, onUpdateItem } = useViewList(props.list); + + return ( + + {list.map((item) => ( + { + onUpdateItem(_item); + props.refetch(); + }} + /> + ))} + + ); +} + +export default MyFollowerList; + +interface ItemProps { + item: FollowerMemberWithStatusType; + onUpdateItem: (member: FollowerMemberWithStatusType) => void; +} + +function Item({ item, onUpdateItem }: ItemProps) { + const { mutate } = useAddFollow({ + onSuccess: () => { + onUpdateItem({ ...item, followStatus: FollowStatus.FOLLOWING }); + }, + }); + + const isFollowing = item.followStatus === FollowStatus.FOLLOWING; + + return ( + + mutate(item.memberId)}> + 팔로우 + + ) + } + buttonElement={ + // TODO : 삭제 버튼 추가 필요 (맞팔 관계 팔로우 삭제, 맞팔 x, 팔로워 관계 삭제) + // 일정 상 무리라고 판단 (2/6) 추후 수정 +
+ } + thumbnailUrl={item.profileImageUrl} + name={item.nickname} + /> + + ); +} + +const containerCss = css({ + padding: '16px', +}); + +const followLabelCss = css({ + padding: '8px 12px', +}); diff --git a/src/app/profile/[id]/follows/index.hooks.ts b/src/app/profile/[id]/follows/index.hooks.ts new file mode 100644 index 00000000..4707f670 --- /dev/null +++ b/src/app/profile/[id]/follows/index.hooks.ts @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react'; +import { useGetMembersMe } from '@/apis/member'; +import { type FollowerMemberWithStatusType } from '@/apis/schema/member'; +import { sorFollowerList } from '@/app/profile/[id]/follows/index.utils'; + +export const useViewList = (list: FollowerMemberWithStatusType[]) => { + const myId = useGetMeId(); + const [viewList, setViewList] = useState(list); + + const onUpdateItem = (member: FollowerMemberWithStatusType) => { + setViewList((prev) => prev.map((item) => (item.memberId === member.memberId ? member : item))); + }; + + useEffect(() => { + const sortList = sorFollowerList(list, Number(myId)); + setViewList(sortList); + }, []); + + return { list: viewList, onUpdateItem }; +}; + +// @description 현재 로그인한 사용자의 memberId를 가져옵니다. +export const useGetMeId = () => { + const { data } = useGetMembersMe(); + const memberId = data?.memberId ?? 0; + return memberId; +}; diff --git a/src/app/profile/[id]/follows/index.utils.ts b/src/app/profile/[id]/follows/index.utils.ts new file mode 100644 index 00000000..ab9d33ba --- /dev/null +++ b/src/app/profile/[id]/follows/index.utils.ts @@ -0,0 +1,15 @@ +import { type FollowerMemberWithStatusType, FollowStatus } from '@/apis/schema/member'; + +export const sorFollowerList = (list: FollowerMemberWithStatusType[], myId: number): FollowerMemberWithStatusType[] => { + // 1순위) 내 계정, 내가 팔로잉 중인 계정, 내가 팔로우 중이지 않은 계정 순으로 리스트 나열 + // 2순위) 가나다 순, ABC순으로 나열 + + const myAccount = list.filter((item) => item.memberId === myId); + const followingList = list.filter((item) => item.followStatus === FollowStatus.FOLLOWING); + const followedByMeList = list.filter((item) => item.followStatus === FollowStatus.FOLLOWED_BY_ME); + const notFollowingList = list.filter((item) => item.followStatus === FollowStatus.NOT_FOLLOWING); + + const sortedList = [...myAccount, ...followingList, ...followedByMeList, ...notFollowingList]; + + return sortedList; +}; diff --git a/src/app/profile/[id]/follows/page.tsx b/src/app/profile/[id]/follows/page.tsx new file mode 100644 index 00000000..d51ecba7 --- /dev/null +++ b/src/app/profile/[id]/follows/page.tsx @@ -0,0 +1,83 @@ +'use client'; + +import { useFetFollowList } from '@/apis/follow'; +import FollowingList from '@/app/profile/[id]/follows/FollowingList'; +import { useGetMeId } from '@/app/profile/[id]/follows/index.hooks'; +import MyFollowerList from '@/app/profile/[id]/follows/MyFollowerList'; +import Header from '@/components/Header/Header'; +import FullTab from '@/components/Tab/FullTab'; +import { useTab } from '@/components/Tab/Tab.hooks'; +import { ROUTER } from '@/constants/router'; +import useSearchParamsTypedValue from '@/hooks/useSearchParamsTypedValue'; +import { css } from '@/styled-system/css'; + +type FollowTabType = 'following' | 'follower'; +function FollowListPage({ params }: { params: { id: string } }) { + const { searchParams } = useSearchParamsTypedValue('tab'); + const initTabId = searchParams ?? 'following'; + + const currentMemberId = Number(params.id); + + const { data, refetch, isLoading } = useFetFollowList(Number(params.id)); + + const followingCount = data?.followingList.length ?? ''; + const followerCount = data?.followerList.length ?? ''; + + const _tabs = [ + { + id: 'following', + tabName: `팔로잉 ${followingCount}`, + href: ROUTER.PROFILE.FOLLOW_LIST(currentMemberId, 'following'), + }, + { + id: 'follower', + tabName: `팔로워 ${followerCount}`, + href: ROUTER.PROFILE.FOLLOW_LIST(currentMemberId, 'follower'), + }, + ]; + + const { tabs, activeTab, onTabClick } = useTab(_tabs, initTabId); + + const myId = useGetMeId(); + const isMyFollowList = myId === currentMemberId; + + if (isLoading) return
; // 스켈레톤 고민해보기 + + return ( +
+
+ { + onTabClick(tab); + }} + /> + {/* 내 팔로잉/팔로우 */} + {isMyFollowList && + (activeTab === 'following' ? ( + + ) : ( + + ))} + {/* 다른 사람 팔로잉/팔로우 */} + {!isMyFollowList && + (activeTab === 'following' ? ( + + ) : ( + + ))} +
+ ); +} + +export default FollowListPage; + +const headerCss = css({ + '& h2': { + position: 'absolute', + left: '50%', + transform: 'translateX(-50%)', + textStyle: 'subtitle1', + }, +}); diff --git a/src/app/profile/[id]/page.tsx b/src/app/profile/[id]/page.tsx index f9b221b6..191f5624 100644 --- a/src/app/profile/[id]/page.tsx +++ b/src/app/profile/[id]/page.tsx @@ -1,8 +1,9 @@ 'use client'; -import { FollowStatus, useFollowsCountTargetId } from '@/apis/follow'; +import { useFollowsCountTargetId } from '@/apis/follow'; import { useGetMembersById } from '@/apis/member'; import { useGetMissionStack } from '@/apis/mission'; +import { FollowStatus } from '@/apis/schema/member'; import ProfileMissionList from '@/app/mypage/MyProfileMissionList'; import FollowButton from '@/app/profile/[id]/FollowButton'; import ProfileContent from '@/app/profile/[id]/ProfileContent'; @@ -20,6 +21,7 @@ function FollowProfilePage({ params }: { params: { id: string } }) {
- {data.map((item) => { - const params = { - name: item.nickname, - memberId: item.memberId, - thumbnail: { - url: item.profileImageUrl, - alt: item.nickname, - variant: 'filled', - }, - onButtonClick, - }; - return ( - - {item.followStatus === 'FOLLOWING' ? ( - - ) : ( - - )} - - ); - })} - + + {data.map((item) => ( + + {item.followStatus === 'FOLLOWING' ? ( + + ) : ( + + )} + + ))} + ); } diff --git a/src/components/Header/Header.types.ts b/src/components/Header/Header.types.ts index 86fd7468..204a4f48 100644 --- a/src/components/Header/Header.types.ts +++ b/src/components/Header/Header.types.ts @@ -14,6 +14,7 @@ export interface HeaderBaseType { iconColor?: ColorToken; textColor?: ColorToken; rightElement?: React.ReactNode; + className?: string; } export interface IconHeaderType extends HeaderBaseType { diff --git a/src/components/Header/HeaderBase.tsx b/src/components/Header/HeaderBase.tsx index bfeb9f5d..4bb2be11 100644 --- a/src/components/Header/HeaderBase.tsx +++ b/src/components/Header/HeaderBase.tsx @@ -18,7 +18,7 @@ function HeaderBase( ) { return ( <> -
+
{props.onBackAction && ( - } - {...props} - /> - ); -} - -export default FollowerItem; diff --git a/src/components/ListItem/Follow/FollowingItem.tsx b/src/components/ListItem/Follow/FollowingItem.tsx deleted file mode 100644 index 7137d243..00000000 --- a/src/components/ListItem/Follow/FollowingItem.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { type ComponentProps, type MouseEventHandler } from 'react'; -import { FOLLOW_API } from '@/apis/follow'; -import Button from '@/components/Button/Button'; -import { ProfileListItem } from '@/components/ListItem'; -import { useSnackBar } from '@/components/SnackBar/SnackBarProvider'; - -interface Props - extends Omit, 'buttonElement' | 'subElement' | 'variant' | 'thumbnail'> { - memberId: number; - onButtonClick?: () => void; -} - -function FollowingItem(props: Props) { - const { triggerSnackBar } = useSnackBar(); - - const onFollowerClick: MouseEventHandler = async (e) => { - e.preventDefault(); - - try { - await FOLLOW_API.deleteFollow(props.memberId); - props.onButtonClick?.(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - triggerSnackBar({ - message: error.response.data.data.message, - }); - } - }; - - return ( - - 팔로잉 - - } - {...props} - /> - ); -} - -export default FollowingItem; diff --git a/src/components/ListItem/Follow/MemberItem.tsx b/src/components/ListItem/Follow/MemberItem.tsx new file mode 100644 index 00000000..e91b9e0a --- /dev/null +++ b/src/components/ListItem/Follow/MemberItem.tsx @@ -0,0 +1,65 @@ +import { useAddFollow, useDeleteFollow } from '@/apis/follow'; +import { type FollowerMemberWithStatusType, FollowStatus } from '@/apis/schema/member'; +import Button from '@/components/Button/Button'; +import { ProfileListItem } from '@/components/ListItem'; + +export interface MemberItemProps extends FollowerMemberWithStatusType { + onButtonClick?: (item: FollowerMemberWithStatusType) => void; + + onClick?: (item: FollowerMemberWithStatusType) => void; +} + +export function FollowingMember({ onClick, ...props }: MemberItemProps) { + const { mutate } = useDeleteFollow({ + onSuccess: (res) => { + // TODO : 서버 데이터 잘 받아오는지 체크 + const newStatus = res?.followStatus ?? FollowStatus.NOT_FOLLOWING; + props.onButtonClick?.({ ...props, followStatus: newStatus }); + }, + }); + + const onFollowingCancel = async () => { + mutate(props.memberId); + }; + + return ( + + 팔로잉 + + } + /> + ); +} + +// 팔로잉 되어있지 않은 멤버 +export function NotFollowingMember(props: MemberItemProps) { + const { mutate } = useAddFollow({ + onSuccess: () => { + props.onButtonClick?.({ ...props, followStatus: FollowStatus.FOLLOWING }); + }, + }); + + const onFollowerClick = async () => { + mutate(props.memberId); + }; + + return ( + + {props.followStatus === FollowStatus.FOLLOWED_BY_ME ? '맞팔로우' : '팔로우'} + + } + /> + ); +} + +export function MineMemberItem(props: MemberItemProps) { + return
} thumbnailUrl={props.profileImageUrl} />; +} diff --git a/src/components/ListItem/ProfileListItem.tsx b/src/components/ListItem/ProfileListItem.tsx index 7fa415e7..d340ba65 100644 --- a/src/components/ListItem/ProfileListItem.tsx +++ b/src/components/ListItem/ProfileListItem.tsx @@ -1,12 +1,11 @@ import { type ReactNode } from 'react'; import { oneLineTextCss } from '@/components/ListItem/ListItem.styles'; import Thumbnail from '@/components/Thumbnail/Thumbnail'; -import { type ThumbnailProps } from '@/components/Thumbnail/Thumbnail.types'; import { css, cx } from '@/styled-system/css'; import { flex } from '@/styled-system/patterns'; interface Props { - thumbnail?: ThumbnailProps; + thumbnailUrl?: string; name: string; variant?: 'one-button' | 'two-button'; buttonElement: ReactNode; @@ -18,12 +17,24 @@ function ProfileListItem(props: Props) { return (
  • - -

    + +

    {props.name} - {props.subElement &&
    {props.subElement}
    } -

    -
    {props.buttonElement}
    + {props.subElement && ( +
    e.preventDefault()}> + {props.subElement} +
    + )} +
    +
    { + e.stopPropagation(); + e.preventDefault(); + }} + > + {props.buttonElement} +
  • ); } @@ -50,10 +61,13 @@ const nameCss = css({ color: 'text.secondary', textStyle: 'subtitle4', position: 'relative', + display: 'flex', }); const existFollowerButtonCss = css({ paddingRight: '48px', + width: 'fit-content', + flex: 0, }); const buttonCss = css({ @@ -61,9 +75,6 @@ const buttonCss = css({ }); const followLabelCss = css({ - top: 0, - right: 0, - position: 'absolute', color: 'purple.purple600', textStyle: 'subtitle4', }); diff --git a/src/components/Motion/Motion.constants.ts b/src/components/Motion/Motion.constants.ts new file mode 100644 index 00000000..c3c3b53a --- /dev/null +++ b/src/components/Motion/Motion.constants.ts @@ -0,0 +1,25 @@ +import { type Variants } from 'framer-motion'; + +export const defaultEasing = [0.6, -0.05, 0.01, 0.99]; + +export const fadeInUpVariants: Variants = { + initial: { + opacity: 0, + y: 10, + transition: { duration: 0.5, ease: defaultEasing }, + }, + animate: { + opacity: 1, + y: 0, + transition: { duration: 0.5, ease: defaultEasing }, + }, + exit: { + opacity: 0, + y: 10, + transition: { duration: 0.5, ease: defaultEasing }, + }, +}; + +export const stagger = (delay = 0.3): Variants => ({ + animate: { transition: { staggerChildren: delay } }, +}); diff --git a/src/components/Motion/StaggerWrapper.tsx b/src/components/Motion/StaggerWrapper.tsx new file mode 100644 index 00000000..022f6956 --- /dev/null +++ b/src/components/Motion/StaggerWrapper.tsx @@ -0,0 +1,62 @@ +import { Children, type PropsWithChildren } from 'react'; +import { fadeInUpVariants, stagger } from '@/components/Motion/Motion.constants'; +import { css, cx } from '@/styled-system/css'; +import { motion, type Variants } from 'framer-motion'; + +interface Props extends PropsWithChildren { + /** + * @description wrapper에 적용될 css 입니다. + * @default ```css + * display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + ``` + */ + wrapperOverrideCss?: string; + /** + * @description wrapper에 적용될 variants 입니다. + * @default stagger(0.5) + */ + staggerVariants?: Variants; + /** + * @description paragraph에 적용될 variants 입니다. + * @default fadeInUpVariants + */ + itemVariants?: Variants; +} + +/** + * @description children 노드 각각을 stagger가 적용된 div로 감싸 줍니다. + */ +const StaggerWrapper = ({ + children, + wrapperOverrideCss, + staggerVariants = stagger(0.5), + itemVariants = fadeInUpVariants, +}: Props) => { + return ( + + {Children.toArray(children).map((paragraph, index) => ( + + {paragraph} + + ))} + + ); +}; + +export default StaggerWrapper; + +const wrapperCss = css({ + display: 'flex', + flexDirection: 'column', + width: '100%', +}); diff --git a/src/components/Tab/FullTabParts.tsx b/src/components/Tab/FullTabParts.tsx index 52af4872..1c6396e9 100644 --- a/src/components/Tab/FullTabParts.tsx +++ b/src/components/Tab/FullTabParts.tsx @@ -1,18 +1,26 @@ +import Link from 'next/link'; import { type FullTabPartsProps } from '@/components/Tab/Tab.types'; import { css, cx } from '@/styled-system/css'; -function FullTabParts({ tabName, isActive, onTabClick }: FullTabPartsProps) { +function FullTabParts({ tabName, isActive, onTabClick, href }: FullTabPartsProps) { + const classNames = cx( + tabNameCss, + css({ + color: isActive ? 'text.secondary' : 'text.tertiary', + borderColor: isActive ? 'icon.secondary' : 'gray.gray150', + }), + ); + + if (href) { + return ( + +
    {tabName}
    + + ); + } + return ( -
    +
    {tabName}
    ); diff --git a/src/components/Tab/Tab.hooks.ts b/src/components/Tab/Tab.hooks.ts index a5c2bc09..a96a1708 100644 --- a/src/components/Tab/Tab.hooks.ts +++ b/src/components/Tab/Tab.hooks.ts @@ -3,8 +3,8 @@ import { useState } from 'react'; import { type TabType } from '@/components/Tab/Tab.types'; -export const useTab = (tabs: TabType[]) => { - const [activeTab, setActiveTab] = useState(tabs[0].id); +export const useTab = (tabs: TabType[], initTabId?: string) => { + const [activeTab, setActiveTab] = useState(initTabId ?? tabs[0].id); const onTabClick = (clickedTabType: TabType) => { setActiveTab(clickedTabType.id); diff --git a/src/components/Tab/Tab.types.ts b/src/components/Tab/Tab.types.ts index 718b7b65..47429a26 100644 --- a/src/components/Tab/Tab.types.ts +++ b/src/components/Tab/Tab.types.ts @@ -7,6 +7,7 @@ export interface TabProps { export type TabType = { tabName: string; id: string; + href?: string; }; export interface TabPartsProps extends TabType { diff --git a/src/constants/router.ts b/src/constants/router.ts index f2ef6bcd..b3127019 100644 --- a/src/constants/router.ts +++ b/src/constants/router.ts @@ -20,6 +20,8 @@ export const ROUTER = { }, PROFILE: { DETAIL: (id: number) => `/profile/${id}`, + FOLLOW_LIST: (id: number, query?: 'following' | 'follower') => + `/profile/${id}/follows${query ? `?tab=${query}` : ''}`, }, MYPAGE: { HOME: '/mypage',