기초 18단계+실습 48강에서 시험형 홈페이지로 넘어가는 연결 수업. 공식 시험문제를 복제하지 않은 K-BOOKS 자체 제작입니다. 단계별 START 파일에서 출발하고 입력한 결과를 FINISH와 비교하세요.
공식 안내(2026): 작업형 3시간, 제출 10MB, 인터넷 차단. 연습 단계에서는 정답 참고 가능, 모의시험 단계에서는 빈 파일부터 독립 제작합니다.
문서의 head는 설정, body는 화면에 나타날 내용을 담습니다. UTF-8은 한글을 올바르게 표시하기 위한 문자 인코딩입니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>여울 문화공간 | K-BOOKS 실기 입문</title>
<link rel="stylesheet" href="css/style.css">
<script src="js/script.js" defer></script>
</head>
<body>
<a class="skip-link" href="#main">본문 바로가기</a>
<div class="site-shell">
<header class="site-header">
<!-- STEP_P02_NAV -->
</header>
<main id="main">
<section id="about" aria-label="문화공간 소개">
<p class="stage-message">첫 실기 연습 페이지가 열렸습니다. 다음 단계에서 메뉴와 이미지를 추가합니다.</p>
<!-- STEP_P03_HERO -->
</section>
<section id="program" aria-label="프로그램 안내">
<!-- STEP_P04_CONTENT -->
</section>
<section id="gallery" aria-label="전시·공연">
<!-- STEP_P04_GALLERY -->
</section>
</main>
<footer class="site-footer" id="contact">
<!-- STEP_P04_FOOTER -->
</footer>
</div>
<!-- STEP_P04_POPUP -->
</body>
</html>


한글이 깨지면 <meta charset="UTF-8">과 저장 인코딩을 확인하세요. css/style.css가 없으면 링크를 만들었어도 적용되지 않습니다.
header는 상단 영역, nav는 탐색 메뉴입니다. ul 아래 li가 있고 li 안에 a가 있습니다. href의 #about은 id="about" 위치를 가리킵니다.
<div class="header-inner">
<a class="brand" href="#about" aria-label="여울 문화공간 첫 화면">
<img src="images/logo.svg" alt="여울 문화공간">
</a>
<button class="nav-toggle" type="button" aria-expanded="false" aria-controls="mainNav">전체 메뉴</button>
<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>
</div>


로고가 안 보이면 images/logo.svg와 파일명 대소문자를 확인합니다. 메뉴를 한 줄에 보이게 하는 CSS는 P05에서 작성합니다.
article.slide 3개는 한 세트입니다. 아직 CSS를 넣지 않았으므로 이미지가 세로로 길게 보일 수 있습니다. 이미지를 겹쳐 한 장씩 보여 주는 단계는 P05입니다.
<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 & CREATE</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" aria-live="off">01 / 03</span><button id="slidePause" type="button" aria-pressed="false">일시정지</button></div>
</div>


이미지가 깨지면 이미지 폴더가 index.html과 같은 위치에 있는지 확인하세요. 지금은 세로로 쌓여도 정상이고 P05에서 겹칩니다.
section은 기능별 영역이고, button은 동작을 실행하는 요소입니다. 공지/갤러리 버튼과 팝업은 아직 작동하지 않으며 P07에서 스크립트를 연결합니다.
[STEP_P04_CONTENT]
<div class="panel-grid">
<section class="panel news-panel" aria-labelledby="newsHeading">
<h2 id="newsHeading">여울 소식</h2>
<div class="tab-buttons" role="tablist" aria-label="소식 종류">
<button id="tabNotice" class="is-current" role="tab" aria-selected="true" aria-controls="noticePanel" type="button">공지사항</button>
<button id="tabGallery" role="tab" aria-selected="false" aria-controls="galleryPanel" type="button">갤러리</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>
[STEP_P04_GALLERY]
<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>
[STEP_P04_FOOTER]
<div class="footer-inner"><div><strong>YEOUL CULTURE</strong><p>여울 문화공간 · 전시·공연·교육</p></div><div><span>문의: 현장 연습용 가상 안내</span><small>© K-BOOKS ORIGINAL PRACTICE</small></div></div>
[STEP_P04_POPUP]
<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 type="button" class="popup-close" id="popupClose">닫기 ×</button></div>
<p id="popupText">공지사항 내용을 확인하는 연습용 팝업입니다.</p>
<button class="popup-ok" id="popupOkay" type="button">확인</button>
</section>
</div>


