17 minute read

1. What is bootstrap

IMPORTANT

가장 많이 사용 “반응형 웹 프론트앤드 디자인 툴 킷

  • html, css, javascript 를 이용한 화면을 일정수순 이상의 품질을 보장해주는 웹 화면을 구성을 도와주는 오픈소스 프레임워크
  • 다양한 디바이스에 최적의 웹 레이아웃에 최적화되어 있어 많이 사용하며, 자주 사용하는 위젯과 콤포넌트가 이미 만들어져 있음
  • Bootstrap CSS & JavaScript CDN 추가 (bootstrap getting started)
<!-- CSS CDN -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">

<!-- Javascript CDN -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3"
    crossorigin="anonymous"></script>

2. Layout

  • 반응형 웹에 대한 모든 요소
    • brakpoints, containers, grid 가장 중요한 개념
  • Breakpoints (해상도별 크기 기준)
    • 디바이스별 반응형의 작동 방식 (사용자 해상도 넓이에 따른 기준)
    • bootstrap layout
breakpoint class infix dimensions
X-Small None < 576px
Small sm >= 576px
Medium md >= 768px
Large lg >= 992px
Extra large xl >= 1200px
Extra extra large xxl >= 1400px
  • Containers (기본 블록, 중앙에 위치 시킨다)
    • bootstrap을 만들기 위한 가장 기본 요소 (부트스트랩의 기본 빌딩 블록)
    • bootstrap containers
    • 화면의 해상도에 따라서 넓이를 100% 잡을 것인지 각 크기별로 다르게 줄것인지 결정
  • Grid (12 Grid)
    • bootstrap grid system
    • 화면의 해상도에 컬럼을 12개로 나눈다. (12 grid system 이라고 부른다.)

    • <div class="container">
          <div class="row">
              <div class="col">
                  Column
              </div>
              <div class="col">
                  Column
              </div>
              <div class="col">
                  Column
              </div>
          </div>
      </div>
      
      <div class="row">
          <!-- 
          col-sm-12 : 해상도 sm(≥576px)일때 12 그리드 중 1 col = 12. 즉, row 당 1 col 표시
          col-md-6 : 해상도 md(≥768px)일때 12 그리드 중 1 col = 6. 즉, row 당 2 col 표시
          col-lg-4 : 해상도 lg(≥992px)일때 12 그리드 중 1 col = 4. 즉, row 당 3 col 표시
          col-xl-3 : 해상도 xl(≥1200px)일때 12 그리드 중 1 col = 3. 즉, row 당 4 col 표시
          -->
          <div class="col-md-6 col-lg-4 col-xl-3">A</div>
          <div class="col-md-6 col-lg-4 col-xl-3">B</div>
          <div class="col-md-6 col-lg-4 col-xl-3">C</div>
          <div class="col-md-6 col-lg-4 col-xl-3">D</div>
          <div class="col-md-6 col-lg-4 col-xl-3">E</div>
          <div class="col-md-6 col-lg-4 col-xl-3">F</div>
          <div class="col-md-6 col-lg-4 col-xl-3">G</div>
          <div class="col-md-6 col-lg-4 col-xl-3">H</div>
      </div>
      
  • Columns
  • Gutters
    • bootstrap gutters
    • 그리드 시스템내에서의 간격 조정
    • 컬럼(column)과 컬럼(column)사이의 패딩(padding, 간격) 조정
    • 행(row)과 행(row)사이의 패딩(padding, 간격) 조정
  • Utilities
    • bootstrap utilities
    • 디스플레이(display) - 보이게 말거냐? 포지션
    • 플랙스박스(flexbox) 관련 속성
    • 마진(margin), 패딩(padding) 관련 속성
    • 토글(toggle) 속성
    • 브레이크포인트(breakpoints)를 사용하여 해상도별로 숨김/나타남 처리 유틸리티 유용함
    • z-index
      • 컨텐츠를 포토샵의 레이어처럼 위쪽으로 살짝 올리는 값에 대한 기준
    • CSS grid
      • flexbox(1차원), grid(2차원)

3. Content

  • Reboot, Typography, Images, Tables, Figures
name class name description
Typography h1~6, mark, del, s, ins, u, strong, em 글자 관련된 디자인 클래스
Images img-fluid, img-thumbnail 이미지 관련된 디자인
Tables table, table-primary, table-striped, table-hover, table-bordered 테이블(table) 태그(tag)에 대한 디자인 클래스

4. Forms

  • 입력 관련 컨트롤에 대한 디자인 스타일 제공
name class name description
From control form-label, form-control, disabled readonly type=’text’와 같은 기본 입력 컨트롤
Select form-select, 선택 컨트롤
Check & Radios form-check-input, form-check-label, class=”form-check form-switch”, class=”form-check form-check-inline” 체크 및 라디오
Range form-range 범위
Input group div / class=”input-group”, span / class=”input-group-text” 입력 컨트롤의 그룹핑
Floating labels div / class=”form-floating” 라벨과 입력을 한개의 그룹으로 묶고 라벨을 플로팅 시킴
Layout   layout 패턴 및 스타일
Validation needs-validation, valid-feedback , invalid-feedback 유효성 검사

