AI로 만든 무료 웹 이미지 관리 도구 (ChatGPT, Gemini)

IndexedDB 기반 이미지 관리 도구 소개
일러스트 작업 등의 과정에서 이미지 자료의 관리 효율성을 높이기 위해, 제가 직접 제작한 IndexedDB 기반 이미지 관리 도구를 소개합니다. 이 도구는 OpenAI의 o3-mini와 구글의 Gemini를 활용하여 개발했으며, 제작 과정과 활용 방안을 함께 공유하려 합니다.
개발 배경
좋은 이미지 관리 프로그램은 많지만, 각자의 작업 방식에 완벽하게 딱 맞아 떨어지는 도구를 찾기는 어렵습니다. 예를 들어, 저는 주로 그림 작업 시 PureRef를 사용했는데, 모바일에서도 쉽게 사용할 수 있는 도구가 있으면 좋겠다는 생각을 했습니다. 그래서 HTML 코드만으로 누구나 쉽게 접근하고 모바일에서도 동작하는 이미지 관리 도구를 직접 제작하게 되었습니다.

특히 캐릭터 일러스트와 같은 창작 작업에서는 레퍼런스 이미지(포즈, 의상, 표정), 텍스처, 구도 등 다양한 자료를 효과적으로 관리해야 작업 흐름이 끊기지 않고 퀄리티를 유지할 수 있습니다. 이 도구는 이미지 추가, 순서 변경, 비교 등을 쉽고 효율적으로 할 수 있게 도와줍니다.
도구의 주요 특징
1. 다양한 이미지 추가 방식
- URL 입력: 이미지 주소를 입력하고 버튼 클릭으로 추가
- 로컬 파일 업로드: 파일 선택을 통해 이미지 추가
- 드래그 앤 드롭: 웹 페이지에서 이미지를 드래그하여 추가
2. 이미지 갤러리 및 모달 뷰어
- 섬네일 형태의 갤러리로 이미지 목록을 한눈에 확인
- 클릭 시 모달 팝업으로 원본 크기 이미지 확인 가능
3. 이미지 순서 변경 및 삭제
- PC 환경: 드래그 앤 드롭으로 이미지 순서를 변경
- 모바일 환경: 버튼 클릭으로 이미지 순서를 위아래로 이동
- 불필요한 이미지는 삭제 버튼으로 간편하게 제거
4. 오프라인 사용 (IndexedDB 활용)
- IndexedDB는 웹 브라우저에 내장된 데이터베이스입니다. 이미지와 같은 데이터를 브라우저에 저장하여 인터넷 연결 없이도 도구를 사용할 수 있습니다.
- 브라우저를 껐다 켜도 최종 상태가 그대로 유지됩니다.
IndexedDB는 브라우저별로 용량 제한이 있을 수 있습니다. (대부분의 최신 브라우저는 충분한 용량을 제공합니다.) 대용량 이미지를 많이 저장할 경우, 브라우저의 저장 공간이 부족해질 수 있습니다.
5. HTML 파일 저장 및 반응형 디자인
- 갤러리 상태를 HTML 파일로 저장하여 보관하거나 공유 가능
- PC, 모바일, 태블릿 등 다양한 환경에 최적화된 반응형 디자인 제공
💡Tip: 기능 중 고치고 싶은 부분은 o3-mini (OpenAI의 소형 모델)나 Gemini (Google의 AI 모델)를 활용해 쉽게 수정할 수 있습니다. 예를 들어, 이미지에 태그를 추가하고 싶다면 다음과 같이 프롬프트를 작성해 보세요.
"이미지 관리 도구에 이미지 태그 기능을 추가하고 싶어. 각 이미지에 여러 개의 태그를 추가하고, 태그별로 이미지를 필터링할 수 있는 HTML, CSS, JavaScript 코드를 작성해줘."o3-mini와 Gemini는 텍스트 기반 명령(프롬프트)을 통해 코드를 생성하거나 수정할 수 있습니다. 이 작업에서 o3-mini는 초기 코드 생성에 사용되었고, Gemini는 코드 개선 및 기능 추가에 활용되었습니다.
AI로 쉽게 나만의 도구 만들기
이 도구는 ChatGPT의 o3mini나 Gemini를 활용해 누구나 손쉽게 만들 수 있습니다. AI에게 원하는 기능을 설명하는 프롬프트를 작성하고, AI가 생성한 HTML 코드를 복사하여 실행하면 바로 사용할 수 있습니다.
1.프롬프트 작성
AI에게 원하는 기능을 설명하는 프롬프트를 작성합니다.
웹 브라우저에서 이미지 URL, 파일, 드래그 앤 드롭으로 추가하고, IndexedDB에 저장 및 관리하는 HTML, CSS, JavaScript 코드를 만들어줘. UI는 심플하고 사용하기 편리하게 구성해줘.2. AI가 생성한 HTML 코드 복사
AI가 제공한 코드를 참고하여 HTML 파일을 생성합니다. 헤당 HTML 코드를 메모장을 사용해 HTML로 저장하거나, 아래 링크를 통해 다운로드 할 수 있습니다.
아래 코드를 복사하여 .html 파일로 저장하세요 (예: image_manager.html)
[HTML 코드 펼치기]
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>통합 이미지 관리 도구 (IndexedDB)</title>
<style>
/* 기존 CSS 유지 (이전 답변 코드 참고) - 변경 없음 */
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 20px auto;
padding: 15px;
box-sizing: border-box;
display: flex;
flex-direction: column;
min-height: 100vh;
}
.input-section {
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 15px;
}
#imageInput {
width: 100%;
padding: 12px;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 8px;
box-sizing: border-box;
}
.button-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
padding: 15px 25px;
font-size: 16px;
border: none;
border-radius: 8px;
cursor: pointer;
transition: transform 0.1s;
flex: 1;
min-width: 120px;
}
.add-btn {
background-color: #4CAF50;
color: white;
}
.add-btn:hover {
background-color: #45a049;
}
.reset-btn {
background-color: #ff4444;
color: white;
}
.reset-btn:hover {
background-color: #e63e3e;
}
.image-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 15px;
}
.image-item {
border: 1px solid #eee;
border-radius: 8px;
background: white;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
}
.image-content {
position: relative;
}
.image-item img {
width: 100%;
height: auto;
max-height: 300px;
object-fit: contain;
display: block;
cursor: pointer;
}
.button-area {
display: flex;
justify-content: space-between;
padding: 5px;
gap: 5px;
}
.move-btn, .delete-btn {
background: none;
border: none;
cursor: pointer;
padding: 5px;
border-radius: 50%;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s;
}
.move-btn:hover, .delete-btn:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.move-btn svg {
width: 16px;
height: 16px;
fill: #777;
}
.move-btn:hover svg {
fill: #555;
}
.delete-btn svg {
width: 16px;
height: 16px;
fill: #ff4444;
}
.delete-btn:hover svg {
fill: #cc0000;
}
.file-input-label {
display: block;
padding: 12px;
background: #2196F3;
color: white;
text-align: center;
border-radius: 8px;
cursor: pointer;
box-sizing: border-box;
}
#fileInput {
display: none;
}
@media (max-width: 768px) {
body {
padding: 10px;
margin: 10px auto;
}
.button-group {
flex-direction: column;
}
button {
width: 100%;
padding: 18px;
}
.image-container {
grid-template-columns: 1fr;
}
.image-item img {
max-height: 250px;
}
/* 모바일 환경에서는 이동 버튼 보이기 */
.move-btn {
display: flex !important; /* important 로 우선순위 높임 */
}
}
/* 모달 추가 스타일 (기존 스타일 유지) */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.9);
overflow: auto;
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
display: block;
width: 95%;
max-width: 95%;
max-height: 95vh;
object-fit: contain;
}
.close {
position: absolute;
top: 15px;
right: 35px;
color: #fff;
font-size: 40px;
font-weight: bold;
cursor: pointer;
}
/* 로딩 메시지 스타일 추가 (기존 스타일 유지) */
#loadingMessage {
text-align: center;
margin-top: 10px;
color: #777;
font-style: italic;
display: none;
}
/* "이미지 올리기" 버튼 스타일 조정 (기존 스타일 유지) */
.move-up-btn {
order: -1;
margin-right: auto;
}
/* footer 스타일 (기존 스타일 유지) */
footer {
margin-top: auto;
padding: 20px;
text-align: center;
color: #777;
border-top: 1px solid #eee;
}
footer button.save-as-btn {
background-color: #2196F3;
color: white;
padding: 15px 25px;
font-size: 16px;
border: none;
border-radius: 8px;
cursor: pointer;
}
footer button.save-as-btn:hover {
background-color: #1976D2;
}
/* PC 환경에서는 이동 버튼 숨기기 (기존 스타일 유지) */
.move-btn {
display: none;
}
/* 드래그 앤 드롭 시 커서 스타일 변경 (PC) (기존 스타일 유지) */
.image-container.sortable-dragging .image-item {
cursor: grabbing;
}
.image-container.sortable-dragging {
cursor: grab;
}
/* 드래그 오버 시 스타일 (기존 스타일 유지) */
.image-container.drag-over {
border: 2px dashed #2196F3;
background-color: #f0f8ff;
}
/* 드래그 앤 드롭 영역 스타일 (수정됨: drag-over-area 클래스 선택자 수정) */
#dragDropArea {
border: 2px dashed #ccc;
border-radius: 8px;
padding: 20px;
text-align: center;
margin-bottom: 20px;
cursor: copy; /* 드래그 앤 드롭 가능한 영역 커서 변경 */
color: #777;
}
#dragDropArea.drag-over-area { /* dragDropArea 에 drag-over-area 클래스 적용 시 스타일 */
border-color: #2196F3;
background-color: #f0f8ff;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
</head>
<body>
<h1 style="margin-bottom: 25px; font-size: 1.8em;">📸 통합 이미지 관리 (IndexedDB)</h1>
<div class="input-section">
<input type="url"
id="imageInput"
placeholder="이미지 URL 입력 (예: https://example.com/image.jpg)">
<div class="button-group">
<button class="add-btn" onclick="addImage()">➕ URL 추가</button>
</div>
<div id="dragDropArea">
📂 이미지를 여기에 드래그 앤 드롭하세요
</div>
<label class="file-input-label">
📁 로컬 이미지 업로드
<input type="file"
id="fileInput"
accept="image/*"
multiple
onchange="handleFileUpload(this.files)">
</label>
<div class="button-group">
<button class="reset-btn" onclick="resetImages()">🗑️ 전체 초기화</button>
</div>
<div id="loadingMessage">이미지 로딩 중...</div>
</div>
<div id="imageGallery" class="image-container"></div>
<footer>
<button class="save-as-btn" onclick="saveAsHTML()">💾 다른 이름으로 저장</button>
</footer>
<script>
const DB_NAME = 'imageGalleryDB';
const DB_VERSION = 1; // 스키마 변경 시 버전 업데이트
const OBJECT_STORE_NAME = 'images';
let db; // IndexedDB database instance
const dragDropArea = document.getElementById('dragDropArea'); // 드래그 앤 드롭 영역 요소
const imageGallery = document.getElementById('imageGallery');
let sortableInstance = null; // SortableJS 인스턴스 저장 변수
// IndexedDB 초기화 및 데이터베이스 연결 - 기존과 동일 (변경 없음)
function initIndexedDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = (event) => {
console.error("IndexedDB error:", event.target.errorCode);
reject(event.target.error);
};
request.onsuccess = (event) => {
db = event.target.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore(OBJECT_STORE_NAME, { keyPath: 'url' }); // url을 keyPath로 사용
};
});
}
// 화면 너비 체크 함수 (PC 환경 기준: 768px 이상) - 기존과 동일 (변경 없음)
function isPC() {
return window.innerWidth >= 768;
}
// 페이지 로드 시 IndexedDB 초기화 및 이미지 불러오기 - 기존과 동일 (변경 없음)
document.addEventListener('DOMContentLoaded', () => {
initIndexedDB()
.then(() => {
loadSavedImagesFromIDB();
initDragAndDrop();
initExternalDragAndDrop(); // 외부 드래그앤드롭 초기화 함수 호출 (dragDropArea 에 이벤트 리스너 연결)
})
.catch(error => {
alert('IndexedDB 초기화 실패: ' + error.message);
});
});
// 드래그앤드롭 초기화 함수 (SortableJS) - 기존과 동일 (imageGallery 에 적용, 변경 없음)
function initDragAndDrop() {
if (isPC()) {
// PC 환경에서 SortableJS 활성화
sortableInstance = new Sortable(imageGallery, {
draggable: '.image-item',
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
onUpdate: function (evt/**: Sortable.SortableEvent*/) {
updateStorageOrder(); // 순서 변경 후 localStorage 업데이트
}
});
imageGallery.classList.add('draggable-pc');
} else {
// 모바일 환경에서는 SortableJS 비활성화 (혹시 활성화되어 있다면 파괴)
if (sortableInstance) {
sortableInstance.destroy();
sortableInstance = null;
}
imageGallery.classList.remove('draggable-pc');
}
}
// 외부 드래그 앤 드롭 초기화 함수 (웹 브라우저 이미지) - 수정됨: dragDropArea 에 이벤트 리스너 연결, console.log 추가
function initExternalDragAndDrop() {
dragDropArea.addEventListener('dragover', function(event) { // dragDropArea 에 이벤트 리스너 연결
event.preventDefault(); // 필수: drop 이벤트 발생을 위해
dragDropArea.classList.add('drag-over-area'); // dragDropArea 에 drag-over-area 클래스 추가
console.log('Drag over dragDropArea'); // 로그 추가
});
dragDropArea.addEventListener('dragleave', function(event) { // dragDropArea 에 이벤트 리스너 연결
dragDropArea.classList.remove('drag-over-area'); // dragDropArea 에 drag-over-area 클래스 제거
console.log('Drag leave dragDropArea'); // 로그 추가
});
dragDropArea.addEventListener('drop', function(event) { // dragDropArea 에 이벤트 리스너 연결
event.preventDefault(); // 기본 동작 방지 (이미지 새 탭 열기 등)
dragDropArea.classList.remove('drag-over-area'); // dragDropArea 에 drag-over-area 클래스 제거
console.log('Drop on dragDropArea'); // 로그 추가
const data = event.dataTransfer.getData('URL') || event.dataTransfer.getData('text/uri-list'); // URL 또는 URI 리스트에서 URL 추출
const files = event.dataTransfer.files;
if (files && files.length > 0) {
// 로컬 파일 드랍 처리 (기존 handleFileUpload 재활용)
handleFileUpload(files);
} else if (data) {
// 외부 URL 드랍 처리
addImageFromExternalURL(data);
}
});
}
// 외부 URL 이미지 추가 처리 함수 - 기존과 동일 (변경 없음)
function addImageFromExternalURL(url) {
if (!isValidImageUrl(url)) {
alert('유효한 이미지 URL이 아닙니다.');
return;
}
setLoading(true);
urlToDataUrl(url)
.then(dataUrl => {
const imageItem = createImageItem(dataUrl);
document.getElementById('imageGallery').prepend(imageItem);
saveImageToIDB(dataUrl); // IndexedDB 저장 함수 호출
setLoading(false);
})
.catch(error => {
setLoading(false);
console.error('이미지 로딩 실패 (외부 URL):', error);
alert('이미지 로딩 실패: URL을 확인하거나, CORS 정책을 확인해주세요.');
document.getElementById('imageGallery').prepend(createErrorImageItem());
});
}
// URL을 Data URL로 변환하는 함수 - 기존과 동일 (변경 없음)
function urlToDataUrl(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = function() {
const reader = new FileReader();
reader.onloadend = function() {
resolve(reader.result);
}
reader.onerror = reject;
reader.readAsDataURL(xhr.response);
};
xhr.onerror = reject;
xhr.open('GET', url);
xhr.responseType = 'blob';
xhr.send();
});
}
// 화면 리사이즈 이벤트 핸들러 - 기존과 동일 (변경 없음)
window.addEventListener('resize', function() {
initDragAndDrop();
});
// 로딩 메시지 표시/숨김 함수 - 기존과 동일 (변경 없음)
function setLoading(isLoading) {
const loadingMessage = document.getElementById('loadingMessage');
loadingMessage.style.display = isLoading ? 'block' : 'none';
}
// URL 이미지 추가 - 수정됨: IndexedDB 저장 함수 호출 (기존과 동일, 변경 없음)
function addImage() {
const imageUrl = document.getElementById('imageInput').value.trim();
if (!imageUrl) {
alert('이미지 URL을 입력해주세요');
return;
}
if (!isValidImageUrl(imageUrl)) {
alert('유효한 이미지 URL을 입력해주세요');
return;
}
setLoading(true);
const imageItem = createImageItem(imageUrl);
imageItem.querySelector('img').onload = () => {
document.getElementById('imageGallery').prepend(imageItem);
saveImageToIDB(imageUrl); // IndexedDB 저장 함수 호출
document.getElementById('imageInput').value = '';
setLoading(false);
};
imageItem.querySelector('img').onerror = () => {
setLoading(false);
alert('이미지 로딩 실패: URL을 확인해주세요.');
document.getElementById('imageGallery').prepend(createErrorImageItem());
};
}
// 이미지 URL 유효성 검사 - 기존과 동일 (변경 없음)
function isValidImageUrl(url) {
return(url.match(/\.(jpeg|jpg|gif|png|webp|bmp)$/) != null);
}
// 전체 초기화 - 수정됨: IndexedDB 데이터 삭제 (기존과 동일, 변경 없음)
function resetImages() {
if(confirm('모든 이미지를 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.')) {
clearIDBStore()
.then(() => {
document.getElementById('imageGallery').innerHTML = '';
})
.catch(error => {
console.error('IndexedDB 초기화 오류:', error);
alert('이미지 초기화 중 오류가 발생했습니다.');
});
}
}
// 모달 열기 함수 - 기존과 동일 (변경 없음)
function openModal(src) {
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<span class="close" onclick="closeModal()">×</span>
<img class="modal-content" src="${src}">
`;
modal.onclick = (e) => {
if (e.target === modal) closeModal();
};
document.body.appendChild(modal);
setTimeout(() => {
modal.style.display = 'flex';
}, 10);
}
// 모달 닫기 함수 - 기존과 동일 (변경 없음)
function closeModal() {
const modal = document.querySelector('.modal');
if (modal) {
modal.style.display = 'none';
setTimeout(() => {
modal.remove();
}, 600);
}
}
// 에러 이미지 아이템 생성 - 기존과 동일 (변경 없음)
function createErrorImageItem() {
const errorItem = document.createElement('div');
errorItem.className = 'image-item';
errorItem.innerHTML = '<p style="color:red; padding: 20px; text-align: center;">⚠️ 이미지 로딩 실패</p>';
return errorItem;
}
// 이미지 아이템 생성 - 기존과 동일 (변경 없음)
function createImageItem(url) {
const imageItem = document.createElement('div');
imageItem.className = 'image-item';
imageItem.dataset.url = url;
const imageContent = document.createElement('div');
imageContent.className = 'image-content';
const img = new Image();
img.src = url;
img.alt = "업로드 이미지";
img.style.cursor = 'pointer';
img.onclick = () => openModal(url);
imageContent.appendChild(img);
const buttonArea = document.createElement('div');
buttonArea.className = 'button-area';
// Move Up Button (모바일 환경에서만 표시) - 기존과 동일 (변경 없음)
if (!isPC()) {
const moveUpBtn = document.createElement('button');
moveUpBtn.className = 'move-btn move-up-btn';
moveUpBtn.ariaLabel = 'Move image up';
moveUpBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M7 10l5-5 5 5H7z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>`;
moveUpBtn.onclick = () => moveImageUp(imageItem);
buttonArea.appendChild(moveUpBtn);
const moveDownBtn = document.createElement('button');
moveDownBtn.className = 'move-btn move-down-btn';
moveDownBtn.ariaLabel = 'Move image down';
moveDownBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M7 14l5 5 5-5H7z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>`;
moveDownBtn.onclick = () => moveImageDown(imageItem);
buttonArea.appendChild(moveDownBtn);
}
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-btn';
deleteBtn.ariaLabel = 'Delete image';
deleteBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
<path fill="none" d="M0 0h24v24H0z"/>
</svg>`;
deleteBtn.onclick = () => deleteImage(url, imageItem);
buttonArea.appendChild(deleteBtn);
imageItem.appendChild(imageContent);
imageItem.appendChild(buttonArea);
return imageItem;
}
// 이미지 삭제 - 수정됨: IndexedDB 데이터 삭제 (기존과 동일, 변경 없음)
function deleteImage(url, element) {
if(confirm('이 이미지를 삭제하시겠습니까?')) {
deleteImageFromIDB(url)
.then(() => {
element.remove();
})
.catch(error => {
console.error('IndexedDB 삭제 오류:', error);
alert('이미지 삭제 중 오류가 발생했습니다.');
});
}
}
// 이미지 위로 이동 - 기존과 동일 (순서 업데이트는 미구현, 변경 없음)
function moveImageUp(itemElement) {
if (!isPC()) {
const gallery = document.getElementById('imageGallery');
const prevElement = itemElement.previousElementSibling;
if (prevElement) {
gallery.insertBefore(itemElement, prevElement);
updateStorageOrder(); // IndexedDB 순서 업데이트 함수 (미구현)
}
}
}
// 이미지 아래로 이동 - 기존과 동일 (순서 업데이트는 미구현, 변경 없음)
function moveImageDown(itemElement) {
if (!isPC()) {
const gallery = document.getElementById('imageGallery');
const nextElement = itemElement.nextElementSibling;
if (nextElement) {
gallery.insertBefore(nextElement, itemElement);
updateStorageOrder(); // IndexedDB 순서 업데이트 함수 (미구현)
}
}
}
// 로컬 이미지 업로드 처리 - 수정됨: IndexedDB 저장 함수 호출 (기존과 동일, 변경 없음)
function handleFileUpload(files) {
setLoading(true);
Array.from(files).forEach(file => {
if (!file.type.startsWith('image/')) {
alert('이미지 파일만 업로드할 수 있습니다.');
setLoading(false);
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const imageUrl = e.target.result;
const imageItem = createImageItem(imageUrl);
document.getElementById('imageGallery').prepend(imageItem);
saveImageToIDB(imageUrl); // IndexedDB 저장 함수 호출
setLoading(false);
};
reader.onerror = () => {
setLoading(false);
alert('파일 로딩 실패: ' + file.name);
};
reader.readAsDataURL(file);
});
if (files.length === 0) {
setLoading(false);
}
}
// IndexedDB에 이미지 저장 - 기존과 동일 (변경 없음)
function saveImageToIDB(url) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(OBJECT_STORE_NAME, 'readwrite');
const store = transaction.objectStore(OBJECT_STORE_NAME);
const request = store.add({ url: url }); // url을 key로 사용
request.onsuccess = resolve;
request.onerror = (event) => {
console.error('IndexedDB 저장 오류:', event.target.error);
reject(event.target.error);
alert('이미지 저장 중 오류가 발생했습니다. (IndexedDB)');
};
});
}
// IndexedDB에서 저장된 이미지 불러오기 - 기존과 동일 (변경 없음)
function loadSavedImagesFromIDB() {
return new Promise((resolve, reject) => {
const transaction = db.transaction(OBJECT_STORE_NAME, 'readonly');
const store = transaction.objectStore(OBJECT_STORE_NAME);
const request = store.getAll(); // 모든 데이터 가져오기
request.onsuccess = (event) => {
const savedImages = event.target.result.map(item => item.url); // url만 추출
const gallery = document.getElementById('imageGallery');
gallery.innerHTML = '';
savedImages.forEach(url => {
const imageItem = createImageItem(url);
gallery.appendChild(imageItem);
});
resolve();
};
request.onerror = (event) => {
console.error('IndexedDB 불러오기 오류:', event.target.error);
reject(event.target.error);
alert('저장된 이미지를 불러오는 중 오류가 발생했습니다.');
};
});
}
// IndexedDB에서 이미지 삭제 - 기존과 동일 (변경 없음)
function deleteImageFromIDB(url) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(OBJECT_STORE_NAME, 'readwrite');
const store = transaction.objectStore(OBJECT_STORE_NAME);
const request = store.delete(url); // url 키로 데이터 삭제
request.onsuccess = resolve;
request.onerror = (event) => {
console.error('IndexedDB 삭제 오류:', event.target.error);
reject(event.target.error);
alert('이미지 삭제 중 오류가 발생했습니다.');
};
});
}
// IndexedDB Object Store 비우기 (전체 초기화) - 기존과 동일 (변경 없음)
function clearIDBStore() {
return new Promise((resolve, reject) => {
const transaction = db.transaction(OBJECT_STORE_NAME, 'readwrite');
const store = transaction.objectStore(OBJECT_STORE_NAME);
const request = store.clear();
request.onsuccess = resolve;
request.onerror = (event) => {
console.error('IndexedDB 초기화 오류:', event.target.error);
reject(event.target.error);
alert('이미지 초기화 중 오류가 발생했습니다.');
};
});
}
// 저장된 이미지 순서 업데이트 (IndexedDB에서는 순서 관리가 더 복잡해짐 - 미구현, 변경 없음)
function updateStorageOrder() {
// IndexedDB에서 순서 업데이트 로직은 추가적인 구현이 필요합니다.
// (예: 순서 필드를 각 이미지 객체에 추가하고, IndexedDB에서 순서대로 가져오기 등)
console.warn('IndexedDB 환경에서는 이미지 순서 업데이트 기능이 아직 구현되지 않았습니다.');
}
// HTML 저장 기능 - 기존과 동일 (변경 없음)
function saveAsHTML() {
const now = new Date();
const filename = `image-gallery-${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}-${String(now.getHours()).padStart(2, '0')}-${String(now.getMinutes()).padStart(2, '0')}-${String(now.getSeconds()).padStart(2, '0')}.html`;
const htmlContent = document.documentElement.outerHTML;
const blob = new Blob([htmlContent], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// 엔터키 입력 지원 - 기존과 동일 (변경 없음)
document.getElementById('imageInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') addImage();
});
</script>
</body>
</html>3. 파일 실행 및 테스트
저장한 HTML 파일을 웹 브라우저로 열어 도구가 정상 동작하는지 확인합니다. 이미지 추가, 순서 변경, 삭제, 모달 뷰 등 모든 기능이 제대로 작동하는지 꼼꼼히 테스트해 보세요.
4. 코드 커스터마이징(선택)
- CSS 스타일 변경: <style> 태그 내의 CSS 코드를 수정하여 도구의 디자인을 원하는 대로 변경할 수 있습니다. 예를 들어, 이미지 썸네일 크기, 갤러리 간격, 버튼 색상 등을 변경할 수 있습니다.
- 기능 추가/변경: JavaScript 코드를 수정하여 기능을 추가하거나 변경할 수 있습니다. o3-mini나 Gemini를 활용하면 더욱 쉽게 코드를 수정할 수 있습니다.
실제 활용 예시 및 팁
이 도구는 실제 일러스트 작업 시 레퍼런스 이미지, 텍스처, 구도 등을 관리하는 데 큰 도움이 됩니다. 예를 들어, 작업 도중 빠르게 이미지를 추가하고 정리할 수 있어 작업 흐름이 중단되지 않고, 창작에 집중할 수 있습니다.
- 캐릭터 디자인: 캐릭터의 다양한 포즈, 의상, 표정 레퍼런스를 수집하고 관리하여 일관성 있는 디자인을 유지할 수 있습니다.
- 배경 작업: 배경에 필요한 건물, 자연물, 소품 등의 이미지를 수집하고, 배치 순서를 조정하며 구도를 잡을 수 있습니다.
- 텍스처 수집: 옷감, 금속, 나무 등 다양한 재질의 텍스처 이미지를 수집하여 그림의 디테일을 높일 수 있습니다.
- 색상 팔레트: 여러 이미지에서 마음에 드는 색상 조합을 추출하여 저장하고, 그림에 적용할 수 있습니다.
- 아이디어 스크랩: 웹 서핑 중 발견한 흥미로운 이미지나 디자인을 빠르게 저장하고, 나중에 참고할 수 있습니다.
FAQ
1. IndexedDB 용량 초과 시 해결 방법은?
IndexedDB는 브라우저별로 용량 제한이 있을 수 있습니다. 대용량 이미지를 많이 저장할 경우 브라우저의 저장 공간이 부족해질 수 있으니, 주기적으로 데이터를 정리하는 것이 좋습니다.
2. 모바일에서도 원활히 사용 가능한가요?
네, 반응형 디자인으로 모바일에서도 문제없이 사용 가능합니다. 모바일에서 이미지 순서를 변경하거나 이미지를 추가하는 등의 기능을 손쉽게 이용할 수 있습니다.
3. HTML 파일을 어떻게 저장하고 불러오나요?
생성한 HTML 파일은 로컬에 저장하고, 이후 언제든지 웹 브라우저에서 열어 도구를 사용할 수 있습니다.
마무리
AI를 활용해 만든 이 도구는 여러 레퍼런스 이미지를 효율적으로 정리하고 비교하는 데 도움을 줄 뿐만 아니라, AI를 활용해 손쉽게 커스터마이징할 수 있습니다. 또한, 오프라인에서도 이미지 자료를 관리할 수 있어 편리하게 사용할 수 있습니다.

