Merge branch 'extended' of ssh://cckb9998.synology.me:30022/ckh08045/SearchTrademark into extended

This commit is contained in:
9700X_PC 2025-11-27 15:05:01 +09:00
commit bdde76d7a6
44 changed files with 2567 additions and 592 deletions

View File

@ -0,0 +1,4 @@
chrome.runtime.onInstalled.addListener(() => {
console.log("찜하기 자동화 확장 프로그램이 설치되었습니다.");
});

View File

@ -0,0 +1,166 @@
(() => {
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
window.alert = function(msg) {
console.warn("차단된 alert:", msg);
};
if (window.__zzimAutomationRunning) {
console.warn("이미 실행 중인 자동화 스크립트가 있습니다.");
return;
}
window.__zzimAutomationRunning = true;
let isRunning = true;
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
isRunning = false;
console.log("찜하기 자동화 중단됨 (ESC)");
}
});
function getStoreName() {
const el = document.querySelector('.KasFrJs3SA');
return el ? el.innerText.trim() : "스토어";
}
function waitForZzimButtons(callback) {
const check = () => {
const buttons = document.querySelectorAll('button.zzim_button[type="button"]');
if (buttons.length > 0) {
callback(buttons);
} else {
requestAnimationFrame(check);
}
};
requestAnimationFrame(check);
}
async function waitForButtonToChange(btn, timeout = 1000) {
const start = Date.now();
while (Date.now() - start < timeout) {
const pressed = btn.getAttribute("aria-pressed");
const label = btn.innerText?.trim();
if (pressed === "true" || (label && !label.includes("찜하기"))) {
return true;
}
await sleep(50);
}
return false;
}
function createProgressUI() {
const ui = document.createElement("div");
ui.id = "zzim-progress-ui";
ui.style.position = "fixed";
ui.style.bottom = "20px";
ui.style.right = "20px";
ui.style.background = "#222";
ui.style.color = "#fff";
ui.style.padding = "10px 15px";
ui.style.borderRadius = "8px";
ui.style.zIndex = "99999";
ui.style.fontSize = "14px";
ui.style.whiteSpace = "pre-line";
ui.style.boxShadow = "0 0 5px rgba(0,0,0,0.5)";
ui.innerText = "찜하기 자동화 시작...";
document.body.appendChild(ui);
return ui;
}
async function runAutomation(startPage, finalPage) {
const url = new URL(window.location.href);
const storeName = getStoreName();
let currentPage = parseInt(url.searchParams.get("page") || startPage);
let totalClicked = 0;
const progressUI = createProgressUI();
const today = new Date().toISOString().split("T")[0];
if (currentPage > finalPage) {
progressUI.innerText = `${storeName}
페이지 ${finalPage} / ${finalPage}
완료: 0
완료`;
alert(`${storeName} 작업 이미 완료됨`);
return;
}
while (isRunning && currentPage <= finalPage) {
progressUI.innerText = `${storeName}
페이지 ${currentPage} / ${finalPage}
완료: ${totalClicked}
진행중`;
await new Promise(resolve => waitForZzimButtons(resolve));
const buttons = Array.from(document.querySelectorAll('button.zzim_button[type="button"]'));
for (let i = 0; i < buttons.length; i++) {
const btn = buttons[i];
if (!isRunning) break;
const pressed = btn.getAttribute("aria-pressed");
const label = btn.innerText?.trim();
if (pressed === "false" && label.includes("찜하기")) {
try {
btn.click();
const changed = await waitForButtonToChange(btn);
if (changed) {
totalClicked++;
}
progressUI.innerText = `${storeName}
페이지 ${currentPage} / ${finalPage}
완료: ${totalClicked}
진행중`;
await sleep(200);
} catch (e) {
console.warn("❌ 클릭 실패:", e);
}
}
}
if (!isRunning || currentPage >= finalPage) break;
const nextPage = currentPage + 1;
url.searchParams.set("page", nextPage);
await sleep(1000);
window.location.href = url.toString();
setTimeout(() => document.dispatchEvent(new KeyboardEvent("keydown", {key: "Enter"})), 1500);
return;
}
// 🔒 루프 종료 후 항상 최종 처리
const now = new Date();
const timestamp = now.getFullYear() + "-" +
String(now.getMonth()+1).padStart(2, '0') + "-" +
String(now.getDate()).padStart(2, '0') + " " +
String(now.getHours()).padStart(2, '0') + ":" +
String(now.getMinutes()).padStart(2, '0');
progressUI.innerText = `${storeName}
페이지 ${currentPage} / ${finalPage}
완료: ${totalClicked}
완료`;
alert(`${storeName}\n${timestamp}\n${totalClicked}개 찜 완료`);
}
chrome.storage.local.get(["isZzimRun", "pageCount", "_zzimStartPage", "_zzimFinalPage"], (res) => {
if (!res.isZzimRun) return;
// 제거는 완료 시점에만 수행됨
const url = new URL(window.location.href);
const current = parseInt(url.searchParams.get("page") || "1");
if (!res._zzimStartPage || !res._zzimFinalPage) {
const startPage = current;
const finalPage = startPage + (res.pageCount || 1) - 1;
chrome.storage.local.set({ _zzimStartPage: startPage, _zzimFinalPage: finalPage }, () => {
runAutomation(startPage, finalPage);
});
} else {
runAutomation(res._zzimStartPage, res._zzimFinalPage);
}
});
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