5. Components

  • 간략한 설명
name class name description
Accordion accordion-xx 확장, 축소 컨트롤
Alerts alert 알람
Badge badge 뱃지 카운트, 마크
Breadcrumb breadcrumb 메뉴 네비게이션
Buttons btn 버튼 스타일
Button group btn-group 버튼의 그룹화
Card card-xx 카드 스타일
Carousel carousel-xx 이미지 스라이딩
Close button btn-close 닫힘 버튼
Collapse collapse, data-bs-toggle, data-bs-target 확장, 축소
Dropdowns dropdown 메뉴의 서브메뉴
List group list-group-xx 정형화된 목록
Modal modal-xx, static backdrop 팝업창
Navbar navbar-xx 메뉴
Navs & tabs nav-xx Nav + Tab
Offcanvas offcanvas-xx 스라이딩 캔버스
Pagination Page, pagination 페이징
Placeholders placeholder 로딩 효과
Popovers data-bs-toggle, popover 클릭이나 이벤트시 설명 문구
Progress progress-xx 진행상태
Scrollspy data-bs-spy, scroll 스크롤 이동
Spinners spinner-border 스핀-로딩
Toasts toast-xx 잠깐 보였다 사라지는 알람
Tooltips data-bs-toggle=”tooltip” 툴팁

6. Helpers

  • 다양한 Helper
name class name description
Clearfix clearfix,float-start, float-end Bootstrap Refer
Color & background text-bg-xx 색상, 배경색
Colored links link-xx 링크 스타일
Position fixed-xx, sticky-xx 위치 관련 스타일
Ratio ratio-xx 동영상 이미지 비율
Stacks vstack, hstack 세로, 가로 쌓기
Stretched link   Bootstrap Refer
Text truncation text-truncate 글자 줄임표시
Vertical rule vr 세로 구분자
Visually hidden visually-xx 숨김

7. Utilities

  • 다양한 Utilities
name class name description
API SASS Bootstrap Refer
Background bg-xx 배경색
Borders border-xx Border
Colors text-xx, text-opacity-xx 글자색, 투명도
Display d-{brakpoint}-{value} 표시
Flex d-flex, flex-xx flex layout
Float float-xx float
Interactions user-select-xx text 선택 스타일
Opacity opacity-xx 투명도
Overflow overflow-xx overflow 스타일
Position position-xx position 스타일
Shadows shadow-xx 그림자
Sizing w-xx, h-xx 넓이, 높이 크기
Spacing m-xx, p-xx margin, padding
Text text-xx, text 정렬 등등
Vertical align align-xx Vertical 정렬
Visibility visible, invisible 보이고, 안보이고

8. Extend

  • 확장에 대한 전략
    • 구축하고 유지 관리하는 데 사용되는 기본 원칙, 전략 및 기술
  • Icons

9. Apply Bootstrap

리스트 조회

  • 주요 변경 사항
  • IMPORTANT

    bootstrap css & javascript cdn 추가
    조회 조건 영역 변경(inline forms layout 반영)
    table element 변경
    컨텐츠 중앙 정렬을 위하여 class=”container” 추가

<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>Document</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>

