Go web 10. Todo 웹

JINSOO PARK·2021년 10월 28일

Go 로 만드는 웹

목록 보기
9/16

Todo 리스트 만들기

프론트엔드 부분

ex) todo.html
*BootStrap 차용

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.css">
    <link rel="stylesheet" href="todo.css" >

    <title>Hello, world!</title>
  </head>
  <body>
    <div class="page-content page-container" id="page-content">
    <div class="padding">
        <div class="row container d-flex justify-content-center">
            <div class="col-lg-12">
                <div class="card px-3">
                    <div class="card-body">
                        <h4 class="card-title">Awesome Todo list</h4>
                        <div class="add-items d-flex"> <input type="text" class="form-control todo-list-input" placeholder="What do you need to do today?"> <button class="add btn btn-primary font-weight-bold todo-list-add-btn">Add</button> </div>
                        <div class="list-wrapper">
                            <ul class="d-flex flex-column-reverse todo-list">
                                <!-- <li>
                                    <div class="form-check"> <label class="form-check-label"> <input class="checkbox" type="checkbox"> For what reason would it be advisable. <i class="input-helper"></i></label> </div> <i class="remove mdi mdi-close-circle-outline"></i>
                                </li>
                                <li class="completed">
                                    <div class="form-check"> <label class="form-check-label"> <input class="checkbox" type="checkbox" checked=""> For what reason would it be advisable for me to think. <i class="input-helper"></i></label> </div> <i class="remove mdi mdi-close-circle-outline"></i>
                                </li>
                                <li>
                                    <div class="form-check"> <label class="form-check-label"> <input class="checkbox" type="checkbox"> it be advisable for me to think about business content? <i class="input-helper"></i></label> </div> <i class="remove mdi mdi-close-circle-outline"></i>
                                </li>
                                <li>
                                    <div class="form-check"> <label class="form-check-label"> <input class="checkbox" type="checkbox"> Print Statements all <i class="input-helper"></i></label> </div> <i class="remove mdi mdi-close-circle-outline"></i>
                                </li>
                                <li class="completed">
                                    <div class="form-check"> <label class="form-check-label"> <input class="checkbox" type="checkbox" checked=""> Call Rampbo <i class="input-helper"></i></label> </div> <i class="remove mdi mdi-close-circle-outline"></i>
                                </li>
                                <li>
                                    <div class="form-check"> <label class="form-check-label"> <input class="checkbox" type="checkbox"> Print bills <i class="input-helper"></i></label> </div> <i class="remove mdi mdi-close-circle-outline"></i>
                                </li> -->
                            </ul>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>


    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js" ></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
    <script src="todo.js"></script>
</body>
</html>

ex) todo.css
*BootStrap 차용

body {
    background-color: #f9f9fa
}

.flex {
    -webkit-box-flex: 1;
    -ms-flex: 1 1 auto;
    flex: 1 1 auto
}

@media (max-width:991.98px) {
    .padding {
        padding: 1.5rem
    }
}

@media (max-width:767.98px) {
    .padding {
        padding: 1rem
    }
}

.padding {
    padding: 5rem
}

.card {
    box-shadow: none;
    -webkit-box-shadow: none;
    -moz-box-shadow: none;
    -ms-box-shadow: none
}

.pl-3,
.px-3 {
    padding-left: 1rem !important
}

.card {
    position: relative;
    display: flex;
    flex-direction: column;
    min-width: 0;
    word-wrap: break-word;
    background-color: #fff;
    background-clip: border-box;
    border: 1px solid #d2d2dc;
    border-radius: 0
}

.pr-3,
.px-3 {
    padding-right: 1rem !important
}

.card .card-body {
    padding: 1.25rem 1.75rem
}

.card-body {
    flex: 1 1 auto;
    padding: 1.25rem
}

.card .card-title {
    color: #000000;
    margin-bottom: 0.625rem;
    text-transform: capitalize;
    font-size: 0.875rem;
    font-weight: 500
}

.add-items {
    margin-bottom: 1.5rem;
    overflow: hidden
}

.d-flex {
    display: flex !important
}