View File

@ -0,0 +1,25 @@
{
"manifest_version": 3,
"name": "찜하기 자동화",
"version": "1.0",
"description": "네이버 스마트스토어에서 찜하기 버튼을 자동으로 누릅니다.",
"permissions": ["scripting", "storage", "tabs"],
"host_permissions": ["https://smartstore.naver.com/*"],
"action": {
"default_icon": {
"16": "icon.png"
},
"default_title": "찜하기 자동화",
"default_popup": "options.html"
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["https://smartstore.naver.com/*"],
"js": ["content.js"]
}
]
}

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<style>
body { font-family: sans-serif; padding: 10px; }
.title { color: red; font-weight: bold; font-size: 18px; }
.label { color: red; margin-top: 10px; display: block; }
</style>
</head>
<body>
<div class="title"></div>
<label class="label">페이지 수 :
<input id="pageCount" type="number" value="20">
</label>
<button id="runBtn" style="width:60px; height:30px;">실행</button>
<button id="stopBtn" style="width:60px; height:30px;">종료</button></button>
<script src="options.js"></script>
</body>
</html>

View File

@ -0,0 +1,23 @@
document.getElementById("runBtn").addEventListener("click", () => {
const pageCount = parseInt(document.getElementById("pageCount").value);
const startedAt = new Date().toISOString().slice(0, 16).replace("T", " ");
chrome.storage.local.remove(["_zzimStartPage", "_zzimFinalPage", "isZzimRun", "startedAt"], () => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]) {
chrome.storage.local.set({ pageCount, isZzimRun: true, startedAt }, () => {
chrome.scripting.executeScript({
target: { tabId: tabs[0].id },
files: ["content.js"]
});
});
}
});
});
});
document.getElementById("stopBtn").addEventListener("click", () => {
chrome.storage.local.set({ isZzimRun: false }, () => {
alert("자동화가 종료되었습니다.");
});
});

View File