<body>
    <div class="container">
        <!-- 조회 조건 -->
        <div class="row row-cols-lg-auto g-3 align-items-center mt-1 mb-1">
            <div class="col-12">
                <select class="form-select" name="" id="gender">
                    <option value="">전체</option>
                    <option value="male">남자</option>
                    <option value="female">여자</option>
                </select>
            </div>

            <div class="col-12">
                <input type="search" class="form-control" name="" id="name" placeholder="Name" onkeyup="checkEnter()" />
            </div>

            <div class="col-12">
                <button class="btn btn-primary" onclick="doSearch();">조회</button>
                <button class="btn btn-success" onclick="goToCreate();">생성</button>
                <button class="btn btn-danger" id="btnDelete" onclick="doDelete();" disabled>삭제</button>
            </div>
        </div>

        <!-- 테이블 -->
        <table class="table table-striped table-hover">
            <thead>
                <tr>
                    <th><input type="checkbox" onchange="checkAll();" class="form-check-input" /></th>
                    <th>Name</th>
                    <th>Company</th>
                    <th>Gender</th>
                    <th>Email</th>
                    <th>Phone</th>
                    <th>Address</th>
                </tr>
            </thead>
            <tbody id="tbBody"></tbody>
        </table>
    </div>



    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
        crossorigin="anonymous"></script>
    <script src="//cdn.jsdelivr.net/npm/sweetalert2@11"></script>
    <script>
        // crud - Create, Read, Update, Delete
        async function doSearch() {
            const gender = document.querySelector("#gender").value;
            const name = document.querySelector("#name").value;

            let resource = "http://localhost:3000/customers";
            if (gender === "") {
                if (name != "") {
                    resource = `http://localhost:3000/customers?name_like=${name}`;
                }
            } else {
                if (name !== "") {
                    resource = `http://localhost:3000/customers?gender=${gender}&name_like=${name}`;
                } else {
                    resource = `http://localhost:3000/customers?gender=${gender}`;
                }
            }

            const res = await fetch(resource);
            const resJson = await res.json();
            console.log(resJson);
            renderTable(resJson);
        }

        function renderTable(data) {
            const h = [];
            for (const item of data) {
                h.push(`<tr>`);
                h.push(
                    `<td><input type="checkbox" name="chk" value="${item.id}" onchange="isChecked();" class="form-check-input" /></td>`
                );
                h.push(
                    `<td><a href="javascript:goToDetail('${item.id}');">${item.name}</a></td>`
                );
                h.push(`<td>${item.company}</td>`);
                h.push(`<td>${item.gender}</td>`);
                h.push(`<td>${item.email}</td>`);
                h.push(`<td>${item.phone}</td>`);
                h.push(`<td>${item.address}</td>`);
                h.push(`</tr>`);
            }

            document.querySelector("#tbBody").innerHTML = h.join("");
        }

        function goToCreate() {
            document.location.href = "dom_crud_create.html";
        }

        async function doDelete() {
            const chks = document.querySelectorAll("[name=chk]:checked");
            if (chks.length > 0) {
                Swal.fire({
                    title: "정말 삭제 하시겠습니까?",
                    text: "You won't be able to revert this!",
                    icon: "warning",
                    showCancelButton: true,
                    confirmButtonColor: "#3085d6",
                    cancelButtonColor: "#d33",
                    confirmButtonText: "삭제",
                    cancelButtonText: "취소",
                }).then(async (result) => {
                    if (result.isConfirmed) {
                        for (const chk of chks) {
                            await fetch(`http://localhost:3000/customers/${chk.value}`, {
                                method: "DELETE",
                            });
                        }

                        Swal.fire({
                            icon: "success",
                            title: "데이터가 정상적으로 삭제 되었습니다.",
                        });
                        await doSearch();
                    }
                });
            } else {
                alert("삭제할 아이템을 선택하세요.");
            }
        }

        function checkAll() {
            console.log(event.target.checked);
            const checkValue = event.target.checked;
            const chks = document.querySelectorAll("[name=chk]");
            if (chks.length > 0) {
                for (const chk of chks) {
                    chk.checked = checkValue;
                }
            }

            isChecked();
        }

        function isChecked() {
            const chks = document.querySelectorAll("[name=chk]:checked");
            if (chks.length > 0) {
                document.querySelector("#btnDelete").disabled = false;
            } else {
                document.querySelector("#btnDelete").disabled = true;
            }
        }

        function checkEnter() {
            if (event.keyCode === 13) {
                doSearch();
            }
        }

        function goToDetail(id) {
            document.location.href = `dom_crud_detail.html?id=${id}&v1=1&name=jeremy`;
        }
    </script>
</body>

생성

  • 주요 변경 사항
  • IMPORTANT

    bootstrap css & javascript cdn 추가
    컨텐츠 중앙 정렬을 위하여 class=”container” 추가
    label, text, button 스타일 적용
    alert 적용(sweetalert2)

<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>Document</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>

