React

 

 

 

소스:  https://github.com/braverokmc79/nextjs-pages-router-sample

 

https://nextjs-pages-router-meetup.netlify.app/

 

 

 

 

477. 몽고 DB 로 작업하기

 

1 MongoDB Atlas 계정 생성 및 클러스터 설정

  1. MongoDB Atlas 홈페이지 방문 및 무료 체험 시작: MongoDB Atlas 웹사이트에서 무료 체험 버튼을 클릭하여 계정을 만듭니다.
  2. 클러스터 생성:
    • 기본 설정을 변경하지 않고 진행.
    • Free Tier (M0 Sandbox) 선택.
    • AWS 및 기본 영역 선택.
  3. Network Access 설정:
    • IP 주소 추가 버튼 클릭.
    • 현재 IP 주소 추가.
  4. Database Access 설정:
    • 읽기 및 쓰기 권한을 가진 사용자를 생성

 

 

2. Next.js 프로젝트에서 MongoDB와의 연결 설정

  1. MongoDB 드라이버 설치:
npm install mongodb



 

 

 

3.MongoDB 클러스터와 연결 설정:

  • MongoClient를 사용하여 MongoDB 클러스터에 연결.
  • 연결 문자열에는 MongoDB Atlas에서 제공한 URI를 사용.
  • 연결 시 사용자 이름과 비밀번호 입력.

 

const { MongoClient } = require('mongodb');

async function connectToDatabase() {
  const client = new MongoClient('your_connection_string_here');
  try {
    await client.connect();
    console.log('Connected to database');
    const db = client.db('meetups');
    const meetupsCollection = db.collection('meetups');
    return { client, meetupsCollection };
  } catch (error) {
    console.error('Connection failed', error);
    throw error;
  }
}

 

 

3. 데이터베이스에 데이터 삽입

  1. 데이터 삽입 함수 작성:
    • 데이터베이스에 문서를 삽입하기 위해 insertOne 메서드 사용.
async function insertMeetup(data) {
  const { client, meetupsCollection } = await connectToDatabase();
  try {
    const result = await meetupsCollection.insertOne(data);
    console.log('Meetup inserted:', result.insertedId);
  } finally {
    await client.close();
  }
}

 

 

5. API 경로 설정 및 데이터 삽입 API 구현

  1. API 경로 파일 생성:
    • Node.js Express 서버 또는 Next.js API 경로를 사용하여 데이터 삽입 API 작성.
import { MongoClient } from 'mongodb';

async function handler(req, res) {
  if (req.method === 'POST') {
    const data = req.body;

    const client = new MongoClient('your_connection_string_here');
    await client.connect();
    const db = client.db('meetups');
    const meetupsCollection = db.collection('meetups');

    const result = await meetupsCollection.insertOne(data);
    await client.close();

    res.status(201).json({ message: 'Meetup inserted', result });
  }
}

export default handler;

 

 

6. 클라이언트에서 API 호출

  1. API 호출 코드 작성:
    • 클라이언트 측에서 데이터를 제출하여 API 경로로 요청을 보냄.
async function submitMeetup(meetupData) {
  const response = await fetch('/api/new-meetup', {
    method: 'POST',
    body: JSON.stringify(meetupData),
    headers: {
      'Content-Type': 'application/json',
    },
  });

  const data = await response.json();
  console.log(data);
}

 

 

 

7.요약

MongoDB Atlas 클러스터를 생성하고 next.js 애플리케이션에서 MongoDB 드라이버를 사용하여 클러스터에 연결한 후,

API 경로를 통해 데이터를 삽입할 수 있습니다.

클라이언트 측에서는 API 경로로 요청을 보내어 데이터를 삽입합니다.

이 과정은 MongoDB Atlas 설정, 네트워크 접근 및 데이터베이스 접근 설정,

next.js 프로젝트에서 MongoDB 드라이버 설치 및 사용, 데이터 삽입 함수 작성,

그리고 API 경로 설정 및 클라이언트에서 API 호출로 구성됩니다.

 

 

 

 

 

 

478. API 경로로 Http 요청 보내기

 

1.Meetup  프로젝트

 

1)components/meetups/NewMeetupForm.js

import { useRef } from 'react';

import Card from '../ui/Card';
import classes from './NewMeetupForm.module.css';