.add-items input[type="text"] {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
    width: 100%;
    background: transparent
}

.form-control {
    border: 1px solid #f3f3f3;
    font-weight: 400;
    font-size: 0.875rem
}

.form-control {
    display: block;
    width: 100%;
    padding: 0.875rem 1.375rem;
    font-size: 1rem;
    line-height: 1;
    color: #495057;
    background-color: #ffffff;
    background-clip: padding-box;
    border: 1px solid #ced4da;
    border-radius: 2px;
    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out
}

.add-items .btn {
    margin-left: .5rem
}

.btn {
    font-size: 0.875rem;
    line-height: 1;
    font-weight: 400;
    padding: .7rem 1.5rem;
    border-radius: 0.1275rem
}

.list-wrapper {
    height: 100%;
    max-height: 100%
}

.add-items {
    margin-bottom: 1.5rem;
    overflow: hidden
}

.add-items input[type="text"] {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
    width: 100%;
    background: transparent
}

.add-items .btn,
.add-items .fc button,
.fc .add-items button,
.add-items .ajax-upload-dragdrop .ajax-file-upload,
.ajax-upload-dragdrop .add-items .ajax-file-upload,
.add-items .swal2-modal .swal2-buttonswrapper .swal2-styled,
.swal2-modal .swal2-buttonswrapper .add-items .swal2-styled,
.add-items .wizard>.actions a,
.wizard>.actions .add-items a {
    margin-left: .5rem
}

.rtl .add-items .btn,
.rtl .add-items .fc button,
.fc .rtl .add-items button,
.rtl .add-items .ajax-upload-dragdrop .ajax-file-upload,
.ajax-upload-dragdrop .rtl .add-items .ajax-file-upload,
.rtl .add-items .swal2-modal .swal2-buttonswrapper .swal2-styled,
.swal2-modal .swal2-buttonswrapper .rtl .add-items .swal2-styled,
.rtl .add-items .wizard>.actions a,
.wizard>.actions .rtl .add-items a {
    margin-left: auto;
    margin-right: .5rem
}

.list-wrapper {
    height: 100%;
    max-height: 100%
}

.list-wrapper ul {
    padding: 0;
    text-align: left;
    list-style: none;
    margin-bottom: 0
}

.list-wrapper ul li {
    font-size: .9375rem;
    padding: .4rem 0;
    border-bottom: 1px solid #f3f3f3
}

.list-wrapper ul li:first-child {
    border-bottom: none
}

.list-wrapper ul li .form-check {
    max-width: 90%;
    margin-top: .25rem;
    margin-bottom: .25rem
}

.list-wrapper ul li .form-check label:hover {
    cursor: pointer
}

.list-wrapper input[type="checkbox"] {
    margin-right: 15px
}

.list-wrapper .remove {
    cursor: pointer;
    font-size: 1.438rem;
    font-weight: 600;
    width: 1.25rem;
    height: 1.25rem;
    line-height: 20px;
    text-align: center
}

.list-wrapper .completed {
    text-decoration: line-through;
    text-decoration-color: #3da5f4
}

.list-wrapper ul li .form-check {
    max-width: 90%;
    margin-top: .25rem;
    margin-bottom: .25rem
}

.list-wrapper ul li .form-check,
.list-wrapper ul li .form-check .form-check-label,
.email-wrapper .mail-sidebar .menu-bar .profile-list-item a .user .u-name,
.email-wrapper .mail-sidebar .menu-bar .profile-list-item a .user .u-designation,
.email-wrapper .mail-list-container .mail-list .content .sender-name,
.email-wrapper .message-body .attachments-sections ul li .details p.file-name,
.settings-panel .chat-list .list .info p {
    text-overflow: ellipsis;
    overflow: hidden;
    max-width: 100%;
    white-space: nowrap
}

.form-check {
    position: relative;
    display: block;
    margin-top: 10px;
    margin-bottom: 10px;
    padding-left: 0
}

