diff --git a/bun.lock b/bun.lock
index db9a513..91469e2 100644
--- a/bun.lock
+++ b/bun.lock
@@ -6,6 +6,7 @@
"name": "daleui",
"dependencies": {
"@fontsource-variable/jetbrains-mono": "^5.2.8",
+ "@giscus/react": "^3.1.0",
"@mdx-js/react": "^3.1.1",
"@react-router/node": "^7.15.1",
"browserslist": "^4.28.2",
@@ -265,6 +266,8 @@
"@fortawesome/fontawesome-free": ["@fortawesome/fontawesome-free@6.7.2", "", {}, "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA=="],
+ "@giscus/react": ["@giscus/react@3.1.0", "", { "dependencies": { "giscus": "^1.6.0" }, "peerDependencies": { "react": "^16 || ^17 || ^18 || ^19", "react-dom": "^16 || ^17 || ^18 || ^19" } }, "sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg=="],
+
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
@@ -343,6 +346,10 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+ "@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.6.0", "", {}, "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ=="],
+
+ "@lit/reactive-element": ["@lit/reactive-element@2.1.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0" } }, "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A=="],
+
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
"@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="],
@@ -1309,6 +1316,8 @@
"get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
+ "giscus": ["giscus@1.6.0", "", { "dependencies": { "lit": "^3.2.1" } }, "sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ=="],
+
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
@@ -1551,6 +1560,12 @@
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
+ "lit": ["lit@3.3.3", "", { "dependencies": { "@lit/reactive-element": "^2.1.0", "lit-element": "^4.2.0", "lit-html": "^3.3.0" } }, "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw=="],
+
+ "lit-element": ["lit-element@4.2.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0", "@lit/reactive-element": "^2.1.0", "lit-html": "^3.3.0" } }, "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w=="],
+
+ "lit-html": ["lit-html@3.3.3", "", { "dependencies": { "@types/trusted-types": "^2.0.2" } }, "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA=="],
+
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
diff --git a/package.json b/package.json
index 9a0d0ba..53369c6 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
},
"dependencies": {
"@fontsource-variable/jetbrains-mono": "^5.2.8",
+ "@giscus/react": "^3.1.0",
"@mdx-js/react": "^3.1.1",
"@react-router/node": "^7.15.1",
"browserslist": "^4.28.2",
diff --git a/src/components/Giscus.tsx b/src/components/Giscus.tsx
new file mode 100644
index 0000000..2f04995
--- /dev/null
+++ b/src/components/Giscus.tsx
@@ -0,0 +1,36 @@
+import GiscusWidget from "@giscus/react";
+import { useTheme } from "../hooks/useTheme";
+import { giscusConfig } from "../constants/giscus";
+
+interface GiscusProps {
+ /** 댓글을 붙일 글의 discussion 번호(= slug). */
+ term: string;
+}
+
+/**
+ * giscus 댓글 위젯.
+ *
+ * 스크립트 주입·iframe 생명주기·테마/글 변경 시 setConfig 전달은 공식
+ * `@giscus/react`가 맡는다. 여기서는 설정값(constants/giscus)과 현재 테마만 넘긴다.
+ *
+ * 블로그 글 slug가 곧 "블로그" 카테고리 Discussion 번호라 mapping=number로 연결한다.
+ */
+export function Giscus({ term }: GiscusProps) {
+ const { isDark } = useTheme();
+
+ return (
+
+ );
+}
diff --git a/src/constants/giscus.test.ts b/src/constants/giscus.test.ts
new file mode 100644
index 0000000..c0bcc79
--- /dev/null
+++ b/src/constants/giscus.test.ts
@@ -0,0 +1,14 @@
+import { describe, expect, it } from "vitest";
+import { giscusConfig } from "./giscus";
+
+describe("giscusConfig", () => {
+ it("repo·category 기본값을 노출한다", () => {
+ expect(giscusConfig.repo).toBe("DaleStudy/daleui");
+ expect(giscusConfig.category).toBe("블로그");
+ });
+
+ it("repo-id·category-id를 함께 노출한다", () => {
+ expect(giscusConfig.repoId).toBeTruthy();
+ expect(giscusConfig.categoryId).toBeTruthy();
+ });
+});
diff --git a/src/constants/giscus.ts b/src/constants/giscus.ts
new file mode 100644
index 0000000..9cfc8d8
--- /dev/null
+++ b/src/constants/giscus.ts
@@ -0,0 +1,9 @@
+// giscus 댓글 위젯 설정값. 모두 공개돼도 되는 값이라 그대로 하드코딩한다.
+// 블로그 글은 DaleStudy/daleui "블로그" 카테고리 Discussion에서 오고 slug가 곧
+// discussion 번호라, mapping=number로 글마다 원본 토론에 그대로 연결한다.
+export const giscusConfig = {
+ repo: "DaleStudy/daleui" as `${string}/${string}`,
+ repoId: "R_kgDOMovgOA",
+ category: "블로그",
+ categoryId: "DIC_kwDOMovgOM4CqB1o",
+} as const;
diff --git a/src/routes/blog.$slug.tsx b/src/routes/blog.$slug.tsx
index 35553d7..9a2e371 100644
--- a/src/routes/blog.$slug.tsx
+++ b/src/routes/blog.$slug.tsx
@@ -5,6 +5,7 @@ import type { Route } from "./+types/blog.$slug";
import { css } from "../../styled-system/css";
import { findBlog, listBlog } from "../content/blog/loader";
import { UserProfile } from "../components/UserProfile";
+import { Giscus } from "../components/Giscus";
import DirectionalLink from "../components/DirectionalLink";
import { Navigation } from "../sections/blog/Navigation";
@@ -152,6 +153,10 @@ export default function BlogSlug({ loaderData }: Route.ComponentProps) {
)}
+
+
+
+
>