IT & CODE 이야기

2016 소상공인 재능기부 챌린지 본문

CODE/My Project

2016 소상공인 재능기부 챌린지

Karoid 2017. 2. 2. 03:03

소상공인 재능기부 챌린지

  • 개발기간: 2016. 5. 1. ~ 2016. 11. 30(7개월)
  • 연구 과제명: 반응형 웹앱을 통한 소상공인 재능기부
  • 팀명: 배우미 (한양대학교 중앙동아리 HUHS)
  • 개발 인원: 7명 (디자인 1명, 기획 3명, 개발 3명)
  • 개발언어: MongoDB, AngulerJS, Node.js (MEAN Stack)
  • 서버 호스팅: 아마존 웹 서비스

코드 보기

yoyakseo.hwp


프로젝트 소개

이 프로젝트는 정보통신진흥센터에서 주관하는 2016년 SW동아리재능기부챌린지사업을 한양대학교 산학협력단에 위탁받는 형식으로 저희들이 참여하여 만들게 된 프로젝트입니다.

본 사이트의 취지는 소상공인을 돕기 위한 활동으로 시작되었으며 카페 테블릿 전시물과 주문과 쿠폰등록이 가능한 사이트를 개발하였습니다. 또한 동아리원들의 개발 능력 향상을 동시에 도모하고자 책과 장비를 구비하여 새로 배우고 적용하는 계기로 삼았습니다.

테블릿의 경우 안드로이드로 개발되었으며, 카페 내부 음료 홍보용으로 어플리케이션이 제작되었습니다. 그 코드는 여기에 공개되지 않습니다.

사이트의 경우 위에 상술한 내용처럼 MEAN 스택을 이용하여 개발하였고, 단일한 페이지로 모바일과 데스크톱 환경 모두 커버할 수 있는 반응형으로 만들어졌습니다. 사이트는 다음과 같은 기능을 담고 있습니다.

  • 웹사이트 가입/로그인/수정/탈퇴
  • 쿠폰 열람 및 적립(사장님의 )
  • 음료 주문하기
  • 주문 내역 확인 및 취소하기
  • 카페 소개 페이지
  • 카페 음료 메뉴 열람
  • QnA 게시판 (작성, 수정, 삭제, 열람)

  • 관리자 페이지에서 위 사항을 컨트롤 및 확인 가능

내가 개발한 주요 기능

저는 팀원중에서 개발에 대한 부분을 맡았고, 이전에 웹 개발에 대한 짧은 경험을 살려 개발 일정을 주도해나갔습니다.
제가 주로 개발한 기능은 사이트의 백엔드와 프론트 엔드를 개발하였습니다. 그 중에서도 구현한 핵심적인 기능을 프론트엔드와 백엔드를 구별하여 설명드리도록 하겠습니다.

거의 대부분의 프론트 엔드 작업은 혼자 만들었습니다. 웹 개발 경험이 거의 전무한 나머지 두명에 비해 저의 경우 양쪽다 어느정도 가능하였고, 프론트엔드 부분은 전담하여 만들면서, 곧바로 백엔드 작업을 수행하였습니다. 그래서인지 279 커밋중 절반 이상의 커밋을 기록한 것을 확인하실 수 있습니다.

프론트 엔드

반응형 제작

