-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
release: Anna Karenina (v0.1.0) (#18)
* feat: Next 프로젝트 초기화 * feat: 첫 진입 페이지 구현 * feat: 책 검색 바 컴포넌트 구현 및 Next 서버 API 구현을 통한 책 검색 네이버 API 요청 우회 로직 구현 (#6) * feat: 책 등록 페이지 및 Zustand를 통한 모달 상태 관리 로직 구현 (#11) * feat: 홈페이지 북밋 전체 리스트 기능 구현 (3h/3h) (#13) * feat 홈페이지 책 선택에 따른 로직 구현 (1h/4h) (#15) * feat: 북밋 생성 기능 구현 (1h/4h) (#17)
- Loading branch information
Showing
34 changed files
with
5,924 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"extends": "next/core-web-vitals" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
.yarn/install-state.gz | ||
|
||
# testing | ||
/coverage | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
|
||
# local env files | ||
.env*.local | ||
dummy.json | ||
|
||
# vercel | ||
.vercel | ||
|
||
# typescript | ||
*.tsbuildinfo | ||
next-env.d.ts |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
"use client"; | ||
|
||
import Link from "next/link"; | ||
import { useRouter, useSearchParams } from "next/navigation"; | ||
import { useState } from "react"; | ||
import { LargeButton, BookInfoCard } from "@/src/components"; | ||
import { postBookmit } from "@/src/api"; | ||
|
||
export default function BookmitCreate() { | ||
const searchParams = useSearchParams(); | ||
const router = useRouter(); | ||
|
||
const title = searchParams.get("title")!; | ||
const isbn = searchParams.get("isbn")!; | ||
const startPage = Number(searchParams.get("startpage")!); | ||
const endPage = Number(searchParams.get("endpage")!); | ||
|
||
const onSubmit = () => { | ||
postBookmit(title, isbn, startPage, endPage); | ||
router.push("/"); | ||
}; | ||
|
||
return ( | ||
<main className="relative flex h-full flex-col gap-4 py-16"> | ||
<h1 className="mb-4 text-2xl font-bold">이 기록을 등록할까요?</h1> | ||
<div className="flex w-full justify-between"> | ||
<span className="text-sm text-neutral-400">책 제목</span> | ||
<span className="text-sm font-bold">{searchParams.get("title")}</span> | ||
</div> | ||
<div className="flex w-full justify-between"> | ||
<span className="text-sm text-neutral-400">시작 페이지</span> | ||
<span className="text-sm font-bold"> | ||
{searchParams.get("startpage")} | ||
</span> | ||
</div> | ||
<div className="flex w-full justify-between"> | ||
<span className="text-sm text-neutral-400">마지막 페이지</span> | ||
<span className="text-sm font-bold">{searchParams.get("endpage")}</span> | ||
</div> | ||
|
||
<div className="absolute bottom-28 flex w-full flex-col justify-center gap-4"> | ||
<LargeButton onClick={onSubmit}>북밋 생성하기</LargeButton> | ||
<Link href="/" className="w-full text-center"> | ||
<button className=" text-neutral-400">생성 취소</button> | ||
</Link> | ||
</div> | ||
</main> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
"use client"; | ||
|
||
import Link from "next/link"; | ||
import { useRouter, useSearchParams } from "next/navigation"; | ||
import { ChangeEvent, useState } from "react"; | ||
import { LargeButton } from "@/src/components"; | ||
|
||
export default function BookmitCreate() { | ||
const router = useRouter(); | ||
const searchParams = useSearchParams(); | ||
const [startPage, setStartPage] = useState<number | undefined>(undefined); | ||
const [endPage, setEndPage] = useState<number | undefined>(undefined); | ||
|
||
const handleStartPage = (e: ChangeEvent<HTMLInputElement>) => { | ||
const value = Number(e.currentTarget.value); | ||
if (!value) { | ||
setStartPage(() => undefined); | ||
return; | ||
} | ||
setStartPage(() => (!isNaN(value) ? value : undefined)); | ||
}; | ||
|
||
const handleEndPage = (e: ChangeEvent<HTMLInputElement>) => { | ||
const value = Number(e.currentTarget.value); | ||
if (!value) { | ||
setEndPage(() => undefined); | ||
return; | ||
} | ||
setEndPage(() => (!isNaN(value) ? value : undefined)); | ||
}; | ||
|
||
const onSubmit = () => { | ||
if (!startPage || !endPage) { | ||
alert("페이지를 입력해주세요"); | ||
return; | ||
} | ||
|
||
const isbn = searchParams.get("isbn"); | ||
const title = searchParams.get("title"); | ||
router.push( | ||
`./confirm?isbn=${isbn}&title=${title}&startpage=${startPage}&endpage=${endPage}`, | ||
); | ||
}; | ||
|
||
return ( | ||
<main className="relative flex h-full flex-col gap-4 py-16"> | ||
<h1 className="mb-4 text-2xl font-bold">페이지 정보를 입력해주세요</h1> | ||
<input | ||
className="w-3/4 outline-none" | ||
type="number" | ||
value={startPage} | ||
onChange={handleStartPage} | ||
placeholder="시작 페이지를 입력해주세요" | ||
/> | ||
<input | ||
className="w-3/4 outline-none" | ||
type="number" | ||
value={endPage} | ||
onChange={handleEndPage} | ||
placeholder="마지막 페이지를 입력해주세요" | ||
/> | ||
<div className="absolute bottom-28 flex w-full flex-col justify-center gap-4"> | ||
<LargeButton onClick={onSubmit}>페이지 입력하기</LargeButton> | ||
<Link href="/" className="w-full text-center"> | ||
<button className=" text-neutral-400">입력 취소</button> | ||
</Link> | ||
</div> | ||
</main> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
"use client"; | ||
|
||
import { Suspense } from "react"; | ||
|
||
export default function BookmitLayout({ | ||
children, | ||
}: Readonly<{ | ||
children: React.ReactNode; | ||
}>) { | ||
return <Suspense>{children}</Suspense>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
"use client"; | ||
|
||
import Link from "next/link"; | ||
import { useRef, useState } from "react"; | ||
import { getBookitems } from "@/src/api"; | ||
import { | ||
BookSearchbar, | ||
BookInfoCard, | ||
CraeteBookModalContent, | ||
} from "@/src/components"; | ||
import { SEARCH_BOOKITEMS_OFFSET } from "@/src/constants"; | ||
import type { bookinfo, bottomButtonStatus } from "@/src/types"; | ||
import { useModalStore } from "@/src/stores"; | ||
import { useRouter } from "next/navigation"; | ||
|
||
export default function CreateNewBook() { | ||
const searchPage = useRef(1); | ||
const searchword = useRef(""); | ||
const [bookitems, setBookitems] = useState<bookinfo[]>([]); | ||
const [bottomButtonStatus, setBottomButtonStatus] = | ||
useState<bottomButtonStatus>("nonDisplay"); | ||
|
||
// 검색어가 제출되었을 때의 동작 | ||
const onSearchbarSubmit = async (keyword: string) => { | ||
const { total, bookitems } = await getBookitems( | ||
keyword, | ||
searchPage.current, | ||
); | ||
|
||
searchword.current = keyword; | ||
handleBookitems(total, bookitems); | ||
}; | ||
|
||
// 새로운 검색 결과를 반영 | ||
const handleBookitems = (total: number, bookitems: bookinfo[]) => { | ||
if (!total) { | ||
setBottomButtonStatus(() => "nobooks"); | ||
return; | ||
} | ||
|
||
searchPage.current += 1; | ||
setBookitems(bookitems); | ||
setBottomButtonStatus(() => "more"); | ||
}; | ||
|
||
// 더보기 버튼을 클릭했을 때의 동작 | ||
const onMoreButtonClick = () => { | ||
handleNextBookitems(); | ||
}; | ||
|
||
// 다음 검색 결과를 반영 | ||
const handleNextBookitems = async () => { | ||
if (!searchword.current) return; | ||
|
||
const res = await getBookitems(searchword.current, searchPage.current); | ||
const nextBookitems = [...bookitems, ...res.bookitems]; | ||
setBookitems(() => nextBookitems); | ||
|
||
isLastPage(res.total, searchPage.current) | ||
? setBottomButtonStatus(() => "end") | ||
: (searchPage.current += 1); | ||
}; | ||
|
||
// 마지막 페이지인지 판별 | ||
const isLastPage = (total: number, current: number) => { | ||
return total <= current * SEARCH_BOOKITEMS_OFFSET; | ||
}; | ||
|
||
const craeteModal = useModalStore((state) => state.createModal); | ||
|
||
return ( | ||
<main> | ||
<div className="flex w-full gap-2"> | ||
<BookSearchbar handleSubmit={onSearchbarSubmit} /> | ||
<Link href="/" className="flex shrink-0 items-center justify-center"> | ||
<button className="px-3">취소</button> | ||
</Link> | ||
</div> | ||
<section className="my-4 flex flex-col items-center gap-2"> | ||
{bookitems.map((item) => ( | ||
<BookInfoCard | ||
key={item.isbn} | ||
bookinfo={item} | ||
onClick={() => | ||
craeteModal(<CraeteBookModalContent bookinfo={item} />) | ||
} | ||
/> | ||
))} | ||
{bottomButtonStatus === "more" && ( | ||
<button className="m-2 text-blue-500" onClick={onMoreButtonClick}> | ||
더 보기 | ||
</button> | ||
)} | ||
{bottomButtonStatus === "end" && ( | ||
<div className="end m-2">마지막 책 입니다</div> | ||
)} | ||
</section> | ||
</main> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
"use client"; | ||
|
||
import { useModalStore } from "@/src/stores"; | ||
import { Modal } from "@/src/components"; | ||
|
||
export default function HomeLayout({ | ||
children, | ||
}: Readonly<{ | ||
children: React.ReactNode; | ||
}>) { | ||
const modalContent = useModalStore((state) => state.content); | ||
|
||
return ( | ||
<> | ||
{modalContent && <Modal>{modalContent}</Modal>} | ||
{children} | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
"use client"; | ||
|
||
import Link from "next/link"; | ||
import { useState, useEffect } from "react"; | ||
import { | ||
getReadingbooks, | ||
getAllBookmits, | ||
getSelectedBookmits, | ||
} from "@/src/api"; | ||
import { | ||
ReadingBookShelf, | ||
LargeButton, | ||
BookmitsByDate, | ||
} from "@/src/components"; | ||
import type { BookmitsByDate as bookmitsByDate, bookinfo } from "@/src/types"; | ||
|
||
export default function Home() { | ||
const [readingbooks, setReadingbooks] = useState([]); | ||
const [bookmits, setBookmits] = useState<bookmitsByDate[]>([]); | ||
const [selectedBook, setSelectedBook] = useState<bookinfo | null>(null); | ||
|
||
const handleSelectedBook = (bookinfo: bookinfo) => { | ||
if (bookinfo.isbn === selectedBook?.isbn) { | ||
setSelectedBook(null); | ||
return; | ||
} | ||
setSelectedBook(() => bookinfo); | ||
}; | ||
|
||
useEffect(() => { | ||
const readingbooks = getReadingbooks(); | ||
setReadingbooks(() => readingbooks); | ||
}, []); | ||
|
||
useEffect(() => { | ||
const bookmits = getAllBookmits(); | ||
setBookmits(() => bookmits); | ||
}, []); | ||
|
||
useEffect(() => { | ||
if (!selectedBook) { | ||
const bookmits = getAllBookmits(); | ||
setBookmits(() => bookmits); | ||
return; | ||
} | ||
const bookmits = getSelectedBookmits(selectedBook.isbn); | ||
setBookmits(() => bookmits); | ||
}, [selectedBook]); | ||
|
||
return ( | ||
<main className="flex flex-col gap-1"> | ||
<NewBookButton isReadingbookExist={!!readingbooks.length} /> | ||
<ReadingBookShelf | ||
readingbooks={readingbooks} | ||
selectedBook={selectedBook} | ||
onClick={handleSelectedBook} | ||
/> | ||
{!readingbooks.length && ( | ||
<Link href="./books/create"> | ||
<LargeButton>새 책 등록하기</LargeButton> | ||
</Link> | ||
)} | ||
{selectedBook && ( | ||
<Link | ||
href={`/bookmit/create?isbn=${selectedBook.isbn}&title=${selectedBook.title}`} | ||
> | ||
<LargeButton>새 북밋 생성</LargeButton> | ||
</Link> | ||
)} | ||
{!!bookmits.length && | ||
bookmits.map((bookmitsByDate: bookmitsByDate) => ( | ||
<BookmitsByDate key={bookmitsByDate.date} {...bookmitsByDate} /> | ||
))} | ||
{!bookmits.length && ( | ||
<div className="m-2 text-center text-sm text-neutral-400"> | ||
작성한 북밋이 없어요 | ||
</div> | ||
)} | ||
</main> | ||
); | ||
} | ||
|
||
function NewBookButton({ | ||
isReadingbookExist, | ||
}: { | ||
isReadingbookExist: boolean; | ||
}) { | ||
return ( | ||
<div className="text-right"> | ||
<Link href="/books/create"> | ||
<button | ||
className={`${!isReadingbookExist && "invisible"} text-sm text-blue-500`} | ||
> | ||
새 책 등록 | ||
</button> | ||
</Link> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.