React

 

Next.js를 활용한 AI 기반 풀스택 애플리케이션: GPT Genius

 

이번 프로젝트에서는 GPT Genius라는 AI 기반 풀스택 애플리케이션을 구축하며 Next.js의 다양한 기능을 학습합니다.

프로젝트를 통해 다음과 같은 기능과 개념을 배울 수 있습니다.

 

프로젝트 주요 기능

1. 인증(Authentication) 구현

  • Google 계정 또는 이메일 주소를 사용해 회원가입/로그인 기능을 구현.
  • 인증이 필요한 페이지(대시보드)는 로그인 없이 접근 불가.

 

2. 대시보드와 테마 토글

  • 로그인 후 대시보드로 이동하며, 좌측 사이드바와 메인 콘텐츠 영역으로 구성.
  • 테마 토글 기능: 사용자가 다크 모드 및 라이트 모드 간 전환 가능.
  • 하단에는 로그아웃계정 관리 옵션이 포함됨.

 

3. React Query를 활용한 데이터 캐싱

  • 데이터를 효율적으로 불러오고 캐싱하여 빠른 응답 제공.

 

4. OpenAI API 활용

  • OpenAI API를 사용해 도시 정보와 관광지 목록을 생성.

 

 

주요 페이지 및 기능 설명

1. 홈 페이지

  • 간단한 소개 페이지로 "Get Started" 버튼 클릭 시 대시보드로 이동.

 

2. 채팅 페이지(Chat Page)

  • ChatGPT 클론 기능 구현.
  • 사용자가 도시 이름을 검색하면 해당 도시와 관련된 정보를 표시.
    • 예: 'P'를 입력하면 "포르투(Porto), 포르투갈"과 같은 결과를 즉시 확인 가능.

 

3. 새로운 투어 생성 페이지(New Tours)

  • 사용자가 도시명국가명을 입력하면:
    • 데이터베이스에 있는 도시: 기존 데이터를 즉시 반환.
    • 데이터베이스에 없는 도시: OpenAI API를 통해 정보를 생성한 뒤 데이터베이스에 저장.
  • 생성된 도시는 "Tours" 목록에 추가되어 재사용 가능.

 

4. 프로필 페이지(Profile Page)

  • 사용자 계정 관리 페이지.
  • 기본적으로 계정 정보를 확인하고 관리할 수 있는 기능 제공.

 

5. 투어 리스트 페이지(Tours Page)

  • 생성된 도시 투어를 카드 형식으로 표시.
  • 특정 도시 카드를 클릭하면 세부 정보 및 이미지를 확인할 수 있는 상세 페이지로 이동.

 

주요 학습 포인트

  1. Next.js 인증 구현:

    • Google OAuth 또는 이메일 인증 방식.
  2. React Query 사용법:

    • 데이터를 캐싱하고 빠르게 응답을 제공하는 방식 학습.
  3. OpenAI API 통합:

    • 외부 API를 활용하여 동적인 데이터를 생성하고 데이터베이스에 저장.
  4. 다크/라이트 테마 토글:

    • 사용자 경험을 강화하는 테마 전환 기능 구현.
  5. Full Stack 개발:

    • 백엔드와 프론트엔드가 통합된 애플리케이션 구축.

 

프로젝트 요약

GPT Genius는 사용자 인증, 데이터 검색, API 통합, 테마 전환 등 다양한 기능을 포함한 AI 기반 Next.js 풀스택 프로젝트입니다.

사용자가 도시를 검색하거나 새로운 도시를 입력하면 OpenAI API와 React Query를 활용해 빠르고 직관적인 응답을 제공합니다.

 

 

 

소스 :

https://github.dev/braverokmc79/GeniusGPT-next-gpt-genius

 

 

 

 

 

 

 

 

1.프로젝트 생성

>>>>npx create-next-app@latest gptgenius
Need to install the following packages:
create-next-app@15.1.5
Ok to proceed? (y) y

npm notice Beginning October 4, 2021, all connections to the npm registry - including for package installation - must use TLS 1.2 or higher. You are currently using plaintext http to connect. Please visit the GitHub blog for more information: https://github.blog/2021-08-23-npm-registry-deprecating-tls-1-0-tls-1-1/
npm notice Beginning October 4, 2021, all connections to the npm registry - including for package installation - must use TLS 1.2 or higher. You are currently using plaintext http to connect. Please visit the GitHub blog for more information: https://github.blog/2021-08-23-npm-registry-deprecating-tls-1-0-tls-1-1/
√ Would you like to use TypeScript? ... No / Yes
√ Would you like to use ESLint? ... No / Yes
√ Would you like to use Tailwind CSS? ... No / Yes
√ Would you like your code inside a `src/` directory? ... No / Yes
√ Would you like to use App Router? (recommended) ... No / Yes
√ Would you like to use Turbopack for `next dev`? ... No / Yes
√ Would you like to customize the import alias (`@/*` by default)? ... No / Yes
√ What import alias would you like configured? ... @/*
Creating a new Next.js app in >>>>gptgenius.

Using npm.

Initializing project with template: app-tw


Installing dependencies:
- react
- react-dom
- next

Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom
- postcss
- tailwindcss
- eslint
- eslint-config-next
- @eslint/eslintrc

npm notice Beginning October 4, 2021, all connections to the npm registry - including for package installation - must use TLS 1.2 or higher. You are currently using plaintext http to connect. Please visit the GitHub blog for more information: https://github.blog/2021-08-23-npm-registry-deprecating-tls-1-0-tls-1-1/
npm notice Beginning October 4, 2021, all connections to the npm registry - including for package installation - must use TLS 1.2 or higher. You are currently using plaintext http to connect. Please visit the GitHub blog for more information: https://github.blog/2021-08-23-npm-registry-deprecating-tls-1-0-tls-1-1/

added 373 packages in 51s

141 packages are looking for funding
  run `npm fund` for details
Initialized a git repository.

Success! Created gptgenius >>>>gptgenius


 

추가해야 할 라이브러리

1. dependencies (프로덕션 의존성)

 

1) @clerk/nextjs: 클럭 기반의 인증 및 사용자 관리 라이브러리

npm install @clerk/nextjs

npm install @clerk/localizations

 

 

2) @prisma/client: Prisma ORM의 클라이언트 라이브러리

npm install @prisma/client

 

3)@tanstack/react-query: 서버 상태 관리를 위한 React Query 라이브러리.

npm install @tanstack/react-query


 

4)@tanstack/react-query-devtools: React Query 개발 도구 (React Query 디버깅용).

npm install @tanstack/react-query-devtools

 

5)axios: HTTP 클라이언트 라이브러리

npm install axios
 

 

6)openai: OpenAI API 클라이언트 라이브러리

npm install openai

 

7)react-hot-toast: 알림 UI 라이브러리.

npm install react-hot-toast

 

8)react-icons: 아이콘 라이브러리

npm install react-icons

 

2. devDependencies (개발 의존성)

1) @tailwindcss/typography: TailwindCSS의 타이포그래피 플러그인.

npm install -D @tailwindcss/typography

 

2)autoprefixer: CSS 공급업체 접두사를 자동으로 추가해주는 도구

npm install -D autoprefixer

 

3)daisyui: TailwindCSS를 기반으로 한 UI 컴포넌트 라이브러리

npm install -D daisyui

 

4)prisma: Prisma ORM CLI 도구

npm install -D prisma
 

최종 라이브러리 설치 명령어

# Install new dependencies
npm install @clerk/nextjs @clerk/localizations @prisma/client @tanstack/react-query @tanstack/react-query-devtools axios openai react-hot-toast react-icons

# Install new devDependencies
npm install -D @tailwindcss/typography autoprefixer daisyui prisma


 

 

tailwind.config.ts 설정

import type { Config } from "tailwindcss";
import typography from '@tailwindcss/typography';
import daisyui from 'daisyui';

export default {
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      colors: {
        background: "var(--background)",
        foreground: "var(--foreground)",
      },
    },
  },
  plugins: [
    typography,
    daisyui,
  ],
} satisfies Config;

 

 

 

 

 

 

2.Next.js 15 프로젝트: 대시보드 라우트 그룹 구성 및 페이지 생성

 

요구 사항

  1. 페이지 생성: 다음 세 가지 페이지를 생성합니다.

    • Chat Page (채팅 페이지)
    • Profile Page (프로필 페이지)
    • Tours Page (투어 페이지)
  2. 라우트 그룹:

    • 모든 대시보드 페이지를 dashboard 그룹에 포함합니다.
    • URL 경로에는 **/dashboard**가 나타나지 않도록 설정합니다.
      예) /chat, /profile, /tours처럼 보이게 구성.
  3. 공통 레이아웃:

    • 대시보드 내 모든 페이지에 적용할 공통 layout.tsx 파일을 생성하고, children을 렌더링합니다.

 

구현 코드

1. 폴더 및 파일 구조

src/
└── app/
    ├── (dashboard)/
    │   ├── layout.tsx         # 대시보드 공통 레이아웃
    │   ├── chat/
    │   │   └── page.tsx       # 채팅 페이지
    │   ├── profile/
    │   │   └── page.tsx       # 프로필 페이지
    │   └── tours/
    │       └── page.tsx       # 투어 페이지
    └── globals.css            # Tailwind CSS 및 전역 스타일

 

 

2. layout.tsx (대시보드 공통 레이아웃)

const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <div>
      {/* 공통 레이아웃 요소를 여기 추가할 수 있음 */}
      {children}
    </div>
  );
};

export default DashboardLayout;

 

 

3. Chat Page (채팅 페이지) (chat/page.tsx)

import React from 'react';

const ChatPage: React.FC = () => {
  return (
    <div>
      <h1>Chat Page</h1>
      <p>이곳은 채팅 페이지입니다.</p>
    </div>
  );
};

export default ChatPage;

 

 

 

4. Profile Page (프로필 페이지) (profile/page.tsx)

import React from 'react';

const ProfilePage: React.FC = () => {
  return (
    <div>
      <h1>Profile Page</h1>
      <p>이곳은 프로필 페이지입니다.</p>
    </div>
  );
};

export default ProfilePage;

 

 

5. Tours Page (투어 페이지) (tours/page.tsx)

import React from 'react';

const ToursPage: React.FC = () => {
  return (
    <div>
      <h1>Tours Page</h1>
      <p>이곳은 투어 페이지입니다.</p>
    </div>
  );
};

export default ToursPage;

 

결과

  1. 라우트 URL

    • /chat: 채팅 페이지
    • /profile: 프로필 페이지
    • /tours: 투어 페이지
  2. 라우트 그룹

    • (dashboard) 폴더를 사용해 URL 경로에는 표시되지 않지만 내부적으로 그룹화된 구조를 유지.
  3. 공통 레이아웃

    • layout.tsx를 통해 대시보드 내 페이지에 공통 디자인 요소를 쉽게 추가 가능.

 

 

 

 

 

 

 

3.Next.js 15 프로젝트: 홈 메인 화면 구현 및 메타데이터 추가

 

이번 단계에서는 **홈 페이지(HomePage)**를 구현하고, DaisyUI의 Hero 컴포넌트를 활용하여 페이지를 시각적으로 개선합니다.

또한, Next.js의 메타데이터 설정을 활용하여 프로젝트의 제목과 설명을 추가합니다.

 

 

구현 목표

  1. 홈 페이지 디자인

    • DaisyUI의 Hero 컴포넌트를 사용하여 직관적이고 깔끔한 디자인 적용.
    • 프로젝트 제목, 설명, 그리고 "시작하기" 버튼 추가.
    • 버튼 클릭 시 Chat 페이지로 이동.
  2. 메타데이터 설정

    • Next.js App Router의 layout.tsx 파일에서 프로젝트의 제목과 설명 설정.
  3. DaisyUI와 Tailwind CSS 활용

    • DaisyUI의 기본 스타일 및 Tailwind 유틸리티 클래스를 사용하여 테마 기반 디자인 구현.

 

 

1. 홈 페이지 구성

코드: HomePage 컴포넌트 (src/app/page.tsx)

import Link from 'next/link';
import React from 'react';

const HomePage: React.FC = () => {
  return (
    <div className="hero min-h-screen bg-base-200">
      <div className="hero-content text-center">
        <div className="max-w-md">
          <h1 className="text-6xl font-bold text-primary">GPTGenius</h1>
          <p className="py-6 text-lg leading-loose">
            GPTGenius: GPTGenius는 당신의 AI 언어 동반자입니다. <br />
            OpenAI 기술로 구동되며, 대화, 콘텐츠 제작 등을 한층 더 풍부하게 만들어 드립니다!
          </p>
          <Link href="/chat" className="btn btn-secondary">
            시작하기
          </Link>
        </div>
      </div>
    </div>
  );
};