function NewMeetupForm(props) {
  const titleInputRef = useRef();
  const imageInputRef = useRef();
  const addressInputRef = useRef();
  const descriptionInputRef = useRef();

  function submitHandler(event) {
    event.preventDefault();

    const enteredTitle = titleInputRef.current.value;
    const enteredImage = imageInputRef.current.value;
    const enteredAddress = addressInputRef.current.value;
    const enteredDescription = descriptionInputRef.current.value;

    const meetupData = {
      title: enteredTitle,
      image: enteredImage,
      address: enteredAddress,
      description: enteredDescription,
    };

    props.onAddMeetup(meetupData);
  }

  return (
    <Card>
      <form className={classes.form} onSubmit={submitHandler}>
        <div className={classes.control}>
          <label htmlFor='title'>Meetup 제목</label>
          <input type='text' required id='title' ref={titleInputRef} />
        </div>
        <div className={classes.control}>
          <label htmlFor='image'>Meetup 이미지</label>
          <input type='url' required id='image' ref={imageInputRef} />
        </div>
        <div className={classes.control}>
          <label htmlFor='address'>주소</label>
          <input type='text' required id='address' ref={addressInputRef} />
        </div>
        <div className={classes.control}>
          <label htmlFor='description'>설명</label>
          <textarea
            id='description'
            required
            rows='5'
            ref={descriptionInputRef}
          ></textarea>
        </div>
        <div className={classes.actions}>
          <button>Meetup 추가</button>
        </div>
      </form>
    </Card>
  );
}

export default NewMeetupForm;

 

 

2)pages/new-meetup/index.js

import NewMeetupForm from "@/components/meetups/NewMeetupForm";
import { useRouter } from "next/router";

function NewMeetupPage() {
  const router=useRouter();
  
  async function addMeetupHandler(enteredMeetupData) {

    const response=await fetch('/api/new-meetup',{
        method: 'POST',
        body: JSON.stringify(enteredMeetupData),
        headers: {
          'Content-Type': 'application/json'
        }
    });

    const data=await response.json();

    console.log(data);

    router.push("/");
    
  }

  return (
    <>
      <NewMeetupForm onAddMeetup={addMeetupHandler} />
    </>
  );
}

export default NewMeetupPage;

 

 

 

3)api/new-meetup.js

import { MongoClient } from "mongodb";
// /api/new-meetup
// POST /api/new-meetup

async function handler(req, res) {
  if (req.method === "POST") {
    const data = req.body;

    const clinet = await MongoClient.connect(
      "mongodb+srv://아이디:비번@mongo-macaronics.y37mjuf.mongodb.net/meetups?retryWrites=true&w=majority&appName=mongo-macaronics",
      { useNewUrlParser: true }
    );
    const db = clinet.db();

    const meetupsCollection = db.collection("meetups");

    const result=await  meetupsCollection.insertOne(data);

    console.log(result);    

    clinet.close();

    res.status(201).json({message:'Meetup is created successfully'});

  }
}

export default handler;

 

 

 

 

 

 

 

 

479. 데이터베이스에서 데이터 가져오기

 

pages/api/db/ConnMongoDB.js

//  pages/api/db/ConnMongoDB.js
import { MongoClient } from "mongodb";


export async function ConnMongoDB(){
    const clinet = await MongoClient.connect(
        "mongodb+srv://아이디:비번@mongo-macaronics.y37mjuf.mongodb.net/meetups?retryWrites=true&w=majority&appName=mongo-macaronics",
        { useNewUrlParser: true }
      );
    const db = clinet.db();
  
    const meetupsCollection = db.collection("meetups");
    return {meetupsCollection, clinet };
}

 

pages/index.js

import MeetupList from "@/components/meetups/MeetupList";
import { ConnMongoDB } from "./api/db/ConnMongoDB";

const DUMMY_MEETUPS = [
  {
    id: "m1",
    title: "A first meetup",
    image:
      "https://cdn.pixabay.com/photo/2024/05/11/06/47/tropical-8754092_1280.jpg",
    address: "Some address 1",
    description: "This is a first meetup",
  },
  {
    id: "m2",
    title: "A second meetup",
    image:
      "https://cdn.pixabay.com/photo/2024/04/08/19/54/coffee-8684315_1280.jpg",
    address: "Some address 2",
    description: "This is a second meetup",
  },
];