@ -19,6 +19,9 @@ chrome.runtime.onInstalled.addListener(() => {
// 새 어록 감지 알람 생성 (5분마다)
chrome.alarms.create("checkNewSayings", { periodInMinutes: 5 });
// 1시간마다 자동 품앗이 찜 알람
chrome.alarms.create("autoMutualZzim", { periodInMinutes: 60 });
// 초기 마지막 확인 시간 설정
chrome.storage.local.set({ lastSayingsCheck: Date.now() });
});
@ -325,6 +328,9 @@ chrome.alarms.onAlarm.addListener((alarm) => {
console.log("[background.js] 서비스 워커 유지 알람 실행됨");
} else if (alarm.name === "checkNewSayings") {
checkForNewSayings();
} else if (alarm.name === "autoMutualZzim") {
console.log("[background.js] autoMutualZzim 주기 실행");
chrome.runtime.sendMessage({ action: "autoMutualZzim" });
}
});
@ -2699,7 +2705,9 @@ class TimeAlarmManager {
enabled: true,
workTime: 60, // 분
restTime: 5, // 분
autoZzim: false
autoZzim: false,
pomodoro: false,
cycle: 0 // 포모도로 현재 사이클(0~3)
};
this.startTime = null;
}
@ -2754,9 +2762,10 @@ class TimeAlarmManager {
}
this.startTime = Date.now();
const workTimeMs = this.settings.workTime * 60 * 1000;
const workMin = this.settings.pomodoro ? 35 : this.settings.workTime;
const workTimeMs = workMin * 60 * 1000;
console.log(`[TimeAlarm] 작업 타이머 시작: ${this.settings.workTime}`);
console.log(`[TimeAlarm] 작업 타이머 시작: ${workMin}`);
this.workTimer = setTimeout(() => {
this.showRestModal();
@ -2787,7 +2796,8 @@ class TimeAlarmManager {
}
startBreakTimer(popupId) {
const breakTimeMs = this.settings.restTime * 60 * 1000;
const breakTimeMin = this.settings.restTime;
const breakTimeMs = breakTimeMin * 60 * 1000;
console.log(`[TimeAlarm] 휴식 타이머 시작: ${this.settings.restTime}`);
@ -2802,6 +2812,11 @@ class TimeAlarmManager {
// 작업 완료 알림
this.showWorkCompleteNotification();
// 포모도로 모드: 사이클 증가 및 리셋
if (this.settings.pomodoro) {
this.settings.cycle = (this.settings.cycle + 1) % 4;
}
// 다음 작업 타이머 시작
this.startWorkTimer();
@ -2813,7 +2828,7 @@ class TimeAlarmManager {
type: 'basic',
iconUrl: 'icon.png',
title: '휴식 시간입니다! 🧘‍♀️',
message: `${this.settings.workTime}분간 수고하셨습니다. ${this.settings.restTime}분간 휴식을 취하세요.`
message: `${this.settings.pomodoro ? 35 : this.settings.workTime}분간 수고하셨습니다. ${this.settings.restTime}분간 휴식을 취하세요.`
});
}
@ -2876,7 +2891,7 @@ class TimeAlarmManager {
// 남은 시간 계산
const now = Date.now();
const elapsed = now - this.startTime;
const totalWorkTime = this.settings.workTime * 60 * 1000;
const totalWorkTime = (this.settings.pomodoro ? 35 : this.settings.workTime) * 60 * 1000;
const remainingTime = totalWorkTime - elapsed;
if (remainingTime <= 0) {
@ -2889,7 +2904,7 @@ class TimeAlarmManager {
return {
isRunning: true,
remainingTime: remainingTime,
workTime: this.settings.workTime,
workTime: this.settings.pomodoro ? 35 : this.settings.workTime,
restTime: this.settings.restTime,
startTime: this.startTime,
elapsed: elapsed
@ -3590,3 +3605,110 @@ async function handleDirectTranslation(selectedText, tab) {
});
}
}
// ===== 메시지 리스너: 백그라운드 자동 품앗이 =====
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message && message.action === 'autoMutualZzim') {
autoMutualZzim()
.then(() => sendResponse({ success: true }))
.catch(err => sendResponse({ success: false, error: err.message }));
return true; // async 응답
}
});
// ===== 자동 품앗이 찜 메인 함수 =====
async function autoMutualZzim() {
try {
console.log('[autoMutualZzim] 시작');
// 1. 기본 정보 및 설정
const { access_token, user_id } = await chrome.storage.local.get(['access_token', 'user_id']);
if (!access_token || !user_id) {
console.log('[autoMutualZzim] 토큰/사용자 정보 없음');
return;
}
const { SUPABASE_URL, SUPABASE_ANON_KEY } = getBackendConfig();
// 2. 내 마일리지 및 회원등급 조회
const userRes = await fetch(`${SUPABASE_URL}/rest/v1/users?id=eq.${user_id}&select=available_zzim_mile,membership_level`, {
headers: {
Authorization: `Bearer ${access_token}`,
apikey: SUPABASE_ANON_KEY,
'Content-Type': 'application/json'
}
});
if (!userRes.ok) {
console.warn('[autoMutualZzim] 사용자 조회 실패', userRes.status);
return;
}
const me = (await userRes.json())[0];
const myAvail = me?.available_zzim_mile || 0;
const myLevel = me?.membership_level || 'basic';
// 2-1. 등급별 한도 확인
const levelRes = await fetch(`${SUPABASE_URL}/rest/v1/membership_levels?level=eq.${myLevel}&select=max_zzim_mileage,mileage_per_zzim`, {
headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${access_token}` }
});
const levelConf = levelRes.ok ? (await levelRes.json())[0] : { max_zzim_mileage: 500, mileage_per_zzim: 1 };
if (myAvail >= levelConf.max_zzim_mileage) {
console.log('[autoMutualZzim] 마일리지 최대치 도달 실행 안 함');
return;
}
// 3. 후보 마켓 조회 (v_user_market_stats 뷰)
const candRes = await fetch(`${SUPABASE_URL}/rest/v1/v_user_market_stats?user_id=neq.${user_id}&available_zzim_mile=gt.0&select=user_id,available_zzim_mile,my_markets&limit=100`, {
headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${access_token}`, 'Content-Type': 'application/json' }
});
if (!candRes.ok) {
console.warn('[autoMutualZzim] 후보 조회 실패', candRes.status);
return;
}
const users = await candRes.json();
const pool = [];
users.forEach(u => {
const markets = u.my_markets || [];
markets.forEach(m => {
if (m.is_visible !== false && m.for_mutual_zzim !== false) {
pool.push({ ...m, owner_user_id: u.user_id, owner_available_mileage: u.available_zzim_mile });
}
});
});
if (pool.length === 0) {
console.log('[autoMutualZzim] 후보 마켓 없음');
return;
}
// 4. 랜덤 1개 선택
const target = pool[Math.floor(Math.random() * pool.length)];
const targetUrl = `${target.market_url.replace(/\/$/, '')}/category/ALL?cp=1&auto_zzim=true&max_zzim=50`;
// 5. 백그라운드 찜 실행
const msg = {
action: 'executeBackgroundZzim',
market: { ...target, target_url: targetUrl },
zzimType: 'mutual',
userId: user_id,
accessToken: access_token,
settings: { totalDelay: 1000, latestFirst: false, backgroundMode: true }
};
chrome.runtime.sendMessage(msg, (res) => {
if (chrome.runtime.lastError) {
console.error('[autoMutualZzim] runtime error', chrome.runtime.lastError.message);
} else if (res && res.success) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: '🎉 자동 품앗이 완료',
message: `${target.market_nickname || '마켓'}에 찜 50개 완료!`
});
} else {
console.warn('[autoMutualZzim] 실행 실패', res?.error);
}
});
} catch (e) {
console.error('[autoMutualZzim] 오류', e);
}
}

