/* js/ai.js * Munjanara Image Editor - AI Edit Module * - requires globals from image-editor.js: * canvas, img, cropper, canvasWidth, canvasHeight * addJsonToCanvas(jdata, filePath?), setImageAttr() */ (function () { "use strict"; // ---------- UI ids ---------- const IDS = { preset: "aiPreset2", prompt: "aiPrompt2", apply: "btnAiApply2", undo: "btnAiUndo2", refPick: "btnAiRefPick2", refFile: "aiRefFile2", refName: "aiRefName2", overlay: "aiOverlay2", ovTitle: "aiOvTitle2", ovDesc: "aiOvDesc2", cancel: "aiCancel2", }; // ---------- State ---------- const AI_HISTORY_MAX = 4; let aiPrevStackMap = {}; let controller = null; let undoPreviewBackup = null; // hover ½ÃÀÛ ½Ã ÇöÀç »óÅ ¹é¾÷(JSON) let undoPreviewing = false; // ÇöÀç ÇÁ¸®ºä ÁßÀÎÁö let undoPreviewTimer = null; // ºü¸¥ Åä±Û ¹æÁö¿ë(¼±ÅÃ) // ---------- Helpers ---------- function $(id) { return document.getElementById(id); } function safeAlert(msg) { try { if (typeof Alert === "function") Alert(msg); else alert(msg); } catch (_) { alert(msg); } } function getCurrentStack() { const idx = window.jobIndex ?? 0; if (!aiPrevStackMap[idx]) { aiPrevStackMap[idx] = []; } return aiPrevStackMap[idx]; } function setBusy(on, title, desc) { const ov = $(IDS.overlay); const t = $(IDS.ovTitle); const d = $(IDS.ovDesc); const btnApply = $(IDS.apply); const btnUndo = $(IDS.undo); if (!ov) return; if (on) { if (t) t.textContent = title || "AI ó¸®Áß¡¦"; if (d) d.textContent = desc || "¿äû»çÇ×À» Àû¿ëÇϰí ÀÖ½À´Ï´Ù."; ov.style.display = "flex"; if (btnApply) btnApply.disabled = true; if (btnUndo) btnUndo.disabled = true; } else { ov.style.display = "none"; if (btnApply) btnApply.disabled = false; } } function syncUndoBtn() { const btn = $(IDS.undo); if (!btn) return; const stack = getCurrentStack(); btn.disabled = stack.length === 0; } function pushUndoState() { try { if (!window.canvas) return; const snap = window.canvas.toJSON(); const stack = getCurrentStack(); stack.push(snap); while (stack.length > AI_HISTORY_MAX) { stack.shift(); } syncUndoBtn(); } catch (_) {} } function dataURLToBlob(dataUrl) { const arr = String(dataUrl).split(","); const mime = (arr[0].match(/:(.*?);/) || [])[1] || "image/png"; const bstr = atob(arr[1] || ""); let n = bstr.length; const u8 = new Uint8Array(n); while (n--) u8[n] = bstr.charCodeAt(n); return new Blob([u8], { type: mime }); } function removeCropperIfAny() { try { if (window.cropper && window.canvas) { window.canvas.remove(window.cropper); window.cropper = null; } } catch (_) {} } async function callAiEdit() { if (!window.img) { safeAlert("±âº» À̹ÌÁö°¡ ¼±ÅõÇÁö ¾Ê¾Ò½À´Ï´Ù."); return; } const promptEl = $(IDS.prompt); const prompt = (promptEl ? promptEl.value : "").trim(); if (!prompt) { safeAlert("¿äû»çÇ×À» ÀÔ·ÂÇØÁÖ¼¼¿ä."); return; } var objCnt = canvas.getObjects().length; if (objCnt >= 2 && !confirm("AI¿äû½Ã ÇØ´ç À̹ÌÁö°¡ ÅëÀ̹ÌÁö·Î º¯°æµË´Ï´Ù.\nÁøÇàÇϽðڽÀ´Ï±î?")) return; controller = new AbortController(); try { setBusy(true, "AI ó¸®Áß¡¦", "¿äû»çÇ×À» Àû¿ëÇϰí ÀÖ½À´Ï´Ù."); // undo ½º³À¼¦ pushUndoState(); const resultDurl = makeDataURL(jobIndex); const blob = dataURLToBlob(resultDurl); const ext = (blob.type === "image/jpeg") ? "jpg" : "png"; const file = new File([blob], `ai_result.${ext}`, { type: blob.type }); const fd = new FormData(); fd.append("action", "ai_apply"); fd.append("ai_user_prompt", prompt); fd.append("image", file); const refFile = $(IDS.refFile)?.files?.[0] || null; if (refFile) fd.append("ref_image", refFile); const res = await fetch(location.href, { method: "POST", body: fd, signal: controller.signal, }); if (res.status === 401) { setBusy(false); safeAlert("·Î±×ÀÎ ÈÄ ÀÌ¿ëÇØ ÁÖ¼¼¿ä."); return; } if (res.status === 429) { let j = {}; try { j = await res.json(); } catch (_) {} setBusy(false); safeAlert(j?.error === "DAILY_LIMIT" ? "AI¿äûÀº ÀÏÀÏ »ç¿ë·®ÀÌ Á¦Çѵ˴ϴÙ. ³»ÀÏ ´Ù½Ã ÀÌ¿ëÇØÁÖ¼¼¿ä." : "¿äûÀÌ ³Ê¹« ¸¹½À´Ï´Ù. Àá½Ã ÈÄ ´Ù½Ã ½ÃµµÇØ ÁÖ¼¼¿ä."); return; } const data = await res.json(); if (!data || !data.ok || !data.image) { setBusy(false); safeAlert("AI°¡ À̹ÌÁö¸¦ »ý¼ºÇÏÁö ¸øÇß½À´Ï´Ù.
- À̹ÌÁö¿¡ ¸ÂÁö ¾Ê°Å³ª »çȸÀû ºÐÀ§±â¿¡ ¹ÝÇÏ´Â ¿äû -"); return; } applyAiResultAsUpload(data.image); if ($(IDS.refFile)) $(IDS.refFile).value = ""; if ($(IDS.refName)) $(IDS.refName).textContent = "¾øÀ½"; setBusy(false); syncUndoBtn(); imageCate = 7; makeType = "ai_image"; setGuestCount(makeType); } catch (e) { setBusy(false); if (e?.name === "AbortError") safeAlert("ÀÛ¾÷ÀÌ Ãë¼ÒµÇ¾ú½À´Ï´Ù."); else { console.error(e); safeAlert("½Ã½ºÅÛ ¿À·ù°¡ ¹ß»ýÇß½À´Ï´Ù."); } } finally { controller = null; } } function bindUi() { const presetEl = $(IDS.preset); const promptEl = $(IDS.prompt); const applyBtn = $(IDS.apply); const undoBtn = $(IDS.undo); const refPick = $(IDS.refPick); const refFile = $(IDS.refFile); const refName = $(IDS.refName); const cancelBtn = $(IDS.cancel); if (!applyBtn || !promptEl) return false; if (presetEl) { presetEl.addEventListener("change", () => { const v = (presetEl.value || "").trim(); if (!v) return; promptEl.value = v; //presetEl.value = ""; promptEl.focus(); }); } if (refPick && refFile) { refPick.addEventListener("click", () => refFile.click()); } if (refFile && refName) { refFile.addEventListener("change", (e) => { const f = e.target.files?.[0]; refName.textContent = f ? f.name : "¾øÀ½"; }); } if (cancelBtn) { cancelBtn.addEventListener("click", () => { try { if (controller) controller.abort(); } catch (_) {} setBusy(false); syncUndoBtn(); }); } applyBtn.addEventListener("click", callAiEdit); if (undoBtn) { undoBtn.addEventListener("click", () => { if (undoPreviewing) endUndoPreview(); const stack = getCurrentStack(); if (stack.length === 0) return; if (!confirm("ÀÌÀü »óÅ·ΠµÇµ¹¸®½Ã°Ú½À´Ï±î?")) return; const prev = stack.pop(); syncUndoBtn(); if (typeof window.addJsonToCanvas === "function") { window.addJsonToCanvas(prev); } else { safeAlert("º¹¿ø ÇÔ¼ö(addJsonToCanvas)¸¦ ãÁö ¸øÇß½À´Ï´Ù."); } }); } // ===== Undo hover preview (ºñ±³ ¹Ì¸®º¸±â) ===== function startUndoPreview() { if (!undoBtn || undoBtn.disabled) return; if (undoPreviewing) return; // AI ó¸®Áß ¿À¹ö·¹ÀÌ ¶°ÀÖÀ¸¸é ºñ±³ ¹Ì¸®º¸±â ±ÝÁö(¿øÇϸé Á¦°Å °¡´É) const ov = $(IDS.overlay); if (ov && ov.style.display !== "none") return; const stack = getCurrentStack(); if (!stack || stack.length === 0) return; // ÇöÀç »óÅ ¹é¾÷ (µÇµ¹¾Æ¿Ã ¿ëµµ) try { undoPreviewBackup = window.canvas.toJSON(); } catch (_) { undoPreviewBackup = null; return; } // Á÷Àü »óÅÂ(POPÇÏÁö ¾ÊÀ½!)·Î ¹Ì¸®º¸±â const prev = stack[stack.length - 1]; undoPreviewing = true; try { if (typeof window.addJsonToCanvas === "function") { window.addJsonToCanvas(prev); } } catch (e) { console.error(e); undoPreviewing = false; undoPreviewBackup = null; } } function endUndoPreview() { if (!undoPreviewing) return; if (!undoPreviewBackup) { undoPreviewing = false; return; } try { if (typeof window.addJsonToCanvas === "function") { window.addJsonToCanvas(undoPreviewBackup); } } catch (e) { console.error(e); } finally { undoPreviewing = false; undoPreviewBackup = null; } } // µ¥½ºÅ©Å¾: hover undoBtn.addEventListener("mouseenter", () => { // (¼±ÅÃ) ºü¸£°Ô ½ºÄ¡¸é ±ôºýÀÏ ¼ö À־ 80ms µô·¹ÀÌ clearTimeout(undoPreviewTimer); undoPreviewTimer = setTimeout(startUndoPreview, 80); }); undoBtn.addEventListener("mouseleave", () => { clearTimeout(undoPreviewTimer); endUndoPreview(); }); // ¸ð¹ÙÀÏ/ÅÍÄ¡: ´©¸£°í ÀÖ´Â µ¿¾È ºñ±³ undoBtn.addEventListener("pointerdown", (e) => { // ¹öư Ŭ¸¯(undo)°ú Ãæµ¹ ¹æÁö: "´­·¯¼­ ºñ±³"¸¸ ÇÏ°í ½ÍÀ¸¸é preventDefault // e.preventDefault(); startUndoPreview(); }); undoBtn.addEventListener("pointerup", endUndoPreview); undoBtn.addEventListener("pointercancel", endUndoPreview); syncUndoBtn(); return true; } function waitForEditorReady() { // Fabric canvas »ý¼º/Àü¿ªÇÔ¼ö ÁغñµÇ¸é ºÙÀÓ const maxTry = 200; // ~20s let t = 0; const timer = setInterval(() => { t++; const ok = window.canvas && window.fabric && typeof window.canvas.toJSON === "function" && typeof window.addJsonToCanvas === "function" && typeof window.setImageAttr === "function" && $(IDS.apply); // UI Á¸Àç È®ÀÎ if (ok) { clearInterval(timer); bindUi(); } if (t >= maxTry) { clearInterval(timer); console.warn("[ai.js] editor not ready - skipped binding"); } }, 100); } function dataURLToFile(dataURL, filename = "ai_result.png") { const [header, b64] = dataURL.split(","); const mime = header.match(/data:(.*?);base64/)?.[1] || "image/png"; const bin = atob(b64); const arr = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) { arr[i] = bin.charCodeAt(i); } return new File([arr], filename, { type: mime }); } function applyAiResultAsUpload(aiDataURL) { const file = dataURLToFile(aiDataURL, "ai_result.png"); addImageToCanvas(file, 0); // 1:½ºÆ¼Ä¿, 0:±âº»À̹ÌÁö } syncUndoBtn(); // DOMReady document.addEventListener("DOMContentLoaded", waitForEditorReady); })();