<body>
    <div class="container mt-2">
        <div class="row gy-2">
            <div class="col-4">
                <label for="name" class="form-label">Name</label>
            </div>
            <div class="col-8">
                <input type="text" name="" id="name" class="form-control" onkeyup="checkEnter('company')" />
            </div>
            <div class="col-4">
                <label for="name" class="form-label">Gender</label>
            </div>
            <div class="col-8">
                <input type="radio" name="gender" id="male" value="male" class="form-check-input" checked />
                <label for="male" class="form-check-label">남자</label>
                <input type="radio" name="gender" id="female" value="female" class="form-check-input" />
                <label for="female" class="form-check-label">여자</label>
            </div>
            <div class="col-4">
                <label for="company" class="form-label">Company</label>
            </div>
            <div class="col-8">
                <input type="text" name="" id="company" class="form-control" onkeyup="checkEnter('email')" />
            </div>
            <div class="col-4">
                <label for="email" class="form-label">Email</label>
            </div>
            <div class="col-8">
                <input type="email" name="" id="email" class="form-control" onblur="checkEmail();"
                    onkeyup="checkEnter('phone')" />
                <div id="emailMsg" class="invalid-feedback">
                    올바른 형식의 이메일을 입력하세요.
                </div>
            </div>
            <div class="col-4">
                <label for="phone" class="form-label">Phone</label>
            </div>
            <div class="col-8">
                <input type="tel" name="" id="phone" class="form-control" onblur="checkPhone();"
                    onkeyup="checkEnter('btnDaumAPI')" />
                <div id="phoneMsg" class="invalid-feedback">
                    올바른 형식의 전화번호를 입력하세요.
                </div>
            </div>
            <div class="col-4">
                <label for="address" class="form-label">Address</label>
            </div>
            <div class="col-8">
                <div class="input-group">
                    <button id="btnDaumAPI" onclick="openDaumAPI();" class="btn btn-dark">주소찾기</button>
                    <input type="text" name="" id="zonecode" class="form-control" readonly />
                </div>
                <input type="text" name="" id="address" class="form-control mt-1" class="form-control" readonly />
                <input type="text" name="" id="address2" class="form-control mt-1" placeholder="상세주소"
                    onkeyup="checkEnter('create')" />
            </div>
            <div class="text-center">
                <button onclick="doCreate();" class="btn btn-success">생성</button>
                <button onclick="goToList();" class="btn btn-secondary">목록</button>
            </div>
        </div>
    </div>


    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
        crossorigin="anonymous"></script>
    <script src="//cdn.jsdelivr.net/npm/sweetalert2@11"></script>
    <script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
    <script>
        async function doCreate() {
            const name = document.querySelector("#name").value.trim();
            const gender = document.querySelector("[name=gender]:checked").value;
            const company = document.querySelector("#company").value.trim();
            const email = document.querySelector("#email").value.trim();
            const phone = document.querySelector("#phone").value.trim();
            const address = document.querySelector("#address").value.trim();

            console.log(name);
            console.log(gender);
            console.log(company);
            console.log(email);
            console.log(phone);
            console.log(address);

            if (name === "") {
                return Swal.fire({
                    icon: 'info',
                    title: 'Name을 입력하세요.',
                    text: 'Name은 필수 입력값입니다.',
                })
            }

            if (company === "") {
                return alert("Company를 입력하세요.");
                return Swal.fire({
                    icon: 'info',
                    title: 'Company를 입력하세요.',
                    text: 'Company는 필수 입력값입니다.',
                })
            }

            const regexpEmail =
                /^([a-z]+\d*)+(\.?[a-z]+\d*)+@([a-z]+\d*)+(\.[a-z]{2,3})+$/;
            if (!regexpEmail.test(email)) {
                return Swal.fire({
                    icon: "info",
                    title: "올바른 형식의 Email을 입력하세요.",
                    text: "이메일주소는 알파벳소문자, 숫자, 특수문자는 점(.)만 사용할 수 있습니다.",
                });
            }

            const regexpTel = /^010-\d{4}-\d{4}$/;
            if (!regexpTel.test(phone)) {
                return Swal.fire({
                    icon: "info",
                    title: "올바른 형식의 Phone을 입력하세요.",
                    text: "전화번호는 010-XXXX-XXXX 포맷으로 입력해야 합니다.",
                });
            }

            if (address === "") {
                return Swal.fire({
                    icon: "info",
                    title: "Address를 입력하세요.",
                    text: "Address는 필수 입력값입니다.",
                });
            }

            const zonecode = document.querySelector("#zonecode").value;
            const address2 = document.querySelector("#address2").value;

            Swal.fire({
                title: "정말 저장하시겠습니까?",
                text: "You won't be able to revert this!",
                icon: "warning",
                showCancelButton: true,
                confirmButtonColor: "#3085d6",
                cancelButtonColor: "#d33",
                confirmButtonText: "생성",
                cancelButtonText: "취소",
            }).then(async (result) => {
                if (result.isConfirmed) {
                    const res = await fetch("http://localhost:3000/customers", {
                        method: "POST",
                        body: JSON.stringify({
                            name,
                            gender,
                            company,
                            email,
                            phone,
                            address: `(${zonecode})${address} ${address2}`.trim(),
                            zonecode: zonecode,
                            roadAddress: address,
                            address2: address2,
                        }),
                        headers: {
                            "content-type": "application/json;charset=UTF-8",
                        },
                    });

                    if (res.status === 201) {
                        Swal.fire({
                            icon: "success",
                            title: "고객 정보가 정상적으로 생성되었습니다.",
                        });
                    } else {
                        Swal.fire({
                            icon: "error",
                            title: "고객 정보를 생성하지 못했습니다. 다시 시도하세요.",
                        });
                    }
                }
            });
        }

        function checkEmail() {
            const email = document.querySelector("#email").value;
            if (email !== "") {
                const regexpEmail =
                    /^([a-z]+\d*)+(\.?[a-z]+\d*)+@([a-z]+\d*)+(\.[a-z]{2,3})+$/;
                if (!regexpEmail.test(email)) {
                    document.querySelector("#email").classList.add("is-invalid");
                    document.querySelector("#emailMsg").style.display = "block";
                } else {
                    document.querySelector("#email").classList.remove("is-invalid");
                    document.querySelector("#emailMsg").style.display = "none";
                }
            } else {
                document.querySelector("#email").classList.remove("is-invalid");
                document.querySelector("#emailMsg").style.display = "none";
            }
        }

        function checkPhone() {
            const phone = document.querySelector("#phone").value;
            if (phone !== "") {
                const regexpTel = /^010-\d{4}-\d{4}$/;
                if (!regexpTel.test(phone)) {
                    document.querySelector("#phone").classList.add("is-invalid");
                    document.querySelector("#phoneMsg").style.display = "block";
                } else {
                    document.querySelector("#phone").classList.remove("is-invalid");
                    document.querySelector("#phoneMsg").style.display = "none";
                }
            } else {
                document.querySelector("#phone").classList.remove("is-invalid");
                document.querySelector("#phoneMsg").style.display = "none";
            }
        }

        function openDaumAPI() {
            new daum.Postcode({
                oncomplete: function (data) {
                    // 팝업에서 검색결과 항목을 클릭했을때 실행할 코드를 작성하는 부분입니다.
                    // 예제를 참고하여 다양한 활용법을 확인해 보세요.
                    console.log(data);
                    document.querySelector("#zonecode").value = data.zonecode;
                    document.querySelector("#address").value = data.roadAddress;
                },
            }).open();
        }

        function checkEnter(moveId) {
            if (event.keyCode === 13) {
                if (moveId === "btnDaumAPI") {
                    openDaumAPI();
                } else if (moveId === "create") {
                    doCreate();
                } else {
                    document.querySelector("#" + moveId).focus();
                }
            }
        }

        function goToList() {
            document.location.href = "dom_crud_list.html";
        }
    </script>
