{"id":10433,"date":"2025-06-18T14:00:29","date_gmt":"2025-06-18T05:00:29","guid":{"rendered":"https:\/\/www.skyer9.pe.kr\/wordpress\/?p=10433"},"modified":"2025-06-18T15:50:30","modified_gmt":"2025-06-18T06:50:30","slug":"react-js-next-js-%ea%b2%8c%ec%8b%9c%ed%8c%90-%ec%83%9d%ec%84%b1","status":"publish","type":"post","link":"https:\/\/www.skyer9.pe.kr\/wordpress\/?p=10433","title":{"rendered":"React.js + Next.js + \uac8c\uc2dc\ud310 \uc0dd\uc131"},"content":{"rendered":"<h1>React.js + Next.js + \uac8c\uc2dc\ud310 \uc0dd\uc131<\/h1>\n<p>\uac04\ub2e8\ud55c \uac8c\uc2dc\ud310\uc744 \ub9cc\ub4e4\uc5b4 \ubd05\ub2c8\ub2e4.<br \/>\n\uc778\uacf5\uc9c0\ub2a5\uc774 \uc791\uc131\ud55c \ucf54\ub4dc\uc9c0\ub9cc \uc815\uc0c1\uc791\ub3d9 \ud655\uc778\ub418\uc5c8\uc2b5\ub2c8\ub2e4.<\/p>\n<h2>\ud504\ub85c\uc81d\ud2b8 \uad6c\uc870<\/h2>\n<pre><code class=\"language-bash\"># \ud504\ub85c\uc81d\ud2b8 \uad6c\uc870\nsimple-board\/\n\u251c\u2500\u2500 package.json\n\u251c\u2500\u2500 .env.local\n\u251c\u2500\u2500 lib\/\n\u2502   \u2514\u2500\u2500 db.js\n\u251c\u2500\u2500 pages\/\n\u2502   \u251c\u2500\u2500 index.js (\uac8c\uc2dc\uae00 \ubaa9\ub85d)\n\u2502   \u251c\u2500\u2500 post\/\n\u2502   \u2502   \u251c\u2500\u2500 [id].js (\uac8c\uc2dc\uae00 \uc0c1\uc138)\n\u2502   \u2502   \u2514\u2500\u2500 write.js (\uac8c\uc2dc\uae00 \uc791\uc131)\n\u2502   \u2514\u2500\u2500 api\/\n\u2502       \u2514\u2500\u2500 posts\/\n\u2502           \u251c\u2500\u2500 index.js (\ubaa9\ub85d \uc870\ud68c, \uc791\uc131)\n\u2502           \u2514\u2500\u2500 [id].js (\uc0c1\uc138 \uc870\ud68c, \uc218\uc815, \uc0ad\uc81c)\n\u251c\u2500\u2500 components\/\n\u2502   \u251c\u2500\u2500 Layout.js\n\u2502   \u2514\u2500\u2500 PostForm.js\n\u2514\u2500\u2500 styles\/\n    \u2514\u2500\u2500 globals.css<\/code><\/pre>\n<h2>\ud504\ub85c\uc81d\ud2b8 \uc0dd\uc131 \ubc0f \uc124\uc815<\/h2>\n<pre><code class=\"language-bash\">mkdir simple-board\ncd simple-board\n\nnpm init -y\nnpm install next react@18 react-dom@18 mysql2<\/code><\/pre>\n<pre><code class=\"language-bash\">CREATE DATABASE db_board;\n\nUSE db_board;\n\nCREATE TABLE posts (\n  id INT AUTO_INCREMENT PRIMARY KEY,\n  title VARCHAR(255) NOT NULL,\n  content TEXT NOT NULL,\n  author VARCHAR(100) NOT NULL,\n  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n);<\/code><\/pre>\n<h2>package.json<\/h2>\n<pre><code class=\"language-json\">{\n  &quot;name&quot;: &quot;simple-board&quot;,\n  &quot;version&quot;: &quot;1.0.0&quot;,\n  &quot;private&quot;: true,\n  &quot;scripts&quot;: {\n    &quot;dev&quot;: &quot;next dev&quot;,\n    &quot;build&quot;: &quot;next build&quot;,\n    &quot;start&quot;: &quot;next start&quot;\n  },\n  &quot;keywords&quot;: [],\n  &quot;author&quot;: &quot;&quot;,\n  &quot;description&quot;: &quot;&quot;,\n  &quot;dependencies&quot;: {\n    &quot;mysql2&quot;: &quot;^3.14.1&quot;,\n    &quot;next&quot;: &quot;^15.3.3&quot;,\n    &quot;react&quot;: &quot;^18.3.1&quot;,\n    &quot;react-dom&quot;: &quot;^18.3.1&quot;\n  }\n}<\/code><\/pre>\n<h2>.env.local<\/h2>\n<pre><code class=\"language-bash\">DB_HOST=localhost\nDB_USER=your_username\nDB_PASSWORD=your_password\nDB_NAME=db_board<\/code><\/pre>\n<h2>lib\/db.js<\/h2>\n<pre><code class=\"language-javascript\">import mysql from &#039;mysql2\/promise&#039;;\n\nconst pool = mysql.createPool({\n  host: process.env.DB_HOST,\n  user: process.env.DB_USER,\n  password: process.env.DB_PASSWORD,\n  database: process.env.DB_NAME,\n  waitForConnections: true,\n  connectionLimit: 10,\n  queueLimit: 0,\n  charset: &#039;utf8mb4&#039;  \/\/ \ud55c\uae00 \uc9c0\uc6d0\n});\n\nexport default pool;<\/code><\/pre>\n<h2>components\/Layout.js<\/h2>\n<pre><code class=\"language-javascript\">import Link from &#039;next\/link&#039;;\n\nexport default function Layout({ children }) {\n  return (\n    &lt;div style={{ maxWidth: &#039;800px&#039;, margin: &#039;0 auto&#039;, padding: &#039;20px&#039; }}&gt;\n      &lt;header style={{ borderBottom: &#039;1px solid #eee&#039;, paddingBottom: &#039;20px&#039;, marginBottom: &#039;20px&#039; }}&gt;\n        &lt;h1 style={{ margin: 0 }}&gt;\n          &lt;Link href=&quot;\/&quot; style={{ textDecoration: &#039;none&#039;, color: &#039;#333&#039; }}&gt;\n            \uac04\ub2e8\ud55c \uac8c\uc2dc\ud310\n          &lt;\/Link&gt;\n        &lt;\/h1&gt;\n        &lt;nav style={{ marginTop: &#039;10px&#039; }}&gt;\n          &lt;Link href=&quot;\/&quot; style={{ marginRight: &#039;20px&#039;, textDecoration: &#039;none&#039;, color: &#039;#666&#039; }}&gt;\n            \ubaa9\ub85d\n          &lt;\/Link&gt;\n          &lt;Link href=&quot;\/post\/write&quot; style={{ textDecoration: &#039;none&#039;, color: &#039;#666&#039; }}&gt;\n            \uae00\uc4f0\uae30\n          &lt;\/Link&gt;\n        &lt;\/nav&gt;\n      &lt;\/header&gt;\n      &lt;main&gt;{children}&lt;\/main&gt;\n    &lt;\/div&gt;\n  );\n}<\/code><\/pre>\n<h2>components\/PostForm.js<\/h2>\n<pre><code class=\"language-javascript\">import { useState } from &#039;react&#039;;\nimport { useRouter } from &#039;next\/router&#039;;\n\nexport default function PostForm({ post = null, isEdit = false }) {\n  const [title, setTitle] = useState(post?.title || &#039;&#039;);\n  const [content, setContent] = useState(post?.content || &#039;&#039;);\n  const [author, setAuthor] = useState(post?.author || &#039;&#039;);\n  const [loading, setLoading] = useState(false);\n  const router = useRouter();\n\n  const handleSubmit = async (e) =&gt; {\n    e.preventDefault();\n\n    if (!title.trim() || !content.trim() || !author.trim()) {\n      alert(&#039;\ubaa8\ub4e0 \ud544\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.&#039;);\n      return;\n    }\n\n    setLoading(true);\n\n    try {\n      const url = isEdit ? `\/api\/posts\/${post.id}` : &#039;\/api\/posts&#039;;\n      const method = isEdit ? &#039;PUT&#039; : &#039;POST&#039;;\n\n      const response = await fetch(url, {\n        method,\n        headers: {\n          &#039;Content-Type&#039;: &#039;application\/json&#039;,\n        },\n        body: JSON.stringify({ title, content, author }),\n      });\n\n      if (response.ok) {\n        router.push(&#039;\/&#039;);\n      } else {\n        alert(&#039;\uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.&#039;);\n      }\n    } catch (error) {\n      console.error(&#039;Error:&#039;, error);\n      alert(&#039;\uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.&#039;);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  return (\n    &lt;form onSubmit={handleSubmit} style={{ display: &#039;flex&#039;, flexDirection: &#039;column&#039;, gap: &#039;15px&#039; }}&gt;\n      &lt;div&gt;\n        &lt;label style={{ display: &#039;block&#039;, marginBottom: &#039;5px&#039;, fontWeight: &#039;bold&#039; }}&gt;\uc81c\ubaa9&lt;\/label&gt;\n        &lt;input\n          type=&quot;text&quot;\n          value={title}\n          onChange={(e) =&gt; setTitle(e.target.value)}\n          style={{ \n            width: &#039;100%&#039;, \n            padding: &#039;10px&#039;, \n            border: &#039;1px solid #ddd&#039;, \n            borderRadius: &#039;4px&#039;,\n            fontSize: &#039;16px&#039;\n          }}\n          disabled={loading}\n        \/&gt;\n      &lt;\/div&gt;\n\n      &lt;div&gt;\n        &lt;label style={{ display: &#039;block&#039;, marginBottom: &#039;5px&#039;, fontWeight: &#039;bold&#039; }}&gt;\uc791\uc131\uc790&lt;\/label&gt;\n        &lt;input\n          type=&quot;text&quot;\n          value={author}\n          onChange={(e) =&gt; setAuthor(e.target.value)}\n          style={{ \n            width: &#039;100%&#039;, \n            padding: &#039;10px&#039;, \n            border: &#039;1px solid #ddd&#039;, \n            borderRadius: &#039;4px&#039;,\n            fontSize: &#039;16px&#039;\n          }}\n          disabled={loading || isEdit}\n        \/&gt;\n      &lt;\/div&gt;\n\n      &lt;div&gt;\n        &lt;label style={{ display: &#039;block&#039;, marginBottom: &#039;5px&#039;, fontWeight: &#039;bold&#039; }}&gt;\ub0b4\uc6a9&lt;\/label&gt;\n        &lt;textarea\n          value={content}\n          onChange={(e) =&gt; setContent(e.target.value)}\n          rows=&quot;10&quot;\n          style={{ \n            width: &#039;100%&#039;, \n            padding: &#039;10px&#039;, \n            border: &#039;1px solid #ddd&#039;, \n            borderRadius: &#039;4px&#039;,\n            fontSize: &#039;16px&#039;,\n            resize: &#039;vertical&#039;\n          }}\n          disabled={loading}\n        \/&gt;\n      &lt;\/div&gt;\n\n      &lt;div style={{ display: &#039;flex&#039;, gap: &#039;10px&#039; }}&gt;\n        &lt;button\n          type=&quot;submit&quot;\n          disabled={loading}\n          style={{ \n            padding: &#039;12px 24px&#039;, \n            backgroundColor: &#039;#007bff&#039;, \n            color: &#039;white&#039;, \n            border: &#039;none&#039;, \n            borderRadius: &#039;4px&#039;, \n            cursor: loading ? &#039;not-allowed&#039; : &#039;pointer&#039;,\n            fontSize: &#039;16px&#039;\n          }}\n        &gt;\n          {loading ? &#039;\ucc98\ub9ac\uc911...&#039; : (isEdit ? &#039;\uc218\uc815&#039; : &#039;\uc791\uc131&#039;)}\n        &lt;\/button&gt;\n\n        &lt;button\n          type=&quot;button&quot;\n          onClick={() =&gt; router.back()}\n          style={{ \n            padding: &#039;12px 24px&#039;, \n            backgroundColor: &#039;#6c757d&#039;, \n            color: &#039;white&#039;, \n            border: &#039;none&#039;, \n            borderRadius: &#039;4px&#039;, \n            cursor: &#039;pointer&#039;,\n            fontSize: &#039;16px&#039;\n          }}\n        &gt;\n          \ucde8\uc18c\n        &lt;\/button&gt;\n      &lt;\/div&gt;\n    &lt;\/form&gt;\n  );\n}<\/code><\/pre>\n<h2>pages\/api\/posts\/index.js<\/h2>\n<pre><code class=\"language-javascript\">import pool from &#039;..\/..\/..\/lib\/db&#039;;\n\nexport default async function handler(req, res) {\n  if (req.method === &#039;GET&#039;) {\n    \/\/ \uac8c\uc2dc\uae00 \ubaa9\ub85d \uc870\ud68c\n    try {\n      const [rows] = await pool.execute(\n        &#039;SELECT id, title, author, created_at FROM posts ORDER BY created_at DESC&#039;\n      );\n      res.status(200).json(rows);\n    } catch (error) {\n      console.error(&#039;Database error:&#039;, error);\n      res.status(500).json({ error: &#039;\ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.&#039; });\n    }\n  } else if (req.method === &#039;POST&#039;) {\n    \/\/ \uac8c\uc2dc\uae00 \uc791\uc131\n    const { title, content, author } = req.body;\n\n    if (!title || !content || !author) {\n      return res.status(400).json({ error: &#039;\ubaa8\ub4e0 \ud544\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.&#039; });\n    }\n\n    try {\n      const [result] = await pool.execute(\n        &#039;INSERT INTO posts (title, content, author) VALUES (?, ?, ?)&#039;,\n        [title, content, author]\n      );\n\n      res.status(201).json({ \n        id: result.insertId,\n        message: &#039;\uac8c\uc2dc\uae00\uc774 \uc791\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.&#039; \n      });\n    } catch (error) {\n      console.error(&#039;Database error:&#039;, error);\n      res.status(500).json({ error: &#039;\ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.&#039; });\n    }\n  } else {\n    res.setHeader(&#039;Allow&#039;, [&#039;GET&#039;, &#039;POST&#039;]);\n    res.status(405).json({ error: `Method ${req.method} Not Allowed` });\n  }\n}<\/code><\/pre>\n<h2>pages\/api\/posts\/[id].js<\/h2>\n<pre><code class=\"language-javascript\">import pool from &#039;..\/..\/..\/lib\/db&#039;;\n\nexport default async function handler(req, res) {\n  const { id } = req.query;\n  const postId = parseInt(id);\n\n  if (isNaN(postId)) {\n    return res.status(400).json({ error: &#039;\uc798\ubabb\ub41c \uac8c\uc2dc\uae00 ID\uc785\ub2c8\ub2e4.&#039; });\n  }\n\n  if (req.method === &#039;GET&#039;) {\n    \/\/ \uac8c\uc2dc\uae00 \uc0c1\uc138 \uc870\ud68c\n    try {\n      const [rows] = await pool.execute(\n        &#039;SELECT * FROM posts WHERE id = ?&#039;,\n        [postId]\n      );\n\n      if (rows.length === 0) {\n        return res.status(404).json({ error: &#039;\uac8c\uc2dc\uae00\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.&#039; });\n      }\n\n      res.status(200).json(rows[0]);\n    } catch (error) {\n      console.error(&#039;Database error:&#039;, error);\n      res.status(500).json({ error: &#039;\ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.&#039; });\n    }\n  } else if (req.method === &#039;PUT&#039;) {\n    \/\/ \uac8c\uc2dc\uae00 \uc218\uc815\n    const { title, content, author } = req.body;\n\n    if (!title || !content || !author) {\n      return res.status(400).json({ error: &#039;\ubaa8\ub4e0 \ud544\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.&#039; });\n    }\n\n    try {\n      const [result] = await pool.execute(\n        &#039;UPDATE posts SET title = ?, content = ?, author = ? WHERE id = ?&#039;,\n        [title, content, author, postId]\n      );\n\n      if (result.affectedRows === 0) {\n        return res.status(404).json({ error: &#039;\uac8c\uc2dc\uae00\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.&#039; });\n      }\n\n      res.status(200).json({ message: &#039;\uac8c\uc2dc\uae00\uc774 \uc218\uc815\ub418\uc5c8\uc2b5\ub2c8\ub2e4.&#039; });\n    } catch (error) {\n      console.error(&#039;Database error:&#039;, error);\n      res.status(500).json({ error: &#039;\ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.&#039; });\n    }\n  } else if (req.method === &#039;DELETE&#039;) {\n    \/\/ \uac8c\uc2dc\uae00 \uc0ad\uc81c\n    try {\n      const [result] = await pool.execute(\n        &#039;DELETE FROM posts WHERE id = ?&#039;,\n        [postId]\n      );\n\n      if (result.affectedRows === 0) {\n        return res.status(404).json({ error: &#039;\uac8c\uc2dc\uae00\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.&#039; });\n      }\n\n      res.status(200).json({ message: &#039;\uac8c\uc2dc\uae00\uc774 \uc0ad\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4.&#039; });\n    } catch (error) {\n      console.error(&#039;Database error:&#039;, error);\n      res.status(500).json({ error: &#039;\ub370\uc774\ud130\ubca0\uc774\uc2a4 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.&#039; });\n    }\n  } else {\n    res.setHeader(&#039;Allow&#039;, [&#039;GET&#039;, &#039;PUT&#039;, &#039;DELETE&#039;]);\n    res.status(405).json({ error: `Method ${req.method} Not Allowed` });\n  }\n}<\/code><\/pre>\n<h2>pages\/index.js<\/h2>\n<pre><code class=\"language-javascript\">import Link from &#039;next\/link&#039;;\nimport Layout from &#039;..\/components\/Layout&#039;;\nimport pool from &#039;..\/lib\/db&#039;;\n\nexport default function Home({ posts }) {\n  const formatDate = (dateString) =&gt; {\n    const date = new Date(dateString);\n    return date.toLocaleDateString(&#039;ko-KR&#039;, {\n      year: &#039;numeric&#039;,\n      month: &#039;2-digit&#039;,\n      day: &#039;2-digit&#039;,\n      hour: &#039;2-digit&#039;,\n      minute: &#039;2-digit&#039;\n    });\n  };\n\n  return (\n    &lt;Layout&gt;\n      &lt;div style={{ display: &#039;flex&#039;, justifyContent: &#039;space-between&#039;, alignItems: &#039;center&#039;, marginBottom: &#039;20px&#039; }}&gt;\n        &lt;h2 style={{ margin: 0 }}&gt;\uac8c\uc2dc\uae00 \ubaa9\ub85d&lt;\/h2&gt;\n        &lt;Link \n          href=&quot;\/post\/write&quot;\n          style={{ \n            padding: &#039;10px 20px&#039;, \n            backgroundColor: &#039;#007bff&#039;, \n            color: &#039;white&#039;, \n            textDecoration: &#039;none&#039;, \n            borderRadius: &#039;4px&#039;,\n            fontSize: &#039;14px&#039;\n          }}\n        &gt;\n          \uae00\uc4f0\uae30\n        &lt;\/Link&gt;\n      &lt;\/div&gt;\n\n      {posts.length === 0 ? (\n        &lt;div style={{ \n          textAlign: &#039;center&#039;, \n          padding: &#039;50px&#039;, \n          color: &#039;#666&#039;,\n          border: &#039;1px solid #eee&#039;,\n          borderRadius: &#039;4px&#039;\n        }}&gt;\n          &lt;p&gt;\ub4f1\ub85d\ub41c \uac8c\uc2dc\uae00\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.&lt;\/p&gt;\n          &lt;Link href=&quot;\/post\/write&quot; style={{ color: &#039;#007bff&#039;, textDecoration: &#039;none&#039; }}&gt;\n            \uccab \ubc88\uc9f8 \uae00\uc744 \uc791\uc131\ud574\ubcf4\uc138\uc694!\n          &lt;\/Link&gt;\n        &lt;\/div&gt;\n      ) : (\n        &lt;div style={{ border: &#039;1px solid #ddd&#039;, borderRadius: &#039;4px&#039; }}&gt;\n          {posts.map((post, index) =&gt; (\n            &lt;div \n              key={post.id} \n              style={{ \n                padding: &#039;15px 20px&#039;, \n                borderBottom: index &lt; posts.length - 1 ? &#039;1px solid #eee&#039; : &#039;none&#039;,\n                display: &#039;flex&#039;,\n                justifyContent: &#039;space-between&#039;,\n                alignItems: &#039;center&#039;\n              }}\n            &gt;\n              &lt;div style={{ flex: 1 }}&gt;\n                &lt;Link \n                  href={`\/post\/${post.id}`}\n                  style={{ \n                    textDecoration: &#039;none&#039;, \n                    color: &#039;#333&#039;,\n                    fontSize: &#039;16px&#039;,\n                    fontWeight: &#039;500&#039;\n                  }}\n                &gt;\n                  {post.title}\n                &lt;\/Link&gt;\n                &lt;div style={{ \n                  marginTop: &#039;5px&#039;, \n                  fontSize: &#039;14px&#039;, \n                  color: &#039;#666&#039; \n                }}&gt;\n                  {post.author} \u2022 {formatDate(post.created_at)}\n                &lt;\/div&gt;\n              &lt;\/div&gt;\n            &lt;\/div&gt;\n          ))}\n        &lt;\/div&gt;\n      )}\n    &lt;\/Layout&gt;\n  );\n}\n\n\/\/ \uc11c\ubc84\uc0ac\uc774\ub4dc \ub80c\ub354\ub9c1\uc73c\ub85c \uac8c\uc2dc\uae00 \ubaa9\ub85d\uc744 \uac00\uc838\uc634\nexport async function getServerSideProps() {\n  try {\n    const [rows] = await pool.execute(\n      &#039;SELECT id, title, author, created_at FROM posts ORDER BY created_at DESC&#039;\n    );\n\n    \/\/ Date \uac1d\uccb4\ub97c \ubb38\uc790\uc5f4\ub85c \ubcc0\ud658 (JSON \uc9c1\ub82c\ud654\ub97c \uc704\ud574)\n    const posts = rows.map(post =&gt; ({\n      ...post,\n      created_at: post.created_at.toISOString()\n    }));\n\n    return {\n      props: {\n        posts\n      }\n    };\n  } catch (error) {\n    console.error(&#039;Database error:&#039;, error);\n    return {\n      props: {\n        posts: []\n      }\n    };\n  }\n}<\/code><\/pre>\n<h2>pages\/post\/[id].js<\/h2>\n<pre><code class=\"language-javascript\">import { useState } from &#039;react&#039;;\nimport { useRouter } from &#039;next\/router&#039;;\nimport Link from &#039;next\/link&#039;;\nimport Layout from &#039;..\/..\/components\/Layout&#039;;\nimport PostForm from &#039;..\/..\/components\/PostForm&#039;;\nimport pool from &#039;..\/..\/lib\/db&#039;;\n\nexport default function PostDetail({ post }) {\n  const [isEditing, setIsEditing] = useState(false);\n  const [loading, setLoading] = useState(false);\n  const router = useRouter();\n\n  if (!post) {\n    return (\n      &lt;Layout&gt;\n        &lt;div style={{ textAlign: &#039;center&#039;, padding: &#039;50px&#039; }}&gt;\n          &lt;h2&gt;\uac8c\uc2dc\uae00\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4&lt;\/h2&gt;\n          &lt;Link href=&quot;\/&quot; style={{ color: &#039;#007bff&#039;, textDecoration: &#039;none&#039; }}&gt;\n            \ubaa9\ub85d\uc73c\ub85c \ub3cc\uc544\uac00\uae30\n          &lt;\/Link&gt;\n        &lt;\/div&gt;\n      &lt;\/Layout&gt;\n    );\n  }\n\n  const formatDate = (dateString) =&gt; {\n    const date = new Date(dateString);\n    return date.toLocaleDateString(&#039;ko-KR&#039;, {\n      year: &#039;numeric&#039;,\n      month: &#039;2-digit&#039;,\n      day: &#039;2-digit&#039;,\n      hour: &#039;2-digit&#039;,\n      minute: &#039;2-digit&#039;\n    });\n  };\n\n  const handleDelete = async () =&gt; {\n    if (!confirm(&#039;\uc815\ub9d0 \uc0ad\uc81c\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?&#039;)) {\n      return;\n    }\n\n    setLoading(true);\n\n    try {\n      const response = await fetch(`\/api\/posts\/${post.id}`, {\n        method: &#039;DELETE&#039;,\n      });\n\n      if (response.ok) {\n        alert(&#039;\uac8c\uc2dc\uae00\uc774 \uc0ad\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4.&#039;);\n        router.push(&#039;\/&#039;);\n      } else {\n        alert(&#039;\uc0ad\uc81c \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.&#039;);\n      }\n    } catch (error) {\n      console.error(&#039;Error:&#039;, error);\n      alert(&#039;\uc0ad\uc81c \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.&#039;);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  if (isEditing) {\n    return (\n      &lt;Layout&gt;\n        &lt;h2&gt;\uac8c\uc2dc\uae00 \uc218\uc815&lt;\/h2&gt;\n        &lt;PostForm post={post} isEdit={true} \/&gt;\n      &lt;\/Layout&gt;\n    );\n  }\n\n  return (\n    &lt;Layout&gt;\n      &lt;div style={{ marginBottom: &#039;20px&#039; }}&gt;\n        &lt;Link href=&quot;\/&quot; style={{ color: &#039;#007bff&#039;, textDecoration: &#039;none&#039; }}&gt;\n          \u2190 \ubaa9\ub85d\uc73c\ub85c\n        &lt;\/Link&gt;\n      &lt;\/div&gt;\n\n      &lt;article style={{ border: &#039;1px solid #ddd&#039;, borderRadius: &#039;4px&#039;, padding: &#039;20px&#039; }}&gt;\n        &lt;header style={{ borderBottom: &#039;1px solid #eee&#039;, paddingBottom: &#039;15px&#039;, marginBottom: &#039;20px&#039; }}&gt;\n          &lt;h1 style={{ margin: &#039;0 0 10px 0&#039;, fontSize: &#039;24px&#039;, color: &#039;#333&#039; }}&gt;\n            {post.title}\n          &lt;\/h1&gt;\n          &lt;div style={{ fontSize: &#039;14px&#039;, color: &#039;#666&#039; }}&gt;\n            &lt;span&gt;{post.author}&lt;\/span&gt;\n            &lt;span style={{ margin: &#039;0 10px&#039; }}&gt;\u2022&lt;\/span&gt;\n            &lt;span&gt;\uc791\uc131\uc77c: {formatDate(post.created_at)}&lt;\/span&gt;\n            {post.updated_at !== post.created_at &amp;&amp; (\n              &lt;&gt;\n                &lt;span style={{ margin: &#039;0 10px&#039; }}&gt;\u2022&lt;\/span&gt;\n                &lt;span&gt;\uc218\uc815\uc77c: {formatDate(post.updated_at)}&lt;\/span&gt;\n              &lt;\/&gt;\n            )}\n          &lt;\/div&gt;\n        &lt;\/header&gt;\n\n        &lt;div style={{ \n          lineHeight: &#039;1.6&#039;, \n          fontSize: &#039;16px&#039;, \n          whiteSpace: &#039;pre-wrap&#039;,\n          minHeight: &#039;200px&#039;\n        }}&gt;\n          {post.content}\n        &lt;\/div&gt;\n      &lt;\/article&gt;\n\n      &lt;div style={{ \n        marginTop: &#039;20px&#039;, \n        display: &#039;flex&#039;, \n        gap: &#039;10px&#039;,\n        justifyContent: &#039;flex-end&#039;\n      }}&gt;\n        &lt;button\n          onClick={() =&gt; setIsEditing(true)}\n          style={{ \n            padding: &#039;10px 20px&#039;, \n            backgroundColor: &#039;#28a745&#039;, \n            color: &#039;white&#039;, \n            border: &#039;none&#039;, \n            borderRadius: &#039;4px&#039;, \n            cursor: &#039;pointer&#039;,\n            fontSize: &#039;14px&#039;\n          }}\n        &gt;\n          \uc218\uc815\n        &lt;\/button&gt;\n\n        &lt;button\n          onClick={handleDelete}\n          disabled={loading}\n          style={{ \n            padding: &#039;10px 20px&#039;, \n            backgroundColor: &#039;#dc3545&#039;, \n            color: &#039;white&#039;, \n            border: &#039;none&#039;, \n            borderRadius: &#039;4px&#039;, \n            cursor: loading ? &#039;not-allowed&#039; : &#039;pointer&#039;,\n            fontSize: &#039;14px&#039;\n          }}\n        &gt;\n          {loading ? &#039;\uc0ad\uc81c\uc911...&#039; : &#039;\uc0ad\uc81c&#039;}\n        &lt;\/button&gt;\n      &lt;\/div&gt;\n    &lt;\/Layout&gt;\n  );\n}\n\nexport async function getServerSideProps({ params }) {\n  const { id } = params;\n  const postId = parseInt(id);\n\n  if (isNaN(postId)) {\n    return {\n      notFound: true\n    };\n  }\n\n  try {\n    const [rows] = await pool.execute(\n      &#039;SELECT * FROM posts WHERE id = ?&#039;,\n      [postId]\n    );\n\n    if (rows.length === 0) {\n      return {\n        notFound: true\n      };\n    }\n\n    const post = {\n      ...rows[0],\n      created_at: rows[0].created_at.toISOString(),\n      updated_at: rows[0].updated_at.toISOString()\n    };\n\n    return {\n      props: {\n        post\n      }\n    };\n  } catch (error) {\n    console.error(&#039;Database error:&#039;, error);\n    return {\n      notFound: true\n    };\n  }\n}<\/code><\/pre>\n<h2>pages\/post\/write.js<\/h2>\n<pre><code class=\"language-javascript\">import Link from &#039;next\/link&#039;;\nimport Layout from &#039;..\/..\/components\/Layout&#039;;\nimport PostForm from &#039;..\/..\/components\/PostForm&#039;;\n\nexport default function WritePost() {\n  return (\n    &lt;Layout&gt;\n      &lt;div style={{ marginBottom: &#039;20px&#039; }}&gt;\n        &lt;Link href=&quot;\/&quot; style={{ color: &#039;#007bff&#039;, textDecoration: &#039;none&#039; }}&gt;\n          \u2190 \ubaa9\ub85d\uc73c\ub85c\n        &lt;\/Link&gt;\n      &lt;\/div&gt;\n\n      &lt;h2 style={{ marginBottom: &#039;20px&#039; }}&gt;\uc0c8 \uac8c\uc2dc\uae00 \uc791\uc131&lt;\/h2&gt;\n\n      &lt;PostForm \/&gt;\n    &lt;\/Layout&gt;\n  );\n}<\/code><\/pre>\n<h2>styles\/globals.css<\/h2>\n<pre><code class=\"language-css\">* {\n  box-sizing: border-box;\n  margin: 0;\n  padding: 0;\n}\n\nhtml,\nbody {\n  font-family: -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n  line-height: 1.6;\n  color: #333;\n  background-color: #f8f9fa;\n}\n\na {\n  color: inherit;\n  text-decoration: none;\n}\n\nbutton {\n  font-family: inherit;\n}\n\ninput,\ntextarea {\n  font-family: inherit;\n  outline: none;\n}\n\ninput:focus,\ntextarea:focus {\n  border-color: #007bff;\n  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);\n}\n\nbutton:hover:not(:disabled) {\n  opacity: 0.9;\n  transform: translateY(-1px);\n  transition: all 0.2s ease;\n}\n\nbutton:disabled {\n  opacity: 0.6;\n}<\/code><\/pre>\n<h2>pages\/_app.js<\/h2>\n<pre><code class=\"language-javascript\">import &#039;..\/styles\/globals.css&#039;;\n\nexport default function App({ Component, pageProps }) {\n  return &lt;Component {...pageProps} \/&gt;;\n}<\/code><\/pre>\n<h2>\uc2e4\ud589<\/h2>\n<pre><code class=\"language-bash\">npm run dev<\/code><\/pre>\n<p>\ube0c\ub77c\uc6b0\uc800\uc5d0\uc11c <a href=\"http:\/\/localhost:3000\uc73c\ub85c\">http:\/\/localhost:3000\uc73c\ub85c<\/a> \uc811\uc18d\ud558\uc5ec \uac8c\uc2dc\ud310\uc744 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>React.js + Next.js + \uac8c\uc2dc\ud310 \uc0dd\uc131 \uac04\ub2e8\ud55c \uac8c\uc2dc\ud310\uc744 \ub9cc\ub4e4\uc5b4 \ubd05\ub2c8\ub2e4. \uc778\uacf5\uc9c0\ub2a5\uc774 \uc791\uc131\ud55c \ucf54\ub4dc\uc9c0\ub9cc \uc815\uc0c1\uc791\ub3d9 \ud655\uc778\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud504\ub85c\uc81d\ud2b8 \uad6c\uc870 # \ud504\ub85c\uc81d\ud2b8 \uad6c\uc870 simple-board\/ \u251c\u2500\u2500 package.json \u251c\u2500\u2500 .env.local \u251c\u2500\u2500 lib\/ \u2502 \u2514\u2500\u2500 db.js \u251c\u2500\u2500 pages\/ \u2502 \u251c\u2500\u2500 index.js (\uac8c\uc2dc\uae00 \ubaa9\ub85d) \u2502 \u251c\u2500\u2500 post\/ \u2502 \u2502 \u251c\u2500\u2500 [id].js (\uac8c\uc2dc\uae00 \uc0c1\uc138) \u2502 \u2502 \u2514\u2500\u2500 write.js (\uac8c\uc2dc\uae00 \uc791\uc131) \u2502 \u2514\u2500\u2500 api\/\u2026 <span class=\"read-more\"><a href=\"https:\/\/www.skyer9.pe.kr\/wordpress\/?p=10433\">Read More &raquo;<\/a><\/span><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[40],"tags":[],"class_list":["post-10433","post","type-post","status-publish","format-standard","hentry","category-language"],"_links":{"self":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/10433","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=10433"}],"version-history":[{"count":6,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/10433\/revisions"}],"predecessor-version":[{"id":10439,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/10433\/revisions\/10439"}],"wp:attachment":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=10433"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=10433"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=10433"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}