From c3b990fab5d250879215a94d6757efa51cb0fe1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=98=84=EC=A7=80?= <96380950+kanglocal@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:17:13 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=95=88=EC=AA=BD=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=ED=8D=BC=EB=B8=94?= =?UTF-8?q?=EB=A6=AC=EC=8B=B1=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Design : 리스트 상세페이지 내부의 rankList 컴포넌트 레이아웃 생성 * Design : 리스트 상세페이지 내부의 rankList 컴포넌트 스타일 적용 * Fix : 리스트 상세페이지 내부의 rankList 컴포넌트 스타일 및 타입 수정 * Design : 리스트 상세페이지 내부의 rankList 컴포넌트 스타일 수정 * Design : 리스트 상세페이지 내부의 rankList 컴포넌트에 바텀시트 추가 * Chore : open-graph 라이브러리 설치 * Chore : open-graph 라이브러리 타입 설치 * Chore : cheerio 라이브러리 설치 * feat : 링크 미리보기 기능 일부 구현 (백엔드와 협의 필요) * style : 링크 미리보기 코드 수정 * Design : rankList 스타일 변경 * Feature : 비디오 임베드 추가 및 스타일 변경 * Style : 주석 수정 * Chore : copy-to-clipboard 라이브러리 다운로드 * Feature : 링크 다운로드 기능 구현 * Chore : react-toastify 다운로드 * Feature : 토스트 구현 * Chore : html-to-image 라이브러리 다운로드 * Feature : 리스트상세의 이미지 저장 기능 구현 초안 * Feature : 카카오톡 공유기 초안 * Feature : OG태그 불러오는 로직을 프록시로 이동 * Fix : 목데이터 제거 및 코드 정리 * Fix : 프록시 설정 개발중이기에 localhost:3000 으로 적었으나 이후 배포서버도메인으로 수정하겠습니다. * Fix : 컴포넌트명 변경 추후 Dropdown 컴포넌트가 생길것으로 예상되어 SelectComponent 로 변경했습니다. * Fix : 컴포넌트명 변경 추후 Dropdown 컴포넌트가 생길것으로 예상되어 SelectComponent 로 변경했습니다. * Style : css파일 컨벤션에 맞춰 수정 및 폴더 구조 변경 * fix : layout.tsx에 카카오 sdk, toastContainer 추가 * fix : 카카오톡 공유 템플릿 변경 * fix : 링크 미리보기 수정 일부 og:url 태그가 없는 경우 링크 이동이 안되는 것을 방지하기 위해 인자로받은 linkUrl 사용하도록 수정 * Design : rankList css 수정 * fix : img태그 -> Image로 수정 * Chore : yarn.lock 커밋 * fix : img태그 -> Image태그 * fix : 사용하지 않는 파일 삭제 * fix : Image -> img * Design : 나현님 css에맞춰 수정 * Style : 불필요한 console.log 삭제 * Style : import 순서 변경 * Style : CI/CD에서 잡아준 에러로 수정했습니다. * Style : TODO 주석 생성 * Style : CI/CD 에러 수정 * Style : 오타 수정 * Style : TODO 주석 생성 * Style : CI/CD에러 수정 --- package.json | 9 + public/icons/check_red.svg | 4 +- public/icons/collect.svg | 3 + public/icons/crown.svg | 4 + public/icons/etc.svg | 3 + public/icons/share.svg | 3 + .../BottomSheet/BottomSheet.css.tsx | 74 ++++++++ .../_components/ListDetailInner/Footer.css.ts | 23 +++ .../_components/ListDetailInner/Footer.tsx | 143 +++++++++++++++ .../_components/ListDetailInner/Header.css.ts | 9 + .../_components/ListDetailInner/Header.tsx | 31 ++++ .../ListDetailInner/RankList.css.ts | 163 ++++++++++++++++++ .../_components/ListDetailInner/RankList.tsx | 113 ++++++++++++ .../_components/ListDetailInner/index.css.ts | 25 +++ .../_components/ListDetailInner/index.tsx | 72 ++++++++ .../_components/ListDetailOuter/Header.tsx | 2 +- src/app/create/_components/CreateList.tsx | 8 +- src/app/layout.tsx | 21 ++- .../KakaotalkShare/kakaotalkShare.tsx | 46 +++++ src/components/LinkPreview/LinkPreview.tsx | 53 ++++++ src/components/LinkPreview/style.css.ts | 51 ++++++ .../SelectComponent/SelectComponent.tsx | 65 +++++++ src/components/VideoEmbed/VideoEmbed.tsx | 72 ++++++++ src/components/VideoEmbed/style.css.ts | 17 ++ src/lib/utils/copyUrl.ts | 13 ++ src/lib/utils/saveImageFromHtml.ts | 31 ++++ src/lib/utils/toasting.ts | 23 +++ src/pages/api/getOgDataProxy.ts | 35 ++++ yarn.lock | 158 ++++++++++++++++- 29 files changed, 1264 insertions(+), 10 deletions(-) create mode 100644 public/icons/collect.svg create mode 100644 public/icons/crown.svg create mode 100644 public/icons/etc.svg create mode 100644 public/icons/share.svg create mode 100644 src/app/[userNickname]/[listId]/_components/BottomSheet/BottomSheet.css.tsx create mode 100644 src/app/[userNickname]/[listId]/_components/ListDetailInner/Footer.css.ts create mode 100644 src/app/[userNickname]/[listId]/_components/ListDetailInner/Footer.tsx create mode 100644 src/app/[userNickname]/[listId]/_components/ListDetailInner/Header.css.ts create mode 100644 src/app/[userNickname]/[listId]/_components/ListDetailInner/Header.tsx create mode 100644 src/app/[userNickname]/[listId]/_components/ListDetailInner/RankList.css.ts create mode 100644 src/app/[userNickname]/[listId]/_components/ListDetailInner/RankList.tsx create mode 100644 src/app/[userNickname]/[listId]/_components/ListDetailInner/index.css.ts create mode 100644 src/app/[userNickname]/[listId]/_components/ListDetailInner/index.tsx create mode 100644 src/components/KakaotalkShare/kakaotalkShare.tsx create mode 100644 src/components/LinkPreview/LinkPreview.tsx create mode 100644 src/components/LinkPreview/style.css.ts create mode 100644 src/components/SelectComponent/SelectComponent.tsx create mode 100644 src/components/VideoEmbed/VideoEmbed.tsx create mode 100644 src/components/VideoEmbed/style.css.ts create mode 100644 src/lib/utils/copyUrl.ts create mode 100644 src/lib/utils/saveImageFromHtml.ts create mode 100644 src/lib/utils/toasting.ts create mode 100644 src/pages/api/getOgDataProxy.ts diff --git a/package.json b/package.json index 605f0417..83628230 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "temp", "version": "0.1.0", "private": true, + "proxy": "http://localhost:3000", "scripts": { "dev": "next dev", "build": "next build", @@ -31,12 +32,19 @@ "@vanilla-extract/next-plugin": "^2.3.2", "@yaireo/tagify": "^4.19.0", "axios": "^1.6.5", + "cheerio": "^1.0.0-rc.12", + "copy-to-clipboard": "^3.3.3", + "html-to-image": "^1.11.11", + "http-proxy-middleware": "^2.0.6", "next": "14.0.4", + "open-graph": "^0.2.6", "react": "^18", "react-beautiful-dnd": "^13.1.1", "react-dom": "^18", "react-hook-form": "^7.50.0", "react-scripts": "^5.0.1", + "react-select": "^5.8.0", + "react-toastify": "^10.0.4", "zustand": "^4.4.7" }, "devDependencies": { @@ -51,6 +59,7 @@ "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/jest": "^29.5.11", "@types/node": "^20", + "@types/open-graph": "^0.2.5", "@types/react": "^18", "@types/react-beautiful-dnd": "^13.1.8", "@types/react-dom": "^18", diff --git a/public/icons/check_red.svg b/public/icons/check_red.svg index ed672394..a5f194df 100644 --- a/public/icons/check_red.svg +++ b/public/icons/check_red.svg @@ -1,3 +1,3 @@ - - + + \ No newline at end of file diff --git a/public/icons/collect.svg b/public/icons/collect.svg new file mode 100644 index 00000000..06f5d2b0 --- /dev/null +++ b/public/icons/collect.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/crown.svg b/public/icons/crown.svg new file mode 100644 index 00000000..f5e3e5c2 --- /dev/null +++ b/public/icons/crown.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/etc.svg b/public/icons/etc.svg new file mode 100644 index 00000000..9c2c3720 --- /dev/null +++ b/public/icons/etc.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/share.svg b/public/icons/share.svg new file mode 100644 index 00000000..c4a8a674 --- /dev/null +++ b/public/icons/share.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/[userNickname]/[listId]/_components/BottomSheet/BottomSheet.css.tsx b/src/app/[userNickname]/[listId]/_components/BottomSheet/BottomSheet.css.tsx new file mode 100644 index 00000000..e2734537 --- /dev/null +++ b/src/app/[userNickname]/[listId]/_components/BottomSheet/BottomSheet.css.tsx @@ -0,0 +1,74 @@ +import { keyframes, style } from '@vanilla-extract/css'; + +export const backGround = style({ + position: 'fixed', + top: 0, + left: 0, + bottom: 0, + right: 0, + background: 'rgba(0,0,0,0.3)', + zIndex: 999, +}); + +export const wrapper = style({ + padding: '37px 0 43px', + + position: 'fixed', + bottom: 0, + left: 0, + right: 0, + + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + + backgroundColor: '#ffffff', + borderTopLeftRadius: '25px', + borderTopRightRadius: '25px', + + transitionProperty: 'all', + transitionDuration: '0.2s', +}); + +const slideIn = keyframes({ + from: { transform: 'translateY(100%)' }, + to: { transform: 'translateY(0)' }, +}); + +export const sheetActive = style({ + animation: `${slideIn} 0.2s ease-in-out`, +}); + +export const sheetItemWrapper = style({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + + ':hover': { + backgroundColor: '#EFEFF0', + }, +}); + +export const checkIcon = style({ + display: 'none', + marginRight: '28px', + + selectors: { + [`${sheetItemWrapper}:hover &`]: { + display: 'block', + }, + }, +}); + +export const sheetItem = style({ + width: '100%', + fontSize: '1.4rem', + cursor: 'pointer', + padding: '2.5rem 2.8rem 2.5rem', + + selectors: { + [`${sheetItemWrapper}:hover &`]: { + color: '#FF5454', + }, + }, +}); diff --git a/src/app/[userNickname]/[listId]/_components/ListDetailInner/Footer.css.ts b/src/app/[userNickname]/[listId]/_components/ListDetailInner/Footer.css.ts new file mode 100644 index 00000000..1413050f --- /dev/null +++ b/src/app/[userNickname]/[listId]/_components/ListDetailInner/Footer.css.ts @@ -0,0 +1,23 @@ +import { style } from '@vanilla-extract/css'; + +export const container = style({ + width: '100%', + + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', +}); + +export const shareAndOthers = style({ + width: '100%', + + display: 'flex', + flexDirection: 'row', + justifyContent: 'right', + alignItems: 'center', + gap: '20px', +}); + +export const buttonComponent = style({ + cursor: 'pointer', +}); diff --git a/src/app/[userNickname]/[listId]/_components/ListDetailInner/Footer.tsx b/src/app/[userNickname]/[listId]/_components/ListDetailInner/Footer.tsx new file mode 100644 index 00000000..9efa9ce8 --- /dev/null +++ b/src/app/[userNickname]/[listId]/_components/ListDetailInner/Footer.tsx @@ -0,0 +1,143 @@ +'use client'; + +import { useParams, useRouter } from 'next/navigation'; +import { MouseEvent, useState } from 'react'; +import BottomSheet from '@/app/[userNickname]/[listId]/_components/BottomSheet/BottomSheet'; +import ModalPortal from '@/components/ModalPortal'; +import saveImageFromHtml from '@/lib/utils/saveImageFromHtml'; +import copyUrl from '@/lib/utils/copyUrl'; +import toasting from '@/lib/utils/toasting'; +import kakaotalkShare from '@/components/KakaotalkShare/kakaotalkShare'; +import * as styles from './Footer.css'; +import CollectIcon from '/public/icons/collect.svg'; +import ShareIcon from '/public/icons/share.svg'; +import EtcIcon from '/public/icons/etc.svg'; + +interface BottomSheetOptionsProps { + key: string; + title: string; + onClick: () => void; +} + +interface SheetTypeProps { + type: 'share' | 'etc'; +} + +interface FooterProps { + category: string; + listId: string; + title: string; + description: string; + items: []; + collaborators: []; + ownerNickname: string; +} + +function Footer({ data }: { data: FooterProps }) { + const router = useRouter(); + const params = useParams<{ userNickname: string; listId: string }>(); + + const [isSheetActive, setSheetActive] = useState(false); + const [sheetOptionList, setSheetOptionList] = useState([]); + + const handleSheetOptionList = ({ type }: SheetTypeProps) => { + const listUrl = `${process.env.NEXT_PUBLIC_BASE_URL}/${params?.userNickname}/${params?.listId}`; + + if (type === 'share') { + const optionList = [ + { + key: 'copyLink', + title: '리스트 링크 복사하기', + onClick: () => { + copyUrl(listUrl); + setSheetActive(false); + }, + }, + { + key: 'kakaoShare', + title: '리스트 카카오톡으로 공유하기', + onClick: () => { + // TODO: image로 저장한다음에 해당 image를 보내줘야한다. + kakaotalkShare({ + title: data.title, + description: data.description, + image: + 'https://i.namu.wiki/i/-8Iah6PGZzzQuY1KtJIbj8_KBbX4whnbaq8AYShoqphdJOpfJDskZZ2Y3bU2I5Jpnx8aRi1LXTz1_e0v_fMrp172modjOmKRcxcME5dmM6IDAIgqktw5yIs75is2CgC1GrGoxZPwxpeTXudKIxWn2w.webp', + listItem: data.items, + collaborators: data.collaborators, + listId: data.listId, + userNickname: data.ownerNickname, + }); + setSheetActive(false); + }, + }, + ]; + setSheetOptionList([...optionList]); + return; + } + + if (type === 'etc') { + const optionList = [ + { + key: 'saveToImg', + title: '리스트 이미지로 저장하기', + onClick: () => { + setSheetActive(false); + saveImageFromHtml({ filename: `${data.category}_${data.listId}` }); + }, + }, + { + key: 'copyAndCreateList', + title: '이 리스트 템플릿으로 바로 리스트 작성하기', + onClick: () => { + toasting({ type: 'default', txt: '리스트 작성 페이지로 이동합니다.' }); + router.push(`/create?title=${data.title}&category=${data.category}`); + }, + }, + ]; + setSheetOptionList([...optionList]); + return; + } + }; + + const handleSheetActive = ({ type }: SheetTypeProps) => { + handleSheetOptionList({ type }); + setSheetActive((prev: boolean) => !prev); + }; + + const handleOutsideClick = (e: MouseEvent) => { + if (e.target === e.currentTarget) { + setSheetActive(false); + } + }; + + // TODO: 콜렉트 API생성되면 요청보내고 UI변경시키기 + const handleCollect = () => { + console.log('콜렉트기능 미구현'); + }; + + return ( + <> + {isSheetActive && ( + + + + )} +
+
+ +
+
+
handleSheetActive({ type: 'share' })}> + +
+
handleSheetActive({ type: 'etc' })}> + +
+
+
+ + ); +} + +export default Footer; diff --git a/src/app/[userNickname]/[listId]/_components/ListDetailInner/Header.css.ts b/src/app/[userNickname]/[listId]/_components/ListDetailInner/Header.css.ts new file mode 100644 index 00000000..c0b4b806 --- /dev/null +++ b/src/app/[userNickname]/[listId]/_components/ListDetailInner/Header.css.ts @@ -0,0 +1,9 @@ +import { style } from '@vanilla-extract/css'; + +export const container = style({ + width: '100%', + + display: 'flex', + justifyContent: 'right', + alignItems: 'center', +}); diff --git a/src/app/[userNickname]/[listId]/_components/ListDetailInner/Header.tsx b/src/app/[userNickname]/[listId]/_components/ListDetailInner/Header.tsx new file mode 100644 index 00000000..5b23ed65 --- /dev/null +++ b/src/app/[userNickname]/[listId]/_components/ListDetailInner/Header.tsx @@ -0,0 +1,31 @@ +import * as styles from './Header.css'; +import SelectComponent from '@/components/SelectComponent/SelectComponent'; + +interface OptionsProps { + value: string; + label: string; +} + +interface HeaderProps { + handleChangeListType: (target: OptionsProps) => void | undefined; +} + +const dropdownOptions = [ + { + value: 'simple', + label: '간단히', + }, + { + value: 'detail', + label: '자세히', + }, +]; +function Header({ handleChangeListType }: HeaderProps) { + return ( +
+ +
+ ); +} + +export default Header; diff --git a/src/app/[userNickname]/[listId]/_components/ListDetailInner/RankList.css.ts b/src/app/[userNickname]/[listId]/_components/ListDetailInner/RankList.css.ts new file mode 100644 index 00000000..4217e9e7 --- /dev/null +++ b/src/app/[userNickname]/[listId]/_components/ListDetailInner/RankList.css.ts @@ -0,0 +1,163 @@ +import { style } from '@vanilla-extract/css'; + +export const container = style({ + width: '100%', + display: 'flex', + justifyContent: 'left', + alignItems: 'center', +}); + +export const listWrapper = style({ + width: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + gap: '1.6rem', + alignItems: 'left', +}); + +export const simpleItemWrapper = style({ + width: '100%', + display: 'flex', + justifyContent: 'space-between', + gap: '4rem', + alignItems: 'center', +}); + +export const detailItemWrapper = style({ + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: '1.6rem', + alignItems: 'left', + marginBottom: '6rem', + ':last-child': { + marginBottom: 0, + }, +}); + +export const commentText = style({ + width: '100%', + backgroundColor: 'white', + padding: '2rem', + border: '1px solid #EFEFF0', + borderRadius: '10px', + fontSize: '1.4rem', +}); + +export const rankAndTitle = style({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-start', + gap: '2rem', +}); + +export const rankTextWrapper = style({ + width: '40px', + height: '40px', + + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + + fontSize: '2.4rem', + fontWeight: 'bold', + textAlign: 'center', +}); + +export const firstRankTextWrapper = style({ + width: '40px', + height: '40px', + + position: 'relative', + + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + + backgroundColor: '#0047FF', + borderRadius: '99px', + + fontSize: '2.4rem', + fontWeight: 'bold', + color: 'white', + textAlign: 'center', +}); + +export const crownIcon = style({ + position: 'absolute', + bottom: '40px', +}); + +export const top3RankTextWrapper = style({ + width: '40px', + height: '40px', + + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + + backgroundColor: '#EBF4FF', + borderRadius: '99px', + + fontSize: '2.4rem', + fontWeight: 'bold', + color: '#0047FF', + textAlign: 'center', +}); + +export const rankText = style({ + position: 'relative', + top: '2px', +}); + +export const titleText = style({ + fontSize: '2rem', +}); + +export const simpleImageWrapper = style({ + width: '7rem', + height: '7rem', + + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + + textAlign: 'center', +}); + +export const detailImageWrapper = style({ + width: '100%', + height: 'auto', + + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + + textAlign: 'center', +}); + +export const simpleImage = style({ + width: '7rem', + height: '7rem', + + borderRadius: '10px', + boxShadow: '0px 8px 15px rgba(0, 0, 0, 0.15)', + + objectFit: 'cover', +}); + +export const detailImage = style({ + width: '100%', + maxHeight: '35rem', + height: 'auto', + + border: '1px solid #EFEFF0', + borderRadius: '10px', + + objectFit: 'cover', +}); diff --git a/src/app/[userNickname]/[listId]/_components/ListDetailInner/RankList.tsx b/src/app/[userNickname]/[listId]/_components/ListDetailInner/RankList.tsx new file mode 100644 index 00000000..469e865d --- /dev/null +++ b/src/app/[userNickname]/[listId]/_components/ListDetailInner/RankList.tsx @@ -0,0 +1,113 @@ +'use client'; + +import { ListItemProps } from './index'; +import LinkPreview from '@/components/LinkPreview/LinkPreview'; +import VideoEmbed from '@/components/VideoEmbed/VideoEmbed'; +import * as styles from './RankList.css'; +import CrownIcon from '/public/icons/crown.svg'; + +interface RankListProps { + listData: ListItemProps[]; + type?: string; +} + +function SimpleList({ listData }: RankListProps) { + return listData.map((item, index) => { + return ( +
+
+
+ {index === 0 && } +
{item.rank}
+
+
{item.title}
+
+
+ {item.imageUrl && img설명} +
+
+ ); + }); +} + +function EmbedComponent({ link }: { link: string }) { + let linkType = ''; + // 일반url(link), 비디오(video), 지도(map) 로 구분하기. 지금은 비디오랑 링크만 구분. + // TODO: 지도 추가하기 + const isVideoLink = link.includes('youtube.com') || link.includes('youtu.be') || link.includes('vimeo.com'); + const isMapLink = false; + + if (isVideoLink) { + linkType = 'video'; + } else if (isMapLink) { + linkType = 'map'; + } else { + linkType = 'link'; + } + + if (linkType === 'link') { + return LinkPreview(link); + } + + if (linkType === 'video') { + return ; + } +} +function DetailList({ listData }: RankListProps) { + return listData.map((item, index) => { + return ( +
+
+
+ {index === 0 && } +
{item.rank}
+
+
{item.title}
+
+
{item.comment}
+
+ {item.imageUrl && ( + {`"${item.title}" + )} +
+ {item.link && } +
+ ); + }); +} + +function RankList({ listData, type }: RankListProps) { + return ( +
+
+ {listData ? ( + type == 'simple' ? ( + + ) : ( + + ) + ) : ( +
데이터가 없습니다.
+ )} +
+
+ ); +} + +export default RankList; diff --git a/src/app/[userNickname]/[listId]/_components/ListDetailInner/index.css.ts b/src/app/[userNickname]/[listId]/_components/ListDetailInner/index.css.ts new file mode 100644 index 00000000..da701df4 --- /dev/null +++ b/src/app/[userNickname]/[listId]/_components/ListDetailInner/index.css.ts @@ -0,0 +1,25 @@ +import { style } from '@vanilla-extract/css'; + +export const container = style({ + width: '100%', + + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + gap: '0.2rem', +}); + +export const listAndFooter = style({ + width: '100%', + padding: '4rem 4rem 2rem 4rem', + + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + gap: '4rem', + + borderTop: '1px solid #D9D9D9', + borderBottom: '1px solid #D9D9D9', +}); diff --git a/src/app/[userNickname]/[listId]/_components/ListDetailInner/index.tsx b/src/app/[userNickname]/[listId]/_components/ListDetailInner/index.tsx new file mode 100644 index 00000000..44b3a6b6 --- /dev/null +++ b/src/app/[userNickname]/[listId]/_components/ListDetailInner/index.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useState } from 'react'; +import Header from '@/app/[userNickname]/[listId]/_components/ListDetailInner/Header'; +import RankList from '@/app/[userNickname]/[listId]/_components/ListDetailInner/RankList'; +import Footer from '@/app/[userNickname]/[listId]/_components/ListDetailInner/Footer'; +import * as styles from './index.css'; + +export interface ListItemProps { + id?: number; + rank?: number; + title?: string; + comment?: string; + link?: string | null; + imageUrl?: string | null; +} + +interface OptionsProps { + value: string; + label: string; +} + +interface ListDetailInnerProps { + listId: string; + category: string; + labels: []; + title: string; + description: string; + createdDate: string; + lastUpdatedDate: string; + ownerId: number; + ownerNickname: string; + ownerProfileImageUrl: string; + collaborators: []; + items: []; + isCollected: boolean; + isPublic: boolean; + backgroundColor: string; + collectCount: number; + viewCount: number; +} + +function ListDetailInner({ data }: { data: ListDetailInnerProps }) { + const listData = data.items; + const [listType, setListType] = useState('simple'); + const handleChangeListType = (target: OptionsProps) => { + const value: string = target.value; + setListType(value); + }; + + const footerData = { + category: data.category, + listId: data.listId, + title: data.title, + description: data.description, + items: listData, + collaborators: data.collaborators, + ownerNickname: data.ownerNickname, + }; + + return ( +
+
+
+ +
+
+
+ ); +} + +export default ListDetailInner; diff --git a/src/app/[userNickname]/[listId]/_components/ListDetailOuter/Header.tsx b/src/app/[userNickname]/[listId]/_components/ListDetailOuter/Header.tsx index 2e12cc4d..09b3b5b6 100644 --- a/src/app/[userNickname]/[listId]/_components/ListDetailOuter/Header.tsx +++ b/src/app/[userNickname]/[listId]/_components/ListDetailOuter/Header.tsx @@ -14,7 +14,7 @@ function Header() { }; const handleHistoryButtonClick = () => { - router.push(`/${params.userNickname}/${params.listId}/history`); + router.push(`/${params?.userNickname}/${params?.listId}/history`); }; return ( diff --git a/src/app/create/_components/CreateList.tsx b/src/app/create/_components/CreateList.tsx index 2366d99c..ca8b3a19 100644 --- a/src/app/create/_components/CreateList.tsx +++ b/src/app/create/_components/CreateList.tsx @@ -44,7 +44,7 @@ function CreateList({ onNextClick }: CreateListProps) { const collaboIDs = useWatch({ control, name: 'collaboratorIds' }); const searchParams = useSearchParams(); - const isTemplateCreation = searchParams.has('title') && searchParams.has('category'); + const isTemplateCreation = searchParams?.has('title') && searchParams?.has('category'); const fetchUsers = async () => { try { @@ -65,8 +65,8 @@ function CreateList({ onNextClick }: CreateListProps) { const handleQueryParams = () => { if (isTemplateCreation) { - setValue('title', searchParams.get('title')); - setValue('category', searchParams.get('category')); + setValue('title', searchParams?.get('title')); + setValue('category', searchParams?.get('category')); } }; handleQueryParams(); @@ -100,7 +100,7 @@ function CreateList({ onNextClick }: CreateListProps) { onClick={(item: CategoryType) => { setValue('category', item.nameValue); }} - defaultValue={searchParams.get('category')} + defaultValue={searchParams?.get('category')} /> diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4908be29..ca44e808 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,19 +3,38 @@ import { ReactNode } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import '@/styles/GlobalStyles.css'; +import Script from 'next/script'; +import { ToastContainer } from 'react-toastify'; const queryClient = new QueryClient(); - +declare global { + interface Window { + Kakao: any; + } +} export default function TempLayout({ children }: { children: ReactNode }) { + function kakaoInit() { + window.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_API_KEY); + window.Kakao.isInitialized(); + } + return ( ListyWave + +