export default HomePage;

 

 

설명

  • 배경 색상: bg-base-200 (DaisyUI의 기본 배경 색상 사용).
  • 텍스트 스타일:
    • 제목: text-6xl, font-bold, text-primary (테마에 따라 다른 기본 색상 적용).
    • 본문: text-lg, leading-loose로 가독성을 높임.
  • "시작하기" 버튼:
    • Link 컴포넌트를 사용해 /chat 페이지로 연결.
    • DaisyUI의 버튼 스타일 btn btn-secondary 사용.

 

 

2. 메타데이터 설정

코드: layout.tsx 파일 (src/app/layout.tsx)

export const metadata = {
  title: 'GPTGenius - AI 언어 동반자',
  description: 'GPTGenius는 OpenAI 기술로 구동되는 AI 기반 언어 동반자 앱입니다. 대화, 콘텐츠 제작 등 다양한 기능을 제공합니다.',
};

const RootLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <html lang="ko">
      <body>{children}</body>
    </html>
  );
};

export default RootLayout;

 

설명

  • metadata 객체:
    • title: 브라우저 탭에 표시되는 페이지 제목.
    • description: 검색 엔진에서 표시되는 설명.
  • children: 모든 하위 페이지의 렌더링 내용을 포함.

 

3. DaisyUI의 Hero 컴포넌트 활용

DaisyUI는 Tailwind CSS의 확장 라이브러리로, 다음과 같은 유틸리티 클래스를 제공합니다:

  • hero: Hero 섹션을 정의하는 컨테이너 클래스.
  • hero-content: Hero 내부의 콘텐츠를 정렬.
  • text-center: 중앙 정렬된 텍스트.
  • btn btn-secondary: 버튼 스타일 정의.

 

DaisyUI 설치

npm install daisyui

 

Tailwind 설정 업데이트 (tailwind.config.js)

 

import type { Config } from "tailwindcss";
import typography from '@tailwindcss/typography';
import daisyui from 'daisyui';

export default {
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      colors: {
        background: "var(--background)",
        foreground: "var(--foreground)",
      },
    },
  },
  plugins: [
    typography,
    daisyui,
  ],
} satisfies Config;

 

 

 

 

 

 

 

 

4.clerk 사용하기

@clerk/nextjs 설치

1) 다음 명령을 실행하여 SDK를 설치하세요.

npm install @clerk/nextjs

 

 

2 )환경 변수를 설정하세요

이러한 키를 .env.local 에 추가하거나 파일이 없으면 만드세요. 언제든지 API 키 페이지에서 이러한 키를 검색하세요.

.env.local

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=키값
CLERK_SECRET_KEY=키값

 

 

3)middleware.ts 업데이트

미들웨어 파일을 업데이트하거나 프로젝트 루트 또는 src/ 디렉토리 구조를 사용하는 경우 src/ 디렉토리에 파일을 생성하세요.

clerkMiddleware 도우미는 인증을 활성화하고 보호된 경로를 구성하는 곳입니다.

import { clerkMiddleware } from "@clerk/nextjs/server";

export default clerkMiddleware();

export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    // Always run for API routes
    '/(api|trpc)(.*)',
  ],
};

 

 

 

 

4)앱에 ClerkProvider를 추가하세요

ClerkProvider 구성 요소는 Clerk의 후크와 구성 요소에 세션 및 사용자 컨텍스트를 제공합니다. 인증을 전역적으로 액세스할 수 있도록 전체 앱을 진입점에서 ClerkProvider 로 래핑하는 것이 좋습니다 . 다른 구성 옵션은 참조 문서를 참조하세요 .

Clerk의 사전 구축된 구성 요소를 사용하여 로그인한 사용자와 로그아웃한 사용자가 볼 수 있는 콘텐츠를 제어할 수 있습니다.

앱라우터 방식

/src/app/layout.tsx

import {
  ClerkProvider,
  SignInButton,
  SignedIn,
  SignedOut,
  UserButton
} from '@clerk/nextjs'
import './globals.css'
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>
          <SignedOut>
            <SignInButton />
          </SignedOut>
          <SignedIn>
            <UserButton />
          </SignedIn>
          {children}
        </body>
      </html>
    </ClerkProvider>
  )
}

 

페이지앱 방식

/src/pages/_app.tsx

import {
  ClerkProvider,
  SignInButton,
  SignedIn,
  SignedOut,
  UserButton
} from '@clerk/nextjs'
import ''styles/globals.css'' (see below for file content)
return (
  <ClerkProvider>
    <SignedOut>
      <SignInButton />
    </SignedOut>
    <SignedIn>
      <UserButton />
    </SignedIn>
    <Component {...pageProps} />
  </ClerkProvider>
)

 

 

src/app/layout.tsx

import type { Metadata } from "next";
import { Geist,  Noto_Sans_KR } from "next/font/google";
import "./globals.css";
import {
  ClerkProvider,
  SignedIn,
  UserButton
} from '@clerk/nextjs'


const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const notoSansKR = Noto_Sans_KR({
  subsets: ["latin"],
  weight: ["400", "700"], // 필요한 가중치 추가
});



export const metadata: Metadata = {
  title: "GPTGenius",
  description: `GPTGenius: GPTGenius는 당신의 AI 언어 동반자입니다.
    OpenAI 기술로 구동되며, 대화, 콘텐츠 제작 등을 한층 더 풍부하게 만들어 드립니다!`,
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <ClerkProvider>
      <html lang="ko"> 
        <body className={`${geistSans.variable} ${notoSansKR.className} antialiased`}>
          <div className="flex justify-end">
          <SignedIn>
            <UserButton />
          </SignedIn>
          </div>
         {/* <SignedOut>
            <SignInButton />
           </SignedOut>
          <SignedIn>
            <UserButton />
          </SignedIn> */}

          {children}
        </body>
      </html>
    </ClerkProvider>
  );
}

 

 

src/app/page.tsx

import Link from 'next/link'
import React from 'react'
import {SignedOut,  SignInButton,SignedIn} from '@clerk/nextjs'

const HomePage:React.FC = () => {
  return (
    <div className="hero min-h-screen bg-base-200">
        <div className='hero-content text-center'>
            <div className='max-w-md'>
                <h1 className='text-6xl font-bold text-primary'>GPTGenius</h1>
                <p className='py-6 text-lg leading-loose'>
                GPTGenius: GPTGenius는 당신의 AI 언어 동반자입니다.
                OpenAI 기술로 구동되며, 대화, 콘텐츠 제작 등을 한층 더 풍부하게 만들어 드립니다!
                </p>
                 {/* <Link href='/chat' className='btn btn-secondary'>
                    <SignInButton    />
                </Link> 
                 */}
                <SignedOut>
                  <SignInButton>
                    <span  className='btn btn-secondary'>시작하기</span> 
                  </SignInButton>

                </SignedOut>
                
                <SignedIn>                 
                  <Link href='/chat' className='btn btn-primary'>
                      채팅 시작
                  </Link>                   
                </SignedIn>


            </div>
        </div>     
    </div>
  )

}

export default HomePage;

 

 

clerk 한글 설정

https://clerk.com/docs/customization/localization#languages

 

라이브러리 설치

npm install @clerk/localizations

 

import { koKR } from '@clerk/localizations'



export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <ClerkProvider localization={koKR}>

      <html lang="ko"> 
        <body className={`${geistSans.variable} ${notoSansKR.className} antialiased`}>
          <div className="flex justify-end">
          <SignedIn>
            <UserButton />
          </SignedIn>
          </div>
         {/* <SignedOut>
            <SignInButton />
           </SignedOut>
          <SignedIn>
            <UserButton />
          </SignedIn> */}

          {children}
        </body>
      </html>
    </ClerkProvider>
  );
}


 

 

 

 

 

 

 

 

 

 

5.clerk 사용자 정의 인증 페이지로 변경하기 (Custom Auth Pages )

 

참조:

https://clerk.com/docs/references/nextjs/custom-sign-up-page

 

다음과 같은 구조로 디렉토리 및 페이지를 만듭니다.

src/
└── app/
    ├── sign-in /
    │   ├── [[...sign-in]]  /   
    │   │               └── page.tsx  
    │   │
    └── sign-up /          
	├── [[...sign-up]] /

                         └── page.tsx       
   

 

여기서 [[...sign-in]] 형식은 선택적 캐치 올(optional catch-all) 라우트란 다음과 같습니다.

 

1. 기본 캐치 올 라우트 [...param]

  • **[...param]**은 **캐치 올 라우트(catch-all route)**로, 해당 경로 및 하위 모든 경로를 매핑합니다.

예를 들어:

pages/products/[...slug].js
  • /products/a → [slug]: ['a']
  • /products/a/b → [slug]: ['a', 'b']
  • /products → 이 경우 404 오류 발생.

 

 

2. 선택적 캐치 올 라우트 [[...param]]

  • **[[...param]]**은 **선택적 캐치 올 라우트(optional catch-all route)**로, 캐치 올 라우트와 달리 해당 경로가 비어 있는 경우에도 렌더링됩니다.
  • 예를 들어:
app/sign-in/[[...sign-in]]/page.js
    • /sign-in → sign-in은 undefined (경로가 비어 있음)
    • /sign-in/a → sign-in: ['a']
    • /sign-in/a/b → sign-in: ['a', 'b']

 

 

 

 

.env.local

NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/chat
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/chat

 

 

 

app/sign-in/[[...sign-in]]/page.js

import { SignIn } from '@clerk/nextjs';
import React from 'react'

const SignInPage = () => {
  return (
    <div className='min-h-screen  flex justify-center items-center '>
      <SignIn />
    </div>
    
  )
}

export default SignInPage;

 

 

app/sign-up/[[...sign-up]]/page.js

import { SignUp } from '@clerk/nextjs';
import React from 'react'

const SignUpPage = () => {
  return (
    <div className='min-h-screen  flex justify-center items-center '>
      <SignUp />
    </div>
  )
}

export default SignUpPage;

 

 

 

middleware.ts

import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isPublicRoute = createRouteMatcher([
  '/', // 메인 화면은 로그인 없이 접근 가능
  '/sign-in(.*)', // 로그인 페이지
  '/sign-up(.*)', // 회원가입 페이지
]);


export default clerkMiddleware(async (auth, request) => {
  if (!isPublicRoute(request)) {
    await auth.protect()
  }
})

export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    // Always run for API routes
    '/(api|trpc)(.*)',
  ],
}

 

 

src/app/page.tsx

import Link from 'next/link'
import React from 'react'
import {SignedOut,  SignInButton,SignedIn} from '@clerk/nextjs'

const HomePage:React.FC = () => {
  return (
    <div className="hero min-h-screen bg-base-200">
        <div className='hero-content text-center'>
            <div className='max-w-md'>
                <h1 className='text-6xl font-bold text-primary'>GPTGenius</h1>
                <p className='py-6 text-lg leading-loose'>
                GPTGenius: GPTGenius는 당신의 AI 언어 동반자입니다.
                OpenAI 기술로 구동되며, 대화, 콘텐츠 제작 등을 한층 더 풍부하게 만들어 드립니다!
                </p>
                 {/* <Link href='/chat' className='btn btn-secondary'>
                    <SignInButton    />
                </Link> 
                 */}
                {/* <SignedOut>
                  <SignInButton>
                    <span  className='btn btn-secondary'>시작하기</span> 
                  </SignInButton>
                </SignedOut>
                
                <SignedIn>                 
                  <Link href='/chat' className='btn btn-primary'>
                      채팅 시작
                  </Link>                   
                </SignedIn> */}

                <SignedOut>
                   <Link href='/chat' className='btn btn-secondary'>
                       시작하기
                  </Link>      
                </SignedOut>

            </div>
        </div>     
    </div>
  )

}

export default HomePage;

 

 

 

 

 

 

 

 

 

6.Next.js 앱 전체 레이아웃 설정 (DaisyUI와 React Icons 활용)

 

1. 목표

  • 앱 전반의 레이아웃을 설정하기 위해 Sidebar페이지 콘텐츠를 구성합니다.
  • Sidebar는 큰 화면에서는 항상 보이게, 작은 화면에서는 토글 버튼으로 열고 닫을 수 있도록 설정합니다.
  • DaisyUI의 Drawer 컴포넌트를 사용하여 간단하고 일관성 있는 UI를 구현합니다.

 

다음 참조:

https://v5.daisyui.com/components/drawer/?lang=ko

 

 

