K-BOOKS · WEB DESIGN PRACTICAL

빈 폴더부터 따라 만드는 실기 18단계

R37 · 파일 위치 → 실제 입력 코드 → 이유 → 정상 결과 → 오류 복구 → 직접 변경 · A4 출력용

이 교재는 K-BOOKS 자체 제작 연습용입니다. 각 단계 START에서 시작하여 안내 코드만 변경하면 FINISH를 재현할 수 있습니다. 앞 단계 FINISH는 다음 단계 START와 동일합니다. 시험 3시간 시간제한은 마지막 모의시험에서 별도로 연습합니다.

2026년 Q-Net 기준: 3시간, 제출 데이터 10MB. 시험장 규칙은 최신 공식 공지를 우선합니다.

T01 빈 폴더와 파일 3개 만들기

VS Code에서 첫 폴더를 열고 3개 빈 파일과 제공 이미지를 준비합니다.

복습: B01 · I01

학생에게 먼저 설명: 코드 작성 전 3개 파일이 어떤 역할을 하는지 말할 수 있어야 합니다.

수정 파일: 폴더와 파일

폴더 직접 만들기 / 찾을 곳:

Windows + VS Code에서 이렇게 진행하세요.
1. 파일 탐색기에서 '실기연습' 새 폴더 만들기
2. VS Code > 파일 > 폴더 열기 > 실기연습 선택
3. 왼쪽 탐색기에서 새 파일(index.html) 만들기
4. 새 폴더(css)를 만들고 그 안에 style.css 생성
5. 새 폴더(js)를 만들고 그 안에 script.js 생성
6. 새 폴더(images)를 만들고 00_지급자료의 이미지 7개 복사
7. 새 폴더(source)를 만들고 content.txt 복사
8. Ctrl+S 저장 후 아래 폴더 구조를 대조

실기연습/
├── index.html
├── css/style.css
├── js/script.js
├── images/ (이미지 7개)
└── source/content.txt

왜 필요한가?

코드·용어의미
index.html브라우저가 먼저 읽는 기본 문서입니다.
css/style.css색·너비·배치 같은 모양을 담당합니다.
js/script.js클릭·슬라이드 같은 움직임을 담당합니다.

정상 결과

이 단계에서는 웹 화면이 빈 것이 정상입니다. 파일 준비에 집중합니다.

오류 복구

직접 바꿔보기

images/slide1.jpg를 찾아 열어 보고 파일명이 맞는지 확인합니다.

내가 확인할 것

START = examples/T01/START/ · FINISH = examples/T01/FINISH/

T02 첫 HTML 문서 작성

빈 index.html에 HTML5의 최소 문서를 직접 입력합니다.

복습: B01 · B02

학생에게 먼저 설명: PAGE_CONTENT 주석은 다음 단계에서 코드를 놓을 정확한 자리입니다.

수정 파일: index.html

파일 전체 작성 / 찾을 곳: (빈 파일 전체)

<!doctype html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>여울 문화공간 | 실기 첫걸음</title>
  <link rel="stylesheet" href="css/style.css">
  <script src="js/script.js" defer></script>
</head>
<body>
  <!-- PAGE_CONTENT -->
</body>
</html>

왜 필요한가?

코드·용어의미
<!doctype html>브라우저에 HTML5 문서라고 알려 줍니다.
<head>문자 인코딩·제목·CSS 연결처럼 화면 밖 설정을 담습니다.
<body>실제 페이지에 보일 내용을 담습니다.
deferHTML을 읽은 뒤 JavaScript를 실행하도록 합니다.

정상 결과

화면은 아직 비어 있고 탭 제목만 보입니다.

오류 복구

직접 바꿔보기

브라우저 탭의 제목을 내 이름을 넣어 바꾸고 새로고침합니다.

내가 확인할 것

START = examples/T02/START/ · FINISH = examples/T02/FINISH/

T03 홈페이지 뼈대와 메뉴 만들기

header·main·footer를 만들고 로고와 4개 메뉴를 배치합니다.

복습: B03 · B04 · B07

학생에게 먼저 설명: 부모(header) 안의 자식(nav)을 소리 내어 설명해 보세요.

수정 파일: index.html

표시된 문자열 하나를 통째로 교체 / 찾을 곳: <!-- PAGE_CONTENT -->

<div class="site-shell">
  <header class="site-header">
    <div class="header-inner">
      <a class="brand" href="#about"><img src="images/logo.svg" alt="여울 문화공간"></a>
      <button type="button" class="nav-toggle" aria-controls="mainNav" aria-expanded="false">전체 메뉴</button>
      <!-- NAV_SLOT -->
    </div>
  </header>
  <main id="main">
    <section id="about"><!-- HERO_SLOT --></section>
    <section id="program"><!-- CONTENT_SLOT --></section>
    <section id="gallery"><!-- GALLERY_SLOT --></section>
  </main>
  <footer class="site-footer" id="contact"><!-- FOOTER_SLOT --></footer>