1581
content.js

File diff suppressed because it is too large Load Diff

View File

@ -1,137 +0,0 @@
-- 찜관리 기능을 위한 데이터베이스 스키마
-- 1. 찜 기록 테이블 (public.jjim)
CREATE TABLE IF NOT EXISTS public.jjim (
id BIGSERIAL PRIMARY KEY,
user_id UUID NOT NULL,
market_url TEXT NOT NULL,
market_name TEXT,
market_nickname TEXT,
product_count INTEGER DEFAULT 0,
zzim_type VARCHAR(20) NOT NULL CHECK (zzim_type IN ('my_market', 'mutual')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
is_active BOOLEAN DEFAULT TRUE,
CONSTRAINT fk_jjim_user_id FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE
);
-- 2. 사용자 마켓 등록 테이블 (public.my_markets)
CREATE TABLE IF NOT EXISTS public.my_markets (
id BIGSERIAL PRIMARY KEY,
user_id UUID NOT NULL,
market_url TEXT NOT NULL,
market_name TEXT NOT NULL,
market_nickname TEXT NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT fk_my_markets_user_id FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE,
CONSTRAINT unique_user_market_url UNIQUE (user_id, market_url)
);
-- 3. 찜 설정 테이블 (public.zzim_settings)
CREATE TABLE IF NOT EXISTS public.zzim_settings (
id BIGSERIAL PRIMARY KEY,
user_id UUID NOT NULL,
daily_zzim_limit INTEGER DEFAULT 100,
zzim_mileage_limit INTEGER DEFAULT 1000,
current_daily_count INTEGER DEFAULT 0,
current_mileage INTEGER DEFAULT 0,
last_reset_date DATE DEFAULT CURRENT_DATE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT fk_zzim_settings_user_id FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE,
CONSTRAINT unique_user_zzim_settings UNIQUE (user_id)
);
-- 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_jjim_user_id ON public.jjim(user_id);
CREATE INDEX IF NOT EXISTS idx_jjim_created_at ON public.jjim(created_at);
CREATE INDEX IF NOT EXISTS idx_jjim_zzim_type ON public.jjim(zzim_type);
CREATE INDEX IF NOT EXISTS idx_my_markets_user_id ON public.my_markets(user_id);
CREATE INDEX IF NOT EXISTS idx_zzim_settings_user_id ON public.zzim_settings(user_id);
-- RLS (Row Level Security) 정책 설정
ALTER TABLE public.jjim ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.my_markets ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.zzim_settings ENABLE ROW LEVEL SECURITY;
-- 찜 기록 테이블 RLS 정책
CREATE POLICY "Users can view own jjim records" ON public.jjim
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own jjim records" ON public.jjim
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own jjim records" ON public.jjim
FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Users can delete own jjim records" ON public.jjim
FOR DELETE USING (auth.uid() = user_id);
-- 마켓 등록 테이블 RLS 정책
CREATE POLICY "Users can view own markets" ON public.my_markets
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own markets" ON public.my_markets
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own markets" ON public.my_markets
FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Users can delete own markets" ON public.my_markets
FOR DELETE USING (auth.uid() = user_id);
-- 찜 설정 테이블 RLS 정책
CREATE POLICY "Users can view own zzim settings" ON public.zzim_settings
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own zzim settings" ON public.zzim_settings
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own zzim settings" ON public.zzim_settings
FOR UPDATE USING (auth.uid() = user_id);
-- 트리거 함수: updated_at 자동 업데이트
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- 트리거 생성
CREATE TRIGGER update_jjim_updated_at BEFORE UPDATE ON public.jjim
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_my_markets_updated_at BEFORE UPDATE ON public.my_markets
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_zzim_settings_updated_at BEFORE UPDATE ON public.zzim_settings
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- 초기 설정 데이터 삽입 함수
CREATE OR REPLACE FUNCTION initialize_user_zzim_settings(user_uuid UUID)
RETURNS VOID AS $$
BEGIN
INSERT INTO public.zzim_settings (user_id, daily_zzim_limit, zzim_mileage_limit, current_daily_count, current_mileage)
VALUES (user_uuid, 100, 1000, 0, 0)
ON CONFLICT (user_id) DO NOTHING;
END;
$$ LANGUAGE plpgsql;
-- 일일 카운트 리셋 함수
CREATE OR REPLACE FUNCTION reset_daily_zzim_count()
RETURNS VOID AS $$
BEGIN
UPDATE public.zzim_settings
SET
current_daily_count = 0,
last_reset_date = CURRENT_DATE
WHERE last_reset_date < CURRENT_DATE;
END;
$$ LANGUAGE plpgsql;

View File

@ -21,6 +21,7 @@
"*://smartstore.naver.com/*",
"*://translate.googleapis.com/*",
"*://api.mymemory.translated.net/*",
"https://market.m.taobao.com/*",
"*://openapi.naver.com/*",
"*://api-free.deepl.com/*",
"*://api.deepl.com/*",
@ -34,6 +35,7 @@
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"all_frames": true,
"run_at": "document_end"
}
],

