[modern-react] 리액트 스터디 파일 추가

This commit is contained in:
2025-09-30 23:55:13 +09:00
parent 31bcb2efe1
commit 75ec02d506
546 changed files with 141345 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
const APP_TITLE = 'React 앱';
export function getTriangle(base, height) {
return base * height / 2;
}
export class Article {
getAppTitle() {
return APP_TITLE;
}
}

View File

@@ -0,0 +1,5 @@
export default class Util {
static getCircleArea(radius) {
return (radius ** 2) * Math.PI;
}
}

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script src="const.js"></script>
</body>
</html>

View File

@@ -0,0 +1,3 @@
const author = '야마다 요시히로';
author = 'WINGS 프로젝트';
console.log(author);

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script src="destruct_list.js"></script>
</body>
</html>

View File

@@ -0,0 +1,12 @@
const list = [10, 20, 30];
const [x, y, z] = list;
console.log(x, y, z); // 결과: 10 20 30
const [a, b] = list;
console.log(a, b); // 결과: 10 20
const [l, m, n, o] = list;
console.log(l, m, n, o); // 결과: 10 20 30 undefined
const [p, , r] = list;
console.log(p, r); // 결과: 10 30

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script src="destruct_list_rest.js"></script>
</body>
</html>

View File

@@ -0,0 +1,3 @@
const list = [10, 20, 30];
const [one, ...rest] = list;
console.log(one, rest); // 결과: 10 [20, 30]

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script src="destruct_nest.js"></script>
</body>
</html>

View File

@@ -0,0 +1,10 @@
const member = {
fullname: '사토 리오',
address: {
prefecture: '스즈오카현',
city: '후지에다시'
}
};
const { address, address: { city } } = member;
console.log(address); // 결과: { prefecture: '스즈오카현', city: '후지에다시' }
console.log(city); // 결과: 후지에다시

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script src="destruct_nest_array.js"></script>
</body>
</html>

View File

@@ -0,0 +1,3 @@
const list = [200, [300, 301, 302]];
const [x, [y1, y2, y3]] = list;
console.log(y1, y2, y3); // 결과: 300 301 302

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script src="destruct_obj.js"></script>
</body>
</html>

View File

@@ -0,0 +1,19 @@
const member = {
fullname: '사토 리오',
sex: '여성',
age: 18
};
const { fullname, sex, memo = '---' } = member;
console.log(sex, fullname, memo); // 결과: 여성 사토 리오 ---
// const { sex: gender } = member;
// console.log(gender); // 결과: 여성
// const { fullname, ...rest } = member;
// console.log(fullname); // 결과: 사토 리오
// console.log(rest); // 결과: { sex: '여성', age: 18 }
// let fullname, sex, memo;
// ({ fullname, sex, memo = '---' } = member);
// console.log(sex, fullname, memo); // 결과: 여성 사토 리오 ---

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script src="destruct_obj_param.js"></script>
</body>
</html>

View File

@@ -0,0 +1,10 @@
function greet({ name, age }) {
console.log(`안녕하세요, 저는 ${name}, ${age}세 입니다.`);
}
const my = { name: '사토리오', sex: '여성', age: 18 };
greet(my); // 결과: 안녕하세요, 저는 사토리오, 18세 입니다.
// function greet(obj) {
// console.log(` 안녕하세요, 저는 ${obj.name}, ${obj.age}세 입니다. `);
// }

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script src="func_def.js"></script>
</body>
</html>

View File

@@ -0,0 +1,10 @@
function getTrapezoidArea(upper = 1, lower = 1, height = 1) {
return (upper + lower) * height / 2;
}
console.log(getTrapezoidArea(10, 5, 3)); // 결과: 22.5 (=(10 5) × 3 ÷ 2)
console.log(getTrapezoidArea(10, 5)) // 결과: 7.5 (=(10 5) × 1 ÷ 2)
console.log(getTrapezoidArea(10)); // 결과: 5 (=(10 1) × 1 ÷ 2)
// function getTrapezoidArea(upper = 1, lower = upper, height = upper) { ... }

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script src="func_rest.js"></script>
</body>
</html>