</body>

상세

  • 주요 변경 사항
  • IMPORTANT

    bootstrap css & javascript cdn 추가
    컨텐츠 중앙 정렬을 위하여 class=”container” 추가
    label, text, button 스타일 적용
    alert 적용(sweetalert2)

<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>Document</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>

<body>
    <div class="container mt-2">

        <div class="row mb-3">
            <label for="name" class="col-sm-3 col-form-label">Name</label>
            <div class="col-sm-9"><span id="name"></span></div>
        </div>

        <div class="row mb-3">
            <label for="name" class="col-sm-3 col-form-label">Gender</label>
            <div class="col-sm-9"><span id="gender"></span></div>
        </div>

        <div class="row mb-3">
            <label for="company" class="col-sm-3 col-form-label">Company</label>
            <div class="col-sm-9"><span id="company"></span></div>
        </div>

        <div class="row mb-3">
            <label for="email" class="col-sm-3 col-form-label">Email</label>
            <div class="col-sm-9"><span id="email"></span></div>
        </div>

        <div class="row mb-3">
            <label for="phone" class="col-sm-3 col-form-label">Phone</label>
            <div class="col-sm-9"><span id="phone"></span></div>
        </div>

        <div class="row mb-3">
            <label for="address" class="col-sm-3 col-form-label">Address</label>
            <div class="col-sm-9"><span id="address"></span></div>
        </div>

        <div class="text-center">
            <button onclick="goToUpdate();" class="btn btn-success">수정</button>
            <button onclick="goToList();" class="btn btn-secondary">목록</button>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
        crossorigin="anonymous"></script>
    <script src="./js/common.js"></script>
    <script>
        // queryString
        //   console.log(window.location.search);

        async function doSearchDetail() {
            const { id } = parseQueryString();
            const res = await fetch(`http://localhost:3000/customers/${id}`);
            const resJson = await res.json();
            console.log(resJson);
            renderDetail(resJson);
        }

        function renderDetail(data) {
            document.querySelector("#name").innerText = data.name;
            document.querySelector("#gender").innerText = data.gender;
            document.querySelector("#company").innerText = data.company;
            document.querySelector("#email").innerText = data.email;
            document.querySelector("#phone").innerText = data.phone;
            document.querySelector("#address").innerText = data.address;
        }

        function goToList() {
            document.location.href = "dom_crud_list.html";
        }

        function goToUpdate() {
            const { id } = parseQueryString();
            document.location.href = `dom_crud_update.html?id=${id}`;
        }

        doSearchDetail();
    </script>
</body>

수정

  • 주요 변경 사항
  • IMPORTANT

    bootstrap css & javascript cdn 추가
    컨텐츠 중앙 정렬을 위하여 class=”container” 추가
    label, text, button 스타일 적용
    alert 적용(sweetalert2)

<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>Document</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>