gallery2.jpg가 누락되면 image 경로를 확인합니다. 팝업이 지금 안 열리는 것은 오류가 아니며 P07에 연결합니다.
max-width:1200px은 콘텐츠의 최대 폭입니다. minmax(0,1fr)와 min-width:0은 카드가 너무 좁아지거나 넘치는 현상을 막는 데 중요합니다. 작은 화면의 기본 1열 안전장치도 함께 작성합니다.
/* P05: 데스크톱 레이아웃부터 조립합니다. */
:root { --ink:#21314a; --navy:#1b3151; --coral:#eda76d; --paper:#f6f0e8; }
* { box-sizing:border-box; }
html { scroll-behavior:smooth; }
body { margin:0; background:#f1f5f8; color:var(--ink); font:15px/1.65 system-ui,"Malgun Gothic",sans-serif; }
[hidden] { display:none !important; }
a { color:inherit; text-decoration:none; }
a:focus-visible,button:focus-visible { outline:3px solid #ec8c49; outline-offset:3px; }
img { max-width:100%; display:block; }
button { font:inherit; cursor:pointer; }
.skip-link { position:absolute; top:-100px; left:12px; z-index:200; background:#fff; padding:10px; }
.skip-link:focus { top:12px; }
.site-shell { max-width:1200px; margin:0 auto; background:#fff; box-shadow:0 12px 48px rgba(11,29,55,.08); }
.site-header { background:#fff; position:relative; z-index:20; }
.header-inner { display:flex; align-items:center; justify-content:space-between; gap:25px; min-height:100px; padding:0 32px; }
.brand { flex:none; display:block; width:162px; }
.brand img { width:100%; height:58px; object-fit:contain; }
.main-nav > ul { display:flex; list-style:none; margin:0; padding:0; gap:3px; }
.main-nav li { position:relative; list-style:none; }
.main-nav > ul > li > a { display:block; min-width:126px; padding:15px 8px; text-align:center; font-weight:750; }
.main-nav > ul > li > a:hover,.main-nav > ul > li:focus-within > a { color:#135b83; }
.submenu { position:absolute; left:0; right:0; top:100%; background:var(--navy); color:#fff; margin:0; padding:8px 0; list-style:none; display:none; box-shadow:0 6px 18px #091c3040; }
.main-nav li:hover > .submenu,.main-nav li:focus-within > .submenu { display:block; }
.submenu a { display:block; padding:8px 14px; font-size:13px; }
.submenu a:hover,.submenu a:focus-visible { background:#39516e; }
.nav-toggle { display:none; border:1px solid #d3dfec; padding:8px 12px; border-radius:7px; color:var(--navy); background:white; }
.hero { height:300px; position:relative; overflow:hidden; background:#1c3658; }
.slides,.slide { position:absolute; inset:0; }
.slide { opacity:0; pointer-events:none; transition:opacity .65s ease-in-out; }
.slide.is-active { opacity:1; pointer-events:auto; z-index:1; }
.slide img { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
.slide::after { content:""; position:absolute; inset:0; background:linear-gradient(90deg,rgba(18,30,49,.82),rgba(18,30,49,.1)); }
.slide-caption { position:relative; z-index:2; width:min(740px,88%); padding:45px 54px; color:#fff; }
.slide-caption > span { font-size:12px; letter-spacing:.22em; color:#ffce9c; font-weight:700; }
.slide-caption h1,.slide-caption h2 { font-size:clamp(24px,3vw,39px); letter-spacing:-.055em; line-height:1.3; margin:12px 0; }
.slide-caption p { margin:0; }
.hero-controls { position:absolute; bottom:13px; right:20px; z-index:5; color:#fff; display:flex; gap:14px; align-items:center; font-size:12px; }
.hero-controls button { border:1px solid #ffffff66; background:#152b4499; border-radius:50px; padding:6px 12px; color:#fff; }
.panel-grid { min-height:208px; display:grid; grid-template-columns:1.35fr 1fr 1fr; gap:18px; padding:24px 26px; }
.panel { border:1px solid #dae2ec; min-width:0; border-radius:9px; overflow:hidden; }
.news-panel { padding:14px 18px; }
.news-panel h2 { font-size:16px; margin:0 0 8px; }
.tab-buttons { display:flex; gap:8px; border-bottom:1px solid #dce5ef; }
.tab-buttons button { border:0; color:#61758d; padding:6px 8px; background:transparent; font-weight:700; }
.tab-buttons button.is-current { color:#164c7c; border-bottom:2px solid #2d6698; }
.notice-list { padding:10px 0 0 17px; margin:0; font-size:13px; }
.notice-list li { line-height:1.7; }
.notice-link:hover { text-decoration:underline; }
.thumb-row { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:6px; margin-top:14px; }
.thumb-row img { width:100%; aspect-ratio:1; object-fit:cover; border-radius:5px; }
.promo-panel,.quick-panel { display:flex; flex-direction:column; justify-content:end; gap:5px; color:white; padding:20px; background:linear-gradient(145deg,#304966,#19344e); }
.promo-panel { background:linear-gradient(145deg,#9b6d4a,#543a42); }
.promo-panel span,.quick-panel span { font-size:12px; letter-spacing:.035em; }
.promo-panel strong,.quick-panel strong { font-size:20px; line-height:1.4; }
#gallery { background:var(--paper); padding:30px 26px 36px; }
.gallery-heading { display:flex; align-items:end; justify-content:space-between; gap:20px; margin-bottom:15px; }
.gallery-heading span { font-size:12px; letter-spacing:.18em; color:#a06648; font-weight:800; }
.gallery-heading h2 { margin:3px 0 0; font-size:21px; }
.gallery-heading p { margin:0; font-size:13px; color:#6f6d70; }
.gallery-cards { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:14px; }
.gallery-cards article { min-width:0; border:1px solid #e8e2de; background:white; border-radius:7px; overflow:hidden; }
.gallery-cards img { width:100%; aspect-ratio:16/7; object-fit:cover; }
.gallery-cards h3 { margin:10px 12px; font-size:14px; }
.site-footer { background:#15263e; color:#fff; }
.footer-inner { min-height:100px; padding:20px 27px; display:flex; gap:18px; justify-content:space-between; align-items:center; }
.footer-inner strong { font-size:15px; letter-spacing:.08em; }
.footer-inner p,.footer-inner span,.footer-inner small { display:block; color:#bccce1; font-size:12px; margin:2px 0; }
.popup-backdrop { position:fixed; inset:0; background:#0b2136b8; display:grid; place-items:center; z-index:100; padding:18px; }
.popup-box { width:min(480px,100%); background:white; padding:25px; border-radius:12px; box-shadow:0 20px 55px #07192c4c; }
.popup-head { display:flex; justify-content:space-between; gap:14px; align-items:center; }
.popup-head h2 { margin:0; font-size:19px; }
.popup-close { background:#eef4fa; border:0; border-radius:6px; padding:8px 12px; }
.popup-ok { margin-top:20px; padding:9px 19px; border:0; background:var(--navy); color:white; border-radius:6px; }
/* P05에서도 작은 화면에서 카드가 겹치지 않게 하는 최소 안전장치를 넣습니다. */
@media (max-width:700px) { .header-inner { flex-wrap:wrap; padding:15px; } .main-nav { width:100%; } .main-nav>ul { flex-wrap:wrap; } .main-nav>ul>li { flex:1 1 45%; } .main-nav>ul>li>a { min-width:0; } .panel-grid,.gallery-cards { grid-template-columns:1fr; } .gallery-heading { flex-wrap:wrap; } .slide-caption { padding:38px 20px; width:96%; } .footer-inner { flex-wrap:wrap; } }


Grid가 한 열로 나오면 .panel-grid의 grid-template-columns를 확인하세요. 슬라이드가 모두 보이면 .slide와 .slide.is-active 규칙을 확인합니다.
@media는 화면 너비에 따라 CSS를 바꾸는 조건입니다. PC에서는 3영역, 768px 태블릿에서는 2영역, 390·320px에서는 1영역으로 바꿉니다. 입력 중인 카드 안의 텍스트가 한 글자씩 줄바꿈되지 않아야 합니다.
/* P06: 화면이 좁아져도 카드·메뉴가 한 글자씩 찌그러지지 않게 합니다. */
@media (max-width:900px) {
.header-inner { padding:14px 20px; }
.brand { width:145px; }
.main-nav>ul>li>a { min-width:100px; font-size:13px; }
.panel-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
.news-panel { grid-column:1 / -1; }
}
@media (max-width:680px) {
.header-inner { display:flex; align-items:center; gap:12px; }
.brand { width:138px; }
.nav-toggle { display:block; margin-left:auto; }
.main-nav { flex-basis:100%; width:100%; }
.main-nav>ul { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; }
.main-nav>ul>li { min-width:0; }
.main-nav>ul>li>a { border:1px solid #e1e8f1; border-radius:6px; padding:9px 6px; }
.submenu { display:none; position:static; box-shadow:none; border-radius:6px; }
.main-nav li:hover>.submenu,.main-nav li:focus-within>.submenu { display:block; }
/* 스크립트가 정상 연결된 경우에만 접기 메뉴를 적용합니다. */
.js-ready .main-nav:not(.is-open) { display:none; }
.js-ready .main-nav.is-open { display:block; }
.hero { height:300px; }
.slide-caption h1,.slide-caption h2 { font-size:clamp(23px,6.4vw,32px); }
.panel-grid { grid-template-columns:1fr; gap:10px; padding:14px; }
.news-panel { grid-column:auto; }
#gallery { padding:20px 14px; }
.gallery-cards { grid-template-columns:1fr; }
.gallery-cards img { aspect-ratio:16/6; }
.footer-inner { padding:19px; }
}
@media (prefers-reduced-motion:reduce) {
html { scroll-behavior:auto; }
.slide { transition:none; }
}


모바일에서 글자가 세로로 찌그러지면 .panel-grid와 .gallery-cards의 grid-template-columns를 1fr로 바꾸고, 부모 요소 min-width를 확인하세요.
이 스크립트는 요소를 찾고 이벤트를 연결하는 공통 동작입니다. 초보자는 우선 어떤 id/class가 어떤 버튼에 연결되는지 이해합니다. 시험 환경에서는 인터넷이 끊겨도 동작해야 하므로 외부 라이브러리를 사용하지 않습니다.
/* P07: 메뉴·슬라이드·탭·팝업을 연결합니다. 외부 인터넷 없이 작동합니다. */
document.addEventListener('DOMContentLoaded', () => {
const navButton = document.querySelector('.nav-toggle');
const nav = document.getElementById('mainNav');
if (navButton && nav) {
navButton.addEventListener('click', () => {
const open = nav.classList.toggle('is-open');
navButton.setAttribute('aria-expanded', String(open));
});
nav.addEventListener('click', event => {
if (event.target.closest('a') && window.innerWidth <= 680) {
nav.classList.remove('is-open');
navButton.setAttribute('aria-expanded', 'false');
}
});
}
document.documentElement.classList.add('js-ready');
const slides = [...document.querySelectorAll('.slide')];
const slideStatus = document.getElementById('slideStatus');
const pauseButton = document.getElementById('slidePause');
const hero = document.querySelector('.hero');
let current = 0;
let paused = false;
const updateSlides = () => {
slides.forEach((slide, index) => {
const active = index === current;
slide.classList.toggle('is-active', active);
slide.setAttribute('aria-hidden', String(!active));
});
if (slideStatus) slideStatus.textContent = `${String(current + 1).padStart(2,'0')} / ${String(slides.length).padStart(2,'0')}`;
};
if (pauseButton) pauseButton.addEventListener('click', () => {
paused = !paused;
pauseButton.textContent = paused ? '다시 재생' : '일시정지';
pauseButton.setAttribute('aria-pressed', String(paused));
});
if (slides.length > 1) {
window.setInterval(() => {
if (paused || document.hidden || (hero && (hero.matches(':hover') || hero.contains(document.activeElement)))) return;
current = (current + 1) % slides.length;
updateSlides();
}, 2800);
}
updateSlides();
const tabNotice = document.getElementById('tabNotice');
const tabGallery = document.getElementById('tabGallery');
const noticePanel = document.getElementById('noticePanel');
const galleryPanel = document.getElementById('galleryPanel');
function selectTab(which) {
const onNotice = which === 'notice';
noticePanel.hidden = !onNotice;
galleryPanel.hidden = onNotice;
tabNotice.classList.toggle('is-current', onNotice);
tabGallery.classList.toggle('is-current', !onNotice);
tabNotice.setAttribute('aria-selected', String(onNotice));
tabGallery.setAttribute('aria-selected', String(!onNotice));
}
if (tabNotice && tabGallery && noticePanel && galleryPanel) {
tabNotice.addEventListener('click', () => selectTab('notice'));
tabGallery.addEventListener('click', () => selectTab('gallery'));
[tabNotice, tabGallery].forEach((tab, idx, arr) => {
tab.addEventListener('keydown', event => {
if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft') return;
event.preventDefault();
const next = arr[1 - idx];
selectTab(next === tabNotice ? 'notice' : 'gallery');
next.focus();
});
});
}
const popup = document.getElementById('noticePopup');
const popupBox = popup && popup.querySelector('.popup-box');
const popupText = document.getElementById('popupText');
const closePopup = () => { if (popup) popup.hidden = true; if (lastFocused) lastFocused.focus(); };
let lastFocused = null;
document.querySelectorAll('.notice-link').forEach(link => link.addEventListener('click', event => {
event.preventDefault();
lastFocused = link;
if (popupText) popupText.textContent = `${link.textContent.trim()}
자세한 내용은 문화공간 공지에서 확인하세요.`;
if (popup) popup.hidden = false;
if (popupBox) popupBox.focus();
}));
document.getElementById('popupClose')?.addEventListener('click', closePopup);
document.getElementById('popupOkay')?.addEventListener('click', closePopup);
popup?.addEventListener('click', event => { if (event.target === popup) closePopup(); });
document.addEventListener('keydown', event => {
if (event.key === 'Escape' && popup && !popup.hidden) closePopup();
});
});


버튼이 안 되면 JS 파일 경로와 콘솔 오류를 먼저 확인하세요. 선택자 #tabGallery와 id="tabGallery"가 정확히 일치해야 합니다.
입문 8단계는 익히는 수업입니다. 온라인에서 따라 했다는 사실은 합격이 아닙니다. 실제 시험과 비슷한 조건으로 오프라인 PC에서 빈 시작 파일로 다시 제작하고 3시간 동안 완료해 봅니다.
코드 변경 없음 — P07 FINISH 파일을 기준으로 직접 조작하고 제출 조건을 점검하세요.


로컬에서 안 되면 절대경로(/images/) 또는 CDN 링크가 남아 있는지 확인하세요. 훈련실 ZIP은 개인 백업용이고 시험장 제출 방식은 감독 지시를 따릅니다.