.list-wrapper ul li .form-check,
.list-wrapper ul li .form-check .form-check-label,
.email-wrapper .mail-sidebar .menu-bar .profile-list-item a .user .u-name,
.email-wrapper .mail-sidebar .menu-bar .profile-list-item a .user .u-designation,
.email-wrapper .mail-list-container .mail-list .content .sender-name,
.email-wrapper .message-body .attachments-sections ul li .details p.file-name,
.settings-panel .chat-list .list .info p {
    text-overflow: ellipsis;
    overflow: hidden;
    max-width: 100%;
    white-space: nowrap
}

.form-check .form-check-label {
    min-height: 18px;
    display: block;
    margin-left: 1.75rem;
    font-size: 0.875rem;
    line-height: 1.5
}

.form-check-label {
    margin-bottom: 0
}

.list-wrapper input[type="checkbox"] {
    margin-right: 15px
}

.form-check .form-check-label input {
    position: absolute;
    top: 0;
    left: 0;
    margin-left: 0;
    margin-top: 0;
    z-index: 1;
    cursor: pointer;
    opacity: 0;
    filter: alpha(opacity=0)
}

input[type="radio"],
input[type="checkbox"] {
    box-sizing: border-box;
    padding: 0
}

.list-wrapper ul li .form-check,
.list-wrapper ul li .form-check .form-check-label,
.email-wrapper .mail-sidebar .menu-bar .profile-list-item a .user .u-name,
.email-wrapper .mail-sidebar .menu-bar .profile-list-item a .user .u-designation,
.email-wrapper .mail-list-container .mail-list .content .sender-name,
.email-wrapper .message-body .attachments-sections ul li .details p.file-name,
.settings-panel .chat-list .list .info p {
    text-overflow: ellipsis;
    overflow: hidden;
    max-width: 100%;
    white-space: nowrap
}

.form-check .form-check-label input[type="checkbox"]+.input-helper:before {
    content: "";
    width: 18px;
    height: 18px;
    border-radius: 2px;
    border: solid #405189;
    border-width: 2px;
    -webkit-transition: all;
    -moz-transition: all;
    -ms-transition: all;
    -o-transition: all;
    transition: all;
    transition-duration: 0s;
    -webkit-transition-duration: 250ms;
    transition-duration: 250ms
}

.form-check .form-check-label input[type="checkbox"]+.input-helper:before,
.form-check .form-check-label input[type="checkbox"]+.input-helper:after {
    position: absolute;
    top: 0;
    left: 0
}

.form-check .form-check-label input[type="checkbox"]+.input-helper:after {
    -webkit-transition: all;
    -moz-transition: all;
    -ms-transition: all;
    -o-transition: all;
    transition: all;
    transition-duration: 0s;
    -webkit-transition-duration: 250ms;
    transition-duration: 250ms;
    font-family: Material Design Icons;
    opacity: 0;
    filter: alpha(opacity=0);
    -webkit-transform: scale(0);
    -ms-transform: scale(0);
    -o-transform: scale(0);
    transform: scale(0);
    content: '\F12C';
    font-size: .9375rem;
    font-weight: bold;
    color: #ffffff
}

.form-check .form-check-label input[type="checkbox"]+.input-helper:before,
.form-check .form-check-label input[type="checkbox"]+.input-helper:after {
    position: absolute;
    top: 0;
    left: 0
}

.form-check .form-check-label input[type="checkbox"]:checked+.input-helper:before {
    background: #405189;
    border-width: 0
}

.form-check .form-check-label input[type="checkbox"]+.input-helper:before {
    content: "";
    width: 18px;
    height: 18px;
    border-radius: 2px;
    border: solid #405189;
    border-width: 2px;
    -webkit-transition: all;
    -moz-transition: all;
    -ms-transition: all;
    -o-transition: all;
    transition: all;
    transition-duration: 0s;
    -webkit-transition-duration: 250ms;
    transition-duration: 250ms
}

.form-check .form-check-label input[type="checkbox"]+.input-helper:after {
    font-family: FontAwesome;
    content: "\f095";
    display: inline-block;
    padding-right: 3px;
    vertical-align: middle;
    color: #fff
}

.text-primary,
.list-wrapper .completed .remove {
    color: #405189 !important
}

