독립형 사이트 검사 예시
이 미리보기에서 사용한 독립형 HTML, CSS, JavaScript 전체입니다. index.html로 저장한 뒤 브라우저에서 여세요.
<!doctype html>
<html lang="en">
<meta charset="utf-8" /><meta
name="viewport"
content="width=device-width,initial-scale=1"
/><title>Site inspection panel</title
><style>
* {
box-sizing: border-box;
}
body {
margin: 0;
font:
14px/1.5 Arial,
sans-serif;
color: #141414;
background: #f7f7f7;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
cursor: pointer;
}
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 2px solid #007ac2;
outline-offset: 3px;
}
header {
padding: 20px 24px;
background: #fff;
border-bottom: 1px solid #d4d4d4;
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
}
h1 {
font-size: 20px;
margin: 0;
}
header p {
margin: 2px 0 0;
color: #6b6b6b;
font-size: 12px;
}
.badge {
background: #d6efff;
color: #004874;
padding: 4px 8px;
font-size: 12px;
}
.layout {
display: grid;
grid-template-columns: 240px minmax(0, 1fr) 280px;
min-height: 510px;
}
.sites,
.inspector {
background: #fff;
padding: 20px;
border-right: 1px solid #d4d4d4;
}
.inspector {
border-right: 0;
border-left: 1px solid #d4d4d4;
}
h2 {
font-size: 16px;
margin: 0 0 16px;
}
label {
display: block;
font-weight: 600;
margin: 12px 0 6px;
}
select,
textarea {
width: 100%;
border: 1px solid #949494;
background: #fff;
padding: 8px;
color: inherit;
}
textarea {
resize: vertical;
min-height: 96px;
}
.site {
display: block;
width: 100%;
text-align: left;
background: #fff;
border: 1px solid #dedede;
padding: 12px;
margin-top: 8px;
}
.site[aria-pressed="true"] {
border-left: 4px solid #007ac2;
background: #d6efff;
}
.site small {
display: block;
color: #4a4a4a;
margin-top: 3px;
}
.map {
position: relative;
overflow: hidden;
min-height: 360px;
background: #ebebe5;
}
.map svg {
width: 100%;
height: 100%;
position: absolute;
inset: 0;
}
.map-caption {
position: absolute;
bottom: 12px;
left: 12px;
padding: 4px 8px;
background: #fff;
font-size: 12px;
}
.pin {
position: absolute;
width: 36px;
height: 36px;
border-radius: 50%;
border: 3px solid #fff;
background: #007ac2;
color: #fff;
font-weight: bold;
box-shadow: 0 2px 8px #0004;
}
.pin[aria-pressed="true"] {
background: #004874;
outline: 3px solid #007ac2;
outline-offset: 3px;
}
.pin[hidden] {
display: none;
}
dl {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
font-size: 12px;
}
dt {
color: #6b6b6b;
}
dd {
margin: 0;
text-align: right;
}
.primary {
background: #007ac2;
color: white;
border: 0;
width: 100%;
padding: 10px;
margin-top: 12px;
}
.primary:hover {
background: #00619b;
}
.error {
color: #a11b1b;
font-size: 12px;
}
.saved {
color: #237000;
font-size: 12px;
}
footer {
padding: 16px 24px;
border-top: 1px solid #d4d4d4;
color: #6b6b6b;
font-size: 12px;
background: #fff;
}
@media (max-width: 1000px) {
.layout {
grid-template-columns: 210px minmax(0, 1fr);
}
.inspector {
grid-column: 1/-1;
border-left: 0;
border-top: 1px solid #d4d4d4;
}
.inspector form {
max-width: 640px;
}
}
@media (max-width: 550px) {
header {
align-items: flex-start;
flex-direction: column;
}
.layout {
display: flex;
flex-direction: column;
}
.sites {
border-right: 0;
}
.map {
height: 300px;
min-height: 300px;
}
.inspector {
padding: 20px;
}
.site {
display: inline-block;
width: 100%;
}
}
</style>
<body>
<header>
<div>
<h1>Field inspection workspace</h1>
<p>Original Calcite-inspired layout study · fictional site data</p>
</div>
<span class="badge">Local notes only</span>
</header>
<main class="layout">
<aside class="sites">
<h2>Inspection sites</h2>
<label for="filter">Show sites</label
><select id="filter">
<option value="all">All sites</option>
<option value="review">Needs review</option>
<option value="checked">Checked</option>
</select>
<div id="site-list"></div>
<p id="count" aria-live="polite"></p>
</aside>
<section class="map" aria-label="Illustrated site plan">
<svg
viewBox="0 0 700 510"
preserveAspectRatio="xMidYMid slice"
role="img"
aria-label="Original schematic with a river, roads and three inspection sites; not a geographic map"
>
<rect width="700" height="510" fill="#e4e8db" />
<path
d="M480 -50Q230 110 440 260T360 570"
fill="none"
stroke="#b6dbe9"
stroke-width="100"
/>
<path
d="M-20 150L720 280M160 -20L290 530M-20 400L720 80"
stroke="#fff"
stroke-width="22"
/>
<path
d="M-20 150L720 280M160 -20L290 530M-20 400L720 80"
stroke="#c8c8bf"
stroke-width="2"
/>
<g fill="#c4cfb5">
<rect x="50" y="50" width="100" height="70" />
<rect x="510" y="340" width="130" height="80" />
<rect x="80" y="260" width="70" height="60" />
</g>
</svg>
<div id="pins"></div>
<span class="map-caption">Schematic illustration · not to scale</span>
</section>
<aside class="inspector">
<h2 id="site-title">North access</h2>
<dl>
<dt>Site ID</dt>
<dd id="site-id"></dd>
<dt>Review state</dt>
<dd id="site-state"></dd>
</dl>
<form id="note-form" novalidate>
<label for="note">Inspection note</label
><textarea
id="note"
aria-describedby="note-help error"
maxlength="240"
></textarea>
<p id="note-help">
Add 10–240 characters. Notes stay in this page until reload.
</p>
<p id="error" class="error" hidden>
Write at least 10 characters before saving.
</p>
<button class="primary" type="submit">Save local note</button>
<p id="saved" class="saved" role="status"></p>
</form>
</aside>
</main>
<footer>
Independent HTML/CSS interpretation by OpenDesign. No Calcite runtime,
Esri fonts, ArcGIS map or service is loaded.
</footer>
<script>
const sites = [
{ id: "A-01", name: "North access", state: "review", x: 29, y: 24 },
{ id: "B-02", name: "River crossing", state: "review", x: 62, y: 48 },
{ id: "C-03", name: "South garden", state: "checked", x: 34, y: 76 },
];
let selected = "A-01";
const notes = new Map(),
list = document.querySelector("#site-list"),
pins = document.querySelector("#pins"),
filter = document.querySelector("#filter"),
note = document.querySelector("#note"),
error = document.querySelector("#error"),
saved = document.querySelector("#saved");
function render() {
const visible = sites.filter(
(s) => filter.value === "all" || s.state === filter.value,
);
if (!visible.some((s) => s.id === selected)) selected = visible[0]?.id;
list.replaceChildren();
pins.replaceChildren();
for (const s of visible) {
const b = document.createElement("button");
b.type = "button";
b.className = "site";
b.setAttribute("aria-pressed", String(s.id === selected));
b.textContent = s.name;
const small = document.createElement("small");
small.textContent =
s.id + " · " + (s.state === "review" ? "Needs review" : "Checked");
b.append(small);
b.onclick = () => {
selected = s.id;
render();
};
list.append(b);
const pin = document.createElement("button");
pin.type = "button";
pin.className = "pin";
pin.style.left = s.x + "%";
pin.style.top = s.y + "%";
pin.textContent = s.id[0];
pin.setAttribute("aria-label", "Select " + s.name);
pin.setAttribute("aria-pressed", String(s.id === selected));
pin.onclick = b.onclick;
pins.append(pin);
}
const s = sites.find((s) => s.id === selected);
document.querySelector("#count").textContent =
visible.length + " sites shown";
document.querySelector("#site-title").textContent = s.name;
document.querySelector("#site-id").textContent = s.id;
document.querySelector("#site-state").textContent =
s.state === "review" ? "Needs review" : "Checked";
note.value = notes.get(s.id) || "";
error.hidden = true;
note.removeAttribute("aria-invalid");
saved.textContent = "";
}
filter.onchange = render;
note.oninput = () => {
error.hidden = true;
note.removeAttribute("aria-invalid");
saved.textContent = "";
};
document.querySelector("#note-form").onsubmit = (e) => {
e.preventDefault();
const value = note.value.trim();
if (value.length < 10) {
error.hidden = false;
note.setAttribute("aria-invalid", "true");
note.focus();
return;
}
notes.set(selected, value);
saved.textContent = "Note saved locally for " + selected + ".";
};
render();
</script>
</body>
</html>
- 원본 도식과 애플리케이션 동작입니다. 지도 SDK, 원격 데이터, Esri 런타임은 포함되어 있지 않습니다.


