[modern-react] 리액트 스터디 파일 추가
This commit is contained in:
36
modern-react/my-next/README.md
Normal file
36
modern-react/my-next/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
7
modern-react/my-next/jsconfig.json
Normal file
7
modern-react/my-next/jsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
18
modern-react/my-next/next.config.mjs
Normal file
18
modern-react/my-next/next.config.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
serverActions: true,
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
hostname: 'books.google.com'
|
||||
},
|
||||
{
|
||||
hostname: 'wikibook.co.kr'
|
||||
},
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
5029
modern-react/my-next/package-lock.json
generated
Normal file
5029
modern-react/my-next/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
modern-react/my-next/package.json
Normal file
26
modern-react/my-next/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "my-next",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"vercel-build": "prisma generate && next build",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.12.1",
|
||||
"@vercel/postgres": "^0.8.0",
|
||||
"next": "14.2.2",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.2",
|
||||
"postcss": "^8",
|
||||
"prisma": "^5.17.0",
|
||||
"tailwindcss": "^3.4.1"
|
||||
}
|
||||
}
|
||||
8
modern-react/my-next/postcss.config.mjs
Normal file
8
modern-react/my-next/postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
BIN
modern-react/my-next/prisma/dev.db
Normal file
BIN
modern-react/my-next/prisma/dev.db
Normal file
Binary file not shown.
29
modern-react/my-next/prisma/schema.prisma
Normal file
29
modern-react/my-next/prisma/schema.prisma
Normal file
@@ -0,0 +1,29 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// datasource db {
|
||||
// provider = "postgresql"
|
||||
// url = env("POSTGRES_PRISMA_URL")
|
||||
// directUrl = env("POSTGRES_URL_NON_POOLING")
|
||||
// }
|
||||
|
||||
model reviews {
|
||||
id String @id
|
||||
title String
|
||||
author String
|
||||
price Int
|
||||
publisher String
|
||||
published String
|
||||
image String
|
||||
read DateTime @default(now())
|
||||
memo String
|
||||
}
|
||||
1
modern-react/my-next/public/next.svg
Normal file
1
modern-react/my-next/public/next.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
modern-react/my-next/public/vercel.svg
Normal file
1
modern-react/my-next/public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||
|
After Width: | Height: | Size: 629 B |
28
modern-react/my-next/src/app/books/[[...keyword]]/layout.js
Normal file
28
modern-react/my-next/src/app/books/[[...keyword]]/layout.js
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRef } from 'react';
|
||||
|
||||
// "/books/keyword" 아래에 적용되는 레이아웃
|
||||
export default function BooksLayout({ children }) {
|
||||
const router = useRouter();
|
||||
const txtKeyword = useRef(null);
|
||||
// [검색] 버튼 클릭 시 '/books/keyword'로 리디렉션된다.
|
||||
const handleSearch = () => {
|
||||
router.push(`/books/${txtKeyword.current.value}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form className="mt-2 mb-4">
|
||||
<input type="text" ref={txtKeyword}
|
||||
className="bg-gray-100 text-black border border-gray-600 rounded mr-2 px-2 py-2 focus:bg-white focus:outline-none focus:border-red-500" />
|
||||
<button type="button" onClick={handleSearch}
|
||||
className="bg-blue-600 text-white rounded px-4 py-2 hover:bg-blue-500">
|
||||
검색</button>
|
||||
</form>
|
||||
<hr />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function ApiLoading() {
|
||||
return (
|
||||
<div className="flex justify-center" aria-label="Now Loading...">
|
||||
<div className="animate-spin h-20 w-20 mt-5 border-8 border-blue-500 rounded-full border-b-transparent"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
modern-react/my-next/src/app/books/[[...keyword]]/page.js
Normal file
16
modern-react/my-next/src/app/books/[[...keyword]]/page.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import LinkedBookDetails from '@/components/LinkedBookDetails';
|
||||
import { getBooksByKeyword } from '@/lib/getter';
|
||||
|
||||
// 루트 매개변수 키워드 가져오기(기본값은 리액트)
|
||||
export default async function BookResult({ params: { keyword = '리액트' } }) {
|
||||
// 주어진 키워드로 도서 정보 검색
|
||||
const books = await getBooksByKeyword(keyword);
|
||||
return (
|
||||
<>
|
||||
{/* 획득한 도서 목록 보기 */}
|
||||
{books.map((b,i) => (
|
||||
<LinkedBookDetails book={b} index={i + 1} key={b.id} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
30
modern-react/my-next/src/app/edit/[id]/page.js
Normal file
30
modern-react/my-next/src/app/edit/[id]/page.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// export default function EditPage({ params }) {
|
||||
// return <p>No. {params.id}의 리뷰를 표시하고 있다.</p>;
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Code 11-4-12
|
||||
import BookDetails from '@/components/BookDetails';
|
||||
import FormEdit from '@/components/FormEdit';
|
||||
import { getBookById, getReviewById } from '@/lib/getter';
|
||||
|
||||
export default async function EditPage({ params }) {
|
||||
const book = await getBookById(params.id);
|
||||
const review = await getReviewById(params.id);
|
||||
// 'YYYY-MM-DD' 형식의 날짜 생성
|
||||
const read = (review?.read || new Date()).toLocaleDateString('sv-SE');
|
||||
|
||||
// const read = (review?.read || new Date()).toLocaleDateString('ko-KR',
|
||||
// { year: 'numeric', month: '2-digit', day: '2-digit' }
|
||||
// ).replaceAll('/', '-')
|
||||
|
||||
return (
|
||||
<div id="form">
|
||||
<BookDetails book={book} />
|
||||
<hr />
|
||||
{/* 편집 양식 생성 */}
|
||||
<FormEdit src={{ id: book.id, read, memo: review?.memo }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
modern-react/my-next/src/app/favicon.ico
Normal file
BIN
modern-react/my-next/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
34
modern-react/my-next/src/app/globals.css
Normal file
34
modern-react/my-next/src/app/globals.css
Normal file
@@ -0,0 +1,34 @@
|
||||
/* Tailwind CSS에서 사용하는 스타일 정의 활성화 */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb));
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
65
modern-react/my-next/src/app/layout.js
Normal file
65
modern-react/my-next/src/app/layout.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// import { Inter } from "next/font/google";
|
||||
// import "./globals.css";
|
||||
|
||||
// // 폰트 정보 설정
|
||||
// const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
// // 메타 정보 준비
|
||||
// export const metadata = {
|
||||
// title: "Create Next App",
|
||||
// description: "Generated by create next app",
|
||||
// };
|
||||
|
||||
// // 루트 레이아웃 준비
|
||||
// export default function RootLayout({ children }) {
|
||||
// return (
|
||||
// <html lang="en">
|
||||
// <body className={inter.className}>{children}</body>
|
||||
// </html>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Code 11-4-1
|
||||
import Link from 'next/link';
|
||||
// Tailwind.css 설정 가져오기
|
||||
import './globals.css';
|
||||
import { Inconsolata } from 'next/font/google';
|
||||
|
||||
// 구글 폰트 활성화
|
||||
const fnt = Inconsolata({ subsets: ['latin'] })
|
||||
|
||||
// 메타데이터 정의
|
||||
export const metadata = {
|
||||
title: 'Reading Recorder',
|
||||
description: '내가 읽은 책을 기록하는 앱',
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="ko">
|
||||
<body className={fnt.className}>
|
||||
<h1 className="text-4xl text-indigo-800 font-bold my-2">
|
||||
Reading Recorder</h1>
|
||||
{/* 공통 메뉴 준비 */}
|
||||
<ul className="flex bg-blue-600 mb-4 pl-2">
|
||||
<li className="block px-4 py-2 my-1 hover:bg-gray-100 rounded">
|
||||
<Link className="no-underline text-blue-300" href="/">
|
||||
Home</Link></li>
|
||||
<li className="block text-blue-300 px-4 py-2 my-1 hover:bg-gray-100 rounded">
|
||||
<Link className="no-underline text-blue-300" href="/books">
|
||||
Search</Link></li>
|
||||
<li className="block text-blue-300 px-4 py-2 my-1 hover:bg-gray-100 rounded">
|
||||
<a className="no-underline text-blue-300"
|
||||
href="https://wikibook.co.kr/support/contact/" target="_blank">Support</a></li>
|
||||
</ul>
|
||||
{/* 페이지 구성 요소를 반영하는 영역 */}
|
||||
<div className="ml-2">
|
||||
{children}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
39
modern-react/my-next/src/app/page.js
Normal file
39
modern-react/my-next/src/app/page.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// import { getAllReviews } from '@/lib/getter';
|
||||
// import LinkedBookDetails from '@/components/LinkedBookDetails';
|
||||
|
||||
// // 항상 최신 정보 얻기
|
||||
// export const dynamic = 'force-dynamic';
|
||||
// export default async function Home() {
|
||||
// // 모든 리뷰 정보 얻기
|
||||
// const reviews = await getAllReviews();
|
||||
// return (
|
||||
// <>
|
||||
// {/* 획득한 리뷰 정보를 바탕으로 리스트 생성 */}
|
||||
// {reviews.map((b, i) => (
|
||||
// <LinkedBookDetails book={b} index={i + 1} key={b.id} />
|
||||
// ))}
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Code 11-4-7
|
||||
import { getAllReviews } from '@/lib/getter';
|
||||
import LinkedBookDetails from '@/components/LinkedBookDetails';
|
||||
|
||||
// 항상 최신 정보 얻기
|
||||
export const dynamic = 'force-dynamic';
|
||||
export default async function Home() {
|
||||
// 모든 리뷰 정보 얻기
|
||||
const reviews = await getAllReviews();
|
||||
console.log(reviews);
|
||||
return (
|
||||
<>
|
||||
{/* 획득한 리뷰 정보를 바탕으로 리스트 생성 */}
|
||||
{reviews.map((b, i) => (
|
||||
<LinkedBookDetails book={b} index={i + 1} key={b.id} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
23
modern-react/my-next/src/components/BookDetails.js
Normal file
23
modern-react/my-next/src/components/BookDetails.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function BookDetails({ index, book }) {
|
||||
return (
|
||||
<div className="flex w-full mb-4">
|
||||
<div>
|
||||
{/* 책 그림자 표시 */}
|
||||
<Image src={book.image} alt="" width={140} height={180} />
|
||||
</div>
|
||||
<div>
|
||||
{/* 도서 정보 목록 표시 (index 속성이 지정되면 연속 번호도 표시) */}
|
||||
<ul className="list-none text-black ml-4">
|
||||
<li>{index && index + '.'}</li>
|
||||
<li>{book.title}({book.price}원)</li>
|
||||
<li>{book.author} 지음</li>
|
||||
<li>{book.publisher} 출판</li>
|
||||
<li>{book.published} 출시</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
72
modern-react/my-next/src/components/FormEdit.js
Normal file
72
modern-react/my-next/src/components/FormEdit.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// 'use client';
|
||||
|
||||
// import { addReview, removeReview } from '@/lib/actions';
|
||||
|
||||
// export default function FormEdit({ src: { id, read, memo } }) {
|
||||
// return (
|
||||
// // 제출 시 addReview 메서드를 호출한다.
|
||||
// <form action={addReview}>
|
||||
// <input type="hidden" name="id" defaultValue={id} />
|
||||
// <div className="mb-3">
|
||||
// <label className="font-bold" htmlFor="read">읽은 날짜:</label>
|
||||
// <input type="date" id="read" name="read"
|
||||
// className="block bg-gray-100 border-2 border-gray-600 rounded focus:bg-white focus:outline-none focus:border-red-500"
|
||||
// defaultValue={read}/>
|
||||
// </div>
|
||||
// <div className="mb-3">
|
||||
// <label className="font-bold" htmlFor="memo">소감:</label>
|
||||
// <textarea id="memo" name="memo" rows="3"
|
||||
// className="block bg-gray-100 border-2 border-gray-600 w-full rounded focus:bg-white focus:outline-none focus:border-red-500"
|
||||
// defaultValue={memo}></textarea>
|
||||
// </div>
|
||||
// <button type="submit"
|
||||
// className="bg-blue-600 text-white rounded px-4 py-2 mr-2 hover:bg-blue-500">
|
||||
// 등록하기</button>
|
||||
// {/* [삭제하기] 버튼으로 removeReview 함수를 호출 */}
|
||||
// <button type="submit"
|
||||
// className="bg-red-600 text-white rounded px-4 py-2 hover:bg-red-500"
|
||||
// formAction={removeReview}>
|
||||
// 삭제하기</button>
|
||||
// </form>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Code 11-4-16
|
||||
'use client';
|
||||
|
||||
import { useTransition } from 'react';
|
||||
import { addReview, removeReview } from '@/lib/actions';
|
||||
|
||||
export default function FormEdit({ src: { id, read, memo } }) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
// 이벤트 핸들러를 통해 서버 액션을 호출한다.
|
||||
return (
|
||||
<form action={addReview}>
|
||||
<input type="hidden" name="id" defaultValue={id} />
|
||||
<div className="mb-3">
|
||||
<label className="font-bold" htmlFor="read">읽은 날짜:</label>
|
||||
<input type="date" id="read" name="read"
|
||||
className="block bg-gray-100 border-2 border-gray-600 rounded focus:bg-white focus:outline-none focus:border-red-500"
|
||||
defaultValue={read}/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="font-bold" htmlFor="memo">소감:</label>
|
||||
<textarea id="memo" name="memo" rows="3"
|
||||
className="block bg-gray-100 border-2 border-gray-600 w-full rounded focus:bg-white focus:outline-none focus:border-red-500"
|
||||
defaultValue={memo}></textarea>
|
||||
</div>
|
||||
<button type="submit"
|
||||
className="bg-blue-600 text-white rounded px-4 py-2 mr-2 hover:bg-blue-500">
|
||||
등록하기</button>
|
||||
<button type="button"
|
||||
className="bg-red-600 text-white rounded px-4 py-2 hover:bg-red-500"
|
||||
onClick={() => {
|
||||
startTransition(() => removeReview(id));
|
||||
}}>
|
||||
삭제하기</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
13
modern-react/my-next/src/components/LinkedBookDetails.js
Normal file
13
modern-react/my-next/src/components/LinkedBookDetails.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import Link from 'next/link';
|
||||
import BookDetails from './BookDetails';
|
||||
|
||||
export default function LinkedBookDetails({ index, book }) {
|
||||
// BookDetails 컴포넌트에 링크 부여
|
||||
return (
|
||||
<Link href={`/edit/${book.id}`}>
|
||||
<div className="hover:bg-green-50">
|
||||
<BookDetails index={index} book={book} />
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
91
modern-react/my-next/src/lib/actions.js
Normal file
91
modern-react/my-next/src/lib/actions.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// 'use server';
|
||||
|
||||
// import { redirect } from 'next/navigation';
|
||||
|
||||
// import prisma from './prisma';
|
||||
// import { getBookById } from './getter';
|
||||
|
||||
// // 폼에서 입력한 값을 데이터베이스에 등록
|
||||
// export async function addReview(data) {
|
||||
// const book = await getBookById(data.get('id'));
|
||||
// const input = {
|
||||
// title: book.title,
|
||||
// author: book.author,
|
||||
// price: Number(book.price),
|
||||
// publisher: book.publisher,
|
||||
// published: book.published,
|
||||
// image: book.image,
|
||||
// read: new Date(data.get('read')),
|
||||
// memo: data.get('memo')
|
||||
// };
|
||||
|
||||
// // 신규 데이터라면 등록, 기존 데이터라면 업데이트
|
||||
// await prisma.reviews.upsert({
|
||||
// update: input,
|
||||
// create: Object.assign({}, input, { id: data.get('id') }),
|
||||
// where: {
|
||||
// id: data.get('id')
|
||||
// }
|
||||
// });
|
||||
// // 처리 성공 후 홈페이지로 리디렉션
|
||||
// redirect('/');
|
||||
// }
|
||||
|
||||
// // 삭제 버튼으로 지정된 리뷰 정보 삭제
|
||||
// export async function removeReview(data) {
|
||||
// await prisma.reviews.delete({
|
||||
// where: {
|
||||
// id: data.get('id')
|
||||
// }
|
||||
// });
|
||||
// // 처리 성공 후 홈페이지로 리디렉션
|
||||
// redirect('/');
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Code 11-4-17
|
||||
'use server';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import prisma from './prisma';
|
||||
import { getBookById } from './getter';
|
||||
|
||||
// 폼에서 입력한 값을 데이터베이스에 등록
|
||||
export async function addReview(data) {
|
||||
const book = await getBookById(data.get('id'));
|
||||
const input = {
|
||||
title: book.title,
|
||||
author: book.author,
|
||||
price: Number(book.price),
|
||||
publisher: book.publisher,
|
||||
published: book.published,
|
||||
image: book.image,
|
||||
read: new Date(data.get('read')),
|
||||
memo: data.get('memo')
|
||||
};
|
||||
|
||||
// 신규 데이터라면 등록, 기존 데이터라면 업데이트
|
||||
await prisma.reviews.upsert({
|
||||
update: input,
|
||||
create: Object.assign({}, input, { id: data.get('id') }),
|
||||
where: {
|
||||
id: data.get('id')
|
||||
}
|
||||
});
|
||||
// 처리 성공 후 홈페이지로 리디렉션
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
// 삭제 버튼으로 지정된 리뷰 정보 삭제
|
||||
export async function removeReview(data) {
|
||||
await prisma.reviews.delete({
|
||||
// 직접 id 값을 받기 때문에 수정
|
||||
where: {
|
||||
id: data
|
||||
}
|
||||
});
|
||||
// 처리 성공 후 홈페이지로 리디렉션
|
||||
redirect('/');
|
||||
}
|
||||
54
modern-react/my-next/src/lib/getter.js
Normal file
54
modern-react/my-next/src/lib/getter.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import prisma from './prisma';
|
||||
|
||||
// API를 통해 얻은 도서 정보에서 필요한 정보만을 객체로 재구성
|
||||
export function createBook(book) {
|
||||
const authors = book.volumeInfo.authors;
|
||||
const price = book.saleInfo.listPrice;
|
||||
const img = book.volumeInfo.imageLinks;
|
||||
return {
|
||||
id: book.id,
|
||||
title: book.volumeInfo.title,
|
||||
author: authors ? authors.join(',') : '',
|
||||
price: price ? price.amount : 0,
|
||||
publisher: book.volumeInfo.publisher,
|
||||
published: book.volumeInfo.publishedDate,
|
||||
image: img ? img.smallThumbnail : '/vercel.svg',
|
||||
};
|
||||
}
|
||||
|
||||
// 인수 keyword를 키워드로 Google Books API에서 책 검색하기
|
||||
export async function getBooksByKeyword(keyword) {
|
||||
const res = await fetch(`https://www.googleapis.com/books/v1/volumes?q=${keyword}&langRestrict=ko&maxResults=20&printType=books`);
|
||||
const result = await res.json();
|
||||
const books = [];
|
||||
// 응답 내용을 객체 배열로 리필
|
||||
for (const b of result.items) {
|
||||
books.push(createBook(b));
|
||||
}
|
||||
return books;
|
||||
}
|
||||
|
||||
// id값을 키로 하여 도서 정보를 가져옴
|
||||
export async function getBookById(id) {
|
||||
const res = await fetch(`https://www.googleapis.com/books/v1/volumes/${id}`);
|
||||
const result = await res.json();
|
||||
return createBook(result);
|
||||
}
|
||||
|
||||
// id값을 키로 리뷰 정보 가져오기
|
||||
export async function getReviewById(id) {
|
||||
return await prisma.reviews.findUnique({
|
||||
where: {
|
||||
id: id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAllReviews() {
|
||||
// 읽은 날짜(read) 내림차순으로 검색
|
||||
return await prisma.reviews.findMany({
|
||||
orderBy: {
|
||||
read: 'desc'
|
||||
}
|
||||
});
|
||||
}
|
||||
9
modern-react/my-next/src/lib/prisma.js
Normal file
9
modern-react/my-next/src/lib/prisma.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
// global.prisma에 Prisma 클라이언트가 존재할 경우 재사용
|
||||
const prisma = global.prisma ??
|
||||
new PrismaClient({ log: ['query'] });
|
||||
// Non-Production 환경에서는 global.prisma에 오브젝트를 저장한다.
|
||||
if (process.env.NODE_ENV !== 'production') global.prisma = prisma;
|
||||
|
||||
export default prisma;
|
||||
20
modern-react/my-next/tailwind.config.js
Normal file
20
modern-react/my-next/tailwind.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
// Tailwind CSS를 적용하는 파일군
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
// 공통 스타일 정의
|
||||
extend: {
|
||||
backgroundImage: {
|
||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||
"gradient-conic":
|
||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
Reference in New Issue
Block a user