release: v0.10.0
All checks were successful
ci / build (push) Successful in 42s
release / release (push) Successful in 1m5s

This commit is contained in:
Yun Chan 2026-08-29 21:19:26 +09:00
parent 2b7fa32b5b
commit 6930906583
61 changed files with 15165 additions and 82 deletions

View file

@ -0,0 +1,261 @@
(() => {
"use strict";
const fixture = new URLSearchParams(window.location.search).get("fixture") || "";
const formatWon = (value) => `${new Intl.NumberFormat("ko-KR").format(value)}원`;
const markBrokenImage = (image) => {
const shell = image.closest(".image-shell");
if (shell) shell.classList.add("is-broken");
};
document.querySelectorAll(".image-shell img").forEach((image) => {
image.addEventListener("error", () => markBrokenImage(image));
if (image.complete && image.naturalWidth === 0) markBrokenImage(image);
});
const menuButtons = [...document.querySelectorAll("[data-menu-filter]")];
const menuItems = [...document.querySelectorAll("[data-menu-category]")];
const menuEmpty = document.querySelector("#menu-empty");
const setMenuFilter = (filter) => {
const forceMenuEmpty = new URLSearchParams(window.location.search).get("fixture") === "menu-empty";
let visibleCount = 0;
menuButtons.forEach((button) => {
const active = button.dataset.menuFilter === filter;
button.classList.toggle("is-active", active);
button.setAttribute("aria-pressed", String(active));
});
menuItems.forEach((item) => {
const visible = !forceMenuEmpty && (filter === "all" || item.dataset.menuCategory === filter);
item.hidden = !visible;
if (visible) visibleCount += 1;
});
if (menuEmpty) menuEmpty.hidden = visibleCount > 0;
};
menuButtons.forEach((button) => {
button.addEventListener("click", () => setMenuFilter(button.dataset.menuFilter || "all"));
});
document.querySelector("[data-show-all-menu]")?.addEventListener("click", () => {
if (fixture === "menu-empty") {
const nextUrl = new URL(window.location.href);
nextUrl.searchParams.delete("fixture");
window.history.replaceState({}, "", nextUrl);
}
setMenuFilter("all");
});
if (fixture === "menu-empty") setMenuFilter("all");
const builder = document.querySelector("#burger-builder");
const builderSection = document.querySelector("#builder");
const setOptions = document.querySelector("#set-options");
const summaryHeading = document.querySelector("#summary-heading");
const summaryLines = document.querySelector("#summary-lines");
const summaryServing = document.querySelector("#summary-serving");
const summaryStore = document.querySelector("#summary-store");
const summaryStoreState = document.querySelector("#summary-store-state");
const summaryAllergens = document.querySelector("#summary-allergens");
const summaryTotal = document.querySelector("#summary-total");
const saveButton = document.querySelector("#save-builder");
const saveStatus = document.querySelector("#save-status");
const stores = {
seongsu: { label: "성수 예시점", state: "영업 중 예시", available: true },
yeonnam: { label: "연남 예시점", state: "오늘 영업 종료 예시", available: false },
mullae: { label: "문래 예시점", state: "오늘은 쉬어요", available: false },
};
const fieldValue = (name) => {
const field = builder?.elements.namedItem(name);
if (!field) return "";
if (field instanceof RadioNodeList) return field.value;
return field.value;
};
const setRadio = (name, value) => {
const radio = builder?.querySelector(`input[name="${name}"][value="${value}"]`);
if (radio instanceof HTMLInputElement) radio.checked = true;
};
const builderPrice = ({ flavor, patty, cheese, serving }) => {
const flavorBase = flavor === "spicy" ? 7400 : 6900;
const pattyAdd = Number(patty) === 2 ? 2000 : 0;
const cheeseAdd = Number(cheese) === 0 ? -400 : Number(cheese) === 2 ? 400 : 0;
const setAdd = serving === "set" ? 4100 : 0;
return flavorBase + pattyAdd + cheeseAdd + setAdd;
};
const updateBuilder = () => {
if (!builder) return;
const flavor = fieldValue("flavor");
const patty = fieldValue("patty");
const cheese = fieldValue("cheese");
const onion = fieldValue("onion");
const pickle = fieldValue("pickle");
const sauce = fieldValue("sauce");
const serving = fieldValue("serving");
const storeKey = fieldValue("store");
const store = stores[storeKey] || stores.seongsu;
const soldOut = fixture === "sold-out";
const setEnabled = serving === "set";
builder.querySelectorAll("select[name='side'], select[name='drink']").forEach((select) => {
select.disabled = !setEnabled;
});
setOptions?.classList.toggle("is-disabled", !setEnabled);
const names = {
onion: { none: "양파 없음", regular: "양파 기본", extra: "양파 많이" },
pickle: { none: "피클 없음", regular: "피클 기본", extra: "피클 많이" },
sauce: { none: "소스 없음", regular: "소스 기본", side: "소스 따로" },
};
const pattyLabel = Number(patty) === 2 ? "더블" : "싱글";
const flavorLabel = flavor === "spicy" ? "고추 피클" : "철판";
const cheeseLabel = Number(cheese) === 0 ? "치즈 없음" : `치즈 ${cheese === "2" ? "두" : "한"} 장`;
const productName = `${flavorLabel} ${pattyLabel}`;
const servingLabel = serving === "set" ? "세트 · 얇은 감자와 음료" : "단품";
if (summaryHeading) summaryHeading.textContent = productName;
if (summaryLines) {
summaryLines.textContent = `패티 ${patty === "2" ? "두" : "한"} 장 / ${cheeseLabel} / ${names.onion[onion]} / ${names.pickle[pickle]} / ${names.sauce[sauce]}`;
}
if (summaryServing) summaryServing.textContent = servingLabel;
if (summaryStore) summaryStore.textContent = store.label;
if (summaryStoreState) summaryStoreState.textContent = soldOut ? "오늘 품절 예시" : store.state;
const allergens = ["알", "대두", "밀", "쇠고기"];
if (Number(cheese) > 0) allergens.splice(1, 0, "우유");
if (flavor === "spicy") allergens.push("토마토");
if (summaryAllergens) summaryAllergens.textContent = allergens.join(", ");
if (summaryTotal) summaryTotal.textContent = formatWon(builderPrice({ flavor, patty, cheese, serving }));
if (saveButton) {
saveButton.disabled = soldOut || !store.available;
if (soldOut) saveButton.textContent = "오늘 품절 예시";
else if (!store.available) saveButton.textContent = "다른 매장을 골라 주세요";
else saveButton.textContent = "선택 내용 저장";
}
if (saveStatus) {
saveStatus.textContent = "";
saveStatus.classList.remove("is-error");
}
};
builder?.addEventListener("change", updateBuilder);
const applyPreset = (preset) => {
if (!builder) return;
if (preset === "single") {
setRadio("flavor", "classic");
setRadio("patty", "1");
} else if (preset === "spicy") {
setRadio("flavor", "spicy");
setRadio("patty", "2");
} else {
setRadio("flavor", "classic");
setRadio("patty", "2");
}
updateBuilder();
builderSection?.scrollIntoView({ behavior: "smooth", block: "start" });
window.setTimeout(() => builder.querySelector("input:checked")?.focus({ preventScroll: true }), 350);
};
document.querySelectorAll("[data-preset]").forEach((button) => {
button.addEventListener("click", () => applyPreset(button.dataset.preset || "double"));
});
saveButton?.addEventListener("click", () => {
if (!builder || !saveStatus) return;
const snapshot = Object.fromEntries(new FormData(builder).entries());
try {
if (fixture === "save-fail") throw new Error("fixture");
sessionStorage.setItem("griddle-club-demo-selection", JSON.stringify(snapshot));
saveStatus.classList.remove("is-error");
saveStatus.textContent = "이 기기의 현재 탭에 선택 내용을 남겼어요. 실제 주문으로 전송되지 않습니다.";
} catch {
saveStatus.classList.add("is-error");
saveStatus.textContent = "선택 내용을 남기지 못했어요. 고른 내용은 화면에 그대로 두었습니다.";
}
});
const storeCards = [...document.querySelectorAll("[data-store]")];
const mapPins = [...document.querySelectorAll("[data-map-store]")];
const storeSearch = document.querySelector("#store-search");
const clearStoreSearch = document.querySelector("#clear-store-search");
const storeEmpty = document.querySelector("#store-empty");
const regionButtons = [...document.querySelectorAll("[data-region]")];
let activeRegion = "all";
const selectStore = (storeKey, shouldMove = false) => {
const select = builder?.elements.namedItem("store");
if (select instanceof HTMLSelectElement) select.value = storeKey;
storeCards.forEach((card) => card.classList.toggle("is-selected", card.dataset.store === storeKey));
mapPins.forEach((pin) => pin.classList.toggle("is-active", pin.dataset.mapStore === storeKey));
updateBuilder();
if (shouldMove) builderSection?.scrollIntoView({ behavior: "smooth", block: "start" });
};
document.querySelectorAll("[data-select-store]").forEach((button) => {
button.addEventListener("click", () => selectStore(button.dataset.selectStore, true));
});
mapPins.forEach((button) => {
button.addEventListener("click", () => selectStore(button.dataset.mapStore, false));
});
const filterStores = () => {
const query = storeSearch instanceof HTMLInputElement ? storeSearch.value.trim().toLocaleLowerCase("ko-KR") : "";
let visibleCount = 0;
storeCards.forEach((card) => {
const regionMatch = activeRegion === "all" || card.dataset.region === activeRegion;
const queryMatch = !query || (card.dataset.search || "").toLocaleLowerCase("ko-KR").includes(query);
const visible = fixture !== "store-none" && regionMatch && queryMatch;
card.hidden = !visible;
if (visible) visibleCount += 1;
const pin = mapPins.find((item) => item.dataset.mapStore === card.dataset.store);
if (pin) pin.hidden = !visible;
});
if (storeEmpty) storeEmpty.hidden = visibleCount > 0;
if (clearStoreSearch) clearStoreSearch.hidden = !query;
};
storeSearch?.addEventListener("input", filterStores);
clearStoreSearch?.addEventListener("click", () => {
if (storeSearch instanceof HTMLInputElement) {
storeSearch.value = "";
storeSearch.focus();
}
filterStores();
});
regionButtons.forEach((button) => {
button.addEventListener("click", () => {
activeRegion = button.dataset.region || "all";
regionButtons.forEach((item) => {
const active = item === button;
item.classList.toggle("is-active", active);
item.setAttribute("aria-pressed", String(active));
});
filterStores();
});
});
document.querySelector("#reset-store-search")?.addEventListener("click", () => {
activeRegion = "all";
if (storeSearch instanceof HTMLInputElement) storeSearch.value = "";
regionButtons.forEach((button) => {
const active = button.dataset.region === "all";
button.classList.toggle("is-active", active);
button.setAttribute("aria-pressed", String(active));
});
filterStores();
storeSearch?.focus();
});
updateBuilder();
filterStores();
})();