function HomePage(props) {
  return <MeetupList meetups={props.meetups} />;
}



export async function getStaticProps() {
  const { meetupsCollection, clinet } = await ConnMongoDB();
  const meetups = await meetupsCollection.find().toArray();
  clinet.close();

  return {
    props: {
      meetups: meetups.map(meetup=>({
          title:meetup.title,
          address:meetup.address,
          //description:meetup.description,
          image:meetup.image,
          id:meetup._id.toString()
      })),
    },
    revalidate: 1,
  };
}

export default HomePage;

 

  • ConnMongoDB 함수로 MongoDB에 연결합니다.
  • meetupsCollection.find().toArray()를 사용하여 모든 meetup 데이터를 배열로 가져옵니다.
  • MongoDB에서 가져온 meetups 데이터를 필요한 속성만 추출하여 props로 설정합니다.
  • revalidate는 1초마다 페이지를 새로 고침하여 최신 데이터를 유지합니다.

전체 동작 방식

  1. Next.js는 getStaticProps 함수를 실행하여 빌드 시 또는 페이지 요청 시 MongoDB에서 데이터를 가져옵니다.
  2. 가져온 데이터를 HomePage 컴포넌트의 props로 전달합니다.
  3. HomePage 컴포넌트는 MeetupList 컴포넌트를 사용하여 meetups 데이터를 렌더링합니다.
  4. revalidate 옵션으로 1초마다 데이터를 새로 고침하여 최신 상태를 유지합니다.

 

 

 

 

 

 

 

 

 

480. Meetup 세 정보 데이터 가져오기 및 페이지 준비하기

 

 

pages/[meetupId]/index.js

 

import MeetupDetail from "@/components/meetups/MeetupDetail";
import { useRouter } from "next/router";
import { ConnMongoDB } from "../api/db/ConnMongoDB";
import { ObjectId } from "mongodb";

function MeetUpDeailPage(props) {
  const router = useRouter();

  if (router.isFallback) {
    return <p>Loading...</p>;
  }

  const { id, image, title, address, description}=  props.meetupData;


  return (    
    <MeetupDetail   
      id={id}   
      title={title}
      image={image}
      address={address}
      description={description}
    />
  );
}

export async function getStaticPaths() {

  const {meetupsCollection, client } =await ConnMongoDB();
  const meetups=await meetupsCollection.find({}, { _id: 1}).toArray();
  client.close();
  
  return {
    fallback: false,         
    paths: meetups.map(meetup => ({params :{meetupId : meetup._id.toString() }}))
  }
  
    // return {
    //   fallback: false,
    // paths: [
    //   {
    //     params: {
    //       meetupId: "m1",
    //     },
    //   },
    //   {
    //     params: {
    //       meetupId: "m2",
    //     },
    //   },
    // ],
  //};
}

export async function getStaticProps(context) {

  const meetupId = context.params.meetupId;
  const {meetupsCollection, client } =await ConnMongoDB();

  const selectedMeetup = await meetupsCollection.findOne({ _id: new ObjectId(meetupId) });

  client.close();
  
  return {
    props:{
      meetupData: {
          id:selectedMeetup._id.toString(),
          title:selectedMeetup.title,
          image:selectedMeetup.image,
          address:selectedMeetup.address,
          description:selectedMeetup.description,
      },
      revalidate: 1,
    }
  }


  // return {    
  //   props: {
  //     meetupData: {
  //       title: "A second meetup",
  //       image:
  //         "https://cdn.pixabay.com/photo/2024/04/08/19/54/coffee-8684315_1280.jpg",
  //       address: "Some address 2",
  //       description: "This is a second meetup",
  //       id: meetupId,
  //     },
  //     revalidate: 1, 
  //   },
  // };
}

export default MeetUpDeailPage;

 

 

구성 요소 및 기능 설명

1. MeetupDetail 컴포넌트

  • Meetup의 세부 정보를 표시하는 컴포넌트입니다. @/components/meetups/MeetupDetail 경로에서 가져옵니다.

 

import MeetupDetail from "@/components/meetups/MeetupDetail";


 

2. useRouter 훅

  • Next.js의 useRouter 훅을 사용하여 라우팅과 관련된 기능을 제공합니다.
  • router.isFallback을 사용하여 빌드 시 경로가 아직 생성되지 않은 경우 로딩 상태를 처리합니다.
