Skip to content

Commit

Permalink
Feat: 리스트 생성&수정 페이지 - 콜라보레이터 추가시 팔로잉 리스트 조회 및 유저 검색 기능 추가 API
Browse files Browse the repository at this point in the history
  • Loading branch information
Eugene-A-01 committed Feb 18, 2024
1 parent b8ce41d commit 51746bd
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 68 deletions.
35 changes: 16 additions & 19 deletions src/app/list/create/_components/CreateList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { useEffect, useState } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { useRouter } from 'next/navigation';
import { useSearchParams } from 'next/navigation';
import { useQuery } from '@tanstack/react-query';

// import Header from './list/Header';
import Header from '@/components/Header/Header';
import Section from './list/Section';
import SimpleInput from './list/SimpleInput';
Expand All @@ -15,16 +15,17 @@ import MemberSelector from './list/MemberSelector';
import ColorSelector from './list/ColorSelector';
import RadioInput from './list/RadioInput';

import * as styles from './CreateList.css';

import { listPlaceholder } from '@/lib/constants/placeholder';
import { BACKGROUND_COLOR } from '@/styles/Color';
import { CategoryType } from '@/lib/types/categoriesType';
import { UserProfileType } from '@/lib/types/userProfileType';
import { useUser } from '@/store/useUser';
import { QUERY_KEYS } from '@/lib/constants/queryKeys';
import getCategories from '@/app/_api/category/getCategories';
import getUsers from '@/app/_api/user/getUsers';
import getFollowingList from '@/app/_api/follow/getFollowingList';
import { CategoryType } from '@/lib/types/categoriesType';
import { FollowingListType } from '@/lib/types/followType';
import { BACKGROUND_COLOR } from '@/styles/Color';
import { listPlaceholder } from '@/lib/constants/placeholder';
import { listDescriptionRules, listLabelRules, listTitleRules } from '@/lib/constants/formInputValidationRules';
// import { listDescription } from '@/app/[userNickname]/[listId]/_components/ListDetailOuter/ListInformation.css';

import * as styles from './CreateList.css';