2. 주요 구성 요소

  1. Sidebar 컴포넌트

    • Sidebar에는 메뉴와 사용자 정보, 테마 토글, 로그아웃 버튼 등을 포함할 예정입니다.
    • 현재는 간단한 텍스트 "Sidebar"만 렌더링합니다.
  2. 레이아웃 컴포넌트 (layout)

    • 모든 페이지에 공통으로 적용될 레이아웃입니다.
    • Sidebar와 페이지 콘텐츠를 포함하며, DaisyUI의 Drawer 컴포넌트를 활용합니다.

 

 

3. DaisyUI Drawer의 동작

  • DaisyUI의 Drawer 컴포넌트는 큰 화면과 작은 화면에서 다르게 동작합니다.
    • 큰 화면:
      • Sidebar가 항상 표시됩니다.
    • 작은 화면:
      • 사용자가 토글 버튼을 클릭하면 Sidebar가 나타나며, 오버레이를 통해 화면을 가립니다.

 

 

4. 코드 구조 설명

Sidebar 컴포넌트

import React from 'react'

const Sidebar:React.FC = () => {
  return (
    <ul className="menu bg-base-200 text-base-content min-h-full w-80 p-4">
      <li><a>Sidebar Item 1</a></li>
      <li><a>Sidebar Item 2</a></li>
  </ul>
  )
}

export default Sidebar;

 

간단한 텍스트로 Sidebar의 기본 구조만 구현했습니다.

 

레이아웃 컴포넌트

import { FaBarsStaggered } from "react-icons/fa6";
import Sidebar from '../../components/Sidebar';

const layout = ({ children }: { children: React.ReactNode }) => {
  return (
    // 디폴트 기본 열린상태로 lg:drawer-open
    <div className="drawer lg:drawer-open">
         {/* Drawer 토글 input */}
      <input id="my-drawer-2" type="checkbox" className="drawer-toggle" />
      
      <div className="drawer-content flex flex-col items-center justify-center"> 
           {/* 작은 화면에서 Sidebar를 열기 위한 버튼 */}      
        <label htmlFor="my-drawer-2" className="drawer-button lg:hidden fixed top-6 right-6">
            <FaBarsStaggered className="w-8 h-8 text-primary" />
        </label>


        {/* 페이지별 콘텐츠 렌더링 */}
        <div className="bg-base-0 px-8 py-12 min-h-screen w-full">              
              {children}
        </div>

      </div>


      <div className="drawer-side">
        <label htmlFor="my-drawer-2" aria-label="close sidebar" className="drawer-overlay"></label>        
          <Sidebar />
      </div>

    </div>   
  );
};

export default layout;

 

5. 구현 과정

1) Drawer 컴포넌트 설정

  • DaisyUI의 Drawer 컴포넌트를 사용하여 Sidebar와 콘텐츠를 구성했습니다.
    • 큰 화면에서는 항상 Sidebar를 표시하기 위해 lg:drawer-open 클래스를 추가.
    • 작은 화면에서는 토글 버튼으로 Sidebar를 열고 닫음.

2) 토글 버튼

  • 작은 화면에서만 표시되는 버튼을 구현했습니다.
    • FaBarsStaggered 아이콘은 React Icons를 사용하여 추가했습니다.
    • 버튼은 화면 오른쪽 상단에 고정됩니다.

3) Sidebar

  • Sidebar는 drawer-side 안에 위치하며, 페이지 콘텐츠와 분리되어 있습니다.
  • 작은 화면에서는 오버레이(drawer-overlay)를 통해 사용자 경험을 향상합니다.

4) 페이지 콘텐츠 영역

  • children 속성을 통해 페이지별 콘텐츠를 렌더링합니다.
  • 모든 페이지에 공통적으로 적용될 스타일을 설정:
    • bg-base-200: 배경색 설정
    • px-8 py-12: 내부 여백
    • min-h-screen: 화면 높이를 100%로 설정

6. 동작 설명

  1. 큰 화면:
    • Sidebar는 항상 열려 있고, 페이지 콘텐츠는 Sidebar 옆에 위치.
  2. 작은 화면:
    • 토글 버튼을 클릭하면 Sidebar가 열립니다.
    • Sidebar가 열리면 오버레이가 화면을 덮습니다.

 

7. 주요 CSS 클래스

  • drawer: DaisyUI의 Drawer 컴포넌트를 나타냄.
  • drawer-toggle: Drawer를 열고 닫기 위한 체크박스.
  • drawer-content: 페이지 콘텐츠 영역.
  • drawer-side: Sidebar 영역.
  • drawer-overlay: 작은 화면에서 Sidebar가 열렸을 때 화면을 가리는 오버레이.
  • lg:hidden: 큰 화면에서는 버튼을 숨김.

 

 

 

 

 

 

7.Sidebar 컴포넌트 구성 및 Tailwind 스타일링

 

이 작업은 Sidebar 컴포넌트를 구성하고 스타일링하여 사용자 친화적이고 재사용 가능한 구조를 만드는 것을 목표로 합니다.

이 Sidebar는 세 부분으로 구성되며, 각각의 컴포넌트를 단계적으로 구현합니다.

 

 

1. Sidebar 구조

  • SidebarHeader: Sidebar 상단에 위치하며, 애플리케이션 제목 또는 로고 등을 표시.
  • NavLinks: 중앙 영역으로, 네비게이션 링크 목록을 포함하며 나머지 공간을 채움.
  • MemberProfile: Sidebar 하단에 위치하며, 사용자 이메일, 로그아웃 버튼, 테마 전환 등 사용자 관련 정보를 표시.

 

import React from 'react'
import SidebarHeader from './SidebarHeader';
import NavLinks from './NavLinks';
import MemberProfile from './MemberProfile';

const Sidebar:React.FC = () => {
  return (
    <div className="px-4 w-80 min-h-full bg-base-300 py-12 grid grid-rows-[auto,1fr,auto]"  >
   
      <SidebarHeader />

      <NavLinks />

      <MemberProfile />

     
    </div>
  )
}

export default Sidebar;

 

2. Tailwind CSS로 Sidebar 스타일링

  • px-4: 좌우 여백.
  • w-80: Sidebar의 고정된 너비를 80 Tailwind 단위로 설정.
  • min-h-full: 화면 높이를 채우도록 설정.
  • bg-base-300: 기본 배경색 (페이지 배경색보다 약간 어두운 색).
  • py-12: 상하 여백 설정.
  • grid와 grid-rows-[auto,1fr,auto]:
    • 3개의 행으로 구성.
    • 첫 번째와 마지막 행은 자동 높이(auto).
    • 중앙 행은 나머지 공간을 차지 (1fr).

 

 

3. 작업 순서

1) SidebarHeader

  • Sidebar 상단의 타이틀이나 로고를 표시합니다.
  • 기본적인 컴포넌트 형태로 시작한 뒤, 필요한 추가 기능(예: 애플리케이션 로고)을 단계적으로 구현.

2) NavLinks

  • Sidebar의 중앙 영역으로, 네비게이션 링크를 포함.
  • Tailwind를 활용하여 링크 목록을 정렬 및 스타일링.
  • 이후 링크를 반복적으로 렌더링하여 동적으로 추가 가능.

3) MemberProfile

  • Sidebar 하단에 위치.
  • 사용자의 이메일 표시, 로그아웃 버튼, 테마 변경 버튼을 포함.
  • 하단 고정 및 간단한 사용자 인터페이스를 제공.

 

 

 

4. Tailwind CSS 활용

  • 그리드 시스템: grid와 grid-rows-[auto,1fr,auto]로 3개 행을 구성.
  • 전체 스타일:
    • w-80: Sidebar 고정 너비.
    • min-h-full: Sidebar가 화면 높이를 채우도록 설정.
    • bg-base-300: 배경색 설정.

 

5. 브라우저에서 확인

  • Sidebar는 다음과 같은 구조로 동작합니다:
    • 첫 번째 행: SidebarHeader가 상단에 위치.
    • 두 번째 행: NavLinks가 화면의 나머지 공간을 채움.
    • 세 번째 행: MemberProfile이 하단에 고정.

 

 

 

 

 

 

8.Sidebar Header 구성 및 Theme Toggle 추가

 

목표

  • Sidebar 상단에 표시될 SidebarHeader를 구성하고, 아이콘, 로고, 테마 전환 버튼을 추가합니다.
  • 테마 전환 버튼은 ThemeToggle 컴포넌트로 분리하여 추후에 로직을 추가할 준비를 합니다.

 

 

1. SidebarHeader 주요 구성 요소

  1. 아이콘:
    • react-icons 라이브러리에서 제공되는 SiOpenaigym 아이콘 사용.
  2. 로고:
    • 애플리케이션 이름(GPTGenius)을 강조하여 표시.
  3. 테마 전환 버튼:
    • ThemeToggle 컴포넌트로 구현하며, 현재는 간단한 버튼만 추가.
    • 추후 테마 변경 로직을 추가할 예정.

 

 

2. 코드 설명

ThemeToggle 컴포넌트

import React from 'react';

const ThemeToggle: React.FC = () => {
  return (
    <button className="btn btn-primary btn-sm">
        ThemeToggle
    </button>
  );
};

export default ThemeToggle;

 

  • 단순한 버튼으로 구성.
  • Tailwind CSS 클래스:
    • btn: DaisyUI 기본 버튼 스타일.
    • btn-primary: 테마 색상에 맞춘 주요 버튼 스타일.
    • btn-sm: 작은 버튼 크기.
    •  

SidebarHeader 컴포넌트

 

import React from 'react';
import { SiOpenaigym } from 'react-icons/si';
import ThemeToggle from './ThemeToggle';

const SidebarHeader: React.FC = () => {
  return (
    <div className="flex items-center mb-4 gap-4 px-4">
      <SiOpenaigym className="w-10 h-10 text-primary" />
      <h2 className="text-xl font-extrabold text-primary">GPTGenius</h2>
      <ThemeToggle />
    </div>
  );
};

export default SidebarHeader;

 

  1. 레이아웃:

    • div를 사용하여 Flexbox로 구성.
    • Tailwind CSS 클래스:
      • flex: Flexbox 레이아웃 적용.
      • items-center: 세로 정렬 가운데 정렬.
      • mb-4: 아래쪽 여백.
      • gap-4: 구성 요소 간 간격.
      • px-4: 좌우 패딩 추가.

 

  1. 아이콘:

    • react-icons에서 가져온 SiOpenaigym을 사용.
    • Tailwind CSS 클래스:
      • w-10 h-10: 아이콘 크기 설정.
      • text-primary: 테마 색상 적용.
    •  
  2. 로고:

    • h2 태그를 사용하여 애플리케이션 이름 표시.
    • Tailwind CSS 클래스:
      • text-xl: 큰 글자 크기.
      • font-extrabold: 두꺼운 글꼴.
      • text-primary: 테마 색상 적용.
    •  
  3. ThemeToggle:

    • SidebarHeader의 우측에 위치.

 

 

 

 

 

 

9.NavLinks 컴포넌트 제작

 

  • 이 컴포넌트는 Next.js의 Link 컴포넌트를 활용하여 네비게이션 링크를 생성합니다.
  • 배열(links)에 각 페이지의 URL(href)과 표시될 텍스트(label)를 정의하고 이를 순회하며 링크를 렌더링합니다.
  • DaisyUI의 menu 컴포넌트를 활용하여 UI 스타일링을 추가합니다.

 

구현 세부 내용

  1. 링크 배열 정의

    • 네 가지 링크(/chat, /tours, /tours/new-tour, /profile)를 정의합니다.
    • 각 링크는 href(이동할 URL)와 label(표시할 텍스트)로 이루어진 객체로 구성됩니다.
    • 추후 더 많은 링크를 추가할 수도 있습니다.

 

const links = [
  { href: "/chat", label: "chat" },
  { href: "/tours", label: "tours" },
  { href: "/tours/new-tour", label: "new tour" },
  { href: "/profile", label: "profile" },
];

 

DaisyUI의 menu 컴포넌트를 활용한 스타일 적용

  • div에 DaisyUI의 menu와 text-base-content 클래스를 적용해 텍스트와 스타일을 조정합니다.
  • 각 링크의 텍스트는 capitalize 클래스를 사용해 첫 글자를 대문자로 표기합니다

 

<div className='menu text-base-content'>

 

map 함수로 링크 렌더링

  • 배열 links를 map 함수로 순회하여 각 링크를 렌더링합니다.
  • 각 링크는 li 태그로 감싸져 있으며, key 속성으로 고유값(href)을 설정합니다.
  • Next.js의 Link 컴포넌트를 사용하여 페이지 이동이 가능합니다.

 

