CSS&JS/⚡Thinkers

달력 만들기(3) + To Do List

arancia_ 2022. 12. 15. 17:12

미리보기 : https://ohikmyeong.github.io/cdtd-2022/

 

달력만들기-2022

할 일을 등록해보세요!

ohikmyeong.github.io

깃허브 주소 : https://github.com/OhIkmyeong/cdtd-2022

 

GitHub - OhIkmyeong/cdtd-2022: 달력+To do list

달력+To do list. Contribute to OhIkmyeong/cdtd-2022 development by creating an account on GitHub.

github.com

소요시간 : 이틀 (전과정 포함 약 8~9시간)

 

HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>달력만들기-2022</title>
    <link rel="stylesheet" type="text/css" href="./css/main.css"/>
    <script src="./js/main.js" type="module"></script>
</head>
<body>
 
<div id="wrap-cdtd">
    <!-- TO DO LIST -->
    <section id="wrap-td">
        <!-- 선택한 날짜 출력 -->
        <article id="td-date">
            <!-- 오늘일시 -->
            <div id="ic-today">TODAY</div>
 
            <!--  -->
            <p id="td-date-year">YYYY</p>
            <p id="td-date-day">DAY</p>
            <span id="td-date-month">MM</span><span id="td-date-date">DD</span>
        </article>
 
 
        <!-- 카운트 -->
        <article id="td-count">
            <!-- 카운트-텍스트 -->
            <div id="td-count-text">
                할 일을 등록해보세요!
            </div><!-- td-count-text -->
 
            <!-- 카운트-막대그래프 -->
            <div id="td-count-graph">
                <div id="td-count-graph-bar">
                    <div id="td-count-graph-num">0%</div>
                </div>
            </div><!-- td-count-graph -->
        </article><!-- td-count -->
 
        <!-- 작성된 할 일 목록 출력 -->
        <ul id="td-list"></ul><!-- td-list -->
 
        <!-- 할 일 입력 -->
        <form id="form-add-td">
            <input type="text" id="ipt-add-td" title="할 일 입력" placeholder="Write something TO DO" autocomplete="off"/>
            <button id="btn-add-td" title="할 일 등록">SUBMIT</button>
        </form><!-- form-td-list -->
    </section><!-- wrap-td -->
 
    <!-- CALENDAR -->
    <section id="wrap-cd">
        <!-- 달력 상단 -->
        <article id="cd-head">
            <button id="btn-mm-prev" title="지난달"></button>
            <div>
                <p id="cd-head-yy">YYYY</p>
                <p id="cd-head-mm-num">MM</p>
                <p id="cd-head-mm-eng">MM-ENG-FULL</p>
            </div>
            <button id="btn-mm-next" title="다음달"></button>
        </article><!-- cd-head -->
 
        <!-- 달력 -->
        <table id="cd">
            <caption>달력</caption>
            <colgroup>
                <col style="width:calc(100% / 7);">
                <col style="width:calc(100% / 7);">
                <col style="width:calc(100% / 7);">
                <col style="width:calc(100% / 7);">
                <col style="width:calc(100% / 7);">
                <col style="width:calc(100% / 7);">
                <col style="width:calc(100% / 7);">
            </colgroup>
            <thead>
                <tr>
                    <th>SUN</th>
                    <th>MON</th>
                    <th>TUE</th>
                    <th>WED</th>
                    <th>THU</th>
                    <th>FRI</th>
                    <th>SAT</th>
                </tr>
            </thead>
            <tbody>
            </tbody>
        </table>
    </section><!-- wrap-cd -->
</div><!-- wrap-cdtd -->
 
</body>
</html>
cs

CSS는 생략

 

main.js

1
2
3
4
5
6
7
8
9
10
import { Calendar } from "./Calendar.js";
import { ToDoList } from "./ToDoList.js";
 
(async function(){
    const TODO = new ToDoList();
    const CAL = new Calendar();
    
    await TODO.init(CAL);
    CAL.init(TODO);
})();
cs

FN.js

