From 6fd47738be468e90ae6336a56599c3810a8dfdab Mon Sep 17 00:00:00 2001 From: Eugene Ahn <70089733+Eugene-A-01@users.noreply.github.com> Date: Wed, 14 Feb 2024 16:48:21 +0900 Subject: [PATCH] Feature/edit list page (#27) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: 콜라보레이터 20명 제한 정책 반영 * Fix: 1차QA 반영 - 페이지 이동시 카테고리 선택 풀림 해결 * Fix: 1차QA 반영 - 이메일로 콜라보레이터 검색이 불가능함을 반영 * Design: 1차QA 반영 - 리스트 생성페이지 헤더를 sticky하게 변경 * Design: 1차QA 반영 - IOS 15이상에서 button text가 blue인 이슈 해결 * Design: 1차QA 반영 - 카테고리 버튼들이 space-between이 아닌 고정간격 가지도록 수정 * Chore: 리스트 수정 경로를 위한 폴더 생성 * Chore: 팔로잉, 팔로워 목록 조회를 위한 폴더구조 만들기 * Feat: 팔로잉,팔로워 조회 페이지 공용 헤더 * Feat: 유저 프로필이미지 컴포넌트를 공용 컴포넌트로 변경 * Feat: 팔로잉, 팔로워 페이지 - 유저목록 컴포넌트 구현 * Feat: 팔로잉, 팔로워 조회 페이지 웹 퍼블리싱 완료 * Feat: 팔로잉,팔로우 페이지 - 헤더 뒤로가기 버튼 클릭시 마이피드로 이동 * Feat: 리스트 수정 페이지 - 아이템 타이틀 수정불가 정책 구현 * Feat: 헤더를 공용 컴포넌트로 분리 * Feat: 리스트 상세 조회 API * Feat: 리스트 수정 페이지 - 리스트 수정 API 보내기 전까지 (리스트 상세조회 API로 default value와 form 채워놓기) 완료 * Style: api 함수 코드 컨벤션 반영 * Fix: 리스트 상세조회 관련 타입 충돌 해결 --- src/app/_api/category/getCategories.ts | 4 +- src/app/_api/list/createList.ts | 4 +- src/app/_api/list/getListDetail.ts | 11 + src/app/_api/user/getUsers.ts | 4 +- .../list/create/_components/CreateItem.css.ts | 10 +- .../list/create/_components/CreateItem.tsx | 22 +- .../list/create/_components/CreateList.css.ts | 10 + .../list/create/_components/CreateList.tsx | 52 ++++- .../list/create/_components/item/Items.css.ts | 2 + .../list/create/_components/item/Items.tsx | 7 +- .../_components/list/ButtonSelector.css.ts | 4 +- .../_components/list/ButtonSelector.tsx | 12 +- .../create/_components/list/ColorSelector.tsx | 8 +- .../create/_components/list/Header.css.ts | 7 + .../_components/list/MemberSelector.css.ts | 12 +- .../_components/list/MemberSelector.tsx | 69 +++--- .../create/_components/list/RadioInput.tsx | 13 +- .../create/_components/list/SimpleInput.tsx | 5 +- src/app/list/create/page.tsx | 6 +- .../(follow)/_components/Header.css.ts | 25 +++ .../[userId]/(follow)/_components/Header.tsx | 26 +++ .../(follow)/_components/UserList.css.ts | 41 ++++ .../(follow)/_components/UserList.tsx | 84 ++++++++ .../[userId]/(follow)/_components/mockData.ts | 203 ++++++++++++++++++ .../user/[userId]/(follow)/followers/page.tsx | 12 ++ .../[userId]/(follow)/followings/page.tsx | 12 ++ .../user/[userId]/_components/Categories.tsx | 2 +- src/app/user/[userId]/followers/page.tsx | 3 - src/app/user/[userId]/followings/page.tsx | 3 - .../user/[userId]/list/[listId]/edit/page.tsx | 88 ++++++++ src/components/Header/Header.css.ts | 26 +++ src/components/Header/Header.tsx | 32 +++ .../UserProfileImage/UserProfileImage.css.ts | 5 + .../UserProfileImage/UserProfileImage.tsx | 36 ++++ src/lib/constants/placeholder.ts | 2 +- src/lib/types/listType.ts | 77 +++---- yarn.lock | 15 +- 37 files changed, 834 insertions(+), 120 deletions(-) create mode 100644 src/app/_api/list/getListDetail.ts create mode 100644 src/app/user/[userId]/(follow)/_components/Header.css.ts create mode 100644 src/app/user/[userId]/(follow)/_components/Header.tsx create mode 100644 src/app/user/[userId]/(follow)/_components/UserList.css.ts create mode 100644 src/app/user/[userId]/(follow)/_components/UserList.tsx create mode 100644 src/app/user/[userId]/(follow)/_components/mockData.ts create mode 100644 src/app/user/[userId]/(follow)/followers/page.tsx create mode 100644 src/app/user/[userId]/(follow)/followings/page.tsx delete mode 100644 src/app/user/[userId]/followers/page.tsx delete mode 100644 src/app/user/[userId]/followings/page.tsx create mode 100644 src/app/user/[userId]/list/[listId]/edit/page.tsx create mode 100644 src/components/Header/Header.css.ts create mode 100644 src/components/Header/Header.tsx create mode 100644 src/components/UserProfileImage/UserProfileImage.css.ts create mode 100644 src/components/UserProfileImage/UserProfileImage.tsx diff --git a/src/app/_api/category/getCategories.ts b/src/app/_api/category/getCategories.ts index 816c8f7d..92aa2dc7 100644 --- a/src/app/_api/category/getCategories.ts +++ b/src/app/_api/category/getCategories.ts @@ -1,8 +1,10 @@ import axiosInstance from '@/lib/axios/axiosInstance'; import { CategoryType } from '@/lib/types/categoriesType'; -export const getCategories = async () => { +const getCategories = async () => { const response = await axiosInstance.get('/categories'); return response.data; }; + +export default getCategories; diff --git a/src/app/_api/list/createList.ts b/src/app/_api/list/createList.ts index 21b8b696..71ac20cf 100644 --- a/src/app/_api/list/createList.ts +++ b/src/app/_api/list/createList.ts @@ -1,8 +1,10 @@ import axiosInstance from '@/lib/axios/axiosInstance'; import { ListCreateType, ListIdType } from '@/lib/types/listType'; -export const createList = async (data: ListCreateType) => { +const createList = async (data: ListCreateType) => { const response = await axiosInstance.post('/lists', data); return response.data; }; + +export default createList; diff --git a/src/app/_api/list/getListDetail.ts b/src/app/_api/list/getListDetail.ts new file mode 100644 index 00000000..0fa00be2 --- /dev/null +++ b/src/app/_api/list/getListDetail.ts @@ -0,0 +1,11 @@ +// 리스트 조회 api +import axiosInstance from '@/lib/axios/axiosInstance'; +import { ListDetailType } from '@/lib/types/listType'; + +//리스트 상세 페이지 리스트 조회 api +const getListDetail = async (listId?: number | undefined) => { + const response = await axiosInstance.get(`/lists/${listId}`); + return response.data; +}; + +export default getListDetail; diff --git a/src/app/_api/user/getUsers.ts b/src/app/_api/user/getUsers.ts index 73b94d11..60470e20 100644 --- a/src/app/_api/user/getUsers.ts +++ b/src/app/_api/user/getUsers.ts @@ -1,8 +1,10 @@ import axiosInstance from '@/lib/axios/axiosInstance'; import { UserProfilesType } from '@/lib/types/userProfileType'; -export const getUsers = async () => { +const getUsers = async () => { const response = await axiosInstance.get('/users'); return response.data; }; + +export default getUsers; diff --git a/src/app/list/create/_components/CreateItem.css.ts b/src/app/list/create/_components/CreateItem.css.ts index d272219d..d5d5c973 100644 --- a/src/app/list/create/_components/CreateItem.css.ts +++ b/src/app/list/create/_components/CreateItem.css.ts @@ -1,7 +1,15 @@ -import { style } from '@vanilla-extract/css'; +import { style, styleVariants } from '@vanilla-extract/css'; import { body1, body3 } from '@/styles/font.css'; import { vars } from '@/styles/theme.css'; +//header +export const baseButton = style([body1]); + +export const headerNextButton = styleVariants({ + active: [baseButton], + inactive: [baseButton, { color: vars.color.gray7, cursor: 'default' }], +}); + export const article = style({ padding: '16px 20px 30px', }); diff --git a/src/app/list/create/_components/CreateItem.tsx b/src/app/list/create/_components/CreateItem.tsx index 9aa4c6a8..404c63ce 100644 --- a/src/app/list/create/_components/CreateItem.tsx +++ b/src/app/list/create/_components/CreateItem.tsx @@ -1,6 +1,6 @@ import { useFormContext } from 'react-hook-form'; -import Header from './item/Header'; +import Header from '@/components/Header/Header'; import Items from './item/Items'; import * as styles from './CreateItem.css'; @@ -8,16 +8,30 @@ interface CreateItemProps { onBackClick: () => void; onSubmitClick: () => void; isSubmitting: boolean; + type: 'create' | 'edit'; } -export default function CreateItem({ onBackClick, onSubmitClick, isSubmitting }: CreateItemProps) { +export default function CreateItem({ onBackClick, onSubmitClick, isSubmitting, type }: CreateItemProps) { const { formState: { isValid }, } = useFormContext(); return (
-
+
+ 완료 + + } + />

