// 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 => `
`).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();
});