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

File diff suppressed because it is too large Load Diff

View File

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

205
popup.js
View File

@ -20,16 +20,8 @@ async function getBackendConfig() {
}
} catch (error) {
console.error('[popup.js] 백엔드 설정 로드 실패:', error);
console.log('[popup.js] 폴백 설정 사용');
// 폴백 설정
const fallbackConfig = {
SUPABASE_URL: "https://ko.wrmc.cc",
SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU"
};
console.log('[popup.js] 폴백 설정:', fallbackConfig);
return fallbackConfig;
// 폴백 설정 없이 에러 전파 - background.js에서만 설정 관리
throw new Error('백엔드 설정을 가져올 수 없습니다. 확장 프로그램을 다시 로드해주세요.');
}
}
@ -1244,182 +1236,75 @@ async function loadUserInfo(token) {
await logger.log(LOG_LEVELS.INFO, '사용자 정보 요청 중...');
try {
// 백엔드 설정 가져오기
const { SUPABASE_URL, SUPABASE_ANON_KEY } = await getBackendConfig();
// 먼저 Auth API로 사용자 기본 정보 가져오기
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'
}
// Background Script에 사용자 정보 요청
const response = await chrome.runtime.sendMessage({
action: 'getUserInfo',
token: token
});
if (!authRes.ok) {
// Auth API 응답 오류 처리
let authErrorDetails;
try {
authErrorDetails = await authRes.json();
} catch {
authErrorDetails = await authRes.text();
}
await logger.log(LOG_LEVELS.ERROR, 'Auth API 응답 오류', {
status: authRes.status,
statusText: authRes.statusText,
authErrorDetails
});
throw new Error(`Auth API 오류 - HTTP ${authRes.status}: ${authRes.statusText}`);
if (!response || !response.success) {
throw new Error(response?.error || '사용자 정보를 불러올 수 없습니다.');
}
const authUser = await authRes.json();
await logger.log(LOG_LEVELS.DEBUG, 'Auth API 응답 데이터', { authUser });
const { user } = response;
// 사용자 상세 정보와 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'
}
});
await logger.log(LOG_LEVELS.DEBUG, '사용자 정보 응답', { user });
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 업데이트
updateDebugInfo('✅ 사용자 정보 로드 완료');
await logger.log(LOG_LEVELS.INFO, '사용자 정보 로드 성공', {
email: authUser.email,
hasDetails: !!userDetails,
membershipLevel: userDetails?.membership_level,
membershipLevels: userDetails?.membership_levels
});
// 사용자 ID를 storage에 저장 (금지어 추가 시 사용)
if (userDetails && userDetails.id) {
// API 한도 정보도 함께 저장
// 사용자 ID를 storage에 저장
if (user.id) {
let apiLimit = null;
if (userDetails.membership_levels) {
apiLimit = userDetails.membership_levels.api_call_limit;
if (user.membership_levels) {
apiLimit = user.membership_levels.api_call_limit;
}
// 필요한 정보 저장
const { SUPABASE_URL, SUPABASE_ANON_KEY } = await getBackendConfig();
await chrome.storage.local.set({
user_id: userDetails.id,
user_email: authUser.email,
user_membership_level: userDetails.membership_level,
user_id: user.id,
user_email: user.email,
user_membership_level: user.membership_level,
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_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("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 apiLimit = null;
if (userDetails.membership_levels) {
// 조인된 데이터가 있는 경우
levelText = userDetails.membership_levels.level || "기본";
apiLimit = userDetails.membership_levels.api_call_limit;
} else if (userDetails.membership_level) {
// 직접 필드가 있는 경우
levelText = userDetails.membership_level;
}
document.getElementById("user-level").textContent = levelText;
// Chrome Storage에서 최신 호출량 정보 가져오기 (실시간 반영)
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;
// 오늘 호출량 표시 (current_api_calls/api_call_limit 형태)
let usageText = currentCalls.toString();
if (apiLimit !== null && apiLimit !== undefined) {
usageText = `${currentCalls}/${apiLimit}`;
}
document.getElementById("user-usage").textContent = usageText;
// 만료일 표시
document.getElementById("user-expire").textContent = userDetails.payment_period_end
? 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, '사용자 상세 정보 없음, 기본값 사용');
let levelText = "기본";
let apiLimit = null;
if (user.membership_levels) {
levelText = user.membership_levels.level || "기본";
apiLimit = user.membership_levels.api_call_limit;
} else if (user.membership_level) {
levelText = user.membership_level;
}
// 타이머 초기화는 호출하는 쪽에서 별도로 처리됨
document.getElementById("user-level").textContent = levelText;
const { user_current_api_calls } = await chrome.storage.local.get(['user_current_api_calls']);
const currentCalls = user_current_api_calls || user.current_api_calls || 0;
let usageText = currentCalls.toString();
if (apiLimit !== null && apiLimit !== undefined) {
usageText = `${currentCalls}/${apiLimit}`;
}
document.getElementById("user-usage").textContent = usageText;
document.getElementById("user-expire").textContent = user.payment_period_end
? new Date(user.payment_period_end).toLocaleDateString()
: "없음";
} catch (error) {
updateDebugInfo(`❌ 사용자 정보 로드 실패: ${error.message}`);
await logger.log(LOG_LEVELS.ERROR, '사용자 정보 로드 실패', { error: error.message });

View File

@ -38,11 +38,30 @@ class RestModal {
this.showRandomActivity();
await this.loadRandomSaying();
this.startTimer();
// 자동 품앗이 체크 및 실행
this.checkAutoZzim();
} catch (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() {
try {
const result = await chrome.storage.local.get('time_alarm_settings');
@ -93,28 +112,19 @@ class RestModal {
throw new Error('Access token not found');
}
const SUPABASE_URL = this.config.SUPABASE_URL || "https://ko.wrmc.cc";
const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
console.log('[RestModal] 추천활동 API 호출 (background.js 경유)');
// public.events 테이블에서 event_type이 'rest_time'인 데이터 가져오기
const apiUrl = `${SUPABASE_URL}/rest/v1/events?select=message&event_type=eq.rest_time&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'
}
// background.js를 통해 API 호출
const response = await chrome.runtime.sendMessage({
action: 'getRestActivities',
token: this.config.ACCESS_TOKEN
});
if (!response.ok) {
throw new Error(`API 호출 실패: ${response.status}`);
if (!response || !response.success) {
throw new Error(response?.error || 'API 호출 실패');
}
const events = await response.json();
const events = response.activities;
if (events && events.length > 0) {
// message 필드에서 JSON 파싱하여 활동 목록 생성
@ -170,31 +180,19 @@ class RestModal {
throw new Error('Access token not found');
}
const SUPABASE_URL = this.config.SUPABASE_URL || "https://ko.wrmc.cc";
const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
console.log('[RestModal] 어록 API 호출 (background.js 경유)');
// 최근 1달 이내의 승인된 어록 가져오기
const oneMonthAgo = new Date();
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
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'
}
// background.js를 통해 API 호출
const response = await chrome.runtime.sendMessage({
action: 'getRandomSaying',
token: this.config.ACCESS_TOKEN
});
if (!response.ok) {
throw new Error(`API 호출 실패: ${response.status}`);
if (!response || !response.success) {
throw new Error(response?.error || 'API 호출 실패');
}
const sayings = await response.json();
const sayings = response.sayings;
if (sayings && sayings.length > 0) {
// 랜덤하게 하나 선택

View File

@ -1186,10 +1186,7 @@
🔄 타냐대장경을 불러오는 중...
</div>
<!-- 마크다운 라이브러리를 직접 로드 -->
<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>
<!-- 마크다운 라이브러리는 sayings.js 내장 함수 사용 (CSP 호환) -->
<script src="sayings.js"></script>
</body>
</html>
</html>

View File

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

View File

@ -116,74 +116,21 @@ class SettingsManager {
return;
}
const SUPABASE_URL = this.config.SUPABASE_URL || "https://ko.wrmc.cc";
const SUPABASE_ANON_KEY = this.config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYyMzA2ODc5LCJleHAiOjIwNzc2NjY4Nzl9.aKF_nREC06KK81yOJKA1pOwz9gmgC0xsLwLWqqIVcsU";
console.log('Supabase URL:', SUPABASE_URL);
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'
}
// Background Script에 사용자 정보 요청
const response = await chrome.runtime.sendMessage({
action: 'getUserInfo',
token: this.config.ACCESS_TOKEN
});
console.log('인증 응답 상태:', authRes.status, authRes.statusText);
if (!authRes.ok) {
const errorText = await authRes.text();
console.error('사용자 인증 실패:', errorText);
throw new Error(`사용자 인증 실패: ${authRes.status}`);
if (!response || !response.success) {
throw new Error(response?.error || '사용자 정보를 불러올 수 없습니다.');
}
const authUser = await authRes.json();
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);
this.userInfo = response.user;
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이 없으면 기본값 설정
if (!this.userInfo.membership_level) {
this.userInfo.membership_level = 'basic';
}
// membership_level이 없으면 기본값 설정
if (!this.userInfo.membership_level) {
this.userInfo.membership_level = 'basic';
}
console.log('사용자 정보 로드 완료:', this.userInfo);

View File

@ -20,23 +20,62 @@
}
.stat-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
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 {
font-size: 14px;
color: #666;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #2c3e50;
font-size: 28px;
font-weight: 800;
font-family: 'Segoe UI', sans-serif;
}
.stat-limit {
@ -81,6 +120,7 @@
.market-form input[type="text"]:focus {
outline: none;
border-color: #3498db;
border-color: #3498db;
}
.btn {
@ -600,15 +640,17 @@
</head>
<body>
<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-limit">/ <span id="today-zzim-limit">100</span></div>
<div class="stat-limit">/ <span id="today-zzim-limit">300</span></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-limit">/ <span id="zzim-mileage-limit">1000</span></div>
<div class="stat-limit">보유중 (품앗이 보상 +10%)</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: 5px;">
<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;"
disabled>
<span style="font-size: 12px; color: #666;">ms</span>
@ -681,11 +723,11 @@
</div>
<div style="display: flex; align-items: center; gap: 5px;">
<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 style="font-size: 11px; color: #888; margin-top: 5px;">
💡 찜과 찜 사이의 대기 시간을 설정합니다. (1초~10초)
💡 찜과 찜 사이의 대기 시간을 설정합니다. (0.2초~10초)
</div>
</div>
@ -771,18 +813,7 @@
</label>
<div>
<label class="checkbox-label" for="edit-market-for-mutual-zzim">품앗이 대상으로 포함</label>
<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 class="checkbox-description">체크하면 다른 사람들이 이 마켓에 품앗이 찜을 할 수 있습니다. (자동 노출)</div>
</div>
</div>
</div>
@ -793,4 +824,4 @@
</div>
</div>
</body>
</html>
</html>

330
zzim.js
View File

@ -260,7 +260,7 @@ class ZzimManager {
// 총 찜하기 간격 업데이트
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 totalDelay = baseDelay + userDelay;
@ -272,7 +272,7 @@ class ZzimManager {
// 찜하기 설정 가져오기
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 latestFirst = document.getElementById('latest-first')?.checked || false;
const backgroundMode = document.getElementById('background-mode')?.checked || false;
@ -321,85 +321,24 @@ class ZzimManager {
async loadZzimStats() {
try {
console.log('찜 통계 로드 시작...');
console.log('찜 통계 로드 시작 (Background 요청)...');
// 1. 사용자 기본 정보 및 회원등급 조회 (users 테이블)
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`, {
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
const response = await chrome.runtime.sendMessage({
action: 'getZzimStats',
userId: this.user_id,
token: this.access_token
});
if (!userRes.ok) {
throw new Error('사용자 통계 정보를 불러올 수 없습니다.');
if (!response || !response.success) {
throw new Error(response?.error || '통계 정보를 불러올 수 없습니다.');
}
const { stats: userStats, limits } = response;
const userData = await userRes.json();
const userStats = userData[0] || {};
console.log('users 테이블에서 로드된 통계:', userStats);
// 2. 회원등급별 제한 조회
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;
// 3. 오늘 찜한 개수
let todayZzimCount = userStats.today_zzim_count || 0;
// 날짜가 바뀌었으면 리셋
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. 사용 가능한 찜 마일리지 계산
// 4. 사용 가능한 찜 마일리지
const totalZzimMile = userStats.zzim_mile || 0;
const availableZzimMile = userStats.available_zzim_mile || totalZzimMile;
@ -420,7 +359,6 @@ class ZzimManager {
const todayProgress = (todayZzimCount / limits.daily_zzim_limit) * 100;
const mileageProgress = (availableZzimMile / limits.max_zzim_mileage) * 100;
// 진행률 바 업데이트 (있는 경우)
const todayProgressBar = document.getElementById('today-progress-bar');
const mileageProgressBar = document.getElementById('mileage-progress-bar');
@ -438,7 +376,6 @@ class ZzimManager {
const todayLimitReached = todayZzimCount >= limits.daily_zzim_limit;
const mileageLimitReached = availableZzimMile >= limits.max_zzim_mileage;
// 버튼 상태 업데이트
const myMarketBtn = document.getElementById('my-market-zzim-btn');
const mutualZzimBtn = document.getElementById('mutual-zzim-btn');
const backgroundModeCheckbox = document.getElementById('background-mode');
@ -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.currentStats = {
@ -507,40 +433,20 @@ class ZzimManager {
async loadMyMarkets() {
try {
console.log('마켓 목록 로드 시작:', {
user_id: this.user_id,
supabase_url: this.SUPABASE_URL,
has_token: !!this.access_token
});
console.log('마켓 목록 로드 시작 (Background 요청)...');
// user_markets 테이블에서 my_markets JSONB 필드 가져오기
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/user_markets?user_id=eq.${this.user_id}&select=my_markets`, {
headers: {
'Authorization': `Bearer ${this.access_token}`,
'apikey': this.SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
const response = await chrome.runtime.sendMessage({
action: 'getMyMarkets',
userId: this.user_id,
token: this.access_token
});
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}`);
if (!response || !response.success) {
throw new Error(response?.error || '마켓 목록을 불러올 수 없습니다.');
}
const markets = response.markets || [];
const data = await response.json();
console.log('마켓 목록 로드 데이터:', data);
const markets = data[0]?.my_markets || [];
console.log('파싱된 마켓 목록:', markets);
// 찜관리 창에서는 모든 마켓을 보여줌 (노출여부 관계없이)
// 생성일 기준으로 최신순 정렬
const sortedMarkets = markets.sort((a, b) => {
const dateA = new Date(a.created_at || 0);
@ -548,8 +454,6 @@ class ZzimManager {
return dateB - dateA;
});
console.log('정렬된 마켓 목록:', sortedMarkets);
this.renderMarketsList(sortedMarkets);
} 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-stats">
<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>
</div>
</div>
<div class="market-actions">
<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>
</div>
</div>
@ -651,7 +549,8 @@ class ZzimManager {
});
});
// 노출/숨김 버튼 이벤트
// 노출/숨김 버튼 이벤트 (삭제됨)
/*
const visibilityBtns = document.querySelectorAll('.market-visibility-btn');
visibilityBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
@ -659,6 +558,7 @@ class ZzimManager {
this.toggleMarketVisibility(marketId);
});
});
*/
// 삭제 버튼 이벤트
const deleteBtns = document.querySelectorAll('.market-delete-btn');
@ -724,6 +624,8 @@ class ZzimManager {
market.for_zzim = isChecked;
} else if (checkboxType === 'for_mutual_zzim') {
market.for_mutual_zzim = isChecked;
// 품앗이 대상 체크 시 '노출' 상태도 자동으로 동기화 (사용자 편의성)
market.is_visible = isChecked;
}
market.updated_at = new Date().toISOString();
@ -980,7 +882,6 @@ class ZzimManager {
const nicknameInput = document.getElementById('edit-market-nickname');
const forZzimCheckbox = document.getElementById('edit-market-for-zzim');
const forMutualZzimCheckbox = document.getElementById('edit-market-for-mutual-zzim');
const visibleCheckbox = document.getElementById('edit-market-visible');
// 기존 데이터로 폼 채우기
if (urlInput) urlInput.value = market.market_url || '';
@ -988,7 +889,6 @@ class ZzimManager {
if (nicknameInput) nicknameInput.value = market.market_nickname || '';
if (forZzimCheckbox) forZzimCheckbox.checked = market.for_zzim !== false;
if (forMutualZzimCheckbox) forMutualZzimCheckbox.checked = market.for_mutual_zzim !== false;
if (visibleCheckbox) visibleCheckbox.checked = market.is_visible !== false;
// 모달 표시
if (modal) {
@ -1047,7 +947,6 @@ class ZzimManager {
const nicknameInput = document.getElementById('edit-market-nickname');
const forZzimCheckbox = document.getElementById('edit-market-for-zzim');
const forMutualZzimCheckbox = document.getElementById('edit-market-for-mutual-zzim');
const visibleCheckbox = document.getElementById('edit-market-visible');
const saveBtn = document.querySelector('.btn-modal-save');
const marketUrl = urlInput?.value.trim() || '';
@ -1055,7 +954,8 @@ class ZzimManager {
const marketNickname = nicknameInput?.value.trim() || '';
const forZzim = forZzimCheckbox?.checked || false;
const forMutualZzim = forMutualZzimCheckbox?.checked || false;
const isVisible = visibleCheckbox?.checked || false;
// 품앗이 대상이 체크되면 자동으로 노출도 true로 설정
const isVisible = forMutualZzim;
// 유효성 검사
if (!marketUrl || !marketName || !marketNickname) {
@ -1360,7 +1260,10 @@ class ZzimManager {
const market = mutualMarkets[i];
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} 완료`);
@ -1607,7 +1510,10 @@ class ZzimManager {
// URL에 찜하기 파라미터 추가
const urlObj = new URL(finalTargetUrl);
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가 아님)
if (!urlObj.searchParams.has('cp')) {
@ -1623,12 +1529,12 @@ class ZzimManager {
this.showSuccess(`찜하기 페이지로 이동합니다. (${market.market_nickname})`);
// 현재 탭에서 로그인 리다이렉트 URL로 이동
window.location.href = loginRedirectUrl;
// 찜 기록 (실제 찜하기는 새 페이지에서 실행됨)
await this.recordZzim(market, zzimType);
// 현재 탭에서 로그인 리다이렉트 URL로 이동
window.location.href = loginRedirectUrl;
} catch (error) {
console.error('찜하기 진행 오류:', error);
this.showError('찜하기 진행 중 오류가 발생했습니다: ' + error.message);
@ -1656,134 +1562,9 @@ class ZzimManager {
}
async updateZzimStats(count, zzimType, targetMarket = null) {
try {
console.log('찜 통계 업데이트 시작:', { count, zzimType, targetMarket });
// 현재 제한 정보 확인
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;
}
// 이 함수는 더 이상 직접 호출되지 않습니다.
// Background 스크립트가 처리합니다.
console.log('updateZzimStats는 이제 Background Script에서 처리됩니다.');
}
// 마켓별 찜받은 개수 업데이트
@ -1891,18 +1672,19 @@ class ZzimManager {
return { canZzim: true, remaining: dailyLimit - todayCount };
} 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 {
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: '알 수 없는 찜 타입입니다.' };
@ -1939,7 +1721,8 @@ class ZzimManager {
try {
console.log('[품앗이] 품앗이 마켓 목록 조회 시작');
// 내 현재 마일리지 확인 (users 테이블)
// 내 현재 마일리지 확인 (users 테이블) - 삭제: 품앗이는 마일리지를 버는 행위이므로 보유량 체크 불필요
/*
const myStatsResponse = await fetch(`${this.SUPABASE_URL}/rest/v1/users?id=eq.${this.user_id}&select=available_zzim_mile`, {
headers: {
'Authorization': `Bearer ${this.access_token}`,
@ -1959,6 +1742,7 @@ class ZzimManager {
if (myAvailableMileage <= 0) {
throw new Error('사용 가능한 마일리지가 없습니다. 먼저 다른 사람의 마켓에 찜을 받아 마일리지를 적립하세요.');
}
*/
// 다른 사용자들의 노출된 마켓 목록 가져오기 (품앗이용) - v_user_market_stats 뷰 사용
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/v_user_market_stats?user_id=neq.${this.user_id}&available_zzim_mile=gt.0&select=user_id,available_zzim_mile,my_markets&limit=100`, {
@ -2017,8 +1801,8 @@ class ZzimManager {
return new Date(b.created_at || 0) - new Date(a.created_at || 0);
});
// 내 마일리지로 처리 가능한 개수만큼 반환 (최대 10개)
const maxMarkets = Math.min(10, Math.floor(myAvailableMileage / 50)); // 마켓당 50개 찜 가정
// 내 마일리지로 처리 가능한 개수만큼 반환 (최대 10개) -> 수정: 제한 없음 (단, 하루 최대 10개 마켓 정도만)
const maxMarkets = 10;
const selectedMarkets = mutualMarkets.slice(0, maxMarkets);
console.log('[품앗이] 선택된 품앗이 마켓:', selectedMarkets.length);