feat: 이미지 교체 기능 추가

- URL 입력으로 이미지 수동 교체
- 다양한 User-Agent로 다운로드 시도 (차단 우회)
- base64 변환 + 썸네일 자동 생성
- status를 'manual'로 표시
This commit is contained in:
thug0bin
2026-03-02 23:42:01 +09:00
parent ee28f97c11
commit 4a06e60e29
2 changed files with 194 additions and 0 deletions

View File

@@ -5782,6 +5782,115 @@ def api_delete_product_image(barcode):
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/admin/product-images/<barcode>/replace', methods=['POST'])
def api_replace_product_image(barcode):
"""이미지 URL로 교체"""
import sqlite3
import requests
import base64
from PIL import Image
from io import BytesIO
try:
data = request.get_json()
image_url = data.get('image_url', '').strip()
if not image_url:
return jsonify({'success': False, 'error': 'URL 필요'}), 400
# 다양한 User-Agent와 헤더로 시도
headers_list = [
{
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
'Referer': 'https://www.google.com/',
},
{
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1',
'Accept': 'image/*,*/*;q=0.8',
},
{
'User-Agent': 'Googlebot-Image/1.0',
}
]
response = None
for headers in headers_list:
try:
response = requests.get(image_url, headers=headers, timeout=15, allow_redirects=True)
if response.status_code == 200 and len(response.content) > 1000:
break
except:
continue
if not response or response.status_code != 200:
return jsonify({'success': False, 'error': f'이미지 다운로드 실패 (상태: {response.status_code if response else "연결실패"})'}), 400
# PIL로 이미지 처리
try:
img = Image.open(BytesIO(response.content))
# RGBA -> RGB 변환
if img.mode == 'RGBA':
bg = Image.new('RGB', img.size, (255, 255, 255))
bg.paste(img, mask=img.split()[3])
img = bg
elif img.mode != 'RGB':
img = img.convert('RGB')
# 리사이즈 (최대 500px)
max_size = 500
if max(img.size) > max_size:
ratio = max_size / max(img.size)
new_size = tuple(int(dim * ratio) for dim in img.size)
img = img.resize(new_size, Image.LANCZOS)
# base64 변환
buffer = BytesIO()
img.save(buffer, format='JPEG', quality=85)
image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
# 썸네일 생성
thumb_size = 100
ratio = thumb_size / max(img.size)
thumb_img = img.resize(tuple(int(dim * ratio) for dim in img.size), Image.LANCZOS)
thumb_buffer = BytesIO()
thumb_img.save(thumb_buffer, format='JPEG', quality=80)
thumbnail_base64 = base64.b64encode(thumb_buffer.getvalue()).decode('utf-8')
except Exception as e:
return jsonify({'success': False, 'error': f'이미지 처리 실패: {str(e)}'}), 400
# SQLite 업데이트
db_path = os.path.join(os.path.dirname(__file__), 'db', 'product_images.db')
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("""
UPDATE product_images
SET image_base64 = ?, thumbnail_base64 = ?, image_url = ?,
status = 'manual', error_message = NULL, updated_at = datetime('now')
WHERE barcode = ?
""", (image_base64, thumbnail_base64, image_url, barcode))
if cursor.rowcount == 0:
# 레코드가 없으면 새로 생성
cursor.execute("""
INSERT INTO product_images (barcode, image_base64, thumbnail_base64, image_url, status, product_name)
VALUES (?, ?, ?, ?, 'manual', ?)
""", (barcode, image_base64, thumbnail_base64, image_url, barcode))
conn.commit()
conn.close()
return jsonify({'success': True, 'message': '이미지 교체 완료'})
except Exception as e:
logging.error(f"이미지 교체 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/admin/product-images/stats')
def api_product_images_stats():
"""이미지 통계"""