div#top img{
    padding: 2vh 4vh 0vh 2vh;
    margin-left: 2vh;
    width: 7vh;
    height: 9vh;
}
div.wood{
    width: 100%;
    height: 3vh;
    background-image: url("../img/나무사진.jpg");
    background-size: 100% 100%;
}
@media screen and (max-width: 767px){
  div.main {
      color: black;
      height:270px;
  }
  div.main.expand{
    height: calc(100vh - 44vw);
  }
  div.header{
    height: 44vw;
  }

위에 부분은 극히 일부의 코드이지만, CSS를 자유롭게 사용하고 있다는 것을 알 수 있습니다. 페이지 속의 여백과 글자 크기가 화면의 상황에 따라 동적으로 변하게 조정해놓아서, 어떤 디바이스에서도 제대로된 화면을 볼 수 있도록 만들어 놓았습니다.

See in github

메뉴 주문

var serverip = "http://www.marootshop.com/"
var selected_menu = new Array()
/*class menu start*/
function Menu(){}
Menu.prototype.addMenuData = function(item_el){
  addobj = new Object()
  img_url = item_el.children('.item_frame').children('.item_img').attr('src');
  addobj.eq = parseInt(item_el.attr("value"))
  addobj.img_dir = img_url.split(serverip)[1];
  addobj.item_name = item_el.children('.item_name').text();
  addobj.item_price = item_el.children('.item_price').text().split("원")[0];
  addobj._id = item_el.children('input').val();
  addobj.option = 0
  selected_menu.push(addobj)
  subselect = '<div class="submenu">'+
                '<div class="sub_title">'+
                  '<hr>옵션 선택<hr>'+
                '</div>'+
                '<div class="round noadd">no <br>휘핑'+
                '</div>'+
                '<div class="round noadd">no <br>시럽'+
                '</div>'+
                '<div class="round oneline icehot">ice'+
                '</div>'+
                '<div class="round noadd">no <br>샷'+
                '</div>'+
              '</div>'
  $('.selected_menu').append(this.loadMenuData([addobj])).children('.item').last()
  .children('.item_frame').children('.after').before(subselect)
  .parent('.item').children('.item_price').remove().end()
  $('.selected_menu .item').last().on('click',".after",this.xclickevent)
  .children('.item_frame').children('.submenu').on('click', '.round',this.optionclickevent)
}

위의 코드는 javascript 코드입니다. 이번 개발에서는 JS의 OOP적 특성을 극대화 해서 작성을 해 보았습니다. 아직 ES6를 온전히 지원하는 상황이 아니라서 가독성은 좀 떨어지지만 확실히 적은 코드량으로 각각의 객체를 컨트롤 할 수 있었습니다. 메뉴 주문 페이지에 사용된 라이브러리는 Jquery를 사용했습니다. 확실히 데이터 바인딩이 가능한 라이브러리를 썼다면 더 수월하게 만들 수 있었겠다는 아쉬움이 남았습니다.

See in Github

쿠폰 적립

  $('.coupon').click(function(event) {
    txt = "<img src='/cafe/img/coupon_frame.png' class='coupon_frame'>"+
    '<div class="buttons">'+
    '<button type="submit" class="couponin-button">쿠폰 적립</button>'+
    '<button type="submit" class="couponout-button">쿠폰 사용</button>'+
    '</div>'
    popup(txt)
    $.ajax({
      url: '/cafe/get_coupon_data',
    })
    .done(function(count) {
      fillcoupon($('.coupon_frame'),parseInt(count)%10)
      if (count>=10) {
        $('.popup').append('<div class="coupon_left">'+parseInt(parseInt(count)/10)+'</div>')
      }else {
        $('.popup').append('<div class="stamp_left">'+(10-parseInt(count))+'</div>')
        $('.popup .couponout-button').attr("disabled","true")
      }
      $('.popup .couponout-button').html($('.popup .couponout-button').html()+"("+parseInt(parseInt(count)/10)+")")
    })
    .fail(function() {
      console.log("error");
    })
  });

초기 구상대로 프론트엔드와 백엔드를 최대한 분리시키는 방향으로 만들어졌기 때문에 AJAX를 이용한 데이터 교환이 잦았습니다. 그렇기 때문에 js의 비동기적 특성이 걸림돌로 많이 작용했었는데, 콜백 구조를 이용해서 일단 순서의 문제를 해결했습니다. 앞으로 Promise를 이용한 동기 방식을 학습해 볼 생각입니다.

See in Github

구글 맵 API를 이용한 지도 첨부

  <script>
    function initMap() {
      // Create a map object and specify the DOM element for display.
      var map = new google.maps.Map(document.getElementById('map'), {
        center: {lat: 37.575481 , lng: 126.973204},
        scrollwheel: false,
        zoom: 18
      });
      // Create a marker and set its position.
      var marker = new google.maps.Marker({
        map: map,
        position: {lat: 37.575481 , lng: 126.973204},
        title: '마루티샵'
      });
    }
  </script>
  <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBLvsPLai6LwXrH4dxlw0HxkQcBZG3OmvI&callback=initMap"
  async defer></script>

처음으로 google API를 사용해보았습니다. JS상으로 구현하는 것이 상당히 쉬운 편이라서 어렵지 않게 구현하였는데, url 주소와 발급된 토큰이 일치하지 않으면 맵이 보이지 않는 구조로 되어있다는 점이 인상깊었습니다.

See in Github

백엔드

백엔드로 이번에 node.js를 처음 접하게 되었는데, 기존에 JS에 대해서 익숙해서인지 빠르게 습득하고 코드를 작성해 나갔습니다. 하지만 JS의 가독성 문제가 뒤로 갈수록 붉어졌는데, 이는 지속적으로 작성하면서 가독성을 높이는 방향의 기술을 습득해 나가야겠다는 생각을 하였습니다.

크로스 오리진 문제 해결

app.use(function (req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    if (!Object.keys(req.body).length == 0) {
        logger.log("info", "body:" + JSON.stringify(req.body).replace(/\"password\":\"\w*\"/g, "password") + "url:" + JSON.stringify(req.url) + "\n")
        logger.log("info", req.headers['x-real-ip'] || req.connection.remoteAddress)
    }
    next();
});
app.use('/user', require('./User/user')); //로그인 라우팅 연결
app.use('/cafe', require('./Cafe/cafe')); //카페 사이트 라우팅 연결

만든 사이트의 구조상 iframe을 포함하고 있기 때문에, 개발할 시 띄워놓은 서버와 개발 환경사이에 크로스 오리진 문제가 발생하였습니다. 그래서 해결 방법을 찾던중, 다른 사이트에서 iframe을 걸어도 허용해주는 헤더를 패킷에 포함할 수 있다는 사실을 알게 되었습니다. 그리고 그 구조가 node.js에서는 미들웨어라는 개념을 이용해 접근할 수 있다는 것을 배웠습니다.

See in Github

로그인 기능 구현(해시화)

var mongoose = require('mongoose'),
    bcrypt = require('bcryptjs'),
    SALT_WORK_FACTOR = 10,
// these values can be whatever you want - we're defaulting to a
// max of 5 attempts, resulting in a 2 hour lock
    MAX_LOGIN_ATTEMPTS = 5,
    LOCK_TIME = 2 * 60 * 60 * 1000;

var userSchema = new mongoose.Schema({
    username: {type: String, required: true, index: {unique: true}},
    password: {type: String, required: true},
    realname: {type: String, required: true},
    address: {type: String, required: true},
    loginAttempts: {type: Number, required: true, default: 0},
    lockUntil: {type: Number},
    coupon: {type: Number, default: 0}
    //마지막 접속날짜
    //마지막 주문날짜
    //동의여부, 번호, 
});

userSchema.virtual('isLocked').get(function () {
    // check for a future lockUntil timestamp
    return !!(this.lockUntil && this.lockUntil > Date.now());
});

userSchema.pre('save', function (next) {
    var user = this;

    // only hash the password if it has been modified (or is new)
    if (!user.isModified('password')) return next();

    // generate a salt
    bcrypt.genSalt(SALT_WORK_FACTOR, function (err, salt) {
        if (err) return next(err);

        // hash the password using our new salt
        bcrypt.hash(user.password, salt, function (err, hash) {
            if (err) return next(err);

            // set the hashed password back on our user document
            user.password = hash;
            next();
        });
    });
});

사실 node.js에는 Passport라는 아주 좋은 로그인 라이브러리가 존재한다는 것을 알았습니다. 하지만 저희들이 구현해야 할 기능이 한두가지가 아니라는 사실을 깨닫고, 비회원이 로그인해서 주문을 하게끔 해야한다는 사실까지 알았을때 Passport를 모두 이해하고 적용하기보다는 직접 로그인을 작성하는 것이 낫겠다는 판단을 하였습니다. mongoose와 bycript를 이용하여 암호를 해시화하고 salt를 추가하여 저장하도록 구현하였습니다.

See in Github

게시판 기능과 댓글 기능 구현

//Qna CRUD 라우팅
router.get('/QnA_cu/:id?/:redirect_url?', function (req, res) {
    fs.readFile('./Cafe/QnA_cu.html', 'utf8', function (err, data) {
        if (err) {
            logger.log("error",err);
        } else {
            var user = req.session.username
            if (req.params.id && !req.params.redirect_url) {
                //수정하기
                Qna.find({_id: req.params.id}, function (err, documents) {
                    var saved_user = documents[0].username;
                    var logged_in_user = req.session.username
                    var not_logged_in = saved_user == "nonuser" && !logged_in_user;
                    var logged_inNits_my_article = logged_in_user == saved_user && logged_in_user != "nonuser";
                    var isAdmin = logged_in_user == "admin"
                    if (not_logged_in || logged_inNits_my_article || isAdmin) {
                        return res.end(ejs.render(data, {data: documents[0], user: req.session.username}))
                    } else {
                        return res.end("수정 권한이 없습니다") //warning
                    }
                })
            } else if (req.params.id && req.params.redirect_url) {
                //삭제 비밀번호 받기
                res.end(ejs.render(data, {data: [unescape(req.params.redirect_url), req.params.id], user: req.session.username, content:urlencode.decode(req.query.content)}))
            } else {
                //글쓰기
                res.end(ejs.render(data, {data: {}, user: req.session.username}))
            }
        }
    })
});

이전에 ROR에서 사용했던 CRUD의 개념이 확고했기에, 수월하게 구현할 수 있었습니다. 다만 MVC구조가 확실한 Rails 와 다르게 node.js는 정리가 잘 안된다는 느낌을 많이 받았습니다. 코드의 가독성이 떨어지는 것도 피할 수 없었습니다.

See in Github

'CODE > My Project' 카테고리의 다른 글

한학기 커뮤니스 활동을 되돌아보면서  (0) 2017.10.06
Mandalart 기획초안  (0) 2017.05.22
대학생 연합동아리 Communis를 소개합니다  (2) 2017.02.24
Universe War  (0) 2017.02.02
대학축제 프로젝트  (0) 2016.09.21
Comments