{links.map(link => {
    return (
        <li key={link.href}>
            <Link href={link.href} className='capitalize'>
                {link.label}
            </Link>
        </li>
    );
})}

 

div 대신 ol 태그 사용

  • DaisyUI의 문서에서 권장하는 방식에 맞게 div가 아닌 ol 태그를 사용해 HTML 구조를 올바르게 작성합니다.
  • ol 태그는 순서가 있는 리스트를 나타내므로, 네비게이션에 적합합니다.

 

import Link from 'next/link';
import React from 'react';

const links = [
  { href: "/chat", label: "chat" },
  { href: "/tours", label: "tours" },
  { href: "/tours/new-tour", label: "new tour" },
  { href: "/profile", label: "profile" },
];

const NavLinks: React.FC = () => {
  return (
    <ol className='menu text-base-content'>
      {links.map(link => (
        <li key={link.href}>
          <Link href={link.href} className='capitalize'>
            {link.label}
          </Link>
        </li>
      ))}
    </ol>
  );
};

export default NavLinks;

 

 

 

 

 

 

 

 

 

10.MemberProfile 컴포넌트 

 

  • Clerk의 UserButton 컴포넌트를 사용해 사용자 프로필 버튼을 생성합니다.
  • **auth**와 currentUser 함수를 사용해 사용자 정보를 가져옵니다.
    • auth: 사용자 ID 등 기본 인증 정보를 가져오는 함수.
    • currentUser: 현재 로그인된 사용자의 세부 정보를 가져오는 함수.
  • 사용자 이메일 주소와 프로필 버튼을 화면에 표시합니다.

 

 

작업 과정

1. 컴포넌트 기본 구조

  • MemberProfile은 React 함수형 컴포넌트로 작성되며, 비동기 함수(async)로 선언됩니다.
  • auth와 currentUser 함수는 각각 Clerk에서 제공하는 기능으로, 사용자 정보를 가져오는 데 사용됩니다.
  • useEffect를 사용하지 않고, 컴포넌트가 비동기적으로 데이터를 처리하도록 구성되었습니다.

 

2. auth 함수와 currentUser 함수

  • auth()

    • 현재 로그인된 사용자의 ID를 가져옵니다.
    • auth().userId를 통해 사용자 ID를 직접 추출할 수 있습니다.
    • 로그용으로 console.log를 활용해 사용자 ID를 출력.
  • currentUser()

    • 로그인된 사용자의 상세 정보를 가져옵니다.
    • 기본적으로 Clerk의 설정에 따라 제공되는 사용자 필드(이메일, 이름 등)를 확인할 수 있습니다.
    • 여기서는 이메일 주소 배열(emailAddresses) 중 첫 번째 값을 가져옵니다.

 

3. UserButton 컴포넌트

  • Clerk에서 제공하는 **UserButton**은 사용자가 로그인/로그아웃 및 프로필 관리 기능을 사용할 수 있는 버튼 컴포넌트입니다.
  • afterSignOutUrl 속성을 통해 로그아웃 후 리디렉션될 URL을 지정할 수 있습니다.
    • 여기서는 로그아웃 후 메인 페이지(/)로 이동하도록 설정.

 

4. CSS 스타일링

  • 간단한 TailwindCSS 클래스를 사용해 스타일을 지정합니다:
    • px-4: 내부 여백(수평 방향).
    • flex: Flexbox 레이아웃 사용.
    • items-center: 수직 중앙 정렬.
    • gap-2: Flexbox 아이템 간 간격 설정.

 

 

import { UserButton } from '@clerk/nextjs';
import { auth, currentUser } from '@clerk/nextjs/server';
import React from 'react'

const MemberProfile:React.FC = async() => {
  const user=await currentUser();
  const {userId} =await auth();
  console.log(":유저 아이디 " ,userId);

  return (
    <div className="px-4 flex items-center gap-2">
      <UserButton />
      <p>
        {user?.emailAddresses[0].emailAddress}
      </p>
    </div>
  )
}

export default MemberProfile;

 

 

 

 

 

 

11.테마 변경 기능 구현

 

1. DaisyUI 테마 설정

  • DaisyUI를 활용하여 애플리케이션에 테마를 추가.
  • DaisyUI의 다양한 테마 중 원하는 테마를 선택 가능.
  • 선택한 테마: Winter(라이트 테마)와 Dracula(다크 테마).

 

 

2. Tailwind 설정

  1. tailwind.config.js에 DaisyUI 플러그인 추가
    plugins 배열에 DaisyUI를 추가.

  2. 사용할 테마 지정
    DaisyUI에서 제공하는 테마 중 필요한 테마만 추가.

 

daisyui: {
    themes: ["winter", "dracula"], // 필요한 테마만 설정
}

 

3.서버 재시작
  테마 설정 후, 변경 사항을 적용하기 위해 npm run dev로 개발 서버를 재시작.

 

3. 테마 초기 설정

  • HTML에 테마 적용
    HTML 태그에 data-theme 속성을 추가하고, 기본값을 설정.
<html data-theme="winter">


 

  • data-theme="winter": 기본 테마로 Winter 설정.
  • 테마를 동적으로 변경하려면 자바스크립트에서 해당 속성을 업데이트해야 함.

 

4. 테마 토글 컴포넌트 구현

  • React에서 테마를 동적으로 변경하는 기능 구현.
  • 컴포넌트 구조
"use client";
import React, { useState } from "react";
import { BsMoonFill, BsSunFill } from "react-icons/bs";

const themes = {
    winter: "winter",
    dracula: "dracula",
};

const ThemeToggle = () => {
    const [theme, setTheme] = useState(themes.winter);

    const toggleTheme = () => {
        const newTheme = theme === themes.winter ? themes.dracula : themes.winter;
        document.documentElement.setAttribute("data-theme", newTheme);
        setTheme(newTheme);
    };

    return (
        <button className="btn btn-sm btn-outline" onClick={toggleTheme}>
            {theme === "winter" ? (
                <BsMoonFill className="w-4 h-4" />
            ) : (
                <BsSunFill className="w-4 h-4" />
            )}
        </button>
    );
};

export default ThemeToggle;

 

5. 컴포넌트 주요 로직

  1. 테마 상태 관리

    • useState를 사용하여 현재 테마를 상태로 저장.
    • 초기값: themes.winter.

 

  1. 테마 토글 함수 (toggleTheme)

    • 현재 테마가 Winter면 Dracula로, 반대면 Winter로 변경.
    • document.documentElement.setAttribute()를 사용해 HTML 태그의 data-theme 속성 변경.
  2.  
  3. 아이콘 조건부 렌더링

    • 현재 테마에 따라 다른 아이콘(BsMoonFill 또는 BsSunFill) 표시.
    • 버튼 클래스: btn btn-sm btn-outline(DaisyUI 기본 스타일).

 

 

 

 

 

 

 

12.프로필 페이지 구현 과정 정리

 

1. 프로필 페이지가 가장 쉬운 이유

  • Clerk의 UserProfile 컴포넌트를 활용하여 간단하게 구성 가능.
  • UserProfile 컴포넌트가 사용자와 관련된 모든 유용한 정보를 자동으로 제공.

 

2. 구현 과정

  1. 페이지 파일 위치

    • 경로: app/dashboard/profile/page.tsx
    • 이 파일에서 UserProfile 컴포넌트를 사용하여 프로필 페이지를 구현.
  2. 컴포넌트 설정

    • UserProfile 컴포넌트를 Clerk에서 가져와서 렌더링.
    • routing="hash" 속성을 추가하여 해시 기반 라우팅을 사용.
    • div 태그로 감싸고 Tailwind CSS로 가운데 정렬.

 

import { UserProfile } from '@clerk/nextjs'
import React from 'react'

const ProfilePage: React.FC = () => {
  return (
    <div className='flex justify-center'>
      <UserProfile routing="hash" />
    </div>
  )
}

export default ProfilePage;

 

  1. 브라우저에서 확인

    • 페이지를 저장한 후 브라우저에서 확인.
    • Clerk에서 제공하는 깔끔한 UI의 사용자 프로필 관리 화면이 나타남.

 

3. 프로필 페이지의 필요성

  • UserProfile 컴포넌트는 이미 Clerk의 UserButton에서 "Manage Account" 버튼을 통해 접근 가능.
  • 따라서, 별도의 프로필 페이지가 필수는 아니지만, UserProfile 컴포넌트를 프로젝트에 포함하기 위해 추가.

 

 

 

 

 

 

 

13.React Hot Toast와 Providers 구성 정리

 

1. React Hot Toast 추가 및 Providers 파일 구성

React Hot Toast를 프로젝트에 추가하면서, 전역 상태를 관리하는 Providers 컴포넌트를 설정하였습니다.
이는 추후 React Query와 같은 전역 관리 라이브러리를 추가할 때도 재사용됩니다.

 

2. 구현 과정

  1. React Hot Toast 설치

    • react-hot-toast 라이브러리를 설치.
    • 만약 이전에 설치했다면, 다시 설치할 필요는 없음.
  2. Providers 컴포넌트 생성

    • **Providers**는 전역적으로 필요한 설정(예: Toast, React Query 등)을 감싸는 역할.
    • React 컴포넌트 트리에 전역 상태를 적용하기 위해 **children**을 감쌈.

 

"use client";
import React from "react";
import { Toaster } from "react-hot-toast";

interface ProvidersProps {
  children: React.ReactNode;
}

const Providers: React.FC<ProvidersProps> = ({ children }) => {
  return (
    <>
      <Toaster position="top-center" /> {/* Toast 위치 설정 */}
      {children} {/* 자식 컴포넌트 출력 */}
    </>
  );
};

export default Providers;

 

RootLayout에 Providers 추가

  • RootLayout에서 Providers 컴포넌트를 **children**을 감싸는 형태로 추가.
  • Providers 내부에서 react-hot-toast와 같은 전역 상태 관리 컴포넌트를 사용할 수 있음.

 

import Providers from "./providers";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <ClerkProvider localization={koKR}>
      <html lang="ko" data-theme="winter">
        <body className={`${geistSans.variable} ${notoSansKR.className} antialiased`}>
          <Providers>{children}</Providers> {/* Providers로 감싸기 */}
        </body>
      </html>
    </ClerkProvider>
  );
}

 

3. Providers 설정의 장점

  • 전역 상태 관리: react-hot-toast 외에, 추가로 React Query와 같은 라이브러리를 쉽게 통합 가능.
  • 유지보수성 증가: Providers 컴포넌트 하나로 전역 설정을 관리하므로 코드의 가독성과 재사용성이 향상.
  • 위치 설정: Toast 위치를 top-center로 설정하여 화면 중앙 상단에 알림 표시.

 

4. React Query 추가를 대비

Providers는 현재 react-hot-toast만 포함하지만, 추후 React Query 또는 다른 라이브러리를 추가할 때에도 같은 방식으로 확장 가능.

 

 

 

 

 

 

 

14.Chat Page 구조 구현

 

1. 페이지 개요

  • 이 페이지는 ChatGPT 클론처럼 동작하며, 사용자가 AI에게 질문을 할 수 있도록 설계됨.
  • React Query, Next.js Server Actions, OpenAI API가 함께 사용될 예정.
  • 이번 단계에서는 구조 설정 및 기본 폼 구현까지 완료.

 

 

2. 구현 목적

  1. 구조 설계: 페이지와 컴포넌트 분리.
  2. 기본 상태값 설정: 사용자 입력(text)과 메시지(messages) 상태 관리.
  3. 기본 UI 구현: 입력 폼과 메시지 표시 영역.
  4. 기능 테스트: 폼 제출 시 데이터가 정상적으로 처리되는지 확인.

 

 

3. 구현 세부 단계

1) Chat 컴포넌트 생성

  • 클라이언트 컴포넌트로 설정 ("use client").
  • 사용자의 입력값을 관리하기 위해 **useState**로 상태값 관리

 

"use client";
import React, { useState } from "react";

const Chat: React.FC = () => {
  const [text, setText] = useState(""); // 사용자 입력
  const [messages, setMessages] = useState<{ text: string; user: string }[]>([]); // 메시지 리스트

 

 

2) 폼 제출 이벤트 핸들러

  • 폼 제출 시 페이지 새로고침을 막고(preventDefault), 사용자가 입력한 메시지를 상태값에 추가.
const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault();
  setMessages([...messages, { text, user: "user" }]); // 메시지 추가
  setText(""); // 입력창 초기화
};

 

3) 컴포넌트 레이아웃

  • Grid 레이아웃: 메시지 영역과 입력 폼을 분리.
  • 상단 메시지 영역: 전체 화면의 대부분을 차지.
  • 하단 입력 폼: 화면 아래에 고정.

 

