From b9642c7edc4512c4f501ae8e770f87760f1c532c Mon Sep 17 00:00:00 2001 From: sjw4371 Date: Tue, 14 Oct 2025 13:12:16 +0900 Subject: [PATCH 1/5] =?UTF-8?q?docs:=20readme=EC=97=90=20env=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=84=A4=EB=AA=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2c24aa8..dcf8645 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,36 @@ # TechBlog - ## 프로젝트 개요 NextJS를 기반으로 한 개인 기술 블로그와 포트폴리오 웹 애플리케이션입니다. 테크 관련 게시물을 작성하고 개인 프로젝트를 전시하는 공간으로 활용하는 프로젝트 -### 블로그 +### 블로그 + ![image](https://github.com/user-attachments/assets/240cc1a5-5ad6-4921-bbef-ee1cd76fa379) -### 블로그 작성 +### 블로그 작성 + ![image](https://github.com/user-attachments/assets/4bb0c223-cfa2-4414-9a47-59883820d08b) -### 시리즈 -![image](https://github.com/user-attachments/assets/25d14078-8734-44a4-8943-aa7a2f70951f) +### 시리즈 +![image](https://github.com/user-attachments/assets/25d14078-8734-44a4-8943-aa7a2f70951f) ## 기술 스택 ### Frontend + - **Framework**: [Next.js](https://nextjs.org/) (App Router) - **Language**: [TypeScript](https://www.typescriptlang.org/) - **Styling**: [TailwindCSS](https://tailwindcss.com/) - **State Management**: [Zustand](https://github.com/pmndrs/zustand) -- **UI Components**: +- **UI Components**: - [React Icons](https://react-icons.github.io/react-icons/) - [React Lottie Player](https://github.com/LottieFiles/react-lottie-player) - **Markdown Editor**: [@uiw/react-md-editor](https://uiwjs.github.io/react-md-editor/) ### Backend + - **Runtime**: [Node.js](https://nodejs.org/) - **Database**: [MongoDB](https://www.mongodb.com/) (with [Mongoose](https://mongoosejs.com/)) - **Authentication**: [NextAuth.js](https://next-auth.js.org/) @@ -35,8 +38,9 @@ NextJS를 기반으로 한 개인 기술 블로그와 포트폴리오 웹 애플 - **HTTP Client**: [Axios](https://axios-http.com/) ### DevOps & Testing + - **Testing Framework**: [Jest](https://jestjs.io/) with [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) -- **Linting & Formatting**: +- **Linting & Formatting**: - [ESLint](https://eslint.org/) - [Prettier](https://prettier.io/) @@ -63,3 +67,19 @@ TechBlog/ ├── .github/ # GitHub 워크플로우 └── __test__/ # 테스트 파일 ``` + +## 필요한 환경변수 + +.env 파일에 필요한 환경변수는 다음과 같다. + +```text + 1 GITHUB_ID=your_github_client_id + 2 GITHUB_SECRET=your_github_client_secret + 3 ADMIN_EMAIL=your_admin_email@example.com + 4 NEXTAUTH_SECRET=your_nextauth_secret + 5 NEXTAUTH_URL=http://localhost:3000 + 6 DB_URI=your_mongodb_connection_string + 7 NEXT_PUBLIC_DEPLOYMENT_URL=https://your-deployment-url.com + 8 NEXT_PUBLIC_URL=http://localhost:3000 + 9 BLOB_READ_WRITE_TOKEN=your_vercel_blob_token +``` From 4561c22c8dd20458cae1c95d223226ae9d4e9369 Mon Sep 17 00:00:00 2001 From: sjw4371 Date: Tue, 14 Oct 2025 14:25:53 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=82=B4=EB=B6=80=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EB=AA=A8=EC=95=84=EB=B3=B4=EA=B8=B0=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/comments/page.tsx | 138 ++++++++++++++++ app/api/admin/comments/route.ts | 171 ++++++++++++++++++++ app/entities/admin/comments/CommentItem.tsx | 56 +++++++ app/entities/admin/comments/IssueCard.tsx | 168 +++++++++++++++++++ 4 files changed, 533 insertions(+) create mode 100644 app/admin/comments/page.tsx create mode 100644 app/api/admin/comments/route.ts create mode 100644 app/entities/admin/comments/CommentItem.tsx create mode 100644 app/entities/admin/comments/IssueCard.tsx diff --git a/app/admin/comments/page.tsx b/app/admin/comments/page.tsx new file mode 100644 index 0000000..4bf9181 --- /dev/null +++ b/app/admin/comments/page.tsx @@ -0,0 +1,138 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useSession } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import IssueCard from '@/app/entities/admin/comments/IssueCard'; + +interface GitHubUser { + login: string; + avatar_url: string; +} + +interface GitHubComment { + id: number; + user: GitHubUser; + created_at: string; + updated_at: string; + body: string; + html_url: string; +} + +interface GitHubIssue { + id: number; + number: number; + title: string; + html_url: string; + state: string; + comments: number; + created_at: string; + updated_at: string; + user: GitHubUser; + body?: string; +} + +interface IssueWithComments { + issue: GitHubIssue; + comments: GitHubComment[]; +} + +const AdminCommentsPage = () => { + const { status } = useSession(); + const router = useRouter(); + const [issuesWithComments, setIssuesWithComments] = useState< + IssueWithComments[] + >([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (status === 'unauthenticated') { + router.push('/admin'); + } + }, [status, router]); + + useEffect(() => { + if (status === 'authenticated') { + fetchComments(); + } + }, [status]); + + const fetchComments = async () => { + try { + setLoading(true); + const response = await fetch('/api/admin/comments'); + const data = await response.json(); + + if (data.success) { + setIssuesWithComments(data.data); + } else { + setError(data.error || '댓글을 불러올 수 없습니다.'); + } + } catch (err) { + setError('댓글을 불러오는 중 오류가 발생했습니다.'); + console.error(err); + } finally { + setLoading(false); + } + }; + + if (status === 'loading' || loading) { + return ( +
+

