코딩 1도 모르는 왕초보를 위한 완벽 가이드

나만의 바탕화면 AI 알림 펫
복사 & 붙여넣기로 완성하기

어려운 코딩은 전혀 필요 없습니다. 순서대로 따라오기만 하면 됩니다.

사전 준비: Node.js 설치

프로그램을 조립하기 위해 필수 부품인 Node.js가 컴퓨터에 설치되어 있어야 합니다.
인터넷 검색창에 Node.js를 검색하고, 공식 홈페이지에서 'LTS 버전(Windows 설치 프로그램)'을 다운받아 다음-다음 버튼만 눌러 설치해 주세요.

Step 1. 펫 뼈대 파일 만들기

1. 내 컴퓨터의 C드라이브(C:\)에 들어가서 gemini-pet 이라는 이름의 새 폴더를 만듭니다.
2. 준비해둔 펫 캐릭터 이미지 2장(gemini-pet.webp, gemini-pet_green.png)을 이 폴더 안에 넣습니다.
3. 윈도우 메모장을 켜고 아래 4개의 코드를 각각 복사하여, 파일 이름을 똑같이 적고 모든 파일(*.*) 형식으로 저장합니다.
1번 파일 저장명 : package.json
{
  "name": "gemini-pet",
  "productName": "gemini-pet",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "build": "electron-builder --win nsis --x64"
  }
}
2번 파일 저장명 : main.js
const { app, BrowserWindow, ipcMain, Tray, Menu, shell } = require('electron');
const path = require('path');
const http = require('http');

let mainWindow;
let tray = null;
let isMuted = false;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 180,
    height: 220,
    transparent: true,
    frame: false,
    alwaysOnTop: true,
    resizable: false,
    skipTaskbar: true,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
    }
  });
  mainWindow.loadFile('index.html');
  moveToBottomRight();
}

function moveToBottomRight() {
  const { width, height } = require('electron').screen.getPrimaryDisplay().workAreaSize;
  mainWindow.setBounds({ x: width - 180, y: height - 220, width: 180, height: 220 });
}

function createTray() {
  tray = new Tray(path.join(__dirname, 'gemini-pet_green.png'));
  updateTrayMenu();
}

function updateTrayMenu() {
  const loginSettings = app.getLoginItemSettings();
  const isAutoLaunch = loginSettings.openAtLogin;
  const contextMenu = Menu.buildFromTemplate([
    { 
      label: mainWindow && mainWindow.isVisible() ? '펫 숨기기' : '펫 보이기', 
      click: () => { 
        if(mainWindow) { mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show(); }
        updateTrayMenu(); 
      } 
    },
    { label: '화면오른쪽아래로 옮기기', click: () => moveToBottomRight() },
    { 
      label: isMuted ? '알림 다시 켜기' : '알림쉬기(조용히)', 
      click: () => { isMuted = !isMuted; updateTrayMenu(); } 
    },
    { type: 'separator' },
    { 
      label: 'Window시작시 자동실행', 
      type: 'checkbox', 
      checked: isAutoLaunch, 
      click: (menuItem) => {
        app.setLoginItemSettings({ openAtLogin: menuItem.checked, path: app.getPath('exe') });
        updateTrayMenu();
      } 
    },
    { label: '펫폴더 열기', click: () => shell.openPath(__dirname) },
    { type: 'separator' },
    { label: '종료', click: () => app.quit() }
  ]);
  
  tray.setToolTip('gemini-pet');
  tray.setContextMenu(contextMenu);
}

function startDirectReceiver() {
  const server = http.createServer((req, res) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

    if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }
    if (req.method === 'POST') {
      let body = '';
      req.on('data', chunk => { body += chunk.toString(); });
      req.on('end', () => {
        try {
          const data = JSON.parse(body);
          if (mainWindow && !isMuted) { mainWindow.webContents.send('update-status', data); }
        } catch (e) { console.error(e); }
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ success: true }));
      });
    }
  });
  server.listen(5002, '127.0.0.1', () => {});
}

app.whenReady().then(() => {
  createWindow();
  createTray();
  startDirectReceiver();
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') { app.quit(); }
});
3번 파일 저장명 : index.html
<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <title>gemini-pet</title>
  <style>
    body {
      margin: 0;
      overflow: hidden;
      font-family: 'Jua', 'Cafe24 Ssurround', 'NanumSquareRound', 'Pretendard', sans-serif;
      user-select: none;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: flex-end;
      height: 100vh;
      background-color: transparent;
    }
    #bubble {
      width: 140px;
      min-height: 35px;
      background-color: #CCCCCC;
      border-radius: 12px;
      padding: 8px;
      margin-bottom: 8px;
      box-shadow: 0 4px 6px rgba(0,0,0,0.2);
      display: flex;
      align-items: center;
      justify-content: center;
      text-align: center;
      word-break: keep-all;
      line-height: 1.3;
      font-weight: bold;
      font-size: 11px;
      color: #000;
      position: relative;
    }
    #bubble::after {
      content: '';
      position: absolute;
      bottom: -8px;
      left: 50%;
      transform: translateX(-50%);
      border-width: 8px 8px 0;
      border-style: solid;
      border-color: inherit;
      border-left-color: transparent;
      border-right-color: transparent;
      border-bottom-color: transparent;
    }
    #petImage {
      width: 100px;
      height: 75px;
      -webkit-app-region: drag;
      cursor: move;
    }
  </style>
</head>
<body>
  <div id="bubble" style="border-top-color: #CCCCCC;">
    <span id="message">💤대기중 입니다</span>
  </div>
  <img id="petImage" src="gemini-pet.webp" alt="gemini-pet" draggable="false">
  
  <script src="renderer.js"></script>