return (
  <div className="min-h-[calc(100vh-6rem)] grid grid-rows-[1fr,auto]">
    {/* 메시지 영역 */}
    <div>
      <h2 className="text-5xl">messages</h2>
    </div>

    {/* 입력 폼 */}
    <form onSubmit={handleSubmit} className="max-w-4xl pt-12">
      <div className="join w-full">
        {/* 입력 필드 */}
        <input
          type="text"
          placeholder="GeniusGPT 메시지 보내기..."
          className="input input-bordered join-item w-full"
          value={text}
          required
          onChange={(e) => setText(e.target.value)}
        />
        {/* 버튼 */}
        <button className="btn btn-primary join-item" type="submit">
          질문하기
        </button>
      </div>
    </form>
  </div>
);

 

 

4) CSS 및 Tailwind 클래스

  • min-h-[calc(100vh-6rem)]: 화면 높이에서 6rem(레이아웃 여백)만큼 제외하여 영역 차지.
  • grid-rows-[1fr,auto]: 메시지 영역은 1fr(남은 공간), 폼은 auto(내용 높이)로 설정.
  • DaisyUI의 join 컴포넌트:
    • input과 button을 한 줄에 배치.
    • join-item 클래스를 추가하여 DaisyUI가 자동으로 스타일 적용.

 

4. 결과 테스트

  1. 브라우저에서 Chat 페이지로 이동.
  2. 입력 필드에 텍스트 입력 후 "질문하기" 버튼 클릭.
  3. 입력한 메시지가 콘솔에 출력되어야 함.

 

 

 

 

 

 

 

 

15. React Query 개념과 Next.js 프로젝트 설정 정리

 

1. React Query를 사용하는 이유

  • 간단한 설정: 초기 설정이 매우 간단하며, useEffect와 로컬 상태 관리를 직접 구현할 필요가 없음.
  • 캐싱 지원: 요청 데이터를 캐싱하여 애플리케이션이 더 빠르고 반응성이 높음.
  • 유용한 DevTools: React Query DevTools를 통해 데이터 상태와 요청을 시각적으로 확인 가능.

 

2. React Query 설치

  • React Query와 DevTools 설치

 

npm install @tanstack/react-query @tanstack/react-query-devtools

 

3. React Query 주요 개념

1) Query Client와 Query Client Provider

  • QueryClient: React Query의 핵심 인스턴스.
  • QueryClientProvider: React 트리에 QueryClient를 주입하는 컴포넌트.

설정 예시:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60000, // 데이터 유효 시간: 1분
    },
  },
});

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} /> {/* DevTools 추가 */}
    </QueryClientProvider>
  );
}

 

2) 주요 훅: useQuery

  • 데이터를 서버에서 가져올 때 사용.
  • 매개변수:
    • queryKey: 캐싱 및 요청 관리를 위한 고유 키 (배열 형식).
    • queryFn: 데이터를 가져오는 비동기 함수 (Promise 반환).

 

사용 예시:

import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

const fetchTasks = async () => {
  const { data } = await axios.get('/api/tasks');
  return data;
};

const TaskList = () => {
  const { data, isLoading, isError } = useQuery(['tasks'], fetchTasks);

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error loading tasks.</div>;

  return (
    <ul>
      {data.tasks.map(task => (
        <li key={task.id}>{task.title}</li>
      ))}
    </ul>
  );
};

 

3) 주요 훅: useMutation

  • 서버에서 데이터를 생성, 수정, 삭제할 때 사용.
  • 매개변수:
    • mutationFn: 데이터를 조작하는 비동기 함수.
    • 옵션: 성공(onSuccess) 및 에러(onError) 핸들러 설정 가능.

 

사용 예시:

import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';

const createTask = async (newTask) => {
  const { data } = await axios.post('/api/tasks', newTask);
  return data;
};

const NewTaskForm = () => {
  const queryClient = useQueryClient();
  const mutation = useMutation(createTask, {
    onSuccess: () => {
      // 'tasks' 키를 가진 캐시를 무효화
      queryClient.invalidateQueries(['tasks']);
    },
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    const newTask = { title: e.target.task.value };
    mutation.mutate(newTask); // 새로운 작업 생성
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="task" placeholder="New Task" />
      <button type="submit">Add Task</button>
    </form>
  );
};

 

 

4. React Query 주요 기능

  1. 캐싱: 동일한 queryKey로 데이터를 요청하면 캐싱된 데이터를 반환.
  2. Stale Time: staleTime 동안 데이터를 재요청하지 않고 캐싱된 데이터를 사용.
  3. Prefetching: 서버에서 데이터를 미리 가져와 캐시에 저장.
  4. Query Invalidation: 데이터가 변경되었을 때 특정 쿼리를 무효화하여 데이터를 새로 가져옴.

 

5. React Query로 Chat 페이지 구현

  • Chat 페이지에서 사용할 주요 기능:
    • useQuery: OpenAI API를 호출하여 응답 데이터를 가져옴.
    • useMutation: 사용자 입력 데이터를 서버로 전송.
    • staleTime: 서버와 클라이언트 간의 데이터 동기화를 효율적으로 관리.

 

 

 

 

 

 

 

16. HydrationBoundary와 dehydrate(queryClient)의 역할과 동작 원리

 

React Query와 Next.js를 함께 사용할 때, 서버에서 생성된 쿼리 데이터를 클라이언트로 효율적으로 전달하기 위해 사용하는 방식입니다

 

1. 핵심 개념

1) HydrationBoundary

  • React Query에서 제공하는 컴포넌트.
  • 서버에서 생성된 React Query 데이터를 클라이언트로 전달하고, 이를 클라이언트 쪽에서 React Query로 사용할 수 있도록 "하이드레이션"(Hydration)을 처리.
  • 클라이언트가 초기 데이터를 빠르게 렌더링하면서 서버-클라이언트 간 데이터 동기화를 효율적으로 수행함.

 

2) dehydrate(queryClient)

  • **서버에서 생성된 QueryClient의 상태를 직렬화(Serialize)**하는 함수.
  • 서버에서 가져온 쿼리 데이터를 JSON 형식으로 변환하여 클라이언트로 전달.
  • 변환된 데이터를 HydrationBoundary의 state 속성을 통해 클라이언트에 주입.

 

2. 작동 원리

  1. 서버에서 React Query 데이터 생성:

    • 서버에서 쿼리 데이터를 가져오기 위해 QueryClient를 생성하고, 해당 데이터를 React Query의 캐시에 저장.
const queryClient = new QueryClient();

 

2.React Query 데이터를 직렬화:

  • 서버에서 React Query 데이터를 클라이언트로 전달하기 위해 dehydrate 함수로 데이터를 JSON으로 직렬화.

 

const dehydratedState = dehydrate(queryClient);

 

3.HydrationBoundary로 데이터 전달:

  • HydrationBoundary의 state 속성을 통해 직렬화된 데이터를 전달.
  • 클라이언트에서 React Query의 캐시에 데이터를 주입하여, 초기 데이터와 함께 렌더링.

 

<HydrationBoundary state={dehydrate(queryClient)}>
  <Chat />
</HydrationBoundary>

 

4.

클라이언트에서 캐시 데이터 활용:

  • 클라이언트는 서버에서 전달받은 데이터를 기반으로 초기 렌더링을 수행.
  • React Query는 기존 캐시 데이터를 재활용하며, 필요 시 추가 데이터를 가져옴.

 

 

3. 코드 동작 설명

<HydrationBoundary state={dehydrate(queryClient)}>
  <Chat />
</HydrationBoundary>

 

  • queryClient 생성:

    • 서버에서 데이터를 요청하고 React Query의 캐시에 저장하기 위해 QueryClient를 생성.
    • 이 queryClient는 서버에서 데이터 상태를 관리.
  •  
  • dehydrate(queryClient):

    • 서버에서 생성된 queryClient의 상태를 JSON으로 직렬화.
    • 직렬화된 상태는 클라이언트에서 React Query의 초기 상태로 사용.
  •  
  • HydrationBoundary:

    • 직렬화된 데이터를 state 속성을 통해 클라이언트로 전달.
    • 클라이언트는 이를 통해 React Query 캐시를 초기화하고, 서버에서 전달된 데이터를 즉시 사용할 수 있게 됨.

 

 

4. 왜 사용하는가?

1) 서버에서 데이터를 미리 가져옴 (Prefetching)

  • React Query는 클라이언트가 데이터를 요청하기 전에 서버에서 데이터를 미리 가져올 수 있음.
  • 서버에서 데이터를 가져와 클라이언트에 전달하면:
    • 초기 렌더링 속도가 빨라짐.
    • 클라이언트가 데이터를 새로 요청하지 않아도 되므로 네트워크 비용 절감.

2) 데이터 동기화 유지

  • 클라이언트는 서버에서 직렬화된 데이터를 기반으로 동작하므로, 서버-클라이언트 간 데이터 불일치 문제가 줄어듦.

3) React Query 캐싱 활용

  • 서버에서 전달된 데이터를 클라이언트의 React Query 캐시에 바로 저장하여 효율적으로 데이터 관리.

 

5. 간단한 흐름 요약

  1. 서버에서 QueryClient를 생성하고 데이터를 요청하여 React Query 캐시에 저장.
  2. dehydrate(queryClient)로 데이터를 JSON으로 직렬화.
  3. 직렬화된 데이터를 HydrationBoundary의 state 속성으로 전달.
  4. 클라이언트는 전달받은 데이터를 React Query 캐시에 초기화하여 빠른 렌더링을 수행.

 

6. 실제 데이터 흐름 예시

// 1. 서버에서 QueryClient와 React Query 데이터 생성
const queryClient = new QueryClient();
await queryClient.prefetchQuery(["chatMessages"], fetchChatMessages);

// 2. 데이터를 직렬화하여 클라이언트에 전달
const dehydratedState = dehydrate(queryClient);

// 3. HydrationBoundary로 클라이언트에 전달
return (
  <HydrationBoundary state={dehydratedState}>
    <Chat />
  </HydrationBoundary>
);

 

7. 결론

  • HydrationBoundary와 dehydrate를 사용하면 서버에서 가져온 React Query 데이터를 클라이언트로 전달할 수 있습니다.
  • 이를 통해 초기 렌더링 성능을 최적화하고, 서버-클라이언트 간 데이터 동기화를 간단히 관리할 수 있습니다.
  • 이 방식은 특히 Next.js와 같은 서버 렌더링 환경에서 빠른 사용자 경험을 제공하는 데 매우 유용합니다

 

 

 

 

 

 

17. React Query와 Next.js를 활용한 Chat 페이지 구현 정리

1. 개요

이 단계에서는 React Query를 설정하고, Next.js Server Actions와 통합하여 클라이언트와 서버 간 데이터 교환을 구현했습니다.
React Query의 강력한 기능을 활용하여 캐싱서버 통신을 효율적으로 처리하고, boilerplate 설정을 통해 재사용 가능한 구조를 만들었습니다.

 

2. 주요 구현 내용

1) React Query 기본 설정

Query Client Provider 설정

  • QueryClient를 생성하고, staleTime(데이터 유효 시간)을 1분으로 설정

 

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60000, // 1분 동안 데이터 유효
    },
  },
});

 

  • 이유:
    • 데이터 요청을 최소화하여 성능 최적화.
    • 데이터가 자주 변경되지 않는 경우 캐싱된 데이터를 재사용.

 

QueryClientProvider로 전역 상태 관리

  • QueryClientProvider로 전체 애플리케이션에 QueryClient를 주입.
  • React Query DevTools를 추가하여 디버깅에 활용

 

import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient({ /* 설정 */ });

return (
  <QueryClientProvider client={queryClient}>
    <YourApp />
    <ReactQueryDevtools initialIsOpen={false} />
  </QueryClientProvider>
);

 

2) 페이지별 React Query 설정

HydrationBoundary 사용

  • Next.js의 서버 컴포넌트와 클라이언트 컴포넌트 간 데이터를 공유하기 위해 HydrationBoundary를 사용.
  • dehydrate를 통해 서버에서 생성한 React Query 데이터를 클라이언트로 전달.

ChatPage 구성:

import Chat from '@/components/Chat';
import React from 'react';
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';

const ChatPage: React.FC = () => {
  const queryClient = new QueryClient();

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Chat />
    </HydrationBoundary>
  );
};

export default ChatPage;

 

3) Chat 컴포넌트 구성

상태 관리

  • **useState**로 입력값(text)과 메시지 배열(messages) 관리.
  • React Query의 **useMutation**으로 OpenAI API 호출.