<body>
    <div class="container mt-2">
        <div class="row gy-2">
            <!-- name -->
            <div class="col-4">
                <label for="name" class="form-label">Name</label>
            </div>
            <div class="col-8">
                <input type="text" name="" id="name" class="form-control" onkeyup="checkEnter('company')" />
            </div>

            <!-- gender -->
            <div class="col-4">
                <label for="name" class="form-label">Gender</label>
            </div>
            <div class="col-8">
                <input type="radio" name="gender" id="male" value="male" class="form-check-input" checked />
                <label for="male" class="form-check-label">남자</label>
                <input type="radio" name="gender" id="female" value="female" class="form-check-input" />
                <label for="female" class="form-check-label">여자</label>
            </div>

            <!-- company -->
            <div class="col-4">
                <label for="company" class="form-label">Company</label>
            </div>
            <div class="col-8">
                <input type="text" name="" id="company" class="form-control" onkeyup="checkEnter('email')" />
            </div>

            <!-- email -->
            <div class="col-4">
                <label for="email" class="form-label">Email</label>
            </div>
            <div class="col-8">
                <input type="email" name="" id="email" class="form-control" onblur="checkEmail();"
                    onkeyup="checkEnter('phone')" />
                <div id="emailMsg" class="invalid-feedback">
                    올바른 형식의 이메일을 입력하세요.
                </div>
            </div>

            <!-- phone -->
            <div class="col-4">
                <label for="phone" class="form-label">Phone</label>
            </div>
            <div class="col-8">
                <input type="tel" name="" id="phone" class="form-control" onblur="checkPhone();"
                    onkeyup="checkEnter('btnDaumAPI')" />
                <div id="phoneMsg" class="invalid-feedback">
                    올바른 형식의 전화번호를 입력하세요.
                </div>
            </div>

            <!-- address -->
            <div class="col-4">
                <label for="address" class="form-label">Address</label>
            </div>
            <div class="col-8">
                <div class="input-group">
                    <button id="btnDaumAPI" onclick="openDaumAPI();" class="btn btn-dark">주소찾기</button>
                    <input type="text" name="" id="zonecode" class="form-control" readonly />
                </div>
                <input type="text" name="" id="address" class="form-control mt-1" readonly />
                <input type="text" name="" id="address2" class="form-control mt-1" placeholder="상세주소"
                    onkeyup="checkEnter('create')" />
            </div>

            <!-- button -->
            <div class="text-center">
                <button onclick="doSave();" class="btn btn-success">저장</button>
                <button onclick="goToList();" class="btn btn-secondary">목록</button>
            </div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
        crossorigin="anonymous"></script>
    <script src="//cdn.jsdelivr.net/npm/sweetalert2@11"></script>
    <script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
    <script src="./js/common.js"></script>
    <script>
        async function doSearchDetail() {
            const { id } = parseQueryString();
            const res = await fetch(`http://localhost:3000/customers/${id}`);
            const resJson = await res.json();
            console.log(resJson);
            renderDetail(resJson);
        }

        function renderDetail(data) {
            document.querySelector("#name").value = data.name;
            document.querySelector("#" + data.gender).checked = true;
            document.querySelector("#company").value = data.company;
            document.querySelector("#email").value = data.email;
            document.querySelector("#phone").value = data.phone;
            document.querySelector("#address").value = data.roadAddress;
            document.querySelector("#address2").value = data.address2;
            document.querySelector("#zonecode").value = data.zonecode;
        }

        doSearchDetail();

        async function doSave() {
            const name = document.querySelector("#name").value.trim();
            const gender = document.querySelector("[name=gender]:checked").value;
            const company = document.querySelector("#company").value.trim();
            const email = document.querySelector("#email").value.trim();
            const phone = document.querySelector("#phone").value.trim();
            const address = document.querySelector("#address").value.trim();

            console.log(name);
            console.log(gender);
            console.log(company);
            console.log(email);
            console.log(phone);
            console.log(address);

            if (name === "") {
                return Swal.fire({
                    icon: "info",
                    title: "Name을 입력하세요.",
                    text: "Name은 필수 입력값입니다.",
                });
            }

            if (company === "") {
                return Swal.fire({
                    icon: "info",
                    title: "Company를 입력하세요.",
                    text: "Company는 필수 입력값입니다.",
                });
            }

            const regexpEmail =
                /^([a-z]+\d*)+(\.?[a-z]+\d*)+@([a-z]+\d*)+(\.[a-z]{2,3})+$/;
            if (!regexpEmail.test(email)) {
                return Swal.fire({
                    icon: "info",
                    title: "올바른 형식의 Email을 입력하세요.",
                    text: "이메일주소는 알파벳소문자, 숫자, 특수문자는 점(.)만 사용할 수 있습니다.",
                });
            }

            const regexpTel = /^010-\d{4}-\d{4}$/;
            if (!regexpTel.test(phone)) {
                return Swal.fire({
                    icon: "info",
                    title: "올바른 형식의 Phone을 입력하세요.",
                    text: "전화번호는 010-XXXX-XXXX 포맷으로 입력해야 합니다.",
                });
            }

            if (address === "") {
                return Swal.fire({
                    icon: "info",
                    title: "Address를 입력하세요.",
                    text: "Address는 필수 입력값입니다.",
                });
            }

            const zonecode = document.querySelector("#zonecode").value;
            const address2 = document.querySelector("#address2").value;

            const { id } = parseQueryString();

            Swal.fire({
                title: "정말 저장하시겠습니까?",
                text: "You won't be able to revert this!",
                icon: "warning",
                showCancelButton: true,
                confirmButtonColor: "#3085d6",
                cancelButtonColor: "#d33",
                confirmButtonText: "생성",
                cancelButtonText: "취소",
            }).then(async (result) => {
                if (result.isConfirmed) {
                    const res = await fetch(`http://localhost:3000/customers/${id}`, {
                        method: "PUT",
                        body: JSON.stringify({
                            name,
                            gender,
                            company,
                            email,
                            phone,
                            address: `(${zonecode})${address} ${address2}`.trim(),
                            zonecode: zonecode,
                            roadAddress: address,
                            address2: address2,
                        }),
                        headers: {
                            "content-type": "application/json;charset=UTF-8",
                        },
                    });

                    if (res.status === 200) {
                        Swal.fire({
                            icon: "success",
                            title: "정상적으로 저장되었습니다.",
                        });
                    } else {
                        Swal.fire({
                            icon: "error",
                            title: "고객 정보를 저장하지 못했습니다. 다시 시도하세요.",
                        });
                    }
                }
            });
        }

        function checkEmail() {
            const email = document.querySelector("#email").value;
            if (email !== "") {
                const regexpEmail =
                    /^([a-z]+\d*)+(\.?[a-z]+\d*)+@([a-z]+\d*)+(\.[a-z]{2,3})+$/;
                if (!regexpEmail.test(email)) {
                    document.querySelector("#email").classList.add("is-invalid");
                    document.querySelector("#emailMsg").style.display = "block";
                } else {
                    document.querySelector("#email").classList.remove("is-invalid");
                    document.querySelector("#emailMsg").style.display = "none";
                }
            } else {
                document.querySelector("#email").classList.remove("is-invalid");
                document.querySelector("#emailMsg").style.display = "none";
            }
        }

        function checkPhone() {
            const phone = document.querySelector("#phone").value;
            if (phone !== "") {
                const regexpTel = /^010-\d{4}-\d{4}$/;
                if (!regexpTel.test(phone)) {
                    document.querySelector("#phone").classList.add("is-invalid");
                    document.querySelector("#phoneMsg").style.display = "block";
                } else {
                    document.querySelector("#phone").classList.remove("is-invalid");
                    document.querySelector("#phoneMsg").style.display = "none";
                }
            } else {
                document.querySelector("#phone").classList.remove("is-invalid");
                document.querySelector("#phoneMsg").style.display = "none";
            }
        }

        function openDaumAPI() {
            new daum.Postcode({
                oncomplete: function (data) {
                    // 팝업에서 검색결과 항목을 클릭했을때 실행할 코드를 작성하는 부분입니다.
                    // 예제를 참고하여 다양한 활용법을 확인해 보세요.
                    console.log(data);
                    document.querySelector("#zonecode").value = data.zonecode;
                    document.querySelector("#address").value = data.roadAddress;
                },
            }).open();
        }

        function checkEnter(moveId) {
            if (event.keyCode === 13) {
                if (moveId === "btnDaumAPI") {
                    openDaumAPI();
                } else if (moveId === "create") {
                    doCreate();
                } else {
                    document.querySelector("#" + moveId).focus();
                }
            }
        }

        function goToList() {
            document.location.href = "dom_crud_list.html";
        }
    </script>
