> 웹 프론트엔드 > JS 튜토리얼 > Next.js로 동적 블로그 대시보드 구축

Next.js로 동적 블로그 대시보드 구축

Barbara Streisand
풀어 주다: 2024-12-08 17:04:10
원래의
636명이 탐색했습니다.

소개

안녕하세요? 프로그래밍 기술을 향상시키는 데 도움이 되는 새로운 프로젝트로 돌아온 Vítor입니다. 마지막으로 튜토리얼을 게시한 지 꽤 시간이 지났습니다. 지난 몇 달 동안 저는 휴식을 취하고 다른 활동에 집중하는 시간을 가졌습니다. 이 기간 동안 저는 이 튜토리얼의 초점이 된 블로그라는 작은 웹 프로젝트를 개발했습니다.

이 가이드에서는 마크다운을 렌더링할 수 있는 블로그 페이지의 프런트엔드를 만들어 보겠습니다. 애플리케이션에는 공개 및 비공개 경로, 사용자 인증, 마크다운 텍스트 작성, 사진 추가, 기사 표시 등의 기능이 포함됩니다.

원하는 대로 애플리케이션을 자유롭게 맞춤설정하세요. 권장합니다.

여기에서 이 애플리케이션의 저장소에 액세스할 수 있습니다.

Building a Dynamic Blog Dashboard with Next.js 곤드락08 / 블로그 플랫폼

Next.js/typescript로 만든 블로그 플랫폼입니다.

Plataforma 파라 블로그

  • 텍스트로 튜토리얼

성분

  • next-auth - Next.js에 대한 인증 도서
  • github.com/markdown-it/markdown-it - markdown biblioteca.
  • github.com/sindresorhus/github-markdown-css- Para dar estilo ao nosso editor markdown.
  • github.com/remarkjs/react-markdown - 반응 구성 요소인 마크다운 렌더링을 위한 도서관.
  • github.com/remarkjs/remark-react/tree/4722bdf - Markdown em React를 변환하는 플러그인.
  • codemirror.net - 웹용 편집기 구성 요소
  • 반응 아이콘 - 반응에 따른 아이콘 라이브러리입니다.

코모 우사르

npm i
npm run start
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

서버

EM 서버에 대한 você pode encontrar o servidor dessa aplicação em server


GitHub에서 보기


이 튜토리얼에는 이 가이드에서 사용될 Node.js 서버 작성도 포함되어 있습니다.

즐기시기 바랍니다.

즐거운 코딩하세요!

도서관

다음은 이 프로젝트에 사용된 라이브러리에 대한 요약입니다.

  • next-auth - Next.js용 인증 라이브러리
  • github.com/markdown-it/markdown-it - 마크다운 라이브러리.
  • github.com/sindresorhus/github-markdown-css - Markdown 편집기 스타일을 지정합니다.
  • github.com/remarkjs/react-markdown - React 구성 요소에서 Markdown을 렌더링하기 위한 라이브러리입니다.
  • github.com/remarkjs/remark-react/tree/4722bdf - Markdown을 React로 변환하는 플러그인.
  • codemirror.net - 웹 구성 요소 편집기.
  • React-icons - React용 아이콘 라이브러리

반응 프로젝트 만들기

이 튜토리얼을 작성할 당시 버전 13.4인 Next.js 프레임워크의 최신 버전을 사용하겠습니다.

다음 명령을 실행하여 프로젝트를 생성하세요.

npm i
npm run start
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

설치하는 동안 템플릿 설정을 선택하세요. 이 튜토리얼에서는 TypeScript를 프로그래밍 언어로 사용하고 Tailwind CSS 프레임워크를 사용하여 애플리케이션 스타일을 지정하겠습니다.

구성

이제 사용할 라이브러리를 모두 설치해 보겠습니다.