1
2
3
4
5
6
7
8
9
10
11
/**
 * Date 객체를 받아서 String으로 반환한다
 * @param date
 * @returns yyyy-mm-dd
*/
export function return_date_string(date){
    const yyyy = date.getFullYear();
    const mm = String(date.getMonth() + 1).padStart(2,"0");
    const dd = String(date.getDate()).padStart(2,"0");
    return `${yyyy}-${mm}-${dd}`;
}//return_date_string
cs

ToDoList.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
import { return_date_string } from "./FN.js";
 
/** 📌TO DO LIST 관련 */
export class ToDoList {
    /** TO DO LIST 관련 */
    constructor(){
        this.$icToday = document.getElementById('ic-today');
 
        this.$year = document.getElementById('td-date-year');
        this.$month = document.getElementById('td-date-month');
        this.$date = document.getElementById('td-date-date');
        this.$day = document.getElementById('td-date-day');
 
        this.$countText = document.getElementById('td-count-text');
        this.$countBar = document.getElementById('td-count-graph-bar');
        this.$countNum = document.getElementById('td-count-graph-num');
 
        this.$tdList = document.getElementById('td-list');
 
        this.$form = document.getElementById('form-add-td');
        this.$formIpt = document.getElementById('ipt-add-td');
        this.$formBtn = document.getElementById('btn-add-td');
 
        this.today = null;
        this.currDate = null;
        this.days = ["SUN","MON","TUE","WED","THU","FRI","SAT"];
        this.data = null;
 
        this.CAL = null;
    }//constructor
 
    /** 
     * 최초 화면 로드시  
     * @param {Class}CAL 캘린더 클래스 받아오기
    */
    async init(CAL){
        //캘린더 클래스 받아오기
        this.CAL = CAL;
 
        //데이터 설정
        this.data = await fetch('./json/data.json').then(res=>res.json()).then(json=>json.todo);
        
        //오늘 날짜 설정
        this.today = return_date_string(new Date());
 
        //화면 변경 총괄
        this.display_all(this.today);
 
        //form에 이벤트 추가
        this.$form.addEventListener('submit',(e)=>{
            //등록시
            this.on_submit(e);
            
            //카운트 수정
            this.display_count();
 
            //캘린더에 표시 수정
            this.CAL.change_rmn_done(this.currDate);
        });
 
        //to do list에 이벤트 추가(삭제 가능하도록);
        this.$tdList.addEventListener('click',(e)=>{
            switch(e.target.tagName){
                case "BUTTON" : {
                    /* 삭제버튼 */
                    //아이템 지우고 데이터에서도 삭제
                    this.delete_td_item(e.target);
 
                    //카운트 수정
                    this.display_count();
 
                    //캘린더에 표시 수정
                    this.CAL.change_rmn_done(this.currDate);
                }break;
                
                case "INPUT" : {
                    /* 체크박스 */
                    //데이터에서 수정
                    this.modify_data(e.target);
 
                    //카운트 수정
                    this.display_count();
 
                    //캘린더에 표시 수정
                    this.CAL.change_rmn_done(this.currDate);
                }break;
            }//switch
        });
    }//init
 
    /**
     * 화면 바꾸기 총괄
     * @param {String}dateString yyyy-mm-dd
     */
    display_all(dateString){
        //현재 보여줄 날짜 변경
        this.change_curr_date(dateString);
                
        //날짜 표시 변경
        this.display_date_info(dateString);
 
        //오늘 아이콘 표시
        this.is_today(dateString);
 
        //오늘의 할 일 목록 표시
        this.fill_td_list(dateString);
 
        //오늘의 할 일 성과 표시
        this.display_count();
    }//display_all
 
 
    /**
     * TO DO LIST에 표시되는 날짜를 변경합니다
     * @param {String}dateString yyyy-mm-dd
     */
    change_curr_date(dateString){
        this.currDate = dateString
    }//change_curr_date
 
    /** 
     * 날짜 변경 
     * @param {String}dateString yyyy-mm-dd
    */
    display_date_info(dateString){
        const [yyyy,mm,dd] = dateString.split('-');
        this.$year.textContent = yyyy;
        this.$month.textContent = mm;
        this.$date.textContent = String(dd).padStart(2,"0");
 
        const day = this.days[new Date(dateString).getDay()];
        this.$day.textContent = day;
    }//display_date_info
 