useMutation으로 서버 액션 호출

  • 서버 액션인 generateChatResponse를 호출하여 AI 응답을 받아 처리.
  • useMutation의 옵션:
    • mutationFn: API 호출 함수.
    • onSuccess: 응답 성공 시 메시지 배열에 추가.
    • onError: 에러 처리.

 

const { mutate, isPending, isError } = useMutation<string, Error, string, boolean>({
  mutationFn: (message: string) => generateChatResponse(message),
  onSuccess: (response: string) => {
    setMessages((prev) => [...prev, { text: response, user: "bot" }]);
  },
  onError: (error: Error) => {
    console.error("에러 발생:", error);
  },
});

 

폼 제출 이벤트 처리

  • 사용자가 메시지를 입력하고 폼을 제출하면:
    • text를 messages 상태값에 추가.
    • mutate로 서버에 메시지 전송.
const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault();
  if (!text.trim()) return;

  setMessages((prev) => [...prev, { text, user: "user" }]);
  mutate(text); // 서버 액션 호출
  setText(""); // 입력 초기화
};

렌더링

  • 메시지를 화면에 출력:
    • 사용자 메시지: 오른쪽 정렬, 파란색 배경.
    • AI 응답 메시지: 왼쪽 정렬, 회색 배경.

 

<div className="pt-4">
  {messages.map((message, index) => (
    <div
      key={index}
      className={`p-4 rounded-lg mb-2 ${
        message.user === "user"
          ? "bg-blue-500 text-white text-right"
          : "bg-gray-200 text-black"
      }`}
    >
      {message.text}
    </div>
  ))}
</div>

 

 

 

4) 서버 액션 구현

generateChatResponse 서버 액션

  • Next.js의 서버 액션으로 메시지를 받고 응답을 반환.

현재는 테스트 목적으로 단순 문자열 반환

"use server";

export const generateChatResponse = async (chatMessage: string) => {
  console.log(chatMessage); // 클라이언트에서 받은 메시지 출력
  return "ai response"; // AI 응답(테스트용)
};

 

결과 확인

  • 사용자가 입력한 메시지는 터미널(console.log)에 출력.
  • 응답 메시지는 클라이언트의 messages 상태값에 추가.

3. React Query와 Server Actions의 통합 장점

  1. 서버 액션과 완벽 통합:

    • React Query의 useMutation에서 서버 액션 호출 가능.
    • 서버 액션은 Promise를 반환하므로 React Query와 잘 맞음.
  2. 데이터 관리 간소화:

    • useQuery와 useMutation을 통해 상태 관리, API 호출, 에러 처리를 간단하게 구현.
  3. 재사용 가능한 Boilerplate:

    • 각 페이지에서 HydrationBoundary와 React Query 설정을 복사해 재사용 가능.
    • 서버-클라이언트 통합 로직이 표준화됨.

 

4. 결론

  • 이번 작업에서는 React QueryNext.js Server Actions를 활용해 효율적인 데이터 요청 및 상태 관리를 구현했습니다.
  • 클라이언트와 서버 통합을 통해 사용자는 빠르고 매끄러운 경험을 제공받습니다.

 

 

 

 

 

 

 

 

 

18. Chat 페이지 -OpenAi  구축

 

 

 

openAI 설정 및 사용방법

링크 :    https://platform.openai.com/docs/overview

 

 

키 발급 주소 :   https://platform.openai.com/api-keys

 

 

 

 

 

1) generateChatResponse.ts

"use server";
// 서버 사이드 코드로 실행됨을 명시

import OpenAI from "openai";

// OpenAI 라이브러리를 사용하여 GPT 모델과 상호작용

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY, // 환경 변수에서 OpenAI API 키를 가져옴
});

// 입력 메시지 형식을 정의하는 인터페이스
interface GenerateChatResponseParams {
  role: "system" | "user" | "assistant"; // 메시지 역할: 시스템, 사용자, 또는 어시스턴트
  content: string; // 메시지 내용
}

// GPT-3.5 모델을 사용하여 채팅 응답을 생성하는 함수
export const generateChatResponse = async (chatMessages: GenerateChatResponseParams[]) => {
  try {
    // OpenAI API를 호출하여 채팅 완료 요청 생성
    const response = await openai.chat.completions.create({
      messages: [
        { role: "system", content: "You are a helpful assistant" }, // 시스템 초기 메시지
        ...chatMessages, // 사용자가 제공한 메시지 배열 추가
      ],
      model: "gpt-3.5-turbo", // 사용할 GPT 모델 지정
      temperature: 0, // 생성 텍스트의 무작위성 조정 (0 = 결정론적)
    });

    // API 응답 출력 (디버깅 용도)
    console.log("API Response:", response);

    // 첫 번째 선택 항목의 메시지를 반환하거나 null 반환
    return response.choices[0].message || null;
  } catch (error) {
    // 오류 발생 시 로그 출력 및 null 반환
    console.error("generateChatResponse error:", error);
    return null;
  }
};

 

주석 설명

  1. "use server":

    • 이 코드는 서버에서 실행되도록 설정합니다. Next.js에서 사용되는 서버 전용 코드를 작성할 때 사용하는 선언입니다.

 

  1. GenerateChatResponseParams 인터페이스:

    • role과 content 필드를 가진 입력 메시지의 타입을 정의합니다.
    • role은 메시지의 역할("system", "user", "assistant")을 나타내며, content는 메시지의 내용을 나타냅니다.

 

  1. generateChatResponse 함수:

    • 이 함수는 OpenAI API를 호출하여 사용자가 보낸 메시지 배열(chatMessages)에 대한 GPT 응답을 생성합니다.

 

  1. API 호출 파라미터:

    • messages:
      • role: "system"으로 정의된 시스템 초기 메시지는 GPT 모델의 행동을 제어합니다.
      • chatMessages는 사용자와 어시스턴트 간의 이전 대화를 포함합니다.
    • model: 사용할 GPT 모델 이름을 명시합니다.
    • temperature: 생성 텍스트의 무작위성을 제어합니다. 낮은 값은 더 결정론적인 응답을 생성합니다.

 

  1. 에러 처리:

    • 오류 발생 시 콘솔에 오류 메시지를 출력하고 null을 반환합니다.
  2. 응답 반환:

    • GPT 응답 중 첫 번째 선택지(response.choices[0].message)를 반환하거나 유효하지 않은 경우 null을 반환합니다.

 

 

2) components/Chat.tsx

"use client";

import React, { useState, useRef, useEffect } from "react";
import { useMutation } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { generateChatHuggingFaceResponse } from "@/actions/chat/chatHuggingFaceActions";

// 메시지 타입 정의
type Message = {
  role: "user" | "assistant" | "system";
  content: string;
};

const AI_TYPE = "DialoGPT-medium";

const Chat: React.FC = () => {
  const [text, setText] = useState<string>(""); // 입력 필드 상태
  const [messages, setMessages] = useState<Message[]>([]); // 메시지 목록 상태

  const messageListRef = useRef<HTMLDivElement | null>(null); // 메시지 리스트 DOM 참조

  // 메시지 추가 시 자동 스크롤 처리
  useEffect(() => {
    if (messageListRef.current) {
      messageListRef.current.scrollTop = messageListRef.current.scrollHeight;
    }
  }, [messages]);

  const { mutate, isPending, isError } = useMutation<string | null, Error, Message>({
    mutationFn: async (query) => {
      const response = await generateChatHuggingFaceResponse([...messages, query]);
      console.log("API Response: ", response);
      if (AI_TYPE === "DialoGPT-medium") return response || null;
      return response?.content || null;
    },
    onSuccess: (response) => {
      if (!response) {
        toast.error("Something went wrong...");
        return;
      }
      setMessages((prev) => [...prev, { role: "assistant", content: response }]);
    },
    onError: (error) => {
      console.error("Error occurred:", error);
      toast.error("API 요청 중 오류가 발생했습니다.");
    },
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!text.trim()) return;

    const query: Message = { role: "user", content: text };
    setMessages((prev) => [...prev, query]);
    mutate(query);
    setText("");
  };

  return (
    <div className="min-h-[calc(100vh-6rem)] grid grid-rows-[1fr,auto]">
      {/* 메시지 목록 영역 */}
      <div ref={messageListRef} 
       className="overflow-y-auto flex-1 p-4"
       style={{ maxHeight: "calc(100vh - 12rem)" }} // 동적으로 높이 설정
     >
      
        {messages.map(({role,content }, index) => {
          const avatar =role =='user' ? '????' : '????';

          return (<div
            key={index}
            className={`p-4 rounded-lg mb-2 ${
              role === "user"
                ? "bg-base-100 text-black text-right"
                : "bg-base-200 text-black"
            }`}
          >
            {role === "user" ? <><span className="mr-2">{avatar}</span> {content}</> : 
                        <>
                        <span className="mr-4">{avatar}</span> 
                        <p className="max-w-3xl">{content}</p>
                    </>            
            }
                
          </div>
          );
        })}

          {isPending ?<span className="loading"></span> :null}      
      </div>


      {/* 입력 폼 영역 */}
      <form onSubmit={handleSubmit} className="max-w-4xl pt-4">
        <div className="join w-full">
          <input
            type="text"
            placeholder="메시지 입력..."
            className="input input-bordered join-item w-full"
            value={text}
            onChange={(e) => setText(e.target.value)}
            required
          />
          <button
            type="submit"
            className="btn btn-primary join-item"
            disabled={isPending}
          >
            {isPending ? "전송 중..." : "보내기"}
          </button>
        </div>
      </form>

      {/* 에러 메시지 표시 */}
      {isError && (
        <div className="text-red-500 mt-4">
          <p>에러가 발생했습니다. 다시 시도해 주세요.</p>
        </div>
      )}
    </div>
  );
};

export default Chat;

 

  • useState로 상태 관리:

    • text: 입력 필드의 값을 저장.
    • messages: 대화 메시지를 배열로 저장.
  •  
  • useMutation을 활용한 비동기 요청:

    • mutationFn: API 호출 로직 정의. generateChatResponse로 대화 메시지를 서버로 전송.
    • onSuccess: API 응답이 성공적으로 수신되면 assistant의 메시지를 추가.
    • onError: 오류 발생 시 콘솔에 로그를 출력하고 사용자에게 알림.
  •  
  • 폼 제출 핸들러:

    • 입력된 메시지를 messages 배열에 추가.
    • API 호출을 통해 서버 응답을 받음.
  •  
  • UI 구성:

    • 메시지 목록:
      • user 메시지는 오른쪽 정렬, 파란색 배경.
      • assistant 메시지는 왼쪽 정렬, 회색 배경.
    • 입력 폼:
      • 메시지를 입력하고 전송할 수 있는 UI.
    • 에러 메시지 표시:
      • API 요청 중 오류 발생 시 사용자에게 알림.
    •  
  • Tailwind CSS 스타일:

    • 버튼, 입력 필드, 메시지 UI 등에 Tailwind CSS 클래스를 사용하여 간결한 스타일링.
  •  

 

 

 

 

 

 

 

19. 새로운 투어 페이지(New Tour Page) 개발 가이드

 

 

NewTourPage.tsx

import React from 'react'
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import NewTour from '@/components/NewTour';


const NewTourPage:React.FC = () => {
  const queryClient =new QueryClient();
  
  return (
    //1.dehydrate QueryClient의 상태를 직렬화(Serialize)*
    //2.변환된 데이터를 HydrationBoundary의 state 속성을 통해 클라이언트에 주입.
    <HydrationBoundary state={dehydrate(queryClient)}>
      <NewTour  />
    </HydrationBoundary>
  )


}

export default NewTourPage;

 

 

NewTour.tsx

import React from 'react';

const NewTour: React.FC = () => {
  return (
    <div>NewTour</div>
  );
};

export default NewTour;

 

 

 

다음 단계

  • 데이터베이스와 연동하여 AI 응답을 저장.
  • 투어에 포함될 도시 목록을 보여주는 기능 추가.
  • AI 프롬프트를 정교하게 구성하여 보다 복잡한 질문이 가능하도록 설정.

 

 

핵심 포인트

  • 재사용성: ChatPage를 기반으로 새 페이지를 구성하여 개발 속도를 향상.
  • React Query: 서버 상태 관리 및 직렬화 작업을 통해 클라이언트와 데이터 동기화.
  • 컴포넌트 구조화: NewTourPage와 TourInfo로 나누어 투어 정보를 효과적으로 관리.

 

 

 

 

 

 

 

