Stage 2: Frontend Fundamentals

JavaScript · Lesson 06

DOM Manipulation

querySelector, createElement and updating the page from JavaScript.

JavaScript3 min readFree lesson

Every interactive page you have ever used works the same way: the browser parses your HTML into a tree of objects, and JavaScript changes that tree after the page has loaded. That tree is the Document Object Model. This lesson is where JavaScript stops being console exercises and starts changing what people see.

Learning objectives

  • Select elements with getElementById, querySelector and querySelectorAll.
  • Read and write content safely with textContent instead of innerHTML.
  • Create, insert and remove elements.
  • Toggle styling with classList and store data on elements with dataset.

1. Selecting elements

const title = document.getElementById("title");        // one element by id
const firstCard = document.querySelector(".card");      // first match, CSS selector
const allCards = document.querySelectorAll(".card");    // static NodeList
const input = document.querySelector("form input[type='text']");

querySelectorAll returns a static NodeList: it does not update when the page later changes. Convert it when you need array methods:

const cards = [...document.querySelectorAll(".card")];
cards.forEach((card) => card.classList.add("is-visible"));

2. textContent vs innerHTML

const output = document.querySelector("#output");

output.textContent = "Saved";                       // safe: text only
output.innerHTML = "<strong>Saved</strong>";        // parses HTML

innerHTML executes whatever markup it receives. If any part of that string came from a user — a comment, a name, a search query — you have created an XSS hole. Use textContent by default and build markup with createElement.

3. Creating, inserting and removing elements

const list = document.querySelector("#list");

const item = document.createElement("li");
item.textContent = "New task";
item.classList.add("task");

list.append(item);          // add at the end
list.prepend(item);         // add at the start
item.remove();              // remove from the page

append accepts strings and multiple nodes; appendChild accepts a single node. Prefer append.

4. classList

const box = document.querySelector(".box");

box.classList.add("selected");
box.classList.remove("selected");
box.classList.toggle("selected");      // add if missing, remove if present
box.classList.contains("selected");    // true / false

Never write element.className = "selected" unless you intend to delete every other class on the element.

5. dataset — data on the element itself

<button class="swatch" data-color="#FF5733" data-id="42">#FF5733</button>
button.addEventListener("click", () => {
  console.log(button.dataset.color); // "#FF5733"
  console.log(button.dataset.id);    // "42"
});

data-* attributes let you keep per-element state in the markup instead of in parallel arrays. Values are always strings.

6. A complete pattern: render from data

const people = [
  { name: "Aarav", role: "Frontend" },
  { name: "Meera", role: "Backend" },
];

const list = document.querySelector("#people");
list.replaceChildren(); // clear without innerHTML

for (const person of people) {
  const card = document.createElement("article");
  card.className = "person";

  const name = document.createElement("h3");
  name.textContent = person.name;

  const role = document.createElement("p");
  role.textContent = person.role;

  card.append(name, role);
  list.append(card);
}

This is the same loop React will generate for you later — it is worth doing once by hand.

Common mistakes

  • Forgetting that the script runs before the element exists. Place the script at the end of <body> or use defer.
  • Using innerHTML with anything a user typed.
  • Re-querying the DOM inside a long loop. Query once, reuse the reference.

Practice

Build the colour palette generator described in the assignment below. It combines every idea in this lesson: creating elements, storing the HEX value in dataset, toggling a selected class and removing nodes.

Example files

Complete, runnable files used in this lesson. Copy them into your project and experiment.