댓글 관리

+
+
+

댓글을 불러오는 중...

+
+
+ ); + } + + if (error) { + return ( +
+

댓글 관리

+
+ {error} +
+
+ ); + } + + return ( +
+
+

댓글 관리

+ + 대시보드로 돌아가기 + +
+ + {issuesWithComments.length === 0 ? ( +
+

아직 댓글이 없습니다.

+
+ ) : ( +
+
+

+ 총 {issuesWithComments.length}개의 글에 댓글이 + 달려있습니다. +

+
+ + {issuesWithComments.map(({ issue, comments }) => ( + + ))} +
+ )} +
+ ); +}; + +export default AdminCommentsPage; diff --git a/app/api/admin/comments/route.ts b/app/api/admin/comments/route.ts new file mode 100644 index 0000000..4179c7b --- /dev/null +++ b/app/api/admin/comments/route.ts @@ -0,0 +1,171 @@ +// GET /api/admin/comments - 관리자용 GitHub Issues 댓글 조회 +import { getServerSession } from 'next-auth'; + +export const dynamic = 'force-dynamic'; + +interface GitHubIssue { + id: number; + number: number; + title: string; + html_url: string; + state: string; + comments: number; + created_at: string; + updated_at: string; + user: { + login: string; + avatar_url: string; + }; + body?: string; + pull_request?: { + url: string; + }; +} + +interface GitHubComment { + id: number; + user: { + login: string; + avatar_url: string; + }; + created_at: string; + updated_at: string; + body: string; + html_url: string; +} + +export async function GET() { + try { + // 인증 확인 + const session = await getServerSession(); + if (!session) { + return Response.json( + { success: false, error: 'Unauthorized' }, + { status: 401 } + ); + } + + const GITHUB_REPO = 'ShipFriend0516/TechBlog'; + const GITHUB_TOKEN = process.env.GITHUB_TOKEN; + + if (!GITHUB_TOKEN) { + console.error( + 'GitHub token 이 설정되지 않았습니다. .env 파일을 확인하세요.' + ); + return Response.json( + { + success: false, + error: 'GitHub token 이 설정되지 않았습니다. 관리자에게 문의하세요.', + }, + { status: 500 } + ); + } + + // GitHub Issues API로 모든 이슈 가져오기 (utterances가 생성한 이슈들) + const issuesResponse = await fetch( + `https://api.github.com/repos/${GITHUB_REPO}/issues?state=all&per_page=100`, + { + headers: { + Authorization: `Bearer ${GITHUB_TOKEN}`, + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + }, + } + ); + + if (!issuesResponse.ok) { + const errorText = await issuesResponse.text(); + console.error('GitHub API error:', issuesResponse.status, errorText); + throw new Error( + `Failed to fetch issues from GitHub: ${issuesResponse.status} ${errorText}` + ); + } + + const allIssues: GitHubIssue[] = await issuesResponse.json(); + + if (!Array.isArray(allIssues)) { + console.error('Invalid response from GitHub API:', allIssues); + throw new Error('Invalid response from GitHub API'); + } + + // Pull Request를 제외하고 실제 Issues만 필터링 + const issues = allIssues.filter((issue) => !issue.pull_request); + + // 각 이슈에 대한 댓글 가져오기 + const issuesWithComments = await Promise.all( + issues.map(async (issue) => { + if (!issue || issue.comments === 0) { + return { + issue, + comments: [], + }; + } + + try { + const commentsResponse = await fetch( + `https://api.github.com/repos/${GITHUB_REPO}/issues/${issue.number}/comments`, + { + headers: { + Authorization: `Bearer ${GITHUB_TOKEN}`, + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + }, + } + ); + + if (!commentsResponse.ok) { + console.error( + `Failed to fetch comments for issue ${issue.number}:`, + commentsResponse.status + ); + return { + issue, + comments: [], + }; + } + + const comments: GitHubComment[] = await commentsResponse.json(); + + return { + issue, + comments: Array.isArray(comments) ? comments : [], + }; + } catch (error) { + console.error( + `Error fetching comments for issue ${issue.number}:`, + error + ); + return { + issue, + comments: [], + }; + } + }) + ); + + // 댓글이 있는 이슈만 필터링 + const issuesWithCommentsOnly = issuesWithComments.filter( + (item) => item.comments.length > 0 + ); + + return Response.json( + { + success: true, + data: issuesWithCommentsOnly, + total: issuesWithCommentsOnly.length, + }, + { + status: 200, + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0', + }, + } + ); + } catch (error) { + console.error('Error fetching comments:', error); + return Response.json( + { success: false, error: '댓글 불러오기 실패', detail: error }, + { status: 500 } + ); + } +} diff --git a/app/entities/admin/comments/CommentItem.tsx b/app/entities/admin/comments/CommentItem.tsx new file mode 100644 index 0000000..114a71f --- /dev/null +++ b/app/entities/admin/comments/CommentItem.tsx @@ -0,0 +1,56 @@ +import { formatDate } from '@/app/lib/utils/format'; + +interface GitHubUser { + login: string; + avatar_url: string; +} + +interface GitHubComment { + id: number; + user: GitHubUser; + created_at: string; + updated_at: string; + body: string; + html_url: string; +} + +interface CommentItemProps { + comment: GitHubComment; +} + +const CommentItem = ({ comment }: CommentItemProps) => { + return ( +
+
+ {comment.user.login} +
+
+ + {comment.user.login} + + + {formatDate(new Date(comment.created_at).getTime())} + +
+
+ {comment.body} +
+ + GitHub에서 보기 → + +
+
+
+ ); +}; + +export default CommentItem; diff --git a/app/entities/admin/comments/IssueCard.tsx b/app/entities/admin/comments/IssueCard.tsx new file mode 100644 index 0000000..1ad535b --- /dev/null +++ b/app/entities/admin/comments/IssueCard.tsx @@ -0,0 +1,168 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import CommentItem from './CommentItem'; + +interface GitHubUser { + login: string; + avatar_url: string; +} + +interface GitHubComment { + id: number; + user: GitHubUser; + created_at: string; + updated_at: string; + body: string; + html_url: string; +} + +interface GitHubIssue { + id: number; + number: number; + title: string; + html_url: string; + state: string; + comments: number; + created_at: string; + updated_at: string; + user: GitHubUser; + body?: string; +} + +interface IssueCardProps { + issue: GitHubIssue; + comments: GitHubComment[]; +} + +// 이슈 제목에서 slug 추출 +const extractSlugFromTitle = (title: string): string => { + try { + let titleString = title; + if (title.startsWith('posts/')) { + titleString = title.slice(6).trim(); + } + // 마지막 슬래시 제거 + if (titleString.endsWith('/')) { + titleString = titleString.slice(0, -1); + } + return decodeURIComponent(titleString); + } catch { + return title; + } +}; + +// 이슈 제목을 읽기 쉬운 형태로 변환 +const extractPostTitle = (title: string): string => { + try { + let titleString = title; + if (title.startsWith('posts/')) { + titleString = title.slice(6).trim(); + } + + const decodedSlug = decodeURIComponent(titleString); + + const readableTitle = decodedSlug + .split(/[-_]/) + .map((word) => { + if (!word) return word; + return word.charAt(0).toUpperCase() + word.slice(1); + }) + .join(' '); + + return readableTitle || decodedSlug || title; + } catch { + return title; + } +}; + +const IssueCard = ({ issue, comments }: IssueCardProps) => { + const [isExpanded, setIsExpanded] = useState(false); + const postTitle = extractPostTitle(issue.title); + const slug = extractSlugFromTitle(issue.title); + + return ( +
+
setIsExpanded(!isExpanded)} + > +
+
+