    /** 
     * 오늘이면 아이콘 표시 
     * @param {String}dateString yyyy-mm-dd
     * */
    is_today(dateString){
        const bool = dateString == this.today;
        this.$icToday.classList.toggle("off"!bool);
    }//is_today
 
    /** 
     * form에 내용 등록시
     */
    on_submit = e => {
        e.preventDefault();
        const val = this.$formIpt.value.trim();
        if(!val) return;
 
        //ipt내용 비우고 다시 포커스 주기
        this.$formIpt.value = "";
        this.$formIpt.focus();
 
        //리스트에 아이템 추가
        const $item = this.make_td_item(val,false);
        this.$tdList.appendChild($item);
        this.$tdList.scrollTo({
            top : this.$tdList.scrollHeight,
            behavior : "smooth"
        })
 
        //this.data에 추가
        this.add_data(val,false);
    }//on_submit
 
    /**
     * 날짜로부터 할 일들을 받아와서 todoList에 채워줍니다
     * @param {String} dateString yyyy-mm-dd 
     */
    fill_td_list(dateString){
        //비우고
        this.$tdList.innerHTML = '';
 
        //DOM 추가
        const items = this.data[dateString];
        if(!items?.lengthreturn;
        const $frag = document.createDocumentFragment();
        items.forEach(item => {
            const {val,done} = item;
            $frag.appendChild(this.make_td_item(val,done));
        });
        this.$tdList.appendChild($frag);
    }//fill_td_list
 
    /**
     * 할 일 등록
     * @param {String}val
     * @param {Boolean}done
     * @returns {DOM} LI.td-list-item
     */
    make_td_item(val,done = false){
        //DOM 추가
        const $li = document.createElement('LI');
        const $lbl = document.createElement('LABEL');
        const $chk = document.createElement('INPUT');
        const $ic = document.createElement('SPAN');
        const $txt = document.createElement('P');
        const $btnDel = document.createElement('BUTTON');
 
        $li.classList.add('td-list-item');
        $lbl.classList.add('lbl-chk');
        $chk.type = "checkbox";
        $chk.checked = done;
        $ic.classList.add('ic');
        $txt.classList.add('td-list-item-txt');
        $txt.textContent = val;
        $btnDel.classList.add('btn-del');
        $btnDel.title = "삭제";
 
        $lbl.appendChild($chk);
        $lbl.appendChild($ic);
        $lbl.appendChild($txt);
        $li.appendChild($lbl);
        $li.appendChild($btnDel);
        return $li;
    }//make_td_item
 
    /**
     * 할 일 아이템 삭제
     * @param {DOM} $btnDel
     */
    delete_td_item($btnDel){
        const $li = $btnDel.parentElement;
        const val = $li.getElementsByClassName('td-list-item-txt')[0].textContent;
 
        //DOM 제거
        this.$tdList.removeChild($li);
 
        //data에서도 삭제 
        this.remove_data(val);
    }//delete_td_item
 
    /**
     * 할 일 계산
     */
    display_count(){
        const $$li = Array.from(this.$tdList.children);
        const len = $$li.length;
 
 
        //오늘의 할 일 없을 때
        if(!len){
            this.$countText.textContent = '할 일을 등록해보세요!';
            this.$countBar.style.width = '0%';
            this.$countNum.textContent = '0%';
            return;
        }
 
        const done = $$li.filter($li => {
            const $ipt = $li.querySelector('INPUT');
            if($ipt.checked) return $li;
        }).length;
 
        const per = parseInt((done / len).toFixed(2* 100);
 
        this.$countText.innerHTML = per == 100 ? '👍 <strong>대단해요! 할 일을 전부 해냈습니다!</strong>' :`<strong>${len}</strong>개의 할 일 중 <strong>${done}</strong>개를 달성하였습니다.`;
        this.$countBar.style.width = `${per}%`;
        this.$countNum.textContent = `${per}%`;
    }//display_count
 
    /**
     * this.data에 새 아이템을 등록합니다
     * @param {String}val value
     * @param {Boolean}done 
     */
    add_data(val,done){
        const obj = {
            val : val,
            done : done ?? false
        }
        if(!this.data[this.currDate]) this.data[this.currDate] = []; 
        this.data[this.currDate].push(obj);
 
        console.log(this.data);
    }//add_data
 
    /**
     * data에서도 제거
     * @param {String} val value
     */
    remove_data(val){
        if(!this.data[this.currDate]) return;
        const idx = this.data[this.currDate].findIndex(item=>item.val == val);
        if(idx < 0return;
        this.data[this.currDate].splice(idx,1);
 
        console.log(this.data);
    }//remove_data
 
    /**
     * 체크박스시 data의 값을 수정합니다
     * @param {DOM}$ipt input[type="checkbox"]
     */
    modify_data($ipt){
        const val = $ipt.parentElement.getElementsByClassName('td-list-item-txt')[0].textContent;
        const done = $ipt.checked;
        const idx = this.data[this.currDate].findIndex(item=>item.val == val);
        if(idx < 0return;
        this.data[this.currDate][idx].done = done;
    }//modify_data
}//class-ToDoList
cs

Calendar.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
import { return_date_string } from "./FN.js";
 
/** 📌달력 관련 */
export class Calendar {
    /** 달력 관련 */
    constructor(){
        this.$head = document.getElementById('cd-head');
        this.$year = document.getElementById('cd-head-yy');
        this.$monthNum = document.getElementById('cd-head-mm-num');
        this.$monthEng = document.getElementById('cd-head-mm-eng');
        this.$tbody = document.getElementById('cd').getElementsByTagName('TBODY')[0];
 
        this.today = return_date_string(new Date());
        this.curr = this.today; 
 
        this.mm = ["January","February","March","April","May","June","July","August","Septemper","October","November","December"];
 
        this.TODO = null;
    }//constructor
 
    /** 
     * 최초 화면 로드시  
     * @param {Class} TODO To Do List 클래스 받아오기
    */
    init(TODO){
        //todo 클래스 받아오기
        this.TODO = TODO;
 
        //상단날짜, 캘린더 채우기
        this.display_all(this.today);
        
        //좌우 버튼에 이벤트 달기
        this.$head.addEventListener('click',this.on_click_prev_next);
 
        //tbody에 이벤트 달기
        this.$tbody.addEventListener('click',this.on_click_btn_date);
    }//init
 
    /**
     * 캘린더, 상단 날짜 전체 변경
     * @param {String}dateString yyyy-mm-dd
     */
    display_all(dataString){
        //tbody reset
        this.reset_tbody();
                
        //상단 날짜 표기
        this.display_cd_head(dataString);
 
        //달력 채우기
        this.draw_calendar(dataString);
    }//display_all
 
    /** reset tbody */
    reset_tbody(){
        this.$tbody.innerHTML = '';
    }//reset_tbody
 
    /**
     * 상단의 날짜 표기
     * @param {String}dateString yyyy-mm-dd
     */
    display_cd_head(dateString){
        const [yyyy,mm] = dateString.split('-');
        this.$year.textContent = yyyy;
        this.$monthNum.textContent = mm;
        this.$monthEng.textContent = this.mm[Number(mm) - 1];
    }//display_cd_head
 
    /**
     * 📌달력 날짜 채우기
     * @param {String} dateString yyyy-mm-dd 
     * @memo https://jerryjerryjerry.tistory.com/26
     */
    draw_calendar(dateString){
        const [cy,cm,cd] = dateString.split('-');
        const start = new Date(cy,Number(cm)-1,1);
        const last = new Date(cy, cm, 0);
 
        let cnt = start.getDay();
        let $tr = this.$tbody.insertRow();
 
        //날짜 첫 요일까지 빈칸 삽입
        for(let k=0; k<cnt; k++) $tr.insertCell();
 
        //날짜 그리기
        for(let i=1; i<=last.getDate(); i++){
            cnt++;
            const $td = $tr.insertCell();
            const dataString = `${cy}-${cm.padStart(2,"0")}-${String(i).padStart(2,"0")}`; 
            
            //날짜 버튼 추가
            const $btn = document.createElement("BUTTON");
            $btn.classList.add('cd-cell');
            $btn.textContent = i;
            $btn.dataset.dateInfo = dataString;
            if( dataString == this.today) $btn.classList.add('today');
            $td.appendChild($btn);
 
            //남아있는일,한 일 표시
            const thisTodo = this.TODO.data[dataString]; 
            if(thisTodo?.length){
                const rmn = thisTodo.filter(obj=>!obj.done).length;
                const done = thisTodo.length - rmn;
                if(rmn){
                    const $rmn = document.createElement('DIV');
                    $rmn.classList.add('cd-td-num');
                    $rmn.classList.add('rmn');
                    $rmn.textContent = rmn;
                    $rmn.title = `${rmn}개의 업무가 남아있습니다.`;
                    $td.appendChild($rmn);
                }
                
                if(done){
                    const $done = document.createElement('DIV');
                    $done.classList.add('cd-td-num');
                    $done.classList.add('done');
                    $done.textContent = done;
                    $done.title = rmn ? `${done + rmn}개중 ${done}개 완료!` : `전부 완료!`;
                    $td.appendChild($done);
                }
            }//if
 
            //줄바꿈,토요일
            if(cnt  % 7 == 0){
                $tr = this.$tbody.insertRow();
                $btn.classList.add('saturday');
            }//if
 
            //일요일
            if(cnt % 7 == 1) $btn.classList.add('sunday');
        }//for
    }//draw_calendar
 
    /**
     * tbody의 날짜 버튼을 클릭시
     * @param {Event}e
     */
    on_click_btn_date = (e) =>{
        if(e.target.tagName != "BUTTON"return;
 
        //클래스 "on" toggle
        const $BTN = e.target;
        const $$btn = this.$tbody.querySelectorAll('BUTTON');
        $$btn.forEach($btn => $btn.classList.toggle('on', $btn == $BTN));
 
        //to do list에 반영
        const dataString = $BTN.dataset.dateInfo;
        this.TODO.display_all(dataString);
    }//on_click_btn_date
 
    /**
     * 캘린더의 표시 수정
     */
    change_rmn_done(dataString){
        const $btn = this.$tbody.querySelector(`[data-date-info="${dataString}"]`)
        if(!$btn) return;
        const $cell = $btn.parentElement;
        const thisTodo = this.TODO.data[dataString];
        const rmn = thisTodo.filter(item => !(item.done)).length;
        const done = thisTodo.length - rmn;
 
        const $rmn = $cell.getElementsByClassName('rmn')[0];
        const $done = $cell.getElementsByClassName('done')[0];
 
        //남아있는거 처리
        if(!rmn){
            $rmn && $cell.removeChild($rmn);
        }else{
            if($rmn){
                $rmn.textContent = rmn;
            }else{
                const $rmnNew = document.createElement('DIV');
                $rmnNew.classList.add('cd-td-num');
                $rmnNew.classList.add('rmn');
                $rmnNew.textContent = rmn;
                $cell.appendChild($rmnNew);
                if($done) $cell.insertBefore($rmnNew,$done);
            }
        }//if-rmn
 
        //해결한거 처리
        if(!done){
            $done && $cell.removeChild($done);
        }else{
            if($done){
                $done.textContent = done;
            }else{
                const $doneNew = document.createElement('DIV');
                $doneNew.classList.add('cd-td-num');
                $doneNew.classList.add('done');
                $doneNew.textContent = done;
                $cell.appendChild($doneNew);
            }
        }//if-done
    }//change_rmn_done
 
    /** 
     * 달력 바꾸기 (전,후) 버튼 클릭시
     */
    on_click_prev_next = e => {
        if(e.target.tagName != "BUTTON"return;
        const dir = [...e.target.id.matchAll(/(btn\-mm\-)(\w*)/g)][0][2];
        const prevNext = dir == "next" ? 1 : -1;
        const [yyyy,mm] = this.curr.split('-');
        const newDate = new Date(yyyy, Number(mm - 1+ prevNext, 1);
        this.curr = return_date_string(newDate);
 
        this.display_all(this.curr);
    }//on_click_prev_next
}//class-Calendar
cs