Skip to content

Commit c7cd400

Browse files
committed
feat: 방명록
1 parent 5fd97a6 commit c7cd400

File tree

12 files changed

+257
-103
lines changed

12 files changed

+257
-103
lines changed

app/api/guestbooks/route.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
3+
import { prisma } from '@/src/utils/prisma';
4+
import { stringifyBigIntId } from '@/src/utils/stringify-big-int-id';
5+
6+
export async function GET() {
7+
const guestbooks = (await prisma.guestbook.findMany())
8+
.map(stringifyBigIntId)
9+
.sort((a, b) => b.created_at.getTime() - a.created_at.getTime());
10+
11+
return NextResponse.json({ guestbooks });
12+
}
13+
14+
export async function POST(request: NextRequest) {
15+
const { name, message } = await request.json();
16+
17+
const guestbook = await prisma.guestbook.create({
18+
data: {
19+
name,
20+
message,
21+
},
22+
});
23+
24+
return NextResponse.json({ guestbook: stringifyBigIntId(guestbook) });
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
'use client';
2+
3+
import { useRouter } from 'next/navigation';
4+
import { useState } from 'react';
5+
6+
import { createGuestbook } from '@/src/apis/guestbooks';
7+
import { cn } from '@/src/utils/class-name';
8+
import { revalidatePath } from 'next/cache';
9+
10+
export function GuestbookForm() {
11+
const [loading, setLoading] = useState(false);
12+
const [name, setName] = useState('');
13+
const [message, setMessage] = useState('');
14+
const router = useRouter();
15+
16+
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
17+
event.preventDefault();
18+
setLoading(true);
19+
20+
const formData = new FormData(event.currentTarget);
21+
const name = formData.get('name')?.toString();
22+
const message = formData.get('message')?.toString();
23+
24+
if (!name || !message) {
25+
alert('이름 혹은 메세지를 입력해주세요.');
26+
setLoading(false);
27+
return;
28+
}
29+
30+
await createGuestbook({ name, message });
31+
32+
setLoading(false);
33+
setName('');
34+
setMessage('');
35+
router.refresh();
36+
}
37+
38+
function handleChangeName(event: React.ChangeEvent<HTMLInputElement>) {
39+
setName(event.target.value);
40+
}
41+
42+
function handleChangeMessage(event: React.ChangeEvent<HTMLTextAreaElement>) {
43+
setMessage(event.target.value);
44+
}
45+
46+
function handleClickRandomName() {
47+
setName(pickRandomName());
48+
}
49+
50+
return (
51+
<form className='flex flex-col gap-2' onSubmit={handleSubmit}>
52+
<div className='flex gap-2'>
53+
<input
54+
name='name'
55+
className='w-full max-w-[120px] self-start rounded-md border border-gray-300 p-2 text-sm'
56+
maxLength={12}
57+
placeholder='name'
58+
value={name}
59+
onChange={handleChangeName}
60+
/>
61+
<button
62+
type='button'
63+
onClick={handleClickRandomName}
64+
className='w-fit rounded-md bg-gray-500 px-2 py-1 text-sm text-white'>
65+
랜덤 변경
66+
</button>
67+
</div>
68+
<textarea
69+
name='message'
70+
className='w-full rounded-md border border-gray-300 p-2 text-sm'
71+
maxLength={200}
72+
placeholder='Hello!'
73+
value={message}
74+
onChange={handleChangeMessage}
75+
/>
76+
<button
77+
disabled={loading}
78+
className={cn(
79+
'h-[38px] w-fit self-end rounded-md bg-gray-500 px-2 py-1 text-sm text-white',
80+
loading && 'cursor-not-allowed opacity-50',
81+
)}>
82+
방명록 남기기
83+
</button>
84+
</form>
85+
);
86+
}
87+
88+
function pickRandomName() {
89+
const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
90+
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
91+
92+
return `${adjective}${noun}`;
93+
}
94+
95+
const ADJECTIVES = [
96+
'잘생긴',
97+
'친절한',
98+
'예쁜',
99+
'반짝반짝',
100+
'다정한',
101+
'낼름낼름',
102+
'냄새나는',
103+
'향긋한',
104+
'말안듣는',
105+
'하마닮은',
106+
'쓰다버린',
107+
'회생불가',
108+
'건실한',
109+
];
110+
111+
const NOUNS = [
112+
'미남',
113+
'미녀',
114+
'청년',
115+
'하마',
116+
'코털',
117+
'발가락',
118+
'아줌마',
119+
'아저씨',
120+
'벌레',
121+
'대머리',
122+
'발톱',
123+
'돼지',
124+
'메뚜기',
125+
'인력거',
126+
'호랑이',
127+
];

app/guestbook/page.tsx

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { getGuestbooks } from '@/src/apis/guestbooks';
2+
3+
import { GuestbookForm } from './_components/guestbook-form';
4+
5+
export default async function GuestbookPage() {
6+
const { guestbooks } = await getGuestbooks();
7+
8+
return (
9+
<section className='flex flex-col gap-6'>
10+
<GuestbookForm />
11+
<ul className='flex flex-col gap-4'>
12+
{guestbooks.map(({ id, name, message }) => (
13+
<li key={id} className='flex items-start gap-2'>
14+
<span className='whitespace-nowrap text-sm font-bold text-gray-500'>{name} </span>
15+
<p className='text-sm'>{message}</p>
16+
</li>
17+
))}
18+
</ul>
19+
</section>
20+
);
21+
}

package.json

+3-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
"preinstall": "npx only-allow pnpm"
1313
},
1414
"dependencies": {
15-
"@giscus/react": "^2.4.0",
1615
"@prisma/client": "5.18.0",
1716
"@types/node": "20.6.3",
1817
"@types/react": "18.2.22",
@@ -39,14 +38,14 @@
3938
"sharp": "^0.32.6",
4039
"shiki": "^0.14.5",
4140
"tailwind-merge": "^2.0.0",
42-
"tailwindcss": "3.3.3",
43-
"typescript": "5.2.2"
41+
"tailwindcss": "3.3.3"
4442
},
4543
"devDependencies": {
4644
"@tailwindcss/typography": "^0.5.10",
4745
"@types/gtag.js": "^0.0.18",
4846
"eslint-plugin-simple-import-sort": "^12.0.0",
4947
"prettier-plugin-tailwindcss": "^0.5.4",
50-
"prisma": "^5.18.0"
48+
"prisma": "^5.18.0",
49+
"typescript": "^5.6.2"
5150
}
5251
}

0 commit comments

Comments
 (0)