+ {postTitle} +

+
+ + + + + {comments.length}개의 댓글 + + e.stopPropagation()} + > + 글 보러가기 → + + e.stopPropagation()} + > + GitHub에서 보기 → + +
+
+ +
+
+ + {isExpanded && ( +
+
+ {comments.map((comment) => ( + + ))} +
+
+ )} +
+ ); +}; + +export default IssueCard; From 7663c32ec30dabf6368cd0c2ff68584fc4013a16 Mon Sep 17 00:00:00 2001 From: sjw4371 Date: Tue, 14 Oct 2025 14:26:17 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=B5=9C=EC=8B=A0=20=EA=B8=80=EA=B3=BC,?= =?UTF-8?q?=20=EA=B8=80=20=EB=B0=9C=ED=96=89=EB=9F=89=20=ED=86=B5=EA=B3=84?= =?UTF-8?q?=20=EB=B3=BC=20=EC=88=98=20=EC=9E=88=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/page.tsx | 10 +-- app/api/admin/posts/recent/route.ts | 44 +++++++++ app/api/admin/stats/route.ts | 53 +++++++++++ app/entities/admin/dashboard/QuickStats.tsx | 89 ++++++++++++++++++ .../admin/dashboard/RecentActivity.tsx | 90 +++++++++++++++++++ 5 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 app/api/admin/posts/recent/route.ts create mode 100644 app/api/admin/stats/route.ts create mode 100644 app/entities/admin/dashboard/QuickStats.tsx create mode 100644 app/entities/admin/dashboard/RecentActivity.tsx diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 487ce41..9ede45c 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -13,6 +13,8 @@ import BubbleBackground from '@/app/entities/common/Background/BubbleBackground' import { useEffect } from 'react'; import useToast from '@/app/hooks/useToast'; import { FaBuffer } from 'react-icons/fa6'; +import RecentActivity from '@/app/entities/admin/dashboard/RecentActivity'; +import QuickStats from '@/app/entities/admin/dashboard/QuickStats'; const AdminDashboard = () => { const { data: session } = useSession(); @@ -126,12 +128,8 @@ const AdminDashboard = () => {
-
-

최근 활동

-
-
-

빠른 통계

-
+ +
); diff --git a/app/api/admin/posts/recent/route.ts b/app/api/admin/posts/recent/route.ts new file mode 100644 index 0000000..8eb4d7d --- /dev/null +++ b/app/api/admin/posts/recent/route.ts @@ -0,0 +1,44 @@ +// GET /api/admin/posts/recent - 관리자용 최근 게시글 조회 +import Post from '@/app/models/Post'; +import dbConnect from '@/app/lib/dbConnect'; +import { getServerSession } from 'next-auth'; + +export const dynamic = 'force-dynamic'; + +export async function GET(req: Request) { + try { + const session = await getServerSession(); + if (!session) { + return Response.json( + { success: false, error: 'Unauthorized' }, + { status: 401 } + ); + } + + await dbConnect(); + + const recentPosts = await Post.find({}) + .select('title slug date createdAt') + .sort({ date: -1 }) + .limit(3); + + return Response.json( + { + success: true, + posts: recentPosts, + }, + { + status: 200, + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0', + }, + } + ); + } catch (error) { + console.error('Error fetching recent posts:', error); + return Response.json( + { success: false, error: '최근 게시글 불러오기 실패', detail: error }, + { status: 500 } + ); + } +} diff --git a/app/api/admin/stats/route.ts b/app/api/admin/stats/route.ts new file mode 100644 index 0000000..c64035e --- /dev/null +++ b/app/api/admin/stats/route.ts @@ -0,0 +1,53 @@ +// GET /api/admin/stats - 관리자용 블로그 통계 +import Post from '@/app/models/Post'; +import Series from '@/app/models/Series'; +import dbConnect from '@/app/lib/dbConnect'; +import { getServerSession } from 'next-auth'; + +export const dynamic = 'force-dynamic'; + +export async function GET(req: Request) { + try { + const session = await getServerSession(); + if (!session) { + return Response.json( + { success: false, error: 'Unauthorized' }, + { status: 401 } + ); + } + + await dbConnect(); + + const [totalPosts, totalSeries, publicPosts, privatePosts] = + await Promise.all([ + Post.countDocuments({}), + Series.countDocuments({}), + Post.countDocuments({ isPrivate: false }), + Post.countDocuments({ isPrivate: true }), + ]); + + return Response.json( + { + success: true, + stats: { + totalPosts, + totalSeries, + publicPosts, + privatePosts, + }, + }, + { + status: 200, + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0', + }, + } + ); + } catch (error) { + console.error('Error fetching stats:', error); + return Response.json( + { success: false, error: '통계 불러오기 실패', detail: error }, + { status: 500 } + ); + } +} diff --git a/app/entities/admin/dashboard/QuickStats.tsx b/app/entities/admin/dashboard/QuickStats.tsx new file mode 100644 index 0000000..1b45c36 --- /dev/null +++ b/app/entities/admin/dashboard/QuickStats.tsx @@ -0,0 +1,89 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +interface Stats { + totalPosts: number; + totalSeries: number; + publicPosts: number; + privatePosts: number; +} + +const QuickStats = () => { + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchStats = async () => { + try { + setLoading(true); + const response = await fetch('/api/admin/stats'); + const data = await response.json(); + + if (data.success) { + setStats(data.stats); + } else { + setError(data.error || '통계를 불러올 수 없습니다.'); + } + } catch (err) { + setError('통계를 불러오는 중 오류가 발생했습니다.'); + console.error(err); + } finally { + setLoading(false); + } + }; + + fetchStats(); + }, []); + + if (loading) { + return ( +
+

빠른 통계

+
로딩 중...
+
+ ); + } + + if (error || !stats) { + return ( +
+

빠른 통계

+
{error || '통계를 불러올 수 없습니다.'}
+
+ ); + } + + return ( +
+

빠른 통계

+
+
+

전체 게시글

+

{stats.totalPosts}

+
+
+

전체 시리즈

+

+ {stats.totalSeries} +

+
+
+

공개 게시글

+

+ {stats.publicPosts} +

+
+
+

비공개 게시글

+

+ {stats.privatePosts} +

+
+
+
+ ); +}; + +export default QuickStats; diff --git a/app/entities/admin/dashboard/RecentActivity.tsx b/app/entities/admin/dashboard/RecentActivity.tsx new file mode 100644 index 0000000..b692bec --- /dev/null +++ b/app/entities/admin/dashboard/RecentActivity.tsx @@ -0,0 +1,90 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Link from 'next/link'; +import { formatDate } from '@/app/lib/utils/format'; + +interface RecentPost { + _id: string; + title: string; + slug: string; + date: number; + createdAt: string; +} + +const RecentActivity = () => { + const [posts, setPosts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchRecentPosts = async () => { + try { + setLoading(true); + const response = await fetch('/api/admin/posts/recent'); + const data = await response.json(); + + if (data.success) { + setPosts(data.posts); + } else { + setError(data.error || '최근 게시글을 불러올 수 없습니다.'); + } + } catch (err) { + setError('최근 게시글을 불러오는 중 오류가 발생했습니다.'); + console.error(err); + } finally { + setLoading(false); + } + }; + + fetchRecentPosts(); + }, []); + + if (loading) { + return ( +
+

최근 활동

+
로딩 중...
+
+ ); + } + + if (error) { + return ( +
+

최근 활동

+
{error}
+
+ ); + } + + return ( +
+

최근 활동

+ {posts.length === 0 ? ( +

최근 게시글이 없습니다.

+ ) : ( +
    + {posts.map((post) => ( +
  • + + {post.title} + + + {formatDate(post.date)} + +
  • + ))} +
+ )} +
+ ); +}; + +export default RecentActivity; From 953acc4696e4ba0e1f04d3415da711c81155bd5a Mon Sep 17 00:00:00 2001 From: sjw4371 Date: Tue, 14 Oct 2025 15:23:19 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20decryptedText=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/page.tsx | 19 ++- app/entities/bits/DecryptedText.tsx | 241 ++++++++++++++++++++++++++++ package.json | 1 + pnpm-lock.yaml | 60 +++++++ 4 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 app/entities/bits/DecryptedText.tsx diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 9ede45c..2c2054f 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -15,6 +15,7 @@ import useToast from '@/app/hooks/useToast'; import { FaBuffer } from 'react-icons/fa6'; import RecentActivity from '@/app/entities/admin/dashboard/RecentActivity'; import QuickStats from '@/app/entities/admin/dashboard/QuickStats'; +import DecryptedText from '../entities/bits/DecryptedText'; const AdminDashboard = () => { const { data: session } = useSession(); @@ -97,8 +98,22 @@ const AdminDashboard = () => {
-

관리자 대시보드

-

{session.user?.name}님, 환영합니다

+

+ +

+

+ +