import { useRouter } from "next/router";

 

3. ConnMongoDB 함수

  • MongoDB에 연결하는 함수입니다. ../api/db/ConnMongoDB 경로에서 가져옵니다.

 

import { ConnMongoDB } from "../api/db/ConnMongoDB";

 

4. ObjectId 클래스

  • MongoDB에서 문서의 _id 필드를 다루기 위해 mongodb 패키지에서 ObjectId를 가져옵니다.

 

import { ObjectId } from "mongodb";

 

5. MeetUpDeailPage 컴포넌트

  • 특정 meetup의 세부 정보를 렌더링하는 페이지 컴포넌트입니다.
  • router.isFallback을 사용하여 로딩 상태를 처리합니다.

 

function MeetUpDeailPage(props) {
  const router = useRouter();

  if (router.isFallback) {
    return <p>Loading...</p>;
  }

  const { id, image, title, address, description } = props.meetupData;

  return (
    <MeetupDetail
      id={id}
      title={title}
      image={image}
      address={address}
      description={description}
    />
  );
}

 

6. getStaticPaths 함수

  • Next.js의 getStaticPaths를 사용하여 정적 생성할 경로를 정의합니다.
  • MongoDB에서 모든 meetup의 _id를 가져와 paths를 생성합니다.

 

export async function getStaticPaths() {
  const { meetupsCollection, client } = await ConnMongoDB();
  const meetups = await meetupsCollection.find({}, { _id: 1 }).toArray();
  client.close();

  return {
    fallback: false,
    paths: meetups.map(meetup => ({
      params: { meetupId: meetup._id.toString() }
    }))
  };
}

 

  • fallback: false로 설정하면, 지정된 경로 외의 다른 경로는 404 페이지를 표시합니다.
  • 모든 meetup의 _id를 가져와 params 객체로 변환하여 paths를 생성합니다.
  •  

7. getStaticProps 함수

  • Next.js의 getStaticProps를 사용하여 빌드 시 meetup 데이터를 가져와 props로 전달합니다.
  • context.params를 통해 동적 경로 매개변수를 가져옵니다.

 

export async function getStaticProps(context) {
  const meetupId = context.params.meetupId;
  const { meetupsCollection, client } = await ConnMongoDB();

  const selectedMeetup = await meetupsCollection.findOne({ _id: new ObjectId(meetupId) });

  client.close();

  return {
    props: {
      meetupData: {
        id: selectedMeetup._id.toString(),
        title: selectedMeetup.title,
        image: selectedMeetup.image,
        address: selectedMeetup.address,
        description: selectedMeetup.description,
      },
      revalidate: 1,
    }
  };
}

 

  • context.params.meetupId를 사용하여 특정 meetup의 ID를 가져옵니다.
  • MongoDB에서 해당 ID를 가진 meetup 데이터를 찾고, 필요한 정보를 props로 전달합니다.
  • revalidate를 통해 1초마다 페이지를 다시 생성하여 최신 데이터를 유지합니다.

 

전체 동작 방식

  1. 정적 경로 생성 (getStaticPaths):

    • MongoDB에서 모든 meetup의 ID를 가져와 paths를 생성합니다.
    • fallback: false로 설정하여 지정된 경로 외의 다른 경로는 404 페이지를 표시합니다.
  2. 정적 데이터 생성 (getStaticProps):

    • context.params.meetupId를 사용하여 특정 meetup의 데이터를 MongoDB에서 가져옵니다.
    • 가져온 데이터를 props로 설정하여 MeetUpDeailPage 컴포넌트에 전달합니다.
    • 1초마다 페이지를 다시 생성하여 최신 데이터를 유지합니다.
  3. 페이지 렌더링 (MeetUpDeailPage):

    • useRouter를 사용하여 라우팅과 로딩 상태를 처리합니다.
    • props로 전달받은 데이터를 MeetupDetail 컴포넌트에 전달하여 meetup의 세부 정보를 렌더링합니다.

이를 통해 사용자는 특정 meetup의 세부 정보를 볼 수 있으며, 데이터는 MongoDB에서 가져와 최신 상태를 유지합니다.

 

 

 

 

 

 

 

 

481. "head" 메타 데이터 추가하기

 

