• [Node.js] ์œ ํŠœ๋ธŒ ํด๋ก  02

    2020. 2. 24.

    by. ๋‚˜๋‚˜ (nykim)

    ๐Ÿ‘ฉ‍๐Ÿ’ป ์ด ๊ธ€์€ ๋…ธ๋งˆ๋“œ ์ฝ”๋”์˜ [์œ ํŠœ๋ธŒ ํด๋ก  ์ฝ”๋”ฉ] ๋‚ด์šฉ์„ ๋‹ด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

     

    + ์ด ์‹œ๋ฆฌ์ฆˆ์˜ ์ฒซ ๋ฒˆ์งธ ๊ธ€ ๐Ÿ‘‰ ์œ ํŠœ๋ธŒ ํด๋ก  01

     

     

     

     

    (2-18) Search Controller

    ์ด๋ฒˆ์—๋Š” ๊ฒ€์ƒ‰ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด ๋ด…์‹œ๋‹ค. ๊ฒ€์ƒ‰์ฐฝ์€ ํ—ค๋” ์•ˆ์— ๋“ค์–ด๊ฐˆ ๊ฑฐ๋‹ˆ header.pug๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.

     

    /* header.pug */
    
    header
      h1.title
        a(href=routes.home) #{siteName}
      div.search
        form(action=routes.search, method="get")
          input(type="text", placeholder="๊ฒ€์ƒ‰", name="term")
    ...

     

    form์˜ action์€ ํผ ๋ฐ์ดํ„ฐ๊ฐ€ ๋„์ฐฉํ•  URL์„ ๋งํ•ฉ๋‹ˆ๋‹ค. ์šฐ๋ฆฐ search ํŽ˜์ด์ง€์—์„œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ค„ ๊ฑฐ๋‹ˆ๊นŒ, locals์— ์ €์žฅ๋œ routes.search๋ฅผ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

    form์˜ method๋Š” ํผ ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ค HTTP ๋ฉ”์†Œ๋“œ๋กœ ๋ณด๋‚ผ ๊ฑด์ง€ ์ •ํ•˜๋Š” ๊ฑด๋ฐ์š”, ๊ฒ€์ƒ‰ ๋“ฑ ๋‹จ์ˆœ ์ž‘์—…์—๋Š” ์ฃผ๋กœ GET์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

     

    GET์€ URL์— ํผ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์„œ๋ฒ„๋กœ ์ „๋‹ฌํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ฃผ์†Œ์ฐฝ ๋’ค์— ๋ฐ์ดํ„ฐ๊ฐ€ ์ซ„๋ž˜์ซ„๋ž˜ ๋ถ™์–ด ๋‚˜์˜ต๋‹ˆ๋‹ค.

    ๋˜ ๋ธŒ๋ผ์šฐ์ €์— ์บ์‹œ๋˜์–ด ์ €์žฅ๋˜๋ฉฐ, ๊ธธ์ด์˜ ์ œํ•œ์ด ์žˆ๋‹ค๋Š” ํŠน์ง•๋„ ์žˆ์–ด์š”.

     

    input์— name์„ ๋ช…์‹œํ•˜๋ฉด ํผ ๋ฐ์ดํ„ฐ๋ฅผ ์ฐธ์กฐํ•  ๋•Œ ์“ธ ์ˆ˜ ์žˆ์–ด์š”. ํ•œ ๋ฒˆ ์‹œํ—˜ํ•ด ๋ณด์ฃ .

     

     

     

    ํผ์— ๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅ ํ›„ ์—”ํ„ฐ๋ฅผ ๋™‡ ์ณค๋”๋‹ˆ ์ฃผ์†Œ์ฐฝ ๋’ค์— '?term=๋‚˜๋‚˜' ๋ผ๊ณ  ํ‘œ์‹œ๋˜๋„ค์š”!

     

    ์ž, ๊ทธ๋Ÿผ ์ด์ œ ์ด ๊ฒ€์ƒ‰์–ด๋ฅผ ํ™”๋ฉด์— ๋ณด์—ฌ์ฃผ๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. '๋‚˜๋‚˜๋กœ ๊ฒ€์ƒ‰ํ•œ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค' ์ด๋Ÿฐ ์‹์œผ๋กœ ํ‘œ์‹œํ•  ๊ฑด๋ฐ์š”, ์ด ๊ฒ€์ƒ‰์–ด๋Š” ์–ด๋–ป๊ฒŒ ์•Œ ์ˆ˜ ์žˆ์„๊นŒ์š”? ์Œ... ์ƒ๊ฐํ•ด๋ณด๋‹ˆ ์ด๊ฑด ์‚ฌ์šฉ์ž๊ฐ€ '์š”์ฒญ'ํ•œ ๋‚ด์šฉ์ด๊ตฐ์š”. ๋”ฐ๋ผ์„œ reqest object ์•ˆ์— ๋“ค์–ด์žˆ์„ ๊ฑฐ ๊ฐ™๋„ค์š”.

    ์–ผ๋ฅธ ์ฝ˜์†”์— ์ฐ์–ด ํ™•์ธํ•ด ๋ด…๋‹ˆ๋‹ค.

     

    /* videoConteroller.js */
    
    export const search = (req, res) => {
      console.log(req);
      res.render("search", {
        pageTitle: "Search"
      });
    };

     

    ์ฝ˜์†”์— ์ฐ์—ˆ๋”๋‹ˆ ์–ด๋งˆ์–ด๋งˆํ•œ ์˜ค๋ธŒ์ ํŠธ๋ฅผ ๋˜์ ธ์คฌ์Šต๋‹ˆ๋‹ค (ํฌ์—Œ)

    ๊ฒ€์ƒ‰ํ•ด๋ณด๋‹ˆ req.query ์•ˆ์— { tem: 'HeyNana' } ํ˜•ํƒœ๋กœ ๋“ค์–ด์žˆ์—ˆ๋„ค์š”. ์ด๊ฑธ search.pug์— ๋„ฃ์–ด์ฃผ๋ฉด ๋˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๋„˜๊ธธ ์ด๋ฆ„์€ searchTerm์ด๋ผ๊ณ  ํ• ๊ฒŒ์š”!

     

    /* videoController.js */
    
    const searchTerm = req.query.term;
      res.render("search", {
        pageTitle: "Search",
        searchTerm: searchTerm
      });
    };
    /* search.pug */
    
    extends layouts/main
    block content
      .search__header
        h3 '#{searchTerm}'๋กœ ๊ฒ€์ƒ‰ํ•œ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.

     

    ์œ„์™€ req.qeury.tem์ด๋ผ ์จ๋„ ๋˜์ง€๋งŒ, ๋‚œ ๋ญ”๊ฐ€ ์ข€ ๋” ๊นŒ๋ฆฌํ•˜๊ฒŒ ์“ฐ๊ณ  ์‹ถ๋‹ค! ๊ทธ๋Ÿด ๋• ์ด๋ ‡๊ฒŒ ์จ๋ณผ ์ˆ˜๋„ ์žˆ์–ด์š”.

     

    const { query: { term } } = req;

     

    ๋ฐ”๋กœ ์ด๋ ‡๊ฒŒ์š”! ์—Œ... ์ด๊ฑด ES6 ๋ฌธ๋ฒ•์ธ๋ฐ, ์—„์ฒญ ์‰ฝ๊ฒŒ ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์™”๋„ค์š” ๋œจ๋“ .

    ๊ทธ๋ฆฌ๊ณ  ์ด ๊ฐ€์ ธ์˜จ ์• ํ•œํ…Œ ์ด๋ฆ„์„ ๋ถ™์ผ ์ˆ˜๋„ ์žˆ์–ด์š”.

    const { query: { term : searchTerm } } = req;

     

    ๊ทธ๋ฆฌ๊ณ  searchTerm: searchTerm์ผ ๋•Œ ๊ทธ๋ƒฅ searchTerm์ด๋ผ๊ณ ๋งŒ ์จ๋„ ์•Œ์•„์„œ ์ธ์‹ํ•ฉ๋‹ˆ๋‹ค.

     

    /* videoController.js */
    
    export const search = (req, res) => {
      const { 
        query: { term: searchTerm }
      } = req;
      res.render("search", {
        pageTitle: "Search",
        searchTerm
      });
    };

     

     


     

    (2-19~20) HTML Forms

    ์ด์ œ ํผ์ด ํ•„์š”ํ•œ ๋ช‡ ๊ฐ€์ง€ ํŽ˜์ด์ง€์˜ HTML์„ ์ž‘์—…ํ•ฉ๋‹ˆ๋‹ค. ๋Œ€์• ์• ์ถฉ ๋ผˆ๋Œ€๋งŒ ๋งŒ๋“ค์–ด๋ด์š”!

    ์˜ˆ๋ฅผ ๋“ค๋ฉด ์•„๋ž˜์ฒ˜๋Ÿผ ํ”„๋กœํ•„์„ ์ˆ˜์ •ํ•˜๋Š” ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

     

    /* editProfile.pug */
    
    extends layouts/main
    block content
      .editProfile
        h3 ํ”„๋กœํ•„ ์ˆ˜์ •
        form(action=routes.editProfile, method="post")
          label(for="avatar") Avatar 
          input(type="file", id="avatar", name="avatar")
          input(type="text", name="userName", placeholder="์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”.")
          input(type="email", name="email", placeholder="์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•˜์„ธ์š”." )
          input(type="submit", value="ํ”„๋กœํ•„ ์—…๋ฐ์ดํŠธ")
        a(href=`${routes.users}${routes.changePassword}`) ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝํ•˜๊ธฐ

     

    ์ด ๊ฒฝ์šฐ์—๋Š” ์‚ฌ์šฉ์ž ๋น„๋ฐ€๋ฒˆํ˜ธ์ฒ˜๋Ÿผ ์ค‘์š”ํ•œ ์ •๋ณด๊ฐ€ ๋“ค์–ด์˜ค๋‹ˆ http ํ”„๋กœํ† ์ฝœ์ด POST ๋ฐฉ์‹์ด์—ฌ์•ผ๊ฒ ์ฃ ?

    ๋Œ€์‹  ์šฐ๋ฆฌ๋Š” GET ๋ฐฉ์‹์— ๋Œ€ํ•ด์„œ๋งŒ ์ž‘์„ฑํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ง€๊ธˆ์€ ๋™์ž‘ํ•˜์ง€ ์•Š์„ ๊ฑฐ์—์š”. ์š”๊ฑด ๋‚˜์ค‘์— ์ˆ˜์ •ํ•ด ์ค์‹œ๋‹ค.

     

    ์ด์ œ userController.js์™€ userRouter.js์— ๊ฐ€์„œ ์ด์–ด์ฃผ๋Š” ์ž‘์—…์„ ํ•ฉ๋‹ˆ๋‹ค.

     

    /* userController.js */
    
    export const editProfile = (req, res) => {
      res.render("editProfile", {
        pageTitle: "Edit Your Profile"
      })
    };
    /* userRouter.js */
    
    import { editProfile } from "../controllers/userController";
    userRouter.get(routes.editProfile, editProfile);

     

    ์ด๋Ÿฐ ์‹์œผ๋กœ ํŽ˜์ด์ง€๋“ค์„ ์ญ‰์ญ‰ ์—ฐ๊ฒฐํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๐Ÿ”—

     

    ์ด๋•Œ ์ฃผ์˜ํ•  ์ ! userController.js๋ฅผ ์ž‘์„ฑํ•  ๋•Œ, editProfileuserDetail ์ˆœ์„œ๋Œ€๋กœ get() ์‹œ์ผœ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ๋งŒ์•ฝ userDetail editProfile ์ˆœ๋Œ€๋กœ ํ•œ๋‹ค๋ฉด, /:id ์ž๋ฆฌ์— /edit-profile์ด ๋“ค์–ด๊ฐ„๋‹ค๊ณ  ์ธ์‹ํ•˜๊ธฐ ๋•Œ๋ฌธ์— editProfile ํŽ˜์ด์ง€๊ฐ€ ๋‚˜์˜ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

     

    /* routes.js */
    
    // Users
    const USERS = "/users";
    const USERS_DETAIL = "/:id";
    const EDIT_PROFILE = "/edit-profile";
    const CHANGE_PASSWORD = "/change-password";
    /* userRouter.js */
    
    userRouter.get(routes.editProfile, editProfile);
    userRouter.get(routes.userDetail, userDetail);

     


     

    (2-21~22) Home Controller 

     

    ์ด๋ฒˆ์—” ๊ฐ€์งœ DB ๋ฐ์ดํ„ฐ๋ฅผ ๋งŒ๋“ค์–ด ํ™”๋ฉด์— ๋ณด์—ฌ์ฃผ๋Š” ์ž‘์—…์„ ํ•ด๋ณผ๊ฒŒ์š”!

    db.js ๋ฅผ ์ƒ์„ฑ ํ›„, ์•„๋ž˜์ฒ˜๋Ÿผ ๊ฐ€์งœ ๋ฐ์ดํ„ฐ๋ฅผ ๋งŒ๋“ค์–ด ์ค๋‹ˆ๋‹ค. ๋‚ด์šฉ์€ ์›ํ•˜๋Š” ๋งŒํผ ์ž‘์„ฑํ•˜์„ธ์š”~!

     

    /* db.js */
    
    export const videos = [{
        id: 1881,
        title: 'A Study in Scarlet',
        description: "I should like to meet him.",
        views: 28,
        videoFile: "https://interactive-examples.mdn.mozilla.net/media/examples/flower.webm",
        creator: {
          id: 12345,
          name: "Nana",
          email: "nykim@nykim.net",
        }
    }]

     

    ๊ทธ๋ฆฌ๊ณ  videoController.js ์— importํ•˜์—ฌ ํ™ˆ ํ™”๋ฉด์— ๋„˜๊ฒจ์ค๋‹ˆ๋‹ค.

     

    /* videoController.js */
    
    import {
      videos
    } from "../db";
    
    export const home = (req, res) => {
      res.render("home", {
        pageTitle: "Main",
        videos: videos  //videos ๋ผ๊ณ ๋งŒ ์จ๋„ ๋ผ์š”!
      });
    };
    

     

    ๊ทธ๋ฆฌ๊ณ  home.pug๋กœ ๋„˜์–ด์™€ each๋ฌธ์„ ์จ์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ซ˜์•…- ๋ฟŒ๋ ค์ค๋‹ˆ๋‹ค.

     

    /* home.pug */
    
    extends layouts/main
    block content
      .videos
        h1 video
          each video in videos
            h2=video.title
            p=video.description

     

     

     

    ์—ฌ๊ธฐ์„œ ๋งˆ์ณ๋„ ์ถฉ๋ถ„ํžˆ ๋ฐ์ดํ„ฐ๋ฅผ ์ž˜ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์ง€๋งŒ, HTML ์ž‘์—…์„ ์ข€ ๋” ํšจ์œจ์ ์œผ๋กœ ํ•ด๋ณผ๊ฒŒ์š”.

    ๋ฐ”๋กœ pug์˜ mixin์„ ์ด์šฉํ•ด์„œ์š” ๐Ÿคฉ

     

    SASS์—์„œ mixin์„ ์จ๋ณธ ์ ์ด ์žˆ๋‹ค๋ฉด ์ง์ž‘ํ•  ์ˆ˜ ์žˆ๊ฒ ์ง€๋งŒ, ๋ฐ˜๋ณต๋˜๋Š” ์ฝ”๋“œ๋ฅผ ๋‹ด๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํ•„์š”ํ•  ๋•Œ๋งˆ๋‹ค ๋ถˆ๋Ÿฌ ์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์˜ˆ๋ฅผ ๋“ค์–ด, "<h1>๋‚˜๋Š” ๋‚˜๋‚˜๋‹ค!</h1>"๋ผ๋Š” HTML์„ ๋งค๋ฒˆ ์จ์•ผํ•œ๋‹ค๋ฉด mixin nana()๋ฅผ ๋งŒ๋“ค๋ฉด ๋ฉ๋‹ˆ๋‹ค.

    ํ•œ ๋ฒˆ ์‹œํ—˜ํ•ด๋ณด์ฃ ! ์šฐ์„  views/ ํด๋” ๋‚ด์— mixins/ ํด๋”๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

     

    ๊ทธ๋ฆฌ๊ณ  ์ด๋ฆ„์„ nanaTitle.pug๋กœ ์ง“๊ณ  ์•„๋ž˜์ฒ˜๋Ÿผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

    /* nanaTitle.pug */
    
    mixin nanaTitle()
      h1="๋‚˜๋Š” ๋‚˜๋‚˜๋‹ค!"

     

    ์ด๊ฑธ ์“ฐ๋ ค๋ฉด include๋กœ ๊ฐ€์ ธ์˜ค๋ฉด ๋ฉ๋‹ˆ๋‹ค. home.pug์—์„œ ์‹œํ—˜ํ•ด ๋ณผ๊ฒŒ์š”.

     

    /* home.pug */
    
    extends layouts/main
    include mixins/nanaTitle
    
    block content
      +nanaTitle

     

    ๊ทธ๋ฆฌ๊ณ  localhost:4000์— ์ ‘์†ํ•˜๋ฉด ์ž˜ ๊ฐ€์ ธ์™€์„œ ์“ด ๊ฑธ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

     

     

    ๋ณธ๋ก ์œผ๋กœ ๋Œ์•„๊ฐ€์„œ ์›๋ž˜ ํ•˜๋ ค๋˜ ์ž‘์—…์„ ๊ณ„์† ์ง„ํ–‰ํ•ด ๋ด…์‹œ๋‹ค.

    ํ™ˆ ํ™”๋ฉด์—์„œ๋Š” ๋น„๋””์˜ค ์ •๋ณด๋ฅผ ๋ณด์—ฌ์ค„ ๊ฑด๋ฐ์š”, ์ด๊ฒƒ์— ๋Œ€ํ•œ mixin์ด ์žˆ์œผ๋ฉด ์ž‘์—…์ด ์ฐธ ํŽธํ•  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

    ๊ทธ๋Ÿผ ์•„๊นŒ์ฒ˜๋Ÿผ mixins/ ํด๋” ๋‚ด์— videoBlock.pug๋ฅผ ๋งŒ๋“ค๋ฉด ๋˜๊ฒ ์ฃ ?

     

    /* videoBlock.pug */
    
    mixin videoBlock()
      .videoBlock

     

    ์ด๋ฒˆ์—๋Š” mixin ๋‚ด์— ์ธ์ž๋ฅผ ํ•˜๋‚˜ ๋„ฃ์„ ๊ฑฐ์—์š”. ์ด๋ฆ„์ด video์ธ ๋นˆ ๊ฐ์ฒด๋ฅผ ์™ ๋„ฃ์–ด์ค์‹œ๋‹ค.

     

    /* videoBlock.pug */
    
    mixin videoBlock(video = {})
      .videoBlock

     

    ๊ทธ๋ฆฌ๊ณ  ์ด ๊ฐ์ฒด๋กœ๋ถ€ํ„ฐ ๋ฐ›์•„์˜จ ํ”„๋กœํผํ‹ฐ๋ฅผ ๋ฟŒ๋ ค์ค์‹œ๋‹ค.

     

    /* videoBlock.pug */
    
    mixin videoBlock(video = {})
      .videoBlock
        video(src=video.videoFile)
        h2=video.title
        p=video.description

     

    ์œ„ ์ฝ”๋“œ๊ฐ€ ์ดํ•ด ๋˜์‹œ๋‚˜์š”?

    videoBlock์ด๋ผ๋Š” mixin์— ์ธ์ž๊ฐ€ ํ•˜๋‚˜ ์ž…๋ ฅ๋˜๋ฉด, ๊ทธ ์ธ์ž์˜ ์ด๋ฆ„์„ video๋กœ ํ•˜๊ฒ ๋‹ค๋Š” ๋œป์ž…๋‹ˆ๋‹ค.

    ๊ทธ๋ฆฌ๊ณ  ๊ทธ ์ธ์ž์˜ ์†์„ฑ๋“ค(videoFile, title, description)์„ ๊ฐ€์ ธ์™€ ๋ณด์—ฌ์ฃผ๊ฒ ๋‹ค๋Š” ๊ฑฐ๊ณ ์š”.

     

    ๊ทธ๋Ÿผ ์ด์ œ ์ด HTML์ด ํ•„์š”ํ•œ home.pug๋กœ ๊ฐ€์„œ include ํ•ด์ค๋‹ˆ๋‹ค.

     

    /* home.pug */
    
    extends layouts/main
    include mixins/videoBlock
    
    block content
      .videos
        h1 ๋น„๋””์˜ค ๋ชฉ๋ก
        each item in videos
          +videoBlock({
            title: item.title,
            description: item.description,
            videoFile: item.videoFile
          })

     

    each๋ฌธ์œผ๋กœ videos๋‚ด์— ๋ฃจํ”„๋ฅผ ๋Œ๋ฆฌ๊ณ , ๊ทธ ์ •๋ณด๋ฅผ ์ฐจ๊ณก์ฐจ๊ณก ๊ฐ€์ ธ์™€ mixin์œผ๋กœ ๋„˜๊ฒจ์ฃผ๊ณ  ์žˆ๋Š” ๋ชจ์Šต์ž…๋‹ˆ๋‹ค.

    ๋ฐ์ดํ„ฐ๊ฐ€ ์Šค๋ฌด์Šคํ•˜๊ฒŒ ํ˜๋Ÿฌํ˜๋Ÿฌ ๋“ค์–ด๊ฐ€๋Š” ๋ชจ์Šต์„ ๋ณด๋‹ˆ ๊ธฐ๋ถ„์ด ์ฃ ์•„์š”!

     

    ์•„์ฐธ, ์ €๋Š” ์—ฌ๊ธฐ์„œ ๋ฌธ๋“ '์‘...? ๊ทผ๋ฐ each item in videos์—์„œ videos๋Š” ์–ด๋””์„œ ์˜จ ๊ฑฐ์ง€? include๋„ ์•ˆ ํ–ˆ๋Š”๋ฐ?'๋ผ๊ณ  ์ƒ๊ฐํ–ˆ๋Š”๋ฐ์š”.

    ์ด๋Š” home.pug๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” videoController.js์—์„œ ๋„˜๊ฒจ์คฌ๊ธฐ ๋•Œ๋ฌธ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

    ๋„ต. ์ด ๊ธ€์„ ์ฝ์„ ๋ฏธ๋ž˜์˜ ์ €์—๊ฒŒ ๊นŒ๋จน์ง€ ๋ง๋ผ๊ณ  ์ ๋Š” ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค ๐Ÿ™„

     

    /* videoController.js */
    
    import {
      videos
    } from "../db";
    
    export const home = (req, res) => {
      res.render("home", {
        pageTitle: "Main",
        videos: videos
      });
    };

     

     

    ์—ฌ๊ธฐ๊นŒ์ง€ ํ–ˆ์œผ๋ฉด search.pug๋„ ์ž ๊น ์†๋ณด๊ณ  ๊ฐ‘์‹œ๋‹ค.

    ๊ฒ€์ƒ‰์„ ์™„๋ฃŒํ–ˆ์„ ๋•Œ ํ•ด๋‹น ํŽ˜์ด์ง€์—๋„ ๋น„๋””์˜ค ๋ชฉ๋ก์„ ๋ฟŒ๋ ค์ค„ ๊ฑฐ์ž–์•„์š”? ๋”ฐ๋ผ์„œ videoBlock์„ ๋ถˆ๋Ÿฌ์™€ ์“ธ ์ˆ˜ ์žˆ๊ฒ ๋„ค์š”.

     

     

    /* videoController.js */
    
    export const search = (req, res) => {
      const {
        query: {
          term: searchTerm
        }
      } = req;
      res.render("search", {
        pageTitle: "Search",
        searchTerm,
        videos
      });
    };
    /* search.pug */
    
    extends layouts/main
    include mixins/videoBlock
    
    block content
      .search__header
        h3 '#{searchTerm}'๋กœ ๊ฒ€์ƒ‰ํ•œ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.
      .search__video
        each item in videos
          +videoBlock({
            title: item.title,
            description: item.description,
            videoFile: item.videoFile
          })

     

    ์•„์•„์•„์•„์ฃผ ์ฃ ์•„์š”์˜ค

     


     

    (2-23) Join Controller 

     

     

    ์ด๋ฒˆ์—๋Š” Join ์ชฝ์„ ๊ฑด๋“œ๋ ค ๋ณผ๊ฒŒ์š”. userController.js๋กœ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค.

    ๊ธฐ์กด์— ์žˆ๋˜ Join์„ getJoin์œผ๋กœ ์ด๋ฆ„์„ ๋ฐ”๊พธ๊ณ , POST์— ๋Œ€ํ•œ postJoin๋„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

     

    /* userController.js */
    
    export const getJoin = (req, res) => {
      res.render("join", {
        pageTitle: "Join"
      })
    };
    
    export const postJoin = (req, res) => {
      res.render("join", {
        pageTitle: "Join"
      })
    }

     

    getJoin์€ /join ํŽ˜์ด์ง€๋กœ GET ์š”์ฒญ์ด ์™”์„ ๋•Œ ๋ณด์—ฌ์ฃผ๊ณ ,

    postJoin์€ /join ํŽ˜์ด์ง€ ๋‚ด์—์„œ ํผ ์ „์†ก์ด ์ด๋ค„์กŒ์„ ๋•Œ(=POST ์š”์ฒญ์ด ์™”์„ ๋•Œ) ๋ณด์—ฌ์ฃผ๋ฉด ๋˜๊ฒ ๋„ค์š”.

     

    ์ข‹์•„์š”, ๊ทธ๋Ÿผ globalRouter๋กœ ๊ฐ€์„œ ์—ฐ๊ฒฐ์‹œ์ผœ ์ค๋‹ˆ๋‹ค.

     

    /* globalRouter.js */
    
    import {
      getJoin,
      postJoin
    } from "../controllers/userController";
    
    globalRouter.get(routes.join, getJoin);
    globalRouter.post(routes.join, postJoin);

     

    ํšŒ์›๊ฐ€์ž… ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ, ๊ฐ€์ž…์ด ์™„๋ฃŒ๋๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ณ  ํ™ˆํ™”๋ฉด์œผ๋กœ ๋ณด๋‚ด๋Š” ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

    ์ด๋•Œ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์ž…๋ ฅ ๋‘ ํผ์˜ ๋‚ด์šฉ์ด ๊ฐ™์€์ง€ ์•„๋‹Œ์ง€ ํŒ๋‹จ์ด ํ•„์š”ํ•œ๋ฐ์š”, req์— ๋”ธ๋ ค์˜ค๋Š” ์ •๋ณด๋ฅผ ๋ณด๋ฉด ๋  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

    ๋„ต, ์š” ์ •๋ณด๋Š” req.body์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์š”!

     

    /* userController.js */
    
    export const postJoin = (req, res) => {
      console.log(req.body);
    }

     

     

    ์ด๊ฑธ const bodyInfo = req.body ์ด๋ ‡๊ฒŒ ๊ฐ€์ ธ์™€ bodyInfo.userName, bodyInfo.email์™€ ๊ฐ™์ด ์“ธ ์ˆ˜๋„ ์žˆ์ง€๋งŒ,

    const { body: {} } = req;์ฒ˜๋Ÿผ ํšจ์œจ์ ์œผ๋กœ ์“ฐ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์•„์š”!

     

    /* userController.js */
    
    //...
    
    export const postJoin = (req, res) => {
      const { body: { userName, email, password, password2 } } = req;
    }

     

    ๊ทธ๋ ‡๋‹ค๋ฉด ์ด๊ฑธ ๊ฐ€์ง€๊ณ  password !== password2 ์ธ์ง€ ํ™•์ธํ•˜๋ฉด ๋˜๊ฒ ๋„ค์š”.

    ๋งŒ์•ฝ ์ผ์น˜ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ์—” ์—๋Ÿฌ ์ฝ”๋“œ 400์„ ๋„˜๊ฒจ์ฃผ์ž๊ณ ์š”. res.status()๋ฅผ ์“ฐ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

    ๊ทธ๋Ÿผ ๋ธŒ๋ผ์šฐ์ €๋Š” "์•— ์—๋Ÿฌ๋‹น!"ํ•˜๊ณ  ๋น„๋ฐ€๋ฒˆํ˜ธ ์ €์žฅ ๋ฉ”์‹œ์ง€ ๊ฐ™์€ ๊ฑธ ๋ฌป์ง€ ์•Š์„ ๊ฑฐ์—์š”.

     

    /* userController.js */
    
    //...
    
    export const postJoin = (req, res) => {
      const { body: { userName, email, password, password2 } } = req;
      if (password !== password2) {
        res.status(400)
      }
    }

     

    ๋งŒ์•ฝ ์ผ์น˜ํ•œ๋‹ค๋ฉด home ํ™”๋ฉด์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ํ•ด์ค„ ๊ฑฐ์—์š”. routes๋ฅผ importํ•œ ๋’ค, res.redirect()๋ฅผ ์จ์ค๋‹ˆ๋‹ค.

     

    /* userController.js */
    
    import routes from "../routes";
    
    //...
    
    export const postJoin = (req, res) => {
      const { body: { userName, email, password, password2 } } = req;
      if (password !== password2) {
        res.status(400)
      } else {
        res.redirect(routes.home)
      }
    }

     

    ์ด์ œ ํผ์— ์•Œ๋งž๊ฒŒ ์ž…๋ ฅ ํ›„ ํšŒ์›๊ฐ€์ž… ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ํ™ˆ ํ™”๋ฉด์œผ๋กœ ์Šˆ์Š ์ด๋™ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

     

     

     


     

    (2-24) Log In and User Profile Controller 

     

     

    Join๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋กœ๊ทธ์ธ์— ๋Œ€ํ•œ ๊ฒƒ๋„ getLogin๊ณผ postLogin์œผ๋กœ ๋‚˜๋ˆ„์–ด ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

    /* userController.js */
    
    //...
    
    export const getLogin = (req, res) => {
      res.render("login", {
        pageTitle: "Login"
      })
    };
    
    export const postLogin = (req, res) => {
      console.log("๋กœ๊ทธ์ธ ์„ฑ๊ณต!");
      res.redirect(routes.home);
    }
    
    //...
    /* globalRouter.js */
    
    //...
    
    import {
      //...
      getLogin,
      postLogin
    } from "../controllers/userController";
    
    globalRouter.get(routes.login, getLogin);
    globalRouter.post(routes.login, postLogin);
    
    
    //...

     

     

    ํ•œํŽธ, ๋กœ๊ทธ์ธ ๋์„ ๋•Œ๋Š” ํ—ค๋”์— ๋‹ค๋ฅธ ๋ฉ”๋‰ด๋ฅผ ๋ณด์—ฌ์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์—…๋กœ๋“œ, ํ”„๋กœํ•„ ํ™•์ธ, ๋กœ๊ทธ์•„์›ƒ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ์จ์•ผ ํ•˜๋‹ˆ๊นŒ์š”.

    header.pug๋กœ ๊ฐ€์„œ ์กฐ๊ฑด๋ฌธ์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

     

    /* header.pug */
    
    header
      h1.title
        a(href=routes.home) #{siteName}
      div.search
        form(action=routes.search, method="get")
          input(type="text", placeholder="๊ฒ€์ƒ‰", name="term", value=searchTerm)
      ul
        if !user.isAuthenticated
          li
            a(href=routes.login) Login
          li
            a(href=routes.join) Join
        else
          li
            a(href=`/videos/${routes.upload}`) Upload
          li
            a(href=routes.userDetail) Profile
          li
            a(href=routes.logout) Logout
      hr

     

    ์ €๊ธฐ์— ์žˆ๋Š” user.isAuthenticated๋Š” ์•„์ง ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ณ€์ˆ˜์ž…๋‹ˆ๋‹ค.

    middleware.js๋กœ ๊ฐ€์„œ ์ž„์˜๋กœ ์ž‘์„ฑํ•ด ์ค๋‹ˆ๋‹ค. ๋กœ๊ทธ์•ˆํ•œ ์…ˆ ์น˜์ž๋Š” ๊ฑฐ์ฃ  ๐Ÿ˜›

     

    /* middleware.js */
    
    
    export const localsMiddleware = (req, res, next) => {
      //...
      res.locals.user = {
        isAuthenticated: true,
        id: 12345
      }
      //...
    }

     

     

    ์งœ์ž”- ๋ฉ”๋‰ด๊ฐ€ ๋ฐ”๋€ ๊ฑธ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

     

     

    ๋‹ค๋งŒ ์ด ์ƒํƒœ์—์„œ Profile ๋งํฌ๋ฅผ ๋ˆ„๋ฅด๋ฉด localhost:4000/:id ๋กœ ์ด๋™ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ์•„๊นŒ ์šฐ๋ฆฌ๊ฐ€ ์ž„์˜๋กœ ๋„ฃ์–ด๋’€๋˜ locals.user.id๋ฅผ ์—ฐ๊ฒฐ์‹œํ‚ค๋Š” ์ž‘์—…์„ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

     

    routes.js๋กœ ๊ฐ€์„œ userDetail ๋ถ€๋ถ„์„ ํ•จ์ˆ˜๋กœ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค. ์ธ์ž๊ฐ€ ๋“ค์–ด์˜ค๋ฉด /users/${์ธ์ž}๋ฅผ ๋ฆฌํ„ดํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

     

    /* routes.js */
    
    const routes = {
      //...
      userDetail: (id) => {
        if (id) {
          return `/users/${id}`;
        }
      }
     }

     

    ์ธ์ž๊ฐ€ ์—†๋‹ค๋ฉด, ๊ธฐ์กด๋Œ€๋กœ /:id ๊ฐ€ ๋“ค์–ด๊ฐ€๊ฒŒ ํ•ด์ค์‹œ๋‹ค.

     

    /* routes.js */
    
    const routes = {
      //...
      userDetail: (id) => {
        if (id) {
          return `/users/${id}`;
        } else {
          return USERS_DETAIL;
        }
      }
     }

     

    ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ธ์ž๋กœ "nana"๋ฅผ ๋„˜๊ฒผ์„ ๊ฒฝ์šฐ /users/nana ๋กœ routes๊ฐ€ ์„ค์ •๋˜๊ฒ ์ฃ ?

    locals์— ๋„ฃ์–ด๋‘” user.id์™€ ์—ฐ๊ฒฐ์‹œ์ผœ์ค๋‹ˆ๋‹ค.

     

    /* header.pug */
    
    header
      //...
        li
          a(href=routes.userDetail(user.id)) Profile

     

    ๋งˆ์ง€๋ง‰์œผ๋กœ userRouter.js๋กœ ๊ฐ€์„œ ํ•จ์ˆ˜๋กœ ๋ฐ”๋€ userDetail์„ ์‹คํ–‰์‹œ์ผœ ์ค๋‹ˆ๋‹ค.

     

    /* userRouter.js */
    
    userRouter.get(routes.userDetail(), userDetail);

    ์ด์ œ Profile ๋ฉ”๋‰ด๋ฅผ ๋ˆ„๋ฅด๋ฉด ์•Œ์•„์„œ ์งœ์ž” ์ž˜ ์ด๋™ํ•˜๋Š” ๊ฑธ ๋ณผ ์ˆ˜ ์žˆ์–ด์š”.

     

    ๋ฌผ๋ก  ์ฃผ์†Œ์ฐฝ์— /users/121212์ด๋‚˜ /users/nananana ์ด๋Ÿฐ ์‹์œผ๋กœ ์ง์ ‘ ์ณ๋„ ์—ฌ์ „ํžˆ ์ž˜ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

    ์™œ๋ƒํ•˜๋ฉด ์ธ์ž ์—†์ด userRouter.get(routes.userDetail(), userDetail);๊ฐ€ ์‹คํ–‰๋์œผ๋‹ˆ, /users/:id๋กœ ๋“ค์–ด์˜จ ์…ˆ์ด ๋˜๊ฑฐ๋“ ์š”.

     

     

     


     

     

     

    (2-25) More Controllers

     

     

    userDetail๊ณผ ๋น„์Šทํ•˜๊ฒŒ videosDetail๋„ /:id๋ฅผ ๋ฐ›์œผ๋‹ˆ ์•„๊นŒ์ฒ˜๋Ÿผ ์ˆ˜์ •ํ•˜๋ฉด ๋  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

     

    /* routes.js */
    
    const routes = {
      //...
      videoDetail: (id) => {
        if (id) {
          return `/users/${id}`;
        } else {
          return USERS_DETAIL;
        }
      }
     }
    /* videoRouter.js */
    
    videoRouter.get(routes.videoDetail(), videosDetail);

     

     

    ๊ทธ๋ฆฌ๊ณ  ๋น„๋””์˜ค ๋ชฉ๋ก์„ ๋ˆŒ๋ €์„ ๋•Œ videosDetail๋กœ ์ด๋™ํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

    videoBlock.pug๋กœ ๊ฐ€์„œ a ํƒœ๊ทธ๋กœ ๊ฐ์‹ธ์ฃผ๋Š”๋ฐ, ์ด๋•Œ video.id๋ฅผ ์ธ์ž๋กœ ๋„˜๊ฒจ์ค๋‹ˆ๋‹ค.

    ๊ทธ๋Ÿฌ๋ ค๋ฉด mixin์„ ๋ถˆ๋Ÿฌ์™€ ์“ธ ๋•Œ๋„ id๊ฐ’์„ ๋„˜๊ฒจ์ค˜์•ผ๊ฒ ์ฃ ? id:item.id๋กœ ์ „๋‹ฌํ•˜๋ฉด ๋˜๊ฒ ์Šต๋‹ˆ๋‹ค.

     

    /* videoBlock.pug */
    
    mixin videoBlock(video = {})
      .videoBlock
        a(href=routes.videoDetail(video.id))
          video(src=video.videoFile)
          h2=video.title
          p=video.description
        
    /* home.pug */
    
    extends layouts/main
    include mixins/videoBlock
    
    block content
      .videos
        h1 ๋น„๋””์˜ค ๋ชฉ๋ก
        each item in videos
          +videoBlock({
            title: item.title,
            description: item.description,
            videoFile: item.videoFile,
            id: item.id
          })

     

     

    ์ด๋ฒˆ์—” ๋กœ๊ทธ์•„์›ƒ ์ฐจ๋ก€์ž…๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ์—” ์ฒ˜๋ฆฌ๊ฐ€ ๋๋‚˜๋ฉด ํ™ˆ ํ™”๋ฉด์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์‹œํ‚ค๋ฉด ๋˜๊ฒ ๋„ค์š”.

    ์šฐ์„ ์€ res.redirect(routes.home);๋งŒ ์ž‘์„ฑํ•˜๊ณ , ์ƒ์„ธ ๊ธฐ๋Šฅ์€ ๋’ค์—์„œ ์ž‘์—…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

     

    /* userController.js */
    
    export const logout = (req, res) => {
      //TODO: ๋กœ๊ทธ์•„์›ƒ ๊ธฐ๋Šฅ ๊ตฌํ˜„
      res.redirect(routs.home);
    }

     

     

    ๊ทธ ๋‹ค์Œ์—” ๋‚จ์€ ๊ฑด... ์•„, ๋น„๋””์˜ค ์—…๋กœ๋“œ๊ฐ€ ์žˆ์—ˆ๊ตฐ์š”.

    ์ด๊ฒƒ๋„ GET๊ณผ POST ๋‘˜ ๋‹ค ์š”์ฒญ์ด ์žˆ์„ ํ…Œ๋‹ˆ, ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋‘ ๊ฐœ๋กœ ๋‚˜๋ˆ„์–ด์ค๋‹ˆ๋‹ค.

     

    /* videoController.js */
    
    export const getUpload = (req, res) => {
      res.render("videosUpload", {
        pageTitle: "Upload your video"
      })
    }
    
    export const postUpload = (req, res) => {
      res.render("videosUpload", {
        pageTitle: "Upload your video"
      })
    }
    
    //...
    /* videoRouter.js */
    
    //...
    import {
      getUpload,
      postUpload,
      //...
    } from "../controllers/videoContoller"
    const videoRouter = express.Router();
    
    videoRouter.get(routes.upload, getUpload);
    videoRouter.post(routes.upload, postUpload);
    /* videosUpload.pug */
    
    extends layouts/main
    block content
      .video-upload
        h3 ๋น„๋””์˜ค ์—…๋กœ๋“œ
        form(action=routes.postUpload, method="post")
          input(type="file", name="videoFile")
          input(type="text", name="videoTitle", placeholder="์ œ๋ชฉ")
          input(type="textarea", name="videoDesc", placeholder="์„ค๋ช…")
          input(type="submit", value="์—…๋กœ๋“œ")

     

     

    ์ž, ์ด์ œ postUpload()์— ๋Œ€ํ•œ ๋ณด์™„์ด ํ•„์š”ํ•˜๊ฒ ๊ตฐ์š”.

    ์กฐ๊ธˆ ์ „์— postJoin()์—์„œ req.body๋กœ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์™”๋˜ ๊ฒƒ๊ณผ ๋น„์Šทํ•˜๊ฒŒ ํ•˜๋ฉด ๋ผ์š”.

     

    /* videoContoller.js */
    
    import routes from "../routes";
    
    export const postUpload = (req, res) => {
      const {
        body: {
          videoFile,
          videoTitle,
          videoDesc
        }
      } = req;
      res.redirect(routes.home)
    }
    
    
    //...

     

    ํฌ์œผ ๋ญ”๊ฐ€ ์Šˆ์Šˆ์Š ์ž˜ ์ด๋™ํ•˜๋Š” ๊ฑธ ๋ณด๋‹ˆ ๋ฟŒ๋“ฏํ•˜๋„ค์š” ๐Ÿ˜š

    ์ง€๊ธˆ๊นŒ์ง€๋Š” ๊ฐ€์งœ ๋ฐ์ดํ„ฐ๋กœ ์ž‘์—…์„ ํ–ˆ์ง€๋งŒ, ๋‹ค์Œ๋ถ€ํ„ฐ๋Š” ์ง„์งœ! ๋ ˆ์•Œ! DB๋ฅผ ๊ฐ–๊ณ  ์ž‘์—…ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

     

    ๋‹ค์Œ ๊ธ€์—์„œ ๋งŒ๋‚˜์š”! (์–ด.. ์–ธ์  ๊ฐ€ ์˜ต๋‹ˆ๋‹ค!)

     

     

    ๋Œ“๊ธ€ 3

    • ํ”„๋กœํ•„์‚ฌ์ง„

      1ํŽธ๊ณผ 2ํŽธ ์ •๋ง ์ž˜๋ดค์Šต๋‹ˆ๋‹ค.. ์ •๋ง ์ •๋ฆฌ ์ž˜ํ•ด๋†“์œผ์‹  ๊ฒƒ ๊ฐ™์•„์š”!! ์ •๋ง ๊ฐ์‚ฌํ•ด์š” ๋ณต์žกํ–ˆ๋˜ ๋จธ๋ฆฌ๊ฐ€ ํ•œ ๋ฐฉ์— ์ •๋ฆฌ๋œ ๊ฒƒ๊ฐ™์•„์š”,, 3ํŽธ์€ ์–ธ์ œ์ฏค ์˜ฌ๋ผ์˜ฌ๊นŒ์š”~~?ใ…Žใ…Ž

      • ใ„ด
        ํ”„๋กœํ•„์‚ฌ์ง„

        ๋Œ“๊ธ€ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค! ์ข‹๊ฒŒ ๋ด์ฃผ์…”์„œ ๋„ˆ๋ฌด ๊ธฐ๋ถ„์ด ์ข‹๋„ค์š”~!!
        ์•„์‰ฝ๊ฒŒ๋„ ์ œ๊ฐ€ ์—…๋ฌด์ƒ ๋‹ค๋ฅธ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๋จผ์ € ์ตํ˜€์•ผ ํ•ด์„œ ๋‹ค๋ฅธ ๊ณต๋ถ€๋ฅผ ์ง„ํ–‰ํ•˜๊ณ  ์žˆ์–ด์š”.
        ์‹œ๊ฐ„์ด ๋˜๋ฉด ์–ผ๋ฅธ ์ง„ํ–‰ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค :)

        +
        ๋ธ”๋กœ๊ทธ ๋ฐฉ๋ฌธํ–ˆ๋”๋‹ˆ ์ข‹์€ ๊ธ€์ด ๋งŽ์•„์„œ ์Šฌ์ฉ ์—ผํƒํ–ˆ์Šต๋‹ˆ๋‹คใ…Žใ…Ž ๊ฐ™์ด ํ™”์ดํŒ…ํ•ด์š”!!

      • ใ„ด
        ํ”„๋กœํ•„์‚ฌ์ง„

        ์•„ ๊ทธ๋ ‡๊ตฐ์š” ์•„์‰ฝ์Šต๋‹ˆ๋‹คใ… ใ…  ๊ทธ๋Ÿผ ์ œ๊ฐ€ ๋จผ์ € ์ •๋ฆฌ ํ•ด๋†“์„๊ฒŒ์š”! ๋‚˜์ค‘์— ๋˜ ๋“ค๋Ÿฌ์ฃผ์„ธ์š”~!! ใ…Žใ…Ž ์ €๋Š” ์•„์ง ํ•™์ƒ์ด์ง€๋งŒ.. ๊ฐ™์ด ์—ด์‹ฌํžˆ ํ™งํŒ…!!