// zzim.js - 찜관리 모듈 class ZzimManager { constructor() { this.SUPABASE_URL = null; this.SUPABASE_ANON_KEY = null; this.access_token = null; this.user_id = null; this.zzimInProgress = false; } async init() { console.log('ZzimManager 초기화 중...'); try { // Chrome Extension 환경에서 설정값 가져오기 if (typeof chrome !== 'undefined' && chrome.storage) { const config = await chrome.storage.local.get(['zzim_config']); if (config.zzim_config) { this.access_token = config.zzim_config.ACCESS_TOKEN; this.user_id = config.zzim_config.USER_ID; this.SUPABASE_URL = config.zzim_config.SUPABASE_URL || "http://146.56.101.199:8000"; this.SUPABASE_ANON_KEY = config.zzim_config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; } else { // 기존 방식으로 폴백 const fallbackConfig = await chrome.storage.local.get(['access_token', 'user_id', 'SUPABASE_URL', 'SUPABASE_ANON_KEY']); this.access_token = fallbackConfig.access_token; this.user_id = fallbackConfig.user_id; this.SUPABASE_URL = fallbackConfig.SUPABASE_URL || "http://146.56.101.199:8000"; this.SUPABASE_ANON_KEY = fallbackConfig.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; } if (!this.access_token || !this.user_id) { throw new Error('로그인이 필요합니다.'); } console.log('Chrome Extension 환경에서 설정 로드 완료', { hasToken: !!this.access_token, hasUserId: !!this.user_id, hasUrl: !!this.SUPABASE_URL, hasKey: !!this.SUPABASE_ANON_KEY }); } else { throw new Error('Chrome Extension 환경이 아닙니다.'); } // 사용자 찜 설정 초기화 await this.initializeUserZzimSettings(); // 통계 및 마켓 정보 로드 await Promise.all([ this.loadZzimStats(), this.loadMyMarkets() ]); // 이벤트 바인딩 this.bindEvents(); console.log('ZzimManager 초기화 완료'); } catch (error) { console.error('ZzimManager 초기화 실패:', error); this.showError('초기화 실패: ' + error.message); } } async initializeUserZzimSettings() { try { const response = await fetch(`${this.SUPABASE_URL}/rest/v1/rpc/initialize_user_zzim_settings`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ user_uuid: this.user_id }) }); if (!response.ok) { console.error('찜 설정 초기화 실패:', response.status); } } catch (error) { console.error('찜 설정 초기화 오류:', error); } } bindEvents() { // 마켓 추가 버튼 const addMarketBtn = document.getElementById('add-market-btn'); if (addMarketBtn) { addMarketBtn.addEventListener('click', () => this.addMarket()); } // 내 마켓 찜하기 버튼 const myMarketZzimBtn = document.getElementById('my-market-zzim-btn'); if (myMarketZzimBtn) { myMarketZzimBtn.addEventListener('click', () => this.startMyMarketZzim()); } // 품앗이 찜하기 버튼 const mutualZzimBtn = document.getElementById('mutual-zzim-btn'); if (mutualZzimBtn) { mutualZzimBtn.addEventListener('click', () => this.startMutualZzim()); } // 마켓 URL 입력 시 자동 변환 const marketUrlInput = document.getElementById('market-url'); if (marketUrlInput) { marketUrlInput.addEventListener('input', (e) => { let url = e.target.value; if (url && !url.startsWith('https://smartstore.naver.com/')) { // URL이 smartstore로 시작하지 않으면 자동으로 앞에 추가 if (!url.startsWith('http')) { url = 'https://smartstore.naver.com/' + url; e.target.value = url; } } }); } } async loadZzimStats() { try { const response = await fetch(`${this.SUPABASE_URL}/rest/v1/zzim_settings?user_id=eq.${this.user_id}`, { headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`찜 통계 로드 실패: ${response.status}`); } const data = await response.json(); const stats = data[0] || { current_daily_count: 0, daily_zzim_limit: 100, current_mileage: 0, zzim_mileage_limit: 1000 }; // UI 업데이트 - HTML의 실제 ID와 일치하도록 수정 const todayCountEl = document.getElementById('today-zzim-count'); const todayLimitEl = document.getElementById('today-zzim-limit'); const mileageCountEl = document.getElementById('zzim-mileage'); const mileageLimitEl = document.getElementById('zzim-mileage-limit'); if (todayCountEl) todayCountEl.textContent = stats.current_daily_count; if (todayLimitEl) todayLimitEl.textContent = stats.daily_zzim_limit; if (mileageCountEl) mileageCountEl.textContent = stats.current_mileage; if (mileageLimitEl) mileageLimitEl.textContent = stats.zzim_mileage_limit; } catch (error) { console.error('찜 통계 로드 오류:', error); this.showError('찜 통계를 불러올 수 없습니다.'); } } async loadMyMarkets() { try { const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?user_id=eq.${this.user_id}&is_active=eq.true&order=created_at.desc`, { headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`마켓 목록 로드 실패: ${response.status}`); } const markets = await response.json(); this.renderMarketsList(markets); } catch (error) { console.error('마켓 목록 로드 오류:', error); this.showError('마켓 목록을 불러올 수 없습니다.'); } } renderMarketsList(markets) { const marketsList = document.getElementById('my-markets-list'); if (!marketsList) return; if (markets.length === 0) { marketsList.innerHTML = '