interface CreateListProps {
onNextClick: () => void;
Expand All @@ -40,7 +41,7 @@ interface CreateListProps {
*/
function CreateList({ onNextClick, type }: CreateListProps) {
const [categories, setCategories] = useState<CategoryType[]>([]);
const [users, setUsers] = useState<UserProfileType[]>([]);
const { user: me } = useUser();

const { setValue, getValues, control } = useFormContext();
const collaboIDs = useWatch({ control, name: 'collaboratorIds' });
Expand All @@ -51,12 +52,10 @@ function CreateList({ onNextClick, type }: CreateListProps) {
const searchParams = useSearchParams();
const isTemplateCreation = searchParams?.has('title') && searchParams?.has('category');

const fetchUsers = async () => {
try {
const data = await getUsers();
setUsers(data.userInfos);
} catch (error) {}
};
const { data: followingList } = useQuery<FollowingListType>({
queryKey: [QUERY_KEYS.getFollowingList, me.id],
queryFn: () => getFollowingList(me.id),
});

useEffect(() => {
const fetchCategories = async () => {
Expand Down Expand Up @@ -140,8 +139,7 @@ function CreateList({ onNextClick, type }: CreateListProps) {
<Section title="콜라보레이터 추가">
<MemberSelector
placeholder={listPlaceholder.collaborator}
members={users}
fetchData={fetchUsers}
followingList={followingList?.followings || []}
onClickAdd={(userId: number) => {
setValue('collaboratorIds', [...collaboIDs, userId]);
}}
Expand All @@ -157,7 +155,6 @@ function CreateList({ onNextClick, type }: CreateListProps) {
errorMessage: `콜라보레이터는 최대 20명까지 지정할 수 있어요.`,
},
}}
defaultValue={getValues('collaboratorIds')}
/>
</Section>

Expand Down
139 changes: 91 additions & 48 deletions src/app/list/create/_components/list/MemberSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { useParams } from 'next/navigation';
import { useQuery } from '@tanstack/react-query';

import UserProfileImage from '@/components/UserProfileImage/UserProfileImage';
import SearchIcon from '/public/icons/search.svg';
import EraseButton from '/public/icons/x_circle_fill.svg';

import * as styles from './MemberSelector.css';

import getListDetail from '@/app/_api/list/getListDetail';
import getUsersByNicknameSearch from '@/app/_api/user/getUsersByNicknameSearch';
import { QUERY_KEYS } from '@/lib/constants/queryKeys';
import { UserSearchType } from '@/lib/types/user';
import { UserProfileType } from '@/lib/types/userProfileType';
import { ListDetailType } from '@/lib/types/listType';

import * as styles from './MemberSelector.css';

interface MemberSelectorProps {
placeholder: string;
members: UserProfileType[];
fetchData: () => Promise<void>;
followingList: UserProfileType[];
onClickAdd: (userId: number) => void;
onClickDelete: (userId: number) => void;
rules?: {
Expand All @@ -24,36 +30,38 @@ interface MemberSelectorProps {
errorMessage: string;
};
};
defaultValue?: UserProfileType[];
}

/**
* MemberSelector 컴포넌트:
* 사용자의 프로필 목록을 보여주고, 검색을 통해 사용자를 선택하는 기능 제공
*
* @param placeholder - 검색 입력란에 보여질 placeholder
* @param members - 드롭다운 보여질 사용자 프로필 목록
* @param fetchData - 검색어가 입력됨에따라 보여질 멤버 목록을 업데이트할 함수
* @param followingList - 처음 드롭다운에 보여질 팔로우한 사용자 목록
* @param onClickAdd - 선택한 멤버를 추가하는 함수
* @param onClickDelete - 사용자를 선택 취소하는 함수
*/
function MemberSelector({
placeholder,
members = [],
fetchData,
onClickAdd,
onClickDelete,
rules,
defaultValue,
}: MemberSelectorProps) {
function MemberSelector({ placeholder, followingList, onClickAdd, onClickDelete, rules }: MemberSelectorProps) {
const [input, setInput] = useState('');
const [selectedList, setSelectedList] = useState<UserProfileType[]>(defaultValue || []);
const [selectedList, setSelectedList] = useState<UserProfileType[]>([]);
const [isDropDownOpen, setIsDropDownOpen] = useState(false);

const inputRef = useRef<HTMLInputElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const listRef = useRef<HTMLDivElement>(null);

const { data: searchResult } = useQuery<UserSearchType>({
queryKey: [QUERY_KEYS.getUsersByNicknameSearch, input],
queryFn: () => getUsersByNicknameSearch(input),
});

const param = useParams<{ userId: string; listId: string }>();

const { data: listDataBeforeEdit } = useQuery<ListDetailType>({
queryKey: [QUERY_KEYS.getListDetail, param?.listId],
queryFn: () => getListDetail(Number(param?.listId)),
});

useEffect(() => {
const closeDropdown = (event: MouseEvent) => {
if (
Expand All @@ -69,18 +77,14 @@ function MemberSelector({
};
document.addEventListener('click', closeDropdown);

fetchData();

return () => {
document.removeEventListener('click', closeDropdown);
};
}, []);

useEffect(() => {
if (defaultValue) {
setSelectedList(defaultValue);
}
}, [defaultValue]);
if (listDataBeforeEdit) setSelectedList(listDataBeforeEdit.collaborators);
}, [listDataBeforeEdit]);

return (
<div className={styles.container}>
Expand All @@ -102,31 +106,70 @@ function MemberSelector({
{/* 멤버 검색 드롭다운 */}
{isDropDownOpen && (
<div className={styles.dropdown} ref={dropdownRef} style={{ height: isDropDownOpen ? '152px' : '0' }}>
{members
.filter((user) => user.nickname.toLocaleLowerCase().includes(input.toLocaleLowerCase()))
.map((user) => (
<div
key={user.id}
className={styles.profileContainer}
onClick={() => {
if (rules?.maxNum && selectedList.length >= rules.maxNum.value) {
return;
}
if (!selectedList.find((selectedUser: UserProfileType) => selectedUser.id === user.id)) {
setSelectedList([...selectedList, user]);
onClickAdd(user.id);
}
}}
>
<UserProfileImage src={user.profileImageUrl} size={30} />
{user.nickname}
{selectedList.find((collaboUser: UserProfileType) => collaboUser.id === user.id) && (
<span className={styles.checkedIcon}></span>
)}
</div>
))}
{members.filter((user) => user.nickname.toLocaleLowerCase().includes(input.toLocaleLowerCase())).length ===
0 && <div className={styles.noResultMessage}>검색결과가 없어요.</div>}
{input !== '' ? (
searchResult?.collaborators.length === 0 ? (
<>
{searchResult?.collaborators.filter((user) =>
user.nickname.toLocaleLowerCase().includes(input.toLocaleLowerCase())
).length === 0 && <div className={styles.noResultMessage}>검색결과가 없어요.</div>}
</>
) : (
<>
{searchResult?.collaborators.map((user) => (
<div
key={user.id}
className={styles.profileContainer}
onClick={() => {
if (rules?.maxNum && selectedList.length >= rules.maxNum.value) {
return;
}
if (!selectedList.find((selectedUser: UserProfileType) => selectedUser.id === user.id)) {
console.log(user);
setSelectedList([...selectedList, user]);
onClickAdd(user.id);
}
}}
>
<UserProfileImage src={user.profileImageUrl} size={30} />
{user.nickname}
{selectedList.find((collaboUser: UserProfileType) => collaboUser.id === user.id) && (
<span className={styles.checkedIcon}></span>
)}
</div>
))}
</>
)
) : followingList.length === 0 ? (
<>
{searchResult?.collaborators.filter((user) =>
user.nickname.toLocaleLowerCase().includes(input.toLocaleLowerCase())
).length === 0 && <div className={styles.noResultMessage}>검색결과가 없어요.</div>}
</>
) : (
<>
{followingList.map((user) => (
<div
key={user.id}
className={styles.profileContainer}
onClick={() => {
if (rules?.maxNum && selectedList.length >= rules.maxNum.value) {
return;
}
if (!selectedList.find((selectedUser: UserProfileType) => selectedUser.id === user.id)) {
setSelectedList([...selectedList, user]);
onClickAdd(user.id);
}
}}
>
<UserProfileImage src={user.profileImageUrl} size={30} />
{user.nickname}
{selectedList.find((collaboUser: UserProfileType) => collaboUser.id === user.id) && (
<span className={styles.checkedIcon}></span>
)}
</div>
))}
</>
)}
</div>
)}
{rules?.maxNum && selectedList.length >= rules.maxNum.value && (
Expand Down
2 changes: 1 addition & 1 deletion src/app/user/[userId]/list/[listId]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function EditPage() {
const param = useParams<{ userId: string; listId: string }>();

const { data } = useQuery<ListDetailType>({
queryKey: [QUERY_KEYS.getListDetail],
queryKey: [QUERY_KEYS.getListDetail, param?.listId],
queryFn: () => getListDetail(Number(param?.listId)),
});

Expand Down
8 changes: 8 additions & 0 deletions src/lib/types/user.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { UserProfileType } from './userProfileType';

// 로그인한 사용자 리스폰스 타입
export interface UserOnLoginType {
id: number;
Expand All @@ -10,3 +12,9 @@ export interface UserOnLoginType {
isFirst: boolean;
accessToken: string;
}

export interface UserSearchType {
collaborators: UserProfileType[];
totalCount: number;
hasNext: boolean;
}

0 comments on commit 51746bd

Please sign in to comment.