616 lines
21 KiB
JavaScript
616 lines
21 KiB
JavaScript
// zzim.js - 찜관리 모듈
|
|
|
|
class ZzimManager {
|
|
constructor() {
|
|
this.SUPABASE_URL = null;
|
|
this.SUPABASE_ANON_KEY = null;
|
|
this.access_token = null;
|
|
this.user_id = null;
|
|
this.zzimInProgress = false;
|
|
}
|
|
|
|
async init() {
|
|
console.log('ZzimManager 초기화 중...');
|
|
|
|
try {
|
|
// Chrome Extension 환경에서 설정값 가져오기
|
|
if (typeof chrome !== 'undefined' && chrome.storage) {
|
|
const config = await chrome.storage.local.get(['zzim_config']);
|
|
|
|
if (config.zzim_config) {
|
|
this.access_token = config.zzim_config.ACCESS_TOKEN;
|
|
this.user_id = config.zzim_config.USER_ID;
|
|
this.SUPABASE_URL = config.zzim_config.SUPABASE_URL || "http://146.56.101.199:8000";
|
|
this.SUPABASE_ANON_KEY = config.zzim_config.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE";
|
|
} else {
|
|
// 기존 방식으로 폴백
|
|
const fallbackConfig = await chrome.storage.local.get(['access_token', 'user_id', 'SUPABASE_URL', 'SUPABASE_ANON_KEY']);
|
|
this.access_token = fallbackConfig.access_token;
|
|
this.user_id = fallbackConfig.user_id;
|
|
this.SUPABASE_URL = fallbackConfig.SUPABASE_URL || "http://146.56.101.199:8000";
|
|
this.SUPABASE_ANON_KEY = fallbackConfig.SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE";
|
|
}
|
|
|
|
if (!this.access_token || !this.user_id) {
|
|
throw new Error('로그인이 필요합니다.');
|
|
}
|
|
|
|
console.log('Chrome Extension 환경에서 설정 로드 완료', {
|
|
hasToken: !!this.access_token,
|
|
hasUserId: !!this.user_id,
|
|
hasUrl: !!this.SUPABASE_URL,
|
|
hasKey: !!this.SUPABASE_ANON_KEY
|
|
});
|
|
} else {
|
|
throw new Error('Chrome Extension 환경이 아닙니다.');
|
|
}
|
|
|
|
// 사용자 찜 설정 초기화
|
|
await this.initializeUserZzimSettings();
|
|
|
|
// 통계 및 마켓 정보 로드
|
|
await Promise.all([
|
|
this.loadZzimStats(),
|
|
this.loadMyMarkets()
|
|
]);
|
|
|
|
// 이벤트 바인딩
|
|
this.bindEvents();
|
|
|
|
console.log('ZzimManager 초기화 완료');
|
|
} catch (error) {
|
|
console.error('ZzimManager 초기화 실패:', error);
|
|
this.showError('초기화 실패: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async initializeUserZzimSettings() {
|
|
try {
|
|
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/rpc/initialize_user_zzim_settings`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.access_token}`,
|
|
'apikey': this.SUPABASE_ANON_KEY,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ user_uuid: this.user_id })
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.error('찜 설정 초기화 실패:', response.status);
|
|
}
|
|
} catch (error) {
|
|
console.error('찜 설정 초기화 오류:', error);
|
|
}
|
|
}
|
|
|
|
bindEvents() {
|
|
// 마켓 추가 버튼
|
|
const addMarketBtn = document.getElementById('add-market-btn');
|
|
if (addMarketBtn) {
|
|
addMarketBtn.addEventListener('click', () => this.addMarket());
|
|
}
|
|
|
|
// 내 마켓 찜하기 버튼
|
|
const myMarketZzimBtn = document.getElementById('my-market-zzim-btn');
|
|
if (myMarketZzimBtn) {
|
|
myMarketZzimBtn.addEventListener('click', () => this.startMyMarketZzim());
|
|
}
|
|
|
|
// 품앗이 찜하기 버튼
|
|
const mutualZzimBtn = document.getElementById('mutual-zzim-btn');
|
|
if (mutualZzimBtn) {
|
|
mutualZzimBtn.addEventListener('click', () => this.startMutualZzim());
|
|
}
|
|
|
|
// 마켓 URL 입력 시 자동 변환
|
|
const marketUrlInput = document.getElementById('market-url');
|
|
if (marketUrlInput) {
|
|
marketUrlInput.addEventListener('input', (e) => {
|
|
let url = e.target.value;
|
|
if (url && !url.startsWith('https://smartstore.naver.com/')) {
|
|
// URL이 smartstore로 시작하지 않으면 자동으로 앞에 추가
|
|
if (!url.startsWith('http')) {
|
|
url = 'https://smartstore.naver.com/' + url;
|
|
e.target.value = url;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async loadZzimStats() {
|
|
try {
|
|
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/zzim_settings?user_id=eq.${this.user_id}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${this.access_token}`,
|
|
'apikey': this.SUPABASE_ANON_KEY,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`찜 통계 로드 실패: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const stats = data[0] || {
|
|
current_daily_count: 0,
|
|
daily_zzim_limit: 100,
|
|
current_mileage: 0,
|
|
zzim_mileage_limit: 1000
|
|
};
|
|
|
|
// UI 업데이트 - HTML의 실제 ID와 일치하도록 수정
|
|
const todayCountEl = document.getElementById('today-zzim-count');
|
|
const todayLimitEl = document.getElementById('today-zzim-limit');
|
|
const mileageCountEl = document.getElementById('zzim-mileage');
|
|
const mileageLimitEl = document.getElementById('zzim-mileage-limit');
|
|
|
|
if (todayCountEl) todayCountEl.textContent = stats.current_daily_count;
|
|
if (todayLimitEl) todayLimitEl.textContent = stats.daily_zzim_limit;
|
|
if (mileageCountEl) mileageCountEl.textContent = stats.current_mileage;
|
|
if (mileageLimitEl) mileageLimitEl.textContent = stats.zzim_mileage_limit;
|
|
|
|
} catch (error) {
|
|
console.error('찜 통계 로드 오류:', error);
|
|
this.showError('찜 통계를 불러올 수 없습니다.');
|
|
}
|
|
}
|
|
|
|
async loadMyMarkets() {
|
|
try {
|
|
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?user_id=eq.${this.user_id}&is_active=eq.true&order=created_at.desc`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${this.access_token}`,
|
|
'apikey': this.SUPABASE_ANON_KEY,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`마켓 목록 로드 실패: ${response.status}`);
|
|
}
|
|
|
|
const markets = await response.json();
|
|
this.renderMarketsList(markets);
|
|
|
|
} catch (error) {
|
|
console.error('마켓 목록 로드 오류:', error);
|
|
this.showError('마켓 목록을 불러올 수 없습니다.');
|
|
}
|
|
}
|
|
|
|
renderMarketsList(markets) {
|
|
const marketsList = document.getElementById('my-markets-list');
|
|
if (!marketsList) return;
|
|
|
|
if (markets.length === 0) {
|
|
marketsList.innerHTML = '<p style="text-align: center; color: #666; padding: 20px;">등록된 마켓이 없습니다.</p>';
|
|
return;
|
|
}
|
|
|
|
marketsList.innerHTML = markets.map(market => `
|
|
<div class="market-item" data-id="${market.id}">
|
|
<div class="market-header">
|
|
<div class="market-info">
|
|
<div class="market-name">${market.market_name}</div>
|
|
<div class="market-nickname">${market.market_nickname}</div>
|
|
<div class="market-url">${market.market_url}</div>
|
|
</div>
|
|
<div class="market-actions">
|
|
<button onclick="zzimManager.editMarket(${market.id})" class="btn btn-warning">수정</button>
|
|
<button onclick="zzimManager.deleteMarket(${market.id})" class="btn btn-danger">삭제</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
async addMarket() {
|
|
const marketUrl = document.getElementById('market-url').value.trim();
|
|
const marketName = document.getElementById('market-name').value.trim();
|
|
const marketNickname = document.getElementById('market-nickname').value.trim();
|
|
|
|
if (!marketUrl || !marketName || !marketNickname) {
|
|
this.showError('모든 필드를 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
if (!marketUrl.startsWith('https://smartstore.naver.com/')) {
|
|
this.showError('올바른 네이버 스마트스토어 URL을 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.access_token}`,
|
|
'apikey': this.SUPABASE_ANON_KEY,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
user_id: this.user_id,
|
|
market_url: marketUrl,
|
|
market_name: marketName,
|
|
market_nickname: marketNickname
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.message || '마켓 추가 실패');
|
|
}
|
|
|
|
// 입력 필드 초기화
|
|
document.getElementById('market-url').value = '';
|
|
document.getElementById('market-name').value = '';
|
|
document.getElementById('market-nickname').value = '';
|
|
|
|
// 마켓 목록 다시 로드
|
|
await this.loadMyMarkets();
|
|
this.showSuccess('마켓이 추가되었습니다.');
|
|
|
|
} catch (error) {
|
|
console.error('마켓 추가 오류:', error);
|
|
this.showError('마켓 추가 중 오류가 발생했습니다: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async editMarket(marketId) {
|
|
// 편집 모달을 표시하거나 인라인 편집 구현
|
|
const newName = prompt('새로운 마켓 이름을 입력하세요:');
|
|
if (!newName) return;
|
|
|
|
const newNickname = prompt('새로운 마켓 별명을 입력하세요:');
|
|
if (!newNickname) return;
|
|
|
|
try {
|
|
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?id=eq.${marketId}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.access_token}`,
|
|
'apikey': this.SUPABASE_ANON_KEY,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
market_name: newName,
|
|
market_nickname: newNickname
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('마켓 수정 실패');
|
|
}
|
|
|
|
await this.loadMyMarkets();
|
|
this.showSuccess('마켓이 수정되었습니다.');
|
|
|
|
} catch (error) {
|
|
console.error('마켓 수정 오류:', error);
|
|
this.showError('마켓 수정 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
|
|
async deleteMarket(marketId) {
|
|
if (!confirm('정말로 이 마켓을 삭제하시겠습니까?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?id=eq.${marketId}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.access_token}`,
|
|
'apikey': this.SUPABASE_ANON_KEY,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
is_active: false
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('마켓 삭제 실패');
|
|
}
|
|
|
|
await this.loadMyMarkets();
|
|
this.showSuccess('마켓이 삭제되었습니다.');
|
|
|
|
} catch (error) {
|
|
console.error('마켓 삭제 오류:', error);
|
|
this.showError('마켓 삭제 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
|
|
async startMyMarketZzim() {
|
|
if (this.zzimInProgress) {
|
|
this.showError('찜하기가 이미 진행 중입니다.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 내 마켓 목록 가져오기
|
|
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?user_id=eq.${this.user_id}&is_active=eq.true`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${this.access_token}`,
|
|
'apikey': this.SUPABASE_ANON_KEY,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('마켓 목록을 가져올 수 없습니다.');
|
|
}
|
|
|
|
const markets = await response.json();
|
|
if (markets.length === 0) {
|
|
this.showError('등록된 마켓이 없습니다.');
|
|
return;
|
|
}
|
|
|
|
// 랜덤 마켓 선택
|
|
const randomMarket = markets[Math.floor(Math.random() * markets.length)];
|
|
this.showStatus(`"${randomMarket.market_nickname}" 마켓에서 찜하기를 시작합니다...`);
|
|
|
|
await this.executeZzim(randomMarket, 'my_market', false); // 포그라운드에서 실행
|
|
|
|
} catch (error) {
|
|
console.error('내 마켓 찜하기 오류:', error);
|
|
this.showError('내 마켓 찜하기 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
|
|
async startMutualZzim() {
|
|
if (this.zzimInProgress) {
|
|
this.showError('찜하기가 이미 진행 중입니다.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 다른 사용자의 마켓 목록 가져오기 (품앗이용)
|
|
const response = await fetch(`${this.SUPABASE_URL}/rest/v1/my_markets?user_id=neq.${this.user_id}&is_active=eq.true`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${this.access_token}`,
|
|
'apikey': this.SUPABASE_ANON_KEY,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('품앗이 마켓 목록을 가져올 수 없습니다.');
|
|
}
|
|
|
|
const markets = await response.json();
|
|
if (markets.length === 0) {
|
|
this.showError('품앗이할 마켓이 없습니다.');
|
|
return;
|
|
}
|
|
|
|
// 랜덤 마켓 선택
|
|
const randomMarket = markets[Math.floor(Math.random() * markets.length)];
|
|
this.showStatus(`품앗이 마켓 "${randomMarket.market_nickname}"에서 찜하기를 시작합니다...`);
|
|
|
|
await this.executeZzim(randomMarket, 'mutual', true); // 백그라운드에서 실행
|
|
|
|
} catch (error) {
|
|
console.error('품앗이 찜하기 오류:', error);
|
|
this.showError('품앗이 찜하기 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
|
|
async executeZzim(market, zzimType, inBackground = false) {
|
|
this.zzimInProgress = true;
|
|
|
|
try {
|
|
// 찜하기 스크립트
|
|
const zzimScript = `
|
|
(function() {
|
|
let zzimCount = 0;
|
|
const maxZzim = 50; // 최대 찜할 개수
|
|
|
|
function clickZzimButtons() {
|
|
const zzimButtons = document.querySelectorAll('.zzim_button:not(.active)');
|
|
console.log('찾은 찜 버튼 개수:', zzimButtons.length);
|
|
|
|
if (zzimButtons.length === 0) {
|
|
console.log('더 이상 찜할 상품이 없습니다.');
|
|
return false;
|
|
}
|
|
|
|
// 최대 10개씩 찜하기
|
|
const buttonsToClick = Array.from(zzimButtons).slice(0, Math.min(10, maxZzim - zzimCount));
|
|
|
|
buttonsToClick.forEach((btn, index) => {
|
|
setTimeout(() => {
|
|
try {
|
|
btn.click();
|
|
zzimCount++;
|
|
console.log(\`찜 버튼 클릭: \${zzimCount}개\`);
|
|
} catch (e) {
|
|
console.error('찜 버튼 클릭 오류:', e);
|
|
}
|
|
}, index * 500); // 0.5초 간격
|
|
});
|
|
|
|
return buttonsToClick.length > 0 && zzimCount < maxZzim;
|
|
}
|
|
|
|
// 페이지 스크롤 및 찜하기 반복
|
|
function scrollAndZzim() {
|
|
if (zzimCount >= maxZzim) {
|
|
console.log('최대 찜하기 완료:', zzimCount);
|
|
return;
|
|
}
|
|
|
|
// 페이지 하단으로 스크롤
|
|
window.scrollTo(0, document.body.scrollHeight);
|
|
|
|
setTimeout(() => {
|
|
if (clickZzimButtons()) {
|
|
setTimeout(scrollAndZzim, 3000); // 3초 후 다시 시도
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
// 시작
|
|
scrollAndZzim();
|
|
|
|
return zzimCount;
|
|
})();
|
|
`;
|
|
|
|
if (inBackground) {
|
|
// 백그라운드에서 실행 (새 탭에서 백그라운드로)
|
|
const tab = await chrome.tabs.create({
|
|
url: market.market_url,
|
|
active: false
|
|
});
|
|
|
|
// 페이지 로드 완료 후 스크립트 실행
|
|
chrome.tabs.onUpdated.addListener(function listener(tabId, info) {
|
|
if (tabId === tab.id && info.status === 'complete') {
|
|
chrome.tabs.onUpdated.removeListener(listener);
|
|
|
|
chrome.scripting.executeScript({
|
|
target: { tabId: tab.id },
|
|
func: eval(`(${zzimScript})`)
|
|
}).then(() => {
|
|
// 일정 시간 후 탭 닫기
|
|
setTimeout(() => {
|
|
chrome.tabs.remove(tab.id);
|
|
}, 30000); // 30초 후
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
// 포그라운드에서 실행 (현재 창에서)
|
|
window.open(market.market_url, '_blank');
|
|
}
|
|
|
|
// 찜 기록 저장
|
|
await this.recordZzim(market, zzimType);
|
|
|
|
this.showSuccess(`찜하기가 시작되었습니다. (${market.market_nickname})`);
|
|
|
|
} catch (error) {
|
|
console.error('찜하기 실행 오류:', error);
|
|
this.showError('찜하기 실행 중 오류가 발생했습니다.');
|
|
} finally {
|
|
this.zzimInProgress = false;
|
|
}
|
|
}
|
|
|
|
async recordZzim(market, zzimType) {
|
|
try {
|
|
// 찜 기록 저장
|
|
await fetch(`${this.SUPABASE_URL}/rest/v1/jjim`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.access_token}`,
|
|
'apikey': this.SUPABASE_ANON_KEY,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
user_id: this.user_id,
|
|
market_url: market.market_url,
|
|
market_name: market.market_name,
|
|
market_nickname: market.market_nickname,
|
|
zzim_type: zzimType,
|
|
product_count: 50 // 예상 찜 개수 (실제로는 스크립트 결과를 받아야 함)
|
|
})
|
|
});
|
|
|
|
// 통계 업데이트
|
|
await this.updateZzimStats(50, zzimType);
|
|
|
|
// UI 새로고침
|
|
await this.loadZzimStats();
|
|
|
|
} catch (error) {
|
|
console.error('찜 기록 저장 오류:', error);
|
|
}
|
|
}
|
|
|
|
async updateZzimStats(count, zzimType) {
|
|
try {
|
|
const mileageIncrease = zzimType === 'mutual' ? count * 2 : count; // 품앗이는 마일리지 2배
|
|
|
|
await fetch(`${this.SUPABASE_URL}/rest/v1/zzim_settings?user_id=eq.${this.user_id}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.access_token}`,
|
|
'apikey': this.SUPABASE_ANON_KEY,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
current_daily_count: `current_daily_count + ${count}`,
|
|
current_mileage: `current_mileage + ${mileageIncrease}`
|
|
})
|
|
});
|
|
} catch (error) {
|
|
console.error('찜 통계 업데이트 오류:', error);
|
|
}
|
|
}
|
|
|
|
showStatus(message) {
|
|
const statusEl = document.getElementById('zzim-status');
|
|
const errorEl = document.getElementById('zzim-error');
|
|
const successEl = document.getElementById('zzim-success');
|
|
|
|
// 모든 상태 메시지 숨기기
|
|
if (statusEl) statusEl.style.display = 'none';
|
|
if (errorEl) errorEl.style.display = 'none';
|
|
if (successEl) successEl.style.display = 'none';
|
|
|
|
// 상태 메시지 표시
|
|
if (statusEl) {
|
|
statusEl.textContent = message;
|
|
statusEl.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
showError(message) {
|
|
const statusEl = document.getElementById('zzim-status');
|
|
const errorEl = document.getElementById('zzim-error');
|
|
const successEl = document.getElementById('zzim-success');
|
|
|
|
// 모든 상태 메시지 숨기기
|
|
if (statusEl) statusEl.style.display = 'none';
|
|
if (errorEl) errorEl.style.display = 'none';
|
|
if (successEl) successEl.style.display = 'none';
|
|
|
|
// 오류 메시지 표시
|
|
if (errorEl) {
|
|
errorEl.textContent = message;
|
|
errorEl.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
showSuccess(message) {
|
|
const statusEl = document.getElementById('zzim-status');
|
|
const errorEl = document.getElementById('zzim-error');
|
|
const successEl = document.getElementById('zzim-success');
|
|
|
|
// 모든 상태 메시지 숨기기
|
|
if (statusEl) statusEl.style.display = 'none';
|
|
if (errorEl) errorEl.style.display = 'none';
|
|
if (successEl) successEl.style.display = 'none';
|
|
|
|
// 성공 메시지 표시
|
|
if (successEl) {
|
|
successEl.textContent = message;
|
|
successEl.style.display = 'block';
|
|
}
|
|
}
|
|
}
|
|
|
|
// 전역 인스턴스 생성
|
|
let zzimManager;
|
|
|
|
// DOM 로드 완료 후 초기화
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
zzimManager = new ZzimManager();
|
|
await zzimManager.init();
|
|
});
|