20. New Tour - Form

 

  • 클라이언트 컴포넌트 선언

    • "use client";를 선언해 React 클라이언트 컴포넌트로 설정.
    • NewTour라는 함수형 컴포넌트를 정의.
  • 폼 구성

    • max-w-2xl, p-4, border, rounded-lg, shadow와 같은 Tailwind CSS 클래스를 활용하여 폼의 크기와 스타일 지정.
    • input 필드는 name 속성을 city와 country로 설정해 FormData에서 식별 가능하도록 구성.
    • button은 type="submit" 속성을 사용해 폼 제출 버튼으로 설정.
  • handleSubmit 함수

    • 폼이 제출되면 handleSubmit이 실행.
    • e.preventDefault()로 기본 폼 동작 방지.
    • FormData 객체를 생성하고 Object.fromEntries()로 입력값을 객체로 변환.
    • 데이터 유효성을 검사하며, 입력값이 없으면 에러 메시지를 출력.
  • TourInfo 컴포넌트

    • 폼 하단에 TourInfo 컴포넌트를 렌더링해 추가 정보를 표시할 수 있도록 준비.

 

FormData API 활용
FormData API를 사용하면 입력값을 쉽게 객체로 변환할 수 있음.

 

"use client";
import React, { FormEvent } from "react";
import TourInfo from "./TourInfo";

const NewTour: React.FC = () => {

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log("NewTour form submitted");

    // FormData를 객체로 변환
    const formData = new FormData(e.currentTarget);
    const destination = Object.fromEntries(formData.entries());

    // 입력 값 확인
    if (!destination.city || !destination.country) {
      console.error("도시와 국가를 모두 입력해 주세요.");
      return;
    }

    console.log("FormData:", destination);
    //alert(`투어가 생성되었습니다: ${destination.city}, ${destination.country}`);
    
  };

  
  return (
    <>
      <form onSubmit={handleSubmit} className="max-w-2xl mx-auto p-4 border rounded-lg shadow">
        <h2 className="mb-4 text-lg font-semibold">사는 지역을 선택해 주세요.</h2>
        <div className="flex space-x-2">
          <input
            type="text"
            className="input input-bordered flex-1"
            name="city"
            placeholder="도시"
            required
          />
          <input
            type="text"
            className="input input-bordered flex-1"
            name="country"
            placeholder="국가"
            required
          />
          <button type="submit" className="btn btn-primary">
            투어 생성
          </button>
        </div>
      </form>

      <div className="mt-16">
        <TourInfo />
      </div>
    </>
  );
};

export default NewTour;

 

 

 

 

 

 

 

 

21.React와 OpenAI API를 활용하여 사용자가

입력한 도시와 국가에 따라 여행 가이드 만들기

 

1. Next.js 15 (앱 라우터)와 서버 캐시

  • Next.js 15의 App Router 방식은 서버 컴포넌트 기반으로 동작하며, 서버에서 데이터를 캐시하고 이를 클라이언트로 전달하는 강력한 캐싱 메커니즘을 제공합니다.
  • **Static Generation (SG)**이나 **Server-side Rendering (SSR)**을 사용할 때, 데이터를 자동으로 빌드 시간 또는 요청 시간에 캐싱할 수 있습니다.
  • 이러한 캐싱은 서버 측에서 이루어지며, 클라이언트에서는 이를 그대로 렌더링만 합니다.

 

2. 클라이언트 캐싱 (React Query)

  • React Query는 클라이언트 측에서 데이터 요청과 캐싱을 관리하기 위한 라이브러리입니다.
  • 클라이언트 측에서는 기본적으로 Next.js가 제공하는 서버 캐시와 별도로 캐싱 관리가 필요하며, 이를 위해 **React Query의 queryClient**를 사용합니다.
  • queryClient는 데이터를 요청한 후 이를 로컬 캐시에 저장하며, 동일한 데이터를 재요청할 때 API 호출을 줄이고 빠르게 데이터를 반환합니다.

 

3.결론: 설명 정리

  • Next.js의 서버 캐시: 앱 라우터(App Router) 방식에서 서버 측 캐싱이 기본적으로 제공됩니다.
  • React Query의 클라이언트 캐시: 클라이언트 측에서 데이터를 캐싱하여 네트워크 요청을 최소화하고, 사용자 경험을 향상시킵니다.

따라서, 아래와 같이 정리하는 것이 더 정확합니다:

"Next.js 15 (앱 라우터)는 서버에서 강력한 캐싱 기능을 제공하지만, 클라이언트 측에서는 기본적으로 캐시가 없으므로 React Query의 queryClient를 활용해 캐시를 관리하며 클라이언트 캐싱을 구현하였습니다."

 

 

 

prisma 는 다음을 참조 https://macaronics.net/m04/react/view/2359

 

 

1. 프로젝트 설정 및 디렉토리 구조

주요 패키지

  • Next.js (v15): React 기반의 SSR/ISR 프레임워크로, 폴더 구조에 따라 페이지를 자동으로 구성합니다.
  • React Query: 서버 상태를 클라이언트에서 효율적으로 관리하며 데이터 패칭, 캐싱, 상태 동기화를 처리합니다.
  • Prisma: 데이터베이스 ORM으로, TypeScript와 함께 사용해 안전한 데이터 모델링과 쿼리를 제공합니다.
  • OpenAI API: 여행지 정보 생성을 위한 AI 모델 호출에 사용됩니다.
  • DaisyUI & TailwindCSS: UI 디자인과 스타일링을 담당합니다.

디렉토리 구조

  • src/app/tours/[tourId]/page.tsx: 특정 여행지의 상세 정보를 표시합니다.
  • src/app/tours/[new-tour]/page.tsx: 새로운 여행지를 생성하는 페이지입니다.
  • src/app/tours/page.tsx: 모든 여행지를 조회하고 검색할 수 있는 페이지입니다.

 

2. 기능 설명 및 구현 과정

(1) 여행 데이터 모델

model Tour{
  id String @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  city String
  country String
  title String
  description String @db.Text
  image String ? @db.Text 
  stops Json
  @@unique([city, country])

}

 

 

toursActions.ts

"use server";
import OpenAI from "openai";
import prisma from "@/utils/db";

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY, // 환경 변수에서 OpenAI API 키를 가져옴
});



export interface ToursParams {
    city: string;
    country: string;
}



interface OpenAIResponse {
    usage: {
      total_tokens: number;
    };
    choices: {
      message: {
        content: string;
      };
    }[];
  }
  

export interface TourData {
  id?: string;
  createdAt?: Date;
  updatedAt?: Date;
  city: string;
  country: string;
  title: string;
  description: string;
  image: string | null;
  stops: string[];
}

export interface TourAiResponseData {
  tour: TourData;
}

export interface GenerateTourResponseData{
  tour: TourData | null;
  tokens: number | string;
}


 
export const generateTourResponse = async ({ city, country }: ToursParams) : Promise<GenerateTourResponseData | null> => {
    const query = `
      1. 정확히 이 ${city}가 ${country} 안에 있는지 확인하세요.
      2. 만약 ${city}와 ${country}가 존재한다면,  ${city}, ${country}에서 할 수 있는 활동의 목록을 만드세요.
      3. 목록이 준비되면 하루 동안의 여행 가이드를 만드세요. 응답은 다음 JSON 형식이어야 합니다:
      {
        "tour": {
          "city": "${city}",
          "country": "${country}",
          "title": "투어의 제목",
          "description": "도시와 투어에 대한 짧은 설명 : 300글자로 작성로 작성할것",
          "stops": ["정류장 이름 내용을  50글자로 작성", "정류장 이름 내용을  50글자로 작성", "정류장 이름 내용을  50글자로 작성"]
        }
      }
      4. "stops" 속성은 정류장 이름 세 개만 포함해야 합니다.
      5. 만약 정확한 ${city}에 대한 정보를 찾을 수 없거나, ${city}가 존재하지 않거나,
         해당 ${city}의 인구가 1명 미만이거나, 또는 해당 ${city}가 ${country}에 위치하지 않은 경우,
         아래와 같이 응답하세요:
         { "tour": null }
      6. 이외의 추가적인 문자는 포함하지 마세요.
    `;
  
    try {
      // OpenAI API 호출
      const response = (await openai.chat.completions.create({
        messages: [
          { role: 'system', content: 'you are a tour guide' },
          { role: 'user', content: query },
        ],
        model: 'gpt-3.5-turbo', // 사용할 모델 이름 지정 (gpt-4, gpt-3.5-turbo 등)
        temperature: 0,
      })) as OpenAIResponse;
  
      // 응답 데이터 검증
      const content = response.choices[0].message.content;
  
      console.log(" AI 응답 :",content);

      
      // JSON 형식 확인 및 파싱
      if (!content) {
        console.error("OpenAI 응답이 비어 있습니다.");
        return null;
      }
  
      let tourData;
      try {
        tourData = JSON.parse(content) as TourAiResponseData;
      } catch (parseError) {
        console.error("JSON 파싱 오류:", parseError);
        console.error("응답 내용:", content);
        return null;
      }
  
      // 결과 값이 null인지 확인
      if (!tourData.tour) {
        console.warn("투어 정보를 찾을 수 없습니다.");
        return null;
      }
  
      return { tour: tourData.tour, tokens: response.usage.total_tokens };
    } catch (error) {
      console.error("OpenAI API 호출 중 오류 발생:", error);
      return null;
    }
  };
  
  


  //등록된 여행이 있는지 확인
  export const getExistingTour = async ({city,country} :ToursParams )=>{
    console.log("* getExistingTour  :",city,country);

    return prisma.tour.findUnique({
      where: {
        city_country: {
          city: city,
          country: country
        }
      }
    })
}


//여행 등록
export const createNewTour =async(tour:TourData) =>{
    console.log(" * createNewTour : ",tour);
  return prisma.tour.create({data: tour});    
}



//여행 리스트 가져오기
export const getAllTours =async(searchTerm:string) =>{
  if(!searchTerm){
    const tours =await prisma.tour.findMany({
      orderBy:{
        city:'asc'
      }
    });

    return tours;
  }

  const tours = await prisma.tour.findMany({
    where:{
      OR:[
        { city:{contains:searchTerm} },
        { country:{contains:searchTerm} },
      ]
    },
    orderBy:{
      city:'asc'
    }
  });

  return tours;
}



export const getSingleTour =async (tourId:string) =>{
    return prisma.tour.findUnique({where:{id:tourId}});

}






 

 

tours/page.tsx

import React from 'react'
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import ToursPage from '@/components/ToursPage';
import { getAllTours } from '@/actions/tours/toursActions';

const AllToursPage:React.FC = async () => {
  const queryClient =new QueryClient();

  // prefetchQuery는 특정 쿼리 키(queryKey)를 기준으로 데이터를 가져와 React Query의 캐시에 저장합니다.
  // 이를 통해 해당 쿼리가 필요한 컴포넌트가 처음 렌더링될 때 데이터가 이미 로드되어 있어 로딩 상태를 생략할 수 있습니다.
  await queryClient.prefetchQuery({
    queryKey: ["tours" , ''],
    queryFn: () =>getAllTours(""),

  });


  return (
    <HydrationBoundary state={dehydrate(queryClient)}>      
      <ToursPage />
    </HydrationBoundary>
  )

}

export default AllToursPage;

 

 

(2) 여행 가이드 생성 (AI 활용)

파일 위치: src/actions/tours/toursActions.ts

주요 함수: generateTourResponse

이 함수는 OpenAI API를 사용해 새로운 여행 정보를 생성합니다.

  1. API 호출 구조: generateTourResponse 함수는 여행지를 입력받아 GPT 모델에 요청을 보냅니다.

 

2 .요청 템플릿

const query = `
  1. 정확히 이 ${city}가 ${country} 안에 있는지 확인하세요.
  2. 만약 ${city}와 ${country}가 존재한다면, ${city}, ${country}에서 할 수 있는 활동의 목록을 만드세요.
  ...
`;

 

  • API 응답 처리: AI가 JSON 형식의 데이터를 반환하며, 이는 TourData 타입으로 파싱됩니다.
  • 결과 검증: 유효하지 않은 데이터(tour: null)는 무시하고, 유효한 데이터를 Prisma를 통해 저장합니다.

 

 

export const createNewTour = async (tour: TourData) => {
  return prisma.tour.create({ data: tour });
};

 

(3) 여행 데이터 조회

파일 위치: src/actions/tours/toursActions.ts

  1. 모든 여행지 조회 (getAllTours):

    • 검색어가 없을 경우 모든 여행지를 반환하며, 검색어가 있을 경우 도시명 또는 국가명에 검색어를 포함한 데이터를 필터링합니다.

 

export const getAllTours = async (searchTerm: string) => {
  return prisma.tour.findMany({
    where: {
      OR: [
        { city: { contains: searchTerm } },
        { country: { contains: searchTerm } },
      ],
    },
    orderBy: { city: 'asc' },
  });
};

 