</body>

삭제

<script src="//cdn.jsdelivr.net/npm/sweetalert2@11"></script>

async function doDelete() {
    const chks = document.querySelectorAll("[name=chk]:checked");
    if (chks.length > 0) {
        Swal.fire({
            title: "정말 삭제 하시겠습니까?",
            text: "You won't be able to revert this!",
            icon: "warning",
            showCancelButton: true,
            confirmButtonColor: "#3085d6",
            cancelButtonColor: "#d33",
            confirmButtonText: "삭제",
            cancelButtonText: "취소",
        }).then(async (result) => {
            if (result.isConfirmed) {
                for (const chk of chks) {
                    await fetch(`http://localhost:3000/customers/${chk.value}`, {
                        method: "DELETE",
                    });
                }

                Swal.fire({
                    icon: "success",
                    title: "데이터가 정상적으로 삭제 되었습니다.",
                });
                await doSearch();
            }
        });
    } else {
        alert("삭제할 아이템을 선택하세요.");
    }
}

멀티생성

  • 주요 변경 사항
  • IMPORTANT

    bootstrap css & javascript cdn 추가
    컨텐츠 중앙 정렬을 위하여 class=”container” 추가
    label, text, button 스타일 적용
    저장시 유효성 검사 및 알람창 표시
    alert 적용(sweetalert2)

<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>Document</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>

