본문 바로가기
컴퓨터/웹 : JS

[Node.js](개인 프로젝트) 메뉴 목록 다듬기 및 디자인

by 도도새 도 2023. 4. 23.

메뉴 만들기

 

오늘은 메뉴 목록을 보여주도록 할 것이다. 현재 메뉴는 가격과 이름만 등록 가능하도록 설정이 되어있다. 이것을 확장하여

메뉴는 사진 등록이 가능하도록(필수 X)

설명 추가(필수X)

를 추가하도록 한다.

 

로그인 관련 설정

 

우선 카페 목록을 생성하는 페이지는 로그인이 되어야만 접근이 가능하도록 하기 위해 미들웨어 함수를 하나 생성해준다. 미들웨어 함수는 controller 폴더 내에 위치시킨다.

module .exports .isLoggedIn = async (req , res , next )=>{

   if (!req .isAuthenticated ()){

     req .session .returnTo = await req .originalUrl ;

     return res .redirect ('/user/signin');

   }

   next ();

}

}

}

isAuthenticatedPassport 모듈의 함수로서 현재 로그인 중인지 아닌지를 판단해준다. , 로그인 중이 아니라면 세션의 returnTo에 현재 url주소를 저장하고, 로그인 페이지로 보낸다. 만약 로그인 중이라면 next로 이어서 실행되도록 하였다.

 

그리고 app.js 내에서 아래와 같이 설정하였다.

app .get ('/createCafe', isLoggedIn , (req , res )=>{

   res .render ('createCafe');

})

 

app .post ('/user/signin', passport .authenticate ('local', {falureFlash :true , failureRedirect :'/user/login'}), async (req , res )=>{

  if (req .user ){

   const redirectUrl = req .session .returnTo ||'/cafe';

   req .session .user = req .user ;

   delete req .session .returnTo ;

   res .redirect (redirectUrl );

  }

  else {

   res .redirect ('/user/login');

  }

})

 

그런데 이렇게 하니 req.session.returnTo의 값이 항상 undefined로 나오는 문제가 생겼다.

 

세션 설정에서 문제가 생겼나 한참 헤메다가 정답을 찾았다.

 