pages/[meetupId]/index.js

  return (
    <Fragment>
      <Head>
        <title>{title}</title>
        <meta name="description" content={description} />
      </Head>
      <MeetupDetail
        id={id}
        title={title}
        image={image}
        address={address}
        description={description}
      />
    </Fragment>
  );

 

 

 

1. next/head 모듈 임포트

먼저 next/head 모듈을 페이지 컴포넌트에 임포트합니다.

import Head from 'next/head';

 

 

2. Head 컴포넌트 사용

Head 컴포넌트를 사용하여 메타데이터를 추가합니다. 예를 들어, 제목과 메타 태그를 추가하는 방법은 다음과 같습니다.

 

import Head from 'next/head';

function HomePage() {
  return (
    <>
      <Head>
        <title>My Awesome Meetup</title>
        <meta name="description" content="This is a description of my awesome meetup page." />
        <meta property="og:title" content="My Awesome Meetup" />
        <meta property="og:description" content="This is a description of my awesome meetup page." />
        <meta property="og:image" content="/images/meetup-image.jpg" />
        <meta property="og:url" content="https://myawesomewebsite.com/meetup" />
        <meta name="twitter:card" content="summary_large_image" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main>
        {/* 페이지 내용 */}
      </main>
    </>
  );
}

export default HomePage;

 

설명

  1. Head 컴포넌트: Head 컴포넌트 안에 모든 메타데이터 태그를 추가합니다.
  2. <title> 태그: 페이지의 제목을 설정합니다.
  3. 메타 태그들:
    • <meta name="description" ... />: 페이지 설명을 설정합니다.
    • <meta property="og:title" ... />: Open Graph 제목을 설정합니다 (소셜 미디어 공유 시).
    • <meta property="og:description" ... />: Open Graph 설명을 설정합니다.
    • <meta property="og:image" ... />: Open Graph 이미지 URL을 설정합니다.
    • <meta property="og:url" ... />: Open Graph URL을 설정합니다.
    • <meta name="twitter:card" ... />: Twitter 카드 타입을 설정합니다.
  4. 파비콘: <link rel="icon" href="/favicon.ico" />를 사용하여 파비콘을 설정합니다.

이렇게 하면 해당 페이지를 렌더링할 때, Next.js가 <head> 섹션에 지정한 메타데이터를 포함하게 됩니다.

 

동적 메타데이터

동적 메타데이터를 설정하려면 getStaticProps 또는 getServerSideProps를 사용하여 데이터를 가져온 후, 해당 데이터를 Head 컴포넌트에 사용합니다.

import Head from 'next/head';
import { ConnMongoDB } from './api/db/ConnMongoDB';

function MeetupDetailPage({ meetupData }) {
  return (
    <>
      <Head>
        <title>{meetupData.title}</title>
        <meta name="description" content={meetupData.description} />
        <meta property="og:title" content={meetupData.title} />
        <meta property="og:description" content={meetupData.description} />
        <meta property="og:image" content={meetupData.image} />
        <meta property="og:url" content={`https://myawesomewebsite.com/meetup/${meetupData.id}`} />
        <meta name="twitter:card" content="summary_large_image" />
      </Head>
      <main>
        {/* 페이지 내용 */}
      </main>
    </>
  );
}

export async function getStaticProps(context) {
  const meetupId = context.params.meetupId;
  const { meetupsCollection, client } = await ConnMongoDB();
  const selectedMeetup = await meetupsCollection.findOne({ _id: new ObjectId(meetupId) });
  client.close();

  return {
    props: {
      meetupData: {
        id: selectedMeetup._id.toString(),
        title: selectedMeetup.title,
        image: selectedMeetup.image,
        address: selectedMeetup.address,
        description: selectedMeetup.description,
      },
    },
    revalidate: 1,
  };
}

export default MeetupDetailPage;

 

 

이렇게 하면 동적으로 데이터를 가져와 <head> 섹션에 반영할 수 있습니다.

 

 

 

 

 

 

 

 

482. Next.js 프로젝트 배포하기

 

1. Vercel 계정 생성 및 로그인

  1. Vercel 웹사이트로 이동합니다.
  2. 계정이 없으면 "Sign Up" 버튼을 클릭하여 가입합니다.
    • GitHub, GitLab, Bitbucket 계정으로 가입 및 로그인할 수 있습니다.
  3. 이미 계정이 있다면 "Login" 버튼을 클릭하여 로그인합니다.