<body>
    <div class="container">
        <!-- 조회 조건 -->
        <div class="mb-2 mt-3">
            <button id="btnSave" onclick="doSave();" class="btn btn-success" disabled>
                저장
            </button>
            <button onclick="addLine();" class="btn btn-dark">라인추가</button>
            <button id="btnRemove" onclick="removeLine();" class="btn btn-danger" disabled>
                라인삭제
            </button>
        </div>

        <!-- 테이블 -->
        <table class="table table-striped table-hover">
            <thead>
                <tr>
                    <th><input type="checkbox" onchange="checkAll();" class="form-check-input" /></th>
                    <th>Name</th>
                    <th>Company</th>
                    <th>Gender</th>
                    <th>Email</th>
                    <th>Phone</th>
                    <th>Address</th>
                </tr>
            </thead>
            <tbody id="tbBody"></tbody>
        </table>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
        crossorigin="anonymous"></script>
    <script src="//cdn.jsdelivr.net/npm/sweetalert2@11"></script>
    <script>
        function addLine() {
            const h = [];
            h.push(`<tr>`);
            h.push(`<td><input type="checkbox" name="chk" onchange="isChecked();" class="form-check-input" /></td>`);
            h.push(`<td><input type="text" name="name" class="form-control" /></td>`);
            h.push(`<td><input type="text" name="company" class="form-control" /></td>`);
            h.push(`<td><select name="gender" class="form-select"><option value="male" selected>남자</option><option value="female">여자</option></select></td>`);
            h.push(`<td><input type="text" name="email" class="form-control" /></td>`);
            h.push(`<td><input type="text" name="phone" class="form-control" /></td>`);
            h.push(`<td><input type="text" name="address" class="form-control" /></td>`);
            h.push(`</tr>`);

            document
                .querySelector("#tbBody")
                .insertAdjacentHTML("beforeend", h.join(""));
        }

        function removeLine() {
            const chks = document.querySelectorAll("[name=chk]:checked");
            if (chks.length > 0) {
                chks.forEach((chk) => {
                    let tr = chk; // INPUT
                    while (tr.tagName !== "TR") {
                        tr = tr.parentNode; // 바로 부모 노드를 가져옴.
                    }

                    tr.remove();
                });
            }
        }

        function checkAll() {
            console.log(event.target.checked);
            const checkValue = event.target.checked;
            const chks = document.querySelectorAll("[name=chk]");
            if (chks.length > 0) {
                for (const chk of chks) {
                    chk.checked = checkValue;
                }
            }

            isChecked();
        }

        function isChecked() {
            const chks = document.querySelectorAll("[name=chk]:checked");
            if (chks.length > 0) {
                document.querySelector("#btnRemove").disabled = false;
                document.querySelector("#btnSave").disabled = false;
            } else {
                document.querySelector("#btnRemove").disabled = true;
                document.querySelector("#btnSave").disabled = true;
            }
        }

        async function doSave() {
            const chks = document.querySelectorAll("[name=chk]");

            if (chks.length > 0) {
                let passRequired = true;
                const blankRows = [];
                let passEmail = true;
                const wrongEmails = [];
                let passPhone = true;
                const wrongPhones = [];
                const customers = [];

                const regexpEmail =
                    /^([a-z]+\d*)+(\.?[a-z]+\d*)+@([a-z]+\d*)+(\.[a-z]{2,3})+$/;
                const regexpTel = /^010-\d{4}-\d{4}$/;
                chks.forEach((chk, index) => {
                    if (chk.checked) {
                        let tr = chk;
                        while (tr.tagName !== "TR") {
                            tr = tr.parentNode; // 바로 부모 노드를 가져옴.
                        }

                        const name = tr.querySelectorAll("[name=name]")[0].value;
                        const company = tr.querySelectorAll("[name=company]")[0].value;
                        const gender = tr.querySelectorAll("[name=gender]")[0].value;
                        const email = tr.querySelectorAll("[name=email]")[0].value;
                        const phone = tr.querySelectorAll("[name=phone]")[0].value;
                        const address = tr.querySelectorAll("[name=address]")[0].value;

                        customers.push({
                            name,
                            company,
                            gender,
                            email,
                            phone,
                            address,
                        });

                        if (name === "" || company === "" || email === "" || phone === "" || address === "") {
                            passRequired = false;
                            blankRows.push(index + 1);
                        }

                        if (!regexpEmail.test(email)) {
                            passEmail = false;
                            wrongEmails.push(index + 1);
                        }

                        if (!regexpTel.test(phone)) {
                            passPhone = false;
                            wrongPhones.push(index + 1);
                        }
                    }
                });

                if (!passRequired) {
                    //return alert(`${blankRows.join(",")}행에 비어 있는 값이 존재합니다. 모든 값을 입력하세요.`);
                    return Swal.fire({
                        icon: "info",
                        title: "필수값 입력 체크",
                        text: `${blankRows.join(",")}행에 비어 있는 값이 존재합니다. 모든 값을 입력하세요.`,
                    });
                }

                if (!passEmail) {
                    //return alert(`${wrongEmails.join(",")}행에 입력한 이메일 형식이 올바르지 않습니다.`);
                    return Swal.fire({
                        icon: "info",
                        title: "이메일 형식 체크",
                        text: `${wrongEmails.join(",")}행에 입력한 이메일 형식이 올바르지 않습니다.`,
                    });
                }

                if (!passPhone) {
                    //return alert(`${wrongPhones.join(",")}행에 입력한 전화번호 형식이 올바르지 않습니다.`);
                    return Swal.fire({
                        icon: "info",
                        title: "전화번호 형식 체크",
                        text: `${wrongPhones.join(",")}행에 입력한 전화번호 형식이 올바르지 않습니다.`,
                    });
                }

                const failData = [];
                Swal.fire({
                    title: "정말 저장하시겠습니까?",
                    text: "You won't be able to revert this!",
                    icon: "warning",
                    showCancelButton: true,
                    confirmButtonColor: "#3085d6",
                    cancelButtonColor: "#d33",
                    confirmButtonText: "생성",
                    cancelButtonText: "취소",
                }).then(async (result) => {
                    if (result.isConfirmed) {
                        for (const customer of customers) {
                            const res = await fetch("http://localhost:3000/customers", {
                                method: "POST",
                                body: JSON.stringify(customer),
                                headers: {
                                    "content-type": "application/json;charset=UTF-8",
                                },
                            });

                            if (res.status !== 201) {
                                failData.push(customer);
                            }
                        }

                        if (failData.length > 0) {
                            Swal.fire({
                                icon: "error",
                                title: `저장에 실패한 데이터가 ${failData.length}건 있습니다.`,
                            });
                        } else {
                            Swal.fire({
                                icon: "success",
                                title: "정상적으로 저장 되었습니다.",
                            });
                        }
                    }
                });
            }
        }
    </script>
</body>

10. 참고

Leave a comment