가격 인하
npx create-next-app myblog
로그인 후 복사
로그인 후 복사
로그인 후 복사
반응 비고
npm i  markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
로그인 후 복사
로그인 후 복사
로그인 후 복사
코드미러
remark remark-gfm remark-react
로그인 후 복사
로그인 후 복사
로그인 후 복사
아이콘
npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
로그인 후 복사
로그인 후 복사
로그인 후 복사

그런 다음 사용하지 않을 모든 것을 제거하여 설치의 초기 구조를 정리하세요.

건축학

이것이 우리 애플리케이션의 최종 구조입니다.

npm i react-icons @types/react-icons
로그인 후 복사
로그인 후 복사
로그인 후 복사

첫 번째 단계

next.config 구성

프로젝트 루트의 next.config.js 파일에서 기사의 이미지에 액세스할 도메인 주소를 구성해 보겠습니다. 이 튜토리얼에서는 또는 로컬 서버를 사용하는 경우 localhost를 사용합니다.

애플리케이션에 이미지가 올바르게 로드되도록 하려면 이 구성을 포함해야 합니다.

src-
  |- app/
  |    |-(pages)/
  |    |      |- (private)/
  |    |      |       |- (home)
  |    |      |       |- editArticle/[id]
  |    |      |       |
  |    |      |       |- newArticle
  |    |      | - (public)/
  |    |              | - article/[id]
  |    |              | - login
  |    |
  |   api/
  |    |- auth/[...nextAuth]/route.ts
  |    |- global.css
  |    |- layout.tsx
  |
  | - components/
  | - context/
  | - interfaces/
  | - lib/
  | - services/
middleware.ts
로그인 후 복사
로그인 후 복사
로그인 후 복사

미들웨어 구성

애플리케이션 src/의 루트 폴더에 middleware.ts를 생성하여 비공개 경로에 대한 액세스를 확인하세요.

const nextConfig = {
   images: {
    domains: ["localhost"],
  },
};
로그인 후 복사
로그인 후 복사

미들웨어와 미들웨어로 수행할 수 있는 모든 작업에 대해 자세히 알아보려면 설명서를 확인하세요.

인증 경로 구성

/app 폴더 내 api/auth/[...nextauth]에 Route.ts라는 파일을 생성합니다. 여기에는 CredentialsProvider를 사용하여 인증 API에 연결하는 경로 구성이 포함됩니다.

CredentialsProvider를 사용하면 사용자 이름과 비밀번호, 도메인, 2단계 인증, 하드웨어 장치 등과 같은 임의의 자격 증명으로 로그인을 처리할 수 있습니다.

먼저 프로젝트 루트에 .env.local 파일을 생성하고 비밀으로 사용될 토큰을 추가합니다.

npm i
npm run start
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

다음으로 NEXTAUTH_SECRET이 src/app/auth/[...nextauth]/routes.ts 파일의 비밀에 추가되는 인증 시스템을 작성해 보겠습니다.

npx create-next-app myblog
로그인 후 복사
로그인 후 복사
로그인 후 복사

인증 제공자

비공개 경로의 페이지 전체에서 사용자 데이터를 공유할 인증 공급자인 컨텍스트를 만들어 보겠습니다. 나중에 이를 레이아웃.tsx 파일 중 하나를 래핑하는 데 사용할 것입니다.

src/context/auth-provider.tsx에 다음 내용이 포함된 파일을 만듭니다.

npm i  markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
로그인 후 복사
로그인 후 복사
로그인 후 복사

글로벌 스타일

전체적으로 우리 애플리케이션에서는 Tailwind CSS를 사용하여 스타일을 만듭니다. 그러나 일부 장소에서는 페이지와 구성 요소 간에 사용자 정의 CSS 클래스를 공유합니다.

remark remark-gfm remark-react
로그인 후 복사
로그인 후 복사
로그인 후 복사

레이아웃

이제 비공개 및 공개 레이아웃을 작성해 보겠습니다.

앱/layout.tsx

npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
로그인 후 복사
로그인 후 복사
로그인 후 복사

페이지/layout.tsx

npm i react-icons @types/react-icons
로그인 후 복사
로그인 후 복사
로그인 후 복사

API 호출

우리 애플리케이션은 API를 여러 번 호출하며, 외부 API를 사용하도록 이 애플리케이션을 조정할 수 있습니다. 이 예에서는 로컬 애플리케이션을 사용하고 있습니다. 백엔드 튜토리얼과 서버 생성을 아직 못 보신 분들은 꼭 확인해보세요.

src/services/에 다음 함수를 작성해 보겠습니다.

  1. authService.ts: 서버에서 사용자를 인증하는 기능입니다.
src-
  |- app/
  |    |-(pages)/
  |    |      |- (private)/
  |    |      |       |- (home)
  |    |      |       |- editArticle/[id]
  |    |      |       |
  |    |      |       |- newArticle
  |    |      | - (public)/
  |    |              | - article/[id]
  |    |              | - login
  |    |
  |   api/
  |    |- auth/[...nextAuth]/route.ts
  |    |- global.css
  |    |- layout.tsx
  |
  | - components/
  | - context/
  | - interfaces/
  | - lib/
  | - services/
middleware.ts
로그인 후 복사
로그인 후 복사
로그인 후 복사

2.refreshAccessToken.tsx:

const nextConfig = {
   images: {
    domains: ["localhost"],
  },
};
로그인 후 복사
로그인 후 복사
  1. getArticles.tsx: 데이터베이스에 저장된 모든 기사를 가져오는 역할을 담당하는 함수:
export { default } from "next-auth/middleware";
export const config = {
  matcher: ["/", "/newArticle/", "/article/", "/article/:path*"],
};
로그인 후 복사
  1. postArticle.tsx: 기사 데이터를 서버에 제출하는 기능을 담당합니다.
.env.local
NEXTAUTH_SECRET = SubsTituaPorToken
로그인 후 복사
  1. editArticle.tsx: 데이터베이스 내의 특정 기사를 수정하는 기능입니다.
