SearchTrademark/zzim.js

2015 lines
70 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
this.currentEditIndex = null;
}
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 || "https://ko.wrmc.cc";
this.SUPABASE_ANON_KEY = config.zzim_config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
} 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 || "https://ko.wrmc.cc";
this.SUPABASE_ANON_KEY = fallbackConfig.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
}
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();
// 버튼 상태 초기화
this.updateButtonStates();
// 총 찜하기 간격 초기화
this.updateTotalDelay();
console.log('ZzimManager 초기화 완료');
} catch (error) {
console.error('ZzimManager 초기화 실패:', error);
this.showError('초기화 실패: ' + error.message);
}
}
async initializeUserZzimSettings() {
try {
console.log('사용자 찜 설정 초기화 시작:', {
user_id: this.user_id,
supabase_url: this.SUPABASE_URL
});
// user_markets 테이블에 사용자 레코드가 없으면 생성
const userMarketsResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}`, {
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
console.log('user_markets 조회 응답:', {
status: userMarketsResponse.status,
ok: userMarketsResponse.ok
});
if (userMarketsResponse.ok) {
const userMarkets = await userMarketsResponse.json();
console.log('기존 user_markets 데이터:', userMarkets);
if (userMarkets.length === 0) {
console.log('user_markets 초기 레코드 생성 중...');
// user_markets 테이블에 초기 레코드 생성
const createResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
'Prefer': 'return=representation'
},
body: JSON.stringify({
user_id: this.user_id,
my_markets: []
})
});
console.log('user_markets 생성 응답:', {
status: createResponse.status,
ok: createResponse.ok
});
if (!createResponse.ok) {
const errorText = await createResponse.text();
console.error('user_markets 생성 실패:', errorText);
// 409 Conflict (이미 존재)는 무시하고 계속 진행
if (createResponse.status !== 409) {
throw new Error('user_markets 레코드 생성 실패: ' + errorText);
} else {
console.log('user_markets 레코드가 이미 존재함 (409 Conflict 무시)');
}
} else {
const createResult = await createResponse.json();
console.log('user_markets 초기 레코드 생성 완료:', createResult);
}
} else {
console.log('기존 user_markets 레코드 존재함');
}
} else {
console.error('user_markets 조회 실패:', userMarketsResponse.status);
const errorText = await userMarketsResponse.text();
console.error('user_markets 조회 오류 상세:', errorText);
// 404 Not Found인 경우 테이블이 없을 수 있으므로 계속 진행
if (userMarketsResponse.status !== 404) {
throw new Error('user_markets 조회 실패: ' + errorText);
}
}
console.log('users 테이블 필드 초기화 중...');
// users 테이블의 my_zzim, zzim_mile 필드가 null이면 초기화
const usersUpdateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
'Prefer': 'return=representation'
},
body: JSON.stringify({
my_zzim: 0, // COALESCE 대신 직접 0으로 설정
zzim_mile: 0
})
});
console.log('users 테이블 업데이트 응답:', {
status: usersUpdateResponse.status,
ok: usersUpdateResponse.ok
});
if (!usersUpdateResponse.ok) {
const errorText = await usersUpdateResponse.text();
console.error('users 테이블 업데이트 실패:', errorText);
// users 테이블 업데이트 실패는 치명적이지 않으므로 경고만 출력
console.warn('users 테이블 업데이트 실패, 계속 진행');
} else {
const usersResult = await usersUpdateResponse.json();
console.log('users 테이블 필드 초기화 완료:', usersResult);
}
console.log('사용자 찜 설정 초기화 완료');
} catch (error) {
console.error('찜 설정 초기화 오류:', error);
this.showError('찜 설정 초기화 중 오류가 발생했습니다: ' + error.message);
}
}
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());
}
// 찜하기 중단 버튼
const stopZzimBtn = document.getElementById('stop-zzim-btn');
if (stopZzimBtn) {
stopZzimBtn.addEventListener('click', () => this.stopZzim());
}
// 모달 취소 버튼
const modalCancelBtn = document.getElementById('modal-cancel-btn');
if (modalCancelBtn) {
modalCancelBtn.addEventListener('click', () => this.closeEditModal());
}
// 모달 저장 버튼
const modalSaveBtn = document.getElementById('modal-save-btn');
if (modalSaveBtn) {
modalSaveBtn.addEventListener('click', () => this.saveMarketEdit());
}
// 모달 닫기 버튼 (×)
const modalCloseBtn = document.getElementById('modal-close-btn');
if (modalCloseBtn) {
modalCloseBtn.addEventListener('click', () => this.closeEditModal());
}
// 마켓 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;
}
}
});
}
// 찜하기 속도 설정 이벤트
const userDelayInput = document.getElementById('user-delay');
if (userDelayInput) {
userDelayInput.addEventListener('input', () => this.updateTotalDelay());
}
// 디버깅용 테스트 버튼 (있는 경우에만)
const testBtn = document.getElementById('test-debug-btn');
if (testBtn) {
testBtn.addEventListener('click', () => this.testDebug());
}
}
// 총 찜하기 간격 업데이트
updateTotalDelay() {
const baseDelay = parseInt(document.getElementById('base-delay')?.value || 500);
const userDelay = parseInt(document.getElementById('user-delay')?.value || 0);
const totalDelay = baseDelay + userDelay;
const totalDelayEl = document.getElementById('total-delay');
if (totalDelayEl) {
totalDelayEl.textContent = `${totalDelay}ms`;
}
}
// 찜하기 설정 가져오기
getZzimSettings() {
const baseDelay = parseInt(document.getElementById('base-delay')?.value || 500);
const userDelay = parseInt(document.getElementById('user-delay')?.value || 0);
const latestFirst = document.getElementById('latest-first')?.checked || false;
const backgroundMode = document.getElementById('background-mode')?.checked || false;
return {
totalDelay: baseDelay + userDelay,
latestFirst: latestFirst,
backgroundMode: backgroundMode
};
}
// 마켓 URL 생성 (최신상품 우선 옵션 고려)
generateMarketUrl(marketUrl, latestFirst = false) {
try {
// 기본 마켓 URL에서 마지막 슬래시 제거
const baseUrl = marketUrl.replace(/\/$/, '');
console.log('[ZzimManager] 마켓 URL 생성:', {
baseUrl: baseUrl,
latestFirst: latestFirst
});
// 네이버 스마트스토어 URL 구조 분석
const urlParts = baseUrl.split('/');
const storeName = urlParts[urlParts.length - 1];
let targetUrl;
if (latestFirst) {
// 최신상품 우선: /best?cp=1
targetUrl = `${baseUrl}/best?cp=1`;
} else {
// 전체상품: /category/ALL?cp=1
targetUrl = `${baseUrl}/category/ALL?cp=1`;
}
console.log('[ZzimManager] 생성된 마켓 URL:', targetUrl);
return targetUrl;
} catch (error) {
console.error('[ZzimManager] 마켓 URL 생성 오류:', error);
// 오류 발생 시 기본 URL 반환
return marketUrl + (latestFirst ? '/best?cp=1' : '/category/ALL?cp=1');
}
}
async loadZzimStats() {
try {
console.log('찜 통계 로드 시작 (Background 요청)...');
const response = await chrome.runtime.sendMessage({
action: 'getZzimStats',
userId: this.user_id,
token: this.access_token
});
if (!response || !response.success) {
throw new Error(response?.error || '통계 정보를 불러올 수 없습니다.');
}
const { stats: userStats, limits } = response;
// 3. 오늘 찜한 개수
let todayZzimCount = userStats.today_zzim_count || 0;
// 4. 사용 가능한 찜 마일리지
const totalZzimMile = userStats.zzim_mile || 0;
const availableZzimMile = userStats.available_zzim_mile || totalZzimMile;
// 5. UI 업데이트
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');
const totalZzimEl = document.getElementById('total-zzim-received');
if (todayCountEl) todayCountEl.textContent = todayZzimCount;
if (todayLimitEl) todayLimitEl.textContent = limits.daily_zzim_limit;
if (mileageCountEl) mileageCountEl.textContent = availableZzimMile;
if (mileageLimitEl) mileageLimitEl.textContent = limits.max_zzim_mileage;
if (totalZzimEl) totalZzimEl.textContent = userStats.my_zzim || 0;
// 6. 진행률 표시
const todayProgress = (todayZzimCount / limits.daily_zzim_limit) * 100;
const mileageProgress = (availableZzimMile / limits.max_zzim_mileage) * 100;
const todayProgressBar = document.getElementById('today-progress-bar');
const mileageProgressBar = document.getElementById('mileage-progress-bar');
if (todayProgressBar) {
todayProgressBar.style.width = `${Math.min(todayProgress, 100)}%`;
todayProgressBar.className = `progress-bar ${todayProgress >= 100 ? 'full' : todayProgress >= 80 ? 'warning' : 'normal'}`;
}
if (mileageProgressBar) {
mileageProgressBar.style.width = `${Math.min(mileageProgress, 100)}%`;
mileageProgressBar.className = `progress-bar ${mileageProgress >= 100 ? 'full' : mileageProgress >= 80 ? 'warning' : 'normal'}`;
}
// 7. 제한 상태 표시
const todayLimitReached = todayZzimCount >= limits.daily_zzim_limit;
const mileageLimitReached = availableZzimMile >= limits.max_zzim_mileage;
const myMarketBtn = document.getElementById('my-market-zzim-btn');
const mutualZzimBtn = document.getElementById('mutual-zzim-btn');
const backgroundModeCheckbox = document.getElementById('background-mode');
if (myMarketBtn) {
myMarketBtn.disabled = todayLimitReached;
myMarketBtn.title = todayLimitReached ? `일일 찜 제한 도달 (${todayZzimCount}/${limits.daily_zzim_limit})` : '';
}
if (mutualZzimBtn) {
mutualZzimBtn.disabled = !limits.mutual_zzim_enabled || mileageLimitReached;
mutualZzimBtn.title = !limits.mutual_zzim_enabled
? '품앗이 기능이 비활성화되어 있습니다.'
: mileageLimitReached
? `마일리지 한도 도달 (${availableZzimMile}/${limits.max_zzim_mileage})`
: '';
}
if (backgroundModeCheckbox) {
backgroundModeCheckbox.disabled = !limits.background_zzim_enabled;
if (!limits.background_zzim_enabled) {
backgroundModeCheckbox.checked = false;
backgroundModeCheckbox.title = '백그라운드 모드는 프리미엄/VIP 회원만 사용 가능합니다.';
}
}
// 8. 상태 메시지 표시
const statusMessage = document.getElementById('zzim-status-message');
if (statusMessage) {
if (todayLimitReached) {
statusMessage.innerHTML = `⚠️ 오늘 찜 제한에 도달했습니다. (${todayZzimCount}/${limits.daily_zzim_limit})`;
statusMessage.className = 'status-warning';
} else if (mileageLimitReached) {
statusMessage.innerHTML = `⚠️ 찜 마일리지 한도에 도달했습니다. (${availableZzimMile}/${limits.max_zzim_mileage})`;
statusMessage.className = 'status-warning';
} else {
statusMessage.innerHTML = `✅ 찜하기 가능 (오늘: ${todayZzimCount}/${limits.daily_zzim_limit}, 마일리지: ${availableZzimMile}/${limits.max_zzim_mileage})`;
statusMessage.className = 'status-success';
}
}
// 현재 제한 정보를 인스턴스 변수에 저장
this.currentLimits = limits;
this.currentStats = {
todayZzimCount,
availableZzimMile,
totalReceived: userStats.my_zzim || 0
};
} catch (error) {
console.error('찜 통계 로드 오류:', error);
this.showError('찜 통계를 불러올 수 없습니다: ' + error.message);
}
}
async loadMyMarkets() {
try {
console.log('마켓 목록 로드 시작 (Background 요청)...');
const response = await chrome.runtime.sendMessage({
action: 'getMyMarkets',
userId: this.user_id,
token: this.access_token
});
if (!response || !response.success) {
throw new Error(response?.error || '마켓 목록을 불러올 수 없습니다.');
}
const markets = response.markets || [];
// 생성일 기준으로 최신순 정렬
const sortedMarkets = markets.sort((a, b) => {
const dateA = new Date(a.created_at || 0);
const dateB = new Date(b.created_at || 0);
return dateB - dateA;
});
this.renderMarketsList(sortedMarkets);
} catch (error) {
console.error('마켓 목록 로드 오류:', error);
this.showError('마켓 목록을 불러올 수 없습니다: ' + error.message);
}
}
renderMarketsList(markets) {
console.log('마켓 목록 렌더링 시작:', markets);
const marketsList = document.getElementById('my-markets-list');
if (!marketsList) {
console.error('my-markets-list 엘리먼트를 찾을 수 없습니다.');
return;
}
if (markets.length === 0) {
console.log('등록된 마켓이 없음');
marketsList.innerHTML = '<p style="text-align: center; color: #666; padding: 20px;">등록된 마켓이 없습니다.</p>';
return;
}
console.log('마켓 목록 HTML 생성 중...');
const marketHTML = markets.map((market, index) => {
console.log(`마켓 ${index} 렌더링:`, market);
// 고유 식별자 생성 (마켓 URL + 생성일시)
const marketId = `${market.market_url}_${market.created_at}`;
return `
<div class="market-item" data-index="${index}" data-market-id="${marketId}">
<div class="market-header">
<div class="market-checkboxes">
<div class="market-checkbox">
<input type="checkbox" id="market-check-${index}" class="market-checkbox-input"
${market.for_zzim !== false ? 'checked' : ''}
data-market-id="${marketId}" data-checkbox-type="for_zzim">
<label for="market-check-${index}" class="market-checkbox-label">내 찜하기</label>
</div>
<div class="market-checkbox">
<input type="checkbox" id="market-mutual-${index}" class="market-checkbox-input"
${market.for_mutual_zzim !== false ? 'checked' : ''}
data-market-id="${marketId}" data-checkbox-type="for_mutual_zzim">
<label for="market-mutual-${index}" class="market-checkbox-label">품앗이 대상</label>
</div>
</div>
<div class="market-info">
<div class="market-name">${market.market_name || '마켓명 없음'}</div>
<div class="market-nickname">${market.market_nickname || '별명 없음'}</div>
<div class="market-url" title="${market.market_url || ''}">${this.truncateUrl(market.market_url || '')}</div>
<div class="market-stats">
<span class="zzim-count">찜받은수: ${market.zzim_received_count || 0}개</span>
<span class="created-date">등록일: ${this.formatDate(market.created_at)}</span>
</div>
</div>
<div class="market-actions">
<button class="btn btn-warning market-edit-btn" data-market-id="${marketId}">수정</button>
<button class="btn btn-danger market-delete-btn" data-market-id="${marketId}">삭제</button>
</div>
</div>
</div>
`;
}).join('');
marketsList.innerHTML = marketHTML;
// 이벤트 리스너 바인딩
this.bindMarketEvents();
console.log('마켓 목록 렌더링 완료');
}
// 마켓 관련 이벤트 바인딩
bindMarketEvents() {
// 체크박스 이벤트
const checkboxes = document.querySelectorAll('.market-checkbox-input');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const marketId = e.target.dataset.marketId;
const checkboxType = e.target.dataset.checkboxType;
this.toggleMarketCheckbox(marketId, checkboxType, e.target.checked);
});
});
// 수정 버튼 이벤트
const editBtns = document.querySelectorAll('.market-edit-btn');
editBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
const marketId = e.target.dataset.marketId;
this.editMarket(marketId);
});
});
// 노출/숨김 버튼 이벤트 (삭제됨)
/*
const visibilityBtns = document.querySelectorAll('.market-visibility-btn');
visibilityBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
const marketId = e.target.dataset.marketId;
this.toggleMarketVisibility(marketId);
});
});
*/
// 삭제 버튼 이벤트
const deleteBtns = document.querySelectorAll('.market-delete-btn');
deleteBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
const marketId = e.target.dataset.marketId;
this.deleteMarket(marketId);
});
});
}
// URL을 적절한 길이로 자르기
truncateUrl(url) {
if (!url) return '';
if (url.length <= 50) return url;
return url.substring(0, 47) + '...';
}
// 날짜 포맷팅
formatDate(dateString) {
if (!dateString) return '알 수 없음';
try {
const date = new Date(dateString);
return date.toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
} catch (error) {
return '알 수 없음';
}
}
// 마켓 체크박스 토글 (통합)
async toggleMarketCheckbox(marketId, checkboxType, isChecked) {
try {
// 기존 마켓 목록 가져오기
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, {
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('마켓 목록을 가져올 수 없습니다.');
}
const data = await response.json();
const markets = data[0]?.my_markets || [];
// 마켓 ID로 해당 마켓 찾기
const market = markets.find(market => `${market.market_url}_${market.created_at}` === marketId);
if (!market) {
this.showError('잘못된 마켓 ID입니다.');
return;
}
// 체크박스 타입에 따라 해당 필드 업데이트
if (checkboxType === 'for_zzim') {
market.for_zzim = isChecked;
} else if (checkboxType === 'for_mutual_zzim') {
market.for_mutual_zzim = isChecked;
// 품앗이 대상 체크 시 '노출' 상태도 자동으로 동기화 (사용자 편의성)
market.is_visible = isChecked;
}
market.updated_at = new Date().toISOString();
// user_markets 테이블 업데이트
const updateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?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({
my_markets: markets
})
});
if (!updateResponse.ok) {
throw new Error('마켓 설정 변경 실패');
}
const statusMessage = checkboxType === 'for_zzim'
? (isChecked ? '내 찜하기 대상에 포함' : '내 찜하기 대상에서 제외')
: (isChecked ? '품앗이 대상에 포함' : '품앗이 대상에서 제외');
this.showSuccess(`마켓이 ${statusMessage}되었습니다.`);
} catch (error) {
console.error('마켓 설정 변경 오류:', error);
this.showError('마켓 설정 변경 중 오류가 발생했습니다.');
// 오류 발생 시 체크박스 상태 원복
const checkbox = document.querySelector(`[data-market-id="${marketId}"][data-checkbox-type="${checkboxType}"]`);
if (checkbox) {
checkbox.checked = !isChecked;
}
}
}
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();
console.log('마켓 추가 시작:', {
marketUrl,
marketName,
marketNickname
});
if (!marketUrl || !marketName || !marketNickname) {
this.showError('모든 필드를 입력해주세요.');
return;
}
if (!marketUrl.startsWith('https://smartstore.naver.com/')) {
this.showError('올바른 네이버 스마트스토어 URL을 입력해주세요.');
return;
}
try {
console.log('기존 마켓 목록 조회 중...');
// 기존 마켓 목록 가져오기
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, {
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
console.log('기존 마켓 목록 조회 응답:', {
status: response.status,
ok: response.ok
});
if (!response.ok) {
const errorText = await response.text();
console.error('기존 마켓 목록 조회 실패:', errorText);
throw new Error('기존 마켓 목록을 가져올 수 없습니다: ' + errorText);
}
const data = await response.json();
console.log('기존 마켓 데이터:', data);
let existingMarkets = [];
// 데이터가 없는 경우 처리
if (!data || data.length === 0) {
console.log('기존 user_markets 레코드가 없음, 새로 생성 필요');
// user_markets 레코드가 없으면 먼저 생성
const createResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_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,
my_markets: []
})
});
console.log('user_markets 레코드 생성 응답:', {
status: createResponse.status,
ok: createResponse.ok
});
if (!createResponse.ok) {
const createErrorText = await createResponse.text();
console.error('user_markets 레코드 생성 실패:', createErrorText);
throw new Error('사용자 마켓 레코드 생성 실패: ' + createErrorText);
}
existingMarkets = [];
} else {
existingMarkets = data[0]?.my_markets || [];
}
console.log('기존 마켓 목록:', existingMarkets);
// 새 마켓 객체 생성
const newMarket = {
market_url: marketUrl,
market_name: marketName,
market_nickname: marketNickname,
is_visible: true, // 다른 사람에게 노출 (기본값: 노출)
for_zzim: true, // 내가 찜하기 할 때 포함 (기본값: 포함)
for_mutual_zzim: true, // 품앗이 대상 여부 (기본값: 포함)
zzim_received_count: 0,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
console.log('새 마켓 객체:', newMarket);
// 중복 URL 확인
const isDuplicate = existingMarkets.some(market => market.market_url === marketUrl);
if (isDuplicate) {
console.log('중복 URL 발견');
this.showError('이미 등록된 마켓 URL입니다.');
return;
}
// 새 마켓 추가
const updatedMarkets = [...existingMarkets, newMarket];
console.log('업데이트된 마켓 목록:', updatedMarkets);
// user_markets 테이블 업데이트
console.log('마켓 목록 업데이트 요청 중...');
const updateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
'Prefer': 'return=representation'
},
body: JSON.stringify({
my_markets: updatedMarkets
})
});
console.log('마켓 목록 업데이트 응답:', {
status: updateResponse.status,
ok: updateResponse.ok,
statusText: updateResponse.statusText
});
if (!updateResponse.ok) {
const errorData = await updateResponse.text();
console.error('마켓 추가 실패 상세:', errorData);
throw new Error('마켓 추가 실패: ' + errorData);
}
// 응답 데이터 확인
const updateResult = await updateResponse.json();
console.log('업데이트 결과:', updateResult);
console.log('마켓 추가 성공, 입력 필드 초기화 중...');
// 입력 필드 초기화
document.getElementById('market-url').value = '';
document.getElementById('market-name').value = '';
document.getElementById('market-nickname').value = '';
console.log('마켓 목록 다시 로드 중...');
// 약간의 지연 후 마켓 목록 다시 로드 (DB 반영 시간 고려)
setTimeout(async () => {
await this.loadMyMarkets();
this.showSuccess('마켓이 추가되었습니다.');
// 새로 추가된 마켓을 위로 스크롤
setTimeout(() => {
const marketsList = document.getElementById('my-markets-list');
if (marketsList) {
marketsList.scrollTop = 0;
}
}, 100);
}, 500);
} catch (error) {
console.error('마켓 추가 오류:', error);
this.showError('마켓 추가 중 오류가 발생했습니다: ' + error.message);
}
}
async editMarket(marketId) {
try {
// 기존 마켓 목록 가져오기
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, {
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('마켓 목록을 가져올 수 없습니다.');
}
const data = await response.json();
const markets = data[0]?.my_markets || [];
// 마켓 ID로 해당 마켓 찾기
const market = markets.find(market => `${market.market_url}_${market.created_at}` === marketId);
if (!market) {
this.showError('잘못된 마켓 ID입니다.');
return;
}
// 모달에 기존 데이터 채우기
this.currentEditIndex = markets.indexOf(market);
this.openEditModal(market);
} catch (error) {
console.error('마켓 수정 오류:', error);
this.showError('마켓 정보를 불러올 수 없습니다.');
}
}
// 모달 열기
openEditModal(market) {
const modal = document.getElementById('edit-market-modal');
const urlInput = document.getElementById('edit-market-url');
const nameInput = document.getElementById('edit-market-name');
const nicknameInput = document.getElementById('edit-market-nickname');
const forZzimCheckbox = document.getElementById('edit-market-for-zzim');
const forMutualZzimCheckbox = document.getElementById('edit-market-for-mutual-zzim');
// 기존 데이터로 폼 채우기
if (urlInput) urlInput.value = market.market_url || '';
if (nameInput) nameInput.value = market.market_name || '';
if (nicknameInput) nicknameInput.value = market.market_nickname || '';
if (forZzimCheckbox) forZzimCheckbox.checked = market.for_zzim !== false;
if (forMutualZzimCheckbox) forMutualZzimCheckbox.checked = market.for_mutual_zzim !== false;
// 모달 표시
if (modal) {
modal.style.display = 'block';
// 첫 번째 입력 필드에 포커스
setTimeout(() => {
if (nameInput) nameInput.focus();
}, 100);
}
// ESC 키로 모달 닫기
this.modalEscapeHandler = (e) => {
if (e.key === 'Escape') {
this.closeEditModal();
}
};
document.addEventListener('keydown', this.modalEscapeHandler);
// 모달 배경 클릭으로 닫기
this.modalClickHandler = (e) => {
if (e.target === modal) {
this.closeEditModal();
}
};
modal.addEventListener('click', this.modalClickHandler);
}
// 모달 닫기
closeEditModal() {
const modal = document.getElementById('edit-market-modal');
if (modal) {
modal.style.display = 'none';
}
// 이벤트 리스너 제거
if (this.modalEscapeHandler) {
document.removeEventListener('keydown', this.modalEscapeHandler);
this.modalEscapeHandler = null;
}
if (this.modalClickHandler) {
modal.removeEventListener('click', this.modalClickHandler);
this.modalClickHandler = null;
}
// 편집 인덱스 초기화
this.currentEditIndex = null;
}
// 마켓 수정 저장
async saveMarketEdit() {
const urlInput = document.getElementById('edit-market-url');
const nameInput = document.getElementById('edit-market-name');
const nicknameInput = document.getElementById('edit-market-nickname');
const forZzimCheckbox = document.getElementById('edit-market-for-zzim');
const forMutualZzimCheckbox = document.getElementById('edit-market-for-mutual-zzim');
const saveBtn = document.querySelector('.btn-modal-save');
const marketUrl = urlInput?.value.trim() || '';
const marketName = nameInput?.value.trim() || '';
const marketNickname = nicknameInput?.value.trim() || '';
const forZzim = forZzimCheckbox?.checked || false;
const forMutualZzim = forMutualZzimCheckbox?.checked || false;
// 품앗이 대상이 체크되면 자동으로 노출도 true로 설정
const isVisible = forMutualZzim;
// 유효성 검사
if (!marketUrl || !marketName || !marketNickname) {
this.showError('모든 필드를 입력해주세요.');
return;
}
if (!marketUrl.startsWith('https://smartstore.naver.com/')) {
this.showError('올바른 네이버 스마트스토어 URL을 입력해주세요.');
return;
}
if (this.currentEditIndex === null) {
this.showError('편집할 마켓 정보를 찾을 수 없습니다.');
return;
}
try {
// 저장 버튼 비활성화
if (saveBtn) {
saveBtn.disabled = true;
saveBtn.textContent = '저장 중...';
}
// 기존 마켓 목록 가져오기
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, {
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('마켓 목록을 가져올 수 없습니다.');
}
const data = await response.json();
const markets = data[0]?.my_markets || [];
if (this.currentEditIndex < 0 || this.currentEditIndex >= markets.length) {
throw new Error('잘못된 마켓 인덱스입니다.');
}
// URL 중복 검사 (자기 자신 제외)
const isDuplicate = markets.some((market, index) =>
index !== this.currentEditIndex && market.market_url === marketUrl
);
if (isDuplicate) {
throw new Error('이미 등록된 마켓 URL입니다.');
}
// 마켓 정보 업데이트
const existingMarket = markets[this.currentEditIndex];
markets[this.currentEditIndex] = {
...existingMarket,
market_url: marketUrl,
market_name: marketName,
market_nickname: marketNickname,
for_zzim: forZzim,
for_mutual_zzim: forMutualZzim,
is_visible: isVisible,
updated_at: new Date().toISOString()
};
// user_markets 테이블 업데이트
const updateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?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({
my_markets: markets
})
});
if (!updateResponse.ok) {
const errorText = await updateResponse.text();
throw new Error('마켓 수정 실패: ' + errorText);
}
// 성공 처리
this.closeEditModal();
await this.loadMyMarkets();
this.showSuccess('마켓 정보가 성공적으로 수정되었습니다.');
} catch (error) {
console.error('마켓 수정 저장 오류:', error);
this.showError('마켓 수정 중 오류가 발생했습니다: ' + error.message);
} finally {
// 저장 버튼 복원
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.textContent = '저장';
}
}
}
async toggleMarketVisibility(marketId) {
try {
// 기존 마켓 목록 가져오기
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, {
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('마켓 목록을 가져올 수 없습니다.');
}
const data = await response.json();
const markets = data[0]?.my_markets || [];
// 마켓 ID로 해당 마켓 찾기
const market = markets.find(market => `${market.market_url}_${market.created_at}` === marketId);
if (!market) {
this.showError('잘못된 마켓 ID입니다.');
return;
}
// 노출 상태 토글
market.is_visible = !market.is_visible;
market.updated_at = new Date().toISOString();
// user_markets 테이블 업데이트
const updateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?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({
my_markets: markets
})
});
if (!updateResponse.ok) {
throw new Error('마켓 노출 설정 변경 실패');
}
await this.loadMyMarkets();
this.showSuccess(`마켓이 ${market.is_visible ? '노출' : '숨김'} 상태로 변경되었습니다.`);
} catch (error) {
console.error('마켓 노출 설정 변경 오류:', error);
this.showError('마켓 노출 설정 변경 중 오류가 발생했습니다.');
}
}
async deleteMarket(marketId) {
try {
// 기존 마켓 목록 가져오기
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, {
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('마켓 목록을 가져올 수 없습니다.');
}
const data = await response.json();
const markets = data[0]?.my_markets || [];
// 마켓 ID로 해당 마켓 찾기
const market = markets.find(market => `${market.market_url}_${market.created_at}` === marketId);
if (!market) {
this.showError('잘못된 마켓 ID입니다.');
return;
}
if (!confirm(`정말로 "${market.market_nickname}" 마켓을 삭제하시겠습니까?`)) {
return;
}
// 마켓 삭제 (배열에서 제거)
markets.splice(markets.indexOf(market), 1);
// user_markets 테이블 업데이트
const updateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?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({
my_markets: markets
})
});
if (!updateResponse.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 {
this.zzimInProgress = true;
this.updateButtonStates();
const markets = await this.getActiveMarkets();
if (markets.length === 0) {
this.showError('등록된 마켓이 없습니다. 먼저 마켓을 등록해주세요.');
return;
}
const settings = this.getZzimSettings();
this.showProgress(0, markets.length, '내 마켓 찜하기 준비 중...');
for (let i = 0; i < markets.length; i++) {
if (!this.zzimInProgress) {
this.showError('찜하기가 중단되었습니다.');
break;
}
const market = markets[i];
this.showProgress(i, markets.length, `${market.market_nickname} 찜하기 중...`);
await this.executeZzim(market, 'my_market', settings);
this.showProgress(i + 1, markets.length, `${market.market_nickname} 완료`);
// 마켓 간 대기 시간 (설정된 속도 적용)
if (i < markets.length - 1 && this.zzimInProgress) {
this.showProgress(i + 1, markets.length, `다음 마켓 대기 중... (${settings.totalDelay}ms)`);
await new Promise(resolve => setTimeout(resolve, settings.totalDelay));
}
}
if (this.zzimInProgress) {
this.hideProgress();
this.showSuccess(`내 마켓 찜하기 완료! ${markets.length}개 마켓 처리됨`);
setTimeout(() => {
window.close();
}, 2000); // 2초 후 창 닫기
}
} catch (error) {
console.error('내 마켓 찜하기 오류:', error);
this.hideProgress();
this.showError('내 마켓 찜하기 중 오류가 발생했습니다.');
} finally {
this.zzimInProgress = false;
this.updateButtonStates();
}
}
async startMutualZzim() {
if (this.zzimInProgress) {
this.showError('이미 찜하기가 진행 중입니다.');
return;
}
try {
this.zzimInProgress = true;
this.updateButtonStates();
// 품앗이 마켓 목록 가져오기
const mutualMarkets = await this.getMutualMarkets();
if (mutualMarkets.length === 0) {
this.showError('품앗이 마켓이 없습니다.');
return;
}
const settings = this.getZzimSettings();
this.showProgress(0, mutualMarkets.length, '품앗이 찜하기 준비 중...');
for (let i = 0; i < mutualMarkets.length; i++) {
if (!this.zzimInProgress) {
this.showError('찜하기가 중단되었습니다.');
break;
}
const market = mutualMarkets[i];
this.showProgress(i, mutualMarkets.length, `${market.market_nickname} 품앗이 중...`);
// 백그라운드 모드를 강제로 활성화 (품앗이는 항상 백그라운드로)
const mutualSettings = { ...settings, backgroundMode: true };
await this.executeZzim(market, 'mutual', mutualSettings);
this.showProgress(i + 1, mutualMarkets.length, `${market.market_nickname} 완료`);
// 마켓 간 대기 시간 (설정된 속도 적용)
if (i < mutualMarkets.length - 1 && this.zzimInProgress) {
this.showProgress(i + 1, mutualMarkets.length, `다음 마켓 대기 중... (${settings.totalDelay}ms)`);
await new Promise(resolve => setTimeout(resolve, settings.totalDelay));
}
}
if (this.zzimInProgress) {
this.hideProgress();
this.showSuccess(`품앗이 찜하기 완료! ${mutualMarkets.length}개 마켓 처리됨`);
setTimeout(() => {
window.close();
}, 2000); // 2초 후 창 닫기
}
} catch (error) {
console.error('품앗이 찜하기 오류:', error);
this.hideProgress();
this.showError('품앗이 찜하기 중 오류가 발생했습니다.');
} finally {
this.zzimInProgress = false;
this.updateButtonStates();
}
}
async executeZzim(market, zzimType, settings) {
this.zzimInProgress = true;
try {
// 마켓 URL 생성 (최신상품 우선 옵션 적용)
const targetUrl = this.generateMarketUrl(market.market_url, settings.latestFirst);
if (settings.backgroundMode) {
// 백그라운드 실행을 위해 background.js로 메시지 전송
const response = await chrome.runtime.sendMessage({
action: 'executeBackgroundZzim',
market: {
...market,
target_url: targetUrl
},
zzimType: zzimType,
userId: this.user_id,
accessToken: this.access_token,
settings: settings
});
if (response.success) {
this.showSuccess(`백그라운드 찜하기가 시작되었습니다. (${market.market_nickname})`);
await this.recordZzim(market, zzimType);
} else {
throw new Error(response.error || '백그라운드 찜하기 실행 실패');
}
} else {
// 포그라운드에서 실행 - 로그인 상태 확인 후 처리
await this.checkLoginAndExecute(market, targetUrl, settings, zzimType);
}
} catch (error) {
console.error('찜하기 실행 오류:', error);
this.showError('찜하기 실행 중 오류가 발생했습니다: ' + error.message);
} finally {
this.zzimInProgress = false;
}
}
// 로그인 상태 확인 및 찜하기 실행
async checkLoginAndExecute(market, targetUrl, settings, zzimType) {
try {
// 먼저 마켓 페이지에 접근해서 로그인 상태 확인
const testUrl = market.market_url;
// 현재 탭에서 마켓으로 이동하여 로그인 상태 확인
const urlType = settings.latestFirst ? '최신상품' : '전체상품';
// 로그인 확인을 위한 모달 표시
this.showLoginCheckModal(market, targetUrl, settings, zzimType, urlType);
} catch (error) {
console.error('로그인 확인 오류:', error);
this.showError('로그인 상태 확인 중 오류가 발생했습니다.');
}
}
// 로그인 확인 모달 표시
showLoginCheckModal(market, targetUrl, settings, zzimType, urlType) {
// 기존 모달이 있으면 제거
this.removeLoginModal();
// 모달 HTML 생성
const modalHTML = `
<div id="login-check-modal" style="
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 999999;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', sans-serif;
">
<div style="
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
text-align: center;
">
<h3 style="
color: #333;
margin-bottom: 20px;
font-size: 18px;
font-weight: 600;
">🔐 로그인 확인</h3>
<p style="
color: #666;
margin-bottom: 25px;
line-height: 1.5;
font-size: 14px;
">
<strong>${market.market_nickname}</strong> 마켓에서 찜하기를 실행합니다.<br>
(${urlType} 모드)
</p>
<div style="
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-bottom: 25px;
border-left: 4px solid #007bff;
">
<p style="
margin: 0;
color: #495057;
font-size: 13px;
line-height: 1.4;
">
네이버에 로그인되어 있지 않으면 자동으로 로그인 페이지로 이동됩니다.<br>
로그인 후 다시 찜하기를 실행해 주세요.
</p>
</div>
<div style="display: flex; gap: 10px; justify-content: center;">
<button id="login-proceed-btn" style="
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
" onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'">
🚀 찜하기 시작
</button>
<button id="login-cancel-btn" style="
background: #6c757d;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
" onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'">
❌ 취소
</button>
</div>
</div>
</div>
`;
// 모달을 body에 추가
document.body.insertAdjacentHTML('beforeend', modalHTML);
// 이벤트 리스너 추가
const proceedBtn = document.getElementById('login-proceed-btn');
const cancelBtn = document.getElementById('login-cancel-btn');
const modal = document.getElementById('login-check-modal');
if (proceedBtn) {
proceedBtn.addEventListener('click', () => {
this.removeLoginModal();
this.proceedWithZzim(market, targetUrl, settings, zzimType);
});
}
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
this.removeLoginModal();
this.showStatus('찜하기가 취소되었습니다.');
});
}
// ESC 키로 모달 닫기
const escapeHandler = (e) => {
if (e.key === 'Escape') {
this.removeLoginModal();
this.showStatus('찜하기가 취소되었습니다.');
document.removeEventListener('keydown', escapeHandler);
}
};
document.addEventListener('keydown', escapeHandler);
// 모달 배경 클릭으로 닫기
if (modal) {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
this.removeLoginModal();
this.showStatus('찜하기가 취소되었습니다.');
}
});
}
}
// 로그인 모달 제거
removeLoginModal() {
const modal = document.getElementById('login-check-modal');
if (modal) {
modal.remove();
}
}
// 찜하기 진행 (로그인 리다이렉트 포함)
async proceedWithZzim(market, targetUrl, settings, zzimType) {
try {
// 마켓 URL에서 스토어 ID 추출
const storeId = market.market_url.split('/').pop();
// 올바른 네이버 스마트스토어 URL 생성
let finalTargetUrl = targetUrl;
// URL에 찜하기 파라미터 추가
const urlObj = new URL(finalTargetUrl);
urlObj.searchParams.set('auto_zzim', 'true');
// 일일 제한 확인 (settings.maxZzim이 있으면 사용, 없으면 기본값)
const maxZzim = settings.maxZzim || this.currentLimits?.daily_zzim_limit || 100;
urlObj.searchParams.set('max_zzim', maxZzim.toString());
// 네이버 스마트스토어는 cp 파라미터를 사용 (page가 아님)
if (!urlObj.searchParams.has('cp')) {
urlObj.searchParams.set('cp', '1'); // 첫 번째 페이지부터 시작
}
finalTargetUrl = urlObj.toString();
console.log('[ZzimManager] 최종 찜하기 URL:', finalTargetUrl);
// 로그인 리다이렉트 URL 생성
const loginRedirectUrl = `https://nid.naver.com/nidlogin.login?url=${encodeURIComponent(finalTargetUrl)}`;
this.showSuccess(`찜하기 페이지로 이동합니다. (${market.market_nickname})`);
// 찜 기록 (실제 찜하기는 새 페이지에서 실행됨)
await this.recordZzim(market, zzimType);
// 현재 탭에서 로그인 리다이렉트 URL로 이동
window.location.href = loginRedirectUrl;
} catch (error) {
console.error('찜하기 진행 오류:', error);
this.showError('찜하기 진행 중 오류가 발생했습니다: ' + error.message);
}
}
async recordZzim(market, zzimType) {
try {
// 찜 기록을 별도 테이블에 저장하지 않고 통계만 업데이트
// (필요시 나중에 zzim_history 테이블 추가 가능)
// 통계 업데이트 (실제 찜한 개수는 스크립트 결과를 받아야 하지만 임시로 50개로 설정)
const estimatedCount = 50;
await this.updateZzimStats(estimatedCount, zzimType, market);
// UI 새로고침
await Promise.all([
this.loadZzimStats(),
this.loadMyMarkets()
]);
} catch (error) {
console.error('찜 기록 저장 오류:', error);
}
}
async updateZzimStats(count, zzimType, targetMarket = null) {
// 이 함수는 더 이상 직접 호출되지 않습니다.
// Background 스크립트가 처리합니다.
console.log('updateZzimStats는 이제 Background Script에서 처리됩니다.');
}
// 마켓별 찜받은 개수 업데이트
async updateMarketZzimCount(marketUrl, count, userId = null) {
try {
const targetUserId = userId || this.user_id;
const marketsResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${targetUserId}&select=my_markets`, {
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (marketsResponse.ok) {
const marketsData = await marketsResponse.json();
const markets = marketsData[0]?.my_markets || [];
// 해당 마켓 찾아서 찜받은 개수 증가
const updatedMarkets = markets.map(market => {
if (market.market_url === marketUrl) {
return {
...market,
zzim_received_count: (market.zzim_received_count || 0) + count,
updated_at: new Date().toISOString()
};
}
return market;
});
// 업데이트된 마켓 목록 저장
await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${targetUserId}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
my_markets: updatedMarkets
})
});
console.log(`마켓 ${marketUrl} 찜받은 개수 ${count} 증가`);
}
} catch (error) {
console.error('마켓 찜받은 개수 업데이트 오류:', error);
}
}
// 찜 기록 저장
async saveZzimRecord(zzimType, targetMarket, zzimCount, mileageEarned) {
try {
const record = {
user_id: this.user_id,
market_url: targetMarket?.market_url || '',
market_name: targetMarket?.market_name || '',
market_nickname: targetMarket?.market_nickname || '',
zzim_type: zzimType,
zzim_count: zzimCount,
mileage_earned: mileageEarned,
target_user_id: targetMarket?.owner_user_id || null,
created_at: new Date().toISOString()
};
const response = 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(record)
});
if (!response.ok) {
console.warn('찜 기록 저장 실패:', response.status);
} else {
console.log('찜 기록 저장 완료');
}
} catch (error) {
console.error('찜 기록 저장 오류:', error);
}
}
// 찜하기 가능 여부 확인
canZzim(zzimType) {
if (!this.currentStats || !this.currentLimits) {
return { canZzim: false, reason: '통계 정보를 불러오는 중입니다.' };
}
if (zzimType === 'my_market') {
// 내 마켓 찜하기: 오늘 찜한 개수 확인
const todayCount = this.currentStats.todayZzimCount;
const dailyLimit = this.currentLimits.daily_zzim_limit;
if (todayCount >= dailyLimit) {
return {
canZzim: false,
reason: `오늘 찜 제한에 도달했습니다. (${todayCount}/${dailyLimit})`
};
}
return { canZzim: true, remaining: dailyLimit - todayCount };
} else if (zzimType === 'mutual') {
// 품앗이 찜하기: 내 마일리지 확인 필요 없음 (오히려 벌어야 함)
// 단, 계정 보호를 위해 '오늘 찜한 개수' 제한은 동일하게 적용
const todayCount = this.currentStats.todayZzimCount;
const dailyLimit = this.currentLimits.daily_zzim_limit;
if (todayCount >= dailyLimit) {
return {
canZzim: false,
reason: `오늘 찜 제한에 도달했습니다. (품앗이 포함 통합 제한: ${todayCount}/${dailyLimit})`
};
}
return { canZzim: true, remaining: dailyLimit - todayCount };
}
return { canZzim: false, reason: '알 수 없는 찜 타입입니다.' };
}
async getActiveMarkets() {
try {
// user_markets 테이블에서 내 마켓 목록 가져오기
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, {
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('마켓 목록을 가져올 수 없습니다.');
}
const data = await response.json();
const markets = data[0]?.my_markets || [];
// 찜하기 대상으로 체크된 마켓만 반환
return markets.filter(market => market.for_zzim !== false);
} catch (error) {
console.error('활성 마켓 목록 조회 오류:', error);
throw error;
}
}
async getMutualMarkets() {
try {
console.log('[품앗이] 품앗이 마켓 목록 조회 시작');
// 내 현재 마일리지 확인 (users 테이블) - 삭제: 품앗이는 마일리지를 버는 행위이므로 보유량 체크 불필요
/*
const myStatsResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}&select=available_zzim_mile`, {
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
let myAvailableMileage = 0;
if (myStatsResponse.ok) {
const myData = await myStatsResponse.json();
myAvailableMileage = myData[0]?.available_zzim_mile || 0;
}
console.log('[품앗이] 내 사용 가능한 마일리지:', myAvailableMileage);
if (myAvailableMileage <= 0) {
throw new Error('사용 가능한 마일리지가 없습니다. 먼저 다른 사람의 마켓에 찜을 받아 마일리지를 적립하세요.');
}
*/
// 다른 사용자들의 노출된 마켓 목록 가져오기 (품앗이용) - v_user_market_stats 뷰 사용
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/v_user_market_stats?user_id=neq.${this.user_id}&available_zzim_mile=gt.0&select=user_id,available_zzim_mile,my_markets&limit=100`, {
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('품앗이 마켓 목록을 가져올 수 없습니다.');
}
const data = await response.json();
console.log('[품앗이] 조회된 사용자 데이터:', data.length);
const mutualMarkets = [];
// 각 사용자의 노출된 마켓들을 수집
data.forEach(userMarket => {
const markets = userMarket.my_markets || [];
const userMileage = userMarket.available_zzim_mile || 0;
// 마일리지가 있는 사용자의 노출된 마켓만 수집
if (userMileage > 0) {
const visibleMarkets = markets.filter(market =>
market.is_visible !== false &&
market.for_mutual_zzim !== false // 품앗이 대상으로 설정된 마켓만
);
// 사용자 정보를 마켓에 추가
visibleMarkets.forEach(market => {
mutualMarkets.push({
...market,
owner_user_id: userMarket.user_id,
owner_available_mileage: userMileage
});
});
}
});
console.log('[품앗이] 수집된 품앗이 마켓 수:', mutualMarkets.length);
if (mutualMarkets.length === 0) {
throw new Error('현재 품앗이 가능한 마켓이 없습니다.');
}
// 우선순위 기반 정렬 (마일리지가 많은 사용자 우선)
mutualMarkets.sort((a, b) => {
// 1차: 마일리지 많은 순
if (b.owner_available_mileage !== a.owner_available_mileage) {
return b.owner_available_mileage - a.owner_available_mileage;
}
// 2차: 최신 등록 순
return new Date(b.created_at || 0) - new Date(a.created_at || 0);
});
// 내 마일리지로 처리 가능한 개수만큼 반환 (최대 10개) -> 수정: 제한 없음 (단, 하루 최대 10개 마켓 정도만)
const maxMarkets = 10;
const selectedMarkets = mutualMarkets.slice(0, maxMarkets);
console.log('[품앗이] 선택된 품앗이 마켓:', selectedMarkets.length);
return selectedMarkets;
} catch (error) {
console.error('품앗이 마켓 목록 조회 오류:', error);
throw error;
}
}
showStatus(message) {
const statusEl = document.getElementById('zzim-status');
// 진행률 표시 숨기기
this.hideProgress();
// 모든 상태 메시지 숨기기
this.hideAllMessages();
// 상태 메시지 표시
if (statusEl) {
statusEl.textContent = message;
statusEl.style.display = 'block';
}
}
showError(message) {
const errorEl = document.getElementById('zzim-error');
// 진행률 표시 숨기기
this.hideProgress();
// 모든 상태 메시지 숨기기
this.hideAllMessages();
// 오류 메시지 표시
if (errorEl) {
errorEl.textContent = message;
errorEl.style.display = 'block';
}
}
showSuccess(message) {
const successEl = document.getElementById('zzim-success');
// 진행률 표시 숨기기
this.hideProgress();
// 모든 상태 메시지 숨기기
this.hideAllMessages();
// 성공 메시지 표시
if (successEl) {
successEl.textContent = message;
successEl.style.display = 'block';
}
}
showProgress(current, total, message) {
const progressDiv = document.getElementById('zzim-progress');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
const progressPercent = document.getElementById('progress-percent');
// 진행률 표시 영역 보이기
if (progressDiv) {
progressDiv.style.display = 'block';
}
// 다른 상태 메시지들 숨기기
this.hideAllMessages();
const percentage = Math.round((current / total) * 100);
if (progressBar) {
progressBar.style.width = `${percentage}%`;
}
if (progressText) {
progressText.textContent = message;
}
if (progressPercent) {
progressPercent.textContent = `${percentage}%`;
}
}
hideProgress() {
const progressDiv = document.getElementById('zzim-progress');
if (progressDiv) {
progressDiv.style.display = 'none';
}
}
hideAllMessages() {
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';
}
stopZzim() {
this.zzimInProgress = false;
this.updateButtonStates();
this.hideProgress();
this.showStatus('찜하기를 중단했습니다.');
}
updateButtonStates() {
const myMarketBtn = document.getElementById('my-market-zzim-btn');
const mutualBtn = document.getElementById('mutual-zzim-btn');
const stopBtn = document.getElementById('stop-zzim-btn');
if (this.zzimInProgress) {
// 찜하기 진행 중
if (myMarketBtn) {
myMarketBtn.disabled = true;
myMarketBtn.textContent = '💝 찜하기 진행 중...';
}
if (mutualBtn) {
mutualBtn.disabled = true;
mutualBtn.textContent = '🤝 찜하기 진행 중...';
}
if (stopBtn) {
stopBtn.style.display = 'inline-block';
stopBtn.disabled = false;
}
} else {
// 찜하기 중지 상태
if (myMarketBtn) {
myMarketBtn.disabled = false;
myMarketBtn.textContent = '💝 내 마켓 찜하기';
}
if (mutualBtn) {
mutualBtn.disabled = false;
mutualBtn.textContent = '🤝 품앗이 찜하기';
}
if (stopBtn) {
stopBtn.style.display = 'none';
}
}
}
// 디버깅용 테스트 함수
async testDebug() {
console.log('=== 디버깅 테스트 시작 ===');
try {
// 1. 현재 설정 확인
console.log('현재 설정:', {
user_id: this.user_id,
has_token: !!this.access_token,
supabase_url: this.SUPABASE_URL
});
// 2. user_markets 테이블 상태 확인
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, {
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
console.log('user_markets 테이블 상태:', {
status: response.status,
ok: response.ok
});
if (response.ok) {
const data = await response.json();
console.log('user_markets 데이터:', data);
} else {
const errorText = await response.text();
console.error('user_markets 조회 오류:', errorText);
}
// 3. 강제로 마켓 목록 다시 로드
console.log('마켓 목록 강제 새로고침...');
await this.loadMyMarkets();
this.showSuccess('디버깅 테스트 완료 - 콘솔을 확인하세요');
} catch (error) {
console.error('디버깅 테스트 오류:', error);
this.showError('디버깅 테스트 실패: ' + error.message);
}
console.log('=== 디버깅 테스트 종료 ===');
}
}
// 전역 인스턴스 생성
let zzimManager;
// DOM 로드 완료 후 초기화
document.addEventListener('DOMContentLoaded', async () => {
zzimManager = new ZzimManager();
// 전역 접근을 위해 window 객체에도 등록
window.zzimManager = zzimManager;
await zzimManager.init();
});