아이템 추가 * @@ -27,7 +41,7 @@ export default function CreateItem({ onBackClick, onSubmitClick, isSubmitting }: 최소 3개, 최대 10개까지 아이템을 추가할 수 있어요.
아이템의 순서대로 순위가 정해져요.

- +

); diff --git a/src/app/list/create/_components/CreateList.css.ts b/src/app/list/create/_components/CreateList.css.ts index 8415eb67..06c37afc 100644 --- a/src/app/list/create/_components/CreateList.css.ts +++ b/src/app/list/create/_components/CreateList.css.ts @@ -9,3 +9,13 @@ export const body = style({ justifyContent: 'space-between', rowGap: '50px', }); + +export const headerNextButton = style({ + fontSize: '1.6rem', + color: '#AFB1B6', + cursor: 'default', +}); + +export const headerNextButtonActive = style({ + fontSize: '1.6rem', +}); diff --git a/src/app/list/create/_components/CreateList.tsx b/src/app/list/create/_components/CreateList.tsx index 5e20a5b3..240330d4 100644 --- a/src/app/list/create/_components/CreateList.tsx +++ b/src/app/list/create/_components/CreateList.tsx @@ -2,9 +2,11 @@ import { useEffect, useState } from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; +import { useRouter } from 'next/navigation'; import { useSearchParams } from 'next/navigation'; -import Header from './list/Header'; +// import Header from './list/Header'; +import Header from '@/components/Header/Header'; import Section from './list/Section'; import SimpleInput from './list/SimpleInput'; import ButtonSelector from './list/ButtonSelector'; @@ -19,13 +21,14 @@ 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 { getCategories } from '@/app/_api/category/getCategories'; -import { getUsers } from '@/app/_api/user/getUsers'; +import getCategories from '@/app/_api/category/getCategories'; +import getUsers from '@/app/_api/user/getUsers'; import { listDescriptionRules, listLabelRules, listTitleRules } from '@/lib/constants/formInputValidationRules'; // import { listDescription } from '@/app/[userNickname]/[listId]/_components/ListDetailOuter/ListInformation.css'; interface CreateListProps { onNextClick: () => void; + type: 'create' | 'edit'; } /** @@ -35,15 +38,16 @@ interface CreateListProps { * * @param props.onNextClick - 헤더의 '다음'버튼을 클릭했을때 동작시킬 함수 */ -function CreateList({ onNextClick }: CreateListProps) { +function CreateList({ onNextClick, type }: CreateListProps) { const [categories, setCategories] = useState([]); const [users, setUsers] = useState([]); - const { setValue, control } = useFormContext(); + const { setValue, getValues, control } = useFormContext(); const collaboIDs = useWatch({ control, name: 'collaboratorIds' }); const title = useWatch({ control, name: 'title' }); const category = useWatch({ control, name: 'category' }); + const router = useRouter(); const searchParams = useSearchParams(); const isTemplateCreation = searchParams?.has('title') && searchParams?.has('category'); @@ -76,12 +80,33 @@ function CreateList({ onNextClick }: CreateListProps) { return (
{/* 헤더 */} -
+
{ + router.back(); + }} + right={ + + } + />
{/* 리스트 제목 */}
- +
{/* 리스트 소개 */} @@ -91,6 +116,7 @@ function CreateList({ onNextClick }: CreateListProps) { name="description" placeholder={listPlaceholder.description} rules={listDescriptionRules} + defaultValue={getValues('description')} > @@ -101,7 +127,7 @@ function CreateList({ onNextClick }: CreateListProps) { onClick={(item: CategoryType) => { setValue('category', item.nameValue); }} - defaultValue={searchParams?.get('category')} + defaultValue={category} /> @@ -125,13 +151,20 @@ function CreateList({ onNextClick }: CreateListProps) { collaboIDs.filter((collaboId: number) => collaboId !== userId) ); }} + rules={{ + maxNum: { + value: 20, + errorMessage: `콜라보레이터는 최대 20명까지 지정할 수 있어요.`, + }, + }} + defaultValue={getValues('collaboratorIds')} /> {/* 배경 색상 */}
{ setValue('backgroundColor', color); @@ -149,6 +182,7 @@ function CreateList({ onNextClick }: CreateListProps) { onClick={(b: boolean) => { setValue('isPublic', b); }} + defaultValue={getValues('isPublic')} />
diff --git a/src/app/list/create/_components/item/Items.css.ts b/src/app/list/create/_components/item/Items.css.ts index c1e28674..962f63fe 100644 --- a/src/app/list/create/_components/item/Items.css.ts +++ b/src/app/list/create/_components/item/Items.css.ts @@ -47,6 +47,8 @@ export const title = style([ placeholder, { flexGrow: 1, + backgroundColor: 'transparent', + ':disabled': { cursor: 'not-allowed' }, }, ]); diff --git a/src/app/list/create/_components/item/Items.tsx b/src/app/list/create/_components/item/Items.tsx index c9bdcbd1..ff3f940b 100644 --- a/src/app/list/create/_components/item/Items.tsx +++ b/src/app/list/create/_components/item/Items.tsx @@ -26,7 +26,11 @@ const ensureHttp = (link: string) => { // return domain; // }; -export default function Items() { +interface ItemsProps { + disabled?: boolean; +} + +export default function Items({ disabled }: ItemsProps) { const [currentLink, setCurrentLink] = useState(''); const { register, @@ -107,6 +111,7 @@ export default function Items() { autoComplete="off" maxLength={100} {...register(`items.${index}.title`, itemTitleRules)} + disabled={disabled} /> } commentTextArea={ diff --git a/src/app/list/create/_components/list/ButtonSelector.css.ts b/src/app/list/create/_components/list/ButtonSelector.css.ts index f3e5fada..18e33994 100644 --- a/src/app/list/create/_components/list/ButtonSelector.css.ts +++ b/src/app/list/create/_components/list/ButtonSelector.css.ts @@ -2,9 +2,7 @@ import { style } from '@vanilla-extract/css'; export const container = style({ display: 'flex', - justifyContent: 'space-between', alignItems: 'center', - columnGap: '12px', overflow: 'auto', whiteSpace: 'nowrap', @@ -16,11 +14,13 @@ export const container = style({ export const button = style({ height: '40px', + marginRight: '12px', padding: '8px 12px', fontSize: '1.6rem', fontWeight: '600', + color: '#000', backgroundColor: 'transparent', whiteSpace: 'nowrap', diff --git a/src/app/list/create/_components/list/ButtonSelector.tsx b/src/app/list/create/_components/list/ButtonSelector.tsx index 4d119306..c93ec12c 100644 --- a/src/app/list/create/_components/list/ButtonSelector.tsx +++ b/src/app/list/create/_components/list/ButtonSelector.tsx @@ -1,11 +1,11 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import * as styles from './ButtonSelector.css'; import { CategoryType } from '@/lib/types/categoriesType'; interface ButtonSelectorProps { list: CategoryType[]; onClick: (item: CategoryType) => void; - defaultValue?: string | null; + defaultValue: string; } /** @@ -18,14 +18,18 @@ interface ButtonSelectorProps { * @param defaultValue - 기본으로 선택되어있는 요소 */ function ButtonSelector({ list, onClick, defaultValue }: ButtonSelectorProps) { - const [selectedButton, setSelectedButton] = useState(defaultValue || ''); + const [selectedButton, setSelectedButton] = useState(defaultValue); + + useEffect(() => { + setSelectedButton(defaultValue); + }, [defaultValue]); return (
{list.map((item) => ( +
+ ); +} + +interface UserListProps { + type: 'follower' | 'following'; + list: UserProfileType[]; +} + +const data = { + follower: { + emptyMessage: '아직은 팔로워가 없어요', + button: { + name: '삭제', + onClick: () => { + console.log('팔로워 삭제'); //삭제 예정 + }, + }, + }, + following: { + emptyMessage: '아직 팔로우한 사람이 없어요', + button: { + name: '취소', + onClick: () => { + console.log('팔로잉 취소'); //삭제 예정 + }, + }, + }, +}; + +function UserList({ type, list }: UserListProps) { + return ( +
+ {list.length === 0 ? ( +
{data[type].emptyMessage}
+ ) : ( + list.map((user) => ( + + )) + )} +
+ ); +} + +export default UserList; diff --git a/src/app/user/[userId]/(follow)/_components/mockData.ts b/src/app/user/[userId]/(follow)/_components/mockData.ts new file mode 100644 index 00000000..a295a56b --- /dev/null +++ b/src/app/user/[userId]/(follow)/_components/mockData.ts @@ -0,0 +1,203 @@ +interface UserProfileType { + id: number; + profileImageUrl: string; + nickname: string; +} + +export const mockFollowers: UserProfileType[] = [ + { + id: 1, + profileImageUrl: 'https://randomuser.me/api/portraits/men/1.jpg', + nickname: 'JohnDoe1', + }, + { + id: 2, + profileImageUrl: 'https://randomuser.me/api/portraits/women/2.jpg', + nickname: 'JaneDoe2', + }, + { + id: 3, + profileImageUrl: 'https://randomuser.me/api/portraits/men/3.jpg', + nickname: 'BobSmith3', + }, + { + id: 4, + profileImageUrl: 'https://randomuser.me/api/portraits/women/4.jpg', + nickname: 'AliceJohnson4', + }, + { + id: 5, + profileImageUrl: 'https://randomuser.me/api/portraits/men/5.jpg', + nickname: 'CharlieBrown5', + }, + { + id: 6, + profileImageUrl: 'https://randomuser.me/api/portraits/women/6.jpg', + nickname: 'EmmaWatson6', + }, + { + id: 7, + profileImageUrl: 'https://randomuser.me/api/portraits/men/7.jpg', + nickname: 'DavidMiller7', + }, + { + id: 8, + profileImageUrl: 'https://randomuser.me/api/portraits/women/8.jpg', + nickname: 'OliviaSmith8', + }, + { + id: 9, + profileImageUrl: 'https://randomuser.me/api/portraits/men/9.jpg', + nickname: 'SophiaJohnson9', + }, + { + id: 10, + profileImageUrl: 'https://randomuser.me/api/portraits/women/10.jpg', + nickname: 'LiamBrown10', + }, + { + id: 11, + profileImageUrl: 'https://randomuser.me/api/portraits/men/11.jpg', + nickname: 'AvaDavis11', + }, + { + id: 12, + profileImageUrl: 'https://randomuser.me/api/portraits/women/12.jpg', + nickname: 'NoahWilliams12', + }, + { + id: 13, + profileImageUrl: 'https://randomuser.me/api/portraits/men/13.jpg', + nickname: 'IsabellaMoore13', + }, + { + id: 14, + profileImageUrl: 'https://randomuser.me/api/portraits/women/14.jpg', + nickname: 'LucasTaylor14', + }, + { + id: 15, + profileImageUrl: 'https://randomuser.me/api/portraits/men/15.jpg', + nickname: 'MiaAnderson15', + }, + { + id: 16, + profileImageUrl: 'https://randomuser.me/api/portraits/women/16.jpg', + nickname: 'EthanThomas16', + }, + { + id: 17, + profileImageUrl: 'https://randomuser.me/api/portraits/men/17.jpg', + nickname: 'AriaMartinez17', + }, + { + id: 18, + profileImageUrl: 'https://randomuser.me/api/portraits/women/18.jpg', + nickname: 'LoganJackson18', + }, + { + id: 19, + profileImageUrl: 'https://randomuser.me/api/portraits/men/19.jpg', + nickname: 'ChloeWhite19', + }, + { + id: 20, + profileImageUrl: 'https://randomuser.me/api/portraits/women/20.jpg', + nickname: 'ElijahHill20', + }, + { + id: 21, + profileImageUrl: 'https://randomuser.me/api/portraits/men/21.jpg', + nickname: 'AbigailCarter21', + }, + { + id: 22, + profileImageUrl: 'https://randomuser.me/api/portraits/women/22.jpg', + nickname: 'CarterLee22', + }, + { + id: 23, + profileImageUrl: 'https://randomuser.me/api/portraits/men/23.jpg', + nickname: 'GraceKing23', + }, + { + id: 24, + profileImageUrl: 'https://randomuser.me/api/portraits/women/24.jpg', + nickname: 'JacksonScott24', + }, + { + id: 25, + profileImageUrl: 'https://randomuser.me/api/portraits/men/25.jpg', + nickname: 'LilyAdams25', + }, + { + id: 26, + profileImageUrl: 'https://randomuser.me/api/portraits/women/26.jpg', + nickname: 'AidenWilson26', + }, + { + id: 27, + profileImageUrl: 'https://randomuser.me/api/portraits/men/27.jpg', + nickname: 'ZoeEvans27', + }, + { + id: 28, + profileImageUrl: 'https://randomuser.me/api/portraits/women/28.jpg', + nickname: 'MasonHall28', + }, + { + id: 29, + profileImageUrl: 'https://randomuser.me/api/portraits/men/29.jpg', + nickname: 'MadisonWard29', + }, + { + id: 30, + profileImageUrl: 'https://randomuser.me/api/portraits/women/30.jpg', + nickname: 'EzraBrooks30', + }, + { + id: 31, + profileImageUrl: 'https://randomuser.me/api/portraits/men/31.jpg', + nickname: 'AveryBennett31', + }, + { + id: 32, + profileImageUrl: 'https://randomuser.me/api/portraits/women/32.jpg', + nickname: 'WyattMiller32', + }, + { + id: 33, + profileImageUrl: 'https://randomuser.me/api/portraits/men/33.jpg', + nickname: 'ScarlettFisher33', + }, + { + id: 34, + profileImageUrl: 'https://randomuser.me/api/portraits/women/34.jpg', + nickname: 'NathanSimmons34', + }, + { + id: 35, + profileImageUrl: 'https://randomuser.me/api/portraits/men/35.jpg', + nickname: 'HannahLopez35', + }, + { + id: 36, + profileImageUrl: 'https://randomuser.me/api/portraits/women/36.jpg', + nickname: 'JackCooper36', + }, + { + id: 37, + profileImageUrl: 'https://randomuser.me/api/portraits/men/37.jpg', + nickname: 'AddisonFoster37', + }, + { + id: 38, + profileImageUrl: 'https://randomuser.me/api/portraits/women/38.jpg', + nickname: 'AndrewMurray38', + }, + { + id: 39, + profileImageUrl: 'https://randomuser.me/api/portraits/men/39.jpg', + nickname: 'Joe', + }, +]; diff --git a/src/app/user/[userId]/(follow)/followers/page.tsx b/src/app/user/[userId]/(follow)/followers/page.tsx new file mode 100644 index 00000000..79aa3ebd --- /dev/null +++ b/src/app/user/[userId]/(follow)/followers/page.tsx @@ -0,0 +1,12 @@ +import Header from '../_components/Header'; +import UserList from '../_components/UserList'; +import { mockFollowers } from '../_components/mockData'; + +export default function FollowersPage() { + return ( +
+
+ +
+ ); +} diff --git a/src/app/user/[userId]/(follow)/followings/page.tsx b/src/app/user/[userId]/(follow)/followings/page.tsx new file mode 100644 index 00000000..b7fd0c1e --- /dev/null +++ b/src/app/user/[userId]/(follow)/followings/page.tsx @@ -0,0 +1,12 @@ +import Header from '../_components/Header'; +import UserList from '../_components/UserList'; +import { mockFollowers } from '../_components/mockData'; + +export default function FollowingPage() { + return ( +
+
+ +
+ ); +} diff --git a/src/app/user/[userId]/_components/Categories.tsx b/src/app/user/[userId]/_components/Categories.tsx index 168b52b7..cb126587 100644 --- a/src/app/user/[userId]/_components/Categories.tsx +++ b/src/app/user/[userId]/_components/Categories.tsx @@ -9,7 +9,7 @@ import { useQuery } from '@tanstack/react-query'; import * as styles from './Categories.css'; -import { getCategories } from '@/app/_api/category/getCategories'; +import getCategories from '@/app/_api/category/getCategories'; import { CategoryType } from '@/lib/types/categoriesType'; import { QUERY_KEYS } from '@/lib/constants/queryKeys'; diff --git a/src/app/user/[userId]/followers/page.tsx b/src/app/user/[userId]/followers/page.tsx deleted file mode 100644 index 92d06a47..00000000 --- a/src/app/user/[userId]/followers/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function FollowersPage() { - return
팔로워페이지
; -} diff --git a/src/app/user/[userId]/followings/page.tsx b/src/app/user/[userId]/followings/page.tsx deleted file mode 100644 index 7ef0ab86..00000000 --- a/src/app/user/[userId]/followings/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function FollowersPage() { - return
팔로잉페이지
; -} diff --git a/src/app/user/[userId]/list/[listId]/edit/page.tsx b/src/app/user/[userId]/list/[listId]/edit/page.tsx new file mode 100644 index 00000000..5529662a --- /dev/null +++ b/src/app/user/[userId]/list/[listId]/edit/page.tsx @@ -0,0 +1,88 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { FieldErrors, FormProvider, useForm } from 'react-hook-form'; +import { useQuery } from '@tanstack/react-query'; +import { useParams } from 'next/navigation'; + +import CreateItem from '@/app/list/create/_components/CreateItem'; +import CreateList from '@/app/list/create/_components/CreateList'; + +import { QUERY_KEYS } from '@/lib/constants/queryKeys'; +import getListDetail from '@/app/_api/list/getListDetail'; +import { ListDetailType, ListEditType } from '@/lib/types/listType'; + +export type FormErrors = FieldErrors; + +export default function EditPage() { + const [step, setStep] = useState<'list' | 'item'>('list'); + const param = useParams<{ userId: string; listId: string }>(); + + const { data } = useQuery({ + queryKey: [QUERY_KEYS.getListDetail], + queryFn: () => getListDetail(Number(param?.listId)), + }); + + const methods = useForm({ + mode: 'onChange', + defaultValues: { + ownerId: Number(param?.userId), + category: 'culture', + labels: [], + collaboratorIds: [], + title: '', + description: '', + isPublic: true, + backgroundColor: '#ffffff', + items: [], + }, + }); + + const handleStepChange = (step: 'list' | 'item') => { + setStep(step); + }; + + const handleSubmit = () => { + console.log('제출'); + }; + + useEffect(() => { + if (data) { + methods.reset({ + ownerId: data.ownerId, + category: data.category, + labels: data.labels.map((obj) => obj.name), + collaboratorIds: data.collaborators, + title: data.title, + description: data.description, + isPublic: data.isPublic, + backgroundColor: data.backgroundColor, + items: data.items, + }); + } + }, [data, methods]); + + return ( + <> + + {step === 'list' ? ( + { + handleStepChange('item'); + }} + type="edit" + /> + ) : ( + { + handleStepChange('list'); + }} + onSubmitClick={handleSubmit} //3차 MVP 수정필요 + isSubmitting={false} //3차 MVP 수정필요 + type="edit" + /> + )} + + + ); +} diff --git a/src/components/Header/Header.css.ts b/src/components/Header/Header.css.ts new file mode 100644 index 00000000..df7fa84a --- /dev/null +++ b/src/components/Header/Header.css.ts @@ -0,0 +1,26 @@ +import { style } from '@vanilla-extract/css'; + +export const header = style({ + width: '100%', + height: '90px', + paddingLeft: '20px', + paddingRight: '20px', + + position: 'sticky', + top: '0', + left: '0', + zIndex: '10', + + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + + backgroundColor: '#fff', + + borderBottom: '1px solid rgba(0, 0, 0, 0.10)', +}); + +export const headerTitle = style({ + fontSize: '2rem', +}); diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 00000000..c71ee03d --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,32 @@ +'use client'; +import { ReactNode } from 'react'; + +import CloseButton from '/public/icons/close_button.svg'; +import BackIcon from '/public/icons/back.svg'; + +import * as styles from './Header.css'; + +interface HeaderProps { + title: string; + left?: 'close' | 'back'; + leftClick?: () => void; + right: ReactNode; +} + +function Header({ title, left, leftClick, right }: HeaderProps) { + return ( +
+ + +

{title}

+ + {right} +
+ ); +} + +export default Header; diff --git a/src/components/UserProfileImage/UserProfileImage.css.ts b/src/components/UserProfileImage/UserProfileImage.css.ts new file mode 100644 index 00000000..bc533fdb --- /dev/null +++ b/src/components/UserProfileImage/UserProfileImage.css.ts @@ -0,0 +1,5 @@ +import { style } from '@vanilla-extract/css'; + +export const profileImage = style({ + borderRadius: '50%', +}); diff --git a/src/components/UserProfileImage/UserProfileImage.tsx b/src/components/UserProfileImage/UserProfileImage.tsx new file mode 100644 index 00000000..6f9bb8b7 --- /dev/null +++ b/src/components/UserProfileImage/UserProfileImage.tsx @@ -0,0 +1,36 @@ +import { useEffect, useState } from 'react'; +import Image from 'next/image'; + +import DefaultProfile from '/public/icons/default_profile.svg'; +import axiosInstance from '@/lib/axios/axiosInstance'; +import * as styles from './UserProfileImage.css'; +import axios from 'axios'; + +function UserProfileImage({ src, size }: { src: string; size: number }) { + const [isValidImage, setIsValidImage] = useState(false); + + useEffect(() => { + const handleFetchImage = async () => { + if (!src) { + setIsValidImage(false); + return; + } + try { + await axios.get(src); + } catch (error) { + setIsValidImage(false); + return; + } + setIsValidImage(true); + }; + handleFetchImage(); + }, []); + + return isValidImage ? ( + 이미지 프로필 + ) : ( + + ); +} + +export default UserProfileImage; diff --git a/src/lib/constants/placeholder.ts b/src/lib/constants/placeholder.ts index 56e0ce7f..6a075fb3 100644 --- a/src/lib/constants/placeholder.ts +++ b/src/lib/constants/placeholder.ts @@ -2,7 +2,7 @@ export const listPlaceholder = { title: '리스트 제목을 적어주세요', description: '리스트에 대한 간단한 소개를 작성해주세요.', label: '라벨 입력 후 Enter를 눌러주세요. (최대 3개)', - collaborator: '닉네임 또는 이메일을 입력해주세요.', + collaborator: '닉네임을 입력해주세요.', }; //item diff --git a/src/lib/types/listType.ts b/src/lib/types/listType.ts index dcfd1ce6..6d217409 100644 --- a/src/lib/types/listType.ts +++ b/src/lib/types/listType.ts @@ -1,3 +1,5 @@ +import { UserProfileType } from './userProfileType'; + // 아이템 생성 타입 export interface ItemCreateType { rank: number; @@ -20,6 +22,19 @@ export interface ListCreateType { items: ItemCreateType[]; } +// 리스트 수정 타입 +export interface ListEditType { + ownerId: number; + category: string; + labels: string[]; + collaboratorIds: UserProfileType[]; + title: string; + description: string; + isPublic: boolean; + backgroundColor: string; + items: ItemType[]; +} + export interface ListIdType { listId: number; } @@ -43,25 +58,33 @@ export interface PresignedUrlType { export type PresignedUrlListType = PresignedUrlType[]; -//리스트 상세조회 타입 -export interface LabelType { - id: number; - name: string; +// 리스트 전체 조회 타입 +export interface AllListType { + cursorId: number; + hasNext: boolean; + feedLists: ListType[]; } -export interface CollaboratorType { - id?: number; - userId?: number; - userProfileImageUrl: string; +export interface ListType { + id: number; + title: string; + isPublic: boolean; + backgroundColor: string; + listItems: Omit[]; } -export interface ListItemsType { +// 리스트 상세조회 타입 +export interface ItemType { id: number; rank: number; title: string; - comment: string; - link: string; - imageUrl: string; + comment?: string; + link?: string; + imageUrl?: string; +} + +export interface LabelType { + name: string; } export interface ListDetailType { @@ -70,37 +93,15 @@ export interface ListDetailType { title: string; description: string; createdDate: Date; - lastUpdatedDated: Date; + lastUpdatedDate: Date; ownerId: number; + ownerNickname: string; ownerProfileImageUrl: string; - Collaborators: CollaboratorType[]; - items: ListItemsType[]; + collaborators: UserProfileType[]; + items: ItemType[]; isCollected: boolean; isPublic: boolean; backgroundColor: string; collectCount: number; viewCount: number; } -// 리스트 전체 조회 타입 -export interface AllListType { - cursorId: number; - hasNext: boolean; - feedLists: ListType[]; -} - -export interface ListType { - id: number; - title: string; - isPublic: boolean; - backgroundColor: string; - listItems: Omit[]; -} - -export interface ItemType { - id: number; - ranking: number; - title: string; - comment?: string; - link?: string; - imageUrl?: string; -} diff --git a/yarn.lock b/yarn.lock index a0929a66..8d9465b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1121,7 +1121,14 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": + version "7.23.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.8.tgz#8ee6fe1ac47add7122902f257b8ddf55c898f650" + integrity sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/runtime@^7.12.0", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": version "7.23.9" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw== @@ -10291,9 +10298,9 @@ postcss@^7.0.35: source-map "^0.6.1" postcss@^8.3.5, postcss@^8.4.23, postcss@^8.4.32, postcss@^8.4.33, postcss@^8.4.4: - version "8.4.34" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.34.tgz#563276e86b4ff20dfa5eed0d394d4c53853b2051" - integrity sha512-4eLTO36woPSocqZ1zIrFD2K1v6wH7pY1uBh0JIM2KKfrVtGvPFiAku6aNOP0W1Wr9qwnaCsF0Z+CrVnryB2A8Q== + version "8.4.33" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742" + integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg== dependencies: nanoid "^3.3.7" picocolors "^1.0.0"