</div>
<!-- POPUP_SLOT -->

수정 파일: index.html

표시된 문자열 하나를 통째로 교체 / 찾을 곳: <!-- NAV_SLOT -->

<nav class="main-nav" id="mainNav" aria-label="주 메뉴">
  <ul>
    <li><a href="#about">공간 소개</a>
      <ul class="submenu"><li><a href="#about">인사말</a></li><li><a href="#contact">오시는 길</a></li></ul>
    </li>
    <li><a href="#program">프로그램</a>
      <ul class="submenu"><li><a href="#program">교육 안내</a></li><li><a href="#program">신청 방법</a></li></ul>
    </li>
    <li><a href="#gallery">전시·공연</a>
      <ul class="submenu"><li><a href="#gallery">전시 소식</a></li><li><a href="#gallery">공연 일정</a></li></ul>
    </li>
    <li><a href="#contact">이용 안내</a>
      <ul class="submenu"><li><a href="#contact">오시는 길</a></li><li><a href="#contact">문의</a></li></ul>
    </li>
  </ul>
</nav>

왜 필요한가?

코드·용어의미
header / main / footer상단, 본문, 하단을 나누는 큰 부모 영역입니다.
nav > ul > li > a메뉴의 부모·자식 관계입니다. ul은 목록, li는 항목, a는 링크입니다.
href="#gallery"id="gallery"를 가진 영역으로 이동한다는 뜻입니다.

정상 결과

로고와 주 메뉴 4개가 보이고, 아직 디자인 전이라 단순하게 출력됩니다.

오류 복구

직접 바꿔보기

두 번째 메뉴의 글자를 내가 원하는 이름으로 바꿔 보세요.

내가 확인할 것

START = examples/T03/START/ · FINISH = examples/T03/FINISH/

T04 헤더 CSS와 1200px 폭

로고와 메뉴를 가로 배치하고 페이지 최대 폭을 지정합니다.

복습: B06 · B07 · B09

학생에게 먼저 설명: CSS를 쓰는 시점부터 PC와 모바일을 함께 확인합니다.

수정 파일: css/style.css

파일 끝에 추가 / 찾을 곳: (파일 맨 아래)