</body>
</html>
4번 파일 저장명 : renderer.js
const { ipcRenderer } = require('electron');

const bubble = document.getElementById('bubble');
const messageSpan = document.getElementById('message');

const COLORS = {
  대기: '#CCCCCC',
  질문: '#E6E6FA',
  완료: '#4CAF50'
};

let currentTitle = '';
let currentState = '대기';
let displayInterval = null;
let isShowingTitle = false;

function setBubbleStyle(color) {
  bubble.style.backgroundColor = color;
  bubble.style.borderTopColor = color;
}

function updateDisplay() {
  if (displayInterval) {
    clearInterval(displayInterval);
    displayInterval = null;
  }

  if (currentState === '대기') {
    setBubbleStyle(COLORS.대기);
    messageSpan.textContent = "💤대기중 입니다";
    return;
  }

  const isDone = currentState === '완료';
  setBubbleStyle(isDone ? COLORS.완료 : COLORS.질문);

  const titleText = currentTitle;
  const statusText = isDone 
    ? `🎉 ${titleText} 작업이 완료되었습니다`
    : `🙋‍♀️ ${titleText} 질문있습니다!`;

  isShowingTitle = true;
  messageSpan.textContent = titleText;

  displayInterval = setInterval(() => {
    isShowingTitle = !isShowingTitle;
    messageSpan.textContent = isShowingTitle ? titleText : statusText;
  }, 1000);
}

ipcRenderer.on('update-status', (event, data) => {
  currentTitle = data.title || '작업';

  if (data.state === 'generating') {
    currentState = '질문';
  } else if (data.state === 'done') {
    currentState = '완료';
  } else {
    currentState = '대기';
  }

  updateDisplay();
});

Step 2. 설치 파일(EXE) 조립하기

파일 4개를 모두 만드셨나요? 이제 이 파일들을 하나의 진짜 윈도우 프로그램으로 압축할 차례입니다.
1. 화면 왼쪽 아래 Windows 시작 버튼을 누르고 cmd를 검색해 [명령 프롬프트]를 켭니다.
2. 검은 창이 뜨면 아래 명령어 3줄을 순서대로 하나씩 복사해서 붙여넣고 엔터(Enter)를 치세요.
C:\Users\User> cd C:\gemini-pet
C:\gemini-pet> npm install electron electron-builder --save-dev
(설치가 끝날 때까지 1~2분 정도 멈춘 것처럼 보일 수 있으니 가만히 기다려주세요.)
C:\gemini-pet> npm run build
설치 완료!
조립이 끝나면 C:\gemini-pet 폴더 안에 [dist] 라는 새 폴더가 생깁니다. 그 안에 있는 gemini-pet Setup 1.0.0.exe 파일을 더블클릭해서 설치하면 바탕화면에 펫이 나타납니다!

Step 3. 크롬 감지기 만들기

펫이 제미나이 화면을 쳐다볼 수 있도록 눈알을 달아주는 작업입니다.
1. C:\gemini-pet 폴더 안에 extension 이라는 이름의 새 폴더를 하나 만듭니다.
2. 그 안에 메모장으로 아래 2개의 파일을 각각 모든 파일(*.*) 형식으로 만들어 저장합니다.
5번 파일 저장명 : manifest.json
{
  "manifest_version": 3,
  "name": "gemini-pet 통합 감지기",
  "version": "1.0.0",
  "description": "gemini의 답변 상태를 바탕화면 펫에 알립니다.",
  "permissions": ["activeTab", "scripting"],
  "host_permissions": ["https://gemini.google.com/*", "http://127.0.0.1:5002/*"],
  "content_scripts": [
    {
      "matches": ["https://gemini.google.com/*"],
      "js": ["content.js"]
    }
  ]
}
6번 파일 저장명 : content.js
let isGenerating = false;

// 1. 강제 기상 나팔: 스크립트가 켜지자마자 펫을 즉시 깨웁니다.
fetch('http://127.0.0.1:5002', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title: 'gemini', status: 'done', state: 'done' })
}).catch(err => console.log('Error:', err));

// 2. 0.3초 단위 초정밀 감시 시작
setInterval(() => {
    const stopButton = document.querySelector('button[aria-label*="중지"], button[aria-label*="Stop"]');
    const currentlyGenerating = !!stopButton;

    if (currentlyGenerating !== isGenerating) {
        isGenerating = currentlyGenerating;
        const currentStatus = isGenerating ? 'generating' : 'done';

        // 서버 통신 오류 방지를 위해 양쪽 변수(status, state) 모두 전송
        fetch('http://127.0.0.1:5002', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ title: 'gemini', status: currentStatus, state: currentStatus })
        }).catch(err => console.log('Error:', err));
    }
}, 300);

Step 4. 크롬에 감지기 달아주기

1. 크롬 주소창에 chrome://extensions 를 입력하고 엔터를 칩니다.
2. 오른쪽 위에 있는 [개발자 모드] 스위치를 켭니다.
3. 왼쪽 위에 [압축해제된 확장 프로그램을 로드합니다] 버튼을 누르고, 방금 만든 C:\gemini-pet\extension 폴더를 선택합니다.
4. 등록된 카드 오른쪽 아래의 [사이트 액세스] 관련 스위치 2개를 모두 파란색으로 켜줍니다.

🎉 완성! 이제 제미나이 창을 열고 새로고침(F5)을 한 뒤 질문을 던져보세요! 펫이 반응할 겁니다.