Skip to content

Commit

Permalink
release: Anna Karenina (v0.1.0) (#18)
Browse files Browse the repository at this point in the history
* 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
MayOwall authored Feb 23, 2024
1 parent 88ef929 commit d25a1b8
Show file tree
Hide file tree
Showing 34 changed files with 5,924 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.DS_Store
3 changes: 3 additions & 0 deletions FE/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
37 changes: 37 additions & 0 deletions FE/.gitignore
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.
49 changes: 49 additions & 0 deletions FE/app/(home)/bookmit/confirm/page.tsx
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>
);
}
70 changes: 70 additions & 0 deletions FE/app/(home)/bookmit/create/page.tsx
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>
);
}
11 changes: 11 additions & 0 deletions FE/app/(home)/bookmit/layout.tsx
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>;
}
100 changes: 100 additions & 0 deletions FE/app/(home)/books/create/page.tsx
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>
);
}
19 changes: 19 additions & 0 deletions FE/app/(home)/layout.tsx
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}
</>
);
}
99 changes: 99 additions & 0 deletions FE/app/(home)/page.tsx
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>
);
}
Loading

0 comments on commit d25a1b8

Please sign in to comment.