프로젝트/ToDoApp프로젝트-FireBase

[ToDoApp-Firebase] Web 구현 01

sintory-04 2025. 2. 8. 21:44

    1. 간단한 HTML 제작하기

    크게 toast-containercontainer-full 로 분리했다.

    toast-container 에는 toast가 실행될 div를 만들어 두었다.

    container-full 가 실제 실행코드들로 보면 된다.

    container-full 에는 Head 부분Btn 부분Data 부분 으로 구성되어 있다.

    Todo와 Done 을 구분하기 위해 다른 웹 페이지 test.html 을 만들었다. test.html도 아래의 html과 유사해 생략하였다. 

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>ToDoApp</title>
        <link rel="stylesheet" href="/css/index.css">
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
            integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
        <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js"></script>
        <script type="module" src="/js/index.js"></script>
    </head>
    <body>
        <!--Toast Message-->
        <div id="toast-container"></div>
        <!--실제 화면-->
        <div class="container-full">
            <!--Head 부분: 제목과 Input-->
            <div class="container-head">
                <!--제목과 Fetch(온도)-->
                <div id="title">
                    <h1>† To Do App †</h1>
                    <p>오늘 서울의 온도는 <span style="color:brown;" id="tempInfo"></span>°C 이며, 날씨는 <span  style="color:brown;"id="weatherInfo"></span> 입니다.</p>
                </div>
                <!--일정추가란-->
                <div class="todo_Input">
                    <div class="input-group mb-3">
                        <input type="text" class="form-control" id="todoText" placeholder="할일을 적어주세요.">
                        <button type="button" class="btn btn-outline-dark" id="todoBtn">
                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
                                class="bi bi-plus-square-fill" viewBox="0 0 16 16">
                                <path
                                    d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0" />
                            </svg>
                            일정 추가
                        </button>
                    </div>
                </div>
            </div>
            <div id="line"></div>
            <div id="line2"></div>
            <!--Btn 부분: Todo와 Done 이동 부분-->
            <div id="todoDonerow">
                <a href="/index.html"><button id="todohref" class="btn hrefthis">To Do</button></a>
                <a href="/test.html"><button id="donehref" class="btn">Done</button></a>
            </div>
            <!--Data 부분: 실제 데이터 쌓는 부분-->
            <div class="container-stack" >
                <ul class="list-group" id="text-stack">
                    <!--여기에 데이터 쌓임.-->
                </ul>
            </div>
        </div>
    </body>
    </html>

    - css 는 생략하겠다.

     

    2. 데이터 처리 (Firebase)

    1) 데이터 연동

    - 프론트를 만든 후, Firebase와 프론트를 연결했다.

    - API 키 보호를 위해 비공개로 비워두었다.

    // Firebase SDK 라이브러리 가져오기
    import { initializeApp } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-app.js";
    import { getFirestore } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore.js";
    import { collection, addDoc } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore.js";
    import { getDocs } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore.js";
    
    // Firebase 구성 정보 설정
    // For Firebase JS SDK v7.20.0 and later, measurementId is optional
    const firebaseConfig = {
        //비공개
    };
    
    // Firebase 인스턴스 초기화
    const app = initializeApp(firebaseConfig);
    const db = getFirestore(app);

    2) 데이터 전달하기

    - 데이터 전달에 앞서, 데이터는 크게 4가지로 구성되어 있다. IDtext, complete, date 이렇게 있다. 각 데이터를 설명하자면,

    • ID: 일정 추가를 한 시간을 밀리초로 환산한 string
    • text: 일정
    • complete: 일정 완료 여부
    • date: 일정 추가/수정한 '년-월-일 시간:분'

     

    - todoBtn 을 클릭시 해당 값이 DB 로 전달될 수 있게 Jquery 로 코드를 작성하였다.

    - FirebaseDATA 에 고유한 ID 값을 주기 위하여 현재 시간을 밀리초로 환산한 값을 string으로 저장하였다.

    - getTodayDate()라고 오늘 시간을 년-월-일 시간:분 으로 바꾸어주는 함수로 date값을 저장하였다.

    - showToast를 통해 일정이 추가되었다는 메시지를 Toast로 보여주었다.

    - 일정이 추가되고 난 후에는 widow.location.reload()를 해주었다.

    $('#todoBtn').click(async function () {
        let taskText = $('#todoText').val();
        let customId = Date.now().toString();  
        let complete = false;
        let date = getTodayDate();
        // customId 로 id 지정해서 추가하기.
        const docRef = doc(db, "todos", customId);
        try {
            await setDoc(docRef, {
                id: customId,
                text: taskText,
                complete: complete,
                date: date
            });
            showToast("[ " + taskText.slice(0, 8) + "... ] ", "일정을 추가 하였습니다.");
            window.location.reload();
        } catch (error) {
            console.error("에러가 발생했습니다. ", error);
        }
    })
    // 날씨 환산 함수
    function getTodayDate() {
        let today = new Date();
        let year = today.getFullYear();
        let month = today.getMonth() + 1;
        let day = today.getDate();
        let hour = today.getHours();
        let minute = today.getMinutes();
    
        return `${year}-${month < 10 ? '0' + month : month}-${day < 10 ? '0' + day : day} ${hour < 10 ? '0' + hour : hour}:${minute < 10 ? '0' + minute : minute}`;
    }

    3) 데이터 띄우기

    - todos에 db를 저장해두고, 저장한 db 값을 forEach를 통해 화면에 띄운다.

    - complete 여부에 따라 하나는 'Todo' 로 가고 하나는 'Done'으로 간다.

      let todos = await getDocs(collection(db, "todos"))
      todos.forEach((todo) => {
          let text = todo.data()['text'];
          let complete = todo.data()['complete'];
          let id = todo.data()['id'];
          let date = todo.data()['date'];
          let temp_html = ``
          if (complete === true) {
              temp_html = `
                  <li class="list-group-item" id="${id}">
                      <div>
                          <input class="form-check-input" type="checkbox" id="checkbox" checked>
                          <input type="text" id="eidt_text" value="${text}">
                          <p class="date-p">${date}</p>
                      </div>
                      <button type="button" class="btn btn-secondary float-end deleteBtn" id="deleteBtn">삭제</button>
                      <button type="button" class="btn btn-secondary float-end editBtn" id="editBtn">수정</button>
                  </li>`;
              $('#text-stack-done').append(temp_html);
          } else {
              temp_html = `
                  <li class="list-group-item" id="${id}">
                      <div>
                          <input class="form-check-input" type="checkbox" id="checkbox">
                          <input type="text" id="eidt_text" value="${text}">
                          <p class="date-p">${date}</p>
                      </div>
                      <button type="button" class="btn btn-secondary float-end deleteBtn" id="deleteBtn">삭제</button>
                      <button type="button" class="btn btn-secondary float-end editBtn" id="editBtn">수정</button>
                  </li>`;
              $('#text-stack').append(temp_html);
          }
      });

    4) completecheckbox

    - checkbox가 변경될 때 마다 이를 DB에 반영해주는 코드이다.

    - checkboxchange 이벤트는 JS가 각각 독립으로 처리하기 때문에 $(document)로 처리하지 않아도 된다.

    $('.form-check-input').change(async function () {
        let check_id = $(this).closest('li').attr('id');
        let check_text = $(this).closest('li').find('input[type="text"]').val().trim();
        const docRef = doc(db, "todos", check_id);
        console.error("에러가 발생했습니다. ", '1');
        if ($(this).is(':checked')) {
            console.error("에러가 발생했습니다. ", '2');
            showToast("[ " + check_text.slice(0, 8) + "... ] ", "일정을 완료 하였습니다.");
            await updateDoc(docRef, {
                complete: true,
            });
            console.error("에러가 발생했습니다. ", '3')
        } else {
            showToast("[ " + check_text.slice(0, 8) + "... ] ", "일정을 미완료 하였습니다.");
            await updateDoc(docRef, {
                complete: false,
            });
        }
    });

    5) Delete Btn 작동

    - DeleteBtn 클릭 시, 데이터가 DB에서 삭제될 수 있도록 하는 코드이다.

    - 여기서 중요한 포인트가 $(documnet).on 으로 처리했다는 것이다. 만약 바로 click 클래스로 접근하였으면 첫번째 값만 삭제 이벤트가 작동하고, 나머지 아이들은 삭제이벤트가 작동하지 않을 것이다.

    - click 이벤트의 경우, 이벤트 위임을 사용하지 않으면 페이지가 처음 로드될 때 존재하는 요소에만 이벤트가 등록됩니다. 그래서 동적으로 생성된 요소에는 이벤트가 적용되지 않아서 첫 번째 요소만 작동하는 것처럼 보일 수 있다.

    - JS 코드 실행 시점에 존재하는 .editBtn 요소들에 대해서만 click 이벤트를 등록한다. 따라서 코드 실행 이후에 동적으로 추가된 .editBtn 버튼은 이 이벤트를 인식하지 못하게 된다. 따라서 이벤트 위임을 사용하여 페이지 전체(document)에 click 이벤트를 걸고, 이벤트가 발생하면 .editBtn 요소에서만 실행되도록 위임하는 방식으로 접근해야한다.

    - 체크박스의 change 이벤트는 각 요소마다 독립적으로 동작한다. 체크박스는 보통 동적으로 생성되지 않거나, 생성되더라도 jQuery가 기존 요소에 대해 이벤트를 등록하는 방식이 잘 적용된다. 따라서 change 이벤트는 여러 개의 체크박스에서도 잘 동작한다.

    $(document).on('click', '.deleteBtn', async function () {
        let check_id = $(this).closest('li').attr('id');
        let task_text = $(this).closest('li').find('input[type="text"]').val().trim();
        actionModal("이 일정을 삭제하시겠습니까?", "[ " + task_text + " ]", "삭제하기");
        $(document).one('click', '#modal-actionBtn', async function () {
            const docRef = doc(db, 'todos', check_id);
            try {
                await deleteDoc(docRef); 
                $("#dynamicModal").remove()
                showToast("[ " + task_text.slice(0, 8) + "... ]  삭제여부", "삭제되었습니다.");
            } catch (error) {
                console.error("삭제 실패: ", error);
            }
        })
        $(document).on('click','#modal-closeBtn', async function (){
            showToast("[ " + task_text.slice(0, 8) + "... ]  삭제여부", "수정 취소 하였습니다.");
        })
    });

    6) Edit Btn 작동

    - 수정 버튼을 누를 때 마다, 수정된 일정과 새로운 날짜로 바꾸어주는 코드이다.

    $(document).on('click', '.editBtn', async function () {
        let task_text = $(this).closest('li').find('input[type="text"]').val().trim();
        let check_id = $(this).closest('li').attr('id');
        let date = getTodayDate();
        console.log(task_text,check_id);
        actionModal("해당 일정으로 수정하실건가요?", "[ " + task_text + " ]", "수정하기");
        // 실제로 변경할건지 아닌지 물어보기.
        $(document).on('click', '#modal-actionBtn', async function () { 
            const docRef = doc(db, "todos", check_id);
            updateDoc(docRef, {
                text: task_text,
                date: date
            });
            $("#dynamicModal").remove()
            showToast("[ " + task_text.slice(0, 8) + "... ]  수정여부", "수정되었습니다.");
            setTimeout(() => {
                window.location.reload();
            }, 3000); // 3초 후 새로고침
        })
        $(document).on('click','#modal-closeBtn', async function (){
            showToast("[ " + task_text.slice(0, 8) + "... ]  수정여부", "수정 취소 하였습니다.");
        })
    });

    3. Fetch 로 날씨 온도 가져오기 (온도 API) 가져오기

    - 날씨 온도 API 를 가져오는 방법을 적어보겠다.

    1) https://openweathermap.org/api 회원가입하기

    "https://openweathermap.org/api"

    - a. 위 사이트에 들어가서 회원가입을 해준다.

    - b. 이메일 인증한다.

    - c. 메일 인증 하면 아래의 이메일이 한 번 더 날아온다. 빨간색으로 칠해진 부분이 API 키 인데, 이를 복사해두면 된다.

    2) 날씨 데이터 접근해보기

    - a. 아래의 링크 {} 안에 복사해둔 자신의 키를 넣어준다. ({}을 포함하면 안된다.)

    "http://api.openweathermap.org/data/2.5/weather?id=1835848&APPID={**자신의API키**}&lang=kr&units=metric"

    • id=1835848 한국 id 임
    • lang=kr 한국어로 번역
    • units=metric 섭씨온도로 변환

    - b. 링크에 들어가 데이터를 확인해본다. 아래와 같은 JSON 형태로 나오면 성공이다.

    • 온도에 접근하고 싶으면 data.main.temp
    • 날씨에 접근하고 싶으면 data.weather[0].description

    - c. 다른 데이터로 링크를 바꾸어서 데이터 접근도 가능하다. 아래의 사이트에 가서 직접 확인하면 된다.

    "https://openweathermap.org/api/one-call-3"

    3) Fetch 연동

    - 아래의 코드에 넣으면 된다.

    var apiUrl = "http://api.openweathermap.org/data/2.5/weather?id=1835848&APPID={**자신의API키**}&lang=kr&units=metric"
    
    fetch(apiUrl)
        .then(res => res.json())
        .then(data => {
            var temperature = data.main.temp;
            var description = data.weather[0].description;
            $('#tempInfo').text(temperature);
            $('#weatherInfo').text(description);
    })

    4) 결과