js
[JS] 구글 제미나이 api 연동

구글의 제미나이를 내 홈페이지에 넣을 수 있는 HTML + JS 소스입니다.
여기서 수정해야 할 변수는 3개입니다.
- ALLOWED_ORIGINS: 내 소스를 무단으로 퍼가는 것을 막습니다. js 코드는 암호화해서 사용하세요.
- API_KEY: 구글의 제미나이 API 키입니다. 무료입니다.
- PEXELS_KEY: Pexels 키를 발급받아서 사용하세요. 무료이며 사진 출력에 사용됩니다.
js 암호화는 https://obfuscator.io/ 같은 사이트에서 가능합니다.
API 키가 없다면 구글에서 키를 발급받으세요.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gemini Direct Chat</title>
<style>
.post-header{display:none;}
.gemini-chat-wrapper{display:flex;justify-content:center;padding:20px 0;background-color:#fff0}#chat-container{width:100%;max-width:500px;height:600px;background:#fff;border:1px solid #ddd;border-radius:15px;box-shadow:0 5px 15px rgb(0 0 0 / .05);display:flex;flex-direction:column;overflow:hidden}#chat-header{background-color:#fff;padding:15px 20px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #eee}.header-left{display:flex;align-items:center;gap:10px}.status-dot{width:8px;height:8px;background-color:#28a745;border-radius:50%;box-shadow:0 0 5px rgb(40 167 69 / .5)}.header-title{font-size:16px;font-weight:600;color:#333}.header-right{display:flex;align-items:center}.model-badge{background-color:#f0f4ff;color:#007bff;font-size:11px;font-weight:700;padding:3px 8px;border-radius:20px;text-transform:uppercase}#chat-container{width:450px;height:700px;background:#fff;border-radius:15px;box-shadow:0 5px 15px rgb(0 0 0 / .1);display:flex;flex-direction:column;overflow:hidden}#chat-box{flex:1;padding:20px;overflow-y:auto;border-bottom:1px solid #eee;display:flex;flex-direction:column;gap:15px}.message{padding:10px 15px;border-radius:15px;max-width:80%;line-height:1.4;font-size:14px;word-break:break-word}.user{align-self:flex-end;background-color:#007bff;color:#fff;border-bottom-right-radius:2px}.ai{align-self:flex-start;background-color:#e9e9eb;color:#333;border-bottom-left-radius:2px}#input-area{padding:15px;display:flex;gap:10px;background:#fff}input{flex:1;padding:10px;border:1px solid #ddd;border-radius:5px;outline:none}button{padding:10px 15px;background:#007bff;color:#fff;border:none;border-radius:5px;cursor:pointer}.copy-btn{font-size:10px;padding:3px 8px;margin-top:5px;cursor:pointer;border:none;border-radius:5px;background:#6c757d;color:#fff}.ai p{margin:0 0 10px 0}.ai p:last-child{margin-bottom:0}.ai ul,.ai ol{padding-left:20px;margin:5px 0}.ai code{background:#f0f0f0;padding:2px 4px;border-radius:3px;font-family:monospace}.ai pre{background:#2d2d2d;color:#fff;padding:10px;border-radius:5px;overflow-x:auto}.ai table{border-collapse:collapse;width:100%;margin:10px 0}.ai th,.ai td{border:1px solid #ddd;padding:8px;text-align:left}.ai th{background-color:#f2f2f2}.typing{display:inline-flex;align-items:center;gap:3px;font-weight:700;color:#007bff}.dot{width:4px;height:4px;background-color:#007bff;border-radius:50%;animation:bounce 1.4s infinite ease-in-out both}.dot:nth-child(1){animation-delay:-0.32s}.dot:nth-child(2){animation-delay:-0.16s}@keyframes bounce{0%,80%,100%{transform:scale(0)}40%{transform:scale(1)}}
.message { animation: fadeIn 0.4s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
/* 기존 스타일에 추가 */
.ai img { cursor: pointer; transition: transform 0.2s; }
.ai img:hover { transform: scale(1.02); }
/* 모달 스타일 (source 3 기반) */
.modal { display: none; position: fixed; z-index: 10000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.9); overflow: auto; }
.modal-content-wrapper { margin: auto; display: flex; flex-direction: column; align-items: center; padding: 40px 0; }
.modal-content { max-width: 90%; max-height: 70vh; border-radius: 5px; }
.close-modal { position: absolute; top: 20px; right: 35px; color: #f1f1f1; font-size: 40px; font-weight: bold; cursor: pointer; }
.download-btn-modal { margin-top: 20px; padding: 12px 25px; background-color: #05a081; color: #fff; border: none; border-radius: 5px; font-weight: 700; cursor: pointer; }
#modal-caption { margin-top: 15px; color: #ccc; }
</style>
</head>
<body>
<div id="image-modal" class="modal">
<span class="close-modal" onclick="closeImgModal()">×</span>
<div class="modal-content-wrapper">
<img class="modal-content" id="full-image">
<div id="modal-caption"></div>
<button id="modal-download-btn" class="download-btn-modal">PC에 저장</button>
</div>
</div>
<div class="gemini-chat-wrapper">
<div id="chat-container">
<div id="chat-header">
<div class="header-left">
<div class="status-dot"></div>
<span class="header-title">Google AI</span>
</div>
<div class="header-right">
<span class="model-badge">Gemini 1.5 Flash</span>
</div>
</div>
<div id="chat-box">
<div class="message ai">반갑습니다! 무엇을 도와드릴까요?</div>
</div>
<form id="chat-form" style="display:contents;">
<div id="input-area">
<input type="text" id="user-input" placeholder="메시지를 입력하세요..." required>
<button type="submit" id="send-btn">전송</button>
<button type="button" id="stop-btn" style="display:none; background:#ff4d4d;">취소</button>
</div>
</form>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
const ALLOWED_ORIGINS = ['localhost'];
const API_KEY = "YOUR_API_KEY";
(function() {
const currentHost = window.location.hostname;
const isAllowed = ALLOWED_ORIGINS.some(origin => currentHost.includes(origin));
if (!isAllowed) {
alert("이 서비스는 허용되지 않은 도메인에서 실행되었습니다.");
document.body.innerHTML = "<h1>Access Denied</h1>";
throw new Error("Unauthorized domain access.");
}
})();
const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent?key=${API_KEY}`;
let controller;
// Pexels 이미지 가져오기 함수
async function fetchPexelsImage(query) {
const PEXELS_KEY = "YOUR_API_KEY";
const url = `https://api.pexels.com/v1/search?query=${encodeURIComponent(query)}&per_page=1`;
try {
const response = await fetch(url, { headers: { Authorization: PEXELS_KEY } });
const data = await response.json();
return data.photos && data.photos.length > 0 ? data.photos[0].src.large : null;
} catch (error) {
console.error("이미지 호출 실패:", error);
return null;
}
}
document.getElementById('chat-form').addEventListener('submit', async (e) => {
e.preventDefault();
const inputField = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
const stopBtn = document.getElementById('stop-btn');
const message = inputField.value.trim();
if (!message) return;
sendBtn.style.display = 'none';
stopBtn.style.display = 'inline-block';
appendMessage('user', message);
inputField.value = '';
const tempMsgId = 'temp-' + Date.now();
const thinkingHTML = `<div class="typing">AI가 생각 중<span class="dot"></span><span class="dot"></span><span class="dot"></span></div>`;
appendMessage('ai', thinkingHTML, tempMsgId);
controller = new AbortController();
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ contents: [{ parts: [{ text: message }] }] }),
signal: controller.signal
});
const data = await response.json();
// 1. 구글 서버가 에러를 보낸 경우
if (data.error) {
let friendlyMessage = "";
if (data.error.code === 429 || data.error.status === "RESOURCE_EXHAUSTED") {
updateMessage(tempMsgId, "💡 AI가 지금 조금 바쁘네요! 약 20~30초만 기다렸다가 다시 전송 버튼을 눌러주시겠어요? 😊");
} else if (data.error.code === 404) {
updateMessage(tempMsgId, "💡 AI 모델 경로를 찾을 수 없습니다. 주소 설정을 다시 확인해 주세요.");
} else {
updateMessage(tempMsgId, "💡️ 서비스에 잠시 문제가 생겼습니다: " + data.error.message);
}
return;
}
if (data.candidates && data.candidates[0].content) {
const aiResponse = data.candidates[0].content.parts[0].text;
// 여기서 updateMessage를 호출 (async 함수이므로 내부 처리가 부드러워짐)
await updateMessage(tempMsgId, aiResponse);
}
} catch (error) {
updateMessage(tempMsgId, error.name === 'AbortError' ? "🚫 사용자가 요청을 취소했습니다." : "❌ 통신 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
} finally {
sendBtn.style.display = 'inline-block';
stopBtn.style.display = 'none';
}
});
document.getElementById('stop-btn').onclick = () => controller && controller.abort();
function appendMessage(sender, text, id = null) {
const chatBox = document.getElementById('chat-box');
const wrapper = document.createElement('div');
wrapper.style.display = 'flex';
wrapper.style.flexDirection = 'column';
wrapper.style.alignItems = (sender === 'user') ? 'flex-end' : 'flex-start';
const msgDiv = document.createElement('div');
msgDiv.classList.add('message', sender);
if (id) msgDiv.id = id;
if (sender === 'ai') {
msgDiv.innerHTML = text; // 초기 애니메이션용
} else {
msgDiv.innerText = text;
}
wrapper.appendChild(msgDiv);
if (sender === 'ai') {
const copyBtn = document.createElement('button');
copyBtn.innerText = '📋 Copy';
copyBtn.classList.add('copy-btn');
copyBtn.style.display = 'none';
wrapper.appendChild(copyBtn);
}
chatBox.appendChild(wrapper);
chatBox.scrollTop = chatBox.scrollHeight;
}
// 핵심: async 키워드를 붙여 await 사용 가능하게 수정
async function updateMessage(id, newText) {
const msgDiv = document.getElementById(id);
if (!msgDiv) return;
// 취소(Abort) 메시지인지 확인
const isCancelled =
newText.includes("💡️") ||
newText.includes("❌") ||
newText.includes("🚫") ||
newText.includes("서비스에 잠시 문제가 생겼습니다");
let cleanText = newText;
let imageUrl = null;
// JSON 데이터(이미지 요청)가 포함되어 있는지 확인 및 추출
const jsonMatch = newText.match(/\{[\s\S]*\}/);
if (jsonMatch && !isCancelled) {
try {
const parsed = JSON.parse(jsonMatch[0]);
let prompt = "";
if (parsed.action_input) {
const inner = typeof parsed.action_input === 'string' ? JSON.parse(parsed.action_input) : parsed.action_input;
prompt = inner.prompt || "";
}
if (prompt) {
imageUrl = await fetchPexelsImage(prompt);
cleanText = newText.replace(jsonMatch[0], "").trim();
}
} catch (e) { console.error("JSON 파싱 에러"); }
}
// 부드러운 타이핑 효과
let i = 0;
msgDiv.innerHTML = "";
// 취소된 메시지나 에러 메시지는 타이핑 없이 즉시 출력
if (isCancelled) {
msgDiv.innerText = cleanText;
// 옆에 붙어있던 복사 버튼을 찾아서 삭제합니다.
const sideBtn = msgDiv.parentElement.querySelector('.copy-btn');
if (sideBtn) sideBtn.remove();
return; // 복사 버튼 등을 만들지 않고 종료
}
const timer = setInterval(() => {
if (i < cleanText.length) {
msgDiv.innerText = cleanText.slice(0, i + 1);
i++;
const chatBox = document.getElementById('chat-box');
chatBox.scrollTop = chatBox.scrollHeight;
} else {
clearInterval(timer);
msgDiv.innerHTML = marked.parse(cleanText);
if (imageUrl) {
const img = document.createElement('img');
img.src = imageUrl;
img.style.cssText = 'width: 100%; border-radius: 10px; display: block; margin-top:10px;';
// 이미지 클릭 시 모달 열기 연결[cite: 3]
img.onclick = () => openImgModal(imageUrl, "AI 추천 이미지");
img.onload = () => {
const chatBox = document.getElementById('chat-box');
chatBox.scrollTop = chatBox.scrollHeight;
};
msgDiv.appendChild(img);
// [수정] 채팅창 안의 '이미지 저장' 버튼도 모달을 거치지 않고 즉시 다운로드 가능하도록 수정
const quickDlBtn = document.createElement('button');
quickDlBtn.innerText = '💾 즉시 저장';
quickDlBtn.classList.add('copy-btn');
quickDlBtn.style.cssText = 'display: block; margin-top: 5px; background: #28a745; color:white;';
quickDlBtn.onclick = () => { currentDownloadUrl = imageUrl; document.getElementById('modal-download-btn').click(); };
msgDiv.appendChild(quickDlBtn);
}
const btn = msgDiv.parentElement.querySelector('.copy-btn');
if (btn) {
btn.style.display = 'inline-block';
btn.onclick = () => copyToClipboard(cleanText, btn);
}
}
}, 30); // 타이핑 속도 조절
}
// 이미지 다운로드 함수
async function downloadImage(url) {
try {
const response = await fetch(url);
const blob = await response.blob();
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = `gemini-image-${Date.now()}.jpg`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(blobUrl);
} catch (error) {
alert("이미지 다운로드에 실패했습니다.");
}
}
async function copyToClipboard(text, btn) {
await navigator.clipboard.writeText(text);
btn.innerText = "🔴 Copied!";
setTimeout(() => btn.innerText = '🔵 Copy', 2000);
}
let currentDownloadUrl = "";
// 1. 모달 열기 함수
function openImgModal(url, alt) {
currentDownloadUrl = url;
const modal = document.getElementById('image-modal');
modal.style.display = "block";
document.getElementById('full-image').src = url;
document.getElementById('modal-caption').innerText = alt || "Pexels Image";
}
// 2. 모달 닫기 함수
function closeImgModal() {
document.getElementById('image-modal').style.display = "none";
}
// 3. 강제 다운로드 로직 (Blob 방식)[cite: 3]
document.getElementById('modal-download-btn').onclick = async () => {
try {
const response = await fetch(currentDownloadUrl);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `gemini-photo-${Date.now()}.jpg`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
alert("다운로드에 실패했습니다. 이미지를 우클릭하여 저장해 주세요.");
}
};
</script>
</body>
</html>
0 댓글