Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,46 @@
# 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/)
- **File Storage**: [Vercel Blob](https://vercel.com/docs/storage/vercel-blob)
- **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/)

Expand All @@ -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
```
138 changes: 138 additions & 0 deletions app/admin/comments/page.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(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 (
<div className="p-6 max-w-7xl mx-auto">
<h1 className="text-3xl font-bold mb-6">댓글 관리</h1>
<div className="text-center py-10">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<p className="mt-4 text-gray-600">댓글을 불러오는 중...</p>
</div>
</div>
);
}

if (error) {
return (
<div className="p-6 max-w-7xl mx-auto">
<h1 className="text-3xl font-bold mb-6">댓글 관리</h1>
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{error}
</div>
</div>
);
}

return (
<div className="p-6 max-w-7xl mx-auto">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">댓글 관리</h1>
<Link
href="/admin"
className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-all"
>
대시보드로 돌아가기
</Link>
</div>

{issuesWithComments.length === 0 ? (
<div className="bg-white rounded-lg shadow-md p-8 text-center">
<p className="text-gray-500 text-lg">아직 댓글이 없습니다.</p>
</div>
) : (
<div className="space-y-4">
<div className="bg-blue-50 border border-blue-200 px-4 py-3 rounded-lg mb-4">
<p className="text-blue-800">
총 <strong>{issuesWithComments.length}</strong>개의 글에 댓글이
달려있습니다.
</p>
</div>

{issuesWithComments.map(({ issue, comments }) => (
<IssueCard key={issue.id} issue={issue} comments={comments} />
))}
</div>
)}
</div>
);
};

export default AdminCommentsPage;
29 changes: 21 additions & 8 deletions app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ 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';
import DecryptedText from '../entities/bits/DecryptedText';

const AdminDashboard = () => {
const { data: session } = useSession();
Expand Down Expand Up @@ -95,8 +98,22 @@ const AdminDashboard = () => {
<div className="p-6 max-w-7xl mx-auto">
<header className="mb-8 flex justify-between">
<div>
<h1 className="text-3xl font-bold mb-2">관리자 대시보드</h1>
<p className=" text-default">{session.user?.name}님, 환영합니다</p>
<h1 className="text-3xl font-bold mb-2">
<DecryptedText
text="관리자 대시보드"
speed={60}
revealDirection="start"
animateOn="view"
/>
</h1>
<p className=" text-default">
<DecryptedText
text={`${session.user?.name}님, 환영합니다`}
speed={120}
revealDirection="start"
animateOn="view"
/>
</p>
</div>
<button
className="right-0 px-4 py-1 bg-red-500 text-white rounded-md shadow-md hover:bg-red-700 transition-all"
Expand Down Expand Up @@ -126,12 +143,8 @@ const AdminDashboard = () => {
</div>

<div className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-6 text-black">
<div className="bg-white p-6 rounded-lg shadow-md">
<h3 className="text-xl font-semibold mb-4">최근 활동</h3>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h3 className="text-xl font-semibold mb-4">빠른 통계</h3>
</div>
<RecentActivity />
<QuickStats />
</div>
</div>
);
Expand Down
Loading