View File

@@ -0,0 +1,13 @@
function sum(...nums) {
let result = 0;
for (const num of nums) {
result += num;
}
return result;
}
console.log(sum(10, 25, 2)); // 결과: 37
console.log(sum(7, 13, 25, 6, 100)); // 결과: 151
// console.log(sum(...[10, 25, 2]));

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script src="let.js"></script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
if (true) {
let x = 13;
}
// 블록 아래에서 선언한 변수를 참조하면 ...
console.log(x);

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script type="module" src="module_alias.js"></script>
</body>
</html>

View File

@@ -0,0 +1,3 @@
import { getTriangle as tri } from './App.js';
console.log(tri(10, 2)); // 결과: 10

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script type="module" src="module_all.js"></script>
</body>
</html>

View File

@@ -0,0 +1,3 @@
import * as app from './App.js';
console.log(app.getTriangle(10, 2)); // 결과: 10

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script type="module" src="module_basic.js"></script>
</body>
</html>

View File

@@ -0,0 +1,6 @@
import { Article, getTriangle } from './App.js';
console.log(getTriangle(10, 5)); // 결과: 25
const a = new Article();
console.log(a.getAppTitle()); // 결과: React 앱

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script type="module" src="module_dynamic.js"></script>
</body>
</html>

View File

@@ -0,0 +1,6 @@
import('./App.js').then(app => {
console.log(app.getTriangle(10, 5)); // 결과: 25
const a = new app.Article();
console.log(a.getAppTitle()); // 결과: React 앱
});

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script type="module" src="module_use_util.js"></script>
</body>
</html>

View File

@@ -0,0 +1,3 @@
import Util from './Util.js';
console.log(Util.getCircleArea(10)); // 결과: 314.1592653589793

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script src="number.js"></script>
</body>
</html>

View File

@@ -0,0 +1,2 @@
const value = 123_456_789;
console.log(value);

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script src="obj_computed.js"></script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
let i = 0;
const member = {
[`attr${++i}`]: '사토 리오',
[`attr${++i}`]: '여성',
[`attr${++i}`]: '18세'
};
console.log(member);
// 결과: { attr1: '사토 리오', attr2: '여성', attr3: '18세' }

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script src="obj_method.js"></script>
</body>
</html>

View File

@@ -0,0 +1,15 @@
const member = {
name: '사토 리오',
greet: function() {
console.log(`안녕하세요, ${this.name} 님!`);
}
}
// const member = {
// name: '사토 리오',
// greet() {
// console.log(`안녕하세요, ${this.name} 님!`);
// }
// }
member.greet();

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script src="obj_prop.js"></script>
</body>
</html>

View File

@@ -0,0 +1,7 @@
const title = '리액트 입문';
const price = 500;
const book = { title, price };
// const book = { title: title, price: price };
console.log(book);

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script src="optional.js"></script>
</body>
</html>

View File

@@ -0,0 +1,13 @@
const str = null;
console.log(str.substring(1));
// if (str !== null && str !== undefined) {
// console.log(str.substring(1));
// }
// const str = null;
// console.log(str?.substring(1));
// const str = '위키북스';
// console.log(str?.substring(1));

View File

@@ -0,0 +1,13 @@
{
"name": "my-modern",
"version": "1.0.0",
"description": "",
"main": "App.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>리액트 입문</title>
</head>
<body>
<script src="template.js"></script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
const fullname = '홍길동';
const msg = `안녕하세요, ${fullname} 님!
오늘 하루 잘 지내셨나요?`;
console.log(msg);

View 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.

View File

@@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

View File

@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

Binary file not shown.

View 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
}

View 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

View 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

View 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}
</>
);
}

View File

@@ -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>
);
}

View 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} />
))}
</>
);
}

View 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>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View 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;
}
}

View 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>
);
}

