diff --git a/remicon_cost_app.html b/remicon_cost_app.html
index 91c7c9e..202cffe 100644
--- a/remicon_cost_app.html
+++ b/remicon_cost_app.html
@@ -364,7 +364,11 @@
@@ -527,6 +531,16 @@
const SELECTED_PRICE_SET_KEY = "remicon_selected_price_set_v1";
const MIX_DRAFT_KEY = "remicon_mix_draft_v1";
const PRICE_DRAFT_KEY = "remicon_price_draft_v1";
+ const CUSTOM_SPECS_KEY = "remicon_custom_specs_v1";
+ const PRESET_SPECS = [
+ "25-27-600",
+ "25-30-600",
+ "25-35-600",
+ "20-40-600",
+ "20-45-600",
+ "20-50-600",
+ "20-60-600"
+ ];
const DEFAULT_PRICE_SETS = [
{
@@ -690,6 +704,16 @@
}
}
+ function getCustomSpecs() {
+ return loadJsonArray(CUSTOM_SPECS_KEY)
+ .map((x) => String(x || "").trim())
+ .filter((x) => x.length > 0);
+ }
+
+ function saveCustomSpecs(specs) {
+ localStorage.setItem(CUSTOM_SPECS_KEY, JSON.stringify(specs));
+ }
+
function formatDateTime(iso) {
if (!iso) return "-";
const d = new Date(iso);
@@ -1032,6 +1056,58 @@
.sort((a, b) => a.spec.localeCompare(b.spec, "ko-KR", { numeric: true, sensitivity: "base" }));
}
+ function renderSpecDataList(selectedValue = "") {
+ const specSelect = document.getElementById("mSpec");
+ if (!specSelect) return;
+ specSelect.innerHTML = "";
+ const dynamicSpecs = uniqueLatestSpecs().map(({ spec }) => spec);
+ const customSpecs = getCustomSpecs();
+ const mergedSpecs = [...new Set([...PRESET_SPECS, ...customSpecs, ...dynamicSpecs])]
+ .sort((a, b) => a.localeCompare(b, "ko-KR", { numeric: true, sensitivity: "base" }));
+ const placeholder = document.createElement("option");
+ placeholder.value = "";
+ placeholder.textContent = "규격 선택";
+ specSelect.appendChild(placeholder);
+ mergedSpecs.forEach((spec) => {
+ const opt = document.createElement("option");
+ opt.value = spec;
+ opt.textContent = spec;
+ specSelect.appendChild(opt);
+ });
+ const target = selectedValue || "";
+ if (target && !mergedSpecs.includes(target)) {
+ const custom = document.createElement("option");
+ custom.value = target;
+ custom.textContent = target;
+ specSelect.appendChild(custom);
+ }
+ specSelect.value = target;
+ }
+
+ function addCustomSpec() {
+ const input = document.getElementById("mNewSpec");
+ if (!input) return;
+ const next = String(input.value || "").trim();
+ if (!next) {
+ alert("추가할 규격을 입력하세요.");
+ return;
+ }
+ const all = [...new Set([...PRESET_SPECS, ...getCustomSpecs(), ...uniqueLatestSpecs().map((x) => x.spec)])];
+ if (all.includes(next)) {
+ renderSpecDataList(next);
+ input.value = "";
+ return;
+ }
+ const updated = [...getCustomSpecs(), next]
+ .filter((x) => x)
+ .sort((a, b) => a.localeCompare(b, "ko-KR", { numeric: true, sensitivity: "base" }));
+ saveCustomSpecs(updated);
+ renderSpecDataList(next);
+ document.getElementById("mSpec").value = next;
+ saveDraft(MIX_DRAFT_KEY, getMixDraftPayload());
+ input.value = "";
+ }
+
function renderSpecList() {
const area = document.getElementById("specListArea");
const items = uniqueLatestSpecs();
@@ -1379,6 +1455,7 @@
mixModalTitle.textContent = "배합표 수정";
saveMixBtn.textContent = "수정 저장";
+ renderSpecDataList(entry.spec || "");
document.getElementById("mSpec").value = entry.spec || "";
document.getElementById("mWaterCementRatio").value = toNum(entry.mixMeta?.waterCementRatio);
document.getElementById("mFineAggRatio").value = toNum(entry.mixMeta?.fineAggRatio);
@@ -1409,6 +1486,7 @@
mixModalTitle.textContent = "배합표 등록";
saveMixBtn.textContent = "저장";
clearModalFields();
+ renderSpecDataList(selectedSpec || "");
document.getElementById("mSpec").value = selectedSpec || "";
renderMixPriceSetSelect(localStorage.getItem(SELECTED_PRICE_SET_KEY) || "");
applyMixDraft();
@@ -1498,6 +1576,16 @@
function applyMixDraft() {
const draft = loadDraft(MIX_DRAFT_KEY);
if (!draft) return;
+ const draftSpec = String(draft.mSpec || "").trim();
+ if (draftSpec) {
+ const specSelect = document.getElementById("mSpec");
+ if (specSelect && ![...specSelect.options].some((o) => o.value === draftSpec)) {
+ const opt = document.createElement("option");
+ opt.value = draftSpec;
+ opt.textContent = draftSpec;
+ specSelect.appendChild(opt);
+ }
+ }
Object.entries(draft).forEach(([id, val]) => {
const el = document.getElementById(id);
if (el) el.value = val ?? "";
@@ -2234,6 +2322,13 @@
document.getElementById("goPriceBtn").addEventListener("click", () => switchPage("price"));
document.getElementById("refreshBtn").addEventListener("click", renderMain);
document.getElementById("newMixBtn").addEventListener("click", openModal);
+ document.getElementById("addSpecBtn").addEventListener("click", addCustomSpec);
+ document.getElementById("mNewSpec").addEventListener("keydown", (e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ addCustomSpec();
+ }
+ });
document.getElementById("closeModalBtn").addEventListener("click", closeModal);
document.getElementById("cancelBtn").addEventListener("click", closeModal);
document.getElementById("saveMixBtn").addEventListener("click", saveMixEntry);
@@ -2256,12 +2351,13 @@
});
[
- "mSpec", "mPriceSetId", "mWaterCementRatio", "mFineAggRatio", "mWater",
+ "mPriceSetId", "mWaterCementRatio", "mFineAggRatio", "mWater",
"mCement", "mSlag", "mWashedSand", "mCrushedSand", "mMm20", "mMm25", "mAdmixture", "mNote"
].forEach((id) => {
const el = document.getElementById(id);
if (el) el.addEventListener("input", () => saveDraft(MIX_DRAFT_KEY, getMixDraftPayload()));
});
+ document.getElementById("mSpec").addEventListener("change", () => saveDraft(MIX_DRAFT_KEY, getMixDraftPayload()));
[
"pYear", "pMonth", "pCementPrice", "pSlagPrice", "pWashedSandPrice", "pCrushedSandPrice",
@@ -2295,4 +2391,4 @@
init();