View File

@ -330,7 +330,7 @@
<ul class="feature-list">
<li>번역할 문장이나 단어를 드래그 후 단축키(컨트롤+쉬프트+Z) 입력</li>
<li>웹페이지에서 바로 한글과 중국어를 빠르게 번역</li>
<li>위챗등의 대화에서 한글 입력 후 선택 및 단축키로 바로 번역</li>
<li>위챗등의 대화에서 한글 입력 후 선택 및 단축키로 바로 번역[현재 위챗의 특수성으로 적용되지않음]</li>
<li>웹페이지에서 중국어를 드래그 후 단축키로 바로 번역 결과 확인</li>
</ul>
</div>
@ -343,6 +343,7 @@
<li>휴식시간 추천 활동</li>
<li>실시간 타이머 알림</li>
<li>개인화된 시간 설정</li>
<li></li>[포모도로 타이머가 활성화 되면 35분/5분으로 기본설정되며 4사이클 총180분(3시간) 휴식시간 추천 활동 제공]</li>
</ul>
</div>

View File

@ -34,8 +34,8 @@ async function getBackendConfig() {
}
// 디버그 모드 플래그 (개발 시에만 true로 설정)
const DEBUG_MODE = true; // false로 설정하면 디버그 정보 숨김
// const DEBUG_MODE = false; // false로 설정하면 디버그 정보 숨김
// const DEBUG_MODE = true; // false로 설정하면 디버그 정보 숨김
const DEBUG_MODE = false; // false로 설정하면 디버그 정보 숨김
// 로그 레벨 정의
const LOG_LEVELS = {

View File

@ -542,21 +542,29 @@
<div class="toggle-switch" id="timeAlarmToggle"></div>
<span>알람 활성화/비활성화</span>
</div>
<!-- 포모도로 토글 -->
<div class="time-input-group">
<label>포모도로:</label>
<div class="toggle-switch" id="pomodoroToggle"></div>
<span>35분 작업 / 5분 휴식 (4회) 고정</span>
</div>
<div class="time-input-group">
<label>작업 시간:</label>
<input type="number" id="workTimeInput" min="1" max="480" value="60">
<input type="number" id="workTimeInput" min="60" max="480" value="60">
<span>분 (작업 후 휴식 알림)</span>
</div>
<div class="time-input-group">
<label>휴식 시간:</label>
<input type="number" id="restTimeInput" min="1" max="60" value="5">
<input type="number" id="restTimeInput" min="1" max="15" value="5">
<span>분 (휴식 시간 길이)</span>
</div>
<div class="checkbox-group">
<input type="checkbox" id="autoZzimCheckbox" disabled>
<!-- <input type="checkbox" id="autoZzimCheckbox" disabled> -->
<input type="checkbox" id="autoZzimCheckbox">
<label for="autoZzimCheckbox">휴식 중 자동 찜 기능 활성화</label>
</div>
</div>

View File

@ -236,7 +236,9 @@ class SettingsManager {
enabled: true,
workTime: 60, // 분
restTime: 5, // 분
autoZzim: false
autoZzim: false,
pomodoro: false,
cycle: 0
};
console.log('시간 알람 설정 로드 완료:', this.timeAlarmSettings);
} catch (error) {
@ -245,7 +247,9 @@ class SettingsManager {
enabled: true,
workTime: 60,
restTime: 5,
autoZzim: false
autoZzim: false,
pomodoro: false,
cycle: 0
};
}
}
@ -321,6 +325,7 @@ class SettingsManager {
const workTimeInput = document.getElementById('workTimeInput');
const restTimeInput = document.getElementById('restTimeInput');
const autoZzimCheckbox = document.getElementById('autoZzimCheckbox');
const pomodoroToggle = document.getElementById('pomodoroToggle');
if (workTimeInput) {
workTimeInput.addEventListener('change', (e) => {
@ -339,6 +344,18 @@ class SettingsManager {
this.timeAlarmSettings.autoZzim = e.target.checked;
});
}
if (pomodoroToggle) {
pomodoroToggle.addEventListener('click', () => {
this.timeAlarmSettings.pomodoro = !this.timeAlarmSettings.pomodoro;
// 포모도로가 활성화되면 고정 값 적용
if (this.timeAlarmSettings.pomodoro) {
this.timeAlarmSettings.workTime = 35;
this.timeAlarmSettings.restTime = 5;
}
this.updateTimeAlarmUI();
});
}
}
updateUI() {
@ -388,12 +405,23 @@ class SettingsManager {
}
}
const pomodoroToggle = document.getElementById('pomodoroToggle');
if (pomodoroToggle) {
if (this.timeAlarmSettings.pomodoro) {
pomodoroToggle.classList.add('active');
} else {
pomodoroToggle.classList.remove('active');
}
}
if (workTimeInput) {
workTimeInput.value = this.timeAlarmSettings.workTime;
workTimeInput.disabled = this.timeAlarmSettings.pomodoro;
}
if (restTimeInput) {
restTimeInput.value = this.timeAlarmSettings.restTime;
restTimeInput.disabled = this.timeAlarmSettings.pomodoro;
}
if (autoZzimCheckbox) {
@ -412,6 +440,12 @@ class SettingsManager {
this.showLoading(true);
// 시간 알람 설정 업데이트
if (this.timeAlarmSettings.pomodoro) {
// 강제 고정 값
this.timeAlarmSettings.workTime = 35;
this.timeAlarmSettings.restTime = 5;
}
const workTimeInput = document.getElementById('workTimeInput');
const restTimeInput = document.getElementById('restTimeInput');
const autoZzimCheckbox = document.getElementById('autoZzimCheckbox');
@ -484,10 +518,12 @@ class SettingsManager {
// 시간 알람 설정 초기화
this.timeAlarmSettings = {
enabled: false,
enabled: true,
workTime: 60,
restTime: 5,
autoZzim: false
autoZzim: false,
pomodoro: false,
cycle: 0
};
// 저장소에서 시간 알람 설정 제거

195
test/database_schema.sql Normal file
View File

@ -0,0 +1,195 @@
-- 찜관리 기능을 위한 데이터베이스 스키마
-- 1. 찜 기록 테이블 (public.jjim)
CREATE TABLE IF NOT EXISTS public.jjim (
id BIGSERIAL PRIMARY KEY,
user_id UUID NOT NULL,
market_url TEXT NOT NULL,
market_name TEXT,
market_nickname TEXT,
zzim_count INTEGER DEFAULT 0,
zzim_type VARCHAR(20) NOT NULL CHECK (zzim_type IN ('my_market', 'mutual')),
mileage_earned INTEGER DEFAULT 0,
target_user_id UUID,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT fk_jjim_user_id FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE,
CONSTRAINT fk_jjim_target_user_id FOREIGN KEY (target_user_id) REFERENCES auth.users(id) ON DELETE SET NULL
);
-- 2. 사용자 마켓 통합 테이블 (public.user_markets) - 찜 관련 필드 추가
CREATE TABLE IF NOT EXISTS public.user_markets (
id BIGSERIAL PRIMARY KEY,
user_id UUID NOT NULL,
-- 마켓 목록 (JSONB 배열)
my_markets JSONB DEFAULT '[]'::jsonb,
-- 찜 통계 필드들 (users 테이블에서 이관)
my_zzim INTEGER DEFAULT 0, -- 받은 찜 총 개수
zzim_mile INTEGER DEFAULT 0, -- 총 마일리지
available_zzim_mile INTEGER DEFAULT 0, -- 사용 가능한 마일리지
today_zzim_count INTEGER DEFAULT 0, -- 오늘 찜한 개수
today_zzim_date DATE DEFAULT CURRENT_DATE, -- 마지막 찜한 날짜
-- 회원등급 (users 테이블과 동기화)
membership_level VARCHAR(20) DEFAULT 'basic',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT fk_user_markets_user_id FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE,
CONSTRAINT unique_user_markets_user_id UNIQUE (user_id)
);
-- 3. 회원등급별 찜 제한 테이블 (public.membership_zzim_limits) - 개선
CREATE TABLE IF NOT EXISTS public.membership_zzim_limits (
id BIGSERIAL PRIMARY KEY,
membership_level VARCHAR(20) NOT NULL UNIQUE,
-- 찜 제한 설정
daily_zzim_limit INTEGER DEFAULT 50, -- 일일 찜 제한
max_zzim_mileage INTEGER DEFAULT 500, -- 최대 마일리지 보유량
mileage_per_zzim INTEGER DEFAULT 1, -- 찜당 마일리지
-- 추가 제한 설정
max_markets INTEGER DEFAULT 5, -- 최대 등록 가능한 마켓 수
mutual_zzim_enabled BOOLEAN DEFAULT TRUE, -- 품앗이 기능 사용 가능 여부
background_zzim_enabled BOOLEAN DEFAULT FALSE, -- 백그라운드 찜 기능 사용 가능 여부
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 4. 기본 회원등급 데이터 삽입
INSERT INTO public.membership_zzim_limits (membership_level, daily_zzim_limit, max_zzim_mileage, mileage_per_zzim, max_markets, mutual_zzim_enabled, background_zzim_enabled)
VALUES
('basic', 50, 500, 1, 3, TRUE, FALSE),
('premium', 100, 1000, 1, 10, TRUE, TRUE),
('vip', 200, 2000, 1, 20, TRUE, TRUE)
ON CONFLICT (membership_level) DO UPDATE SET
daily_zzim_limit = EXCLUDED.daily_zzim_limit,
max_zzim_mileage = EXCLUDED.max_zzim_mileage,
mileage_per_zzim = EXCLUDED.mileage_per_zzim,
max_markets = EXCLUDED.max_markets,
mutual_zzim_enabled = EXCLUDED.mutual_zzim_enabled,
background_zzim_enabled = EXCLUDED.background_zzim_enabled,
updated_at = NOW();
-- 5. 마켓 데이터 구조 예시 (my_markets JSONB 필드 내용)
/*
my_markets JSONB :
[
{
"market_url": "https://smartstore.naver.com/example",
"market_name": "예시 마켓",
"market_nickname": "내 마켓",
"is_visible": true, -- 다른 사람에게 노출 여부
"for_zzim": true, -- 내 마켓 찜하기 대상 여부
"for_mutual_zzim": true, -- 품앗이 대상 여부 (새로 추가)
"zzim_received_count": 0, -- 받은 찜 개수
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]
*/
-- 6. 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_jjim_user_id ON public.jjim(user_id);
CREATE INDEX IF NOT EXISTS idx_jjim_target_user_id ON public.jjim(target_user_id);
CREATE INDEX IF NOT EXISTS idx_jjim_created_at ON public.jjim(created_at);
CREATE INDEX IF NOT EXISTS idx_jjim_zzim_type ON public.jjim(zzim_type);
CREATE INDEX IF NOT EXISTS idx_user_markets_user_id ON public.user_markets(user_id);
CREATE INDEX IF NOT EXISTS idx_user_markets_membership_level ON public.user_markets(membership_level);
CREATE INDEX IF NOT EXISTS idx_user_markets_today_zzim_date ON public.user_markets(today_zzim_date);
-- 7. RLS (Row Level Security) 정책 설정
-- user_markets 테이블: 품앗이를 위해 읽기는 모든 사용자에게 허용, 쓰기는 본인만
ALTER TABLE public.user_markets ENABLE ROW LEVEL SECURITY;
-- 본인 데이터 읽기/쓰기 허용
CREATE POLICY "Users can view and edit their own user_markets data" ON public.user_markets
FOR ALL USING (auth.uid() = user_id);
-- 품앗이를 위한 다른 사용자 데이터 읽기 허용 (민감하지 않은 정보만)
CREATE POLICY "Users can view others user_markets for mutual zzim" ON public.user_markets
FOR SELECT USING (true);
-- jjim 테이블: 본인이 한 찜 기록만 조회/수정 가능
ALTER TABLE public.jjim ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view and edit their own jjim records" ON public.jjim
FOR ALL USING (auth.uid() = user_id);
-- membership_zzim_limits 테이블: 모든 사용자가 읽기 가능
ALTER TABLE public.membership_zzim_limits ENABLE ROW LEVEL SECURITY;
CREATE POLICY "All users can view membership limits" ON public.membership_zzim_limits
FOR SELECT USING (true);
-- 8. 데이터 마이그레이션 함수 (users 테이블에서 user_markets로 찜 데이터 이관)
CREATE OR REPLACE FUNCTION migrate_zzim_data_to_user_markets()
RETURNS void AS $$
BEGIN
-- users 테이블의 찜 관련 데이터를 user_markets로 이관
INSERT INTO public.user_markets (
user_id,
my_zzim,
zzim_mile,
available_zzim_mile,
today_zzim_count,
today_zzim_date,
membership_level
)
SELECT
id,
COALESCE(my_zzim, 0),
COALESCE(zzim_mile, 0),
COALESCE(available_zzim_mile, COALESCE(zzim_mile, 0)),
COALESCE(today_zzim_count, 0),
COALESCE(today_zzim_date, CURRENT_DATE),
COALESCE(membership_level, 'basic')
FROM auth.users
WHERE id NOT IN (SELECT user_id FROM public.user_markets)
ON CONFLICT (user_id) DO UPDATE SET
my_zzim = EXCLUDED.my_zzim,
zzim_mile = EXCLUDED.zzim_mile,
available_zzim_mile = EXCLUDED.available_zzim_mile,
today_zzim_count = EXCLUDED.today_zzim_count,
today_zzim_date = EXCLUDED.today_zzim_date,
membership_level = EXCLUDED.membership_level,
updated_at = NOW();
RAISE NOTICE 'Zzim data migration completed';
END;
$$ LANGUAGE plpgsql;
-- 9. 트리거 함수: user_markets와 users 테이블 동기화
CREATE OR REPLACE FUNCTION sync_user_markets_to_users()
RETURNS TRIGGER AS $$
BEGIN
-- user_markets 테이블의 변경사항을 users 테이블에 반영
UPDATE auth.users
SET
my_zzim = NEW.my_zzim,
zzim_mile = NEW.zzim_mile,
available_zzim_mile = NEW.available_zzim_mile,
today_zzim_count = NEW.today_zzim_count,
today_zzim_date = NEW.today_zzim_date,
membership_level = NEW.membership_level
WHERE id = NEW.user_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 트리거 생성
CREATE TRIGGER sync_user_markets_to_users_trigger
AFTER INSERT OR UPDATE ON public.user_markets
FOR EACH ROW
EXECUTE FUNCTION sync_user_markets_to_users();
-- 10. 마이그레이션 실행 (주석 해제하여 실행)
-- SELECT migrate_zzim_data_to_user_markets();

View File

56
test/readme_zzim.md Normal file
View File

@ -0,0 +1,56 @@
1) public.users
회원(=auth.users)의 확장 테이블로 사용
집계형 필드만 둡니다.
id (PK, auth.users FK)
membership_level TEXT → FK public.membership_levels.level
my_zzim INT -- 내가 받은 찜 총합
zzim_mile INT -- 누적 마일리지
available_zzim_mile INT -- 사용가능(미소모) 마일리지
today_zzim_count INT
today_zzim_date DATE
created_at, updated_at
2) public.membership_levels
level PK ('basic','premium'…)
api_call_limit INT
daily_zzim_limit INT
max_zzim_mileage INT
mileage_per_zzim INT
3) public.user_markets
id PK
user_id FK → public.users.id
my_markets JSONB -- [{ market_url , … , zzim_received_count , … }]
created_at, updated_at
4) 뷰(View) 하나 추가하면 코드가 훨씬 단순합니다.
CREATE OR REPLACE VIEW public.v_user_market_stats AS
SELECT
u.id AS user_id,
u.membership_level,
u.available_zzim_mile,
u.my_zzim,
u.zzim_mile,
m.my_markets
FROM public.users u
LEFT JOIN public.user_markets m ON m.user_id = u.id;
-- 한번만 실행
WITH s AS (
SELECT user_id,
COALESCE(SUM((m->>'zzim_received_count')::INT),0) AS total_zzim
FROM public.user_markets,
LATERAL jsonb_array_elements(my_markets) m
GROUP BY user_id
)
UPDATE public.users u
SET my_zzim = s.total_zzim
FROM s
WHERE u.id = s.user_id;

View File

@ -187,6 +187,12 @@
margin-bottom: 10px;
}
.market-checkboxes {
display: flex;
flex-direction: column;
gap: 5px;
}
.market-checkbox-input {
width: 18px;
height: 18px;
@ -195,7 +201,7 @@
}
.market-checkbox-label {
font-size: 14px;
font-size: 12px;
font-weight: 600;
color: #2c3e50;
cursor: pointer;
@ -758,6 +764,17 @@
</div>
</div>
<div class="checkbox-container">
<label class="custom-checkbox">
<input type="checkbox" id="edit-market-for-mutual-zzim">
<span class="checkbox-checkmark"></span>
</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">

729
zzim.js

File diff suppressed because it is too large Load Diff

BIN
매뉴얼/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
매뉴얼/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
매뉴얼/11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

BIN
매뉴얼/12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

BIN
매뉴얼/13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

BIN
매뉴얼/14.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

BIN
매뉴얼/15.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

BIN
매뉴얼/16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

BIN
매뉴얼/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
매뉴얼/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

BIN
매뉴얼/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

BIN
매뉴얼/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
매뉴얼/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

BIN
매뉴얼/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

BIN
매뉴얼/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

BIN
매뉴얼/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
매뉴얼/ext/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
매뉴얼/ext/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 KiB

BIN
매뉴얼/ext/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
매뉴얼/ext/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
매뉴얼/ext/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB