Implement background message handlers for user info, sayings, and banned words management. Refactor settings and API calls to streamline data retrieval through background script. Update UI elements and improve error handling across various modules.

This commit is contained in:
9700X_PC 2025-11-27 23:37:57 +09:00
parent bdde76d7a6
commit e2d353afe3
11 changed files with 1747 additions and 2111 deletions

File diff suppressed because it is too large Load Diff

View File

@ -2,12 +2,12 @@
class BannedWordsManager { class BannedWordsManager {
constructor() { constructor() {
// 초기값 설정 (chrome.storage에서 로드될 때까지 임시) // 초기값 설정 (chrome.storage에서 로드될 때까지 임시)
this.SUPABASE_URL = null; // SUPABASE 설정은 background.js에서 중앙 관리됨
this.SUPABASE_ANON_KEY = null;
this.DEBUG_MODE = true; this.DEBUG_MODE = true;
this.ACCESS_TOKEN = null; this.ACCESS_TOKEN = null;
this.isConfigLoaded = false; this.isConfigLoaded = false;
this.buttonEventListenerAttached = false; // 이벤트 리스너 중복 등록 방지 this.buttonEventListenerAttached = false; // 이벤트 리스너 중복 등록 방지
this.userId = null; // 사용자 ID 저장
this.debugLog('BannedWordsManager 생성자 시작 - 설정 로드 대기 중'); this.debugLog('BannedWordsManager 생성자 시작 - 설정 로드 대기 중');
@ -60,8 +60,6 @@ class BannedWordsManager {
if (configData.bannedWords_config) { if (configData.bannedWords_config) {
const config = configData.bannedWords_config; const config = configData.bannedWords_config;
this.debugLog('bannedWords_config에서 설정 로드', { this.debugLog('bannedWords_config에서 설정 로드', {
hasUrl: !!config.SUPABASE_URL,
hasKey: !!config.SUPABASE_ANON_KEY,
hasToken: !!config.ACCESS_TOKEN, hasToken: !!config.ACCESS_TOKEN,
debugMode: config.DEBUG_MODE, debugMode: config.DEBUG_MODE,
timestamp: config.timestamp, timestamp: config.timestamp,
@ -73,8 +71,6 @@ class BannedWordsManager {
this.debugLog('⚠️ 설정이 오래되었습니다', { age: Date.now() - config.timestamp }); this.debugLog('⚠️ 설정이 오래되었습니다', { age: Date.now() - config.timestamp });
} }
this.SUPABASE_URL = config.SUPABASE_URL;
this.SUPABASE_ANON_KEY = config.SUPABASE_ANON_KEY;
this.DEBUG_MODE = config.DEBUG_MODE !== undefined ? config.DEBUG_MODE : true; this.DEBUG_MODE = config.DEBUG_MODE !== undefined ? config.DEBUG_MODE : true;
this.ACCESS_TOKEN = config.ACCESS_TOKEN; this.ACCESS_TOKEN = config.ACCESS_TOKEN;
@ -82,41 +78,25 @@ class BannedWordsManager {
// 2. 개별 설정 확인 (fallback) // 2. 개별 설정 확인 (fallback)
this.debugLog('bannedWords_config가 없음, 개별 설정 확인 중'); this.debugLog('bannedWords_config가 없음, 개별 설정 확인 중');
const storageData = await chrome.storage.local.get([ const storageData = await chrome.storage.local.get(['access_token']);
'access_token',
'SUPABASE_URL',
'SUPABASE_ANON_KEY'
]);
this.debugLog('개별 설정 조회 결과', { this.debugLog('개별 설정 조회 결과', {
hasToken: !!storageData.access_token, hasToken: !!storageData.access_token
hasUrl: !!storageData.SUPABASE_URL,
hasKey: !!storageData.SUPABASE_ANON_KEY
}); });
// 기본값 설정
this.SUPABASE_URL = storageData.SUPABASE_URL || 'https://ko.wrmc.cc';
this.SUPABASE_ANON_KEY = storageData.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU';
this.ACCESS_TOKEN = storageData.access_token; this.ACCESS_TOKEN = storageData.access_token;
} }
this.isConfigLoaded = true; this.isConfigLoaded = true;
this.debugLog('설정 로드 완료', { this.debugLog('설정 로드 완료', {
SUPABASE_URL: this.SUPABASE_URL,
hasToken: !!this.ACCESS_TOKEN, hasToken: !!this.ACCESS_TOKEN,
tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0, tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0,
DEBUG_MODE: this.DEBUG_MODE, DEBUG_MODE: this.DEBUG_MODE,
isConfigLoaded: this.isConfigLoaded isConfigLoaded: this.isConfigLoaded
}); });
// 설정 검증 // 설정 검증 - SUPABASE 설정은 background.js에서 관리
if (!this.SUPABASE_URL) {
throw new Error('SUPABASE_URL이 설정되지 않았습니다');
}
if (!this.SUPABASE_ANON_KEY) {
throw new Error('SUPABASE_ANON_KEY가 설정되지 않았습니다');
}
if (!this.ACCESS_TOKEN) { if (!this.ACCESS_TOKEN) {
throw new Error('ACCESS_TOKEN이 없습니다. 로그인이 필요합니다'); throw new Error('ACCESS_TOKEN이 없습니다. 로그인이 필요합니다');
} }
@ -197,96 +177,41 @@ class BannedWordsManager {
// 토큰 유효성 검증 // 토큰 유효성 검증
async validateToken() { async validateToken() {
try { try {
this.debugLog('토큰 검증 API 호출 준비', { this.debugLog('토큰 검증 API 호출 준비 (background.js 경유)', {
url: `${this.SUPABASE_URL}/auth/v1/user`,
hasToken: !!this.ACCESS_TOKEN, hasToken: !!this.ACCESS_TOKEN,
tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0, tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0,
tokenPreview: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.substring(0, 30) + '...' : 'none' tokenPreview: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.substring(0, 30) + '...' : 'none'
}); });
const headers = { // background.js를 통해 토큰 검증
'Authorization': `Bearer ${this.ACCESS_TOKEN}`, const response = await chrome.runtime.sendMessage({
'apikey': this.SUPABASE_ANON_KEY, action: 'validateToken',
'Content-Type': 'application/json', token: this.ACCESS_TOKEN
'Accept': 'application/json'
};
this.debugLog('요청 헤더 정보', {
hasAuthorization: !!headers.Authorization,
hasApikey: !!headers.apikey,
authPreview: headers.Authorization ? headers.Authorization.substring(0, 20) + '...' : 'none'
});
const authRes = await fetch(`${this.SUPABASE_URL}/auth/v1/user`, {
method: 'GET',
headers: headers,
mode: 'cors', // CORS 모드 명시적 설정
credentials: 'omit' // 크로스 오리진 요청에서 자격 증명 제외
}); });
this.debugLog('토큰 검증 API 응답', { this.debugLog('토큰 검증 API 응답', {
status: authRes.status, success: response?.success,
statusText: authRes.statusText, hasUser: !!response?.user
ok: authRes.ok,
type: authRes.type,
url: authRes.url,
headers: Object.fromEntries(authRes.headers.entries())
}); });
if (!authRes.ok) { if (!response || !response.success) {
let errorText = ''; throw new Error(response?.error || '토큰 검증 실패');
let errorJson = null;
try {
const responseText = await authRes.text();
errorText = responseText;
// JSON 파싱 시도
if (responseText.trim().startsWith('{')) {
errorJson = JSON.parse(responseText);
}
} catch (parseError) {
this.debugLog('응답 파싱 실패', { parseError: parseError.message });
} }
this.debugLog('토큰 검증 실패 - 상세 정보', { const userData = response.user;
status: authRes.status,
statusText: authRes.statusText,
errorText: errorText,
errorJson: errorJson
});
throw new Error(`토큰이 유효하지 않습니다 (${authRes.status}: ${authRes.statusText})\n응답: ${errorText}`);
}
const userData = await authRes.json();
this.debugLog('토큰 검증 성공', { this.debugLog('토큰 검증 성공', {
email: userData.email, email: userData.email,
id: userData.id, id: userData.id
userData: userData
}); });
return userData; return userData;
} catch (error) { } catch (error) {
this.debugLog('토큰 검증 중 오류 - 상세 분석', { this.debugLog('토큰 검증 중 오류', {
errorName: error.name, errorName: error.name,
errorMessage: error.message, errorMessage: error.message
errorStack: error.stack,
isNetworkError: error.name === 'TypeError' && error.message.includes('fetch'),
isCorsError: error.message.includes('CORS') || error.message.includes('cors'),
isTimeoutError: error.message.includes('timeout') || error.message.includes('Timeout')
}); });
// 에러 타입별 상세 메시지
if (error.name === 'TypeError' && error.message.includes('fetch')) {
throw new Error(`네트워크 연결 오류: 서버에 연결할 수 없습니다.\n- URL: ${this.SUPABASE_URL}/auth/v1/user\n- 원본 에러: ${error.message}`);
} else if (error.message.includes('CORS')) {
throw new Error(`CORS 오류: 크로스 오리진 요청이 차단되었습니다.\n- 서버 CORS 설정을 확인해주세요\n- 원본 에러: ${error.message}`);
} else if (error.message.includes('timeout')) {
throw new Error(`요청 시간 초과: 서버 응답이 지연되고 있습니다.\n- 원본 에러: ${error.message}`);
}
throw error; throw error;
} }
} }
@ -309,7 +234,7 @@ class BannedWordsManager {
// 금지어 목록 로드 // 금지어 목록 로드
async loadBannedWords() { async loadBannedWords() {
this.debugLog('금지어 목록 로드 시작'); this.debugLog('금지어 목록 로드 시작 (background.js 경유)');
const loading = document.getElementById("banned-words-loading"); const loading = document.getElementById("banned-words-loading");
const tbody = document.getElementById("banned-words-tbody"); const tbody = document.getElementById("banned-words-tbody");
@ -318,117 +243,34 @@ class BannedWordsManager {
tbody.innerHTML = ""; tbody.innerHTML = "";
try { try {
// 공통 헤더 설정 // background.js를 통해 금지어 목록 조회
const headers = { const response = await chrome.runtime.sendMessage({
'Authorization': `Bearer ${this.ACCESS_TOKEN}`, action: 'getBannedWords',
'apikey': this.SUPABASE_ANON_KEY, token: this.ACCESS_TOKEN
'Content-Type': 'application/json',
'Accept': 'application/json'
};
const fetchOptions = {
method: 'GET',
headers: headers,
mode: 'cors',
credentials: 'omit'
};
// 현재 사용자의 ID 가져오기
this.debugLog('Auth API 호출 중...');
const authRes = await fetch(`${this.SUPABASE_URL}/auth/v1/user`, fetchOptions);
this.debugLog('Auth API 응답', {
status: authRes.status,
statusText: authRes.statusText,
ok: authRes.ok,
url: authRes.url
}); });
if (!authRes.ok) { this.debugLog('금지어 목록 API 응답', {
const errorDetail = await authRes.text(); success: response?.success,
this.debugLog('Auth API 에러 상세', { count: response?.bannedWords?.length
status: authRes.status,
error: errorDetail
}); });
throw new Error(`사용자 정보를 가져올 수 없습니다 (${authRes.status}: ${authRes.statusText})\n응답: ${errorDetail}`);
if (!response || !response.success) {
throw new Error(response?.error || '금지어 목록 조회 실패');
} }
const authUser = await authRes.json(); const bannedWords = response.bannedWords;
this.debugLog('Auth 사용자 정보 수신', { this.userId = response.userId; // 사용자 ID 저장 (추가 시 필요)
email: authUser.email,
id: authUser.id
});
// 사용자의 user_id 가져오기
this.debugLog('Users API 호출 중...');
const userRes = await fetch(`${this.SUPABASE_URL}/rest/v1/users?select=id&email=eq.${encodeURIComponent(authUser.email)}&limit=1`, fetchOptions);
this.debugLog('Users API 응답', {
status: userRes.status,
statusText: userRes.statusText,
ok: userRes.ok,
url: userRes.url
});
if (!userRes.ok) {
const errorDetail = await userRes.text();
this.debugLog('Users API 에러 상세', {
status: userRes.status,
error: errorDetail
});
throw new Error(`사용자 정보를 찾을 수 없습니다 (${userRes.status}: ${userRes.statusText})\n응답: ${errorDetail}`);
}
const userData = await userRes.json();
this.debugLog('Users 데이터 수신', {
count: userData.length,
data: userData
});
if (!userData || userData.length === 0) {
throw new Error('사용자 데이터를 찾을 수 없습니다');
}
const userId = userData[0].id;
this.debugLog('사용자 ID 확인', { userId });
// 금지어 목록 가져오기
this.debugLog('BannedWords API 호출 중...');
const bannedWordsRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words?select=*&user_id=eq.${userId}&order=created_at.desc`, fetchOptions);
this.debugLog('BannedWords API 응답', {
status: bannedWordsRes.status,
statusText: bannedWordsRes.statusText,
ok: bannedWordsRes.ok,
url: bannedWordsRes.url
});
if (!bannedWordsRes.ok) {
const errorDetail = await bannedWordsRes.text();
this.debugLog('BannedWords API 에러 상세', {
status: bannedWordsRes.status,
error: errorDetail
});
throw new Error(`금지어 목록을 가져올 수 없습니다 (${bannedWordsRes.status}: ${bannedWordsRes.statusText})\n응답: ${errorDetail}`);
}
const bannedWords = await bannedWordsRes.json();
this.debugLog('금지어 목록 로드 완료', { count: bannedWords.length }); this.debugLog('금지어 목록 로드 완료', { count: bannedWords.length });
this.displayBannedWords(bannedWords); this.displayBannedWords(bannedWords);
} catch (error) { } catch (error) {
this.debugLog('금지어 목록 로드 실패', { this.debugLog('금지어 목록 로드 실패', {
errorName: error.name, errorName: error.name,
errorMessage: error.message, errorMessage: error.message
isNetworkError: error.name === 'TypeError' && error.message.includes('fetch')
}); });
let errorMessage = error.message; tbody.innerHTML = `<tr><td colspan="4" style="text-align: center; color: red;">오류: ${error.message}</td></tr>`;
if (error.name === 'TypeError' && error.message.includes('fetch')) {
errorMessage = `네트워크 연결 오류: ${error.message}`;
}
tbody.innerHTML = `<tr><td colspan="4" style="text-align: center; color: red;">오류: ${errorMessage}</td></tr>`;
} finally { } finally {
loading.style.display = "none"; loading.style.display = "none";
} }
@ -723,35 +565,22 @@ class BannedWordsManager {
throw new Error('로그인이 필요합니다'); throw new Error('로그인이 필요합니다');
} }
this.debugLog('등급 업데이트 API 호출', { wordId, selectedGrade }); this.debugLog('등급 업데이트 API 호출 (background.js 경유)', { wordId, selectedGrade });
const updateRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words?word_id=eq.${wordId}`, { // background.js를 통해 등급 수정
method: 'PATCH', const response = await chrome.runtime.sendMessage({
headers: { action: 'updateBannedWordGrade',
'Authorization': `Bearer ${this.ACCESS_TOKEN}`, token: this.ACCESS_TOKEN,
'apikey': this.SUPABASE_ANON_KEY, wordId: wordId,
'Content-Type': 'application/json', grade: selectedGrade
'Accept': 'application/json'
},
body: JSON.stringify({
grade: selectedGrade,
updated_at: new Date().toISOString()
})
}); });
this.debugLog('등급 업데이트 API 응답', { this.debugLog('등급 업데이트 API 응답', {
status: updateRes.status, success: response?.success
statusText: updateRes.statusText,
ok: updateRes.ok
}); });
if (!updateRes.ok) { if (!response || !response.success) {
const errorDetail = await updateRes.text(); throw new Error(response?.error || '등급 업데이트 실패');
this.debugLog('등급 업데이트 실패', {
status: updateRes.status,
error: errorDetail
});
throw new Error(`등급 업데이트 실패 (${updateRes.status}: ${updateRes.statusText})\n응답: ${errorDetail}`);
} }
this.debugLog('등급 수정 성공', { wordId, selectedGrade }); this.debugLog('등급 수정 성공', { wordId, selectedGrade });
@ -828,31 +657,21 @@ class BannedWordsManager {
throw new Error('로그인이 필요합니다'); throw new Error('로그인이 필요합니다');
} }
this.debugLog('금지어 삭제 API 호출', { wordId, word }); this.debugLog('금지어 삭제 API 호출 (background.js 경유)', { wordId, word });
const deleteRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words?word_id=eq.${wordId}`, { // background.js를 통해 금지어 삭제
method: 'DELETE', const response = await chrome.runtime.sendMessage({
headers: { action: 'deleteBannedWord',
'Authorization': `Bearer ${this.ACCESS_TOKEN}`, token: this.ACCESS_TOKEN,
'apikey': this.SUPABASE_ANON_KEY, wordId: wordId
'Content-Type': 'application/json',
'Accept': 'application/json'
}
}); });
this.debugLog('금지어 삭제 API 응답', { this.debugLog('금지어 삭제 API 응답', {
status: deleteRes.status, success: response?.success
statusText: deleteRes.statusText,
ok: deleteRes.ok
}); });
if (!deleteRes.ok) { if (!response || !response.success) {
const errorDetail = await deleteRes.text(); throw new Error(response?.error || '금지어 삭제 실패');
this.debugLog('금지어 삭제 실패', {
status: deleteRes.status,
error: errorDetail
});
throw new Error(`금지어 삭제 실패 (${deleteRes.status}: ${deleteRes.statusText})\n응답: ${errorDetail}`);
} }
this.debugLog('금지어 삭제 성공', { wordId, word }); this.debugLog('금지어 삭제 성공', { wordId, word });
@ -914,42 +733,28 @@ class BannedWordsManager {
throw new Error('word_id가 없습니다. 데이터베이스에서 word_id 값을 확인해주세요.'); throw new Error('word_id가 없습니다. 데이터베이스에서 word_id 값을 확인해주세요.');
} }
this.debugLog('키프리스 데이터 조회 API 호출', { this.debugLog('키프리스 데이터 조회 API 호출 (background.js 경유)', {
wordWordId,
word,
queryUrl: `${this.SUPABASE_URL}/rest/v1/user_banned_words_kipris?select=*&banned_word_id=eq.${wordWordId}&order=created_at.desc`
});
// 올바른 테이블 이름과 필드명 사용: word_id → banned_word_id
const resultsRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words_kipris?select=*&banned_word_id=eq.${wordWordId}&order=created_at.desc`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.ACCESS_TOKEN}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
this.debugLog('키프리스 데이터 API 응답', {
status: resultsRes.status,
statusText: resultsRes.statusText,
ok: resultsRes.ok,
url: resultsRes.url
});
if (!resultsRes.ok) {
const errorDetail = await resultsRes.text();
this.debugLog('키프리스 데이터 조회 실패', {
status: resultsRes.status,
error: errorDetail,
wordWordId, wordWordId,
word word
}); });
throw new Error(`키프리스 결과를 가져올 수 없습니다 (${resultsRes.status}: ${resultsRes.statusText})\n응답: ${errorDetail}`);
// background.js를 통해 키프리스 결과 조회
const response = await chrome.runtime.sendMessage({
action: 'getKiprisResults',
token: this.ACCESS_TOKEN,
wordId: wordWordId
});
this.debugLog('키프리스 데이터 API 응답', {
success: response?.success,
count: response?.kiprisData?.length
});
if (!response || !response.success) {
throw new Error(response?.error || '키프리스 결과 조회 실패');
} }
const kiprisData = await resultsRes.json(); const kiprisData = response.kiprisData;
this.debugLog('키프리스 결과 로드 성공', { this.debugLog('키프리스 결과 로드 성공', {
count: kiprisData.length, count: kiprisData.length,
wordWordId, wordWordId,
@ -1129,9 +934,9 @@ class BannedWordsManager {
// 현재 상태 정보 수집 // 현재 상태 정보 수집
const statusInfo = { const statusInfo = {
'초기화 상태': this.isConfigLoaded ? '✅ 완료' : '❌ 미완료', '초기화 상태': this.isConfigLoaded ? '✅ 완료' : '❌ 미완료',
'SUPABASE_URL': this.SUPABASE_URL || '❌ 설정되지 않음',
'ACCESS_TOKEN': this.ACCESS_TOKEN ? `✅ 있음 (${this.ACCESS_TOKEN.length}자)` : '❌ 없음', 'ACCESS_TOKEN': this.ACCESS_TOKEN ? `✅ 있음 (${this.ACCESS_TOKEN.length}자)` : '❌ 없음',
'DEBUG_MODE': this.DEBUG_MODE ? '✅ 활성화' : '❌ 비활성화', 'DEBUG_MODE': this.DEBUG_MODE ? '✅ 활성화' : '❌ 비활성화',
'사용자 ID': this.userId || '❌ 없음',
'현재 시간': new Date().toLocaleString() '현재 시간': new Date().toLocaleString()
}; };
@ -1284,7 +1089,7 @@ class BannedWordsManager {
return; return;
} }
this.debugLog('추가할 금지어 목록', { wordsToAdd, selectedGrade, count: wordsToAdd.length }); this.debugLog('추가할 금지어 목록 (background.js 경유)', { wordsToAdd, selectedGrade, count: wordsToAdd.length });
try { try {
confirmBtn.textContent = '🔄 추가 중...'; confirmBtn.textContent = '🔄 추가 중...';
@ -1295,61 +1100,24 @@ class BannedWordsManager {
throw new Error('로그인이 필요합니다'); throw new Error('로그인이 필요합니다');
} }
// 현재 사용자 정보 가져오기 // 사용자 ID 확인 (loadBannedWords에서 저장됨)
const authRes = await fetch(`${this.SUPABASE_URL}/auth/v1/user`, { const userId = this.userId;
method: 'GET', if (!userId) {
headers: { throw new Error('사용자 ID를 찾을 수 없습니다. 페이지를 새로고침해주세요.');
'Authorization': `Bearer ${this.ACCESS_TOKEN}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
'Accept': 'application/json'
} }
// background.js를 통해 기존 금지어 목록 조회 (중복 검사용)
const existingResponse = await chrome.runtime.sendMessage({
action: 'getExistingBannedWords',
token: this.ACCESS_TOKEN,
userId: userId
}); });
if (!authRes.ok) { if (!existingResponse || !existingResponse.success) {
throw new Error('사용자 인증 실패'); throw new Error(existingResponse?.error || '기존 금지어 목록 조회 실패');
} }
const authUser = await authRes.json(); const existingWords = existingResponse.existingWords;
// 사용자 ID 가져오기
const userRes = await fetch(`${this.SUPABASE_URL}/rest/v1/users?select=id&email=eq.${encodeURIComponent(authUser.email)}&limit=1`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.ACCESS_TOKEN}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (!userRes.ok) {
throw new Error('사용자 정보 조회 실패');
}
const userData = await userRes.json();
if (!userData || userData.length === 0) {
throw new Error('사용자 데이터를 찾을 수 없습니다');
}
const userId = userData[0].id;
// 기존 금지어 목록 가져오기 (중복 검사용)
const existingRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words?select=banned_word&user_id=eq.${userId}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.ACCESS_TOKEN}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (!existingRes.ok) {
throw new Error('기존 금지어 목록 조회 실패');
}
const existingWords = await existingRes.json();
const existingWordSet = new Set(existingWords.map(item => item.banned_word.toLowerCase())); const existingWordSet = new Set(existingWords.map(item => item.banned_word.toLowerCase()));
// 중복 검사 // 중복 검사
@ -1411,32 +1179,22 @@ class BannedWordsManager {
created_at: new Date().toISOString() created_at: new Date().toISOString()
})); }));
this.debugLog('금지어 배치 추가 API 호출', { count: wordsData.length, grade: selectedGrade }); this.debugLog('금지어 배치 추가 API 호출 (background.js 경유)', { count: wordsData.length, grade: selectedGrade });
const addRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words`, { // background.js를 통해 금지어 추가
method: 'POST', const addResponse = await chrome.runtime.sendMessage({
headers: { action: 'addBannedWords',
'Authorization': `Bearer ${this.ACCESS_TOKEN}`, token: this.ACCESS_TOKEN,
'apikey': this.SUPABASE_ANON_KEY, userId: userId,
'Content-Type': 'application/json', wordsData: wordsData
'Accept': 'application/json'
},
body: JSON.stringify(wordsData)
}); });
this.debugLog('금지어 추가 API 응답', { this.debugLog('금지어 추가 API 응답', {
status: addRes.status, success: addResponse?.success
statusText: addRes.statusText,
ok: addRes.ok
}); });
if (!addRes.ok) { if (!addResponse || !addResponse.success) {
const errorDetail = await addRes.text(); throw new Error(addResponse?.error || '금지어 추가 실패');
this.debugLog('금지어 추가 실패', {
status: addRes.status,
error: errorDetail
});
throw new Error(`금지어 추가 실패 (${addRes.status}: ${addRes.statusText})\n응답: ${errorDetail}`);
} }
this.debugLog('금지어 추가 성공', { newWords, selectedGrade }); this.debugLog('금지어 추가 성공', { newWords, selectedGrade });

View File

@ -110,8 +110,7 @@ function getSelectedTextAdvanced() {
return selectedText; return selectedText;
} }
} catch (e) { } catch (e) {
// 크로스 오리진 iframe은 접근 불가 // 크로스 오리진 iframe은 접근 불가 (로그 생략)
console.log('[content.js] iframe 접근 제한:', e.message);
} }
} }
@ -529,8 +528,11 @@ function setupIframeEventListeners() {
addKeyboardListeners(iframeDoc); addKeyboardListeners(iframeDoc);
} catch (e) { } catch (e) {
// SecurityError는 무시 (로그 출력 안함)
if (!e.message.includes('SecurityError') && !e.message.includes('Blocked a frame')) {
console.log('[content.js] iframe 이벤트 설정 실패:', e.message); console.log('[content.js] iframe 이벤트 설정 실패:', e.message);
} }
}
}); });
} }
@ -681,7 +683,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// 자동 찜하기 시작 // 자동 찜하기 시작
if (message.action === "startAutoZzim") { if (message.action === "startAutoZzim") {
console.log('[AutoZzim] 자동 찜하기 시작:', message); console.log('[AutoZzim] 자동 찜하기 시작:', message);
startAutoZzim(message.maxZzim || 50, message.delay || 1000); startAutoZzim(message.maxZzim || 50, message.delay || 300, message.zzimType, message.market);
sendResponse({ success: true }); sendResponse({ success: true });
return true; return true;
} }
@ -1430,111 +1432,12 @@ let autoZzimState = {
startTime: null startTime: null
}; };
// 찜하기 버튼 찾기 함수 (전역) - 정확한 선택자 사용 // 찜하기 버튼 찾기 함수 (전역) - Python 로직 기반 단순화
function findZzimButtons() { function findZzimButtons() {
console.log('[AutoZzim] 찜 버튼 찾기 시작...'); // Python: self.page.locator("div#CategoryProducts .zzim_button[type='button']")
const selector = "div#CategoryProducts .zzim_button[type='button']";
// 네이버 스마트스토어 정확한 선택자 사용 const buttons = document.querySelectorAll(selector);
const zzimButtonSelector = 'div#CategoryProducts button.zzim_button[aria-pressed="false"]'; return Array.from(buttons);
try {
const zzimButtons = document.querySelectorAll(zzimButtonSelector);
console.log(`[AutoZzim] 찜 가능한 버튼 발견: ${zzimButtons.length}`);
// 실제로 보이고 클릭 가능한 버튼만 필터링
const visibleButtons = Array.from(zzimButtons).filter(btn => {
const rect = btn.getBoundingClientRect();
const style = window.getComputedStyle(btn);
const isVisible = rect.width > 0 &&
rect.height > 0 &&
style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0' &&
!btn.disabled;
if (isVisible) {
// 버튼 주변에 다른 클릭 가능한 요소가 있는지 확인
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
// 버튼 중심 좌표에서 실제 클릭될 요소 확인
const elementAtPoint = document.elementFromPoint(centerX, centerY);
// 클릭될 요소가 찜 버튼이거나 찜 버튼의 자식 요소인지 확인
const isCorrectElement = elementAtPoint === btn || btn.contains(elementAtPoint);
if (!isCorrectElement) {
console.log('[AutoZzim] 버튼이 다른 요소에 가려짐:', {
button: btn,
elementAtPoint: elementAtPoint,
elementAtPointTag: elementAtPoint?.tagName,
elementAtPointClass: elementAtPoint?.className
});
return false;
}
// 버튼 텍스트 확인 (찜하기 관련 텍스트가 있는지)
const buttonText = btn.textContent?.trim().toLowerCase() || '';
const buttonAriaLabel = btn.getAttribute('aria-label')?.toLowerCase() || '';
const isZzimButton = buttonText.includes('찜') ||
buttonText.includes('하트') ||
buttonAriaLabel.includes('찜') ||
buttonAriaLabel.includes('wishlist') ||
btn.className.includes('zzim') ||
btn.className.includes('wish');
if (!isZzimButton) {
console.log('[AutoZzim] 찜하기 버튼이 아님:', {
text: buttonText,
ariaLabel: buttonAriaLabel,
className: btn.className
});
return false;
}
// 판매자 정보나 다른 기능 버튼이 아닌지 확인
const isNotOtherButton = !buttonText.includes('판매자') &&
!buttonText.includes('상점') &&
!buttonText.includes('문의') &&
!buttonText.includes('연락') &&
!buttonText.includes('전화') &&
!buttonText.includes('리뷰') &&
!buttonText.includes('평점') &&
!buttonAriaLabel.includes('판매자') &&
!buttonAriaLabel.includes('상점');
if (!isNotOtherButton) {
console.log('[AutoZzim] 다른 기능 버튼임:', {
text: buttonText,
ariaLabel: buttonAriaLabel
});
return false;
}
console.log('[AutoZzim] 찜 가능한 버튼 확인:', {
text: btn.textContent?.trim().substring(0, 30),
className: btn.className,
ariaPressed: btn.getAttribute('aria-pressed'),
position: `x:${Math.round(rect.x)}, y:${Math.round(rect.y)}`,
size: `${Math.round(rect.width)}x${Math.round(rect.height)}`,
isCorrectElement: isCorrectElement,
isZzimButton: isZzimButton,
isNotOtherButton: isNotOtherButton
});
}
return isVisible;
});
console.log(`[AutoZzim] 최종 찜 가능한 버튼: ${visibleButtons.length}`);
return visibleButtons;
} catch (error) {
console.error('[AutoZzim] 찜 버튼 찾기 오류:', error);
return [];
}
} }
// 이미 찜한 상품인지 확인 함수 (전역) - 단순화 // 이미 찜한 상품인지 확인 함수 (전역) - 단순화
@ -1556,302 +1459,42 @@ function isAlreadyZzimed_Improved(button) {
} }
} }
// 찜하기 버튼 클릭 함수 (전역) - 개선된 버전 // 찜하기 버튼 클릭 함수 (전역) - Python 로직 기반 단순화
async function clickZzimButton(button) { async function clickZzimButton(button) {
console.log('[AutoZzim] 찜 버튼 클릭 시작:', {
text: button.textContent?.trim().substring(0, 30),
className: button.className,
ariaPressed: button.getAttribute('aria-pressed'),
tagName: button.tagName,
id: button.id
});
return new Promise((resolve) => {
try { try {
// 클릭 전 상태 확인
const beforePressed = button.getAttribute('aria-pressed'); const beforePressed = button.getAttribute('aria-pressed');
console.log('[AutoZzim] 클릭 전 aria-pressed:', beforePressed); if (beforePressed === 'true' || button.disabled) {
return false;
if (beforePressed === 'true') {
console.log('[AutoZzim] 이미 찜된 상품, 건너뛰기');
resolve(false);
return;
} }
console.log('[AutoZzim] 찜 버튼 클릭 시도');
// 버튼이 클릭 가능한지 확인
if (button.disabled) {
console.log('[AutoZzim] 버튼이 비활성화되어 있음');
resolve(false);
return;
}
// 버튼 주변 요소 확인 (판매자 정보 등 다른 클릭 가능한 요소가 있는지)
const rect = button.getBoundingClientRect();
console.log('[AutoZzim] 버튼 위치 및 크기:', {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
centerX: rect.x + rect.width / 2,
centerY: rect.y + rect.height / 2
});
// 버튼이 화면에 보이도록 스크롤 (부드럽게)
button.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
setTimeout(() => {
if (!autoZzimState.isRunning) {
console.log('[AutoZzim] 찜하기가 중단됨');
resolve(false);
return;
}
// 버튼 클릭 시도 (이벤트 전파 방지)
try {
console.log('[AutoZzim] 정확한 버튼 클릭 시도 (이벤트 전파 방지)');
// 방법 1: 이벤트 전파를 막는 직접 클릭
const clickHandler = (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
console.log('[AutoZzim] 클릭 이벤트 전파 차단됨');
};
// 임시 이벤트 리스너 추가 (이벤트 전파 방지)
button.addEventListener('click', clickHandler, { capture: true, once: true });
// 포커스 설정
if (button.focus) {
button.focus();
}
// 직접 클릭 (가장 안전한 방법)
button.click(); button.click();
console.log('[AutoZzim] 직접 클릭 완료'); return new Promise((resolve) => {
let checks = 0;
// 클릭 후 상태 변화 확인 const interval = setInterval(() => {
let checkAttempts = 0;
const maxAttempts = 10; // 최대 5초간 확인
const checkStateChange = () => {
checkAttempts++;
const afterPressed = button.getAttribute('aria-pressed'); const afterPressed = button.getAttribute('aria-pressed');
if (afterPressed === 'true') {
console.log(`[AutoZzim] 상태 확인 ${checkAttempts}/${maxAttempts}:`, { clearInterval(interval);
before: beforePressed, console.log('[AutoZzim] 찜하기 성공');
after: afterPressed, if (typeof autoZzimState !== 'undefined') autoZzimState.actualZzimCount++;
success: beforePressed === 'false' && afterPressed === 'true'
});
// 성공 조건: false → true로 변경
if (beforePressed === 'false' && afterPressed === 'true') {
console.log('[AutoZzim] ✅ 찜하기 성공!');
autoZzimState.actualZzimCount++;
// 확인 모달 처리 (필요한 경우)
setTimeout(() => {
handleConfirmationModals();
}, 200);
resolve(true); resolve(true);
return; } else if (checks++ > 20) { // 2초 대기
clearInterval(interval);
// 실패해도 에러 아님, 단순히 false 반환
resolve(button.getAttribute('aria-pressed') === 'true');
} }
}, 100);
// 재시도 조건
if (checkAttempts < maxAttempts) {
setTimeout(checkStateChange, 500); // 0.5초마다 재확인
} else {
console.log('[AutoZzim] ❌ 찜하기 실패 (상태 변화 없음), 대안 방법 시도');
// 대안 방법: 더 정확한 이벤트 생성
try {
console.log('[AutoZzim] 대안 방법: 정확한 좌표로 클릭 이벤트 생성');
// 버튼의 정확한 중심 좌표 계산
const updatedRect = button.getBoundingClientRect();
const centerX = updatedRect.left + updatedRect.width / 2;
const centerY = updatedRect.top + updatedRect.height / 2;
// 더 정확한 마우스 이벤트 생성 (이벤트 전파 방지)
const mouseDownEvent = new MouseEvent('mousedown', {
bubbles: false, // 이벤트 전파 방지
cancelable: true,
view: window,
clientX: centerX,
clientY: centerY,
button: 0
}); });
} catch (error) {
const mouseUpEvent = new MouseEvent('mouseup', { console.error('[AutoZzim] 클릭 중 오류:', error);
bubbles: false, // 이벤트 전파 방지 return false;
cancelable: true,
view: window,
clientX: centerX,
clientY: centerY,
button: 0
});
const clickEvent = new MouseEvent('click', {
bubbles: false, // 이벤트 전파 방지
cancelable: true,
view: window,
clientX: centerX,
clientY: centerY,
button: 0
});
// 순차적으로 이벤트 발생
button.dispatchEvent(mouseDownEvent);
setTimeout(() => {
button.dispatchEvent(mouseUpEvent);
setTimeout(() => {
button.dispatchEvent(clickEvent);
// 최종 상태 확인
setTimeout(() => {
const finalPressed = button.getAttribute('aria-pressed');
if (beforePressed === 'false' && finalPressed === 'true') {
console.log('[AutoZzim] ✅ 대안 방법으로 찜하기 성공!');
autoZzimState.actualZzimCount++;
resolve(true);
} else {
console.log('[AutoZzim] ❌ 대안 방법도 실패');
resolve(false);
} }
}, 1000);
}, 50);
}, 50);
} catch (alternativeError) {
console.error('[AutoZzim] 대안 방법 실패:', alternativeError);
resolve(false);
}
}
};
// 첫 번째 상태 확인은 0.5초 후
setTimeout(checkStateChange, 500);
} catch (clickError) {
console.error('[AutoZzim] 버튼 클릭 오류:', clickError);
resolve(false);
}
}, 800); // 스크롤 후 충분한 대기 시간
} catch (e) {
console.error('[AutoZzim] 찜 버튼 클릭 중 예외 오류:', e);
resolve(false);
}
});
} }
// 확인 모달/팝업 처리 함수 (전역) - 개선된 버전 // 확인 모달/팝업 처리 함수 (전역) - Python 로직 기반 단순화
function handleConfirmationModals() { function handleConfirmationModals() {
console.log('[AutoZzim] 확인 모달 처리 시작'); // Python 코드에서는 모달 처리 로직이 없으므로 빈 함수로 둡니다.
// 필요하다면 여기에 팝업 닫기 로직을 추가할 수 있습니다.
// 찜하기 관련 확인 모달만 처리하도록 제한
const confirmSelectors = [
// 네이버 스마트스토어 찜하기 전용 확인 버튼
'button[data-testid="wishlist-confirm"]',
'button[data-testid="zzim-confirm"]',
'.zzim_confirm_btn',
'.wishlist_confirm_btn',
// 일반적인 확인 버튼 (찜하기 관련 텍스트가 있는 경우만)
'button[class*="confirm"]:contains("찜")',
'button[class*="confirm"]:contains("확인")',
// 모달 내부의 확인 버튼 (찜하기 관련만)
'.modal button[class*="confirm"]',
'.popup button[class*="confirm"]',
'.dialog button[class*="confirm"]',
// 레이어 팝업 내 확인 버튼
'[class*="layer"] button[class*="confirm"]',
'[role="dialog"] button[class*="primary"]'
];
for (const selector of confirmSelectors) {
try {
let confirmButtons = [];
// :contains() 선택자 수동 처리
if (selector.includes(':contains(')) {
const [baseSelector, containsText] = selector.split(':contains(');
const searchText = containsText.replace(/[\(\)\"\']/g, '');
const candidateElements = document.querySelectorAll(baseSelector);
confirmButtons = Array.from(candidateElements).filter(el => {
const text = el.textContent && el.textContent.trim();
return text && text.includes(searchText);
});
} else {
confirmButtons = Array.from(document.querySelectorAll(selector));
}
// 보이고 클릭 가능한 버튼 중에서 찜하기 관련만 필터링
for (const btn of confirmButtons) {
if (btn && btn.offsetParent !== null && !btn.disabled) {
const rect = btn.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
// 버튼 텍스트 확인 - 찜하기 관련 텍스트가 있는지 확인
const buttonText = btn.textContent?.trim().toLowerCase() || '';
const isZzimRelated = buttonText.includes('찜') ||
buttonText.includes('확인') ||
buttonText.includes('ok') ||
buttonText.includes('예') ||
buttonText.includes('yes');
// 판매자 정보나 다른 팝업이 아닌지 확인
const isNotSellerInfo = !buttonText.includes('판매자') &&
!buttonText.includes('상점') &&
!buttonText.includes('업체') &&
!buttonText.includes('문의') &&
!buttonText.includes('연락') &&
!buttonText.includes('전화');
if (isZzimRelated && isNotSellerInfo) {
console.log('[AutoZzim] 찜하기 관련 확인 버튼 클릭:', {
selector: selector,
text: buttonText,
className: btn.className
});
// 이벤트 전파를 막고 클릭
const clickHandler = (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
};
btn.addEventListener('click', clickHandler, { capture: true, once: true });
btn.click();
return; // 첫 번째 적절한 확인 버튼만 클릭하고 종료
} else {
console.log('[AutoZzim] 찜하기와 무관한 버튼 무시:', {
text: buttonText,
isZzimRelated: isZzimRelated,
isNotSellerInfo: isNotSellerInfo
});
}
}
}
}
} catch (e) {
console.log('[AutoZzim] 확인 버튼 검색 오류:', selector, e.message);
}
}
console.log('[AutoZzim] 찜하기 관련 확인 버튼을 찾지 못했습니다');
} }
// 현재 페이지 찜하기 처리 함수 (전역) - 개선된 버전 // 현재 페이지 찜하기 처리 함수 (전역) - 개선된 버전
@ -1900,66 +1543,120 @@ function handleConfirmationModals() {
return processedInPage > 0; return processedInPage > 0;
} }
// 자동 찜하기 메인 루프 함수 (전역) // 모든 상품 로딩을 위한 스크롤 함수 (Lazy Loading 대응)
async function scrollToLoadAllProducts() {
console.log('[AutoZzim] 상품 로딩을 위해 스크롤 실행');
updateZzimStatus(autoZzimState.statusDiv, '상품 로딩 중... (스크롤)');
// 현재 높이
let lastHeight = document.body.scrollHeight;
let currentPos = 0;
const step = window.innerHeight;
// 끝까지 스크롤
while (true) {
currentPos += step;
window.scrollTo(0, currentPos);
await new Promise(r => setTimeout(r, 200)); // 0.2초 대기
if (currentPos >= document.body.scrollHeight) {
// 끝에 도달했으면 잠시 대기 후 높이 변화 확인
await new Promise(r => setTimeout(r, 1000));
// 높이가 늘어났으면 계속 진행, 아니면 종료
if (document.body.scrollHeight <= lastHeight + 100) { // 약간의 오차 허용
break;
}
lastHeight = document.body.scrollHeight;
}
}
// 맨 위로 복귀 (찜하기는 위에서부터 순차적으로)
window.scrollTo(0, 0);
await new Promise(r => setTimeout(r, 500));
console.log('[AutoZzim] 스크롤 완료');
}
// 자동 찜하기 메인 루프 (Python 로직 이식)
async function autoZzimMainLoop() { async function autoZzimMainLoop() {
console.log('[AutoZzim] 메인 루프 시작'); console.log('[AutoZzim] 메인 루프 시작 (Python Logic)');
if (!autoZzimState.isRunning) return;
while (autoZzimState.isRunning && autoZzimState.actualZzimCount < autoZzimState.maxZzim && autoZzimState.currentPage <= 10) { while (autoZzimState.isRunning && autoZzimState.actualZzimCount < autoZzimState.maxZzim) {
console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage} 처리 시작`); // 0. 스크롤하여 모든 상품 로드 (Lazy Loading 대응)
await scrollToLoadAllProducts();
// 페이지 로드 대기 // 1. 현재 페이지의 버튼 찾기
await new Promise(resolve => setTimeout(resolve, 2000)); const buttons = findZzimButtons();
console.log(`[AutoZzim] 현재 페이지 발견 버튼: ${buttons.length}`);
// 현재 페이지 처리 // 2. 버튼 순회하며 클릭
const hasProducts = await processCurrentPage(); for (const button of buttons) {
if (!autoZzimState.isRunning || autoZzimState.actualZzimCount >= autoZzimState.maxZzim) break;
if (!hasProducts) { const success = await clickZzimButton(button);
console.log(`[AutoZzim] 페이지 ${autoZzimState.currentPage}에 더 이상 찜할 상품이 없음`); if (success) {
updateZzimStatus(autoZzimState.statusDiv, `찜하기 진행 중... (${autoZzimState.actualZzimCount}/${autoZzimState.maxZzim})`);
// 다음 페이지로 이동 시도 // Random delay: Base delay +/- 50%
updateZzimStatus(autoZzimState.statusDiv, `다음 페이지로 이동 중... (${autoZzimState.actualZzimCount}/${autoZzimState.maxZzim})`); // Min: delay * 0.5, Max: delay * 1.5
const baseDelay = autoZzimState.delay;
const randomFactor = 0.5 + Math.random(); // 0.5 ~ 1.5
const waitTime = Math.round(baseDelay * randomFactor);
console.log(`[AutoZzim] 대기: ${waitTime}ms (설정값: ${baseDelay}ms, 범위: ${Math.round(baseDelay*0.5)}~${Math.round(baseDelay*1.5)}ms)`);
await new Promise(r => setTimeout(r, waitTime));
}
}
// 3. 목표 달성 확인
if (autoZzimState.actualZzimCount >= autoZzimState.maxZzim) break;
// 4. 다음 페이지 이동
const moved = await goToNextPage(); const moved = await goToNextPage();
if (!moved) { if (!moved) {
console.log('[AutoZzim] 더 이상 다음 페이지가 없음'); console.log('[AutoZzim] 더 이상 페이지가 없거나 이동 실패');
break; break;
} }
} else {
// 현재 페이지에서 더 찜할 수 있는지 확인 // 페이지 로드 후 추가 대기 (Python: time.sleep(2))
const remainingButtons = findZzimButtons(); await new Promise(r => setTimeout(r, 2000));
if (remainingButtons.length === 0) {
// 다음 페이지로 이동
updateZzimStatus(autoZzimState.statusDiv, `다음 페이지로 이동 중... (${autoZzimState.actualZzimCount}/${autoZzimState.maxZzim})`);
const moved = await goToNextPage();
if (!moved) {
console.log('[AutoZzim] 더 이상 다음 페이지가 없음');
break;
}
}
} }
// 페이지 간 대기 console.log('[AutoZzim] 작업 완료');
await new Promise(resolve => setTimeout(resolve, 2000)); updateZzimStatus(autoZzimState.statusDiv, `완료! 총 ${autoZzimState.actualZzimCount}개 찜함`);
}
// 완료 처리
console.log(`[AutoZzim] 찜하기 완료: 실제 ${autoZzimState.actualZzimCount}개 찜함`);
updateZzimStatus(autoZzimState.statusDiv, `찜하기 완료! (실제 ${autoZzimState.actualZzimCount}개 찜함)`);
// 상태 초기화
autoZzimState.isRunning = false; autoZzimState.isRunning = false;
// 5초 후 상태 UI 제거 // 작업 완료 후 백그라운드 스크립트로 통계 업데이트 요청
console.log(`[AutoZzim] 통계 업데이트 요청: ${autoZzimState.actualZzimCount}`);
// 1. 통계 업데이트
chrome.runtime.sendMessage({
action: 'updateZzimStats',
count: autoZzimState.actualZzimCount,
zzimType: autoZzimState.zzimType || 'my_market',
market: autoZzimState.market // 마켓 정보 추가 전달 (품앗이 시 owner_id 식별용)
});
// 2. 작업 완료 메시지 (탭 닫기 등)
setTimeout(() => {
chrome.runtime.sendMessage({
action: 'zzimComplete',
count: autoZzimState.actualZzimCount
});
}, 1000); // 통계 업데이트 메시지 전송 후 잠시 대기
setTimeout(() => { setTimeout(() => {
if (autoZzimState.statusDiv && autoZzimState.statusDiv.parentNode) { if (autoZzimState.statusDiv && autoZzimState.statusDiv.parentNode) {
autoZzimState.statusDiv.parentNode.removeChild(autoZzimState.statusDiv); autoZzimState.statusDiv.parentNode.removeChild(autoZzimState.statusDiv);
autoZzimState.statusDiv = null; autoZzimState.statusDiv = null;
} }
}, 5000); }, 5000);
} }
// 자동 찜하기 기능 (개선된 버전) // 자동 찜하기 기능 (개선된 버전)
function startAutoZzim(maxZzim = 50, delay = 1000) { function startAutoZzim(maxZzim = 50, delay = 300, zzimType = 'my_market', market = null) {
console.log('[AutoZzim] 자동 찜하기 시작:', { maxZzim, delay }); console.log('[AutoZzim] 자동 찜하기 시작:', { maxZzim, delay, zzimType });
// 상태 초기화 // 상태 초기화
autoZzimState = { autoZzimState = {
@ -1969,20 +1666,29 @@ function startAutoZzim(maxZzim = 50, delay = 1000) {
currentPage: 1, currentPage: 1,
delay: delay, delay: delay,
statusDiv: null, statusDiv: null,
startTime: Date.now() startTime: Date.now(),
zzimType: zzimType, // 타입 저장
market: market // 마켓 정보 저장
}; };
// 상태 표시 UI 생성 // 상태 표시 UI 생성
autoZzimState.statusDiv = createZzimStatusUI(); autoZzimState.statusDiv = createZzimStatusUI();
updateZzimStatus(autoZzimState.statusDiv, `찜하기 시작... (최대 ${maxZzim}개)`); updateZzimStatus(autoZzimState.statusDiv, `찜하기 시작... (최대 ${maxZzim}개)`);
// 60초 후 자동 종료 // 30분 후 자동 종료 (안전장치)
setTimeout(() => { setTimeout(() => {
if (autoZzimState.isRunning) { if (autoZzimState.isRunning) {
autoZzimState.isRunning = false; autoZzimState.isRunning = false;
console.log('[AutoZzim] 시간 초과로 찜하기 종료'); console.log('[AutoZzim] 시간 초과로 찜하기 종료');
updateZzimStatus(autoZzimState.statusDiv, `시간 초과로 종료 (실제 ${autoZzimState.actualZzimCount}개 찜함)`); updateZzimStatus(autoZzimState.statusDiv, `시간 초과로 종료 (실제 ${autoZzimState.actualZzimCount}개 찜함)`);
// 시간 초과 종료 메시지 전송 (탭 닫기 요청 포함)
chrome.runtime.sendMessage({
action: 'zzimComplete',
count: autoZzimState.actualZzimCount,
reason: 'timeout'
});
// 5초 후 상태 UI 제거 // 5초 후 상태 UI 제거
setTimeout(() => { setTimeout(() => {
if (autoZzimState.statusDiv && autoZzimState.statusDiv.parentNode) { if (autoZzimState.statusDiv && autoZzimState.statusDiv.parentNode) {
@ -1991,232 +1697,60 @@ function startAutoZzim(maxZzim = 50, delay = 1000) {
} }
}, 5000); }, 5000);
} }
}, 60000); }, 1800000);
// 메인 루프 시작 // 메인 루프 시작
setTimeout(autoZzimMainLoop, 2000); // 페이지 로드 후 2초 대기 setTimeout(autoZzimMainLoop, 2000); // 페이지 로드 후 2초 대기
} }
// 다음 페이지로 이동 함수 (기존 goToNextPage 함수 수정) // 다음 페이지 이동 (Python Logic)
async function goToNextPage() { async function goToNextPage() {
try { console.log('[AutoZzim] 다음 페이지 이동 시도');
console.log(`[AutoZzim] 다음 페이지로 이동 시도: ${autoZzimState.currentPage}${autoZzimState.currentPage + 1}`);
// 네이버 스마트스토어 URL 분석 // Python: self.page.locator("div#CategoryProducts div[data-shp-area-id='pgn']")
const urlInfo = parseNaverSmartStoreUrl(window.location.href); const paginationContainer = document.querySelector("div#CategoryProducts div[data-shp-area-id='pgn']");
if (!paginationContainer) {
if (!urlInfo) { console.log('[AutoZzim] 페이지네이션 컨테이너를 찾을 수 없음');
console.error('[AutoZzim] 네이버 스마트스토어 URL이 아닙니다:', window.location.href);
return false; return false;
} }
if (!urlInfo.isProductListPage) { // 현재 페이지 찾기 (aria-current='true')
console.log('[AutoZzim] 상품 목록 페이지가 아님, 올바른 페이지로 이동'); const currentBtn = paginationContainer.querySelector("a[aria-current='true'][role='menuitem']");
if (!currentBtn) {
// 올바른 상품 목록 페이지 URL 생성 console.log('[AutoZzim] 현재 페이지 버튼을 찾을 수 없음');
const correctUrl = generateCorrectProductListUrl(urlInfo.storeName, urlInfo.origin, false, 50);
console.log('[AutoZzim] 올바른 상품 목록 페이지로 이동:', correctUrl);
window.location.href = correctUrl;
return true;
}
// 1단계: 현재 페이지 번호 확인
const currentPageButton = document.querySelector(`div#MAIN_CONTENT_ROOT_ID [aria-current='true'][data-shp-contents-id]`);
let actualCurrentPage = 1;
if (currentPageButton) {
const currentPageId = currentPageButton.getAttribute('data-shp-contents-id');
actualCurrentPage = parseInt(currentPageId) || 1;
console.log(`[AutoZzim] 현재 페이지 확인: ${actualCurrentPage} (DOM에서 감지)`);
} else {
// aria-current 없이 data-shp-contents-id로만 확인
const allPageButtons = document.querySelectorAll(`div#MAIN_CONTENT_ROOT_ID [data-shp-contents-id]`);
console.log(`[AutoZzim] 전체 페이지 버튼 개수: ${allPageButtons.length}`);
// URL의 cp 파라미터로 현재 페이지 추정
const urlParams = new URLSearchParams(window.location.search);
const cpParam = urlParams.get('cp');
if (cpParam) {
actualCurrentPage = parseInt(cpParam) || 1;
console.log(`[AutoZzim] URL 파라미터에서 현재 페이지 확인: ${actualCurrentPage}`);
}
}
// currentPage 변수 업데이트
autoZzimState.currentPage = actualCurrentPage;
const nextPageNum = autoZzimState.currentPage + 1;
console.log(`[AutoZzim] 다음 페이지 번호: ${nextPageNum}`);
// 2단계: 다음 페이지 버튼 찾기 (네이버 스마트스토어 특화)
const nextPageSelectors = [
// 네이버 스마트스토어 페이지 버튼 (가장 정확한 방법)
`div#MAIN_CONTENT_ROOT_ID [data-shp-contents-id='${nextPageNum}']`,
// 일반적인 다음 페이지 버튼
'a.pagination_next:not(.pagination_disabled)',
'a[class*="pagination"][class*="next"]:not([class*="disabled"])',
'button[class*="pagination"][class*="next"]:not([disabled])',
'.paginate_next:not(.disabled)',
'[class*="paging"] a[class*="next"]:not([class*="disabled"])',
// aria-label 기반
'a[aria-label*="다음"]:not([aria-disabled="true"])',
'button[aria-label*="다음"]:not([disabled])',
'a[title*="다음"]:not([class*="disabled"])',
// 페이지 번호로 다음 페이지 찾기 (URL 기반)
`a[href*="cp=${nextPageNum}"]`,
`button[data-page="${nextPageNum}"]`,
`a[href*="page=${nextPageNum}"]`
];
let nextButton = null;
let selectedMethod = '';
for (const selector of nextPageSelectors) {
try {
const buttons = document.querySelectorAll(selector);
console.log(`[AutoZzim] 선택자 "${selector}" 검색 결과: ${buttons.length}`);
for (const btn of buttons) {
if (btn && btn.offsetParent !== null && !btn.disabled && !btn.classList.contains('disabled')) {
nextButton = btn;
selectedMethod = selector;
console.log(`[AutoZzim] 다음 페이지 버튼 발견:`, {
selector: selector,
text: btn.textContent?.trim(),
className: btn.className,
dataShpContentsId: btn.getAttribute('data-shp-contents-id'),
href: btn.href,
tagName: btn.tagName
});
break;
}
}
if (nextButton) break;
} catch (e) {
console.log(`[AutoZzim] 선택자 오류 (${selector}):`, e.message);
}
}
if (nextButton) {
// 다음 페이지 버튼 클릭
console.log(`[AutoZzim] 다음 페이지 버튼 클릭 시도 (방법: ${selectedMethod})`);
// 버튼이 화면에 보이도록 스크롤
nextButton.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
await new Promise(resolve => setTimeout(resolve, 1000));
// 클릭 전 상태 로그
console.log('[AutoZzim] 클릭 전 버튼 상태:', {
disabled: nextButton.disabled,
offsetParent: nextButton.offsetParent !== null,
classList: Array.from(nextButton.classList)
});
// 다양한 방식으로 클릭 시도
try {
// 방법 1: 일반 클릭
nextButton.click();
console.log('[AutoZzim] 일반 클릭 완료');
} catch (e1) {
console.log('[AutoZzim] 일반 클릭 실패, 이벤트 방식 시도:', e1.message);
try {
// 방법 2: 마우스 이벤트 시뮬레이션
const rect = nextButton.getBoundingClientRect();
const clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
clientX: rect.left + rect.width / 2,
clientY: rect.top + rect.height / 2
});
nextButton.dispatchEvent(clickEvent);
console.log('[AutoZzim] 마우스 이벤트 클릭 완료');
} catch (e2) {
console.log('[AutoZzim] 마우스 이벤트 클릭 실패:', e2.message);
// 방법 3: href가 있는 경우 직접 이동
if (nextButton.href) {
console.log('[AutoZzim] href로 직접 이동:', nextButton.href);
window.location.href = nextButton.href;
}
}
}
// 페이지 로드 대기
console.log('[AutoZzim] 페이지 로드 대기 중...');
await new Promise(resolve => setTimeout(resolve, 3000));
// 페이지 변경 확인
const newPageButton = document.querySelector(`div#MAIN_CONTENT_ROOT_ID [aria-current='true'][data-shp-contents-id]`);
if (newPageButton) {
const newPageId = newPageButton.getAttribute('data-shp-contents-id');
const newPageNum = parseInt(newPageId) || 1;
if (newPageNum > actualCurrentPage) {
console.log(`[AutoZzim] ✅ 페이지 이동 성공: ${actualCurrentPage}${newPageNum}`);
autoZzimState.currentPage = newPageNum;
return true;
} else {
console.log(`[AutoZzim] ❌ 페이지 이동 실패: 여전히 ${newPageNum} 페이지`);
}
} else {
// aria-current가 없어도 URL로 확인
const urlParams = new URLSearchParams(window.location.search);
const newCpParam = urlParams.get('cp');
if (newCpParam && parseInt(newCpParam) > actualCurrentPage) {
const newPageNum = parseInt(newCpParam);
console.log(`[AutoZzim] ✅ 페이지 이동 성공 (URL 확인): ${actualCurrentPage}${newPageNum}`);
autoZzimState.currentPage = newPageNum;
return true;
}
}
return false; return false;
} }
// 3단계: 버튼이 없으면 URL 직접 변경 const currentPage = parseInt(currentBtn.textContent.trim());
console.log('[AutoZzim] 다음 페이지 버튼이 없음, URL 직접 변경 시도'); if (isNaN(currentPage)) return false;
// 마지막 페이지 번호 확인 const nextPageNum = currentPage + 1;
const allPageButtons = document.querySelectorAll(`div#MAIN_CONTENT_ROOT_ID [data-shp-contents-id]`); console.log(`[AutoZzim] 현재 페이지: ${currentPage}, 다음 페이지: ${nextPageNum}`);
const pageNumbers = Array.from(allPageButtons).map(btn => {
const id = btn.getAttribute('data-shp-contents-id');
return parseInt(id) || 0;
}).filter(num => num > 0);
const maxPage = Math.max(...pageNumbers); // 다음 페이지 버튼 찾기 (숫자 버튼 또는 '다음')
console.log(`[AutoZzim] 감지된 페이지 번호들:`, pageNumbers); const menuItems = Array.from(paginationContainer.querySelectorAll("[role='menuitem']"));
console.log(`[AutoZzim] 최대 페이지 번호: ${maxPage}`);
if (nextPageNum > maxPage) { // 1. 숫자 버튼 찾기
console.log(`[AutoZzim] 다음 페이지 ${nextPageNum}이 최대 페이지 ${maxPage}를 초과함`); let nextBtn = menuItems.find(el => el.textContent.trim() === nextPageNum.toString());
return false;
// 2. 없으면 '다음' 버튼 찾기
if (!nextBtn) {
nextBtn = menuItems.find(el => el.textContent.trim().includes('다음'));
} }
// URL 직접 변경 if (nextBtn) {
const currentUrl = new URL(window.location.href); console.log('[AutoZzim] 다음 페이지 버튼 클릭');
currentUrl.searchParams.set('cp', nextPageNum.toString()); nextBtn.click();
// Python: time.sleep(3)
await new Promise(r => setTimeout(r, 3000));
console.log(`[AutoZzim] URL 직접 변경: cp=${actualCurrentPage} → cp=${nextPageNum}`); // 페이지 번호 업데이트
console.log('[AutoZzim] 다음 페이지 URL:', currentUrl.href); autoZzimState.currentPage = nextPageNum;
window.location.href = currentUrl.href;
return true; return true;
} catch (e) {
console.error('[AutoZzim] 다음 페이지 이동 오류:', e);
return false;
} }
console.log('[AutoZzim] 다음 페이지 버튼을 찾을 수 없음');
return false;
} }
function createZzimStatusUI() { function createZzimStatusUI() {
@ -2291,6 +1825,25 @@ function waitForPageLoad() {
}); });
} }
// 팝업/알림 차단 스크립트 주입 (페이지 컨텍스트)
function injectPopupSuppressor() {
const script = document.createElement('script');
script.textContent = `
window.alert = function(msg) { console.log('[AutoZzim] Alert blocked:', msg); return true; };
window.confirm = function(msg) { console.log('[AutoZzim] Confirm blocked:', msg); return true; };
window.prompt = function(msg) { console.log('[AutoZzim] Prompt blocked:', msg); return null; };
// 네이버 스마트스토어 전용 모달 차단 시도
if (window.$ && window.$.fancybox) {
try { window.$.fancybox.close(); } catch(e) {}
window.$.fancybox.open = function() { console.log('[AutoZzim] Fancybox blocked'); return; };
}
`;
(document.head || document.documentElement).appendChild(script);
// 스크립트를 제거하지 않고 유지하여 지속적인 차단 효과 기대 (필요시)
// setTimeout(() => script.remove(), 100);
console.log('[AutoZzim] 팝업 차단 스크립트 주입 완료');
}
// 페이지 로드 시 URL 파라미터 확인 (개선된 버전) // 페이지 로드 시 URL 파라미터 확인 (개선된 버전)
async function checkAutoZzimParam() { async function checkAutoZzimParam() {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
@ -2306,6 +1859,9 @@ async function checkAutoZzimParam() {
if (autoZzim === 'true') { if (autoZzim === 'true') {
console.log('[AutoZzim] URL 파라미터로 자동 찜하기 시작'); console.log('[AutoZzim] URL 파라미터로 자동 찜하기 시작');
// 팝업 차단 스크립트 주입
injectPopupSuppressor();
// 네이버 스마트스토어 URL 분석 // 네이버 스마트스토어 URL 분석
const urlInfo = parseNaverSmartStoreUrl(window.location.href); const urlInfo = parseNaverSmartStoreUrl(window.location.href);
@ -2812,7 +2368,11 @@ detectEnvironment();
// iframe 환경에서도 이벤트 리스너 설정 // iframe 환경에서도 이벤트 리스너 설정
if (isIframeEnvironment) { if (isIframeEnvironment) {
try {
addKeyboardListeners(window.parent.document); addKeyboardListeners(window.parent.document);
} catch (e) {
// Cross-origin parent 접근 실패 시 무시
}
} }
// iframe 이벤트 설정 // iframe 이벤트 설정

View File

@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "내차는언제타냐 통합 확장", "name": "내차는언제타냐 통합 확장",
"version": "1.2", "version": "1.3",
"description": "드래그한 텍스트를 우클릭 → '지재권 검색'으로 MarkInfo 검색을 수행하고 결과를 툴팁으로 표시합니다.", "description": "드래그한 텍스트를 우클릭 → '지재권 검색'으로 MarkInfo 검색을 수행하고 결과를 툴팁으로 표시합니다.",
"permissions": [ "permissions": [
"storage", "storage",

175
popup.js
View File

@ -20,16 +20,8 @@ async function getBackendConfig() {
} }
} catch (error) { } catch (error) {
console.error('[popup.js] 백엔드 설정 로드 실패:', error); console.error('[popup.js] 백엔드 설정 로드 실패:', error);
console.log('[popup.js] 폴백 설정 사용'); // 폴백 설정 없이 에러 전파 - background.js에서만 설정 관리
throw new Error('백엔드 설정을 가져올 수 없습니다. 확장 프로그램을 다시 로드해주세요.');
// 폴백 설정
const fallbackConfig = {
SUPABASE_URL: "https://ko.wrmc.cc",
SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU"
};
console.log('[popup.js] 폴백 설정:', fallbackConfig);
return fallbackConfig;
} }
} }
@ -1244,182 +1236,75 @@ async function loadUserInfo(token) {
await logger.log(LOG_LEVELS.INFO, '사용자 정보 요청 중...'); await logger.log(LOG_LEVELS.INFO, '사용자 정보 요청 중...');
try { try {
// 백엔드 설정 가져오기 // Background Script에 사용자 정보 요청
const { SUPABASE_URL, SUPABASE_ANON_KEY } = await getBackendConfig(); const response = await chrome.runtime.sendMessage({
action: 'getUserInfo',
// 먼저 Auth API로 사용자 기본 정보 가져오기 token: token
const authUrl = `${SUPABASE_URL}/auth/v1/user`;
await logger.log(LOG_LEVELS.DEBUG, '사용자 Auth 정보 API 호출', { authUrl });
const authRes = await fetch(authUrl, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
}); });
if (!authRes.ok) { if (!response || !response.success) {
// Auth API 응답 오류 처리 throw new Error(response?.error || '사용자 정보를 불러올 수 없습니다.');
let authErrorDetails;
try {
authErrorDetails = await authRes.json();
} catch {
authErrorDetails = await authRes.text();
} }
await logger.log(LOG_LEVELS.ERROR, 'Auth API 응답 오류', { const { user } = response;
status: authRes.status,
statusText: authRes.statusText,
authErrorDetails
});
throw new Error(`Auth API 오류 - HTTP ${authRes.status}: ${authRes.statusText}`); await logger.log(LOG_LEVELS.DEBUG, '사용자 정보 응답', { user });
}
const authUser = await authRes.json();
await logger.log(LOG_LEVELS.DEBUG, 'Auth API 응답 데이터', { authUser });
// 사용자 상세 정보와 membership_levels 조인해서 가져오기
let userDetails = null;
try {
// membership_levels 테이블과 조인하여 api_call_limit도 함께 가져오기
const detailsUrl = `${SUPABASE_URL}/rest/v1/users?select=*,membership_levels(level,api_call_limit)&email=eq.${encodeURIComponent(authUser.email)}&limit=1`;
await logger.log(LOG_LEVELS.DEBUG, '사용자 상세 정보 API 호출 (조인)', { detailsUrl });
const detailsRes = await fetch(detailsUrl, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (detailsRes.ok) {
const detailsData = await detailsRes.json();
await logger.log(LOG_LEVELS.DEBUG, '사용자 상세 정보 응답 (조인)', { detailsData });
userDetails = detailsData[0];
} else {
// 조인이 실패하면 기본 users 테이블만 조회
await logger.log(LOG_LEVELS.WARN, '조인 쿼리 실패, 기본 사용자 정보만 조회', {
status: detailsRes.status
});
const fallbackUrl = `${SUPABASE_URL}/rest/v1/users?select=*&email=eq.${encodeURIComponent(authUser.email)}&limit=1`;
const fallbackRes = await fetch(fallbackUrl, {
headers: {
Authorization: `Bearer ${token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (fallbackRes.ok) {
const fallbackData = await fallbackRes.json();
await logger.log(LOG_LEVELS.DEBUG, '기본 사용자 정보 응답', { fallbackData });
userDetails = fallbackData[0];
}
}
} catch (detailsError) {
await logger.log(LOG_LEVELS.WARN, '사용자 상세 정보 조회 중 오류', {
error: detailsError.message
});
}
// UI 업데이트 // UI 업데이트
updateDebugInfo('✅ 사용자 정보 로드 완료'); updateDebugInfo('✅ 사용자 정보 로드 완료');
await logger.log(LOG_LEVELS.INFO, '사용자 정보 로드 성공', {
email: authUser.email,
hasDetails: !!userDetails,
membershipLevel: userDetails?.membership_level,
membershipLevels: userDetails?.membership_levels
});
// 사용자 ID를 storage에 저장 (금지어 추가 시 사용) // 사용자 ID를 storage에 저장
if (userDetails && userDetails.id) { if (user.id) {
// API 한도 정보도 함께 저장
let apiLimit = null; let apiLimit = null;
if (userDetails.membership_levels) { if (user.membership_levels) {
apiLimit = userDetails.membership_levels.api_call_limit; apiLimit = user.membership_levels.api_call_limit;
} }
// 필요한 정보 저장
const { SUPABASE_URL, SUPABASE_ANON_KEY } = await getBackendConfig();
await chrome.storage.local.set({ await chrome.storage.local.set({
user_id: userDetails.id, user_id: user.id,
user_email: authUser.email, user_email: user.email,
user_membership_level: userDetails.membership_level, user_membership_level: user.membership_level,
user_api_limit: apiLimit, user_api_limit: apiLimit,
user_current_api_calls: userDetails.current_api_calls || 0, user_current_api_calls: user.current_api_calls || 0,
// 백엔드 설정도 저장 (다른 페이지에서 사용할 수 있도록)
SUPABASE_URL, SUPABASE_URL,
SUPABASE_ANON_KEY SUPABASE_ANON_KEY
}); });
await logger.log(LOG_LEVELS.INFO, '사용자 정보 저장 완료', {
user_id: userDetails.id,
email: authUser.email,
membership_level: userDetails.membership_level,
api_limit: apiLimit,
current_api_calls: userDetails.current_api_calls || 0
});
} }
document.getElementById("login-section").style.display = "none"; document.getElementById("login-section").style.display = "none";
document.getElementById("user-info-section").style.display = "block"; document.getElementById("user-info-section").style.display = "block";
document.getElementById("user-email").textContent = authUser.email; document.getElementById("user-email").textContent = user.email;
// 상세 정보 표시 // 상세 정보 표시
if (userDetails) {
// 회원등급 표시 (membership_levels 조인 데이터 또는 membership_level 필드)
let levelText = "기본"; let levelText = "기본";
let apiLimit = null; let apiLimit = null;
if (userDetails.membership_levels) { if (user.membership_levels) {
// 조인된 데이터가 있는 경우 levelText = user.membership_levels.level || "기본";
levelText = userDetails.membership_levels.level || "기본"; apiLimit = user.membership_levels.api_call_limit;
apiLimit = userDetails.membership_levels.api_call_limit; } else if (user.membership_level) {
} else if (userDetails.membership_level) { levelText = user.membership_level;
// 직접 필드가 있는 경우
levelText = userDetails.membership_level;
} }
document.getElementById("user-level").textContent = levelText; document.getElementById("user-level").textContent = levelText;
// Chrome Storage에서 최신 호출량 정보 가져오기 (실시간 반영)
const { user_current_api_calls } = await chrome.storage.local.get(['user_current_api_calls']); const { user_current_api_calls } = await chrome.storage.local.get(['user_current_api_calls']);
const currentCalls = user_current_api_calls || userDetails.current_api_calls || 0; const currentCalls = user_current_api_calls || user.current_api_calls || 0;
// 오늘 호출량 표시 (current_api_calls/api_call_limit 형태)
let usageText = currentCalls.toString(); let usageText = currentCalls.toString();
if (apiLimit !== null && apiLimit !== undefined) { if (apiLimit !== null && apiLimit !== undefined) {
usageText = `${currentCalls}/${apiLimit}`; usageText = `${currentCalls}/${apiLimit}`;
} }
document.getElementById("user-usage").textContent = usageText; document.getElementById("user-usage").textContent = usageText;
// 만료일 표시 document.getElementById("user-expire").textContent = user.payment_period_end
document.getElementById("user-expire").textContent = userDetails.payment_period_end ? new Date(user.payment_period_end).toLocaleDateString()
? new Date(userDetails.payment_period_end).toLocaleDateString()
: "없음"; : "없음";
await logger.log(LOG_LEVELS.INFO, '사용자 정보 UI 업데이트 완료', {
level: levelText,
usage: usageText,
apiLimit: apiLimit,
currentCallsFromStorage: user_current_api_calls,
currentCallsFromDB: userDetails.current_api_calls
});
} else {
// 상세 정보가 없는 경우 기본값
document.getElementById("user-level").textContent = "기본";
document.getElementById("user-usage").textContent = "0";
document.getElementById("user-expire").textContent = "없음";
await logger.log(LOG_LEVELS.WARN, '사용자 상세 정보 없음, 기본값 사용');
}
// 타이머 초기화는 호출하는 쪽에서 별도로 처리됨
} catch (error) { } catch (error) {
updateDebugInfo(`❌ 사용자 정보 로드 실패: ${error.message}`); updateDebugInfo(`❌ 사용자 정보 로드 실패: ${error.message}`);
await logger.log(LOG_LEVELS.ERROR, '사용자 정보 로드 실패', { error: error.message }); await logger.log(LOG_LEVELS.ERROR, '사용자 정보 로드 실패', { error: error.message });

View File

@ -38,11 +38,30 @@ class RestModal {
this.showRandomActivity(); this.showRandomActivity();
await this.loadRandomSaying(); await this.loadRandomSaying();
this.startTimer(); this.startTimer();
// 자동 품앗이 체크 및 실행
this.checkAutoZzim();
} catch (error) { } catch (error) {
console.error('[RestModal] 초기화 실패:', error); console.error('[RestModal] 초기화 실패:', error);
} }
} }
async checkAutoZzim() {
if (this.autoZzim) {
console.log('[RestModal] 휴식 중 자동 품앗이 시작 (Fast Mode: 10ms)');
// 백그라운드에 메시지 전송 (100ms 딜레이로 고속 실행)
try {
chrome.runtime.sendMessage({
action: 'autoMutualZzim',
delay: 100
});
} catch (e) {
console.error('[RestModal] 자동 품앗이 요청 실패:', e);
}
}
}
async loadSettings() { async loadSettings() {
try { try {
const result = await chrome.storage.local.get('time_alarm_settings'); const result = await chrome.storage.local.get('time_alarm_settings');
@ -93,28 +112,19 @@ class RestModal {
throw new Error('Access token not found'); throw new Error('Access token not found');
} }
const SUPABASE_URL = this.config.SUPABASE_URL || "https://ko.wrmc.cc"; console.log('[RestModal] 추천활동 API 호출 (background.js 경유)');
const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
// public.events 테이블에서 event_type이 'rest_time'인 데이터 가져오기 // background.js를 통해 API 호출
const apiUrl = `${SUPABASE_URL}/rest/v1/events?select=message&event_type=eq.rest_time&order=created_at.desc&limit=50`; const response = await chrome.runtime.sendMessage({
action: 'getRestActivities',
console.log('[RestModal] 추천활동 API 호출:', apiUrl); token: this.config.ACCESS_TOKEN
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
'apikey': SUPABASE_ANON_KEY,
'Authorization': `Bearer ${this.config.ACCESS_TOKEN}`,
'Content-Type': 'application/json'
}
}); });
if (!response.ok) { if (!response || !response.success) {
throw new Error(`API 호출 실패: ${response.status}`); throw new Error(response?.error || 'API 호출 실패');
} }
const events = await response.json(); const events = response.activities;
if (events && events.length > 0) { if (events && events.length > 0) {
// message 필드에서 JSON 파싱하여 활동 목록 생성 // message 필드에서 JSON 파싱하여 활동 목록 생성
@ -170,31 +180,19 @@ class RestModal {
throw new Error('Access token not found'); throw new Error('Access token not found');
} }
const SUPABASE_URL = this.config.SUPABASE_URL || "https://ko.wrmc.cc"; console.log('[RestModal] 어록 API 호출 (background.js 경유)');
const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
// 최근 1달 이내의 승인된 어록 가져오기 // background.js를 통해 API 호출
const oneMonthAgo = new Date(); const response = await chrome.runtime.sendMessage({
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); action: 'getRandomSaying',
token: this.config.ACCESS_TOKEN
const apiUrl = `${SUPABASE_URL}/rest/v1/tanya_sayings?select=*,sayings_cat(saying_cat),sayings_target(target)&created_at=gte.${oneMonthAgo.toISOString()}&admin_approval=eq.true&order=created_at.desc&limit=50`;
console.log('[RestModal] 어록 API 호출:', apiUrl);
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
'apikey': SUPABASE_ANON_KEY,
'Authorization': `Bearer ${this.config.ACCESS_TOKEN}`,
'Content-Type': 'application/json'
}
}); });
if (!response.ok) { if (!response || !response.success) {
throw new Error(`API 호출 실패: ${response.status}`); throw new Error(response?.error || 'API 호출 실패');
} }
const sayings = await response.json(); const sayings = response.sayings;
if (sayings && sayings.length > 0) { if (sayings && sayings.length > 0) {
// 랜덤하게 하나 선택 // 랜덤하게 하나 선택

View File

@ -1186,10 +1186,7 @@
🔄 타냐대장경을 불러오는 중... 🔄 타냐대장경을 불러오는 중...
</div> </div>
<!-- 마크다운 라이브러리를 직접 로드 --> <!-- 마크다운 라이브러리는 sayings.js 내장 함수 사용 (CSP 호환) -->
<script src="https://cdn.jsdelivr.net/npm/marked@9.1.6/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js"></script>
<script src="sayings.js"></script> <script src="sayings.js"></script>
</body> </body>
</html> </html>

View File

@ -21,78 +21,35 @@ class SayingsManager {
this.debugLog('SayingsManager 생성자 호출'); this.debugLog('SayingsManager 생성자 호출');
} }
// 설정 로드 함수 // 설정 로드 함수 (Background.js 통합 버전)
async loadConfig() { async loadConfig() {
try { try {
this.debugLog('chrome.storage에서 설정 로드 시작'); this.debugLog('chrome.storage에서 설정 로드 시작');
// 1. sayings_config 우선 확인 // access_token만 로드 (SUPABASE 설정은 background.js에서 관리)
const configData = await chrome.storage.local.get('sayings_config'); const storageData = await chrome.storage.local.get(['access_token', 'sayings_config']);
if (configData.sayings_config) {
const config = configData.sayings_config;
this.debugLog('sayings_config에서 설정 로드', {
hasUrl: !!config.SUPABASE_URL,
hasKey: !!config.SUPABASE_ANON_KEY,
hasToken: !!config.ACCESS_TOKEN,
debugMode: config.DEBUG_MODE,
timestamp: config.timestamp,
age: Date.now() - config.timestamp
});
// 설정이 너무 오래된 경우 (5분 이상) 경고
if (Date.now() - config.timestamp > 5 * 60 * 1000) {
this.debugLog('⚠️ 설정이 오래되었습니다', { age: Date.now() - config.timestamp });
}
this.SUPABASE_URL = config.SUPABASE_URL;
this.SUPABASE_ANON_KEY = config.SUPABASE_ANON_KEY;
this.DEBUG_MODE = config.DEBUG_MODE !== undefined ? config.DEBUG_MODE : true;
this.ACCESS_TOKEN = config.ACCESS_TOKEN;
// DEBUG_MODE 설정 확인
if (storageData.sayings_config && storageData.sayings_config.DEBUG_MODE !== undefined) {
this.DEBUG_MODE = storageData.sayings_config.DEBUG_MODE;
} else { } else {
// 2. 개별 설정 확인 (fallback) this.DEBUG_MODE = false; // 기본값: 비활성화
this.debugLog('sayings_config가 없음, 개별 설정 확인 중');
const storageData = await chrome.storage.local.get([
'access_token',
'SUPABASE_URL',
'SUPABASE_ANON_KEY'
]);
this.debugLog('개별 설정 조회 결과', {
hasToken: !!storageData.access_token,
hasUrl: !!storageData.SUPABASE_URL,
hasKey: !!storageData.SUPABASE_ANON_KEY
});
// 기본값 설정
this.SUPABASE_URL = storageData.SUPABASE_URL || 'https://ko.wrmc.cc';
this.SUPABASE_ANON_KEY = storageData.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU';
this.ACCESS_TOKEN = storageData.access_token;
} }
this.isConfigLoaded = true; this.ACCESS_TOKEN = storageData.access_token;
this.debugLog('설정 로드 완료', { this.debugLog('설정 로드 결과', {
SUPABASE_URL: this.SUPABASE_URL,
hasToken: !!this.ACCESS_TOKEN, hasToken: !!this.ACCESS_TOKEN,
tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0, tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0,
DEBUG_MODE: this.DEBUG_MODE, DEBUG_MODE: this.DEBUG_MODE
isConfigLoaded: this.isConfigLoaded
}); });
// 설정 검증 // 토큰 검증
if (!this.SUPABASE_URL) {
throw new Error('SUPABASE_URL이 설정되지 않았습니다');
}
if (!this.SUPABASE_ANON_KEY) {
throw new Error('SUPABASE_ANON_KEY가 설정되지 않았습니다');
}
if (!this.ACCESS_TOKEN) { if (!this.ACCESS_TOKEN) {
throw new Error('ACCESS_TOKEN이 없습니다. 로그인이 필요합니다'); throw new Error('ACCESS_TOKEN이 없습니다. 로그인이 필요합니다');
} }
this.isConfigLoaded = true;
return true; return true;
} catch (error) { } catch (error) {
@ -177,49 +134,26 @@ class SayingsManager {
} }
} }
// 토큰 유효성 검증 // 토큰 유효성 검증 (Background.js 통합 버전)
async validateToken() { async validateToken() {
try { try {
this.debugLog('토큰 검증 API 호출 준비', { this.debugLog('토큰 검증 시작 (Background 요청)');
url: `${this.SUPABASE_URL}/auth/v1/user`,
hasToken: !!this.ACCESS_TOKEN, const response = await chrome.runtime.sendMessage({
tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0 action: 'getUserInfo',
token: this.ACCESS_TOKEN
}); });
const authRes = await fetch(`${this.SUPABASE_URL}/auth/v1/user`, { if (!response || !response.success) {
method: 'GET', throw new Error(response?.error || '토큰이 유효하지 않습니다');
headers: {
'Authorization': `Bearer ${this.ACCESS_TOKEN}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
mode: 'cors',
credentials: 'omit'
});
this.debugLog('토큰 검증 API 응답', {
status: authRes.status,
statusText: authRes.statusText,
ok: authRes.ok
});
if (!authRes.ok) {
const errorText = await authRes.text();
this.debugLog('토큰 검증 실패', {
status: authRes.status,
error: errorText
});
throw new Error(`토큰이 유효하지 않습니다 (${authRes.status}: ${authRes.statusText})`);
} }
const userData = await authRes.json();
this.debugLog('토큰 검증 성공', { this.debugLog('토큰 검증 성공', {
email: userData.email, email: response.user.email,
id: userData.id id: response.user.id
}); });
return userData; return response.user;
} catch (error) { } catch (error) {
this.debugLog('토큰 검증 중 오류', { this.debugLog('토큰 검증 중 오류', {
@ -227,54 +161,29 @@ class SayingsManager {
errorMessage: error.message errorMessage: error.message
}); });
if (error.name === 'TypeError' && error.message.includes('fetch')) {
throw new Error(`네트워크 연결 오류: 서버에 연결할 수 없습니다.`);
}
throw error; throw error;
} }
} }
// 타냐대장경 목록 로드 // 타냐대장경 목록 로드 (Background.js 통합 버전)
async loadSayings() { async loadSayings() {
this.debugLog('타냐대장경 로딩 시작'); this.debugLog('타냐대장경 로딩 시작 (Background 요청)');
try { try {
// 로딩 표시 // 로딩 표시
document.getElementById('sayings-container').innerHTML = '<div class="loading">📚 타냐대장경을 불러오는 중...</div>'; document.getElementById('sayings-container').innerHTML = '<div class="loading">📚 타냐대장경을 불러오는 중...</div>';
// API 호출을 위한 헤더 설정 const response = await chrome.runtime.sendMessage({
const headers = { action: 'getSayings',
'Authorization': `Bearer ${this.ACCESS_TOKEN}`, token: this.ACCESS_TOKEN,
'apikey': this.SUPABASE_ANON_KEY, adminApproval: true
'Accept': 'application/json',
'Content-Type': 'application/json'
};
// 승인된 타냐대장경만 가져오는 쿼리
const query = `${this.SUPABASE_URL}/rest/v1/tanya_sayings?admin_approval=eq.true&order=created_at.desc`;
this.debugLog('타냐대장경 API 호출', {
url: query,
headers: Object.keys(headers)
}); });
const response = await fetch(query, { if (!response || !response.success) {
method: 'GET', throw new Error(response?.error || '타냐대장경 로딩 실패');
headers: headers
});
if (!response.ok) {
const errorText = await response.text();
this.debugLog('타냐대장경 로딩 실패', {
status: response.status,
statusText: response.statusText,
error: errorText
});
throw new Error(`타냐대장경 로딩 실패: ${response.status} ${response.statusText}\n${errorText}`);
} }
const sayings = await response.json(); const sayings = response.sayings || [];
this.debugLog('타냐대장경 로딩 성공', { this.debugLog('타냐대장경 로딩 성공', {
count: sayings.length, count: sayings.length,
@ -804,7 +713,7 @@ class SayingsManager {
<div style="margin-bottom: 8px;"><strong> 시스템 상태:</strong></div> <div style="margin-bottom: 8px;"><strong> 시스템 상태:</strong></div>
<div style="margin-left: 15px; margin-bottom: 10px;"> <div style="margin-left: 15px; margin-bottom: 10px;">
설정 로드: ${this.isConfigLoaded ? '완료' : '실패'}<br> 설정 로드: ${this.isConfigLoaded ? '완료' : '실패'}<br>
API 연결: ${this.SUPABASE_URL ? '설정됨' : '미설정'}<br> API 연결: Background.js 통합<br>
토큰 상태: ${this.ACCESS_TOKEN ? '있음' : '없음'}<br> 토큰 상태: ${this.ACCESS_TOKEN ? '있음' : '없음'}<br>
디버그 모드: ${this.DEBUG_MODE ? '활성화' : '비활성화'} 디버그 모드: ${this.DEBUG_MODE ? '활성화' : '비활성화'}
</div> </div>
@ -895,53 +804,27 @@ class SayingsManager {
// 현재 사용자 정보 로드 // 현재 사용자 정보 로드
async loadCurrentUser() { async loadCurrentUser() {
try { try {
this.debugLog('현재 사용자 정보 로드 시작'); this.debugLog('현재 사용자 정보 로드 시작 (Background 요청)');
// 현재 사용자 정보 가져오기 if (!this.ACCESS_TOKEN) {
const authRes = await fetch(`${this.SUPABASE_URL}/auth/v1/user`, { throw new Error('로그인이 필요합니다.');
method: 'GET', }
headers: {
'Authorization': `Bearer ${this.ACCESS_TOKEN}`, const response = await chrome.runtime.sendMessage({
'apikey': this.SUPABASE_ANON_KEY, action: 'getUserInfo',
'Content-Type': 'application/json', token: this.ACCESS_TOKEN
'Accept': 'application/json'
},
mode: 'cors',
credentials: 'omit'
}); });
if (!authRes.ok) { if (!response || !response.success) {
throw new Error('사용자 인증 실패'); throw new Error(response?.error || '사용자 정보를 불러올 수 없습니다.');
} }
const authUser = await authRes.json(); const user = response.user;
// 사용자 닉네임 가져오기
const userRes = await fetch(`${this.SUPABASE_URL}/rest/v1/users?select=id,nickname&email=eq.${encodeURIComponent(authUser.email)}&limit=1`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.ACCESS_TOKEN}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
mode: 'cors',
credentials: 'omit'
});
if (!userRes.ok) {
throw new Error('사용자 정보 조회 실패');
}
const userData = await userRes.json();
if (!userData || userData.length === 0) {
throw new Error('사용자 데이터를 찾을 수 없습니다');
}
this.currentUser = { this.currentUser = {
id: userData[0].id, id: user.id,
nickname: userData[0].nickname || authUser.email, nickname: user.nickname || user.email,
email: authUser.email email: user.email
}; };
this.debugLog('현재 사용자 정보 로드 완료', this.currentUser); this.debugLog('현재 사용자 정보 로드 완료', this.currentUser);
@ -1227,31 +1110,15 @@ class SayingsManager {
sayingData.content_blocks = contentBlocks; sayingData.content_blocks = contentBlocks;
} }
// API 호출 // API 호출 (Background.js 통합 버전)
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/tanya_sayings`, { const response = await chrome.runtime.sendMessage({
method: 'POST', action: 'createSaying',
headers: { token: this.ACCESS_TOKEN,
'Authorization': `Bearer ${this.ACCESS_TOKEN}`, sayingData: sayingData
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(sayingData)
}); });
this.debugLog('타냐대장경 등록 API 응답', { if (!response || !response.success) {
status: response.status, throw new Error(response?.error || '타냐대장경 등록 실패');
statusText: response.statusText,
ok: response.ok
});
if (!response.ok) {
const errorDetail = await response.text();
this.debugLog('타냐대장경 등록 실패', {
status: response.status,
error: errorDetail
});
throw new Error(`타냐대장경 등록 실패 (${response.status}: ${response.statusText})\n응답: ${errorDetail}`);
} }
this.debugLog('타냐대장경 등록 성공'); this.debugLog('타냐대장경 등록 성공');
@ -1344,34 +1211,26 @@ class SayingsManager {
this.debugLog('성공 팝업 표시 완료', { title, category, target }); this.debugLog('성공 팝업 표시 완료', { title, category, target });
} }
// 카테고리와 타겟 옵션을 백엔드에서 로드 // 카테고리와 타겟 옵션을 백엔드에서 로드 (Background.js 통합 버전)
async loadCategoryAndTargetOptions() { async loadCategoryAndTargetOptions() {
this.debugLog('카테고리와 타겟 옵션 로드 시작'); this.debugLog('카테고리와 타겟 옵션 로드 시작 (Background 요청)');
try { try {
// 카테고리 데이터 가져오기 // 카테고리와 타겟 데이터를 병렬로 가져오기
this.debugLog('카테고리 데이터 요청 중...'); const [categoryResponse, targetResponse] = await Promise.all([
const categoryResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/sayings_cat?select=saying_cat&order=saying_cat.asc`, { chrome.runtime.sendMessage({
headers: { action: 'getSayingsCategories',
'apikey': this.SUPABASE_ANON_KEY, token: this.ACCESS_TOKEN
'Authorization': `Bearer ${this.SUPABASE_ANON_KEY}`, }),
'Content-Type': 'application/json' chrome.runtime.sendMessage({
} action: 'getSayingsTargets',
}); token: this.ACCESS_TOKEN
})
// 타겟 데이터 가져오기 ]);
this.debugLog('타겟 데이터 요청 중...');
const targetResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/sayings_target?select=target&order=target.asc`, {
headers: {
'apikey': this.SUPABASE_ANON_KEY,
'Authorization': `Bearer ${this.SUPABASE_ANON_KEY}`,
'Content-Type': 'application/json'
}
});
// 카테고리 응답 처리 // 카테고리 응답 처리
if (categoryResponse.ok) { if (categoryResponse && categoryResponse.success) {
const categoryData = await categoryResponse.json(); const categoryData = categoryResponse.categories || [];
this.debugLog('카테고리 데이터 로드 성공', { count: categoryData.length }); this.debugLog('카테고리 데이터 로드 성공', { count: categoryData.length });
if (categoryData.length === 0) { if (categoryData.length === 0) {
@ -1381,18 +1240,12 @@ class SayingsManager {
// 카테고리 드롭다운 업데이트 // 카테고리 드롭다운 업데이트
this.updateCategoryDropdowns(categoryData); this.updateCategoryDropdowns(categoryData);
} else { } else {
const errorText = await categoryResponse.text(); throw new Error(categoryResponse?.error || '카테고리 데이터 로드 실패');
this.debugLog('카테고리 데이터 로드 실패', {
status: categoryResponse.status,
statusText: categoryResponse.statusText,
error: errorText
});
throw new Error(`카테고리 데이터 로드 실패: ${categoryResponse.status} ${categoryResponse.statusText}`);
} }
// 타겟 응답 처리 // 타겟 응답 처리
if (targetResponse.ok) { if (targetResponse && targetResponse.success) {
const targetData = await targetResponse.json(); const targetData = targetResponse.targets || [];
this.debugLog('타겟 데이터 로드 성공', { count: targetData.length }); this.debugLog('타겟 데이터 로드 성공', { count: targetData.length });
if (targetData.length === 0) { if (targetData.length === 0) {
@ -1402,13 +1255,7 @@ class SayingsManager {
// 타겟 드롭다운 업데이트 // 타겟 드롭다운 업데이트
this.updateTargetDropdowns(targetData); this.updateTargetDropdowns(targetData);
} else { } else {
const errorText = await targetResponse.text(); throw new Error(targetResponse?.error || '타겟 데이터 로드 실패');
this.debugLog('타겟 데이터 로드 실패', {
status: targetResponse.status,
statusText: targetResponse.statusText,
error: errorText
});
throw new Error(`타겟 데이터 로드 실패: ${targetResponse.status} ${targetResponse.statusText}`);
} }
this.debugLog('카테고리와 타겟 옵션 로드 완료'); this.debugLog('카테고리와 타겟 옵션 로드 완료');
@ -2246,7 +2093,7 @@ class SayingsManager {
this.debugLog('보기 모드로 전환'); this.debugLog('보기 모드로 전환');
} }
// 편집된 타냐대장경 저장 // 편집된 타냐대장경 저장 (Background.js 통합 버전)
async saveEditedSaying() { async saveEditedSaying() {
if (!this.currentEditingSaying) { if (!this.currentEditingSaying) {
console.error('편집 중인 타냐대장경이 없습니다'); console.error('편집 중인 타냐대장경이 없습니다');
@ -2264,23 +2111,19 @@ class SayingsManager {
} }
try { try {
const response = await fetch(`${this.config.supabaseUrl}/rest/v1/tanya_sayings?id=eq.${this.currentEditingSaying.id}`, { const response = await chrome.runtime.sendMessage({
method: 'PATCH', action: 'updateSaying',
headers: { token: this.ACCESS_TOKEN,
'Content-Type': 'application/json', sayingId: this.currentEditingSaying.id,
'apikey': this.config.supabaseKey, sayingData: {
'Authorization': `Bearer ${this.config.supabaseKey}`,
'Prefer': 'return=representation'
},
body: JSON.stringify({
saying_title: title, saying_title: title,
saying: content, saying: content,
cat: category, cat: category,
target: target target: target
}) }
}); });
if (response.ok) { if (response && response.success) {
alert('타냐대장경이 성공적으로 수정되었습니다.'); alert('타냐대장경이 성공적으로 수정되었습니다.');
// 모달 닫기 // 모달 닫기
@ -2290,30 +2133,28 @@ class SayingsManager {
// 타냐대장경 목록 새로고침 // 타냐대장경 목록 새로고침
await this.loadSayings(); await this.loadSayings();
} else { } else {
throw new Error('타냐대장경 수정에 실패했습니다.'); throw new Error(response?.error || '타냐대장경 수정에 실패했습니다.');
} }
} catch (error) { } catch (error) {
console.error('타냐대장경 수정 오류:', error); console.error('타냐대장경 수정 오류:', error);
alert('타냐대장경 수정 중 오류가 발생했습니다.'); alert('타냐대장경 수정 중 오류가 발생했습니다: ' + error.message);
} }
} }
// 타냐대장경 삭제 // 타냐대장경 삭제 (Background.js 통합 버전)
async deleteSaying(saying) { async deleteSaying(saying) {
if (!confirm('정말로 이 타냐대장경을 삭제하시겠습니까?')) { if (!confirm('정말로 이 타냐대장경을 삭제하시겠습니까?')) {
return; return;
} }
try { try {
const response = await fetch(`${this.config.supabaseUrl}/rest/v1/tanya_sayings?id=eq.${saying.id}`, { const response = await chrome.runtime.sendMessage({
method: 'DELETE', action: 'deleteSaying',
headers: { token: this.ACCESS_TOKEN,
'apikey': this.config.supabaseKey, sayingId: saying.id
'Authorization': `Bearer ${this.config.supabaseKey}`
}
}); });
if (response.ok) { if (response && response.success) {
alert('타냐대장경이 성공적으로 삭제되었습니다.'); alert('타냐대장경이 성공적으로 삭제되었습니다.');
// 모달 닫기 // 모달 닫기
@ -2323,11 +2164,11 @@ class SayingsManager {
// 타냐대장경 목록 새로고침 // 타냐대장경 목록 새로고침
await this.loadSayings(); await this.loadSayings();
} else { } else {
throw new Error('타냐대장경 삭제에 실패했습니다.'); throw new Error(response?.error || '타냐대장경 삭제에 실패했습니다.');
} }
} catch (error) { } catch (error) {
console.error('타냐대장경 삭제 오류:', error); console.error('타냐대장경 삭제 오류:', error);
alert('타냐대장경 삭제 중 오류가 발생했습니다.'); alert('타냐대장경 삭제 중 오류가 발생했습니다: ' + error.message);
} }
} }
} }
@ -2341,22 +2182,14 @@ document.addEventListener('DOMContentLoaded', async () => {
let debugMode = false; // 기본값 let debugMode = false; // 기본값
try { try {
const configData = await chrome.storage.local.get('sayings_config'); const configData = await chrome.storage.local.get('sayings_config');
console.log('chrome.storage 설정 확인:', {
hasSayingsConfig: !!configData.sayings_config,
config: configData.sayings_config ? {
hasUrl: !!configData.sayings_config.SUPABASE_URL,
hasKey: !!configData.sayings_config.SUPABASE_ANON_KEY,
hasToken: !!configData.sayings_config.ACCESS_TOKEN,
debugMode: configData.sayings_config.DEBUG_MODE,
timestamp: configData.sayings_config.timestamp
} : null
});
// DEBUG_MODE 설정 확인 // DEBUG_MODE 설정 확인
if (configData.sayings_config && configData.sayings_config.DEBUG_MODE !== undefined) { if (configData.sayings_config && configData.sayings_config.DEBUG_MODE !== undefined) {
debugMode = configData.sayings_config.DEBUG_MODE; debugMode = configData.sayings_config.DEBUG_MODE;
} }
console.log('디버그 모드:', debugMode);
} catch (error) { } catch (error) {
console.error('chrome.storage 설정 확인 실패:', error); console.error('chrome.storage 설정 확인 실패:', error);
} }

View File

@ -116,75 +116,22 @@ class SettingsManager {
return; return;
} }
const SUPABASE_URL = this.config.SUPABASE_URL || "https://ko.wrmc.cc"; // Background Script에 사용자 정보 요청
const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU"; const response = await chrome.runtime.sendMessage({
action: 'getUserInfo',
console.log('Supabase URL:', SUPABASE_URL); token: this.config.ACCESS_TOKEN
console.log('토큰 존재 여부:', !!this.config.ACCESS_TOKEN);
// 사용자 기본 정보 가져오기
const authUrl = `${SUPABASE_URL}/auth/v1/user`;
console.log('사용자 인증 요청:', authUrl);
const authRes = await fetch(authUrl, {
headers: {
Authorization: `Bearer ${this.config.ACCESS_TOKEN}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
}); });
console.log('인증 응답 상태:', authRes.status, authRes.statusText); if (!response || !response.success) {
throw new Error(response?.error || '사용자 정보를 불러올 수 없습니다.');
if (!authRes.ok) {
const errorText = await authRes.text();
console.error('사용자 인증 실패:', errorText);
throw new Error(`사용자 인증 실패: ${authRes.status}`);
} }
const authUser = await authRes.json(); this.userInfo = response.user;
console.log('인증된 사용자:', authUser);
if (!authUser || !authUser.email) {
throw new Error('사용자 이메일 정보가 없습니다.');
}
// 사용자 상세 정보 가져오기
const detailsUrl = `${SUPABASE_URL}/rest/v1/users?select=*&email=eq.${encodeURIComponent(authUser.email)}&limit=1`;
console.log('사용자 상세 정보 요청:', detailsUrl);
const detailsRes = await fetch(detailsUrl, {
headers: {
Authorization: `Bearer ${this.config.ACCESS_TOKEN}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
console.log('상세 정보 응답 상태:', detailsRes.status, detailsRes.statusText);
if (!detailsRes.ok) {
const errorText = await detailsRes.text();
console.error('사용자 정보 조회 실패:', errorText);
throw new Error(`사용자 정보 조회 실패: ${detailsRes.status}`);
}
const detailsData = await detailsRes.json();
console.log('사용자 상세 정보:', detailsData);
if (!detailsData || !Array.isArray(detailsData) || detailsData.length === 0) {
console.warn('사용자 상세 정보가 없습니다. 기본 회원으로 설정합니다.');
this.userInfo = {
email: authUser.email,
membership_level: 'basic'
};
} else {
this.userInfo = detailsData[0];
// membership_level이 없으면 기본값 설정 // membership_level이 없으면 기본값 설정
if (!this.userInfo.membership_level) { if (!this.userInfo.membership_level) {
this.userInfo.membership_level = 'basic'; this.userInfo.membership_level = 'basic';
} }
}
console.log('사용자 정보 로드 완료:', this.userInfo); console.log('사용자 정보 로드 완료:', this.userInfo);
} catch (error) { } catch (error) {

View File

@ -20,23 +20,62 @@
} }
.stat-card { .stat-card {
background: white;
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 4px 12px rgba(0,0,0,0.1);
text-align: center; text-align: center;
position: relative;
overflow: hidden;
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
}
/* 오늘 찜한 갯수: 파란색/활동적인 느낌 */
.stat-card.action-card {
background: linear-gradient(135deg, #ffffff 0%, #f0f7ff 100%);
border: 1px solid #cce5ff;
}
.stat-card.action-card .stat-title {
color: #0056b3;
font-weight: 600;
}
.stat-card.action-card .stat-value {
color: #007bff;
}
/* 찜 마일리지: 금색/화폐 느낌 */
.stat-card.mileage-card {
background: linear-gradient(135deg, #fffdf0 0%, #fff8e1 100%);
border: 1px solid #ffeeba;
}
.stat-card.mileage-card .stat-title {
color: #856404;
font-weight: 600;
}
.stat-card.mileage-card .stat-value {
color: #d39e00;
} }
.stat-title { .stat-title {
font-size: 14px; font-size: 14px;
color: #666;
margin-bottom: 10px; margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
} }
.stat-value { .stat-value {
font-size: 24px; font-size: 28px;
font-weight: bold; font-weight: 800;
color: #2c3e50; font-family: 'Segoe UI', sans-serif;
} }
.stat-limit { .stat-limit {
@ -81,6 +120,7 @@
.market-form input[type="text"]:focus { .market-form input[type="text"]:focus {
outline: none; outline: none;
border-color: #3498db; border-color: #3498db;
border-color: #3498db;
} }
.btn { .btn {
@ -600,15 +640,17 @@
</head> </head>
<body> <body>
<div class="stats-container"> <div class="stats-container">
<div class="stat-card"> <!-- 오늘 활동량 카드 (블루 테마) -->
<div class="stat-title">오늘 찜한 갯수</div> <div class="stat-card action-card">
<div class="stat-title">⚡ 오늘 찜한 갯수</div>
<div class="stat-value" id="today-zzim-count">0</div> <div class="stat-value" id="today-zzim-count">0</div>
<div class="stat-limit">/ <span id="today-zzim-limit">100</span></div> <div class="stat-limit">/ <span id="today-zzim-limit">300</span></div>
</div> </div>
<div class="stat-card"> <!-- 마일리지 카드 (골드 테마) -->
<div class="stat-title">찜 마일리지</div> <div class="stat-card mileage-card">
<div class="stat-title">💰 찜 마일리지</div>
<div class="stat-value" id="zzim-mileage">0</div> <div class="stat-value" id="zzim-mileage">0</div>
<div class="stat-limit">/ <span id="zzim-mileage-limit">1000</span></div> <div class="stat-limit">보유중 (품앗이 보상 +10%)</div>
</div> </div>
</div> </div>
@ -668,7 +710,7 @@
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;"> <div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<div style="display: flex; align-items: center; gap: 5px;"> <div style="display: flex; align-items: center; gap: 5px;">
<span style="font-size: 12px; color: #666;">기본 간격:</span> <span style="font-size: 12px; color: #666;">기본 간격:</span>
<input type="number" id="base-delay" value="1000" min="1000" max="5000" step="100" <input type="number" id="base-delay" value="300" min="200" max="5000" step="100"
style="width: 80px; padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px;" style="width: 80px; padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px;"
disabled> disabled>
<span style="font-size: 12px; color: #666;">ms</span> <span style="font-size: 12px; color: #666;">ms</span>
@ -681,11 +723,11 @@
</div> </div>
<div style="display: flex; align-items: center; gap: 5px;"> <div style="display: flex; align-items: center; gap: 5px;">
<span style="font-size: 12px; color: #666;">= 총 간격:</span> <span style="font-size: 12px; color: #666;">= 총 간격:</span>
<span id="total-delay" style="font-weight: bold; color: #667eea;">1000ms</span> <span id="total-delay" style="font-weight: bold; color: #667eea;">300ms</span>
</div> </div>
</div> </div>
<div style="font-size: 11px; color: #888; margin-top: 5px;"> <div style="font-size: 11px; color: #888; margin-top: 5px;">
💡 찜과 찜 사이의 대기 시간을 설정합니다. (1초~10초) 💡 찜과 찜 사이의 대기 시간을 설정합니다. (0.2초~10초)
</div> </div>
</div> </div>
@ -771,18 +813,7 @@
</label> </label>
<div> <div>
<label class="checkbox-label" for="edit-market-for-mutual-zzim">품앗이 대상으로 포함</label> <label class="checkbox-label" for="edit-market-for-mutual-zzim">품앗이 대상으로 포함</label>
<div class="checkbox-description">체크하면 다른 사람들이 이 마켓에 품앗이 찜을 할 수 있습니다.</div> <div class="checkbox-description">체크하면 다른 사람들이 이 마켓에 품앗이 찜을 할 수 있습니다. (자동 노출)</div>
</div>
</div>
<div class="checkbox-container">
<label class="custom-checkbox">
<input type="checkbox" id="edit-market-visible">
<span class="checkbox-checkmark"></span>
</label>
<div>
<label class="checkbox-label" for="edit-market-visible">다른 사람에게 노출</label>
<div class="checkbox-description">체크하면 품앗이 찜하기에서 다른 사람들이 이 마켓을 찜할 수 있습니다.</div>
</div> </div>
</div> </div>
</div> </div>

324
zzim.js
View File

@ -260,7 +260,7 @@ class ZzimManager {
// 총 찜하기 간격 업데이트 // 총 찜하기 간격 업데이트
updateTotalDelay() { updateTotalDelay() {
const baseDelay = parseInt(document.getElementById('base-delay')?.value || 1000); const baseDelay = parseInt(document.getElementById('base-delay')?.value || 500);
const userDelay = parseInt(document.getElementById('user-delay')?.value || 0); const userDelay = parseInt(document.getElementById('user-delay')?.value || 0);
const totalDelay = baseDelay + userDelay; const totalDelay = baseDelay + userDelay;
@ -272,7 +272,7 @@ class ZzimManager {
// 찜하기 설정 가져오기 // 찜하기 설정 가져오기
getZzimSettings() { getZzimSettings() {
const baseDelay = parseInt(document.getElementById('base-delay')?.value || 1000); const baseDelay = parseInt(document.getElementById('base-delay')?.value || 500);
const userDelay = parseInt(document.getElementById('user-delay')?.value || 0); const userDelay = parseInt(document.getElementById('user-delay')?.value || 0);
const latestFirst = document.getElementById('latest-first')?.checked || false; const latestFirst = document.getElementById('latest-first')?.checked || false;
const backgroundMode = document.getElementById('background-mode')?.checked || false; const backgroundMode = document.getElementById('background-mode')?.checked || false;
@ -321,85 +321,24 @@ class ZzimManager {
async loadZzimStats() { async loadZzimStats() {
try { try {
console.log('찜 통계 로드 시작...'); console.log('찜 통계 로드 시작 (Background 요청)...');
// 1. 사용자 기본 정보 및 회원등급 조회 (users 테이블) const response = await chrome.runtime.sendMessage({
const userRes = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}&select=my_zzim,zzim_mile,available_zzim_mile,membership_level,today_zzim_count,today_zzim_date`, { action: 'getZzimStats',
headers: { userId: this.user_id,
'Authorization': `Bearer ${this.access_token}`, token: this.access_token
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
}); });
if (!userRes.ok) { if (!response || !response.success) {
throw new Error('사용자 통계 정보를 불러올 수 없습니다.'); throw new Error(response?.error || '통계 정보를 불러올 수 없습니다.');
} }
const userData = await userRes.json(); const { stats: userStats, limits } = response;
const userStats = userData[0] || {};
console.log('users 테이블에서 로드된 통계:', userStats);
// 2. 회원등급별 제한 조회 // 3. 오늘 찜한 개수
const membershipLevel = userStats.membership_level || 'basic';
const limitsResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/membership_levels?level=eq.${membershipLevel}`, {
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
let limits = {
daily_zzim_limit: 50,
max_zzim_mileage: 500,
mileage_per_zzim: 1,
max_markets: 5,
mutual_zzim_enabled: true,
background_zzim_enabled: false
};
if (limitsResponse.ok) {
const limitsData = await limitsResponse.json();
if (limitsData.length > 0) {
// membership_levels 테이블에는 제한 필드만 있으므로 기본 limits 객체와 병합
limits = { ...limits, ...limitsData[0] };
}
} else {
console.warn('회원등급별 제한 조회 실패, 기본값 사용');
}
// 3. 오늘 찜한 개수 확인 및 리셋
const today = new Date().toISOString().split('T')[0];
const lastZzimDate = userStats.today_zzim_date;
let todayZzimCount = userStats.today_zzim_count || 0; let todayZzimCount = userStats.today_zzim_count || 0;
// 날짜가 바뀌었으면 리셋 // 4. 사용 가능한 찜 마일리지
if (lastZzimDate !== today) {
console.log('날짜 변경 감지, 일일 찜 카운트 리셋');
// users 테이블에 오늘 카운트를 0으로 초기화
const resetUsersResponse = 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'
},
body: JSON.stringify({
today_zzim_count: 0,
today_zzim_date: today
})
});
if (resetUsersResponse.ok) {
todayZzimCount = 0;
console.log('일일 찜 카운트 리셋 완료');
}
}
// 4. 사용 가능한 찜 마일리지 계산
const totalZzimMile = userStats.zzim_mile || 0; const totalZzimMile = userStats.zzim_mile || 0;
const availableZzimMile = userStats.available_zzim_mile || totalZzimMile; const availableZzimMile = userStats.available_zzim_mile || totalZzimMile;
@ -420,7 +359,6 @@ class ZzimManager {
const todayProgress = (todayZzimCount / limits.daily_zzim_limit) * 100; const todayProgress = (todayZzimCount / limits.daily_zzim_limit) * 100;
const mileageProgress = (availableZzimMile / limits.max_zzim_mileage) * 100; const mileageProgress = (availableZzimMile / limits.max_zzim_mileage) * 100;
// 진행률 바 업데이트 (있는 경우)
const todayProgressBar = document.getElementById('today-progress-bar'); const todayProgressBar = document.getElementById('today-progress-bar');
const mileageProgressBar = document.getElementById('mileage-progress-bar'); const mileageProgressBar = document.getElementById('mileage-progress-bar');
@ -438,7 +376,6 @@ class ZzimManager {
const todayLimitReached = todayZzimCount >= limits.daily_zzim_limit; const todayLimitReached = todayZzimCount >= limits.daily_zzim_limit;
const mileageLimitReached = availableZzimMile >= limits.max_zzim_mileage; const mileageLimitReached = availableZzimMile >= limits.max_zzim_mileage;
// 버튼 상태 업데이트
const myMarketBtn = document.getElementById('my-market-zzim-btn'); const myMarketBtn = document.getElementById('my-market-zzim-btn');
const mutualZzimBtn = document.getElementById('mutual-zzim-btn'); const mutualZzimBtn = document.getElementById('mutual-zzim-btn');
const backgroundModeCheckbox = document.getElementById('background-mode'); const backgroundModeCheckbox = document.getElementById('background-mode');
@ -480,17 +417,6 @@ class ZzimManager {
} }
} }
console.log('찜 통계 로드 완료:', {
membershipLevel,
todayZzimCount,
dailyLimit: limits.daily_zzim_limit,
availableZzimMile,
maxZzimMileage: limits.max_zzim_mileage,
totalReceived: userStats.my_zzim,
mutualEnabled: limits.mutual_zzim_enabled,
backgroundEnabled: limits.background_zzim_enabled
});
// 현재 제한 정보를 인스턴스 변수에 저장 // 현재 제한 정보를 인스턴스 변수에 저장
this.currentLimits = limits; this.currentLimits = limits;
this.currentStats = { this.currentStats = {
@ -507,40 +433,20 @@ class ZzimManager {
async loadMyMarkets() { async loadMyMarkets() {
try { try {
console.log('마켓 목록 로드 시작:', { console.log('마켓 목록 로드 시작 (Background 요청)...');
user_id: this.user_id,
supabase_url: this.SUPABASE_URL, const response = await chrome.runtime.sendMessage({
has_token: !!this.access_token action: 'getMyMarkets',
userId: this.user_id,
token: this.access_token
}); });
// user_markets 테이블에서 my_markets JSONB 필드 가져오기 if (!response || !response.success) {
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, { throw new Error(response?.error || '마켓 목록을 불러올 수 없습니다.');
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
console.log('마켓 목록 로드 응답:', {
status: response.status,
ok: response.ok,
statusText: response.statusText
});
if (!response.ok) {
const errorText = await response.text();
console.error('마켓 목록 로드 실패 상세:', errorText);
throw new Error(`마켓 목록 로드 실패: ${response.status} - ${errorText}`);
} }
const data = await response.json(); const markets = response.markets || [];
console.log('마켓 목록 로드 데이터:', data);
const markets = data[0]?.my_markets || [];
console.log('파싱된 마켓 목록:', markets);
// 찜관리 창에서는 모든 마켓을 보여줌 (노출여부 관계없이)
// 생성일 기준으로 최신순 정렬 // 생성일 기준으로 최신순 정렬
const sortedMarkets = markets.sort((a, b) => { const sortedMarkets = markets.sort((a, b) => {
const dateA = new Date(a.created_at || 0); const dateA = new Date(a.created_at || 0);
@ -548,8 +454,6 @@ class ZzimManager {
return dateB - dateA; return dateB - dateA;
}); });
console.log('정렬된 마켓 목록:', sortedMarkets);
this.renderMarketsList(sortedMarkets); this.renderMarketsList(sortedMarkets);
} catch (error) { } catch (error) {
@ -604,17 +508,11 @@ class ZzimManager {
<div class="market-url" title="${market.market_url || ''}">${this.truncateUrl(market.market_url || '')}</div> <div class="market-url" title="${market.market_url || ''}">${this.truncateUrl(market.market_url || '')}</div>
<div class="market-stats"> <div class="market-stats">
<span class="zzim-count">찜받은수: ${market.zzim_received_count || 0}</span> <span class="zzim-count">찜받은수: ${market.zzim_received_count || 0}</span>
<span class="visibility-status ${market.is_visible !== false ? 'visible' : 'hidden'}">
${market.is_visible !== false ? '노출중' : '숨김'}
</span>
<span class="created-date">등록일: ${this.formatDate(market.created_at)}</span> <span class="created-date">등록일: ${this.formatDate(market.created_at)}</span>
</div> </div>
</div> </div>
<div class="market-actions"> <div class="market-actions">
<button class="btn btn-warning market-edit-btn" data-market-id="${marketId}">수정</button> <button class="btn btn-warning market-edit-btn" data-market-id="${marketId}">수정</button>
<button class="btn ${market.is_visible !== false ? 'btn-secondary' : 'btn-success'} market-visibility-btn" data-market-id="${marketId}">
${market.is_visible !== false ? '숨기기' : '보이기'}
</button>
<button class="btn btn-danger market-delete-btn" data-market-id="${marketId}">삭제</button> <button class="btn btn-danger market-delete-btn" data-market-id="${marketId}">삭제</button>
</div> </div>
</div> </div>
@ -651,7 +549,8 @@ class ZzimManager {
}); });
}); });
// 노출/숨김 버튼 이벤트 // 노출/숨김 버튼 이벤트 (삭제됨)
/*
const visibilityBtns = document.querySelectorAll('.market-visibility-btn'); const visibilityBtns = document.querySelectorAll('.market-visibility-btn');
visibilityBtns.forEach(btn => { visibilityBtns.forEach(btn => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
@ -659,6 +558,7 @@ class ZzimManager {
this.toggleMarketVisibility(marketId); this.toggleMarketVisibility(marketId);
}); });
}); });
*/
// 삭제 버튼 이벤트 // 삭제 버튼 이벤트
const deleteBtns = document.querySelectorAll('.market-delete-btn'); const deleteBtns = document.querySelectorAll('.market-delete-btn');
@ -724,6 +624,8 @@ class ZzimManager {
market.for_zzim = isChecked; market.for_zzim = isChecked;
} else if (checkboxType === 'for_mutual_zzim') { } else if (checkboxType === 'for_mutual_zzim') {
market.for_mutual_zzim = isChecked; market.for_mutual_zzim = isChecked;
// 품앗이 대상 체크 시 '노출' 상태도 자동으로 동기화 (사용자 편의성)
market.is_visible = isChecked;
} }
market.updated_at = new Date().toISOString(); market.updated_at = new Date().toISOString();
@ -980,7 +882,6 @@ class ZzimManager {
const nicknameInput = document.getElementById('edit-market-nickname'); const nicknameInput = document.getElementById('edit-market-nickname');
const forZzimCheckbox = document.getElementById('edit-market-for-zzim'); const forZzimCheckbox = document.getElementById('edit-market-for-zzim');
const forMutualZzimCheckbox = document.getElementById('edit-market-for-mutual-zzim'); const forMutualZzimCheckbox = document.getElementById('edit-market-for-mutual-zzim');
const visibleCheckbox = document.getElementById('edit-market-visible');
// 기존 데이터로 폼 채우기 // 기존 데이터로 폼 채우기
if (urlInput) urlInput.value = market.market_url || ''; if (urlInput) urlInput.value = market.market_url || '';
@ -988,7 +889,6 @@ class ZzimManager {
if (nicknameInput) nicknameInput.value = market.market_nickname || ''; if (nicknameInput) nicknameInput.value = market.market_nickname || '';
if (forZzimCheckbox) forZzimCheckbox.checked = market.for_zzim !== false; if (forZzimCheckbox) forZzimCheckbox.checked = market.for_zzim !== false;
if (forMutualZzimCheckbox) forMutualZzimCheckbox.checked = market.for_mutual_zzim !== false; if (forMutualZzimCheckbox) forMutualZzimCheckbox.checked = market.for_mutual_zzim !== false;
if (visibleCheckbox) visibleCheckbox.checked = market.is_visible !== false;
// 모달 표시 // 모달 표시
if (modal) { if (modal) {
@ -1047,7 +947,6 @@ class ZzimManager {
const nicknameInput = document.getElementById('edit-market-nickname'); const nicknameInput = document.getElementById('edit-market-nickname');
const forZzimCheckbox = document.getElementById('edit-market-for-zzim'); const forZzimCheckbox = document.getElementById('edit-market-for-zzim');
const forMutualZzimCheckbox = document.getElementById('edit-market-for-mutual-zzim'); const forMutualZzimCheckbox = document.getElementById('edit-market-for-mutual-zzim');
const visibleCheckbox = document.getElementById('edit-market-visible');
const saveBtn = document.querySelector('.btn-modal-save'); const saveBtn = document.querySelector('.btn-modal-save');
const marketUrl = urlInput?.value.trim() || ''; const marketUrl = urlInput?.value.trim() || '';
@ -1055,7 +954,8 @@ class ZzimManager {
const marketNickname = nicknameInput?.value.trim() || ''; const marketNickname = nicknameInput?.value.trim() || '';
const forZzim = forZzimCheckbox?.checked || false; const forZzim = forZzimCheckbox?.checked || false;
const forMutualZzim = forMutualZzimCheckbox?.checked || false; const forMutualZzim = forMutualZzimCheckbox?.checked || false;
const isVisible = visibleCheckbox?.checked || false; // 품앗이 대상이 체크되면 자동으로 노출도 true로 설정
const isVisible = forMutualZzim;
// 유효성 검사 // 유효성 검사
if (!marketUrl || !marketName || !marketNickname) { if (!marketUrl || !marketName || !marketNickname) {
@ -1360,7 +1260,10 @@ class ZzimManager {
const market = mutualMarkets[i]; const market = mutualMarkets[i];
this.showProgress(i, mutualMarkets.length, `${market.market_nickname} 품앗이 중...`); this.showProgress(i, mutualMarkets.length, `${market.market_nickname} 품앗이 중...`);
await this.executeZzim(market, 'mutual', settings); // 백그라운드 모드를 강제로 활성화 (품앗이는 항상 백그라운드로)
const mutualSettings = { ...settings, backgroundMode: true };
await this.executeZzim(market, 'mutual', mutualSettings);
this.showProgress(i + 1, mutualMarkets.length, `${market.market_nickname} 완료`); this.showProgress(i + 1, mutualMarkets.length, `${market.market_nickname} 완료`);
@ -1607,7 +1510,10 @@ class ZzimManager {
// URL에 찜하기 파라미터 추가 // URL에 찜하기 파라미터 추가
const urlObj = new URL(finalTargetUrl); const urlObj = new URL(finalTargetUrl);
urlObj.searchParams.set('auto_zzim', 'true'); urlObj.searchParams.set('auto_zzim', 'true');
urlObj.searchParams.set('max_zzim', '50');
// 일일 제한 확인 (settings.maxZzim이 있으면 사용, 없으면 기본값)
const maxZzim = settings.maxZzim || this.currentLimits?.daily_zzim_limit || 100;
urlObj.searchParams.set('max_zzim', maxZzim.toString());
// 네이버 스마트스토어는 cp 파라미터를 사용 (page가 아님) // 네이버 스마트스토어는 cp 파라미터를 사용 (page가 아님)
if (!urlObj.searchParams.has('cp')) { if (!urlObj.searchParams.has('cp')) {
@ -1623,12 +1529,12 @@ class ZzimManager {
this.showSuccess(`찜하기 페이지로 이동합니다. (${market.market_nickname})`); this.showSuccess(`찜하기 페이지로 이동합니다. (${market.market_nickname})`);
// 현재 탭에서 로그인 리다이렉트 URL로 이동
window.location.href = loginRedirectUrl;
// 찜 기록 (실제 찜하기는 새 페이지에서 실행됨) // 찜 기록 (실제 찜하기는 새 페이지에서 실행됨)
await this.recordZzim(market, zzimType); await this.recordZzim(market, zzimType);
// 현재 탭에서 로그인 리다이렉트 URL로 이동
window.location.href = loginRedirectUrl;
} catch (error) { } catch (error) {
console.error('찜하기 진행 오류:', error); console.error('찜하기 진행 오류:', error);
this.showError('찜하기 진행 중 오류가 발생했습니다: ' + error.message); this.showError('찜하기 진행 중 오류가 발생했습니다: ' + error.message);
@ -1656,134 +1562,9 @@ class ZzimManager {
} }
async updateZzimStats(count, zzimType, targetMarket = null) { async updateZzimStats(count, zzimType, targetMarket = null) {
try { // 이 함수는 더 이상 직접 호출되지 않습니다.
console.log('찜 통계 업데이트 시작:', { count, zzimType, targetMarket }); // Background 스크립트가 처리합니다.
console.log('updateZzimStats는 이제 Background Script에서 처리됩니다.');
// 현재 제한 정보 확인
if (!this.currentLimits) {
console.warn('제한 정보가 없음, 통계 다시 로드');
await this.loadZzimStats();
}
const limits = this.currentLimits || { mileage_per_zzim: 1 };
const today = new Date().toISOString().split('T')[0];
if (zzimType === 'my_market') {
// 내 마켓 찜하기: 오늘 찜한 개수 증가 + 받은 찜 개수 증가
console.log('내 마켓 찜하기 통계 업데이트');
// users 테이블 업데이트
const myMarketUpdateResponse = 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'
},
body: JSON.stringify({
today_zzim_count: `today_zzim_count + ${count}`,
today_zzim_date: today,
my_zzim: `my_zzim + ${count}`
})
});
if (!myMarketUpdateResponse.ok) {
throw new Error('내 마켓 찜하기 통계 업데이트 실패');
}
// user_markets 테이블의 집계 필드는 제거되었으므로 별도 업데이트 필요 없음
// 내 마켓의 찜받은 개수도 증가
if (targetMarket) {
await this.updateMarketZzimCount(targetMarket.market_url, count);
}
// 찜 기록 저장
await this.saveZzimRecord(zzimType, targetMarket, count, 0);
} else if (zzimType === 'mutual') {
// 품앗이 찜하기: 내 마일리지 차감 + 상대방 마일리지 증가 + 상대방 받은 찜 개수 증가
console.log('품앗이 찜하기 통계 업데이트');
const mileageUsed = count * limits.mileage_per_zzim;
// 내 마일리지 차감 (users 테이블)
const myMileageUpdateResponse = 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'
},
body: JSON.stringify({
available_zzim_mile: `available_zzim_mile - ${mileageUsed}`
})
});
if (!myMileageUpdateResponse.ok) {
throw new Error('내 마일리지 차감 실패');
}
// 상대방 마일리지 증가 및 받은 찜 개수 증가
if (targetMarket && targetMarket.owner_user_id) {
// 상대방 users 테이블 업데이트
const targetUpdateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${targetMarket.owner_user_id}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
zzim_mile: `zzim_mile + ${mileageUsed}`,
available_zzim_mile: `available_zzim_mile + ${mileageUsed}`,
my_zzim: `my_zzim + ${count}`
})
});
if (!targetUpdateResponse.ok) {
console.warn('상대방 마일리지 및 받은 찜 개수 업데이트 실패');
}
// (중복 패치 제거: targetUpdateResponse 에서 이미 users 테이블을 갱신했습니다.)
// 상대방 users 테이블도 업데이트 (호환성 유지)
const targetUsersUpdateResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${targetMarket.owner_user_id}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
zzim_mile: `zzim_mile + ${mileageUsed}`,
available_zzim_mile: `available_zzim_mile + ${mileageUsed}`,
my_zzim: `my_zzim + ${count}`
})
});
if (!targetUsersUpdateResponse.ok) {
console.warn('상대방 users 테이블 업데이트 실패');
}
// 상대방 마켓의 찜받은 개수도 증가
await this.updateMarketZzimCount(targetMarket.market_url, count, targetMarket.owner_user_id);
}
// 찜 기록 저장
await this.saveZzimRecord(zzimType, targetMarket, count, -mileageUsed);
console.log(`품앗이 완료: 마일리지 ${mileageUsed} 사용, 찜 ${count}개 제공`);
}
// 통계 다시 로드하여 UI 업데이트
await this.loadZzimStats();
console.log('찜 통계 업데이트 완료');
} catch (error) {
console.error('찜 통계 업데이트 오류:', error);
throw error;
}
} }
// 마켓별 찜받은 개수 업데이트 // 마켓별 찜받은 개수 업데이트
@ -1891,18 +1672,19 @@ class ZzimManager {
return { canZzim: true, remaining: dailyLimit - todayCount }; return { canZzim: true, remaining: dailyLimit - todayCount };
} else if (zzimType === 'mutual') { } else if (zzimType === 'mutual') {
// 품앗이 찜하기: 사용 가능한 마일리지 확인 // 품앗이 찜하기: 내 마일리지 확인 필요 없음 (오히려 벌어야 함)
const availableMileage = this.currentStats.availableZzimMile; // 단, 계정 보호를 위해 '오늘 찜한 개수' 제한은 동일하게 적용
const requiredMileage = 50; // 마켓당 50개 찜 가정 const todayCount = this.currentStats.todayZzimCount;
const dailyLimit = this.currentLimits.daily_zzim_limit;
if (availableMileage < requiredMileage) { if (todayCount >= dailyLimit) {
return { return {
canZzim: false, canZzim: false,
reason: `품앗이에 필요한 마일리지가 부족합니다. (보유: ${availableMileage}, 필요: ${requiredMileage})` reason: `오늘 찜 제한에 도달했습니다. (품앗이 포함 통합 제한: ${todayCount}/${dailyLimit})`
}; };
} }
return { canZzim: true, remaining: Math.floor(availableMileage / requiredMileage) }; return { canZzim: true, remaining: dailyLimit - todayCount };
} }
return { canZzim: false, reason: '알 수 없는 찜 타입입니다.' }; return { canZzim: false, reason: '알 수 없는 찜 타입입니다.' };
@ -1939,7 +1721,8 @@ class ZzimManager {
try { try {
console.log('[품앗이] 품앗이 마켓 목록 조회 시작'); console.log('[품앗이] 품앗이 마켓 목록 조회 시작');
// 내 현재 마일리지 확인 (users 테이블) // 내 현재 마일리지 확인 (users 테이블) - 삭제: 품앗이는 마일리지를 버는 행위이므로 보유량 체크 불필요
/*
const myStatsResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}&select=available_zzim_mile`, { const myStatsResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}&select=available_zzim_mile`, {
headers: { headers: {
'Authorization': `Bearer ${this.access_token}`, 'Authorization': `Bearer ${this.access_token}`,
@ -1959,6 +1742,7 @@ class ZzimManager {
if (myAvailableMileage <= 0) { if (myAvailableMileage <= 0) {
throw new Error('사용 가능한 마일리지가 없습니다. 먼저 다른 사람의 마켓에 찜을 받아 마일리지를 적립하세요.'); throw new Error('사용 가능한 마일리지가 없습니다. 먼저 다른 사람의 마켓에 찜을 받아 마일리지를 적립하세요.');
} }
*/
// 다른 사용자들의 노출된 마켓 목록 가져오기 (품앗이용) - v_user_market_stats 뷰 사용 // 다른 사용자들의 노출된 마켓 목록 가져오기 (품앗이용) - 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`, { 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`, {
@ -2017,8 +1801,8 @@ class ZzimManager {
return new Date(b.created_at || 0) - new Date(a.created_at || 0); return new Date(b.created_at || 0) - new Date(a.created_at || 0);
}); });
// 내 마일리지로 처리 가능한 개수만큼 반환 (최대 10개) // 내 마일리지로 처리 가능한 개수만큼 반환 (최대 10개) -> 수정: 제한 없음 (단, 하루 최대 10개 마켓 정도만)
const maxMarkets = Math.min(10, Math.floor(myAvailableMileage / 50)); // 마켓당 50개 찜 가정 const maxMarkets = 10;
const selectedMarkets = mutualMarkets.slice(0, maxMarkets); const selectedMarkets = mutualMarkets.slice(0, maxMarkets);
console.log('[품앗이] 선택된 품앗이 마켓:', selectedMarkets.length); console.log('[품앗이] 선택된 품앗이 마켓:', selectedMarkets.length);