R37 · 파일 위치 → 실제 입력 코드 → 이유 → 정상 결과 → 오류 복구 → 직접 변경 · A4 출력용
이 교재는 K-BOOKS 자체 제작 연습용입니다. 각 단계 START에서 시작하여 안내 코드만 변경하면 FINISH를 재현할 수 있습니다. 앞 단계 FINISH는 다음 단계 START와 동일합니다. 시험 3시간 시간제한은 마지막 모의시험에서 별도로 연습합니다.
2026년 Q-Net 기준: 3시간, 제출 데이터 10MB. 시험장 규칙은 최신 공식 공지를 우선합니다.
복습: 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를 찾아 열어 보고 파일명이 맞는지 확인합니다.
복습: B01 · B02
학생에게 먼저 설명: PAGE_CONTENT 주석은 다음 단계에서 코드를 놓을 정확한 자리입니다.
파일 전체 작성 / 찾을 곳: (빈 파일 전체)
<!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> | 실제 페이지에 보일 내용을 담습니다. |
| defer | HTML을 읽은 뒤 JavaScript를 실행하도록 합니다. |
화면은 아직 비어 있고 탭 제목만 보입니다.
브라우저 탭의 제목을 내 이름을 넣어 바꾸고 새로고침합니다.
복습: B03 · B04 · B07
학생에게 먼저 설명: 부모(header) 안의 자식(nav)을 소리 내어 설명해 보세요.
표시된 문자열 하나를 통째로 교체 / 찾을 곳: <!-- 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 -->
표시된 문자열 하나를 통째로 교체 / 찾을 곳: <!-- 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개가 보이고, 아직 디자인 전이라 단순하게 출력됩니다.
두 번째 메뉴의 글자를 내가 원하는 이름으로 바꿔 보세요.
복습: B06 · B07 · B09
학생에게 먼저 설명: CSS를 쓰는 시점부터 PC와 모바일을 함께 확인합니다.
파일 끝에 추가 / 찾을 곳: (파일 맨 아래)
/* [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:1200px | PC에서 사이트가 지나치게 넓어지지 않게 상한선을 정합니다. |
| display:flex | 부모 안의 로고와 메뉴를 한 줄로 정렬합니다. |
| flex-wrap:wrap | 공간이 모자라면 다음 줄로 자연스럽게 내려가도록 합니다. |
PC는 1200px 이내의 헤더, 모바일은 2열로 줄어드는 메뉴입니다.
max-width를 1000px로 변경한 뒤 PC 화면만 어떻게 달라지는지 보세요.
복습: B04 · B05 · B07
학생에게 먼저 설명: 슬라이드 동작은 JavaScript 단계에서 연결하므로 지금 움직이지 않아도 정상입니다.
표시된 문자열 하나를 통째로 교체 / 찾을 곳: <!-- 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)을 내가 이해한 문장으로 바꿔 보세요.
복습: B07 · B08 · B11
학생에게 먼저 설명: 이미지를 3장 넣는 것과 슬라이드가 작동하는 것은 서로 다른 작업입니다.
파일 끝에 추가 / 찾을 곳: (파일 맨 아래)
/* [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로 바꾸고 변화된 높이를 확인합니다.
복습: B04 · B07 · B10
학생에게 먼저 설명: hidden이 CSS display:none과 어떻게 연결되는지 다음 단계에서 확인합니다.
표시된 문자열 하나를 통째로 교체 / 찾을 곳: <!-- 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-grid | 3개 콘텐츠 상자가 들어갈 바깥 부모입니다. |
| role="tab" | 공지사항과 갤러리를 전환할 버튼임을 나타냅니다. |
| hidden | 갤러리 내용은 처음에 숨긴다는 뜻입니다. |
공지와 두 배너가 순서대로 보입니다. 아직 디자인 전입니다.
공지 글 세 번째 제목을 다른 행사 이름으로 바꿔 보세요.
복습: B10 · B11
학생에게 먼저 설명: 부모 Grid만 변경하면 자식들이 한꺼번에 이동합니다.
파일 끝에 추가 / 찾을 곳: (파일 맨 아래)
/* [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로 바꾸면 첫 카드가 얼마나 넓어지는지 관찰합니다.
복습: B03 · B04 · B05
학생에게 먼저 설명: 이번 단계는 한 파일에서 두 위치를 교체합니다.
표시된 문자열 하나를 통째로 교체 / 찾을 곳: <!-- 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>
표시된 문자열 하나를 통째로 교체 / 찾을 곳: <!-- 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개와 푸터의 텍스트가 출력됩니다.
갤러리 세 번째 카드의 제목을 바꾸고 결과를 확인합니다.
복습: B08 · B10 · B11
학생에게 먼저 설명: PC에서 완성된 Grid라도 모바일에서는 열 수를 다시 설정해야 합니다.
파일 끝에 추가 / 찾을 곳: (파일 맨 아래)
/* [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로 돌립니다.
복습: B04 · S14
학생에게 먼저 설명: 지금 팝업이 안 보이는 것은 고장이 아니라 정상입니다.
표시된 문자열 하나를 통째로 교체 / 찾을 곳: <!-- 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가 찾을 닫기 버튼입니다. |
화면에는 변화가 거의 없고, 숨겨진 팝업 구조만 완성됩니다.
팝업 제목을 내가 만든 안내 문구로 바꿔 봅니다.
복습: B08 · B11 · S15
학생에게 먼저 설명: HTML로 구조를 만들고 CSS로 배치를 만든 후 JS로 동작을 만듭니다.
파일 끝에 추가 / 찾을 곳: (파일 맨 아래)
/* [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 속성을 임시로 해제해 보고 원래대로 돌립니다.
복습: B12 · B13 · LF4
학생에게 먼저 설명: 미디어 쿼리는 맨 아래에 둬야 앞선 PC 규칙을 덮어쓰기 쉽습니다.
파일 끝에 추가 / 찾을 곳: (파일 맨 아래)
/* [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-ready | JS가 실제로 켜졌을 때만 메뉴 접기 기능을 적용하는 안전장치입니다. |
모든 카드가 화면에 맞는 너비로 배치됩니다.
390px에서 .panel-grid 1fr을 2열로 바꿔 깨짐을 확인한 뒤 원상복구합니다.
복습: B15 · S05
학생에게 먼저 설명: JS 파일 전체를 외우지 않아도 HTML class와 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 */
});
| 코드·용어 | 의미 |
|---|---|
| querySelector | HTML에서 원하는 요소를 찾습니다. |
| addEventListener("click") | 버튼을 클릭했을 때 실행할 코드를 등록합니다. |
| classList.toggle | is-open 클래스가 없으면 붙이고 있으면 뗍니다. |
모바일 메뉴가 실제로 열리고 닫힙니다.
메뉴 버튼 글자를 ‘메뉴 열기’로 바꿔 보고 클릭이 유지되는지 확인합니다.
복습: S08 · S09
학생에게 먼저 설명: 시간 변경은 한 숫자만 바꾸는 연습부터 시작합니다.
표시된 문자열 하나를 통째로 교체 / 찾을 곳: /* 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으로 되돌립니다.
복습: S14 · B16
학생에게 먼저 설명: 숨길 때는 내용을 지우는 게 아니라 hidden 상태만 바꾸는 것입니다.
표시된 문자열 하나를 통째로 교체 / 찾을 곳: /* 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 | 보조기술에 어떤 탭을 선택했는지 알려 줍니다. |
탭 하나를 선택하면 다른 패널은 감춰집니다.
탭 버튼 글자를 ‘사진’으로 바꾸어도 정상 전환되는지 확인합니다.
복습: S15 · FINAL
학생에게 먼저 설명: 실수 복구는 Ctrl+Z보다 원인을 찾는 습관이 중요합니다.
표시된 문자열 하나를 통째로 교체 / 찾을 곳: /* 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(); });
| 코드·용어 | 의미 |
|---|---|
| preventDefault | href="#noticePopup"로 즉시 이동하지 않게 막습니다. |
| popup.hidden=false | 숨겨진 팝업을 보이게 만듭니다. |
| Escape | 키보드로도 팝업을 닫을 수 있습니다. |
메뉴·슬라이드·탭·팝업을 모두 갖춘 완성 홈페이지입니다.
공지 글 하나의 제목을 바꾼 뒤 팝업에도 새 제목이 표시되는지 확인합니다.
복습: 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분 안에 만들어 봅니다.