등록된 마켓이 없습니다.

'; return; } marketsList.innerHTML = markets.map(market => `
${market.market_name}
${market.market_nickname}
${market.market_url}
`).join(''); } async addMarket() { const marketUrl = document.getElementById('market-url').value.trim(); const marketName = document.getElementById('market-name').value.trim(); const marketNickname = document.getElementById('market-nickname').value.trim(); if (!marketUrl || !marketName || !marketNickname) { this.showError('모든 필드를 입력해주세요.'); return; } if (!marketUrl.startsWith('https://smartstore.naver.com/')) { this.showError('올바른 네이버 스마트스토어 URL을 입력해주세요.'); return; } try { const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: this.user_id, market_url: marketUrl, market_name: marketName, market_nickname: marketNickname }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || '마켓 추가 실패'); } // 입력 필드 초기화 document.getElementById('market-url').value = ''; document.getElementById('market-name').value = ''; document.getElementById('market-nickname').value = ''; // 마켓 목록 다시 로드 await this.loadMyMarkets(); this.showSuccess('마켓이 추가되었습니다.'); } catch (error) { console.error('마켓 추가 오류:', error); this.showError('마켓 추가 중 오류가 발생했습니다: ' + error.message); } } async editMarket(marketId) { // 편집 모달을 표시하거나 인라인 편집 구현 const newName = prompt('새로운 마켓 이름을 입력하세요:'); if (!newName) return; const newNickname = prompt('새로운 마켓 별명을 입력하세요:'); if (!newNickname) return; try { const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?id=eq.${marketId}`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ market_name: newName, market_nickname: newNickname }) }); if (!response.ok) { throw new Error('마켓 수정 실패'); } await this.loadMyMarkets(); this.showSuccess('마켓이 수정되었습니다.'); } catch (error) { console.error('마켓 수정 오류:', error); this.showError('마켓 수정 중 오류가 발생했습니다.'); } } async deleteMarket(marketId) { if (!confirm('정말로 이 마켓을 삭제하시겠습니까?')) { return; } try { const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?id=eq.${marketId}`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ is_active: false }) }); if (!response.ok) { throw new Error('마켓 삭제 실패'); } await this.loadMyMarkets(); this.showSuccess('마켓이 삭제되었습니다.'); } catch (error) { console.error('마켓 삭제 오류:', error); this.showError('마켓 삭제 중 오류가 발생했습니다.'); } } async startMyMarketZzim() { if (this.zzimInProgress) { this.showError('찜하기가 이미 진행 중입니다.'); return; } try { // 내 마켓 목록 가져오기 const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?user_id=eq.${this.user_id}&is_active=eq.true`, { headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error('마켓 목록을 가져올 수 없습니다.'); } const markets = await response.json(); if (markets.length === 0) { this.showError('등록된 마켓이 없습니다.'); return; } // 랜덤 마켓 선택 const randomMarket = markets[Math.floor(Math.random() * markets.length)]; this.showStatus(`"${randomMarket.market_nickname}" 마켓에서 찜하기를 시작합니다...`); await this.executeZzim(randomMarket, 'my_market', false); // 포그라운드에서 실행 } catch (error) { console.error('내 마켓 찜하기 오류:', error); this.showError('내 마켓 찜하기 중 오류가 발생했습니다.'); } } async startMutualZzim() { if (this.zzimInProgress) { this.showError('찜하기가 이미 진행 중입니다.'); return; } try { // 다른 사용자의 마켓 목록 가져오기 (품앗이용) const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?user_id=neq.${this.user_id}&is_active=eq.true`, { headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error('품앗이 마켓 목록을 가져올 수 없습니다.'); } const markets = await response.json(); if (markets.length === 0) { this.showError('품앗이할 마켓이 없습니다.'); return; } // 랜덤 마켓 선택 const randomMarket = markets[Math.floor(Math.random() * markets.length)]; this.showStatus(`품앗이 마켓 "${randomMarket.market_nickname}"에서 찜하기를 시작합니다...`); await this.executeZzim(randomMarket, 'mutual', true); // 백그라운드에서 실행 } catch (error) { console.error('품앗이 찜하기 오류:', error); this.showError('품앗이 찜하기 중 오류가 발생했습니다.'); } } async executeZzim(market, zzimType, inBackground = false) { this.zzimInProgress = true; try { // 찜하기 스크립트 const zzimScript = ` (function() { let zzimCount = 0; const maxZzim = 50; // 최대 찜할 개수 function clickZzimButtons() { const zzimButtons = document.querySelectorAll('.zzim_button:not(.active)'); console.log('찾은 찜 버튼 개수:', zzimButtons.length); if (zzimButtons.length === 0) { console.log('더 이상 찜할 상품이 없습니다.'); return false; } // 최대 10개씩 찜하기 const buttonsToClick = Array.from(zzimButtons).slice(0, Math.min(10, maxZzim - zzimCount)); buttonsToClick.forEach((btn, index) => { setTimeout(() => { try { btn.click(); zzimCount++; console.log(\`찜 버튼 클릭: \${zzimCount}개\`); } catch (e) { console.error('찜 버튼 클릭 오류:', e); } }, index * 500); // 0.5초 간격 }); return buttonsToClick.length > 0 && zzimCount < maxZzim; } // 페이지 스크롤 및 찜하기 반복 function scrollAndZzim() { if (zzimCount >= maxZzim) { console.log('최대 찜하기 완료:', zzimCount); return; } // 페이지 하단으로 스크롤 window.scrollTo(0, document.body.scrollHeight); setTimeout(() => { if (clickZzimButtons()) { setTimeout(scrollAndZzim, 3000); // 3초 후 다시 시도 } }, 1000); } // 시작 scrollAndZzim(); return zzimCount; })(); `; if (inBackground) { // 백그라운드에서 실행 (새 탭에서 백그라운드로) const tab = await chrome.tabs.create({ url: market.market_url, active: false }); // 페이지 로드 완료 후 스크립트 실행 chrome.tabs.onUpdated.addListener(function listener(tabId, info) { if (tabId === tab.id && info.status === 'complete') { chrome.tabs.onUpdated.removeListener(listener); chrome.scripting.executeScript({ target: { tabId: tab.id }, func: eval(`(${zzimScript})`) }).then(() => { // 일정 시간 후 탭 닫기 setTimeout(() => { chrome.tabs.remove(tab.id); }, 30000); // 30초 후 }); } }); } else { // 포그라운드에서 실행 (현재 창에서) window.open(market.market_url, '_blank'); } // 찜 기록 저장 await this.recordZzim(market, zzimType); this.showSuccess(`찜하기가 시작되었습니다. (${market.market_nickname})`); } catch (error) { console.error('찜하기 실행 오류:', error); this.showError('찜하기 실행 중 오류가 발생했습니다.'); } finally { this.zzimInProgress = false; } } async recordZzim(market, zzimType) { try { // 찜 기록 저장 await fetch(`${this.SUPABASE_URL}/rest/v1/jjim`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: this.user_id, market_url: market.market_url, market_name: market.market_name, market_nickname: market.market_nickname, zzim_type: zzimType, product_count: 50 // 예상 찜 개수 (실제로는 스크립트 결과를 받아야 함) }) }); // 통계 업데이트 await this.updateZzimStats(50, zzimType); // UI 새로고침 await this.loadZzimStats(); } catch (error) { console.error('찜 기록 저장 오류:', error); } } async updateZzimStats(count, zzimType) { try { const mileageIncrease = zzimType === 'mutual' ? count * 2 : count; // 품앗이는 마일리지 2배 await fetch(`${this.SUPABASE_URL}/rest/v1/zzim_settings?user_id=eq.${this.user_id}`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${this.access_token}`, 'apikey': this.SUPABASE_ANON_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ current_daily_count: `current_daily_count + ${count}`, current_mileage: `current_mileage + ${mileageIncrease}` }) }); } catch (error) { console.error('찜 통계 업데이트 오류:', error); } } showStatus(message) { const statusEl = document.getElementById('zzim-status'); const errorEl = document.getElementById('zzim-error'); const successEl = document.getElementById('zzim-success'); // 모든 상태 메시지 숨기기 if (statusEl) statusEl.style.display = 'none'; if (errorEl) errorEl.style.display = 'none'; if (successEl) successEl.style.display = 'none'; // 상태 메시지 표시 if (statusEl) { statusEl.textContent = message; statusEl.style.display = 'block'; } } showError(message) { const statusEl = document.getElementById('zzim-status'); const errorEl = document.getElementById('zzim-error'); const successEl = document.getElementById('zzim-success'); // 모든 상태 메시지 숨기기 if (statusEl) statusEl.style.display = 'none'; if (errorEl) errorEl.style.display = 'none'; if (successEl) successEl.style.display = 'none'; // 오류 메시지 표시 if (errorEl) { errorEl.textContent = message; errorEl.style.display = 'block'; } } showSuccess(message) { const statusEl = document.getElementById('zzim-status'); const errorEl = document.getElementById('zzim-error'); const successEl = document.getElementById('zzim-success'); // 모든 상태 메시지 숨기기 if (statusEl) statusEl.style.display = 'none'; if (errorEl) errorEl.style.display = 'none'; if (successEl) successEl.style.display = 'none'; // 성공 메시지 표시 if (successEl) { successEl.textContent = message; successEl.style.display = 'block'; } } } // 전역 인스턴스 생성 let zzimManager; // DOM 로드 완료 후 초기화 document.addEventListener('DOMContentLoaded', async () => { zzimManager = new ZzimManager(); await zzimManager.init(); });