2. Next.js 프로젝트 준비

Next.js 프로젝트가 없는 경우, 다음 명령어로 새로운 프로젝트를 생성할 수 있습니다:

 

npx create-next-app my-next-app
cd my-next-app

 

3. Git 저장소에 프로젝트 푸시

  1. Git 저장소를 생성합니다 (GitHub, GitLab 또는 Bitbucket에서).
  2. 로컬 프로젝트 디렉토리에서 Git 초기화 및 원격 저장소에 프로젝트를 푸시합니다.

 

git init
git add .
git commit -m "Initial commit"
git remote add origin <your-repository-url>
git push -u origin main

 

4. Vercel에서 새로운 프로젝트 생성

  1. Vercel 대시보드에서 "New Project" 버튼을 클릭합니다.
  2. Git 제공자(GitHub, GitLab, Bitbucket)와 연결하여 배포할 저장소를 선택합니다.

5. 프로젝트 임포트

  1. 배포할 저장소를 선택한 후 "Import" 버튼을 클릭합니다.
  2. Vercel이 자동으로 프로젝트 설정을 감지합니다. 기본적으로 설정된 값을 사용할 수 있습니다.

6. 빌드 설정 확인

Vercel은 Next.js 프로젝트를 자동으로 인식하고 기본 빌드 설정을 적용합니다.

  • Build Command: next build
  • Output Directory: .next

특정 설정이 필요한 경우 이 단계에서 수정할 수 있습니다.

7. 환경 변수 설정 (필요한 경우)

프로젝트에 필요한 환경 변수가 있다면 Vercel 대시보드에서 설정할 수 있습니다.

  1. Vercel 대시보드에서 배포된 프로젝트를 선택합니다.
  2. "Settings" 탭으로 이동합니다.
  3. "Environment Variables" 섹션에서 환경 변수를 추가합니다.

8. 배포 트리거

  1. 프로젝트를 Vercel에 연결하고 저장소에 푸시하면 자동으로 배포가 트리거됩니다.
  2. 모든 푸시마다 Vercel이 프로젝트를 다시 빌드하고 배포합니다.

 

git add .
git commit -m "Update project"
git push origin main

9. 배포 확인

  1. 배포가 완료되면 Vercel 대시보드에서 프로젝트의 URL을 확인할 수 있습니다.
  2. 제공된 URL을 방문하여 Next.js 애플리케이션이 올바르게 작동하는지 확인합니다.

10. 추가 설정 및 관리

  • 도메인 연결: Vercel 대시보드에서 사용자 정의 도메인을 프로젝트에 연결할 수 있습니다.
    • "Domains" 탭에서 도메인을 추가하고 설정을 완료합니다.
  • 배포 미리보기: PR을 생성하면 Vercel이 자동으로 미리보기 URL을 생성하여 변경 사항을 확인할 수 있습니다.
  • 프로젝트 설정: "Settings" 탭에서 프로젝트의 기타 설정을 관리할 수 있습니다.

도메인 설정

  1. Vercel 대시보드에서 프로젝트를 선택합니다.
  2. "Settings" 탭으로 이동하여 "Domains" 섹션으로 스크롤합니다.
  3. "Add Domain" 버튼을 클릭하고 사용자 정의 도메인을 입력합니다.
  4. 도메인 제공자의 DNS 설정에서 Vercel이 제공하는 CNAME 또는 A 레코드를 추가합니다.

지속적인 배포

Vercel은 기본적으로 지속적인 배포를 지원합니다. 코드 변경 사항을 Git 저장소에 푸시하면 Vercel이 자동으로 빌드 및 배포를 트리거합니다.

 

git add .
git commit -m "Update project"
git push origin main

 

이 과정을 통해 Next.js 애플리케이션을 Vercel을 통해 쉽게 배포하고 관리할 수 있습니다.

Vercel의 강력한 기능과 간편한 사용법 덕분에 빠르게 프로덕션 수준의 웹사이트를 운영할 수 있습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

쥐구멍에도 볕들 날이 있다 , 몹시 고생하는 사람도 좋은 때를 만나 운이 트일 날이 있다는 말.

댓글 ( 0)

댓글 남기기

작성