2. 특정 여행지 조회 (getSingleTour):

  • ID 기반으로 여행지를 조회하며, 존재하지 않을 경우 redirect로 모든 여행지 페이지로 이동합니다.
export const getSingleTour = async (tourId: string) => {
  return prisma.tour.findUnique({ where: { id: tourId } });
};

 

(4) 여행 페이지 구성

파일 위치: src/app/tours/[tourId]/page.tsx

  • 여행 상세 페이지: 여행 정보를 받아와 렌더링합니다.
  • redirect를 사용해 유효하지 않은 ID로 접근 시 목록 페이지로 이동합니다.

 

<div>
  <Link href="/tours" className="btn btn-secondary">여행 페이지로...</Link>
  {tour && <TourInfo tour={tour} />}
</div>

 

파일 위치: src/app/tours/page.tsx

  • 여행 목록 페이지: React Query를 사용해 여행 목록을 불러옵니다.
  • 검색 기능과 로딩 상태를 관리합니다.
const { data, isPending } = useQuery({
  queryKey: ["tours", query],
  queryFn: () => getAllTours(searchValue),
  enabled: !!query,
});

 

(5) 새 여행지 생성

파일 위치: src/app/tours/[new-tour]/page.tsx

  • 기능 요약:

    • 입력된 도시와 국가를 기반으로 새로운 여행지를 생성합니다.
    • 기존 데이터가 존재하면 캐시를 갱신하지 않고 반환합니다.
    • 여행지 생성 후, 캐시를 갱신하고 목록을 업데이트합니다.

 

  • React Query 활용:

 

const { mutate, isPending } = useMutation({
  mutationFn: async (destination: ToursParams) => {
    const existingTour = await getExistingTour(destination);
    if (existingTour) return existingTour;

    const newTour = await generateTourResponse(destination);
    if (newTour?.tour) {
      await createNewTour(newTour.tour);
      queryClient.invalidateQueries({ queryKey: ["tours"] });
      return newTour.tour;
    }
    toast.error("일치하는 도시를 찾을 수 없습니다.");
    return null;
  },
});

 

3. 스타일링

  • DaisyUI: 컴포넌트 스타일은 DaisyUI를 활용해 디자인되었습니다.
  • TailwindCSS: 빠른 스타일 적용과 반응형 디자인을 구현합니다.

4. 실행 및 배포

로컬 개발:

  • npm run dev로 로컬 개발 서버를 실행합니다.

빌드:

  • npx prisma generate로 Prisma 클라이언트를 생성한 뒤 npm run build를 실행해 애플리케이션을 빌드합니다.

5. 주요 학습 포인트

  1. React Query와 SSR/ISR:

    • React Query의 useQuery와 HydrationBoundary를 사용해 클라이언트-서버 간 상태를 동기화합니다.
  2. OpenAI와 Prisma 통합:

    • AI 기반 데이터 생성과 ORM 기반 데이터 저장이 매끄럽게 연결됩니다.
  3. Next.js 폴더 구조:

    • 페이지 기반 라우팅과 서버 컴포넌트/클라이언트 컴포넌트를 적절히 조합합니다.

 

 

 

 

 

 

 

22. openAi 로 이미지 생성하기

1. OpenAI 이미지 생성의 장단점

장점:

  • OpenAI를 활용하여 이미지 생성이 가능하며, 텍스트 프롬프트만으로 원하는 이미지를 자동 생성할 수 있음.
  • 같은 OpenAI 인스턴스를 재사용하기 때문에 별도의 설정이 간단함.
  • 단 몇 줄의 코드로 간단하게 통합 가능.

단점:

  1. 이미지 URL의 유효시간: 생성된 이미지의 URL은 2시간 동안만 유효하므로, 데이터를 장기 저장하려면 별도의 작업 필요.
    • 예: Cloudinary 같은 외부 서비스를 사용해 이미지를 저장.
  2.  
  3. 비용: 이미지 생성은 채팅보다 비용이 더 비쌈.
    • 채팅 비용: 약 $0.002
    • 이미지 비용: 상대적으로 더 비싸지만, 과도한 비용은 아님.
  4. 대안 품질: OpenAI의 이미지 생성 결과가 항상 최적은 아니며, Unsplash API 같은 대안을 사용할 때 더 나은 결과를 얻을 수 있음.

 

2. OpenAI 이미지 생성 동작 방식

  1. 프롬프트 작성:

    • "도시와 국가의 파노라마 뷰"와 같은 프롬프트를 사용.
    • 프롬프트가 구체적일수록 더 나은 결과를 얻음.
  2. 이미지 요청 생성:

    • OpenAI API의 images.generate 메서드를 사용.
    • API 요청 시 필요한 매개변수:
      • prompt: 생성할 이미지 설명.
      • number: 생성할 이미지 개수 (1개 권장).
      • size: 이미지 크기 지정 (예: 1024x1024).
  3. 결과 처리:

    • API 응답에서 URL을 추출.
    • URL이 유효하면 이미지를 표시, 유효하지 않으면 null 반환.

 

3. 코드 구현 흐름

1) 이미지 생성 함수 (generateTourImage)

  • 매개변수: city, country.
  • OpenAI API 호출 후 응답에서 URL 추출.
  • URL을 반환하거나 오류 발생 시 null 반환.

 

 

export const generateTourImage = async (city:string, country:string ) => {
  try {
    const response = await openai.images.generate({
      prompt: `Panoramic view of ${city}, ${country}`,
      n: 1,
      size: "512x512",
    });
    return response?.data[0]?.url || null;
  } catch (error) {
    console.error(error);
    return null;
  }
};

 

 

 

2) 단일 투어 페이지에서 이미지 표시

  • 투어 데이터가 있는 경우 generateTourImage를 호출.
  • 반환된 URL을 Next.js의 <Image> 컴포넌트로 렌더링.

 

 

3) next.config.js에 외부 이미지 도메인 허용

  • Next.js는 외부 도메인의 이미지를 로드할 때 도메인 허용 설정이 필요.
  • OpenAI와 Unsplash 이미지 URL을 허용하는 설정 추가
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */

  images: {
    remotePatterns: [
      { protocol: "https", hostname: "oaidalleapiprodscus.blob.core.windows.net" },
      { protocol: "https", hostname: "images.unsplash.com" },
    ],    
  },

  
};

export default nextConfig;

 

4. 서버 재시작 및 테스트

  • npm run dev로 서버를 재시작.
  • 투어 페이지에서 생성된 이미지를 로드:
    • 도시와 국가 정보가 포함된 투어 카드를 클릭.
    • OpenAI로부터 생성된 이미지를 확인 (로딩 시간이 있을 수 있음).

 

 

 

 

5. 대안: Unsplash API

  • OpenAI 이미지 생성은 간단하지만, Unsplash API를 사용하면 더 높은 품질의 이미지를 무료로 얻을 수 있음.

 

 

 

 

 

 

 

 

 

23. Unsplash API 사용하기

 

 

1. Unsplash 개발자 계정 만들기

Unsplash API를 사용하려면 Unsplash 개발자 계정이 필요합니다.

  • 계정 만들기: Unsplash Developers 사이트에서 가입합니다.
  • API 키 받기: 로그인 후, New Application을 만들어 API 키를 생성합니다. 생성된 Access Key와 Secret Key를 환경 변수로 설정합니다.

 

2. 환경 설정 (API 키 저장)

API 키는 보안상 코드에 직접 노출되지 않도록 환경 변수에 저장하는 것이 좋습니다. 이를 위해 .env 파일을 사용합니다.

  • .env 파일 만들기:
    • 프로젝트 루트 디렉토리에 .env 파일을 생성하고, 아래와 같이 Unsplash API 키를 저장합니다.

 

UNSPLASH_API_ACCESS_KEY=your_unsplash_access_key

 

3. Axios 설치 및 설정

Unsplash API와 통신을 위해 Axios 라이브러리를 사용합니다.

  • Axios 설치:
    • npm install axios 명령어로 Axios를 설치합니다.

npm install axios
 

 

4. API 호출 및 이미지 가져오기

이제 Unsplash API를 사용하여 이미지를 가져오는 방법을 설명합니다.

API 호출 URL

Unsplash의 기본 이미지 검색 API는 다음과 같습니다:

 

https://api.unsplash.com/search/photos?query={검색어}&client_id={API_KEY}

 

 

  • query는 검색하고자 하는 키워드 (예: 도시, 풍경 등).
  • client_id는 위에서 받은 Access Key.

 

unsplashAction.ts

 

import axios from "axios";

// 환경 변수에서 API 키 가져오기
const UNSPLASH_API_ACCESS_KEY = process.env.UNSPLASH_API_ACCESS_KEY;



// 이미지 검색 함수
export async function unsplashActionFetchImages(query :string) {
  try {
    const response = await axios.get(`https://api.unsplash.com/search/photos`, {
      params: {
        query: query,
        client_id: UNSPLASH_API_ACCESS_KEY,  // API 키
        per_page: 1,  // 한 페이지에 1개 이미지만 가져옵니다
      }
    });

    // 검색 결과에서 첫 번째 이미지 선택
    const images = response.data.results;
    if (images.length > 0) {
      return images[0].urls.full;  // 첫 번째 이미지의 URL을 반환
    } else {
      return null;  // 이미지가 없으면 null 반환
    }
  } catch (error) {
    console.error('이미지 검색 중 오류 발생:', error);
    return null;
  }
}


/**
 * // 사용 예시
unsplashActionFetchImages('New York').then((imageUrl) => {
  if (imageUrl) {
    console.log('이미지 URL:', imageUrl);
  } else {
    console.log('이미지를 찾을 수 없습니다.');
  }
});

*/

 

 

5. 검색 결과 처리 및 이미지 표시

위 코드에서는 'New York'이라는 검색어를 사용하여 Unsplash에서 이미지를 검색하고, 첫 번째 이미지를 반환합니다.

이미지 URL 처리

Unsplash의 API 응답에는 이미지의 다양한 크기가 포함됩니다. 예를 들어, urls.full, urls.regular, urls.small 등이 있습니다. 보통 웹에서는 urls.regular나 urls.small을 사용하는 것이 적합합니다.

 

 

/src/app/(dashboard)/tours/page.tsx

import {  getSingleTour, TourData } from '@/actions/tours/toursActions';
import { unsplashActionFetchImages } from '@/actions/tours/unsplashAction';
import TourInfo from '@/components/TourInfo';
import Image from 'next/image';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import React from 'react'

interface SingleTourPageProps {
  params:Promise<{tourId:string}>
}

const SingleTourPage:React.FC<SingleTourPageProps> = async ({ params} ) => {
  const {tourId} =await params;
  const tour =await getSingleTour(tourId) as TourData;

  if(!tour){
    redirect(`/tours`);
  }

  const tourImage =await unsplashActionFetchImages(`${tour.country} ${tour.city}`)
  //const tourImage = await generateTourImage(tour.city, tour.country);
  console.log("넥스트 서버에서 받은  tourImage : ",tourImage);

  return (
    <div>
      <Link href="/tours" className='btn btn-secondary mb-12'>
        여행 페이지로...
      </Link>

      {tourImage ? (
        <Image
          src={tourImage}
          width={300}
          height={300}
          alt={tour.title}
          className="rounded-xl shadow-xl mb-16 h-96 w-96 object-cover"
          priority
        />
      ) : null}

      

       <TourInfo  tour={ tour}   />
    </div>
  )

}

export default SingleTourPage;

 

 

 

6. 주의 사항

  • API 호출 제한: Unsplash API는 일정량의 무료 요청만 제공하므로, 너무 많은 요청을 보내지 않도록 주의해야 합니다. 기본적으로 한 시간에 50,000회 요청이 가능합니다.
  •  
  • API 키 보안: API 키는 중요한 정보이므로 절대 공개되지 않도록 환경 변수나 서버 측에서 처리해야 합니다.
  •  
  • CORS 정책: 프론트엔드에서 Unsplash API를 직접 호출할 경우 CORS(Cross-Origin Resource Sharing) 오류가 발생할 수 있습니다. 이 경우 백엔드를 중간 서버로 두고 API 호출을 서버 측
  •  
  • 에서 처리하는 것이 좋습니다.

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

사람은 다만 나이를 먹는다고 늙는 것이 아니다. 이상(理想)을 저버리기 때문에 늙는 것이다. 사람은 햇수와 더불어 피부에 주름이 가겠지만 세상일에 흥미를 잃지 않는다면 마음에 주름은 가지 않을 것이다. -맥아더

댓글 ( 0)

댓글 남기기

작성