Merge branch 'extended' of ssh://cckb9998.synology.me:30022/ckh08045/SearchTrademark into extended
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
console.log("찜하기 자동화 확장 프로그램이 설치되었습니다.");
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
After Width: | Height: | Size: 153 B |
|
|
@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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("자동화가 종료되었습니다.");
|
||||
});
|
||||
});
|
||||
136
background.js
|
|
@ -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
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
4
popup.js
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
44
settings.js
|
|
@ -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
|
||||
};
|
||||
|
||||
// 저장소에서 시간 알람 설정 제거
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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;
|
||||
19
zzim.html
|
|
@ -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">
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 362 KiB |
|
After Width: | Height: | Size: 431 KiB |
|
After Width: | Height: | Size: 460 KiB |
|
After Width: | Height: | Size: 404 KiB |
|
After Width: | Height: | Size: 538 KiB |
|
After Width: | Height: | Size: 472 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 771 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 145 KiB |