/* [1] 전체 폭·글자·링크와 헤더 */
* { box-sizing:border-box; }
html { scroll-behavior:smooth; }
body { margin:0; background:#edf2f7; color:#20344c; font:15px/1.6 system-ui,"Malgun Gothic",sans-serif; }
[hidden] { display:none !important; }
img { display:block; max-width:100%; }
a { color:inherit; text-decoration:none; }
button { font:inherit; cursor:pointer; }
a:focus-visible,button:focus-visible { outline:3px solid #f0a36d; outline-offset:2px; }
.site-shell { width:100%; max-width:1200px; margin:0 auto; background:white; box-shadow:0 14px 50px #142c4212; }
.site-header { position:relative; z-index:20; background:white; }
.header-inner { min-height:94px; padding:15px 28px; display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; }
.brand { display:block; flex:none; width:158px; }
.brand img { width:100%; height:62px; object-fit:contain; }
.nav-toggle { display:none; padding:10px; border:1px solid #cad6e3; background:#fff; border-radius:7px; }
.main-nav>ul { display:flex; flex-wrap:wrap; gap:6px; list-style:none; margin:0; padding:0; }
.main-nav li { position:relative; list-style:none; min-width:0; }
.main-nav>ul>li>a { display:block; padding:12px 9px; font-weight:750; }
.submenu { display:none; position:absolute; top:100%; left:0; min-width:155px; margin:0; padding:6px 0; list-style:none; color:white; background:#1c3553; box-shadow:0 10px 24px #11283c30; }
.main-nav li:hover>.submenu,.main-nav li:focus-within>.submenu { display:block; }
.submenu a { display:block; padding:8px 12px; }
.submenu a:hover { background:#3a5878; }
@media(max-width:700px){.header-inner{padding:12px 16px}.main-nav{width:100%}.main-nav>ul{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.main-nav>ul>li>a{padding:9px 7px;border:1px solid #e4ebf2;border-radius:6px}.submenu{position:static;box-shadow:none}}

왜 필요한가?

코드·용어의미
max-width:1200pxPC에서 사이트가 지나치게 넓어지지 않게 상한선을 정합니다.
display:flex부모 안의 로고와 메뉴를 한 줄로 정렬합니다.
flex-wrap:wrap공간이 모자라면 다음 줄로 자연스럽게 내려가도록 합니다.

정상 결과

PC는 1200px 이내의 헤더, 모바일은 2열로 줄어드는 메뉴입니다.

오류 복구

직접 바꿔보기

max-width를 1000px로 변경한 뒤 PC 화면만 어떻게 달라지는지 보세요.

내가 확인할 것

START = examples/T04/START/ · FINISH = examples/T04/FINISH/

T05 3장 이미지 슬라이드 HTML

같은 부모 안에 이미지 3장과 캡션을 차례로 배치합니다.

복습: B04 · B05 · B07

학생에게 먼저 설명: 슬라이드 동작은 JavaScript 단계에서 연결하므로 지금 움직이지 않아도 정상입니다.

수정 파일: index.html

표시된 문자열 하나를 통째로 교체 / 찾을 곳: <!-- HERO_SLOT -->

<div class="hero" aria-label="이달의 문화 소식">
  <div class="slides" id="heroSlides">
    <article class="slide is-active" aria-hidden="false">
      <img src="images/slide1.jpg" alt="여울 문화공간 전시 이미지">
      <div class="slide-caption"><span>SPACE & ART</span><h1>예술이 머무는<br>우리 동네 문화공간</h1><p>전시와 교육, 공연을 한 자리에서 만나보세요.</p></div>
    </article>
    <article class="slide" aria-hidden="true">
      <img src="images/slide2.jpg" alt="창작 프로그램 이미지">
      <div class="slide-caption"><span>LEARNING</span><h2>함께 만들고 나누는 시간</h2><p>다양한 창작 워크숍이 열립니다.</p></div>
    </article>
    <article class="slide" aria-hidden="true">
      <img src="images/slide3.jpg" alt="공연 행사 이미지">
      <div class="slide-caption"><span>COMMUNITY</span><h2>일상을 채우는 작은 공연</h2><p>여울에서 만나는 새로운 이야기.</p></div>
    </article>
  </div>
  <div class="hero-controls"><span id="slideStatus">01 / 03</span><button type="button" id="slidePause">일시정지</button></div>
</div>

왜 필요한가?

코드·용어의미
.hero큰 슬라이드 영역을 감싸는 부모입니다.
.slide세 장의 슬라이드 각각에 붙는 같은 이름입니다.
.is-active처음 화면에서 보일 한 장을 표시하는 추가 클래스입니다.

정상 결과

슬라이드 이미지 3장과 텍스트가 보입니다. 겹치기 전이라 세로로 보일 수 있습니다.

오류 복구

직접 바꿔보기

첫 번째 이미지 설명(alt)을 내가 이해한 문장으로 바꿔 보세요.

내가 확인할 것

START = examples/T05/START/ · FINISH = examples/T05/FINISH/

T06 3장 이미지를 포개는 CSS

세 장을 같은 자리에 겹치고 첫 장만 보이게 만듭니다.

복습: B07 · B08 · B11

학생에게 먼저 설명: 이미지를 3장 넣는 것과 슬라이드가 작동하는 것은 서로 다른 작업입니다.

수정 파일: css/style.css

파일 끝에 추가 / 찾을 곳: (파일 맨 아래)

/* [2] 3장의 이미지를 같은 자리에 포개는 페이드 슬라이드 */
.hero { position:relative; height:320px; overflow:hidden; background:#243a54; }
.slides,.slide { position:absolute; inset:0; }
.slide { opacity:0; pointer-events:none; transition:opacity .65s ease; }
.slide.is-active { opacity:1; pointer-events:auto; z-index:1; }
.slide img { width:100%; height:100%; object-fit:cover; }
.slide::after { content:""; position:absolute; inset:0; background:linear-gradient(90deg,#0f243cc9,#0f243c14); }
.slide-caption { position:absolute; z-index:2; inset:0 auto auto 0; padding:51px 46px; max-width:80%; color:#fff; }
.slide-caption span { font-size:12px; letter-spacing:.2em; font-weight:800; color:#ffcda5; }
.slide-caption h1,.slide-caption h2 { margin:12px 0; font-size:clamp(26px,3.4vw,41px); line-height:1.28; }
.slide-caption p { margin:0; }
.hero-controls { position:absolute; bottom:16px; right:19px; display:flex; align-items:center; gap:12px; color:#fff; z-index:3; }
.hero-controls button { color:#fff; background:#102744a8; border:1px solid #ffffff80; border-radius:30px; padding:6px 13px; }

왜 필요한가?

코드·용어의미
position:relative슬라이드 영역을 자식 배치의 기준으로 만듭니다.
position:absolute; inset:0각 슬라이드를 부모의 네 모서리에 맞게 겹칩니다.
opacity:0 / 1안 보이는 슬라이드와 보이는 슬라이드를 구분합니다.

정상 결과

한 장의 큰 히어로 배너가 완성됩니다. 자동 이동은 아직 하지 않습니다.

오류 복구

직접 바꿔보기

.hero의 height를 280px로 바꾸고 변화된 높이를 확인합니다.

내가 확인할 것

START = examples/T06/START/ · FINISH = examples/T06/FINISH/

T07 공지사항·갤러리 탭과 배너 HTML

공지 글 3개와 갤러리 썸네일, 배너 2개를 부모에 넣습니다.

복습: B04 · B07 · B10

학생에게 먼저 설명: hidden이 CSS display:none과 어떻게 연결되는지 다음 단계에서 확인합니다.

수정 파일: index.html

표시된 문자열 하나를 통째로 교체 / 찾을 곳: <!-- CONTENT_SLOT -->

<div class="panel-grid">
  <section class="panel news-panel">
    <h2>여울 소식</h2>
    <div class="tab-buttons" role="tablist" aria-label="소식 종류">
      <button type="button" role="tab" id="tabNotice" class="is-current" aria-selected="true" aria-controls="noticePanel">공지사항</button>
      <button type="button" role="tab" id="tabGallery" aria-selected="false" aria-controls="galleryPanel">갤러리</button>
    </div>
    <div id="noticePanel" role="tabpanel" aria-labelledby="tabNotice">
      <ul class="notice-list">
        <li><a class="notice-link" href="#noticePopup">11월 문화 프로그램 안내</a></li>
        <li><a class="notice-link" href="#noticePopup">주말 창작 교실 모집</a></li>
        <li><a class="notice-link" href="#noticePopup">오픈 클래스 참가 안내</a></li>
      </ul>
    </div>
    <div id="galleryPanel" role="tabpanel" aria-labelledby="tabGallery" hidden>
      <div class="thumb-row">
        <img src="images/gallery1.jpg" alt="작가와의 만남">
        <img src="images/gallery2.jpg" alt="창작 교실">
        <img src="images/gallery3.jpg" alt="도시 전시">
      </div>
    </div>
  </section>
  <a class="panel promo-panel" href="#gallery"><span>EXHIBITION</span><strong>여울의 새로운 전시</strong><span>전시 이야기 보기 →</span></a>
  <a class="panel quick-panel" href="#contact"><span>VISIT YEOUL</span><strong>오시는 길과 관람 안내</strong><span>이용 안내 보기 →</span></a>
</div>

왜 필요한가?

코드·용어의미
.panel-grid3개 콘텐츠 상자가 들어갈 바깥 부모입니다.
role="tab"공지사항과 갤러리를 전환할 버튼임을 나타냅니다.
hidden갤러리 내용은 처음에 숨긴다는 뜻입니다.

정상 결과

공지와 두 배너가 순서대로 보입니다. 아직 디자인 전입니다.

오류 복구

직접 바꿔보기

공지 글 세 번째 제목을 다른 행사 이름으로 바꿔 보세요.

내가 확인할 것

START = examples/T07/START/ · FINISH = examples/T07/FINISH/

T08 콘텐츠 3칸 Grid 디자인

공지사항·전시·안내 배너를 3열로 배치하고 탭 모양을 만듭니다.

복습: B10 · B11

학생에게 먼저 설명: 부모 Grid만 변경하면 자식들이 한꺼번에 이동합니다.

수정 파일: css/style.css

파일 끝에 추가 / 찾을 곳: (파일 맨 아래)

/* [3] 부모 Grid 3칸 + 내부 탭 영역 */
.panel-grid { padding:22px 25px; display:grid; grid-template-columns:1.3fr 1fr 1fr; gap:16px; }
.panel { min-width:0; min-height:190px; border:1px solid #d9e2ec; border-radius:8px; overflow:hidden; }
.news-panel { padding:13px 16px; }
.news-panel h2 { margin:0 0 7px; font-size:16px; }
.tab-buttons { display:flex; gap:10px; border-bottom:1px solid #e0e7ee; }
.tab-buttons button { padding:7px 5px; border:0; background:none; color:#5f728a; font-weight:700; }
.tab-buttons .is-current { color:#12507e; border-bottom:2px solid #12507e; }
.notice-list { padding:7px 0 0 18px; margin:0; font-size:13px; }
.notice-list li { margin:5px 0; }
.notice-link:hover { text-decoration:underline; }
.thumb-row { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:6px; margin-top:12px; }
.thumb-row img { width:100%; aspect-ratio:1; object-fit:cover; border-radius:4px; }
.promo-panel,.quick-panel { display:flex; flex-direction:column; justify-content:end; gap:7px; padding:19px; color:#fff; background:linear-gradient(145deg,#a97857,#503847); }
.quick-panel { background:linear-gradient(145deg,#49647b,#182c49); }
.promo-panel span,.quick-panel span { font-size:12px; }
.promo-panel strong,.quick-panel strong { font-size:20px; line-height:1.35; }
/* 모바일에서는 단계 진행 중에도 카드가 한 글자씩 찌그러지지 않아야 합니다. */
@media(max-width:700px){.panel-grid{grid-template-columns:1fr}}

왜 필요한가?

코드·용어의미
display:grid부모가 자식 3개를 칸으로 배치합니다.
1.3fr 1fr 1fr첫 칸을 상대적으로 넓게 만듭니다.
min-width:0카드 속 긴 글자가 부모 너비를 밀어내지 않게 합니다.

정상 결과

PC는 3칸 카드, 모바일은 이 단계에서 3칸이 좁을 수 있으며 T13에서 1칸으로 교정합니다.

오류 복구

직접 바꿔보기

1.3fr을 2fr로 바꾸면 첫 카드가 얼마나 넓어지는지 관찰합니다.

내가 확인할 것

START = examples/T08/START/ · FINISH = examples/T08/FINISH/

T09 갤러리와 푸터 내용 추가

사진 카드 3개와 하단의 안내 문구를 작성합니다.

복습: B03 · B04 · B05

학생에게 먼저 설명: 이번 단계는 한 파일에서 두 위치를 교체합니다.

수정 파일: index.html

표시된 문자열 하나를 통째로 교체 / 찾을 곳: <!-- GALLERY_SLOT -->

<div class="gallery-heading"><div><span>GALLERY</span><h2>문화공간의 하루</h2></div><p>작은 경험이 모여 커다란 이야기가 됩니다.</p></div>
<div class="gallery-cards">
  <article><img src="images/gallery1.jpg" alt="작가와의 만남"><h3>작가와의 만남</h3></article>
  <article><img src="images/gallery2.jpg" alt="창작 교실"><h3>함께하는 창작 교실</h3></article>
  <article><img src="images/gallery3.jpg" alt="도시를 그리는 전시"><h3>도시의 기록</h3></article>
</div>

수정 파일: index.html

표시된 문자열 하나를 통째로 교체 / 찾을 곳: <!-- FOOTER_SLOT -->

<div class="footer-inner">
  <div><strong>YEOUL CULTURE</strong><p>여울 문화공간 · 전시 · 공연 · 교육</p></div>
  <div><span>문의: K-BOOKS 실습용 가상 안내</span><small>© K-BOOKS ORIGINAL PRACTICE</small></div>
</div>

왜 필요한가?

코드·용어의미
article독립적인 사진 카드 한 장을 묶습니다.
id="gallery"상단 전시·공연 메뉴가 내려올 목적지입니다.
footer홈페이지 하단 정보 영역입니다.

정상 결과

사진 카드 3개와 푸터의 텍스트가 출력됩니다.

오류 복구

직접 바꿔보기

갤러리 세 번째 카드의 제목을 바꾸고 결과를 확인합니다.

내가 확인할 것

START = examples/T09/START/ · FINISH = examples/T09/FINISH/

T10 갤러리 3열과 푸터 CSS

사진을 같은 크기 3열로 맞추고 하단 색상을 완성합니다.

복습: B08 · B10 · B11

학생에게 먼저 설명: PC에서 완성된 Grid라도 모바일에서는 열 수를 다시 설정해야 합니다.

수정 파일: css/style.css

파일 끝에 추가 / 찾을 곳: (파일 맨 아래)

/* [4] 갤러리와 푸터 */
#gallery { background:#f6f0e8; padding:27px 25px 34px; }
.gallery-heading { display:flex; align-items:end; justify-content:space-between; flex-wrap:wrap; gap:10px; margin-bottom:15px; }
.gallery-heading span { color:#a86c4f; font-size:12px; letter-spacing:.15em; font-weight:800; }
.gallery-heading h2 { margin:3px 0; font-size:23px; }
.gallery-heading p { margin:0; font-size:13px; color:#6a6d75; }
.gallery-cards { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:14px; }
.gallery-cards article { min-width:0; background:white; border-radius:8px; overflow:hidden; border:1px solid #e6e2dd; }
.gallery-cards img { width:100%; aspect-ratio:16/8; object-fit:cover; }
.gallery-cards h3 { margin:10px 12px 13px; font-size:14px; }
.site-footer { background:#152b46; color:white; }
.footer-inner { padding:19px 25px; min-height:104px; display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:15px; }
.footer-inner strong { font-size:15px; letter-spacing:.08em; }
.footer-inner p,.footer-inner span,.footer-inner small { margin:2px 0; display:block; color:#bacde0; font-size:12px; }
@media(max-width:700px){.gallery-cards{grid-template-columns:1fr}}

왜 필요한가?

코드·용어의미
repeat(3,minmax(0,1fr))3열을 만들고 카드가 불필요하게 커지지 않게 합니다.
object-fit:cover원본 비율이 달라도 카드의 이미지 상자에 맞춰 자릅니다.
flex-wrap:wrap푸터 내용이 좁아지면 다음 줄로 이동합니다.

정상 결과

갤러리와 푸터까지 PC 화면의 주요 영역이 완성됩니다.

오류 복구

직접 바꿔보기

object-fit:contain으로 바꾸고 이미지 여백을 관찰한 뒤 cover로 돌립니다.

내가 확인할 것

START = examples/T10/START/ · FINISH = examples/T10/FINISH/

T11 팝업 HTML 만들기

공지사항을 누르면 열릴 대화상자 구조를 만듭니다.

복습: B04 · S14

학생에게 먼저 설명: 지금 팝업이 안 보이는 것은 고장이 아니라 정상입니다.

수정 파일: index.html

표시된 문자열 하나를 통째로 교체 / 찾을 곳: <!-- POPUP_SLOT -->

<div class="popup-backdrop" id="noticePopup" hidden>
  <section class="popup-box" role="dialog" aria-modal="true" aria-labelledby="popupTitle" tabindex="-1">
    <div class="popup-head"><h2 id="popupTitle">여울 소식</h2><button id="popupClose" type="button">닫기 ×</button></div>
    <p id="popupText">자세한 공지 내용을 확인하세요.</p>
    <button id="popupOkay" type="button">확인</button>
  </section>
</div>

왜 필요한가?

코드·용어의미
hidden처음 화면에서는 팝업을 감춥니다.
role="dialog"일반 페이지와 구분되는 대화상자입니다.
id="popupClose"나중에 JavaScript가 찾을 닫기 버튼입니다.

정상 결과

화면에는 변화가 거의 없고, 숨겨진 팝업 구조만 완성됩니다.

오류 복구

직접 바꿔보기

팝업 제목을 내가 만든 안내 문구로 바꿔 봅니다.

내가 확인할 것

START = examples/T11/START/ · FINISH = examples/T11/FINISH/

T12 팝업 CSS 완성

팝업을 화면 중앙에 놓고 배경을 어둡게 만듭니다.

복습: B08 · B11 · S15

학생에게 먼저 설명: HTML로 구조를 만들고 CSS로 배치를 만든 후 JS로 동작을 만듭니다.

수정 파일: css/style.css

파일 끝에 추가 / 찾을 곳: (파일 맨 아래)

/* [5] 처음에는 숨기고, JavaScript가 hidden을 해제하면 표시 */
.popup-backdrop { position:fixed; inset:0; z-index:100; display:grid; place-items:center; padding:16px; background:#102238b3; }
.popup-box { width:min(460px,100%); padding:24px; background:#fff; border-radius:12px; box-shadow:0 22px 60px #04182f55; }
.popup-head { display:flex; align-items:center; justify-content:space-between; gap:16px; }
.popup-head h2 { margin:0; font-size:21px; }
.popup-head button,.popup-box>button { border:0; background:#e9eff5; border-radius:7px; padding:8px 12px; }
.popup-box>button { margin-top:10px; background:#1c4d7c; color:white; }

왜 필요한가?

코드·용어의미
position:fixed; inset:0스크롤 위치와 관계없이 화면 전체에 배경을 덮습니다.
place-items:center팝업 상자를 부모의 가운데에 둡니다.
[hidden] {display:none !important}hidden일 때 CSS의 display:grid보다 숨김 규칙을 우선합니다.

정상 결과

정중앙 팝업 스타일을 갖추지만, 실제 클릭 동작은 아직 연결 전입니다.

오류 복구

직접 바꿔보기

개발자도구에서 popup의 hidden 속성을 임시로 해제해 보고 원래대로 돌립니다.

내가 확인할 것

START = examples/T12/START/ · FINISH = examples/T12/FINISH/

T13 태블릿 2열·모바일 1열

@media로 1200·768·390·320px의 카드를 정리합니다.

복습: B12 · B13 · LF4

학생에게 먼저 설명: 미디어 쿼리는 맨 아래에 둬야 앞선 PC 규칙을 덮어쓰기 쉽습니다.

수정 파일: css/style.css

파일 끝에 추가 / 찾을 곳: (파일 맨 아래)

/* [6] PC → 태블릿 2열 → 휴대전화 1열 */
@media(max-width:900px){
  .panel-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
  .news-panel { grid-column:1 / -1; }
  .brand { width:140px; }
}
@media(max-width:680px){
  .nav-toggle { display:block; margin-left:auto; }
  .main-nav { width:100%; }
  .main-nav>ul { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; }
  .js-ready .main-nav:not(.is-open) { display:none; }
  .js-ready .main-nav.is-open { display:block; }
  .hero { height:300px; }
  .slide-caption { padding:37px 20px; max-width:95%; }
  .slide-caption h1,.slide-caption h2 { font-size:clamp(22px,6.2vw,30px); }
  .panel-grid { grid-template-columns:1fr; padding:15px; gap:10px; }
  .news-panel { grid-column:auto; }
  .gallery-cards { grid-template-columns:1fr; }
  #gallery { padding:20px 15px; }
  .gallery-cards img { aspect-ratio:16/6; }
  .footer-inner { padding:18px 15px; }
}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}.slide{transition:none}}

왜 필요한가?

코드·용어의미
@media(max-width:900px)화면 너비가 900px 이하일 때 2열 배치를 사용합니다.
@media(max-width:680px)좁은 모바일에서는 갤러리와 카드가 1열이 됩니다.
.js-readyJS가 실제로 켜졌을 때만 메뉴 접기 기능을 적용하는 안전장치입니다.

정상 결과

모든 카드가 화면에 맞는 너비로 배치됩니다.

오류 복구

직접 바꿔보기

390px에서 .panel-grid 1fr을 2열로 바꿔 깨짐을 확인한 뒤 원상복구합니다.

내가 확인할 것

START = examples/T13/START/ · FINISH = examples/T13/FINISH/

T14 모바일 메뉴 버튼 동작

메뉴 버튼 클릭으로 is-open 클래스를 켜고 끕니다.

복습: B15 · S05

학생에게 먼저 설명: JS 파일 전체를 외우지 않아도 HTML class와 JS 선택자의 연결을 이해해야 합니다.

수정 파일: js/script.js

파일 전체 작성 / 찾을 곳: (빈 파일 전체)

/* [1] 모바일 메뉴: 버튼을 누르면 is-open 클래스를 켭니다. */
document.addEventListener('DOMContentLoaded', () => {
  const menuButton = document.querySelector('.nav-toggle');
  const menu = document.getElementById('mainNav');
  if (menuButton && menu) {
    document.documentElement.classList.add('js-ready');
    menuButton.addEventListener('click', () => {
      const opened = menu.classList.toggle('is-open');
      menuButton.setAttribute('aria-expanded', String(opened));
    });
    menu.addEventListener('click', e => {
      if (e.target.closest('a') && innerWidth <= 680) {
        menu.classList.remove('is-open');
        menuButton.setAttribute('aria-expanded','false');
      }
    });
  }
  /* JS_NEXT */
});

왜 필요한가?

코드·용어의미
querySelectorHTML에서 원하는 요소를 찾습니다.
addEventListener("click")버튼을 클릭했을 때 실행할 코드를 등록합니다.
classList.toggleis-open 클래스가 없으면 붙이고 있으면 뗍니다.

정상 결과

모바일 메뉴가 실제로 열리고 닫힙니다.

오류 복구

직접 바꿔보기

메뉴 버튼 글자를 ‘메뉴 열기’로 바꿔 보고 클릭이 유지되는지 확인합니다.

내가 확인할 것

START = examples/T14/START/ · FINISH = examples/T14/FINISH/

T15 자동 페이드 슬라이드 연결

3초마다 다음 이미지로 전환하고 일시정지 버튼을 만듭니다.

복습: S08 · S09

학생에게 먼저 설명: 시간 변경은 한 숫자만 바꾸는 연습부터 시작합니다.

수정 파일: js/script.js

표시된 문자열 하나를 통째로 교체 / 찾을 곳: /* JS_NEXT */

  /* [2] 페이드: 세 장 중 하나만 is-active를 갖습니다. */
  const slides = [...document.querySelectorAll('.slide')];
  const status = document.getElementById('slideStatus');
  const pause = document.getElementById('slidePause');
  let current = 0;
  let stopped = false;
  function showSlide() {
    slides.forEach((slide, i) => {
      const active = i === current;
      slide.classList.toggle('is-active', active);
      slide.setAttribute('aria-hidden', String(!active));
    });
    if (status) status.textContent = `${String(current+1).padStart(2,'0')} / 03`;
  }
  if (pause) pause.addEventListener('click', () => {
    stopped = !stopped;
    pause.textContent = stopped ? '다시 재생' : '일시정지';
  });
  showSlide();
  if (slides.length > 1) setInterval(() => {
    if (stopped || document.hidden) return;
    current = (current + 1) % slides.length;
    showSlide();
  }, 3000);
  /* JS_NEXT */

왜 필요한가?

코드·용어의미
slides.forEach세 장의 슬라이드를 하나씩 순회합니다.
is-active활성 슬라이드 한 장만 opacity:1이 되게 하는 스위치입니다.
setInterval(...,3000)3,000ms마다 이미지 번호를 1씩 바꿉니다.

정상 결과

슬라이드가 자동 페이드되며 일시정지 가능합니다.

오류 복구

직접 바꿔보기

3000을 5000으로 바꿔 5초 재생을 확인한 뒤 3000으로 되돌립니다.

내가 확인할 것

START = examples/T15/START/ · FINISH = examples/T15/FINISH/

T16 공지사항·갤러리 탭 작동

버튼에 따라 공지와 이미지를 번갈아 표시합니다.

복습: S14 · B16

학생에게 먼저 설명: 숨길 때는 내용을 지우는 게 아니라 hidden 상태만 바꾸는 것입니다.

수정 파일: js/script.js

표시된 문자열 하나를 통째로 교체 / 찾을 곳: /* JS_NEXT */

  /* [3] 탭: 공지사항과 갤러리 중 하나만 표시합니다. */
  const noticeBtn = document.getElementById('tabNotice');
  const galleryBtn = document.getElementById('tabGallery');
  const noticePanel = document.getElementById('noticePanel');
  const galleryPanel = document.getElementById('galleryPanel');
  function setTab(showNotice) {
    noticePanel.hidden = !showNotice;
    galleryPanel.hidden = showNotice;
    noticeBtn.classList.toggle('is-current', showNotice);
    galleryBtn.classList.toggle('is-current', !showNotice);
    noticeBtn.setAttribute('aria-selected', String(showNotice));
    galleryBtn.setAttribute('aria-selected', String(!showNotice));
  }
  if (noticeBtn && galleryBtn && noticePanel && galleryPanel) {
    noticeBtn.addEventListener('click', () => setTab(true));
    galleryBtn.addEventListener('click', () => setTab(false));
  }
  /* JS_NEXT */

왜 필요한가?

코드·용어의미
noticePanel.hidden공지사항을 보이거나 숨깁니다.
galleryPanel.hidden갤러리 영역을 보이거나 숨깁니다.
aria-selected보조기술에 어떤 탭을 선택했는지 알려 줍니다.

정상 결과

탭 하나를 선택하면 다른 패널은 감춰집니다.

오류 복구

직접 바꿔보기

탭 버튼 글자를 ‘사진’으로 바꾸어도 정상 전환되는지 확인합니다.

내가 확인할 것

START = examples/T16/START/ · FINISH = examples/T16/FINISH/

T17 공지 팝업과 닫기 완성

공지 링크를 클릭하면 팝업을 열고 닫기·Esc로 종료합니다.

복습: S15 · FINAL

학생에게 먼저 설명: 실수 복구는 Ctrl+Z보다 원인을 찾는 습관이 중요합니다.

수정 파일: js/script.js

표시된 문자열 하나를 통째로 교체 / 찾을 곳: /* JS_NEXT */

  /* [4] 팝업: 링크를 누르면 보이고 닫기·Esc를 누르면 숨깁니다. */
  const popup = document.getElementById('noticePopup');
  const popupBox = popup?.querySelector('.popup-box');
  const popupText = document.getElementById('popupText');
  let lastLink = null;
  function closePopup() {
    if (popup) popup.hidden = true;
    lastLink?.focus();
  }
  document.querySelectorAll('.notice-link').forEach(link => {
    link.addEventListener('click', e => {
      e.preventDefault();
      lastLink = link;
      if (popupText) popupText.textContent = link.textContent.trim() + ' 상세 안내입니다.';
      if (popup) popup.hidden = false;
      popupBox?.focus();
    });
  });
  document.getElementById('popupClose')?.addEventListener('click', closePopup);
  document.getElementById('popupOkay')?.addEventListener('click', closePopup);
  popup?.addEventListener('click', e => { if (e.target === popup) closePopup(); });
  document.addEventListener('keydown', e => { if (e.key === 'Escape' && popup && !popup.hidden) closePopup(); });

왜 필요한가?

코드·용어의미
preventDefaulthref="#noticePopup"로 즉시 이동하지 않게 막습니다.
popup.hidden=false숨겨진 팝업을 보이게 만듭니다.
Escape키보드로도 팝업을 닫을 수 있습니다.

정상 결과

메뉴·슬라이드·탭·팝업을 모두 갖춘 완성 홈페이지입니다.

오류 복구

직접 바꿔보기

공지 글 하나의 제목을 바꾼 뒤 팝업에도 새 제목이 표시되는지 확인합니다.

내가 확인할 것

START = examples/T17/START/ · FINISH = examples/T17/FINISH/

T18 최종 검사·오프라인 제출·180분 재도전

완성 페이지를 파일로 열어 점검하고 빈 파일에서 제한시간 연습으로 넘어갑니다.

복습: P08 · E01

학생에게 먼저 설명: 이 단계는 공식 합격 판정이 아닙니다. 시험 당일의 요구사항과 감독 지시가 우선입니다.

수정 파일: 제출 폴더

코드 변경 없이 점검 / 찾을 곳: (코드 변경 없음)

실기연습/
├── index.html
├── css/style.css
├── js/script.js
├── images/ (7개)
└── source/content.txt

검사: Chrome 오프라인 실행 → 링크/이미지 → 버튼 4종 → 320/390/768/1200px → ZIP 용량

왜 필요한가?

코드·용어의미
상대경로시험 PC에서 인터넷 없이 이미지·CSS·JS를 찾도록 만듭니다.
제출 검사코드가 맞아도 파일 누락·경로 오류면 결과가 달라집니다.
180분이 과정은 따라하기이며 실제 제한시간 훈련은 다음 단계에서 따로 합니다.

정상 결과

코드 이해 + 기능 검증 + 오프라인 제출 점검이 끝나면 E01→E02→E03 모의시험으로 이동합니다.

오류 복구

직접 바꿔보기

완성본을 복사하지 않고 새 폴더를 열어 E01 문제를 180분 안에 만들어 봅니다.

내가 확인할 것

START = examples/T18/START/ · FINISH = examples/T18/FINISH/