View 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} />
))}
</>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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('/');
}

View 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'
}
});
}

View 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;

View 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: [],
};

View File

@@ -0,0 +1,46 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

18797
modern-react/my-react-ts/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
{
"name": "my-react-ts",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.15.15",
"@mui/material": "^5.15.15",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.96",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-query": "^3.39.3",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,26 @@
import React from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;

View File

@@ -0,0 +1,7 @@
export type Book = {
isbn: string,
title: string,
price: number,
summary: string,
download: boolean
};

View File

@@ -0,0 +1,56 @@
import { useReducer } from 'react';
// Props 유형 정의
type HookReducerUpProps = {
init: number
};
// State 타입 정의
type StateType = {
count: number;
};
// Action의 타입 정의
type ActionType = {
type: 'update',
step: number
} | {
type: 'reset',
init: number
};
// Reducer 유형 정의
type CountReducerType = (state: StateType, action: ActionType) => StateType;
export default function HookReducerUp({ init }: HookReducerUpProps) {
// Reducer 타입 정의
const [state, dispatch] = useReducer<CountReducerType>(
// Reducer의 실체
(state, action) => {
switch (action.type) {
case 'update':
return { count: state.count + action.step };
case 'reset' :
return { count: action.init };
default:
return state;
}
},
// State의 초깃값
{
count: init
}
);
const handleUp = () => dispatch({ type: 'update', step: 1 });
const handleDown = () => dispatch({ type: 'update', step: -1 });
const handleReset = () => dispatch({ type: 'reset', init: 0 });
return (
<>
<button onClick={handleUp}></button>
<button onClick={handleDown}></button>
<button onClick={handleReset}></button>
<p>{state.count} .</p>
</>
);
}

View File

@@ -0,0 +1,13 @@
import { useContext } from 'react';
import { Button } from '@mui/material';
import ThemeContext, { ThemeContextType } from './ThemeContext';
export default function HookThemeButton() {
// const { mode, toggleMode } = useContext<ThemeContextType>(ThemeContext);
const { mode, toggleMode } = useContext<Partial<ThemeContextType>>(ThemeContext);
return (
<Button variant="contained" onClick={toggleMode}>
Mode {mode}
</Button>
);
}

View File

@@ -0,0 +1,49 @@
// import React, { FC, ReactNode } from 'react';
// import type { Book } from './Book';
// // Props 타입 선언
// type ListTemplateProps = {
// src: Array<Book>,
// children: (b: Book) => ReactNode
// };
// export default function ListTemplate({ src, children }: ListTemplateProps) {
// return (
// <dl>
// {
// src.map((elem, index) => (
// <React.Fragment key={elem.isbn}>
// {children(elem)}
// </React.Fragment>
// ))
// }
// </dl>
// );
// }
// Code 10-2-16
import React, { FC, ReactNode } from 'react';
import type { Book } from './Book';
type ListTemplateProps = {
src: Array<Book>,
children: (b: Book) => ReactNode
};
const ListTemplate: FC<ListTemplateProps> = ({ src, children }) => {
return (
<dl>
{
src.map((elem, index) => (
<React.Fragment key={index}>
{children(elem)}
</React.Fragment>
))
}
</dl>
);
};
export default ListTemplate;

View File

@@ -0,0 +1,50 @@
import { ReactNode, useState } from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { amber, grey } from '@mui/material/colors';
import { CssBaseline, PaletteMode } from '@mui/material';
import ThemeContext, { ThemeContextType } from './ThemeContext';
// Props 타입 선언
type MyThemeProviderProps = {
children: ReactNode
};
export default function MyThemeProvider({ children }: MyThemeProviderProps) {
const [mode, setMode] = useState<PaletteMode>('light');
const themeConfig: ThemeContextType = {
mode,
toggleMode: () => {
setMode(prev =>
prev === 'light' ? 'dark' : 'light'
)}
};
const theme = createTheme({
palette: {
mode,
...(mode === 'light'
? {
primary: amber,
}
: {
primary: {
main: grey[500],
contrastText: '#fff'
},
background: {
default: grey[900],
paper: grey[900],
},
}),
},
});
return (
<ThemeContext.Provider value={themeConfig}>
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
</ThemeContext.Provider>
);
}

View File

@@ -0,0 +1,39 @@
import { useQuery } from 'react-query';
type WeatherType = {
weather: Array<{
id: number
main: string
description: string
icon: string
}>
}
// 날씨 정보를 얻기 위한 함수
const fetchWeather = async () => {
const res = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=Seoul&lang=kr&appid=ef23a61e94f59784b8451d12c0d07da8`);
if (res.ok) {
return await res.json() as WeatherType;
}
throw new Error(res.statusText);
};
export default function QuerBasic() {
// fetchWeather 함수로 데이터 가져오기
const { data, isLoading, isError, error } = useQuery<WeatherType, Error>('weather', fetchWeather);
if (isLoading) {
return <p>Loading...</p>
}
if (isError) {
return <p>Error: {error.message}</p>
}
return (
<figure>
<img
src={`https://openweathermap.org/img/wn/${data?.weather?.[0]?.icon}.png`}
alt={data?.weather?.[0]?.main} />
<figcaption>{data?.weather?.[0]?.description}</figcaption>
</figure>
);
}