index.htmlHTML
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>강의 06 — DOM 조작 예제 (동적 카드 생성기)</title>
  <style>
    body {
      font-family: 'Segoe UI', sans-serif;
      max-width: 900px;
      margin: 40px auto;
      padding: 0 20px;
      background: #f5f5f5;
    }
    h1 { color: #2c3e50; border-bottom: 3px solid #e67e22; padding-bottom: 10px; }
    /* 컨트롤 영역 */
    .controls {
      background: white;
      border-radius: 12px;
      padding: 20px;
      margin-bottom: 20px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.08);
      display: flex;
      gap: 10px;
      flex-wrap: wrap;
      align-items: flex-end;
    }
    .field {
      display: flex;
      flex-direction: column;
      gap: 5px;
      flex: 1;
      min-width: 150px;
    }
    .field label { font-size: 13px; color: #7f8c8d; font-weight: bold; }
    .field input, .field select {
      padding: 10px;
      border: 2px solid #ddd;
      border-radius: 8px;
      font-size: 14px;
    }
    .field input:focus, .field select:focus { border-color: #e67e22; outline: none; }
    /* 추가 버튼 */
    .add-btn {
      padding: 12px 24px;
      background: #e67e22;
      color: white;
      border: none;
      border-radius: 8px;
      font-size: 15px;
      font-weight: bold;
      cursor: pointer;
      transition: background 0.2s;
      white-space: nowrap;
    }
    .add-btn:hover { background: #ca6f1e; }
    /* 카드 그리드 */
    .card-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
      gap: 16px;
    }
    /* 동적 카드 */
    .card {
      background: white;
      border-radius: 12px;
      padding: 20px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.08);
      position: relative;
      transition: transform 0.2s, box-shadow 0.2s;
      animation: fadeIn 0.3s ease; /* 등장 애니메이션 */
    }
    /* 카드 등장 애니메이션 */
    @keyframes fadeIn {
      from { opacity: 0; transform: scale(0.9); }
      to   { opacity: 1; transform: scale(1); }
    }
    .card:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(0,0,0,0.12); }
    /* 카드 색상 테마 */
    .theme-blue   { border-top: 4px solid #3498db; }
    .theme-green  { border-top: 4px solid #27ae60; }
    .theme-red    { border-top: 4px solid #e74c3c; }
    .theme-purple { border-top: 4px solid #9b59b6; }
    .theme-orange { border-top: 4px solid #e67e22; }
    /* 카드 내용 */
    .card-title { font-size: 17px; font-weight: bold; color: #2c3e50; margin-bottom: 8px; }
    .card-body { font-size: 14px; color: #7f8c8d; line-height: 1.5; }
    .card-footer {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-top: 15px;
      padding-top: 12px;
      border-top: 1px solid #ecf0f1;
    }
    .card-id { font-size: 12px; color: #bdc3c7; }
    /* 카드 내 삭제 버튼 */
    .card-delete {
      padding: 5px 12px;
      background: #fdecea;
      color: #e74c3c;
      border: none;
      border-radius: 6px;
      font-size: 13px;
      cursor: pointer;
      transition: background 0.2s;
    }
    .card-delete:hover { background: #fadbd8; }
    /* 즐겨찾기 토글 버튼 */
    .card-fav {
      position: absolute;
      top: 12px;
      right: 12px;
      background: none;
      border: none;
      font-size: 20px;
      cursor: pointer;
      transition: transform 0.2s;
    }
    .card-fav:hover { transform: scale(1.3); }
    /* 전체 삭제 버튼 */
    .clear-btn {
      padding: 10px 20px;
      background: #ecf0f1;
      color: #7f8c8d;
      border: none;
      border-radius: 8px;
      font-size: 14px;
      cursor: pointer;
      margin-bottom: 15px;
    }
    .clear-btn:hover { background: #dfe6e9; }
    /* 카운터 */
    .counter { font-size: 14px; color: #7f8c8d; margin-bottom: 10px; }
    .empty-state {
      grid-column: 1 / -1;
      text-align: center;
      color: #bbb;
      padding: 50px;
      font-size: 16px;
    }
  </style>
</head>
<body>

<h1>강의 06 — 동적 카드 생성기</h1>

<!-- 컨트롤 영역 -->
<div class="controls">
  <!-- 제목 입력 -->
  <div class="field">
    <label for="cardTitle">제목</label>
    <input type="text" id="cardTitle" placeholder="카드 제목" maxlength="30">
  </div>
  <!-- 내용 입력 -->
  <div class="field">
    <label for="cardBody">내용</label>
    <input type="text" id="cardBody" placeholder="카드 내용" maxlength="60">
  </div>
  <!-- 테마 선택 -->
  <div class="field" style="min-width:120px">
    <label for="cardTheme">테마</label>
    <select id="cardTheme">
      <option value="theme-blue">파랑</option>
      <option value="theme-green">초록</option>
      <option value="theme-red">빨강</option>
      <option value="theme-purple">보라</option>
      <option value="theme-orange">주황</option>
    </select>
  </div>
  <!-- 카드 추가 버튼 -->
  <button class="add-btn" onclick="addCard()">카드 추가</button>
</div>

<!-- 카드 카운터 및 전체 삭제 -->
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
  <div class="counter" id="counter">카드 0개</div>
  <button class="clear-btn" onclick="clearAllCards()">전체 삭제</button>
</div>

<!-- 카드 그리드 컨테이너 -->
<div class="card-grid" id="cardGrid">
  <div class="empty-state" id="emptyState">🃏 위에서 카드를 추가해보세요!</div>
</div>

<script>
  // 카드 ID 카운터 (dataset에 저장)
  let cardCount = 0;

  /**
   * 새 카드를 동적으로 생성하여 그리드에 추가하는 함수
   * createElement, appendChild, classList, dataset 활용
   */
  function addCard() {
    // 입력값 읽기
    const titleInput = document.getElementById('cardTitle');
    const bodyInput  = document.getElementById('cardBody');
    const themeSelect = document.getElementById('cardTheme');

    const title = titleInput.value.trim();
    const body  = bodyInput.value.trim();
    const theme = themeSelect.value;

    // 제목이 없으면 기본값 사용
    const cardTitle = title || '제목 없음';
    const cardBody  = body  || '내용을 입력해주세요.';

    // 고유 ID 증가
    cardCount++;

    // ── createElement로 카드 요소 생성 ──────

    // 카드 컨테이너 div 생성
    const card = document.createElement('div');

    // classList.add로 클래스 추가
    card.classList.add('card', theme);

    // dataset으로 카드 ID 저장
    card.dataset.cardId = cardCount;

    // ── innerHTML로 카드 내용 구성 ──────────
    // (사용자 입력은 textContent로 처리하고,
    //  레이아웃용 HTML만 innerHTML에 사용)
    card.innerHTML = `
      <button class="card-fav" title="즐겨찾기 토글">⭐</button>
      <div class="card-title"></div>
      <div class="card-body"></div>
      <div class="card-footer">
        <span class="card-id">#${cardCount}</span>
        <button class="card-delete">삭제</button>
      </div>
    `;

    // 사용자 입력은 textContent로 안전하게 삽입 (XSS 방지)
    card.querySelector('.card-title').textContent = cardTitle;
    card.querySelector('.card-body').textContent = cardBody;

    // ── 삭제 버튼 이벤트 연결 ───────────────
    const deleteBtn = card.querySelector('.card-delete');
    deleteBtn.addEventListener('click', function() {
      // removeChild 대신 remove()로 삭제
      card.remove();
      updateCounter();
      toggleEmptyState();
    });

    // ── 즐겨찾기 토글 이벤트 ───────────────
    const favBtn = card.querySelector('.card-fav');
    favBtn.addEventListener('click', function() {
      // classList.toggle로 즐겨찾기 상태 전환
      const isFav = card.classList.toggle('favorited');
      favBtn.textContent = isFav ? '⭐' : '☆';

      // dataset에 상태 저장
      card.dataset.favorited = isFav;
    });

    // ── 카드를 그리드에 추가 ─────────────────
    const grid = document.getElementById('cardGrid');

    // 빈 상태 메시지 숨기기
    const emptyState = document.getElementById('emptyState');
    if (emptyState) emptyState.style.display = 'none';

    // appendChild로 그리드에 카드 추가
    grid.appendChild(card);

    // 입력창 초기화
    titleInput.value = '';
    bodyInput.value = '';
    titleInput.focus();

    // 카운터 업데이트
    updateCounter();
  }

  /**
   * 현재 카드 개수를 세어 카운터를 업데이트하는 함수
   * querySelectorAll로 모든 카드 선택
   */
  function updateCounter() {
    // querySelectorAll로 현재 화면의 모든 카드 선택
    const cards = document.querySelectorAll('.card');
    const counter = document.getElementById('counter');
    counter.textContent = `카드 ${cards.length}개`;
  }

  /**
   * 카드가 없을 때 빈 상태 메시지를 표시/숨기는 함수
   */
  function toggleEmptyState() {
    const grid = document.getElementById('cardGrid');
    const cards = grid.querySelectorAll('.card');
    let emptyState = document.getElementById('emptyState');

    if (cards.length === 0) {
      // 빈 상태 메시지가 없으면 새로 생성
      if (!emptyState) {
        emptyState = document.createElement('div');
        emptyState.id = 'emptyState';
        emptyState.className = 'empty-state';
        emptyState.textContent = '🃏 위에서 카드를 추가해보세요!';
        grid.appendChild(emptyState);
      } else {
        emptyState.style.display = 'block';
      }
    }
  }

  /**
   * 모든 카드를 삭제하는 함수
   * querySelectorAll + forEach 활용
   */
  function clearAllCards() {
    const cards = document.querySelectorAll('.card');
    if (cards.length === 0) return;

    // 모든 카드를 forEach로 순회하며 제거
    cards.forEach(card => card.remove());

    updateCounter();
    toggleEmptyState();
  }

  // Enter 키로 카드 추가
  ['cardTitle', 'cardBody'].forEach(id => {
    document.getElementById(id).addEventListener('keydown', e => {
      if (e.key === 'Enter') addCard();
    });
  });
</script>

</body>
</html>

Assignment

Assignment 06 — Colour palette generator

Build a small app that creates colour swatches on demand and lets the visitor select, copy and delete them.

Requirements

  1. A Add colour button appends a new box with a random background colour.
  2. Each box stores its HEX value in data-color (use dataset.color).
  3. Clicking a box marks it as selected (classList), removes the selection from the previous box, and shows the HEX code in a status line.
  4. Clicking the HEX code copies it with navigator.clipboard.writeText.
  5. Every box has a small delete button that removes just that box.

Hints

function randomHex() {
  return "#" + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, "0");
}
  • Create the box with document.createElement("div").
  • Set box.dataset.color = color and box.style.backgroundColor = color.
  • Use box.classList.add("selected") / box.classList.remove("selected").
  • box.remove() deletes a single swatch.

Done when

  • Adding twenty boxes does not slow the page down.
  • The selected outline always follows the last box you clicked.
  • Deleting a box never deletes the others.
Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

index.htmlHTML
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>과제 06 정답 — 색상 팔레트 생성기</title>
  <style>
    body {
      font-family: 'Segoe UI', sans-serif;
      max-width: 750px;
      margin: 40px auto;
      padding: 0 20px;
      background: #1a1a2e;
      color: white;
    }
    h1 { text-align: center; color: white; margin-bottom: 30px; }
    /* 상단 컨트롤 */
    .controls {
      display: flex;
      gap: 10px;
      margin-bottom: 25px;
      flex-wrap: wrap;
    }
    .generate-btn {
      flex: 1;
      padding: 14px;
      background: linear-gradient(135deg, #667eea, #764ba2);
      color: white;
      border: none;
      border-radius: 10px;
      font-size: 16px;
      font-weight: bold;
      cursor: pointer;
      transition: opacity 0.2s, transform 0.1s;
    }
    .generate-btn:hover { opacity: 0.9; }
    .generate-btn:active { transform: scale(0.98); }
    .count-btn {
      padding: 14px 20px;
      background: #16213e;
      color: #a0aec0;
      border: 1px solid #2d3748;
      border-radius: 10px;
      font-size: 14px;
      cursor: pointer;
    }
    .count-btn:hover { background: #2d3748; }
    /* 팔레트 그리드 */
    .palette-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
      gap: 12px;
      margin-bottom: 25px;
      min-height: 120px;
    }
    /* 색상 박스 */
    .color-box {
      height: 100px;
      border-radius: 10px;
      cursor: pointer;
      position: relative;
      transition: transform 0.2s, box-shadow 0.2s;
      display: flex;
      align-items: flex-end;
      justify-content: center;
      padding-bottom: 8px;
    }
    .color-box:hover { transform: scale(1.05); }
    /* 선택된 박스 */
    .color-box.selected {
      outline: 4px solid white;
      outline-offset: 2px;
      transform: scale(1.08);
      box-shadow: 0 0 20px rgba(255,255,255,0.3);
    }
    /* 박스 내 HEX 텍스트 */
    .hex-label {
      font-size: 11px;
      font-weight: bold;
      background: rgba(0,0,0,0.5);
      color: white;
      padding: 2px 6px;
      border-radius: 4px;
      font-family: monospace;
      opacity: 0;
      transition: opacity 0.2s;
    }
    /* 호버 시 HEX 텍스트 표시 */
    .color-box:hover .hex-label { opacity: 1; }
    /* 삭제 버튼 */
    .box-delete {
      position: absolute;
      top: 6px;
      right: 6px;
      width: 22px;
      height: 22px;
      background: rgba(0,0,0,0.5);
      color: white;
      border: none;
      border-radius: 50%;
      font-size: 12px;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      opacity: 0;
      transition: opacity 0.2s;
    }
    .color-box:hover .box-delete { opacity: 1; }
    .box-delete:hover { background: rgba(231,76,60,0.8); }
    /* 선택된 색상 정보 패널 */
    .color-info {
      background: #16213e;
      border-radius: 12px;
      padding: 20px 25px;
      display: none; /* 처음에는 숨김 */
      align-items: center;
      gap: 20px;
      border: 1px solid #2d3748;
    }
    /* 선택된 색상 미리보기 */
    .selected-preview {
      width: 60px;
      height: 60px;
      border-radius: 10px;
      flex-shrink: 0;
    }
    .color-details { flex: 1; }
    .hex-display {
      font-size: 28px;
      font-weight: bold;
      font-family: monospace;
      cursor: pointer;
      transition: opacity 0.2s;
    }
    .hex-display:hover { opacity: 0.7; }
    .copy-hint { font-size: 13px; color: #718096; margin-top: 4px; }
    /* 복사 알림 */
    .copy-toast {
      position: fixed;
      bottom: 30px;
      left: 50%;
      transform: translateX(-50%);
      background: #27ae60;
      color: white;
      padding: 12px 24px;
      border-radius: 24px;
      font-size: 15px;
      font-weight: bold;
      opacity: 0;
      transition: opacity 0.3s;
      pointer-events: none;
      z-index: 100;
    }
    .copy-toast.show { opacity: 1; }
    /* 빈 상태 */
    .empty-palette {
      grid-column: 1 / -1;
      text-align: center;
      color: #4a5568;
      padding: 40px;
      font-size: 15px;
    }
    /* 하단 통계 */
    .stats-bar {
      display: flex;
      justify-content: space-between;
      font-size: 13px;
      color: #718096;
      margin-bottom: 15px;
    }
    .clear-btn {
      background: none;
      color: #e74c3c;
      border: none;
      cursor: pointer;
      font-size: 13px;
    }
    .clear-btn:hover { text-decoration: underline; }
  </style>
</head>
<body>

<h1>🎨 색상 팔레트 생성기</h1>

<!-- 컨트롤 영역 -->
<div class="controls">
  <!-- 단일 박스 생성 버튼 -->
  <button class="generate-btn" onclick="addColorBox()">+ 랜덤 색상 추가</button>
  <!-- 5개 일괄 생성 버튼 -->
  <button class="count-btn" onclick="addMultiple(5)">5개 추가</button>
  <!-- 전체 지우기 버튼 -->
  <button class="count-btn" onclick="clearAll()">전체 삭제</button>
</div>

<!-- 통계 바 -->
<div class="stats-bar">
  <span id="boxCount">박스 0개</span>
  <button class="clear-btn" onclick="clearAll()">모두 지우기</button>
</div>

<!-- 색상 팔레트 그리드 -->
<div class="palette-grid" id="paletteGrid">
  <div class="empty-palette" id="emptyMsg">버튼을 클릭해 색상 박스를 추가해보세요!</div>
</div>

<!-- 선택된 색상 정보 패널 -->
<div class="color-info" id="colorInfo">
  <div class="selected-preview" id="selectedPreview"></div>
  <div class="color-details">
    <div class="hex-display" id="hexDisplay" onclick="copyToClipboard()"></div>
    <div class="copy-hint">클릭하여 HEX 코드 복사</div>
  </div>
</div>

<!-- 복사 알림 토스트 -->
<div class="copy-toast" id="copyToast">✓ 클립보드에 복사되었습니다!</div>

<script>
  // 현재 선택된 박스 참조 (null이면 선택 없음)
  let selectedBox = null;

  // ─────────────────────────────────────────
  // 랜덤 HEX 색상 생성 함수
  // ─────────────────────────────────────────

  /**
   * 랜덤 HEX 색상 문자열을 생성하는 함수
   * @returns {string} '#RRGGBB' 형태의 HEX 색상
   */
  function randomHex() {
    // Math.random() * 0xFFFFFF → 0~16777215 사이의 랜덤 숫자
    // toString(16) → 16진수 문자열로 변환
    // padStart(6, '0') → 6자리 미만이면 앞에 '0' 채우기
    return '#' + Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, '0').toUpperCase();
  }

  // ─────────────────────────────────────────
  // 색상 박스 생성 함수
  // ─────────────────────────────────────────

  /**
   * 랜덤 색상 박스를 생성하여 팔레트 그리드에 추가하는 함수
   * createElement, classList, dataset 활용
   */
  function addColorBox() {
    const grid = document.getElementById('paletteGrid');

    // 빈 상태 메시지 제거
    const emptyMsg = document.getElementById('emptyMsg');
    if (emptyMsg) emptyMsg.remove();

    // 랜덤 HEX 색상 생성
    const color = randomHex();

    // ── createElement로 박스 생성 ────────────
    const box = document.createElement('div');

    // classList.add로 클래스 추가
    box.classList.add('color-box');

    // 배경색 설정
    box.style.backgroundColor = color;

    // dataset에 색상 값 저장 (클릭 시 읽기 위해)
    box.dataset.color = color;

    // 박스 내부 HTML 구성
    box.innerHTML = `
      <span class="hex-label">${color}</span>
      <button class="box-delete" title="삭제">✕</button>
    `;

    // ── 박스 클릭 이벤트 (HEX 표시 + 선택 상태) ──

    box.addEventListener('click', function(event) {
      // 삭제 버튼 클릭은 여기서 처리하지 않음
      if (event.target.classList.contains('box-delete')) return;

      // 이전에 선택된 박스의 selected 클래스 제거
      if (selectedBox) {
        selectedBox.classList.remove('selected'); // classList.remove 사용
      }

      // 현재 박스에 selected 클래스 추가
      box.classList.add('selected'); // classList.add 사용
      selectedBox = box; // 선택된 박스 갱신

      // dataset에서 색상 읽어 패널에 표시
      const hexColor = box.dataset.color; // dataset.color로 읽기
      showColorInfo(hexColor);
    });

    // ── 삭제 버튼 클릭 이벤트 ──────────────

    const deleteBtn = box.querySelector('.box-delete');
    deleteBtn.addEventListener('click', function(event) {
      event.stopPropagation(); // 부모(box) 클릭 이벤트 전파 차단

      // 삭제할 박스가 현재 선택된 박스이면 패널 숨기기
      if (selectedBox === box) {
        selectedBox = null;
        document.getElementById('colorInfo').style.display = 'none';
      }

      // 박스 제거 (remove 메서드)
      box.remove();

      // 박스가 없으면 빈 상태 메시지 복구
      updateEmptyState();
      updateBoxCount();
    });

    // 그리드에 박스 추가
    grid.appendChild(box);

    // 박스 수 업데이트
    updateBoxCount();
  }

  /**
   * 여러 개의 박스를 한 번에 추가하는 함수
   * @param {number} count - 추가할 박스 수
   */
  function addMultiple(count) {
    for (let i = 0; i < count; i++) {
      addColorBox();
    }
  }

  // ─────────────────────────────────────────
  // 색상 정보 패널 표시
  // ─────────────────────────────────────────

  /**
   * 선택된 색상 정보를 하단 패널에 표시하는 함수
   * @param {string} hexColor - 표시할 HEX 색상 문자열
   */
  function showColorInfo(hexColor) {
    const infoPanel = document.getElementById('colorInfo');
    const preview = document.getElementById('selectedPreview');
    const hexDisplay = document.getElementById('hexDisplay');

    // 패널 표시
    infoPanel.style.display = 'flex';

    // 색상 미리보기 업데이트
    preview.style.backgroundColor = hexColor;

    // HEX 코드 텍스트 표시 (textContent로 안전하게)
    hexDisplay.textContent = hexColor;
    hexDisplay.style.color = hexColor; // 색상 자체로 글자 색상 설정
  }

  // ─────────────────────────────────────────
  // 클립보드 복사
  // ─────────────────────────────────────────

  /**
   * 현재 표시된 HEX 코드를 클립보드에 복사하는 함수
   * Clipboard API (비동기) 활용
   */
  function copyToClipboard() {
    const hexDisplay = document.getElementById('hexDisplay');
    const color = hexDisplay.textContent;

    if (!color) return;

    // Clipboard API로 비동기 복사
    navigator.clipboard.writeText(color).then(() => {
      showToast(`${color} 복사됨!`);
    }).catch(() => {
      // Clipboard API를 지원하지 않는 환경을 위한 폴백
      showToast(`${color}`);
    });
  }

  /**
   * 화면 하단에 잠깐 나타났다 사라지는 알림을 표시하는 함수
   * @param {string} message - 표시할 메시지
   */
  function showToast(message) {
    const toast = document.getElementById('copyToast');
    toast.textContent = `✓ ${message} 클립보드에 복사되었습니다!`;

    // classList.add로 토스트 표시
    toast.classList.add('show');

    // 2초 후 classList.remove로 숨기기
    setTimeout(() => {
      toast.classList.remove('show');
    }, 2000);
  }

  // ─────────────────────────────────────────
  // 유틸리티 함수
  // ─────────────────────────────────────────

  /**
   * 박스가 없을 때 빈 상태 메시지를 표시하는 함수
   */
  function updateEmptyState() {
    const grid = document.getElementById('paletteGrid');
    // querySelectorAll로 현재 박스들 확인
    const boxes = grid.querySelectorAll('.color-box');

    if (boxes.length === 0 && !document.getElementById('emptyMsg')) {
      // createElement로 빈 상태 메시지 생성
      const emptyMsg = document.createElement('div');
      emptyMsg.id = 'emptyMsg';
      emptyMsg.className = 'empty-palette';
      emptyMsg.textContent = '버튼을 클릭해 색상 박스를 추가해보세요!';
      grid.appendChild(emptyMsg);
    }
  }

  /**
   * 박스 개수를 세어 통계 바를 업데이트하는 함수
   */
  function updateBoxCount() {
    const count = document.querySelectorAll('.color-box').length;
    document.getElementById('boxCount').textContent = `박스 ${count}개`;
  }

  /**
   * 모든 박스를 삭제하는 함수
   */
  function clearAll() {
    const boxes = document.querySelectorAll('.color-box');
    // forEach로 모든 박스 제거
    boxes.forEach(box => box.remove());

    // 선택 상태 초기화
    selectedBox = null;
    document.getElementById('colorInfo').style.display = 'none';

    updateEmptyState();
    updateBoxCount();
  }
</script>

</body>
</html>

Learn this lesson with a mentor

Work through the Build Your Homepage series on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now