app .post ('/user/signin', passport .authenticate ('local', {falureFlash :true ,  keepSessionInfo :true , failureRedirect :'/user/login'}), async (req , res )=>{

이곳, passprot.authenticate 미들웨어 내에 keepSessionInfo:true 부분을 삽입하여 해결하였다.

 

이제 로그인 안한 채로 create를 시도할시 로그인 페이지가 뜨고 로그인시 create페이지로 리다이렉트된다.

 

모델 수정 및 기타작업

 

기존 메뉴와 대표메뉴는 아래처럼 스키마가 구성되어있었다.

  menu : [

   {

    name :String ,

    price :Number ,

   }

  ],

  repreMenu : [

   {

    name :String ,

    //img:String,

    //required:true

   }

  ],

 

여기에 urldescription, filename이라는 항목을 추가한다. filename은 추후 clooudinary 모듈을 이용하여 저장소에서 파일을 삭제할 때 쓰일 것이다.

 

그리고 create페이지에서 이미지 삽입과 설명 삽입이 가능하도록 html css를 살짝 건드려준다.

 

               <div class ="row mb-3">

                 <input class ='col-8 menuName2'id ="menuName"type ="text"name ="menu[name]"style ="margin-bottom: 3px;background-color: #edede9;"readonly >

                 <input class ='col-4'id ='price'placeholder = ''type ="text"name ="menu[price]"style ="margin-bottom: 3px;">

                 <textarea name ="menu[description]"></textarea >

                 <input type ="file">

               </div >

 

textareadtype=“file”input 태그를 추가하였다.

이에 따라 JS코드도 살짝 변경하면 아래와같이 변한다.

생성 페이지

 

이제 입력을 하면 서버측으로 파일이 전송되는데 앞전에 했던 것과 마찬가지로 데이터 베이스에 저장하면 된다.

 

이후 쉬울 거라 생각했던 곳에서 막혔다. 새로운 파일들을 받을 때 단순히 아래처럼 upload.array를 두 번 써주면 될 거라고 생각했는데, 계속해서 오류가 발생하였다.

app .post ('/cafe', upload .array ('photos'), upload .array ('menuPhoto'), async (req , res ) => {

 

내가 파일을 넘겨주는 태그의 형태는 아래와 유사하다.

<input name ="1"type ="file">

<input name ="1"type ="file">

<input name ="3"type ="file">

 

즉 같은 이름의 태그가 2개 다른 이름의 태그가 1(name)이다. 도큐먼트를 읽고 한 태그에서 여러개의 파일을 넘길 경우는 upload.array를 사용하지만, 위처럼 여러 태그에서 여러 값을 넘기는 경우 field를 사용한다는 것을 알게 되었다.

즉 내가 사용할 name을 객체 형태로 인자로 등록해준다.

req.files를 찍어보면 콘솔에는 아래와 같이 나온다.

 

[Object : null prototype ] {

  menuPhoto : [

   {

    fieldname :'menuPhoto',

    originalname :'aa.png',

    encoding :'7bit',

    mimetype :'image/png',

    path :'https://res.cloudinary.com/dprucna6r/image/upload/v1680785825/CafeBara/d0mp6cgyfurt4wdi7xov.png',

    size :4796 ,

    filename :'CafeBara/d0mp6cgyfurt4wdi7xov'

   },

   {

    fieldname :'menuPhoto',

    originalname :'bb.png',

    encoding :'7bit',

    mimetype :'image/png',

    path :'https://res.cloudinary.com/dprucna6r/image/upload/v1680785826/CafeBara/s5b1tzgvyd9cai6suwve.png',

    size :485 ,

    filename :'CafeBara/s5b1tzgvyd9cai6suwve'

   },

   {

    fieldname :'menuPhoto',

    originalname :'100.jpg',

    encoding :'7bit',

    mimetype :'image/jpeg',

    path :'https://res.cloudinary.com/dprucna6r/image/upload/v1680785826/CafeBara/otgvzy7w0qjbe4wfk6r6.jpg',

    size :826 ,

    filename :'CafeBara/otgvzy7w0qjbe4wfk6r6'

   }

  ],

  photos : [

   {

    fieldname :'photos',

    originalname :'aa.png',

    encoding :'7bit',

    mimetype :'image/png',

    path :'https://res.cloudinary.com/dprucna6r/image/upload/v1680785825/CafeBara/dethqylmofowmhugscxe.png',

    size :4796 ,

    filename :'CafeBara/dethqylmofowmhugscxe'

   }

  ]

}

req.files.menuPhotos[0].path같은 식으로 접근이 가능하겠다.

  const images = req .files .photos .map (f =>({url :f .path , filename :f .filename }));

이 코드에서 photos(썸네일) 배열을 돌면서 pathfilename으로 짝지어진 객체를 새로운 배열 형태로 images에 저장하였다.

  const repreMenu = cafeData .repreMenu &&cafeData .repreMenu .map ((repreItem , idx )=>({

   name :repreItem ,

   imgUrl :req .files .menuPhoto [idx ].path ,

   filename :req .files .menuPhoto [idx ].filename

  }))

이렇게해서 이미지와 설명을 리액트 페이지로 넘길 수 있게 되었다.

app .get ('/cafe/api/:id', async (req , res )=>{

  const cafe = await Cafe .findById (req .params .id )

  .populate ('menu')

  .populate ('repreMenu')

  .populate ({

   path :'comment',

   populate : {

    path :'user',

    model :'User',

    select :'-password'// 유저 데이터 중 password 필드는 제외

   }

  });

  res .json (cafe )

})

서버 코드의 이 부분에서 모델의 각 요소를 populate해서 보내주고 있는데, 이대로 보내면 리액트에서 모든 정보를 잘 받을 수 있을 거라고 희망한다.

 

실제로 리액트에서 아래 코드로 데이터를 받아서 콘솔에 찍어보면

  useEffect (() => {

   const fetchData = async () => {

    try {

     const response = await axios .get (`http://localhost:8080/cafe/api/${id }`);

     setCafeData (response .data );

    } catch (error ) {

     console .log (error );

    }

   };

   fetchData ();

  }, [id ]);

이렇게 모델에 추가한 요소들 역시 데이터로 넘겨지고 있음을 알 수 있다.

 

부모에서 useState로 메뉴만 꺼내서 자식에서 props로 넘기도록 한다.

 

결국 부모 요소에서 아래 코드를 작성하고

useEffect (() => {

   const fetchData = async () => {

    try {

     const response = await axios .get (`http://localhost:8080/cafe/api/${id }`);

     setCafeData (response .data );

     setMenus (response .data .menu );

     setRepreMenus (response .data .repreMenu );

    } catch (error ) {

     console .log (error );

    }

   };

   fetchData ();

  }, [id ]);

 

 

    content = <PageThree repreMenus = {repreMenus} menus ={menus }></PageThree >

PageThree라는 컴포넌트로 메뉴 값을 넘겨주었다.

 

  const repreMenuArr = [];

  for (let repre of props .repreMenus ){

   repreMenuArr .push (

&nbs

자식 컴포넌트에서는 빈 배열을 만들고 해당 요소들을 순회하며 이름을 담아 출력해보았다.

정상적으로 이름들이 출력됨을 확인 할 수 있다.

 

이제, 컴포넌트를 꾸며서 넣으면 된다!

 

리액트 파트

 

이런 이미지로 완성하면 어떻가 싶다. 우선 저 이미지대로 적용을 해보자!

 

라고 생각했는데, 내가 지난번에 데이터베이스에 각 설명과 사진을 추가하는 코드를 미처 다 작성하지 않은 모양이었다. 어차피 코드를 한번 개편하려 했으므로 아래와 같이 작성해주었다.

 

  const menuData = req .body .menu ;

 

  let menu ;

  if (Array .isArray (menuData &&menuData .name )){

   menu = menuData &&menuData .name .map ((name , idx )=>({

    name ,

    price :menuData .price [idx ],

    description :menuData .description [idx ]&&menuData .description [idx ],

    imgUrl :req .files .menuPhoto [idx ]&&req .files .menuPhoto [idx ].path ,

    filename :req .files .menuPhoto [idx ]&&req .files .menuPhoto [idx ].filename

   }))

  }

  else {

   menu = menuData ;

  }

 

우선 배열인 경우 map함수로 처리하고 그렇지 않은 경우는 그냥 menuDatamenu에 할당해 추후 모델에 추가한다.

 

받아온 menuData의 형태는

{ name:[이름1, 이름2, 이름3], price:[가격1, 가격2, 가격3]...}

처럼 각 요소가 배열로 된 객체의 형태이다. 그렇기에 map함수를 이용해 이름1, 가격1... 이런 식으로 각 요소별로 매칭시켜 새로운 배열로 전환한다.

새로운 객체 배열로 바꿀 때 파일의 각 인덱스를 따와서 같이 할당한다. 유효성 검사 역시 작성하였디.

 

그러나 현재 문제가 사진을 중간을 비우고 넣을 경우에, 각 사진이 빈 부분에 할당이 될 것이란 것이다. 왜냐하면 넘겨받은 사진 데이터 객체가 배열에 순서대로 들어가긴 하나, 파일을 넘겨주지 않은 것에 대한 것은 배열에 담기기 않기 때문이다.

 

가장 좋은 방법은 name속성을 각각 다르게 하는 것인데, 나는 동적으로 input을 생성하고 있기에 이 방법이 불가능하다. multer 공식문서를 찾아봤지만, 이 방법에 대한 해법은 나타나있지 않다.

 

이 부분은 추후 해결하고 우선 react를 수정하도록 한다.(결국 모든 요소에 대해 사진, 설명을 추가해야하도록 설정하였다. 추후 사용자가 사진을 할당하지 않으면 asset에서 미리 등록 되어있는 빈 이미지와, 빈 문자열을 추가해서 생성하는 것도 하나의 방법이 될 것이다.)

 

  const repreMenuArr = [];

  for (let repre of props .repreMenus ){

   repreMenuArr .push (

    <div className ='row border-bottom mb-3'>

     <div className ="col-4">

      <div className ="mb-3"style ={{fontWeight :'bold', textAlign :'start'}}>{repre .name }</div >

      <div style ={{textAlign :'start'}}>{repre .price .toLocaleString ()} </div >

     </div >

     <div className ='col-5'>

      <div style ={{textAlign :'start'}}>

       {repre .description }

      </div >

     </div >

     <div className ='col-3'>

      <img

         className ="d-block w-100 mb-2"

         src ={repre .imgUrl }

         style ={{objectFit :'contain', width :'100%', height :'100%'}}

         />

     </div >

    </div >

   );

  }

 

부모로부터 데이터를 받아와서 적당히 예쁜 형태로 가공하였다. 이 가공한 데이터를 컴포넌트에 다시 할당하여 또 꾸몄다.

 

  function RepreMenu (){

   if (repreMenuArr ){

    return (

     <div style ={{border :'3px ridge #BFAA9D', padding :'2rem', margin :'1rem', borderRadius :'15px'}}>

      <h3 style ={{marginBottom :'3rem', color :'#bc8a5f'}}>***대표 메뉴***</h3 >

      {repreMenuArr }

     </div >

    )

   }

   else {

    return (

     <div >ss </div >

    )

   }

  }

 

  return (

   <div style ={{ maxWidth :'850px', minWidth :'300px' }}>

    <RepreMenu />

    <Menu />

   </div >

  )

 

마지막으로 이것을 보여주는 코드를 작성하였다.

 

결론적으로 아래 이미지와 같이 완성되었다.

 

여기까지 하면 이제 보여지는 부분은 모두 완성하였고, 이제 안정성을 추가해주는 것과 에러 처리를 해주는 부분만이 남았다.

 

물론 크기 조정(maxWidth, minWidth 조절 등) 및 디자인을 추가하는 작업이 남았다. 그런데 디자인이라는 것을 배워본 적이 없는지라 지금 한 디자인은 무척 촌스럽다는 것을 인정한다.

 

 

일단 배민 비슷하게 만들었다.

 

댓글