View File

@@ -0,0 +1,51 @@
import { useEffect, useState } from 'react';
// fetch를 통해 취득한 날씨 정보 유형 정의
type WeatherType = {
weather: Array<{
id: number
main: string
description: string
icon: string
}>
}
// 날씨 정보를 얻기 위한 함수
const fetchWeather = async () => {
const res = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=Seoul&lang=kr&appid=ef23a61e94f59784b8451d12c0d07da8`);
if (res.ok) {
return await res.json() as WeatherType;
}
throw new Error(res.statusText);
};
export default function QueryPre() {
// State 선언
const [data, setData] = useState<WeatherType>();
const [isLoading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>('');
useEffect(() => {
setLoading(true);
fetchWeather()
.then(result => setData(result))
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []);
if (isLoading) {
return <p>Loading...</p>
}
if (error) {
return <p>Error: {error}</p>
}
return (
<figure>
<img
src={`https://openweathermap.org/img/wn/${data?.weather?.[0]?.icon}.png`}
alt={data?.weather?.[0]?.main} />
<figcaption>{data?.weather?.[0]?.description}</figcaption>
</figure>
);
}

View File

@@ -0,0 +1,19 @@
import { useState } from 'react';
// Props 타입 선언
type StateBasicProps = {
init: number
};
export default function StateBasic({ init }: StateBasicProps) {
const [count, setCount] = useState<number>(init);
const handleClick = () => setCount(count + 1);
return (
<>
<button onClick={handleClick}></button>
<p>{count} .</p>
</>
);
}

View File

@@ -0,0 +1,28 @@
import { useRef } from 'react';
export default function StateFormUC() {
const name = useRef<HTMLInputElement>(null);
const age = useRef<HTMLInputElement>(null);
const show = () => {
console.log(`안녕하세요, ${name.current?.value}${age.current?.value}세) 님!`);
};
return (
<form>
<div>
<label htmlFor="name">: </label>
<input id="name" name="name" type="text"
ref={name} defaultValue="홍길동" />
</div>
<div>
<label htmlFor="age">: </label>
<input id="age" name="age" type="number"
ref={age} defaultValue="18" />
</div>
<div>
<button type="button" onClick={show}>
</button>
</div>
</form>
);
}

View File

@@ -0,0 +1,21 @@
import { ReactNode } from 'react';
// Props 타입 선언
type StyledPanelProps = {
children: ReactNode
};
export default function StyledPanel({ children }: StyledPanelProps) {
return (
<div style={{
margin: 50,
padding: 20,
border: '1px solid #000',
width: 'fit-content',
boxShadow: '10px 5px 5px #999',
backgroundColor: '#fff'
}}>
{children}
</div>
);
}