.list-wrapper .remove {
    cursor: pointer;
    font-size: 1.438rem;
    font-weight: 600;
    width: 1.25rem;
    height: 1.25rem;
    line-height: 20px;
    text-align: center
}

.ml-auto,
.list-wrapper .remove,
.mx-auto {
    margin-left: auto !important
}

.mdi-close-circle-outline:before {
    content: "\F15A"
}

.list-wrapper ul li {
    font-size: .9375rem;
    padding: .4rem 0;
    border-bottom: 1px solid #f3f3f3
}

.mdi:before {
    font-family: FontAwesome;
    content: "\f00d";
    display: inline-block;
    padding-right: 3px;
    vertical-align: middle;
    font-size: .756em;
    color: #405189
}

.list-wrapper ul {
    padding: 0;
    text-align: left;
    list-style: none;
    margin-bottom: 0
}

.flex-column-reverse {
    flex-direction: column-reverse !important
}

.d-flex,
.loader-demo-box,
.distribution-chart-legend .distribution-chart,
.distribution-chart-legend .distribution-chart .item,
.list-wrapper ul li,
.email-wrapper .mail-sidebar .menu-bar .profile-list-item a,
.email-wrapper .mail-sidebar .menu-bar .profile-list-item a .user,
.email-wrapper .mail-list-container .mail-list .details,
.email-wrapper .message-body .attachments-sections ul li .thumb,
.email-wrapper .message-body .attachments-sections ul li .details .buttons,
.lightGallery .image-tile .demo-gallery-poster,
.swal2-modal,
.navbar .navbar-menu-wrapper .navbar-nav,
.navbar .navbar-menu-wrapper .navbar-nav .nav-item.nav-profile,
.navbar .navbar-menu-wrapper .navbar-nav .nav-item.dropdown .navbar-dropdown .dropdown-item {
    display: flex !important
}

ex) todo.js
부트스트랩 차용

(function($) {
    'use strict';
    $(function() {
        var todoListItem = $('.todo-list');
        var todoListInput = $('.todo-list-input');
        $('.todo-list-add-btn').on("click", function(event) {
            event.preventDefault();
    
            var item = $(this).prevAll('.todo-list-input').val();
    
            if (item) {
                $.post("/todos", {name:item}, addItem);
            //todoListItem.append("<li><div class='form-check'><label class='form-check-label'><input class='checkbox' type='checkbox' />" + item + "<i class='input-helper'></i></label></div><i class='remove mdi mdi-close-circle-outline'></i></li>");
            todoListInput.val("");
            }
        });
        // add 버튼을 클릭했을때 todoListItem에 어팬드 해준다.
        var addItem = function(item) {
            if (item.completed) { // compelted가 true 일경우 줄긋고 체크박스 체크
                todoListItem.append("<li class='completed'"+ " id='" + item.id + "'><div class='form-check'><label class='form-check-label'><input class='checkbox' type='checkbox' checked='checked' />" + item.name + "<i class='input-helper'></i></label></div><i class='remove mdi mdi-close-circle-outline'></i></li>");
            } else { // compelted가 false 일경우
                todoListItem.append("<li "+ " id='" + item.id + "'><div class='form-check'><label class='form-check-label'><input class='checkbox' type='checkbox' />" + item.name + "<i class='input-helper'></i></label></div><i class='remove mdi mdi-close-circle-outline'></i></li>");
            }
        };
        // 데이터를 /todos 경로로 호출
        $.get('/todos', function(items) {
            items.forEach(e => { 
                addItem(e)// 각 아이탬을 돌면서 add해준다.
            });
        });
        
        todoListItem.on('change', '.checkbox', function() {
            var id = $(this).closest("li").attr('id');
            var $self = $(this);
            var complete = true;
            if ($(this).attr('checked')) {
                complete = false;
            }
            $.get("complete-todo/"+id+"?complete="+complete, function(data){
                if (complete) {
                    $self.attr('checked', 'checked');
                } else {
                    $self.removeAttr('checked');
                }
        
                $self.closest("li").toggleClass('completed');
            })
        });
    
        todoListItem.on('click', '.remove', function() {
            var id = $(this).closest("li").attr('id');
            var $self = $(this);
            $.ajax({
                url: "todos/" + id,
                type: "DELETE",
                success: function(data) {
                    if (data.success) {
                        $self.parent().remove();
                    } // 성공 했을 때 삭제
                }
            })
        });
    
    });
    })(jQuery);