import NextAuth from "next-auth/next";
import type { AuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { authenticate } from "@/services/authService";
import refreshAccessToken from "@/services/refreshAccessToken";

export const authOptions: AuthOptions = {
  providers: [
    CredentialsProvider({
      name: "credentials",
      credentials: {
        email: {
          name: "email",
          label: "email",
          type: "email",
          placeholder: "Email",
        },
        password: {
          name: "password",
          label: "password",
          type: "password",
          placeholder: "Password",
        },
      },
      async authorize(credentials, req) {
        if (typeof credentials !== "undefined") {
          const res = await authenticate({
            email: credentials.email,
            password: credentials.password,
          });
          if (typeof res !== "undefined") {
            return { ...res };
          } else {
            return null;
          }
        } else {
          return null;
        }
      },
    }),
  ],

  session: { strategy: "jwt" },
  secret: process.env.NEXTAUTH_SECRET,
  callbacks: {
    async jwt({ token, user, account }: any) {
      if (user && account) {
        return {
          token: user?.token,
          accessTokenExpires: Date.now() + parseInt(user?.expiresIn, 10),
          refreshToken: user?.tokenRefresh,
        };
      }

      if (Date.now() < token.accessTokenExpires) {
        return token;
      } else {
        const refreshedToken = await refreshAccessToken(token.refreshToken);
        return {
          ...token,
          token: refreshedToken.token,
          refreshToken: refreshedToken.tokenRefresh,
          accessTokenExpires:
            Date.now() + parseInt(refreshedToken.expiresIn, 10),
        };
      }
    },
    async session({ session, token }) {
      session.user = token;
      return session;
    },
  },

  pages: {
    signIn: "/login",
    signOut: "/login",
  },
};

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
로그인 후 복사
  1. deleteArticle.tsx: 데이터베이스에서 특정 기사를 제거하는 기능입니다.
'use client';
import React from 'react';
import { SessionProvider } from "next-auth/react";
export default function Provider({
    children,
    session
}: {
    children: React.ReactNode,
    session: any
}): React.ReactNode {
    return (
        <SessionProvider session={session} >
            {children}
        </SessionProvider>
    )
};
로그인 후 복사

구성요소

다음으로 애플리케이션 전반에 걸쳐 사용되는 각 구성 요소를 작성해 보겠습니다.

구성 요소/Navbar.tsx

두 개의 탐색 링크가 있는 간단한 구성 요소입니다.

/*global.css*/
.container {
  max-width: 1100px;
  width: 100%;
  margin: 0px auto;
}

.image-container {
  position: relative;
  width: 100%;
  height: 5em;
  padding-top: 56.25%; /* Aspect ratio 16:9 (dividindo a altura pela largura) */
}

.image-container img {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@keyframes spinner {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.loading-spinner {
  width: 50px;
  height: 50px;
  border: 10px solid #f3f3f3;
  border-top: 10px solid #293d71;
  border-radius: 50%;
  animation: spinner 1.5s linear infinite;
}
로그인 후 복사

구성 요소/Loading.tsx

API 호출이 완료되기를 기다리는 동안 사용되는 간단한 로딩 구성 요소입니다.

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Provider from "@/context/auth-provider";
import { getServerSession } from "next-auth";
import { authOptions } from "./api/auth/[...nextauth]/route";
const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Markdown Text Editor",
  description: "Created by <@vitorAlecrim>",
};

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await getServerSession(authOptions);
  return (
    <Provider session={session}>
      <html lang="en">
        <body className={inter.className}>{children}</body>
      </html>
    </Provider>
  );
}
로그인 후 복사

구성 요소/페이지 매김.tsx

비공개 경로에서 모든 기사를 표시하는 페이지에 사용되는 페이지 매기기 구성 요소입니다. 이 구성 요소를 작성하는 방법에 대한 자세한 내용은 여기에서 확인할 수 있습니다

npm i
npm run start
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

구성 요소/기사Card.tsx

작성된 글을 표시하는 카드 컴포넌트입니다.

이 구성 요소에는 기사 표시 페이지와 이전에 작성된 기사를 편집할 수 있는 페이지로 연결되는 링크도 포함되어 있습니다.

npx create-next-app myblog
로그인 후 복사
로그인 후 복사
로그인 후 복사

구성 요소/ArticleList.tsx

API 호출 및 응답 표시를 담당하는 구성 요소입니다.

여기서는 우리가 작성한 함수를 통해 두 개의 API 호출을 사용합니다.

  1. getArticles.ts - 구성 요소에 표시될 모든 기사를 반환합니다.
  2. RemoveArticle - 목록과 서버에서 특정 기사를 제거합니다.

이전에 작성한 Pagination.tsx 구성 요소를 사용하여 페이지 전체에 기사 수를 분할하겠습니다.

npm i  markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
로그인 후 복사
로그인 후 복사
로그인 후 복사

페이지

다음으로 각 페이지를 해당 경로별로 나누어 살펴보겠습니다.

공개 페이지

로그인

저희 애플리케이션 홈페이지입니다. 간단한 페이지이므로 원하는 대로 수정할 수 있습니다. 본 페이지에서는 next-auth 네비게이션 라이브러리에서 제공하는 로그인 기능을 활용하겠습니다.

src/app/pages/public/login/page.tsx 파일에 있습니다.

remark remark-gfm remark-react
로그인 후 복사
로그인 후 복사
로그인 후 복사

기사 페이지

기사 읽기 페이지를 만들기 위해 동적인 페이지를 개발하겠습니다.

방문한 모든 블로그 플랫폼에는 URL을 통해 액세스할 수 있는 기사 읽기 전용 페이지가 있을 것입니다. 그 이유는 동적 페이지 경로 때문입니다. 다행히 Next.js는 새로운 AppRouter 메소드를 통해 이를 쉽게 수행하여 우리의 삶을 훨씬 더 단순하게 만들어줍니다.

먼저 [id] 폴더를 추가하여 구조에 경로를 생성해야 합니다. 결과적으로 페이지/(공개)/articles/[id]/pages.tsx.

구조가 됩니다.
  • ID는 우리 네비게이션 경로의 슬러그에 해당합니다.
  • params는 탐색 슬러그를 포함하는 애플리케이션의 트리를 통해 전달되는 속성입니다.
npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
로그인 후 복사
로그인 후 복사
로그인 후 복사

두 번째: MarkdownIt 라이브러리를 사용하여 페이지에서 Markdown 형식으로 텍스트를 표시할 수 있습니다.

npm i react-icons @types/react-icons
로그인 후 복사
로그인 후 복사
로그인 후 복사

그리고 마지막으로

페이지가 준비되면 예를 들어 브라우저에서 localhost:3000/articles/1에 액세스하면 제공된 ID로 기사를 볼 수 있습니다.

우리의 경우 ArticleCards.tsx 구성 요소 중 하나를 클릭하면 탐색을 통해 ID가 전달되며, 이는 비공개 경로의 기본 페이지에 렌더링됩니다.

src-
  |- app/
  |    |-(pages)/
  |    |      |- (private)/
  |    |      |       |- (home)
  |    |      |       |- editArticle/[id]
  |    |      |       |
  |    |      |       |- newArticle
  |    |      | - (public)/
  |    |              | - article/[id]
  |    |              | - login
  |    |
  |   api/
  |    |- auth/[...nextAuth]/route.ts
  |    |- global.css
  |    |- layout.tsx
  |
  | - components/
  | - context/
  | - interfaces/
  | - lib/
  | - services/
middleware.ts
로그인 후 복사
로그인 후 복사
로그인 후 복사

비공개 페이지

다음은 애플리케이션에서 사용자가 인증된 후에만 액세스할 수 있는 비공개 페이지입니다.

app/pages/ 폴더 내에서 () 안에 파일이 선언되어 있다는 것은 해당 경로가 /에 해당한다는 의미입니다.

저희 경우 (홈) 폴더는 저희 프라이빗 루트의 홈페이지를 의미합니다. 사용자가 시스템에 인증할 때 보게 되는 첫 번째 페이지입니다. 이 페이지에는 당사 데이터베이스의 기사 목록이 표시됩니다.

데이터는 ArticlesList.tsx 구성 요소에 의해 처리됩니다. 아직 이 코드를 작성하지 않았다면 구성 요소 섹션을 다시 참조하세요.

앱 내/(페이지)/(비공개)/(홈)/page.tsx.

npm i
npm run start
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

새 기사

이 페이지는 기사를 등록할 수 있는 신청서의 가장 중요한 페이지 중 하나입니다.

이 페이지를 통해 사용자는 다음을 수행할 수 있습니다.

  1. 마크다운 형식으로 기사를 작성하세요.
  2. 기사에 이미지를 할당하세요.
  3. Markdown 텍스트를 서버에 제출하기 전에 미리 봅니다.

이 페이지는 여러 후크를 사용합니다.

  1. useCallback - 기능을 메모하는 데 사용됩니다.
  2. useState - 구성 요소에 상태 변수를 추가할 수 있습니다.
  3. useSession - 사용자가 인증되었는지 확인하고 인증 토큰을 얻을 수 있습니다.

이를 위해 두 가지 구성 요소를 사용합니다.

  1. TextEditor.tsx: 이전에 작성한 텍스트 편집기입니다.
  2. Preview.tsx: 마크다운 형식으로 파일을 표시하기 위한 구성 요소입니다.

이 페이지를 구성하는 동안 다음 API를 사용합니다.

  1. POST: postArticle 기능을 사용하여 기사를 서버로 보냅니다.

또한 next-auth 라이브러리에서 제공하는 useSession 후크를 사용하여 서버에 기사를 등록하는 데 사용될 사용자 인증 토큰을 얻습니다.

여기에는 세 가지 개별 API 호출이 포함됩니다.
앱/페이지/(비공개)/newArticle/page.tsx.

"클라이언트 사용";
import React, { ChangeEvent, useCallback, useState } from "react";
"next-auth/react"에서 import { useSession };
"다음/탐색"에서 가져오기 { 리디렉션 };
"@/services/postArticle"에서 postArtical을 가져옵니다.
"react-icons/ai"에서 { AiOutlineFolderOpen }을 가져옵니다.
"react-icons/ri"에서 { RiImageEditLine }을 가져옵니다.

"다음/이미지"에서 이미지를 가져옵니다.
"@/comComponents/textEditor"에서 TextEditor를 가져옵니다.
"@/comComponents/PreviewText"에서 미리보기를 가져옵니다.
"react-icons/ai"에서 { AiOutlineSend }를 가져옵니다.
"react-icons/bs"에서 { BsBodyText }를 가져옵니다.

기본 함수 내보내기 NewArticle(params:any) {
  const { 데이터: 세션 }: any = useSession({
    필수: 사실,
    인증되지 않은() {
      리다이렉트("/로그인");
    },
  });
  const [imageUrl, setImageUrl] = useState<object>({});
  const [previewImage, setPreviewImage] = useState<string>("");
  const [previewText, setPreviewText] = useState(false);
  const [title, setTitle] = useState<string>("");
  const [doc, setDoc] = useState<string>("# Escreva o seu texto...n");
  const handlerDocChange = useCallback((newDoc: any) => {
    setDoc(newDoc);
  }, []);

  if (!session?.user)는 null을 반환합니다.

  const handlerArticleSubmit = async (e:any) => {
        e.preventDefault();
    const 토큰: 문자열 = session.user.token;
    노력하다 {
      const res = postArtical({
        ID: session.user.userId.toString(),
        토큰: 토큰,
        imageUrl: imageUrl,
        제목: "제목,"
        문서: 문서,
      });
      console.log('re--->', res);
      리디렉션('/성공');
    } 잡기(오류) {
      console.error('기사 제출 오류:', error);
      // 필요한 경우 오류를 처리합니다.
      오류 발생;
    }
  };

  const handlerImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files && e.target.files.length > 0) {
      const 파일 = e.target.files[0];
      const url = URL.createObjectURL(파일);
      setPreviewImage(url);
      setImageUrl(파일);
    }
  };

  const handlerTextPreview = (e: 임의) => {
    e.preventDefault();
    setPreviewText(!previewText);
  };
  반품 (
    <section className="w-full h-full min-h-screen 상대 py-8">
      {미리보기텍스트 &&(
        <div className="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100 rounded-xl w-full max-w-[33em] z-30">
          <미리보기
            문서={문서}
            제목={제목}
            PreviewImage={previewImage}
            onPreview={() => setPreviewText(!previewText)}
          />
        </div>
      )}

      <form className="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-slate-50 drop-shadow-xl 플렉스 flex-col gap-2 ">
        {" "}
        <div className="flex justify-between items-center">
          <버튼
            className="border-b-2 rounded-md border-slate-500 p-2 플렉스 항목-센터 gap-2 hover:border-slate-400 hover:text-slate-800"
            onClick={handleTextPreview}
          >
            <BsBodyText />
            시사
          </버튼>{" "}
          <버튼
            className="그룹 테두리 border-b-2 border-slate-500 rounded-md p-2 flex items-center gap-2 hover:border-slate-400 hover:text-slate-800 "
            onClick={handleArticleSubmit}
          >
            엔비아르 텍스토
            <AiOutlineSend className="w-5 h-5 group-hover:text-red-500" />
          </버튼>
        </div>
        <div className="header-wrapper flex flex-col gap-2 ">
          <div className="image-box">
            {previewImage.length === 0 && (
              <div className="select-image">
                <라벨
                  htmlFor="이미지"
                  className="p-4 border-dashed border-4 border-slate-400 커서-포인터 플렉스 플렉스-col 항목-센터 정당화-센터"
                >
                  <AiOutlineFolderOpen className="w-7 h-7" />
                  드래그 앤 드롭 이미지
                </라벨>
                <입력
                 >



<h4>
  
  
  기사 편집
</h4>

<p><em>New Article</em>(newArticle)과 유사하지만 약간의 차이점이 있는 페이지입니다.</p>

<p>먼저 ID를 탐색 매개변수로 받는 동적 경로를 정의합니다. 이는 기사 읽기 페이지에서 수행된 작업과 매우 유사합니다. <br>
app/(페이지)/(private)/editArticle/[id]/page.tsx<br>
</p><pre class="brush:php;toolbar:false">"클라이언트 사용";
"반응"에서 React, { useState, useEffect, useCallback, useRef, ChangeEvent }를 가져옵니다.
"next-auth/react"에서 import { useSession };
"다음/탐색"에서 가져오기 { 리디렉션 };
'다음/이미지'에서 이미지를 가져옵니다.

"@/interfaces/article.interface"에서 { IArticle }을 가져옵니다.
"react-icons/ai"에서 { AiOutlineEdit }를 가져옵니다.
"react-icons/bs"에서 { BsBodyText }를 가져옵니다.
"react-icons/ai"에서 { AiOutlineFolderOpen }을 가져옵니다.
"react-icons/ri"에서 { RiImageEditLine }을 가져옵니다.

"@/comComponents/PreviewText"에서 미리보기를 가져옵니다.
"@/comComponents/textEditor"에서 TextEditor를 가져옵니다.
'@/comComponents/Loading'에서 로드 가져오기;
"@/services/editArticle"에서 editArtical을 가져옵니다.

기본 함수 내보내기 EditArticle({ params }: { params: any }) {
 const { 데이터: 세션 }: any = useSession({
    필수: 사실,
    인증되지 않은() {
      리다이렉트("/로그인");
    },
  });
  const id: 숫자 = params.id;
  const [기사, setArticle] = useState<IArticle | 널>(널);
  const [imageUrl, setImageUrl] = useState<object>({});
  const [previewImage, setPreviewImage] = useState<string>("");
  const [previewText, setPreviewText] = useState(false)
  const [title, setTitle] = useState<string>("");
  const [doc, setDoc] = useState<string>('');
  const handlerDocChange = useCallback((newDoc: any) => {
    setDoc(newDoc);
  }, []);
  const inputRef= useRef<HTMLInputElement>(null);

  const fetchArticle = async (id: 숫자) => {
    노력하다 {
      const 응답 = 가져오기를 기다립니다(
        `http://localhost:8080/articles/getById/${id}`,
      );
      const jsonData = 응답을 기다립니다.json();
      setArticle(jsonData);
    } 잡기 (오류) {
      console.log("뭔가 잘못되었습니다:", err);
    }
  };
  useEffect(() => {
    if (기사 !== null || 기사 !== 정의되지 않음) {
      fetchArticle(id);
    }
  }, [ID]);

  useEffect(()=>{
    if(기사 != null && 기사.콘텐츠){
        setDoc(article.content)
    }

    if(기사 !=null && 기사.이미지){
      setPreviewImage(`http://localhost:8080/` 기사.이미지)
    }
  },[기사])

  const handlerArticleSubmit = async (e:any) => {
     e.preventDefault();
    const 토큰: 문자열 = session.user.token;
    노력하다{
      const res = editArtical({
      아이디: 아이디,
      토큰: 토큰,
      imageUrl:imageUrl,
      제목: 제목,
      문서: 문서,
      });
        console.log('re--->',res)
        해상도를 반환;
    } 잡기(오류){
    console.log("오류:", 오류)
    }
  };
  const handlerImageClick = ()=>{
      console.log('hiii')
    if(inputRef.current){
      inputRef.current.click();
    }
  }const handlerImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files && e.target.files.length > 0) {
      const 파일 = e.target.files[0];
      const url = URL.createObjectURL(파일);
      setPreviewImage(url);
      setImageUrl(파일);
    }

  };
   const handlerTextPreview = (e: 임의) => {
    e.preventDefault();
    setPreviewText(!previewText);
    console.log('미리보기에서 안녕하세요!')
  };

  if(!article) return <Loading/>
  if(기사?.내용)
  반품 (
    <section className='w-full h-full min-h-screen 상대 py-8'>
      {미리보기텍스트 &&(
        <div className="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100 rounded-xl w-full max-w-[33em] z-30">
          <미리보기
            문서={문서}
            제목={제목}
            PreviewImage={previewImage}
            onPreview={() => setPreviewText(!previewText)}
          />
        </div>
      )}

      <div className='relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-white drop- 섀도우-MD 플렉스 플렉스-콜 갭-2'>
        <form className='relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-slate-50 drop-shadow-md flex flex-col gap-2 '>
          {" "}
          <div className='flex justify-between items-center'>
            <버튼
              className='국경-b-2 반올림-md 국경-슬레이트-500 p-2 플렉스 항목-중심 간격-2 hover:border-slate-400 hover:text-slate-800'
              onClick={handleTextPreview}
            >
              <BsBodyText />
              시사
            </버튼>{" "}
            <버튼
              className='그룹 테두리 border-b-2 border-slate-500 rounded-md p-2 flex items-center gap-2 hover:border-slate-400 hover:text-slate-800 '
              onClick={handleArticleSubmit}
            >
                아티고 편집 
              <AiOutlineEdit className='w-5 h-5 group-hover:text-red-500' />
            </버튼>
          </div>
          <div className='header-wrapper flex flex-col gap-2 '>
            <div className='image-box'>
              {previewImage.length === 0 && (
                <div className='select-image'>
                  <라벨
                    htmlFor='이미지'
                    className='p-4 border-dashed border-4 border-slate-400 커서-포인터 flex flex-col 항목-센터 정당화-센터'
                  >
                    <AiOutlineFolderOpen className='w-7 h-7' />
                    드래그 앤 드롭 이미지
                  </라벨>
                  <입력
                   >



<h2>
  
  
  결론
</h2>

<p>먼저 시간을 내어 이 튜토리얼을 읽어주셔서 감사드리며, 튜토리얼을 완료하신 것도 축하드립니다. 도움이 되셨기를 바라며, 단계별 지침을 따라하기 쉬웠기를 바랍니다.</p>

<p>두 번째로, 방금 구축한 기능에 대해 몇 가지 사항을 강조하고 싶습니다. 이는 블로그 시스템의 기초이며, 모든 글을 표시하는 공개 페이지, 사용자 등록 페이지, 사용자 정의 404 오류 페이지 등 추가할 내용이 여전히 많습니다. 튜토리얼을 진행하는 동안 이러한 페이지에 대해 궁금해서 놓친 경우 이는 의도된 것임을 알아두세요. 이 튜토리얼에서는 이러한 새 페이지를 직접 만들고, 다른 페이지를 추가하고, 새로운 기능을 구현하는 데 충분한 경험을 제공했습니다.</p>

<p>정말 감사합니다. 다음 시간까지. ㅇ/</p>


          

            
        
로그인 후 복사

위 내용은 Next.js로 동적 블로그 대시보드 구축의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:dev.to
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
저자별 최신 기사
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