View File

@@ -0,0 +1,14 @@
import { PaletteMode } from '@mui/material';
import { createContext } from 'react';
export type ThemeContextType = {
mode: PaletteMode,
toggleMode: () => void
};
// export default createContext<ThemeContextType>({
// mode: 'light',
// toggleMode: () => {}
// });
export default createContext<Partial<ThemeContextType>>({});

View File

@@ -0,0 +1,43 @@
const books = [
{
isbn: '9791158395124',
title: '게임 개발을 위한 미드저니, 스테이블 디퓨전 완벽 활용법',
slug: 'genai-game',
price: 28000,
summary: '생성형 AI를 활용한 게임 캐릭터, 배경, 아이템 제작부터 유니티 실전 프로젝트까지',
download: true,
},
{
isbn: '9791158395117',
title: '디자인을 위한 미드저니 완벽 활용법',
slug: 'midjourney-design',
price: 24000,
summary: '광고부터 캐릭터, 로고, 일러스트레이션, 표지, 포스터, 타이포까지 독창적인 디자인 만들기',
download: false,
},
{
isbn: '9791158395032',
title: '만들면서 배우는 블렌더 3D 입문',
slug: 'blender-basic',
price: 28000,
summary: '블렌더 기초, 모델링, 머티리얼, 애니메이션, 렌더링까지',
download: true,
},
{
isbn: '9791158395018',
title: '모던 그로스 마케팅',
slug: 'mgm',
price: 24000,
summary: '비용은 최소화하고 매출은 극대화하는 생존 마케팅 전략',
download: false,
},
{
isbn: '9791158395025',
title: '도메인 스토리텔링',
slug: 'domain-storytelling',
price: 28000,
summary: '도메인 주도 소프트웨어 구축을 위한 스토리텔링과 스토리 시각화 기법',
download: true,
},
];
export default books;

View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@@ -0,0 +1,111 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import StateBasic from './StateBasic';
import StyledPanel from './StyledPanel';
import ListTemplate from './ListTemplate';
import books from './books';
import type { Book } from './Book';
import MyThemeProvider from './MyThemeProvider';
import HookThemeButton from './HookThemeButton';
import HookReducerUp from './HookReducerUp';
import StateFormUC from './StateFormUC';
import QueryPre from './QueryPre';
import { QueryClient, QueryClientProvider } from 'react-query';
import QueryBasic from './QueryBasic';
/* eslint-enable @typescript-eslint/no-unused-vars */
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
// root.render(
// <React.StrictMode>
// <App />
// </React.StrictMode>
// );
// Code 10-2-3
// root.render(
// <StateBasic init={0} />
// );
// Code 10-2-4
// root.render(
// <StyledPanel>
// <p>회원 모집 중!</p>
// <p>위키북스 프로젝트에 오신 것을 환영합니다!!</p>
// </StyledPanel>
// );
// Code 10-2-7
// root.render(
// <ListTemplate src={books}>
// {(elem: Book) => (
// <>
// <dt>
// <a href={`https://wikibook.co.kr/images/cover/s/${elem.isbn}.jpg`}>
// {elem.title}{elem.price}원)
// </a>
// </dt>
// <dd>{elem.summary}</dd>
// </>
// )}
// </ListTemplate>
// );
// Code 10-2-11
// root.render(
// <MyThemeProvider>
// <HookThemeButton />
// </MyThemeProvider>
// );
// Code 10-2-12
// root.render(
// <HookReducerUp init={0} />
// );
// Code 10-2-13
// root.render(
// <StateFormUC />
// );
// Code 10-2-14
root.render(
<QueryPre />
);
// Code 10-2-15
const cli = new QueryClient();
root.render(
<QueryClientProvider client={cli}>
<QueryBasic />
</QueryClientProvider>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@@ -0,0 +1,15 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

Some files were not shown because too many files have changed in this diff Show More