백엔드 부분

ex) app.go

package app

import (
	"net/http"
	"strconv"
	"time"

	"github.com/gorilla/mux"
	"github.com/unrolled/render"
)

var rd *render.Render

type Todo struct {
	ID        int       `json:"id"`
	Name      string    `json:"name"`
	Completed bool      `json:"completed"`
	CreatedAt time.Time `json:"created_at"`
}

// Todo의 데이터를 가지고 있는 인메모리 스트럭처
var todoMap map[int]*Todo

func indexHandler(w http.ResponseWriter, r *http.Request) {
	http.Redirect(w, r, "/todo.html", http.StatusTemporaryRedirect)
}

// 리스트를 받아서 제이슨형태로 넘겨줌
func getTodoListHandler(w http.ResponseWriter, r *http.Request) {
	list := []*Todo{}
	for _, v := range todoMap {
		// map의 인덱스 만큼 반복
		// 키는 받지않고 value만 받음
		list = append(list, v)
		// list에 v값을 어팬드
	}
	rd.JSON(w, http.StatusOK, list)
	// json형태로 반환
}

func addTodoHandler(w http.ResponseWriter, r *http.Request) {
	name := r.FormValue("name")                // 리퀘스트의 키로 name을 읽음
	id := len(todoMap) + 1                     // map의 갯수만큼 id
	todo := &Todo{id, name, false, time.Now()} // 포인터형 구조체 Todo를 생성
	todoMap[id] = todo                         // todo의 데이터를 맵에 넣음
	rd.JSON(w, http.StatusOK, todo)            // json형태로 반환

}

type Success struct {
	Success bool `json:"success"`
}

func removeTodoHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id, _ := strconv.Atoi(vars["id"])
	if _, ok := todoMap[id]; ok {
		delete(todoMap, id)
		rd.JSON(w, http.StatusOK, Success{true})
	} else {
		rd.JSON(w, http.StatusOK, Success{false})
	}
}

func completeTodoHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id, _ := strconv.Atoi(vars["id"])
	complete := r.FormValue("complete") == "true"
	// value 값에서 complete의 값을 받음
	if todo, ok := todoMap[id]; ok { // 맵에 있는지 확인
		todo.Completed = complete                // 맵에 있는경우 변경
		rd.JSON(w, http.StatusOK, Success{true}) //제이슨 값으로 변환후 true 반환
	} else {
		rd.JSON(w, http.StatusOK, Success{false})
	} // 맵에 없을 경우 false 반환
}

func addTestTodos() {
	todoMap[1] = &Todo{1, "Buy a milk", false, time.Now()}
	todoMap[2] = &Todo{2, "Exercise", true, time.Now()}
	todoMap[3] = &Todo{3, "Home Work", false, time.Now()}
}

func MakeHandler() http.Handler {
	todoMap = make(map[int]*Todo)
	addTestTodos()
	rd = render.New()
	r := mux.NewRouter()

	r.HandleFunc("/todos", getTodoListHandler).Methods("GET")
	r.HandleFunc("/todos", addTodoHandler).Methods("POST")
	r.HandleFunc("/todos/{id:[0-9]+}", removeTodoHandler).Methods("DELETE")
	r.HandleFunc("complete-todo/{id:[0-9]+", completeTodoHandler)
	r.HandleFunc("/", indexHandler)

	return r
}

ex) main.go

package main

import (
	"net/http"
	"web10/app"

	"github.com/urfave/negroni"
)

func main() {
	m := app.MakeHandler()
	n := negroni.Classic()
	n.UseHandler(m)

	http.ListenAndServe(":3000", n)
}

todo 추가하기

끝난 일은 체크

지난일은 삭제

profile
개린이

0개의 댓글