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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,14 @@ TechBlog/
```text
1 GITHUB_ID=your_github_client_id
2 GITHUB_SECRET=your_github_client_secret
3 ADMIN_EMAIL=your_admin_email@example.com
3 ADMIN_GITHUB_ID=your_admin_github_numeric_id
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
```

`ADMIN_GITHUB_ID`는 이메일이 아닌 GitHub 계정의 숫자 id다.
`https://api.github.com/users/{GitHub 아이디}` 응답의 `id` 값을 그대로 넣으면 된다.
6 changes: 2 additions & 4 deletions app/api/admin/analytics/daily-posts/route.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
// GET /api/admin/analytics/daily-posts?date=YYYY-MM-DD
import { NextRequest } from 'next/server';
import { getServerSession } from 'next-auth';
import { isAdminSession } from '@/app/lib/authz';
import { getAdminSession } from '@/app/lib/authz';
import dbConnect from '@/app/lib/dbConnect';
import View from '@/app/models/View';

export const dynamic = 'force-dynamic';

export async function GET(request: NextRequest) {
try {
const session = await getServerSession();
// 관리자 전용
if (!isAdminSession(session)) {
if (!(await getAdminSession())) {
return Response.json({ success: false, error: 'Unauthorized' }, { status: 401 });
}

Expand Down
6 changes: 2 additions & 4 deletions app/api/admin/analytics/popular/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// GET /api/admin/analytics/popular?type=all|today
import { NextRequest } from 'next/server';
import { getServerSession } from 'next-auth';
import { isAdminSession } from '@/app/lib/authz';
import { getAdminSession } from '@/app/lib/authz';
import dbConnect from '@/app/lib/dbConnect';
import View from '@/app/models/View';

Expand Down Expand Up @@ -47,9 +46,8 @@ const commonProject = {

export async function GET(request: NextRequest) {
try {
const session = await getServerSession();
// 관리자 전용
if (!isAdminSession(session)) {
if (!(await getAdminSession())) {
return Response.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
Expand Down
6 changes: 2 additions & 4 deletions app/api/admin/analytics/referrers/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// GET /api/admin/analytics/referrers?postId=xxx
import { NextRequest } from 'next/server';
import { getServerSession } from 'next-auth';
import { isAdminSession } from '@/app/lib/authz';
import { getAdminSession } from '@/app/lib/authz';
import dbConnect from '@/app/lib/dbConnect';
import View from '@/app/models/View';

Expand All @@ -19,9 +18,8 @@ function normalizeReferrer(ref: string): string {

export async function GET(request: NextRequest) {
try {
const session = await getServerSession();
// 관리자 전용
if (!isAdminSession(session)) {
if (!(await getAdminSession())) {
return Response.json({ success: false, error: 'Unauthorized' }, { status: 401 });
}

Expand Down
6 changes: 2 additions & 4 deletions app/api/admin/comments/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// GET /api/admin/comments - 관리자용 GitHub Issues 댓글 조회
import { getServerSession } from 'next-auth';
import { isAdminSession } from '@/app/lib/authz';
import { getAdminSession } from '@/app/lib/authz';

export const dynamic = 'force-dynamic';

Expand Down Expand Up @@ -38,8 +37,7 @@ interface GitHubComment {
export async function GET() {
try {
// 관리자 전용
const session = await getServerSession();
if (!isAdminSession(session)) {
if (!(await getAdminSession())) {
return Response.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
Expand Down
6 changes: 2 additions & 4 deletions app/api/admin/posts/recent/route.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
// GET /api/admin/posts/recent - 관리자용 최근 게시글 조회
import { getServerSession } from 'next-auth';
import { isAdminSession } from '@/app/lib/authz';
import { getAdminSession } from '@/app/lib/authz';
import dbConnect from '@/app/lib/dbConnect';
import Post from '@/app/models/Post';

export const dynamic = 'force-dynamic';

export async function GET() {
try {
const session = await getServerSession();
// 관리자 전용
if (!isAdminSession(session)) {
if (!(await getAdminSession())) {
return Response.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
Expand Down
6 changes: 2 additions & 4 deletions app/api/admin/settings/llms/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { getServerSession } from 'next-auth';
import { isAdminSession } from '@/app/lib/authz';
import { getAdminSession } from '@/app/lib/authz';
import dbConnect from '@/app/lib/dbConnect';
import { generateLlmsTxt } from '@/app/lib/llmstxt';
import Post from '@/app/models/Post';
import View from '@/app/models/View';

export async function POST() {
const session = await getServerSession();
// 관리자 전용
if (!isAdminSession(session)) {
if (!(await getAdminSession())) {
return Response.json({ success: false, error: 'Unauthorized' }, { status: 401 });
}

Expand Down
6 changes: 2 additions & 4 deletions app/api/admin/stats/daily/route.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
// GET /api/admin/stats/daily - 최근 14일간 일별 조회수
import { getServerSession } from 'next-auth';
import { isAdminSession } from '@/app/lib/authz';
import { getAdminSession } from '@/app/lib/authz';
import dbConnect from '@/app/lib/dbConnect';
import View from '@/app/models/View';

export const dynamic = 'force-dynamic';

export async function GET() {
try {
const session = await getServerSession();
// 관리자 전용
if (!isAdminSession(session)) {
if (!(await getAdminSession())) {
return Response.json({ success: false, error: 'Unauthorized' }, { status: 401 });
}

Expand Down Expand Up @@ -39,7 +37,7 @@
for (let i = 13; i >= 0; i--) {
const d = new Date(now);
d.setDate(d.getDate() - i);
const dateStr = d.toLocaleDateString('ko-KR', {

Check warning on line 40 in app/api/admin/stats/daily/route.ts

View workflow job for this annotation

GitHub Actions / Continuous Integration

'dateStr' is assigned a value but never used
timeZone: 'Asia/Seoul',
year: 'numeric',
month: '2-digit',
Expand Down
6 changes: 2 additions & 4 deletions app/api/admin/stats/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// GET /api/admin/stats - 관리자용 블로그 통계
import { getServerSession } from 'next-auth';
import { isAdminSession } from '@/app/lib/authz';
import { getAdminSession } from '@/app/lib/authz';
import dbConnect from '@/app/lib/dbConnect';
import Post from '@/app/models/Post';
import Series from '@/app/models/Series';
Expand All @@ -10,9 +9,8 @@ export const dynamic = 'force-dynamic';

export async function GET() {
try {
const session = await getServerSession();
// 관리자 전용
if (!isAdminSession(session)) {
if (!(await getAdminSession())) {
return Response.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
Expand Down
6 changes: 2 additions & 4 deletions app/api/admin/subscribers/route.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { getServerSession } from 'next-auth';
import { isAdminSession } from '@/app/lib/authz';
import { getAdminSession } from '@/app/lib/authz';
import dbConnect from '@/app/lib/dbConnect';
import Subscriber from '@/app/models/Subscriber';

export const dynamic = 'force-dynamic';

export async function GET() {
try {
const session = await getServerSession();
// 관리자 전용
if (!isAdminSession(session)) {
if (!(await getAdminSession())) {
return Response.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
Expand Down
6 changes: 2 additions & 4 deletions app/api/atelier/block/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// POST /api/atelier/block - 관리자 전용 사용자 차단 (fingerprint 또는 GitHub ID)
import { getServerSession } from 'next-auth';
import { isAdminSession } from '@/app/lib/authz';
import { getAdminSession } from '@/app/lib/authz';
import dbConnect from '@/app/lib/dbConnect';
import BlockedFingerprint from '@/app/models/BlockedFingerprint';

Expand All @@ -9,8 +8,7 @@ export const POST = async (request: Request) => {
await dbConnect();

// 관리자 전용
const session = await getServerSession();
if (!isAdminSession(session)) {
if (!(await getAdminSession())) {
return Response.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
Expand Down
18 changes: 7 additions & 11 deletions app/api/atelier/messages/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// DELETE /api/atelier/messages/[id] - 관리자 또는 소유자 소프트 삭제
// PATCH /api/atelier/messages/[id] - 관리자 전용 isPublic 토글
// PUT /api/atelier/messages/[id] - 소유자 또는 관리자 메시지 수정
import { getServerSession } from 'next-auth';
import { serializeAtelierMessage, LeanAtelierMessage } from '@/app/lib/atelierSerialize';
import { isAdminSession } from '@/app/lib/authz';
import { getAdminSession, getSession, isAdminSession } from '@/app/lib/authz';
import dbConnect from '@/app/lib/dbConnect';
import AtelierMessage from '@/app/models/AtelierMessage';

Expand All @@ -16,7 +15,7 @@ export const DELETE = async (request: Request, props: RouteParams) => {
try {
await dbConnect();

const session = await getServerSession();
const session = await getSession();
const isAdmin = isAdminSession(session);

const { id } = params;
Expand All @@ -37,7 +36,7 @@ export const DELETE = async (request: Request, props: RouteParams) => {

// 소유권 확인: admin OR 소유자
let isOwner = false;
const sessionGithubId = (session?.user as { id?: string })?.id;
const sessionGithubId = session?.user?.id;
if (sessionGithubId) {
// 로그인 사용자: githubId로 비교
isOwner = message.author?.githubId === sessionGithubId;
Expand Down Expand Up @@ -85,8 +84,7 @@ export const PATCH = async (request: Request, props: RouteParams) => {
await dbConnect();

// 관리자 전용
const session = await getServerSession();
if (!isAdminSession(session)) {
if (!(await getAdminSession())) {
return Response.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
Expand Down Expand Up @@ -146,7 +144,7 @@ export const PUT = async (request: Request, props: RouteParams) => {
try {
await dbConnect();

const session = await getServerSession();
const session = await getSession();
const isAdmin = isAdminSession(session);

const { id } = params;
Expand Down Expand Up @@ -197,7 +195,7 @@ export const PUT = async (request: Request, props: RouteParams) => {

// 소유권 확인: admin OR 소유자
let isOwner = false;
const sessionGithubId = (session?.user as { id?: string })?.id;
const sessionGithubId = session?.user?.id;
if (sessionGithubId) {
// 로그인 사용자: githubId로 비교
isOwner = message.author?.githubId === sessionGithubId;
Expand Down Expand Up @@ -230,9 +228,7 @@ export const PUT = async (request: Request, props: RouteParams) => {
}

const fingerprint = request.headers.get('X-Fingerprint');
const githubId =
(session?.user as { id?: string })?.id || null;
const serialized = serializeAtelierMessage(updated, fingerprint, githubId);
const serialized = serializeAtelierMessage(updated, fingerprint, sessionGithubId || null);

return Response.json(
{ success: true, message: serialized },
Expand Down
8 changes: 3 additions & 5 deletions app/api/atelier/messages/[id]/thread/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// GET /api/atelier/messages/[id]/thread - 스레드 답글 전체 조회
import { getServerSession } from 'next-auth';
import {
LeanAtelierMessage,
serializeAtelierMessage,
} from '@/app/lib/atelierSerialize';
import { isAdminSession } from '@/app/lib/authz';
import { getSession, isAdminSession } from '@/app/lib/authz';
import dbConnect from '@/app/lib/dbConnect';
import AtelierMessage from '@/app/models/AtelierMessage';

Expand All @@ -25,11 +24,10 @@ export const GET = async (request: Request, props: RouteParams) => {
);
}

const session = await getServerSession();
const session = await getSession();
const isAdmin = isAdminSession(session);
const viewerFingerprint = request.headers.get('X-Fingerprint') || null;
const viewerGithubId =
(session?.user as { id?: string })?.id || null;
const viewerGithubId = session?.user?.id || null;

// 쿼리 구성
const query: Record<string, unknown> = {
Expand Down
16 changes: 7 additions & 9 deletions app/api/atelier/messages/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
// GET /api/atelier/messages - 커서 기반 역방향 무한 스크롤
// POST /api/atelier/messages - 메시지 전송 (관리자 / 방문자 자동 분기)
import { NextRequest } from 'next/server';
import { getServerSession } from 'next-auth';
import { decodeCursor, encodeCursor } from '@/app/lib/atelierCursor';
import { parseEffect } from '@/app/lib/atelierEffects';
import {
LeanAtelierMessage,
serializeAtelierMessage,
} from '@/app/lib/atelierSerialize';
import { isAdminSession } from '@/app/lib/authz';
import { getSession, isAdminSession } from '@/app/lib/authz';
import dbConnect from '@/app/lib/dbConnect';
import { checkRateLimit } from '@/app/lib/rateLimit';
import AtelierMessage from '@/app/models/AtelierMessage';
Expand All @@ -21,7 +20,7 @@ export const GET = async (request: NextRequest) => {
try {
await dbConnect();

const session = await getServerSession();
const session = await getSession();
const isAdmin = isAdminSession(session);

const cursorParam = request.nextUrl.searchParams.get('cursor');
Expand All @@ -47,8 +46,7 @@ export const GET = async (request: NextRequest) => {
}

const viewerFingerprint = request.headers.get('X-Fingerprint') || null;
const viewerGithubId =
(session?.user as { id?: string })?.id || null;
const viewerGithubId = session?.user?.id || null;

// hasMore 판정을 위해 limit + 1 조회
const docs = (await AtelierMessage.find(query)
Expand Down Expand Up @@ -99,7 +97,7 @@ export const POST = async (request: Request) => {
await dbConnect();

const fingerprint = request.headers.get('X-Fingerprint') || '';
const session = await getServerSession();
const session = await getSession();
const isAdmin = isAdminSession(session);

const body = (await request.json()) as unknown;
Expand Down Expand Up @@ -196,16 +194,16 @@ export const POST = async (request: Request) => {
};
} else if (session?.user) {
// GitHub 로그인 방문자
const githubLogin = (session.user as { githubLogin?: string })
.githubLogin;
role = 'visitor';
author = {
nickname:
session.user.name ||
(typeof nickname === 'string' && nickname.trim()) ||
'익명',
avatarUrl: session.user.image || undefined,
githubId: githubLogin,
// DELETE/PUT의 소유권 확인은 session.user.id(GitHub 숫자 id)와 비교하므로
// githubLogin(계정명)이 아닌 id를 저장해야 한다.
githubId: session.user.id,
fingerprint,
};
} else {
Expand Down
Loading
Loading