Compare commits
13 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
e2d353afe3 | |
|
|
bdde76d7a6 | |
|
|
9f41be733c | |
|
|
312b13a73f | |
|
|
a0af4de657 | |
|
|
3c8f0419b9 | |
|
|
a532433d9a | |
|
|
7e9dc74a61 | |
|
|
08759f6615 | |
|
|
5d932a87c5 | |
|
|
881da5efde | |
|
|
8f142399ff | |
|
|
5ea5f1ec23 |
|
|
@ -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("자동화가 종료되었습니다.");
|
||||||
|
});
|
||||||
|
});
|
||||||
4310
background.js
473
bannedWords.js
|
|
@ -2,12 +2,12 @@
|
||||||
class BannedWordsManager {
|
class BannedWordsManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
// 초기값 설정 (chrome.storage에서 로드될 때까지 임시)
|
// 초기값 설정 (chrome.storage에서 로드될 때까지 임시)
|
||||||
this.SUPABASE_URL = null;
|
// SUPABASE 설정은 background.js에서 중앙 관리됨
|
||||||
this.SUPABASE_ANON_KEY = null;
|
|
||||||
this.DEBUG_MODE = true;
|
this.DEBUG_MODE = true;
|
||||||
this.ACCESS_TOKEN = null;
|
this.ACCESS_TOKEN = null;
|
||||||
this.isConfigLoaded = false;
|
this.isConfigLoaded = false;
|
||||||
this.buttonEventListenerAttached = false; // 이벤트 리스너 중복 등록 방지
|
this.buttonEventListenerAttached = false; // 이벤트 리스너 중복 등록 방지
|
||||||
|
this.userId = null; // 사용자 ID 저장
|
||||||
|
|
||||||
this.debugLog('BannedWordsManager 생성자 시작 - 설정 로드 대기 중');
|
this.debugLog('BannedWordsManager 생성자 시작 - 설정 로드 대기 중');
|
||||||
|
|
||||||
|
|
@ -60,8 +60,6 @@ class BannedWordsManager {
|
||||||
if (configData.bannedWords_config) {
|
if (configData.bannedWords_config) {
|
||||||
const config = configData.bannedWords_config;
|
const config = configData.bannedWords_config;
|
||||||
this.debugLog('bannedWords_config에서 설정 로드', {
|
this.debugLog('bannedWords_config에서 설정 로드', {
|
||||||
hasUrl: !!config.SUPABASE_URL,
|
|
||||||
hasKey: !!config.SUPABASE_ANON_KEY,
|
|
||||||
hasToken: !!config.ACCESS_TOKEN,
|
hasToken: !!config.ACCESS_TOKEN,
|
||||||
debugMode: config.DEBUG_MODE,
|
debugMode: config.DEBUG_MODE,
|
||||||
timestamp: config.timestamp,
|
timestamp: config.timestamp,
|
||||||
|
|
@ -73,8 +71,6 @@ class BannedWordsManager {
|
||||||
this.debugLog('⚠️ 설정이 오래되었습니다', { age: Date.now() - config.timestamp });
|
this.debugLog('⚠️ 설정이 오래되었습니다', { age: Date.now() - config.timestamp });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.SUPABASE_URL = config.SUPABASE_URL;
|
|
||||||
this.SUPABASE_ANON_KEY = config.SUPABASE_ANON_KEY;
|
|
||||||
this.DEBUG_MODE = config.DEBUG_MODE !== undefined ? config.DEBUG_MODE : true;
|
this.DEBUG_MODE = config.DEBUG_MODE !== undefined ? config.DEBUG_MODE : true;
|
||||||
this.ACCESS_TOKEN = config.ACCESS_TOKEN;
|
this.ACCESS_TOKEN = config.ACCESS_TOKEN;
|
||||||
|
|
||||||
|
|
@ -82,41 +78,25 @@ class BannedWordsManager {
|
||||||
// 2. 개별 설정 확인 (fallback)
|
// 2. 개별 설정 확인 (fallback)
|
||||||
this.debugLog('bannedWords_config가 없음, 개별 설정 확인 중');
|
this.debugLog('bannedWords_config가 없음, 개별 설정 확인 중');
|
||||||
|
|
||||||
const storageData = await chrome.storage.local.get([
|
const storageData = await chrome.storage.local.get(['access_token']);
|
||||||
'access_token',
|
|
||||||
'SUPABASE_URL',
|
|
||||||
'SUPABASE_ANON_KEY'
|
|
||||||
]);
|
|
||||||
|
|
||||||
this.debugLog('개별 설정 조회 결과', {
|
this.debugLog('개별 설정 조회 결과', {
|
||||||
hasToken: !!storageData.access_token,
|
hasToken: !!storageData.access_token
|
||||||
hasUrl: !!storageData.SUPABASE_URL,
|
|
||||||
hasKey: !!storageData.SUPABASE_ANON_KEY
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 기본값 설정
|
|
||||||
this.SUPABASE_URL = storageData.SUPABASE_URL || 'http://146.56.101.199:8000';
|
|
||||||
this.SUPABASE_ANON_KEY = storageData.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey AgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE';
|
|
||||||
this.ACCESS_TOKEN = storageData.access_token;
|
this.ACCESS_TOKEN = storageData.access_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isConfigLoaded = true;
|
this.isConfigLoaded = true;
|
||||||
|
|
||||||
this.debugLog('설정 로드 완료', {
|
this.debugLog('설정 로드 완료', {
|
||||||
SUPABASE_URL: this.SUPABASE_URL,
|
|
||||||
hasToken: !!this.ACCESS_TOKEN,
|
hasToken: !!this.ACCESS_TOKEN,
|
||||||
tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0,
|
tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0,
|
||||||
DEBUG_MODE: this.DEBUG_MODE,
|
DEBUG_MODE: this.DEBUG_MODE,
|
||||||
isConfigLoaded: this.isConfigLoaded
|
isConfigLoaded: this.isConfigLoaded
|
||||||
});
|
});
|
||||||
|
|
||||||
// 설정 검증
|
// 설정 검증 - SUPABASE 설정은 background.js에서 관리
|
||||||
if (!this.SUPABASE_URL) {
|
|
||||||
throw new Error('SUPABASE_URL이 설정되지 않았습니다');
|
|
||||||
}
|
|
||||||
if (!this.SUPABASE_ANON_KEY) {
|
|
||||||
throw new Error('SUPABASE_ANON_KEY가 설정되지 않았습니다');
|
|
||||||
}
|
|
||||||
if (!this.ACCESS_TOKEN) {
|
if (!this.ACCESS_TOKEN) {
|
||||||
throw new Error('ACCESS_TOKEN이 없습니다. 로그인이 필요합니다');
|
throw new Error('ACCESS_TOKEN이 없습니다. 로그인이 필요합니다');
|
||||||
}
|
}
|
||||||
|
|
@ -197,96 +177,41 @@ class BannedWordsManager {
|
||||||
// 토큰 유효성 검증
|
// 토큰 유효성 검증
|
||||||
async validateToken() {
|
async validateToken() {
|
||||||
try {
|
try {
|
||||||
this.debugLog('토큰 검증 API 호출 준비', {
|
this.debugLog('토큰 검증 API 호출 준비 (background.js 경유)', {
|
||||||
url: `${this.SUPABASE_URL}/auth/v1/user`,
|
|
||||||
hasToken: !!this.ACCESS_TOKEN,
|
hasToken: !!this.ACCESS_TOKEN,
|
||||||
tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0,
|
tokenLength: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.length : 0,
|
||||||
tokenPreview: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.substring(0, 30) + '...' : 'none'
|
tokenPreview: this.ACCESS_TOKEN ? this.ACCESS_TOKEN.substring(0, 30) + '...' : 'none'
|
||||||
});
|
});
|
||||||
|
|
||||||
const headers = {
|
// background.js를 통해 토큰 검증
|
||||||
'Authorization': `Bearer ${this.ACCESS_TOKEN}`,
|
const response = await chrome.runtime.sendMessage({
|
||||||
'apikey': this.SUPABASE_ANON_KEY,
|
action: 'validateToken',
|
||||||
'Content-Type': 'application/json',
|
token: this.ACCESS_TOKEN
|
||||||
'Accept': 'application/json'
|
|
||||||
};
|
|
||||||
|
|
||||||
this.debugLog('요청 헤더 정보', {
|
|
||||||
hasAuthorization: !!headers.Authorization,
|
|
||||||
hasApikey: !!headers.apikey,
|
|
||||||
authPreview: headers.Authorization ? headers.Authorization.substring(0, 20) + '...' : 'none'
|
|
||||||
});
|
|
||||||
|
|
||||||
const authRes = await fetch(`${this.SUPABASE_URL}/auth/v1/user`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: headers,
|
|
||||||
mode: 'cors', // CORS 모드 명시적 설정
|
|
||||||
credentials: 'omit' // 크로스 오리진 요청에서 자격 증명 제외
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.debugLog('토큰 검증 API 응답', {
|
this.debugLog('토큰 검증 API 응답', {
|
||||||
status: authRes.status,
|
success: response?.success,
|
||||||
statusText: authRes.statusText,
|
hasUser: !!response?.user
|
||||||
ok: authRes.ok,
|
|
||||||
type: authRes.type,
|
|
||||||
url: authRes.url,
|
|
||||||
headers: Object.fromEntries(authRes.headers.entries())
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!authRes.ok) {
|
if (!response || !response.success) {
|
||||||
let errorText = '';
|
throw new Error(response?.error || '토큰 검증 실패');
|
||||||
let errorJson = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const responseText = await authRes.text();
|
|
||||||
errorText = responseText;
|
|
||||||
|
|
||||||
// JSON 파싱 시도
|
|
||||||
if (responseText.trim().startsWith('{')) {
|
|
||||||
errorJson = JSON.parse(responseText);
|
|
||||||
}
|
|
||||||
} catch (parseError) {
|
|
||||||
this.debugLog('응답 파싱 실패', { parseError: parseError.message });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.debugLog('토큰 검증 실패 - 상세 정보', {
|
const userData = response.user;
|
||||||
status: authRes.status,
|
|
||||||
statusText: authRes.statusText,
|
|
||||||
errorText: errorText,
|
|
||||||
errorJson: errorJson
|
|
||||||
});
|
|
||||||
|
|
||||||
throw new Error(`토큰이 유효하지 않습니다 (${authRes.status}: ${authRes.statusText})\n응답: ${errorText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userData = await authRes.json();
|
|
||||||
this.debugLog('토큰 검증 성공', {
|
this.debugLog('토큰 검증 성공', {
|
||||||
email: userData.email,
|
email: userData.email,
|
||||||
id: userData.id,
|
id: userData.id
|
||||||
userData: userData
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return userData;
|
return userData;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.debugLog('토큰 검증 중 오류 - 상세 분석', {
|
this.debugLog('토큰 검증 중 오류', {
|
||||||
errorName: error.name,
|
errorName: error.name,
|
||||||
errorMessage: error.message,
|
errorMessage: error.message
|
||||||
errorStack: error.stack,
|
|
||||||
isNetworkError: error.name === 'TypeError' && error.message.includes('fetch'),
|
|
||||||
isCorsError: error.message.includes('CORS') || error.message.includes('cors'),
|
|
||||||
isTimeoutError: error.message.includes('timeout') || error.message.includes('Timeout')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 에러 타입별 상세 메시지
|
|
||||||
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
|
||||||
throw new Error(`네트워크 연결 오류: 서버에 연결할 수 없습니다.\n- URL: ${this.SUPABASE_URL}/auth/v1/user\n- 원본 에러: ${error.message}`);
|
|
||||||
} else if (error.message.includes('CORS')) {
|
|
||||||
throw new Error(`CORS 오류: 크로스 오리진 요청이 차단되었습니다.\n- 서버 CORS 설정을 확인해주세요\n- 원본 에러: ${error.message}`);
|
|
||||||
} else if (error.message.includes('timeout')) {
|
|
||||||
throw new Error(`요청 시간 초과: 서버 응답이 지연되고 있습니다.\n- 원본 에러: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -309,7 +234,7 @@ class BannedWordsManager {
|
||||||
|
|
||||||
// 금지어 목록 로드
|
// 금지어 목록 로드
|
||||||
async loadBannedWords() {
|
async loadBannedWords() {
|
||||||
this.debugLog('금지어 목록 로드 시작');
|
this.debugLog('금지어 목록 로드 시작 (background.js 경유)');
|
||||||
|
|
||||||
const loading = document.getElementById("banned-words-loading");
|
const loading = document.getElementById("banned-words-loading");
|
||||||
const tbody = document.getElementById("banned-words-tbody");
|
const tbody = document.getElementById("banned-words-tbody");
|
||||||
|
|
@ -318,117 +243,34 @@ class BannedWordsManager {
|
||||||
tbody.innerHTML = "";
|
tbody.innerHTML = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 공통 헤더 설정
|
// background.js를 통해 금지어 목록 조회
|
||||||
const headers = {
|
const response = await chrome.runtime.sendMessage({
|
||||||
'Authorization': `Bearer ${this.ACCESS_TOKEN}`,
|
action: 'getBannedWords',
|
||||||
'apikey': this.SUPABASE_ANON_KEY,
|
token: this.ACCESS_TOKEN
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchOptions = {
|
|
||||||
method: 'GET',
|
|
||||||
headers: headers,
|
|
||||||
mode: 'cors',
|
|
||||||
credentials: 'omit'
|
|
||||||
};
|
|
||||||
|
|
||||||
// 현재 사용자의 ID 가져오기
|
|
||||||
this.debugLog('Auth API 호출 중...');
|
|
||||||
const authRes = await fetch(`${this.SUPABASE_URL}/auth/v1/user`, fetchOptions);
|
|
||||||
|
|
||||||
this.debugLog('Auth API 응답', {
|
|
||||||
status: authRes.status,
|
|
||||||
statusText: authRes.statusText,
|
|
||||||
ok: authRes.ok,
|
|
||||||
url: authRes.url
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!authRes.ok) {
|
this.debugLog('금지어 목록 API 응답', {
|
||||||
const errorDetail = await authRes.text();
|
success: response?.success,
|
||||||
this.debugLog('Auth API 에러 상세', {
|
count: response?.bannedWords?.length
|
||||||
status: authRes.status,
|
|
||||||
error: errorDetail
|
|
||||||
});
|
});
|
||||||
throw new Error(`사용자 정보를 가져올 수 없습니다 (${authRes.status}: ${authRes.statusText})\n응답: ${errorDetail}`);
|
|
||||||
|
if (!response || !response.success) {
|
||||||
|
throw new Error(response?.error || '금지어 목록 조회 실패');
|
||||||
}
|
}
|
||||||
|
|
||||||
const authUser = await authRes.json();
|
const bannedWords = response.bannedWords;
|
||||||
this.debugLog('Auth 사용자 정보 수신', {
|
this.userId = response.userId; // 사용자 ID 저장 (추가 시 필요)
|
||||||
email: authUser.email,
|
|
||||||
id: authUser.id
|
|
||||||
});
|
|
||||||
|
|
||||||
// 사용자의 user_id 가져오기
|
|
||||||
this.debugLog('Users API 호출 중...');
|
|
||||||
const userRes = await fetch(`${this.SUPABASE_URL}/rest/v1/users?select=id&email=eq.${encodeURIComponent(authUser.email)}&limit=1`, fetchOptions);
|
|
||||||
|
|
||||||
this.debugLog('Users API 응답', {
|
|
||||||
status: userRes.status,
|
|
||||||
statusText: userRes.statusText,
|
|
||||||
ok: userRes.ok,
|
|
||||||
url: userRes.url
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userRes.ok) {
|
|
||||||
const errorDetail = await userRes.text();
|
|
||||||
this.debugLog('Users API 에러 상세', {
|
|
||||||
status: userRes.status,
|
|
||||||
error: errorDetail
|
|
||||||
});
|
|
||||||
throw new Error(`사용자 정보를 찾을 수 없습니다 (${userRes.status}: ${userRes.statusText})\n응답: ${errorDetail}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userData = await userRes.json();
|
|
||||||
this.debugLog('Users 데이터 수신', {
|
|
||||||
count: userData.length,
|
|
||||||
data: userData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userData || userData.length === 0) {
|
|
||||||
throw new Error('사용자 데이터를 찾을 수 없습니다');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = userData[0].id;
|
|
||||||
this.debugLog('사용자 ID 확인', { userId });
|
|
||||||
|
|
||||||
// 금지어 목록 가져오기
|
|
||||||
this.debugLog('BannedWords API 호출 중...');
|
|
||||||
const bannedWordsRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words?select=*&user_id=eq.${userId}&order=created_at.desc`, fetchOptions);
|
|
||||||
|
|
||||||
this.debugLog('BannedWords API 응답', {
|
|
||||||
status: bannedWordsRes.status,
|
|
||||||
statusText: bannedWordsRes.statusText,
|
|
||||||
ok: bannedWordsRes.ok,
|
|
||||||
url: bannedWordsRes.url
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!bannedWordsRes.ok) {
|
|
||||||
const errorDetail = await bannedWordsRes.text();
|
|
||||||
this.debugLog('BannedWords API 에러 상세', {
|
|
||||||
status: bannedWordsRes.status,
|
|
||||||
error: errorDetail
|
|
||||||
});
|
|
||||||
throw new Error(`금지어 목록을 가져올 수 없습니다 (${bannedWordsRes.status}: ${bannedWordsRes.statusText})\n응답: ${errorDetail}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bannedWords = await bannedWordsRes.json();
|
|
||||||
this.debugLog('금지어 목록 로드 완료', { count: bannedWords.length });
|
this.debugLog('금지어 목록 로드 완료', { count: bannedWords.length });
|
||||||
|
|
||||||
this.displayBannedWords(bannedWords);
|
this.displayBannedWords(bannedWords);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.debugLog('금지어 목록 로드 실패', {
|
this.debugLog('금지어 목록 로드 실패', {
|
||||||
errorName: error.name,
|
errorName: error.name,
|
||||||
errorMessage: error.message,
|
errorMessage: error.message
|
||||||
isNetworkError: error.name === 'TypeError' && error.message.includes('fetch')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let errorMessage = error.message;
|
tbody.innerHTML = `<tr><td colspan="4" style="text-align: center; color: red;">오류: ${error.message}</td></tr>`;
|
||||||
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
|
||||||
errorMessage = `네트워크 연결 오류: ${error.message}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody.innerHTML = `<tr><td colspan="4" style="text-align: center; color: red;">오류: ${errorMessage}</td></tr>`;
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.style.display = "none";
|
loading.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
@ -723,35 +565,22 @@ class BannedWordsManager {
|
||||||
throw new Error('로그인이 필요합니다');
|
throw new Error('로그인이 필요합니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.debugLog('등급 업데이트 API 호출', { wordId, selectedGrade });
|
this.debugLog('등급 업데이트 API 호출 (background.js 경유)', { wordId, selectedGrade });
|
||||||
|
|
||||||
const updateRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words?word_id=eq.${wordId}`, {
|
// background.js를 통해 등급 수정
|
||||||
method: 'PATCH',
|
const response = await chrome.runtime.sendMessage({
|
||||||
headers: {
|
action: 'updateBannedWordGrade',
|
||||||
'Authorization': `Bearer ${this.ACCESS_TOKEN}`,
|
token: this.ACCESS_TOKEN,
|
||||||
'apikey': this.SUPABASE_ANON_KEY,
|
wordId: wordId,
|
||||||
'Content-Type': 'application/json',
|
grade: selectedGrade
|
||||||
'Accept': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
grade: selectedGrade,
|
|
||||||
updated_at: new Date().toISOString()
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.debugLog('등급 업데이트 API 응답', {
|
this.debugLog('등급 업데이트 API 응답', {
|
||||||
status: updateRes.status,
|
success: response?.success
|
||||||
statusText: updateRes.statusText,
|
|
||||||
ok: updateRes.ok
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!updateRes.ok) {
|
if (!response || !response.success) {
|
||||||
const errorDetail = await updateRes.text();
|
throw new Error(response?.error || '등급 업데이트 실패');
|
||||||
this.debugLog('등급 업데이트 실패', {
|
|
||||||
status: updateRes.status,
|
|
||||||
error: errorDetail
|
|
||||||
});
|
|
||||||
throw new Error(`등급 업데이트 실패 (${updateRes.status}: ${updateRes.statusText})\n응답: ${errorDetail}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.debugLog('등급 수정 성공', { wordId, selectedGrade });
|
this.debugLog('등급 수정 성공', { wordId, selectedGrade });
|
||||||
|
|
@ -828,31 +657,21 @@ class BannedWordsManager {
|
||||||
throw new Error('로그인이 필요합니다');
|
throw new Error('로그인이 필요합니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.debugLog('금지어 삭제 API 호출', { wordId, word });
|
this.debugLog('금지어 삭제 API 호출 (background.js 경유)', { wordId, word });
|
||||||
|
|
||||||
const deleteRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words?word_id=eq.${wordId}`, {
|
// background.js를 통해 금지어 삭제
|
||||||
method: 'DELETE',
|
const response = await chrome.runtime.sendMessage({
|
||||||
headers: {
|
action: 'deleteBannedWord',
|
||||||
'Authorization': `Bearer ${this.ACCESS_TOKEN}`,
|
token: this.ACCESS_TOKEN,
|
||||||
'apikey': this.SUPABASE_ANON_KEY,
|
wordId: wordId
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.debugLog('금지어 삭제 API 응답', {
|
this.debugLog('금지어 삭제 API 응답', {
|
||||||
status: deleteRes.status,
|
success: response?.success
|
||||||
statusText: deleteRes.statusText,
|
|
||||||
ok: deleteRes.ok
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!deleteRes.ok) {
|
if (!response || !response.success) {
|
||||||
const errorDetail = await deleteRes.text();
|
throw new Error(response?.error || '금지어 삭제 실패');
|
||||||
this.debugLog('금지어 삭제 실패', {
|
|
||||||
status: deleteRes.status,
|
|
||||||
error: errorDetail
|
|
||||||
});
|
|
||||||
throw new Error(`금지어 삭제 실패 (${deleteRes.status}: ${deleteRes.statusText})\n응답: ${errorDetail}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.debugLog('금지어 삭제 성공', { wordId, word });
|
this.debugLog('금지어 삭제 성공', { wordId, word });
|
||||||
|
|
@ -914,42 +733,28 @@ class BannedWordsManager {
|
||||||
throw new Error('word_id가 없습니다. 데이터베이스에서 word_id 값을 확인해주세요.');
|
throw new Error('word_id가 없습니다. 데이터베이스에서 word_id 값을 확인해주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.debugLog('키프리스 데이터 조회 API 호출', {
|
this.debugLog('키프리스 데이터 조회 API 호출 (background.js 경유)', {
|
||||||
wordWordId,
|
|
||||||
word,
|
|
||||||
queryUrl: `${this.SUPABASE_URL}/rest/v1/user_banned_words_kipris?select=*&banned_word_id=eq.${wordWordId}&order=created_at.desc`
|
|
||||||
});
|
|
||||||
|
|
||||||
// 올바른 테이블 이름과 필드명 사용: word_id → banned_word_id
|
|
||||||
const resultsRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words_kipris?select=*&banned_word_id=eq.${wordWordId}&order=created_at.desc`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${this.ACCESS_TOKEN}`,
|
|
||||||
'apikey': this.SUPABASE_ANON_KEY,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.debugLog('키프리스 데이터 API 응답', {
|
|
||||||
status: resultsRes.status,
|
|
||||||
statusText: resultsRes.statusText,
|
|
||||||
ok: resultsRes.ok,
|
|
||||||
url: resultsRes.url
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!resultsRes.ok) {
|
|
||||||
const errorDetail = await resultsRes.text();
|
|
||||||
this.debugLog('키프리스 데이터 조회 실패', {
|
|
||||||
status: resultsRes.status,
|
|
||||||
error: errorDetail,
|
|
||||||
wordWordId,
|
wordWordId,
|
||||||
word
|
word
|
||||||
});
|
});
|
||||||
throw new Error(`키프리스 결과를 가져올 수 없습니다 (${resultsRes.status}: ${resultsRes.statusText})\n응답: ${errorDetail}`);
|
|
||||||
|
// background.js를 통해 키프리스 결과 조회
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
action: 'getKiprisResults',
|
||||||
|
token: this.ACCESS_TOKEN,
|
||||||
|
wordId: wordWordId
|
||||||
|
});
|
||||||
|
|
||||||
|
this.debugLog('키프리스 데이터 API 응답', {
|
||||||
|
success: response?.success,
|
||||||
|
count: response?.kiprisData?.length
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response || !response.success) {
|
||||||
|
throw new Error(response?.error || '키프리스 결과 조회 실패');
|
||||||
}
|
}
|
||||||
|
|
||||||
const kiprisData = await resultsRes.json();
|
const kiprisData = response.kiprisData;
|
||||||
this.debugLog('키프리스 결과 로드 성공', {
|
this.debugLog('키프리스 결과 로드 성공', {
|
||||||
count: kiprisData.length,
|
count: kiprisData.length,
|
||||||
wordWordId,
|
wordWordId,
|
||||||
|
|
@ -961,7 +766,8 @@ class BannedWordsManager {
|
||||||
hasClassificationCode: !!kiprisData[0].classification_code,
|
hasClassificationCode: !!kiprisData[0].classification_code,
|
||||||
hasCategoryDescription: !!kiprisData[0].category_description,
|
hasCategoryDescription: !!kiprisData[0].category_description,
|
||||||
hasDrawing: !!kiprisData[0].drawing,
|
hasDrawing: !!kiprisData[0].drawing,
|
||||||
bannedWordId: kiprisData[0].banned_word_id
|
bannedWordId: kiprisData[0].banned_word_id,
|
||||||
|
hasTradeMarkName: !!kiprisData[0].tradeMark_name
|
||||||
} : null
|
} : null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -983,15 +789,50 @@ class BannedWordsManager {
|
||||||
<div style="background: #f0f8ff; padding: 12px; border-radius: 8px; margin-bottom: 16px; border-left: 4px solid #007bff;">
|
<div style="background: #f0f8ff; padding: 12px; border-radius: 8px; margin-bottom: 16px; border-left: 4px solid #007bff;">
|
||||||
<strong>📊 검색 결과 요약:</strong> 총 ${kiprisData.length}개의 키프리스 데이터를 찾았습니다.
|
<strong>📊 검색 결과 요약:</strong> 총 ${kiprisData.length}개의 키프리스 데이터를 찾았습니다.
|
||||||
</div>
|
</div>
|
||||||
${kiprisData.map((item, index) => `
|
${kiprisData.map((item, index) => {
|
||||||
|
// 상표명 불일치 검사 (빈값이 아닐 때만)
|
||||||
|
const tradeMarkName = item.tradeMark_name;
|
||||||
|
const isNameMismatch = tradeMarkName &&
|
||||||
|
tradeMarkName.trim() !== '' &&
|
||||||
|
word &&
|
||||||
|
word.trim() !== '' &&
|
||||||
|
tradeMarkName.toLowerCase() !== word.toLowerCase() &&
|
||||||
|
!tradeMarkName.toLowerCase().includes(word.toLowerCase()) &&
|
||||||
|
!word.toLowerCase().includes(tradeMarkName.toLowerCase());
|
||||||
|
|
||||||
|
return `
|
||||||
<div class="kipris-item" style="border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-bottom: 16px; background: #f9f9f9;">
|
<div class="kipris-item" style="border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-bottom: 16px; background: #f9f9f9;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||||
<h4 style="margin: 0; color: #333;">📋 검색 결과 ${index + 1}</h4>
|
<h4 style="margin: 0; color: #333;">📋 검색 결과 ${index + 1}</h4>
|
||||||
<small style="color: #666;">등록일: ${new Date(item.created_at).toLocaleDateString()}</small>
|
<small style="color: #666;">등록일: ${new Date(item.created_at).toLocaleDateString()}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 상표명 불일치 경고 표시 -->
|
||||||
|
${isNameMismatch ? `
|
||||||
|
<div style="background: #ffebee; border: 1px solid #f44336; border-radius: 4px; padding: 12px; margin-bottom: 16px;">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
||||||
|
<span style="color: #f44336; font-size: 18px; margin-right: 8px;">⚠️</span>
|
||||||
|
<strong style="color: #f44336;">상표명 불일치 감지</strong>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 14px; color: #666;">
|
||||||
|
<div><strong>검색 키워드:</strong> "${word}"</div>
|
||||||
|
<div><strong>등록 상표명:</strong> <span style="color: red; font-weight: bold;">"${tradeMarkName}" (확인필요)</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
<div class="kipris-content" style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;">
|
<div class="kipris-content" style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;">
|
||||||
<div class="kipris-info">
|
<div class="kipris-info">
|
||||||
|
<div class="kipris-field" style="margin-bottom: 12px;">
|
||||||
|
<strong style="color: #555;">🏷️ 상표명:</strong>
|
||||||
|
<div style="padding: 4px 8px; background: ${tradeMarkName ? '#fff3e0' : '#f5f5f5'}; border-radius: 4px; margin-top: 4px;">
|
||||||
|
${isNameMismatch ?
|
||||||
|
`<span style="color: red; font-weight: bold;">${tradeMarkName} 불일치(확인필요)</span>` :
|
||||||
|
(tradeMarkName || '정보 없음')
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="kipris-field" style="margin-bottom: 12px;">
|
<div class="kipris-field" style="margin-bottom: 12px;">
|
||||||
<strong style="color: #555;">📋 출원상태:</strong>
|
<strong style="color: #555;">📋 출원상태:</strong>
|
||||||
<div style="padding: 4px 8px; background: ${item.application_status ? '#e3f2fd' : '#f5f5f5'}; border-radius: 4px; margin-top: 4px;">
|
<div style="padding: 4px 8px; background: ${item.application_status ? '#e3f2fd' : '#f5f5f5'}; border-radius: 4px; margin-top: 4px;">
|
||||||
|
|
@ -1055,7 +896,8 @@ class BannedWordsManager {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`;
|
||||||
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -1092,9 +934,9 @@ class BannedWordsManager {
|
||||||
// 현재 상태 정보 수집
|
// 현재 상태 정보 수집
|
||||||
const statusInfo = {
|
const statusInfo = {
|
||||||
'초기화 상태': this.isConfigLoaded ? '✅ 완료' : '❌ 미완료',
|
'초기화 상태': this.isConfigLoaded ? '✅ 완료' : '❌ 미완료',
|
||||||
'SUPABASE_URL': this.SUPABASE_URL || '❌ 설정되지 않음',
|
|
||||||
'ACCESS_TOKEN': this.ACCESS_TOKEN ? `✅ 있음 (${this.ACCESS_TOKEN.length}자)` : '❌ 없음',
|
'ACCESS_TOKEN': this.ACCESS_TOKEN ? `✅ 있음 (${this.ACCESS_TOKEN.length}자)` : '❌ 없음',
|
||||||
'DEBUG_MODE': this.DEBUG_MODE ? '✅ 활성화' : '❌ 비활성화',
|
'DEBUG_MODE': this.DEBUG_MODE ? '✅ 활성화' : '❌ 비활성화',
|
||||||
|
'사용자 ID': this.userId || '❌ 없음',
|
||||||
'현재 시간': new Date().toLocaleString()
|
'현재 시간': new Date().toLocaleString()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1247,7 +1089,7 @@ class BannedWordsManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.debugLog('추가할 금지어 목록', { wordsToAdd, selectedGrade, count: wordsToAdd.length });
|
this.debugLog('추가할 금지어 목록 (background.js 경유)', { wordsToAdd, selectedGrade, count: wordsToAdd.length });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
confirmBtn.textContent = '🔄 추가 중...';
|
confirmBtn.textContent = '🔄 추가 중...';
|
||||||
|
|
@ -1258,61 +1100,24 @@ class BannedWordsManager {
|
||||||
throw new Error('로그인이 필요합니다');
|
throw new Error('로그인이 필요합니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 현재 사용자 정보 가져오기
|
// 사용자 ID 확인 (loadBannedWords에서 저장됨)
|
||||||
const authRes = await fetch(`${this.SUPABASE_URL}/auth/v1/user`, {
|
const userId = this.userId;
|
||||||
method: 'GET',
|
if (!userId) {
|
||||||
headers: {
|
throw new Error('사용자 ID를 찾을 수 없습니다. 페이지를 새로고침해주세요.');
|
||||||
'Authorization': `Bearer ${this.ACCESS_TOKEN}`,
|
|
||||||
'apikey': this.SUPABASE_ANON_KEY,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// background.js를 통해 기존 금지어 목록 조회 (중복 검사용)
|
||||||
|
const existingResponse = await chrome.runtime.sendMessage({
|
||||||
|
action: 'getExistingBannedWords',
|
||||||
|
token: this.ACCESS_TOKEN,
|
||||||
|
userId: userId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!authRes.ok) {
|
if (!existingResponse || !existingResponse.success) {
|
||||||
throw new Error('사용자 인증 실패');
|
throw new Error(existingResponse?.error || '기존 금지어 목록 조회 실패');
|
||||||
}
|
}
|
||||||
|
|
||||||
const authUser = await authRes.json();
|
const existingWords = existingResponse.existingWords;
|
||||||
|
|
||||||
// 사용자 ID 가져오기
|
|
||||||
const userRes = await fetch(`${this.SUPABASE_URL}/rest/v1/users?select=id&email=eq.${encodeURIComponent(authUser.email)}&limit=1`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${this.ACCESS_TOKEN}`,
|
|
||||||
'apikey': this.SUPABASE_ANON_KEY,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userRes.ok) {
|
|
||||||
throw new Error('사용자 정보 조회 실패');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userData = await userRes.json();
|
|
||||||
if (!userData || userData.length === 0) {
|
|
||||||
throw new Error('사용자 데이터를 찾을 수 없습니다');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = userData[0].id;
|
|
||||||
|
|
||||||
// 기존 금지어 목록 가져오기 (중복 검사용)
|
|
||||||
const existingRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words?select=banned_word&user_id=eq.${userId}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${this.ACCESS_TOKEN}`,
|
|
||||||
'apikey': this.SUPABASE_ANON_KEY,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!existingRes.ok) {
|
|
||||||
throw new Error('기존 금지어 목록 조회 실패');
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingWords = await existingRes.json();
|
|
||||||
const existingWordSet = new Set(existingWords.map(item => item.banned_word.toLowerCase()));
|
const existingWordSet = new Set(existingWords.map(item => item.banned_word.toLowerCase()));
|
||||||
|
|
||||||
// 중복 검사
|
// 중복 검사
|
||||||
|
|
@ -1374,32 +1179,22 @@ class BannedWordsManager {
|
||||||
created_at: new Date().toISOString()
|
created_at: new Date().toISOString()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.debugLog('금지어 배치 추가 API 호출', { count: wordsData.length, grade: selectedGrade });
|
this.debugLog('금지어 배치 추가 API 호출 (background.js 경유)', { count: wordsData.length, grade: selectedGrade });
|
||||||
|
|
||||||
const addRes = await fetch(`${this.SUPABASE_URL}/rest/v1/user_banned_words`, {
|
// background.js를 통해 금지어 추가
|
||||||
method: 'POST',
|
const addResponse = await chrome.runtime.sendMessage({
|
||||||
headers: {
|
action: 'addBannedWords',
|
||||||
'Authorization': `Bearer ${this.ACCESS_TOKEN}`,
|
token: this.ACCESS_TOKEN,
|
||||||
'apikey': this.SUPABASE_ANON_KEY,
|
userId: userId,
|
||||||
'Content-Type': 'application/json',
|
wordsData: wordsData
|
||||||
'Accept': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(wordsData)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.debugLog('금지어 추가 API 응답', {
|
this.debugLog('금지어 추가 API 응답', {
|
||||||
status: addRes.status,
|
success: addResponse?.success
|
||||||
statusText: addRes.statusText,
|
|
||||||
ok: addRes.ok
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!addRes.ok) {
|
if (!addResponse || !addResponse.success) {
|
||||||
const errorDetail = await addRes.text();
|
throw new Error(addResponse?.error || '금지어 추가 실패');
|
||||||
this.debugLog('금지어 추가 실패', {
|
|
||||||
status: addRes.status,
|
|
||||||
error: errorDetail
|
|
||||||
});
|
|
||||||
throw new Error(`금지어 추가 실패 (${addRes.status}: ${addRes.statusText})\n응답: ${errorDetail}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.debugLog('금지어 추가 성공', { newWords, selectedGrade });
|
this.debugLog('금지어 추가 성공', { newWords, selectedGrade });
|
||||||
|
|
|
||||||
2375
content.js
|
|
@ -1,18 +1,32 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "내차는언제타냐 - 지재권 검색 확장 (컨텍스트 메뉴)",
|
"name": "내차는언제타냐 통합 확장",
|
||||||
"version": "1.2",
|
"version": "1.3",
|
||||||
"description": "드래그한 텍스트를 우클릭 → '지재권 검색'으로 MarkInfo 검색을 수행하고 결과를 툴팁으로 표시합니다.",
|
"description": "드래그한 텍스트를 우클릭 → '지재권 검색'으로 MarkInfo 검색을 수행하고 결과를 툴팁으로 표시합니다.",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"activeTab",
|
||||||
"contextMenus",
|
"contextMenus",
|
||||||
"storage",
|
"storage",
|
||||||
"notifications",
|
"notifications",
|
||||||
"alarms",
|
"alarms",
|
||||||
"activeTab"
|
"tabs",
|
||||||
|
"scripting",
|
||||||
|
"clipboardRead"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://markinfo.kr/*",
|
"*://markinfo.kr/*",
|
||||||
"http://146.56.101.199:8000/*"
|
"https://ko.wrmc.cc/*",
|
||||||
|
"https://oci1ckh08045.duckdns.org:8000/*",
|
||||||
|
"*://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/*",
|
||||||
|
"*://api.openai.com/*",
|
||||||
|
"*://generativelanguage.googleapis.com/*"
|
||||||
],
|
],
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "background.js"
|
"service_worker": "background.js"
|
||||||
|
|
@ -21,16 +35,57 @@
|
||||||
{
|
{
|
||||||
"matches": ["<all_urls>"],
|
"matches": ["<all_urls>"],
|
||||||
"js": ["content.js"],
|
"js": ["content.js"],
|
||||||
|
"all_frames": true,
|
||||||
"run_at": "document_end"
|
"run_at": "document_end"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"action": {
|
"action": {
|
||||||
"default_popup": "popup.html",
|
"default_popup": "popup.html",
|
||||||
"default_icon": "icon.png"
|
"default_icon": "icon.png",
|
||||||
|
"default_title": "내차는언제타냐 통합 확장"
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"trademark-search": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Ctrl+Shift+S"
|
||||||
|
},
|
||||||
|
"description": "지재권 검색"
|
||||||
|
},
|
||||||
|
"multi-translate": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Ctrl+Shift+E"
|
||||||
|
},
|
||||||
|
"description": "멀티번역"
|
||||||
|
},
|
||||||
|
"korean-to-chinese": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Ctrl+Shift+Z"
|
||||||
|
},
|
||||||
|
"description": "한국어↔중국어 양방향 번역"
|
||||||
|
},
|
||||||
|
"direct-translate": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Ctrl+Shift+K"
|
||||||
|
},
|
||||||
|
"description": "직번역 (텍스트 바로 대체)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
"resources": ["bannedWords.html", "bannedWords.js"],
|
"resources": [
|
||||||
|
"bannedWords.html",
|
||||||
|
"bannedWords.js",
|
||||||
|
"sayings.html",
|
||||||
|
"sayings.js",
|
||||||
|
"zzim.html",
|
||||||
|
"zzim.js",
|
||||||
|
"settings.html",
|
||||||
|
"settings.js",
|
||||||
|
"rest-modal.html",
|
||||||
|
"rest-modal.js",
|
||||||
|
"manual.html",
|
||||||
|
"manual.js"
|
||||||
|
],
|
||||||
"matches": ["<all_urls>"]
|
"matches": ["<all_urls>"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,650 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>내차는언제타냐 통합확장기 매뉴얼</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 15px 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6c757d;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
padding: 30px;
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-left: 5px solid #007bff;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
color: #007bff;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-list {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-list li {
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-list li:before {
|
||||||
|
content: '✓';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #545b62;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background: #d1ecf1;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-info {
|
||||||
|
background: #e9ecef;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard-shortcut {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>📚 내차는언제타냐 통합확장기</h1>
|
||||||
|
<p>매뉴얼 및 기능 가이드</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs-container">
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab active" data-tab="overview">🏠 개요</button>
|
||||||
|
<button class="tab" data-tab="trademark">🔍 지재권검색</button>
|
||||||
|
<button class="tab" data-tab="translation">🌐 멀티번역</button>
|
||||||
|
<button class="tab" data-tab="timer">⏰ 시간관리</button>
|
||||||
|
<button class="tab" data-tab="banned-words">🚫 금지어</button>
|
||||||
|
<button class="tab" data-tab="sayings">💬 타냐대장경</button>
|
||||||
|
<button class="tab" data-tab="settings">⚙️ 설정</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content active" id="overview">
|
||||||
|
<h2>🏠 확장 프로그램 개요</h2>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<strong>환영합니다!</strong> 내차는언제타냐 통합확장기는 지재권검색, 멀티번역, 시간관리 등 다양한 기능을 제공하는 올인원 도구입니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<strong>주의사항:</strong> 지재권검색 및 멀티번역은 API 호출량을 소모합니다. 회원등급에 따라 일일 사용량이 제한됩니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>🔍 지재권검색 기능</h3>
|
||||||
|
<p>키프리스(KIPRIS) 지재권검색을 자동화하여 빠르고 정확한 상표조사를 지원합니다.</p>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>검색할 단어를 드래그 후 단축키(컨트롤+쉬프트+S) 입력</li>
|
||||||
|
<li>또는 우클릭으로 해당 지재권을 Search하고 결과 수집</li>
|
||||||
|
<li>현재 활성된 상태의 결과만 필터링</li>
|
||||||
|
<li>해당하는 상품군을 바로 확인할 수 있음</li>
|
||||||
|
<li>내 금지어목록에 바로 적용 및 편집알바생과 연동</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>🌐 멀티번역 기능</h3>
|
||||||
|
<p>여러 언어로 동시 번역하여 애매한 중국어를 이해할수 있도록 지원합니다.</p>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li><strong>멀티번역:</strong> Ctrl+Shift+E</li>
|
||||||
|
<li>또는 우클릭으로 해당 문장을 Translate하고 결과 수집</li>
|
||||||
|
<li>무료로 구글번역과 메모리 번역지원</li>
|
||||||
|
<li>일정등급 이상은 딥러닝 번역 DeepL 사용가능</li>
|
||||||
|
<li>챗GPT로 의역 지원가능 (Gemini는 현재 점검 중)</li>
|
||||||
|
<li>원클릭으로 많은 엔진의 결과를 확인할수 있음</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>🌐 원키번역 기능</h3>
|
||||||
|
<p>구글 기계번역으로 매우 빠르게 번역할수 있습니다.</p>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>번역할 문장이나 단어를 드래그 후 단축키(컨트롤+쉬프트+Z) 입력</li>
|
||||||
|
<li>웹페이지에서 바로 한글과 중국어를 빠르게 번역</li>
|
||||||
|
<li>위챗등의 대화에서 한글 입력 후 선택 및 단축키로 바로 번역[현재 위챗의 특수성으로 적용되지않음]</li>
|
||||||
|
<li>웹페이지에서 중국어를 드래그 후 단축키로 바로 번역 결과 확인</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>⏰ 시간관리 기능</h3>
|
||||||
|
<p>포모도로 기법 기반의 시간관리로 생산성을 극대화합니다.</p>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>자동 작업/휴식 타이머</li>
|
||||||
|
<li>휴식시간 추천 활동</li>
|
||||||
|
<li>실시간 타이머 알림</li>
|
||||||
|
<li>개인화된 시간 설정</li>
|
||||||
|
<li></li>[포모도로 타이머가 활성화 되면 35분/5분으로 기본설정되며 4사이클 총180분(3시간) 휴식시간 추천 활동 제공]</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>🚫 금지어 관리</h3>
|
||||||
|
<p>지재권검색 시 제외할 키워드를 효율적으로 관리합니다.</p>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>개인별 금지어 목록</li>
|
||||||
|
<li>실시간 금지어 추가/삭제</li>
|
||||||
|
<li>금지어 등급관리 및 검색결과 확인</li>
|
||||||
|
<li>수동금지어 등록 및 파생금지어 등록가능</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="version-info">
|
||||||
|
<strong>현재 버전:</strong> v1.0.0 | <strong>최종 업데이트:</strong> 2025년 6월
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="trademark">
|
||||||
|
<h2>🔍 지재권검색 사용법</h2>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
키프리스(KIPRIS)에서 자동으로 지재권검색을 수행하고 결과를 정리해주는 기능입니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>1️⃣ 지재권검색 시작하기</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>검색하고 싶은 단어를 드래그 후 우클릭 - 지재권 검색 선택</li>
|
||||||
|
<li>검색하고 싶은 단어를 드래그 후 단축키(Search) - Ctrl + Shift + S</li>
|
||||||
|
<li>로그인 후 지재권검색 기능 활성화</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>2️⃣ 검색 과정</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>해당단어를 키프리스에서 검색실행</li>
|
||||||
|
<li>검색 결과 유효한 권리상태만 필터링</li>
|
||||||
|
<li>지재권 해당하는 상품군 정리</li>
|
||||||
|
<li>실시간 진행상황 팝업 표시</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>3️⃣ 결과 확인 및 추가</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>검색 완료 후 결과 요약 표시</li>
|
||||||
|
<li>검색결과를 내 금지어에 원클릭 등록가능</li>
|
||||||
|
<li>상표 이미지 및 상세정보 포함</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<strong>주의사항:</strong> 지재권검색은 API 호출량을 소모합니다. 회원등급에 따라 일일 사용량이 제한됩니다.
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="translation">
|
||||||
|
<h2>🌐 멀티번역 사용법</h2>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
하나의 텍스트나 문장을 여러 번역엔진으로 동시에 번역하여 번역기 마다 다른 애매한 내용들을 정리해줍니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>1️⃣ 번역 시작하기</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>번역할 텍스트를 드래그 후 우클릭 - 멀티번역 선택</li>
|
||||||
|
<li>번역할 텍스트를 드래그 후 단축키(Translate) - Ctrl + Shift + T</li>
|
||||||
|
<li>설정에서 선택한 엔진들로 번역</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>2️⃣ 지원 엔진</h3>
|
||||||
|
<div class="grid">
|
||||||
|
<div>
|
||||||
|
<strong>무료 엔진:</strong>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>구글 기계번역</li>
|
||||||
|
<li>myMemory 메모리 번역</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>프리미엄등급 엔진:</strong>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>무료엔진</li>
|
||||||
|
<li>DeepL 딥러닝 번역</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>VIP등급 엔진 - 의역 지원:</strong>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>챗GPT 의역 번역</li>
|
||||||
|
<li>구글 제미나이 의역 번역</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>3️⃣ 번역 결과 활용</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>실시간 번역 결과 확인</li>
|
||||||
|
<li>자연스러운 딥러닝 엔진인 Deepl 사용</li>
|
||||||
|
<li>맥락을 이해하고 현재 단어나 문장이 어떤의미인지 어떨때 쓰이는지 파악</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="timer">
|
||||||
|
<h2>⏰ 시간관리 기능</h2>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
포모도로 기법을 활용한 자동 시간관리로 생산성을 향상시킵니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>1️⃣ 시간관리 설정</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>설정 페이지에서 "시간 알림" 활성화</li>
|
||||||
|
<li>작업 시간 설정 (기본: 60분)</li>
|
||||||
|
<li>휴식 시간 설정 (기본: 5분)</li>
|
||||||
|
<li>알림 방식 선택</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>2️⃣ 자동 타이머 작동</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>로그인 시 자동으로 작업 타이머 시작</li>
|
||||||
|
<li>작업 시간 완료 시 휴식 모달 표시</li>
|
||||||
|
<li>휴식 시간 동안 추천 활동 제공</li>
|
||||||
|
<li>휴식 시간 동안 타냐대장경 제공</li>
|
||||||
|
<li>휴식 완료 후 자동으로 다음 작업 시작</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>3️⃣ 휴식 시간 추천 활동</h3>
|
||||||
|
<div class="grid">
|
||||||
|
<div>
|
||||||
|
<strong>신체 활동:</strong>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>가벼운 스트레칭</li>
|
||||||
|
<li>목과 어깨 마사지</li>
|
||||||
|
<li>잠깐 산책하기</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>정신 건강:</strong>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>깊은 호흡 연습</li>
|
||||||
|
<li>간단한 명상</li>
|
||||||
|
<li>긍정적 생각하기</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>4️⃣ 타이머 상태 확인</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>팝업에서 다음 휴식까지 남은 시간 확인</li>
|
||||||
|
<li>작업/휴식 사이클 진행상황 표시</li>
|
||||||
|
<li>일일 작업 시간 통계</li>
|
||||||
|
<li>생산성 향상 팁 제공</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="banned-words">
|
||||||
|
<h2>🚫 금지어 관리</h2>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
상품편집시 지식재산권을 피하기 위해금지 키워드를 관리합니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>1️⃣ 금지어 추가하기</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>설정 페이지에서 "금지어 관리" 선택</li>
|
||||||
|
<li>새로운 금지어 입력</li>
|
||||||
|
<li>"추가" 버튼 클릭하여 저장</li>
|
||||||
|
<li>실시간으로 금지어 목록 업데이트</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>2️⃣ 금지어 관리</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>기존 금지어 목록 확인</li>
|
||||||
|
<li>불필요한 금지어 삭제</li>
|
||||||
|
<li>금지어 등급변경</li>
|
||||||
|
<li>금지어 검색 및 필터링</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>3️⃣ 금지어 적용</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>편집알바생과 연동된 금지어 목록</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<strong>팁:</strong> 자주 나타나는 불필요한 키워드들을 금지어로 등록하면 편집알바생의 효율성이 크게 향상됩니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-primary" onclick="openBannedWords()">🚫 금지어 관리</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="sayings">
|
||||||
|
<h2>💬 타냐대장경</h2>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
업무에 도움이 되는 타냐센세의 대장경과 조언을 모아놓은 특별한 기능입니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>1️⃣ 타냐대장경이란?</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>업무 효율성을 높이는 대장경 모음</li>
|
||||||
|
<li>동기부여와 영감을 주는 메시지</li>
|
||||||
|
<li>관리자가 승인한 검증된 조언</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>2️⃣ 사용 방법</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>팝업에서 "타냐대장경" 버튼 클릭</li>
|
||||||
|
<li>오늘의 대장경 확인</li>
|
||||||
|
<li>대장경 카테고리별 탐색</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>3️⃣ 대장경 카테고리</h3>
|
||||||
|
<div class="grid">
|
||||||
|
<div>
|
||||||
|
<strong>업무 효율성:</strong>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>시간 관리 조언</li>
|
||||||
|
<li>생산성 향상 팁</li>
|
||||||
|
<li>마켓의 로직변화관리</li>
|
||||||
|
<li>타냐센세의 계절별 조언</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>동기부여:</strong>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>성공 마인드셋</li>
|
||||||
|
<li>도전 정신 격려</li>
|
||||||
|
<li>긍정적 사고방식</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="settings">
|
||||||
|
<h2>⚙️ 설정 가이드</h2>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
확장 프로그램의 모든 기능을 개인 취향에 맞게 설정할 수 있습니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>1️⃣ 번역 설정</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>번역엔진 활성화</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>2️⃣ 시간관리 설정</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>시간 알림 ON/OFF</li>
|
||||||
|
<li>작업 시간 설정 (분 단위)</li>
|
||||||
|
<li>휴식 시간 설정 (분 단위)</li>
|
||||||
|
<li>알림 방식 선택</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>3️⃣ 찜 관리 설정</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
|
<li>추후 업데이트</li>
|
||||||
|
<li>찜 품앗이</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="manual.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
// 탭 전환 기능
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// 탭 전환 기능
|
||||||
|
document.querySelectorAll('.tab').forEach(tab => {
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
console.log('탭 클릭됨:', tab.dataset.tab);
|
||||||
|
|
||||||
|
// 모든 탭과 콘텐츠에서 active 클래스 제거
|
||||||
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||||
|
|
||||||
|
// 클릭된 탭과 해당 콘텐츠에 active 클래스 추가
|
||||||
|
tab.classList.add('active');
|
||||||
|
const targetContent = document.getElementById(tab.dataset.tab);
|
||||||
|
if (targetContent) {
|
||||||
|
targetContent.classList.add('active');
|
||||||
|
console.log('탭 전환 완료:', tab.dataset.tab);
|
||||||
|
} else {
|
||||||
|
console.error('탭 콘텐츠를 찾을 수 없습니다:', tab.dataset.tab);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('매뉴얼 탭 기능 초기화 완료');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 필요한 버튼 클릭 이벤트 함수들만 유지
|
||||||
|
function openSettings() {
|
||||||
|
chrome.tabs.create({ url: chrome.runtime.getURL('settings.html') });
|
||||||
|
}
|
||||||
|
|
||||||
|
function openBannedWords() {
|
||||||
|
chrome.tabs.create({ url: chrome.runtime.getURL('bannedWords.html') });
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSayings() {
|
||||||
|
chrome.tabs.create({ url: chrome.runtime.getURL('sayings.html') });
|
||||||
|
}
|
||||||
36
popup.html
|
|
@ -13,7 +13,8 @@
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
/* overflow: hidden; */
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -118,6 +119,25 @@
|
||||||
background: #005a9e;
|
background: #005a9e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 로그인 화면과 사용자 정보 화면 여백 추가 */
|
||||||
|
#login-section {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-info-section {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 설정 버튼 스타일 */
|
||||||
|
.settings-btn {
|
||||||
|
background: #9b59b6;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-btn:hover {
|
||||||
|
background: #8e44ad;
|
||||||
|
}
|
||||||
|
|
||||||
/* 관리 버튼 스타일 */
|
/* 관리 버튼 스타일 */
|
||||||
.management-buttons {
|
.management-buttons {
|
||||||
margin: 15px 0;
|
margin: 15px 0;
|
||||||
|
|
@ -343,6 +363,7 @@
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -395,10 +416,21 @@
|
||||||
<p><strong>오늘 호출량:</strong> <span id="user-usage"></span>회</p>
|
<p><strong>오늘 호출량:</strong> <span id="user-usage"></span>회</p>
|
||||||
<p><strong>등급 만료일:</strong> <span id="user-expire"></span></p>
|
<p><strong>등급 만료일:</strong> <span id="user-expire"></span></p>
|
||||||
|
|
||||||
|
<!-- 휴식 시간 카운트다운 -->
|
||||||
|
<div id="break-timer-section" style="margin: 15px 0; padding: 10px; background: #e8f5e8; border-radius: 6px; border: 1px solid #c3e6c3;">
|
||||||
|
<p style="margin: 0; font-weight: bold; color: #2d5a2d;">⏰ 다음 휴식까지</p>
|
||||||
|
<p id="break-countdown" style="margin: 5px 0 0 0; font-size: 16px; font-weight: bold; color: #1e7e1e;">계산 중...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 관리 버튼들 -->
|
<!-- 관리 버튼들 -->
|
||||||
<div class="management-buttons">
|
<div class="management-buttons">
|
||||||
|
<button class="btn management-btn" id="settings-btn">⚙️ 설정</button>
|
||||||
<button class="btn management-btn" id="banned-words-btn">🚫 금지어 관리</button>
|
<button class="btn management-btn" id="banned-words-btn">🚫 금지어 관리</button>
|
||||||
<button class="btn management-btn" id="sayings-btn">💬 어록보기</button>
|
<button class="btn management-btn" id="sayings-btn">💬 타냐대장경</button>
|
||||||
|
<!-- <button class="btn management-btn" id="zzim-btn" disabled>💝 찜관리</button> -->
|
||||||
|
<button class="btn management-btn" id="zzim-btn">💝 찜관리</button>
|
||||||
|
<button class="btn management-btn" id="manual-btn">📚 매뉴얼</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn" id="logout-btn">로그아웃</button>
|
<button class="btn" id="logout-btn">로그아웃</button>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,296 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>휴식 시간</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #333;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
animation: modalFadeIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8) translateY(-50px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 20px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rest-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
animation: bounce 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 20%, 50%, 80%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rest-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: #4a5568;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rest-subtitle {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #718096;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-display {
|
||||||
|
font-size: 3rem;
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-title {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: #4a5568;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-suggestion {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #2d3748;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saying-section {
|
||||||
|
background: linear-gradient(135deg, #ffeaa7, #fab1a0);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
color: #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saying-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saying-content {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saying-author {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #cbd5e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background: #e2e8f0;
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid #f3f3f3;
|
||||||
|
border-top: 2px solid #667eea;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="modal-overlay" id="modalOverlay">
|
||||||
|
<div class="modal-container">
|
||||||
|
<button class="close-btn" id="closeBtn" title="ESC키로 닫기">×</button>
|
||||||
|
|
||||||
|
<div class="rest-icon">🧘♀️</div>
|
||||||
|
<h1 class="rest-title">휴식 시간입니다!</h1>
|
||||||
|
<p class="rest-subtitle">열심히 일한 당신, 잠시 휴식을 취하세요</p>
|
||||||
|
|
||||||
|
<div class="timer-display" id="timerDisplay">05:00</div>
|
||||||
|
|
||||||
|
<div class="activity-section">
|
||||||
|
<h3 class="activity-title">💡 추천 활동</h3>
|
||||||
|
<div class="activity-suggestion" id="activitySuggestion">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
활동을 불러오는 중...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="saying-section">
|
||||||
|
<h3 class="saying-title">
|
||||||
|
<span>✨</span>
|
||||||
|
<span>타냐의 한마디</span>
|
||||||
|
<span>✨</span>
|
||||||
|
</h3>
|
||||||
|
<div class="saying-content" id="sayingContent">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
어록을 불러오는 중...
|
||||||
|
</div>
|
||||||
|
<div class="saying-author" id="sayingAuthor"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn btn-secondary" id="skipBtn">휴식 건너뛰기</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="progressFill"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="rest-modal.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,302 @@
|
||||||
|
class RestModal {
|
||||||
|
constructor() {
|
||||||
|
this.restTime = 5; // 기본 5분
|
||||||
|
this.currentTime = this.restTime * 60; // 초 단위
|
||||||
|
this.timer = null;
|
||||||
|
this.config = null;
|
||||||
|
this.currentSaying = null;
|
||||||
|
this.autoZzim = false;
|
||||||
|
|
||||||
|
// 기본 추천 활동 목록 (백엔드 연결 실패 시 사용)
|
||||||
|
this.defaultActivities = [
|
||||||
|
"🚶♀️ 가벼운 산책을 해보세요",
|
||||||
|
"💧 물 한 잔을 마시며 수분을 보충하세요",
|
||||||
|
"🧘♀️ 심호흡을 하며 명상을 해보세요",
|
||||||
|
"🤸♀️ 간단한 스트레칭으로 몸을 풀어보세요",
|
||||||
|
"👀 눈 운동을 하며 눈의 피로를 풀어보세요",
|
||||||
|
"🚽 화장실을 다녀오세요",
|
||||||
|
"🌱 창밖을 보며 자연을 감상해보세요",
|
||||||
|
"📱 잠시 휴대폰을 내려놓고 마음을 비워보세요",
|
||||||
|
"☕ 따뜻한 차 한 잔을 마셔보세요",
|
||||||
|
"🎵 좋아하는 음악을 들으며 휴식하세요",
|
||||||
|
"📚 짧은 글이나 명언을 읽어보세요",
|
||||||
|
"🤝 동료나 가족과 간단한 대화를 나누세요",
|
||||||
|
"🧴 손 마사지나 목 마사지를 해보세요",
|
||||||
|
"🏃♀️ 제자리에서 가볍게 몸을 움직여보세요",
|
||||||
|
"🍎 건강한 간식을 드세요"
|
||||||
|
];
|
||||||
|
|
||||||
|
this.activities = []; // 백엔드에서 가져온 활동 목록
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
await this.loadSettings();
|
||||||
|
await this.loadConfig();
|
||||||
|
this.setupEventListeners();
|
||||||
|
await this.loadRestActivities();
|
||||||
|
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');
|
||||||
|
const settings = result.time_alarm_settings || {};
|
||||||
|
|
||||||
|
this.restTime = settings.restTime || 5;
|
||||||
|
this.autoZzim = settings.autoZzim || false;
|
||||||
|
this.currentTime = this.restTime * 60;
|
||||||
|
|
||||||
|
console.log('[RestModal] 설정 로드:', { restTime: this.restTime, autoZzim: this.autoZzim });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[RestModal] 설정 로드 실패:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadConfig() {
|
||||||
|
try {
|
||||||
|
const result = await chrome.storage.local.get('settings_config');
|
||||||
|
this.config = result.settings_config || {};
|
||||||
|
console.log('[RestModal] 설정 정보 로드:', this.config);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[RestModal] 설정 정보 로드 실패:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// 닫기 버튼
|
||||||
|
document.getElementById('closeBtn').addEventListener('click', () => {
|
||||||
|
this.closeModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 건너뛰기 버튼
|
||||||
|
document.getElementById('skipBtn').addEventListener('click', () => {
|
||||||
|
this.closeModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ESC 키로 닫기
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
this.closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadRestActivities() {
|
||||||
|
try {
|
||||||
|
if (!this.config.ACCESS_TOKEN) {
|
||||||
|
throw new Error('Access token not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[RestModal] 추천활동 API 호출 (background.js 경유)');
|
||||||
|
|
||||||
|
// background.js를 통해 API 호출
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
action: 'getRestActivities',
|
||||||
|
token: this.config.ACCESS_TOKEN
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response || !response.success) {
|
||||||
|
throw new Error(response?.error || 'API 호출 실패');
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = response.activities;
|
||||||
|
|
||||||
|
if (events && events.length > 0) {
|
||||||
|
// message 필드에서 JSON 파싱하여 활동 목록 생성
|
||||||
|
this.activities = [];
|
||||||
|
|
||||||
|
events.forEach(event => {
|
||||||
|
try {
|
||||||
|
// message 필드가 JSON 형식인 경우 파싱
|
||||||
|
const messageData = JSON.parse(event.message);
|
||||||
|
|
||||||
|
// 활동 텍스트 추출 (다양한 형식 지원)
|
||||||
|
if (messageData.activity) {
|
||||||
|
this.activities.push(messageData.activity);
|
||||||
|
} else if (messageData.text) {
|
||||||
|
this.activities.push(messageData.text);
|
||||||
|
} else if (typeof messageData === 'string') {
|
||||||
|
this.activities.push(messageData);
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
// JSON 파싱 실패 시 문자열 그대로 사용
|
||||||
|
if (typeof event.message === 'string' && event.message.trim()) {
|
||||||
|
this.activities.push(event.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[RestModal] 백엔드에서 추천활동 로드 완료:', this.activities.length + '개');
|
||||||
|
} else {
|
||||||
|
throw new Error('추천활동 데이터가 없습니다');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[RestModal] 추천활동 로드 실패:', error);
|
||||||
|
|
||||||
|
// 기본 활동 목록 사용
|
||||||
|
this.activities = [...this.defaultActivities];
|
||||||
|
console.log('[RestModal] 기본 추천활동 사용:', this.activities.length + '개');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showRandomActivity() {
|
||||||
|
if (this.activities.length === 0) {
|
||||||
|
this.activities = [...this.defaultActivities];
|
||||||
|
}
|
||||||
|
|
||||||
|
const randomActivity = this.activities[Math.floor(Math.random() * this.activities.length)];
|
||||||
|
document.getElementById('activitySuggestion').innerHTML = randomActivity;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadRandomSaying() {
|
||||||
|
try {
|
||||||
|
if (!this.config.ACCESS_TOKEN) {
|
||||||
|
throw new Error('Access token not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[RestModal] 어록 API 호출 (background.js 경유)');
|
||||||
|
|
||||||
|
// background.js를 통해 API 호출
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
action: 'getRandomSaying',
|
||||||
|
token: this.config.ACCESS_TOKEN
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response || !response.success) {
|
||||||
|
throw new Error(response?.error || 'API 호출 실패');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sayings = response.sayings;
|
||||||
|
|
||||||
|
if (sayings && sayings.length > 0) {
|
||||||
|
// 랜덤하게 하나 선택
|
||||||
|
const randomSaying = sayings[Math.floor(Math.random() * sayings.length)];
|
||||||
|
this.currentSaying = randomSaying;
|
||||||
|
|
||||||
|
// 어록 표시
|
||||||
|
const sayingContent = document.getElementById('sayingContent');
|
||||||
|
const sayingAuthor = document.getElementById('sayingAuthor');
|
||||||
|
|
||||||
|
sayingContent.innerHTML = `"${randomSaying.saying}"`;
|
||||||
|
|
||||||
|
// 카테고리와 대상 정보 표시
|
||||||
|
const category = randomSaying.sayings_cat?.saying_cat || '';
|
||||||
|
const target = randomSaying.sayings_target?.target || '';
|
||||||
|
const dateStr = new Date(randomSaying.created_at).toLocaleDateString('ko-KR');
|
||||||
|
|
||||||
|
sayingAuthor.innerHTML = `${category} ${target ? `• ${target}` : ''} • ${dateStr}`;
|
||||||
|
|
||||||
|
console.log('[RestModal] 어록 로드 완료:', randomSaying);
|
||||||
|
} else {
|
||||||
|
throw new Error('어록이 없습니다');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[RestModal] 어록 로드 실패:', error);
|
||||||
|
|
||||||
|
// 기본 메시지 표시
|
||||||
|
document.getElementById('sayingContent').innerHTML = '"열심히 일한 당신, 잠시 휴식을 취하며 에너지를 충전하세요!"';
|
||||||
|
document.getElementById('sayingAuthor').innerHTML = '타냐 • 휴식 메시지';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startTimer() {
|
||||||
|
this.updateTimerDisplay();
|
||||||
|
this.updateProgressBar();
|
||||||
|
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
this.currentTime--;
|
||||||
|
|
||||||
|
this.updateTimerDisplay();
|
||||||
|
this.updateProgressBar();
|
||||||
|
|
||||||
|
if (this.currentTime <= 0) {
|
||||||
|
this.onTimerComplete();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimerDisplay() {
|
||||||
|
const minutes = Math.floor(this.currentTime / 60);
|
||||||
|
const seconds = this.currentTime % 60;
|
||||||
|
const display = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||||
|
|
||||||
|
document.getElementById('timerDisplay').textContent = display;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgressBar() {
|
||||||
|
const totalTime = this.restTime * 60;
|
||||||
|
const elapsed = totalTime - this.currentTime;
|
||||||
|
const percentage = (elapsed / totalTime) * 100;
|
||||||
|
|
||||||
|
document.getElementById('progressFill').style.width = `${percentage}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onTimerComplete() {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
|
||||||
|
// 완료 메시지 표시
|
||||||
|
this.showCompletionMessage();
|
||||||
|
|
||||||
|
// 3초 후 자동 닫기
|
||||||
|
setTimeout(() => {
|
||||||
|
this.closeModal();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
showCompletionMessage() {
|
||||||
|
const modalContainer = document.querySelector('.modal-container');
|
||||||
|
modalContainer.innerHTML = `
|
||||||
|
<div class="rest-icon">🚀</div>
|
||||||
|
<h1 class="rest-title">휴식 완료!</h1>
|
||||||
|
<p class="rest-subtitle">이제 다시 열심히 월매출 1억을 향해 달려가세요!</p>
|
||||||
|
<div style="font-size: 1.2rem; color: #667eea; margin-top: 20px;">
|
||||||
|
💪 화이팅! 성공은 바로 앞에 있습니다!
|
||||||
|
</div>
|
||||||
|
<div class="actions" style="margin-top: 30px;">
|
||||||
|
<button class="btn btn-primary" onclick="window.close()">확인</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal() {
|
||||||
|
if (this.timer) {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 창 닫기
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 페이지 로드 시 휴식 모달 초기화
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const restModal = new RestModal();
|
||||||
|
restModal.init();
|
||||||
|
});
|
||||||
544
sayings.html
|
|
@ -537,14 +537,412 @@
|
||||||
color: #1976d2;
|
color: #1976d2;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 새로운 블록 에디터 스타일 */
|
||||||
|
.content-blocks-editor {
|
||||||
|
border: 2px dashed #e0e0e0;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #fafafa;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-blocks-editor:hover {
|
||||||
|
border-color: #4CAF50;
|
||||||
|
background: #f8fff8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
background: linear-gradient(135deg, #4CAF50, #45a049);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-blocks {
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-block {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-block:hover {
|
||||||
|
border-color: #4CAF50;
|
||||||
|
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-block.dragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: rotate(5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-controls {
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
right: -5px;
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-block:hover .block-controls {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-control-btn {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-control-btn.move {
|
||||||
|
background: #2196F3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-control-btn.delete {
|
||||||
|
background: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-control-btn:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-block textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 80px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
font-family: inherit;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-block {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-area {
|
||||||
|
border: 2px dashed #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 40px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-area:hover {
|
||||||
|
border-color: #4CAF50;
|
||||||
|
background: #f0fff0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-area.dragover {
|
||||||
|
border-color: #4CAF50;
|
||||||
|
background: #e8f5e8;
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-image:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-info {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-alt-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 이미지 모달 */
|
||||||
|
.image-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10000;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-modal.active {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-modal img {
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 90%;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 30px;
|
||||||
|
color: white;
|
||||||
|
font-size: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 10001;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 드래그 앤 드롭 표시 */
|
||||||
|
.drop-indicator {
|
||||||
|
height: 3px;
|
||||||
|
background: #4CAF50;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 5px 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-indicator.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 반응형 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.editor-toolbar {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-info {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 상세 보기 모달 스타일 */
|
||||||
|
.view-field {
|
||||||
|
padding: 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-content {
|
||||||
|
padding: 15px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 6px;
|
||||||
|
min-height: 100px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-content .content-text-block {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-content .content-image-block {
|
||||||
|
margin: 15px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-content .content-image {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-content .content-image:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-content .image-caption {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-meta .meta-badge {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-meta .category-badge {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
border: 1px solid #bbdefb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-meta .target-badge {
|
||||||
|
background: #f3e5f5;
|
||||||
|
color: #7b1fa2;
|
||||||
|
border: 1px solid #e1bee7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-author-info {
|
||||||
|
padding: 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-details {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.creation-date {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 어록 카드 클릭 효과 */
|
||||||
|
.saying-card {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saying-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
|
||||||
|
border-left-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saying-card:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 삭제 버튼 스타일 */
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 편집 모드 전환 애니메이션 */
|
||||||
|
#view-mode, #edit-mode {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 모달 버튼 그룹 */
|
||||||
|
.modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons .btn {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>📚 어록 관리</h1>
|
<h1>📚 타냐대장경 관리</h1>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div id="current-user" class="current-user">👤 로딩 중...</div>
|
<div id="current-user" class="current-user">👤 로딩 중...</div>
|
||||||
<button id="add-saying-btn" class="add-saying-btn">✨ 어록 등록</button>
|
<button id="add-saying-btn" class="add-saying-btn">✨ 타냐대장경 등록</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -586,7 +984,7 @@
|
||||||
|
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label for="search-input">검색</label>
|
<label for="search-input">검색</label>
|
||||||
<input type="text" id="search-input" placeholder="어록 검색..." class="search-input">
|
<input type="text" id="search-input" placeholder="타냐대장경 검색..." class="search-input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filter-buttons">
|
<div class="filter-buttons">
|
||||||
|
|
@ -602,7 +1000,7 @@
|
||||||
<!-- 통계 정보가 여기에 표시됩니다 -->
|
<!-- 통계 정보가 여기에 표시됩니다 -->
|
||||||
</div>
|
</div>
|
||||||
<button class="add-saying-btn" id="add-saying-btn">
|
<button class="add-saying-btn" id="add-saying-btn">
|
||||||
➕ 어록 등록
|
➕ 타냐대장경 등록
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -610,29 +1008,42 @@
|
||||||
<div id="add-saying-modal" class="modal">
|
<div id="add-saying-modal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 class="modal-title">➕ 새 어록 등록</h3>
|
<h3 class="modal-title">➕ 새 타냐대장경 등록</h3>
|
||||||
<button class="modal-close" id="modal-close">×</button>
|
<button class="modal-close" id="modal-close">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="saying-form">
|
<form id="saying-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="saying-title">어록 제목</label>
|
<label class="form-label" for="saying-title">타냐대장경 제목</label>
|
||||||
<input type="text" id="saying-title" class="form-input" placeholder="어록의 제목을 입력하세요" required>
|
<input type="text" id="saying-title" class="form-input" placeholder="타냐대장경의 제목을 입력하세요" required>
|
||||||
<div class="form-help">어록을 대표하는 제목을 입력해주세요.</div>
|
<div class="form-help">타냐대장경을 대표하는 제목을 입력해주세요.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="saying-content">어록 내용</label>
|
<label class="form-label" for="saying-content">타냐대장경 내용</label>
|
||||||
<div class="preview-tabs">
|
<div class="preview-tabs">
|
||||||
<div class="preview-tab active" data-tab="edit">✏️ 편집</div>
|
<div class="preview-tab active" data-tab="edit">✏️ 편집</div>
|
||||||
<div class="preview-tab" data-tab="preview">👁️ 미리보기</div>
|
<div class="preview-tab" data-tab="preview">👁️ 미리보기</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea id="saying-content" class="form-textarea" placeholder="어록 내용을 입력하세요 (마크다운 지원)" required></textarea>
|
|
||||||
|
<!-- 새로운 블록 기반 에디터 -->
|
||||||
|
<div id="content-blocks-editor" class="content-blocks-editor">
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<button type="button" class="toolbar-btn" id="add-text-block">📝 텍스트 추가</button>
|
||||||
|
<button type="button" class="toolbar-btn" id="add-image-block">🖼️ 이미지 추가</button>
|
||||||
|
</div>
|
||||||
|
<div id="content-blocks" class="content-blocks">
|
||||||
|
<!-- 블록들이 동적으로 추가됩니다 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 기존 텍스트 에리어 (하위 호환성) -->
|
||||||
|
<textarea id="saying-content" class="form-textarea" placeholder="어록 내용을 입력하세요 (마크다운 지원)" style="display: none;"></textarea>
|
||||||
|
|
||||||
<div id="markdown-preview" class="markdown-preview" style="display: none;"></div>
|
<div id="markdown-preview" class="markdown-preview" style="display: none;"></div>
|
||||||
<div class="form-help">
|
<div class="form-help">
|
||||||
💡 마크다운 문법을 사용할 수 있습니다.
|
💡 텍스트와 이미지를 자유롭게 조합할 수 있습니다.
|
||||||
<strong>**굵게**</strong>, <em>*기울임*</em>,
|
이미지는 자동으로 압축되어 저장됩니다. (최대 2MB)
|
||||||
<code>`코드`</code>, 링크 등을 지원합니다.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -642,7 +1053,7 @@
|
||||||
<option value="">카테고리를 선택하세요</option>
|
<option value="">카테고리를 선택하세요</option>
|
||||||
<!-- 동적으로 로드됩니다 -->
|
<!-- 동적으로 로드됩니다 -->
|
||||||
</select>
|
</select>
|
||||||
<div class="form-help">어록의 성격에 맞는 카테고리를 선택해주세요.</div>
|
<div class="form-help">타냐대장경의 성격에 맞는 카테고리를 선택해주세요.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
@ -651,7 +1062,7 @@
|
||||||
<option value="">타겟을 선택하세요</option>
|
<option value="">타겟을 선택하세요</option>
|
||||||
<!-- 동적으로 로드됩니다 -->
|
<!-- 동적으로 로드됩니다 -->
|
||||||
</select>
|
</select>
|
||||||
<div class="form-help">어록의 대상을 선택해주세요. (기본값: 누구나)</div>
|
<div class="form-help">타냐대장경의 대상을 선택해주세요. (기본값: 누구나)</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
@ -670,19 +1081,112 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 어록 상세 보기/편집 모달 -->
|
||||||
|
<div id="view-saying-modal" class="modal">
|
||||||
|
<div class="modal-content" style="max-width: 800px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title" id="view-modal-title">📖 타냐대장경 상세 보기</h3>
|
||||||
|
<button class="modal-close" id="view-modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 보기 모드 -->
|
||||||
|
<div id="view-mode" style="display: none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">제목</label>
|
||||||
|
<div id="view-title" class="view-field"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">내용</label>
|
||||||
|
<div id="view-content" class="view-content"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">카테고리 / 타겟</label>
|
||||||
|
<div id="view-meta" class="view-meta"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">등록 정보</label>
|
||||||
|
<div id="view-author-info" class="view-author-info"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 편집 모드 -->
|
||||||
|
<div id="edit-mode" style="display: none;">
|
||||||
|
<form id="edit-saying-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="edit-saying-title">타냐대장경 제목</label>
|
||||||
|
<input type="text" id="edit-saying-title" class="form-input" placeholder="타냐대장경의 제목을 입력하세요" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="edit-saying-content">타냐대장경 내용</label>
|
||||||
|
<div class="preview-tabs">
|
||||||
|
<div class="preview-tab active" data-tab="edit">✏️ 편집</div>
|
||||||
|
<div class="preview-tab" data-tab="preview">👁️ 미리보기</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 편집용 블록 에디터 -->
|
||||||
|
<div id="edit-content-blocks-editor" class="content-blocks-editor">
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<button type="button" class="toolbar-btn" id="edit-add-text-block">📝 텍스트 추가</button>
|
||||||
|
<button type="button" class="toolbar-btn" id="edit-add-image-block">🖼️ 이미지 추가</button>
|
||||||
|
</div>
|
||||||
|
<div id="edit-content-blocks" class="content-blocks">
|
||||||
|
<!-- 블록들이 동적으로 추가됩니다 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 기존 텍스트 에리어 (하위 호환성) -->
|
||||||
|
<textarea id="edit-saying-content" class="form-textarea" placeholder="타냐대장경 내용을 입력하세요" style="display: none;"></textarea>
|
||||||
|
|
||||||
|
<div id="edit-markdown-preview" class="markdown-preview" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="edit-saying-category">카테고리</label>
|
||||||
|
<select id="edit-saying-category" class="form-select" required>
|
||||||
|
<option value="">카테고리를 선택하세요</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="edit-saying-target">타겟</label>
|
||||||
|
<select id="edit-saying-target" class="form-select" required>
|
||||||
|
<option value="">타겟을 선택하세요</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<!-- 보기 모드 버튼 -->
|
||||||
|
<div id="view-mode-buttons" style="display: none;">
|
||||||
|
<button type="button" class="btn btn-secondary" id="view-close-btn">닫기</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="edit-mode-btn" style="display: none;">✏️ 편집하기</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 편집 모드 버튼 -->
|
||||||
|
<div id="edit-mode-buttons" style="display: none;">
|
||||||
|
<button type="button" class="btn btn-secondary" id="edit-cancel-btn">취소</button>
|
||||||
|
<button type="button" class="btn btn-danger" id="delete-saying-btn" style="margin-right: auto;">🗑️ 삭제</button>
|
||||||
|
<button type="submit" class="btn btn-primary" id="update-saying-btn">💾 수정 완료</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 어록 컨테이너 -->
|
<!-- 어록 컨테이너 -->
|
||||||
<div id="sayings-container" class="sayings-container">
|
<div id="sayings-container" class="sayings-container">
|
||||||
<!-- 동적으로 생성됨 -->
|
<!-- 동적으로 생성됨 -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="loading" id="sayings-loading" style="display: none;">
|
<div class="loading" id="sayings-loading" style="display: none;">
|
||||||
🔄 어록을 불러오는 중...
|
🔄 타냐대장경을 불러오는 중...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 마크다운 라이브러리를 직접 로드 -->
|
<!-- 마크다운 라이브러리는 sayings.js 내장 함수 사용 (CSP 호환) -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked@9.1.6/marked.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js"></script>
|
|
||||||
|
|
||||||
<script src="sayings.js"></script>
|
<script src="sayings.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
1699
sayings.js
|
|
@ -0,0 +1,590 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>설정 - 내차는언제타냐 통합확장기</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #9b59b6, #8e44ad);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 탭 스타일 */
|
||||||
|
.tab-container {
|
||||||
|
display: flex;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 2px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6c757d;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button.active {
|
||||||
|
color: #9b59b6;
|
||||||
|
border-bottom-color: #9b59b6;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-info {
|
||||||
|
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
|
||||||
|
border: 1px solid #90caf9;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1976d2;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-engines {
|
||||||
|
color: #424242;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-description {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-item:hover {
|
||||||
|
border-color: #9b59b6;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-item.disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
background: #f1f3f4;
|
||||||
|
border-color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-item.disabled:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border-color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-badge {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-free {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-premium {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-vip {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-description {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-features {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9b59b6;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
width: 60px;
|
||||||
|
height: 30px;
|
||||||
|
background: #ccc;
|
||||||
|
border-radius: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch.active {
|
||||||
|
background: #9b59b6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch.disabled {
|
||||||
|
background: #e9ecef;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
transition: left 0.3s;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch.active::after {
|
||||||
|
left: 33px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 시간 알람 설정 스타일 */
|
||||||
|
.time-alarm-section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-input-group label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #495057;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-input-group input {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-input-group span {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background: linear-gradient(135deg, #9b59b6, #8e44ad);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 30px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(155, 89, 182, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: linear-gradient(135deg, #6c757d, #5a6268);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
box-shadow: 0 5px 15px rgba(108, 117, 125, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: none;
|
||||||
|
text-align: center;
|
||||||
|
color: #9b59b6;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 15px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
margin: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch {
|
||||||
|
margin-left: 0;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-input-group {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>⚙️ 설정</h1>
|
||||||
|
<p>번역 엔진 및 시간 알람 설정을 관리합니다</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 탭 메뉴 -->
|
||||||
|
<div class="tab-container">
|
||||||
|
<button class="tab-button active" data-tab="translation">🌐 번역 엔진</button>
|
||||||
|
<button class="tab-button" data-tab="time-alarm">⏰ 시간 알람</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<!-- 번역 엔진 설정 탭 -->
|
||||||
|
<div class="tab-content active" id="translation-tab">
|
||||||
|
<!-- 회원등급 정보 -->
|
||||||
|
<div class="membership-info">
|
||||||
|
<div class="membership-title">현재 회원등급: <span id="current-membership">로딩 중...</span></div>
|
||||||
|
<div class="membership-engines" id="membership-engines-info">회원 정보를 불러오는 중입니다...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 번역 엔진 설정 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">
|
||||||
|
<span>📋</span>
|
||||||
|
노출할 번역 엔진 선택
|
||||||
|
</div>
|
||||||
|
<div class="section-description">
|
||||||
|
체크된 엔진만 멀티번역 결과에 표시됩니다. 회원등급에 따라 일부 엔진은 사용할 수 없습니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="engine-list">
|
||||||
|
<div class="engine-item" data-engine="google">
|
||||||
|
<div class="engine-info">
|
||||||
|
<div class="engine-name">
|
||||||
|
<span>🌐</span>
|
||||||
|
Google 번역
|
||||||
|
<span class="engine-badge badge-free">무료</span>
|
||||||
|
</div>
|
||||||
|
<div class="engine-description">
|
||||||
|
빠른 속도와 다양한 언어 지원을 제공하는 구글의 무료 번역 서비스
|
||||||
|
</div>
|
||||||
|
<div class="engine-features">
|
||||||
|
✓ 100개 이상 언어 지원 ✓ 실시간 번역 ✓ 무료 사용
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="toggle-switch" data-engine="google"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="engine-item" data-engine="mymemory">
|
||||||
|
<div class="engine-info">
|
||||||
|
<div class="engine-name">
|
||||||
|
<span>💾</span>
|
||||||
|
MyMemory 번역
|
||||||
|
<span class="engine-badge badge-free">무료</span>
|
||||||
|
</div>
|
||||||
|
<div class="engine-description">
|
||||||
|
번역 메모리 기반으로 높은 품질의 번역을 제공하는 무료 서비스
|
||||||
|
</div>
|
||||||
|
<div class="engine-features">
|
||||||
|
✓ 번역 메모리 활용 ✓ 높은 번역 품질 ✓ 무료 사용
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="toggle-switch" data-engine="mymemory"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="engine-item" data-engine="deepl">
|
||||||
|
<div class="engine-info">
|
||||||
|
<div class="engine-name">
|
||||||
|
<span>🎯</span>
|
||||||
|
DeepL 번역
|
||||||
|
<span class="engine-badge badge-premium">프리미엄</span>
|
||||||
|
</div>
|
||||||
|
<div class="engine-description">
|
||||||
|
높은 번역 품질로 유명한 AI 번역 서비스
|
||||||
|
</div>
|
||||||
|
<div class="engine-features">
|
||||||
|
✓ 최고 품질 번역 ✓ 문맥 이해 ✓ 프리미엄 이상 사용 가능
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="toggle-switch" data-engine="deepl"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="engine-item" data-engine="openai">
|
||||||
|
<div class="engine-info">
|
||||||
|
<div class="engine-name">
|
||||||
|
<span>🤖</span>
|
||||||
|
OpenAI ChatGPT
|
||||||
|
<span class="engine-badge badge-vip">VIP</span>
|
||||||
|
</div>
|
||||||
|
<div class="engine-description">
|
||||||
|
문맥을 이해하는 고품질 AI 번역, 의역 및 설명 포함
|
||||||
|
</div>
|
||||||
|
<div class="engine-features">
|
||||||
|
✓ AI 기반 번역 ✓ 문맥 이해 ✓ 의역 제공 ✓ VIP 전용
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="toggle-switch" data-engine="openai"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="engine-item disabled" data-engine="gemini">
|
||||||
|
<div class="engine-info">
|
||||||
|
<div class="engine-name">
|
||||||
|
<span>💎</span>
|
||||||
|
Google Gemini
|
||||||
|
<span class="engine-badge badge-vip">비활성화</span>
|
||||||
|
</div>
|
||||||
|
<div class="engine-description">
|
||||||
|
현재 사용할 수 없습니다 (서비스 점검 중)
|
||||||
|
</div>
|
||||||
|
<div class="engine-features">
|
||||||
|
✗ 현재 사용 불가 ✗ 서비스 점검 중
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="toggle-switch disabled" data-engine="gemini"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 시간 알람 설정 탭 -->
|
||||||
|
<div class="tab-content" id="time-alarm-tab">
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">
|
||||||
|
<span>⏰</span>
|
||||||
|
시간 알람 설정
|
||||||
|
</div>
|
||||||
|
<div class="section-description">
|
||||||
|
작업 시간과 휴식 시간을 설정하여 건강한 작업 패턴을 유지하세요.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="time-alarm-section">
|
||||||
|
<div class="time-input-group">
|
||||||
|
<label>시간 알람:</label>
|
||||||
|
<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="60" max="480" value="60">
|
||||||
|
<span>분 (작업 후 휴식 알림)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="time-input-group">
|
||||||
|
<label>휴식 시간:</label>
|
||||||
|
<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">
|
||||||
|
<label for="autoZzimCheckbox">휴식 중 자동 찜 기능 활성화</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message" id="message"></div>
|
||||||
|
<div class="loading" id="loading">⏳ 설정을 저장하는 중...</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn" id="save-settings">💾 설정 저장</button>
|
||||||
|
<button class="btn btn-secondary" id="reset-settings">🔄 기본값으로 복원</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>© 2024 내차는언제타냐 통합확장기 - 번역 엔진 및 시간 알람 설정</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="settings.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,529 @@
|
||||||
|
// 설정 페이지 JavaScript
|
||||||
|
class SettingsManager {
|
||||||
|
constructor() {
|
||||||
|
this.config = null;
|
||||||
|
this.userInfo = null;
|
||||||
|
this.currentSettings = {};
|
||||||
|
this.timeAlarmSettings = {};
|
||||||
|
|
||||||
|
// 회원등급별 사용 가능한 엔진 정의
|
||||||
|
this.availableEngines = {
|
||||||
|
'basic': ['google', 'mymemory'],
|
||||||
|
'premium': ['google', 'mymemory', 'deepl'],
|
||||||
|
'vip': ['google', 'mymemory', 'deepl', 'openai']
|
||||||
|
};
|
||||||
|
|
||||||
|
// 엔진별 상세 정보
|
||||||
|
this.engineInfo = {
|
||||||
|
'google': {
|
||||||
|
name: 'Google 번역',
|
||||||
|
description: '빠른 속도와 다양한 언어 지원을 제공하는 구글의 무료 번역 서비스',
|
||||||
|
level: 'basic'
|
||||||
|
},
|
||||||
|
'mymemory': {
|
||||||
|
name: 'MyMemory 번역',
|
||||||
|
description: '번역 메모리 기반으로 높은 품질의 번역을 제공하는 무료 서비스',
|
||||||
|
level: 'basic'
|
||||||
|
},
|
||||||
|
'deepl': {
|
||||||
|
name: 'DeepL 번역',
|
||||||
|
description: '높은 번역 품질로 유명한 AI 번역 서비스',
|
||||||
|
level: 'premium'
|
||||||
|
},
|
||||||
|
'openai': {
|
||||||
|
name: 'OpenAI ChatGPT',
|
||||||
|
description: '문맥을 이해하는 고품질 AI 번역, 의역 및 설명 포함',
|
||||||
|
level: 'vip'
|
||||||
|
},
|
||||||
|
'gemini': {
|
||||||
|
name: 'Google Gemini',
|
||||||
|
description: '구글의 최신 AI 모델, 자연스러운 번역과 의역 제공',
|
||||||
|
level: 'vip'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
console.log('설정 페이지 초기화 시작');
|
||||||
|
|
||||||
|
// 탭 기능 초기화
|
||||||
|
this.initTabs();
|
||||||
|
|
||||||
|
await this.loadConfig();
|
||||||
|
await this.loadUserInfo();
|
||||||
|
await this.loadCurrentSettings();
|
||||||
|
await this.loadTimeAlarmSettings();
|
||||||
|
|
||||||
|
// UI 초기화
|
||||||
|
this.initializeUI();
|
||||||
|
this.initializeTimeAlarmUI();
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.updateUI();
|
||||||
|
|
||||||
|
console.log('설정 페이지 초기화 완료');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('설정 페이지 초기화 실패:', error);
|
||||||
|
this.showMessage('설정 페이지를 초기화하는 중 오류가 발생했습니다.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 탭 기능 초기화
|
||||||
|
initTabs() {
|
||||||
|
const tabButtons = document.querySelectorAll('.tab-button');
|
||||||
|
const tabContents = document.querySelectorAll('.tab-content');
|
||||||
|
|
||||||
|
tabButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const targetTab = button.dataset.tab;
|
||||||
|
|
||||||
|
// 모든 탭 버튼과 컨텐츠 비활성화
|
||||||
|
tabButtons.forEach(btn => btn.classList.remove('active'));
|
||||||
|
tabContents.forEach(content => content.classList.remove('active'));
|
||||||
|
|
||||||
|
// 선택된 탭 활성화
|
||||||
|
button.classList.add('active');
|
||||||
|
const targetContent = document.getElementById(`${targetTab}-tab`);
|
||||||
|
if (targetContent) {
|
||||||
|
targetContent.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`탭 전환: ${targetTab}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadConfig() {
|
||||||
|
try {
|
||||||
|
const result = await chrome.storage.local.get('settings_config');
|
||||||
|
this.config = result.settings_config || {};
|
||||||
|
console.log('설정을 settings_config에서 로드함:', this.config);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('설정 로드 실패:', error);
|
||||||
|
this.config = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadUserInfo() {
|
||||||
|
try {
|
||||||
|
console.log('사용자 정보 로드 시작...');
|
||||||
|
|
||||||
|
if (!this.config || !this.config.ACCESS_TOKEN) {
|
||||||
|
console.warn('액세스 토큰이 없습니다. 기본 회원으로 설정합니다.');
|
||||||
|
this.userInfo = { membership_level: 'basic' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background Script에 사용자 정보 요청
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
action: 'getUserInfo',
|
||||||
|
token: this.config.ACCESS_TOKEN
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response || !response.success) {
|
||||||
|
throw new Error(response?.error || '사용자 정보를 불러올 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.userInfo = response.user;
|
||||||
|
|
||||||
|
// membership_level이 없으면 기본값 설정
|
||||||
|
if (!this.userInfo.membership_level) {
|
||||||
|
this.userInfo.membership_level = 'basic';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('사용자 정보 로드 완료:', this.userInfo);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('사용자 정보 로드 실패:', error);
|
||||||
|
this.userInfo = { membership_level: 'basic' };
|
||||||
|
console.log('기본 회원으로 설정됨');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadCurrentSettings() {
|
||||||
|
try {
|
||||||
|
const result = await chrome.storage.local.get('translation_engine_settings');
|
||||||
|
this.currentSettings = result.translation_engine_settings || {};
|
||||||
|
|
||||||
|
// 기본값 설정 (구글만 활성화, 나머지는 비활성화)
|
||||||
|
const defaultSettings = {
|
||||||
|
google: true, // 구글만 기본 활성화
|
||||||
|
mymemory: false, // MyMemory 비활성화
|
||||||
|
deepl: false, // DeepL 비활성화
|
||||||
|
openai: false, // OpenAI 비활성화
|
||||||
|
gemini: false // Gemini 비활성화
|
||||||
|
};
|
||||||
|
|
||||||
|
// 기존 설정이 없으면 기본값 사용
|
||||||
|
Object.keys(defaultSettings).forEach(engine => {
|
||||||
|
if (this.currentSettings[engine] === undefined) {
|
||||||
|
this.currentSettings[engine] = defaultSettings[engine];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('현재 설정 로드:', this.currentSettings);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('현재 설정 로드 실패:', error);
|
||||||
|
// 기본값으로 구글만 활성화
|
||||||
|
this.currentSettings = {
|
||||||
|
google: true,
|
||||||
|
mymemory: false,
|
||||||
|
deepl: false,
|
||||||
|
openai: false,
|
||||||
|
gemini: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTimeAlarmSettings() {
|
||||||
|
try {
|
||||||
|
const result = await chrome.storage.local.get('time_alarm_settings');
|
||||||
|
this.timeAlarmSettings = result.time_alarm_settings || {
|
||||||
|
enabled: true,
|
||||||
|
workTime: 60, // 분
|
||||||
|
restTime: 5, // 분
|
||||||
|
autoZzim: false,
|
||||||
|
pomodoro: false,
|
||||||
|
cycle: 0
|
||||||
|
};
|
||||||
|
console.log('시간 알람 설정 로드 완료:', this.timeAlarmSettings);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('시간 알람 설정 로드 실패:', error);
|
||||||
|
this.timeAlarmSettings = {
|
||||||
|
enabled: true,
|
||||||
|
workTime: 60,
|
||||||
|
restTime: 5,
|
||||||
|
autoZzim: false,
|
||||||
|
pomodoro: false,
|
||||||
|
cycle: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeUI() {
|
||||||
|
// 회원등급 정보 표시
|
||||||
|
const membershipElement = document.getElementById('current-membership');
|
||||||
|
const membershipInfoElement = document.getElementById('membership-engines-info');
|
||||||
|
|
||||||
|
if (membershipElement && this.userInfo) {
|
||||||
|
const level = this.userInfo.membership_level || 'basic';
|
||||||
|
const levelNames = {
|
||||||
|
'basic': '기본 회원',
|
||||||
|
'premium': '프리미엄 회원',
|
||||||
|
'vip': 'VIP 회원'
|
||||||
|
};
|
||||||
|
|
||||||
|
membershipElement.textContent = levelNames[level.toLowerCase()] || '기본 회원';
|
||||||
|
|
||||||
|
const availableEngines = this.availableEngines[level.toLowerCase()] || this.availableEngines.basic;
|
||||||
|
const engineNames = availableEngines.map(engine => this.engineInfo[engine].name);
|
||||||
|
|
||||||
|
membershipInfoElement.innerHTML = `
|
||||||
|
<strong>사용 가능한 번역 엔진:</strong><br>
|
||||||
|
${engineNames.join(', ')}<br><br>
|
||||||
|
<strong>등급별 혜택:</strong><br>
|
||||||
|
• 기본: Google, MyMemory (무료 엔진)<br>
|
||||||
|
• 프리미엄: + DeepL (고품질 엔진)<br>
|
||||||
|
• VIP: + ChatGPT
|
||||||
|
`;
|
||||||
|
|
||||||
|
console.log('회원등급 UI 업데이트 완료:', level);
|
||||||
|
} else {
|
||||||
|
console.error('회원등급 표시 요소를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 시간 알람 UI 초기화
|
||||||
|
initializeTimeAlarmUI() {
|
||||||
|
this.updateTimeAlarmUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// 토글 스위치 이벤트
|
||||||
|
const toggleSwitches = document.querySelectorAll('.toggle-switch');
|
||||||
|
toggleSwitches.forEach(toggle => {
|
||||||
|
toggle.addEventListener('click', (e) => {
|
||||||
|
const engine = e.target.dataset.engine;
|
||||||
|
if (engine && !e.target.classList.contains('disabled')) {
|
||||||
|
this.toggleEngine(engine);
|
||||||
|
} else if (e.target.id === 'timeAlarmToggle') {
|
||||||
|
// 시간 알람 토글
|
||||||
|
this.timeAlarmSettings.enabled = !this.timeAlarmSettings.enabled;
|
||||||
|
this.updateTimeAlarmUI();
|
||||||
|
console.log('시간 알람 토글:', this.timeAlarmSettings.enabled);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 저장 버튼
|
||||||
|
const saveButton = document.getElementById('save-settings');
|
||||||
|
if (saveButton) {
|
||||||
|
saveButton.addEventListener('click', () => this.saveSettings());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기본값 복원 버튼
|
||||||
|
const resetButton = document.getElementById('reset-settings');
|
||||||
|
if (resetButton) {
|
||||||
|
resetButton.addEventListener('click', () => this.resetToDefaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 시간 입력 필드 이벤트
|
||||||
|
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) => {
|
||||||
|
this.timeAlarmSettings.workTime = parseInt(e.target.value) || 60;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restTimeInput) {
|
||||||
|
restTimeInput.addEventListener('change', (e) => {
|
||||||
|
this.timeAlarmSettings.restTime = parseInt(e.target.value) || 5;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoZzimCheckbox) {
|
||||||
|
autoZzimCheckbox.addEventListener('change', (e) => {
|
||||||
|
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() {
|
||||||
|
const userLevel = (this.userInfo?.membership_level || 'basic').toLowerCase();
|
||||||
|
const availableEngines = this.availableEngines[userLevel] || this.availableEngines.basic;
|
||||||
|
|
||||||
|
// 각 엔진 아이템 업데이트
|
||||||
|
const engineItems = document.querySelectorAll('.engine-item');
|
||||||
|
engineItems.forEach(item => {
|
||||||
|
const engine = item.dataset.engine;
|
||||||
|
const toggle = item.querySelector('.toggle-switch');
|
||||||
|
|
||||||
|
if (availableEngines.includes(engine)) {
|
||||||
|
// 사용 가능한 엔진
|
||||||
|
item.classList.remove('disabled');
|
||||||
|
toggle.classList.remove('disabled');
|
||||||
|
|
||||||
|
// 현재 설정에 따라 토글 상태 설정
|
||||||
|
if (this.currentSettings[engine]) {
|
||||||
|
toggle.classList.add('active');
|
||||||
|
} else {
|
||||||
|
toggle.classList.remove('active');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 사용 불가능한 엔진
|
||||||
|
item.classList.add('disabled');
|
||||||
|
toggle.classList.add('disabled');
|
||||||
|
toggle.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 시간 알람 UI 업데이트
|
||||||
|
this.updateTimeAlarmUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimeAlarmUI() {
|
||||||
|
const timeAlarmToggle = document.getElementById('timeAlarmToggle');
|
||||||
|
const workTimeInput = document.getElementById('workTimeInput');
|
||||||
|
const restTimeInput = document.getElementById('restTimeInput');
|
||||||
|
const autoZzimCheckbox = document.getElementById('autoZzimCheckbox');
|
||||||
|
|
||||||
|
if (timeAlarmToggle) {
|
||||||
|
if (this.timeAlarmSettings.enabled) {
|
||||||
|
timeAlarmToggle.classList.add('active');
|
||||||
|
} else {
|
||||||
|
timeAlarmToggle.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
autoZzimCheckbox.checked = this.timeAlarmSettings.autoZzim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleEngine(engine) {
|
||||||
|
this.currentSettings[engine] = !this.currentSettings[engine];
|
||||||
|
this.updateUI();
|
||||||
|
console.log(`${engine} 엔진 토글:`, this.currentSettings[engine]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettings() {
|
||||||
|
try {
|
||||||
|
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');
|
||||||
|
|
||||||
|
if (workTimeInput) {
|
||||||
|
this.timeAlarmSettings.workTime = parseInt(workTimeInput.value) || 60;
|
||||||
|
}
|
||||||
|
if (restTimeInput) {
|
||||||
|
this.timeAlarmSettings.restTime = parseInt(restTimeInput.value) || 5;
|
||||||
|
}
|
||||||
|
if (autoZzimCheckbox) {
|
||||||
|
this.timeAlarmSettings.autoZzim = autoZzimCheckbox.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 번역 엔진 설정 저장
|
||||||
|
await chrome.storage.local.set({
|
||||||
|
'translation_engine_settings': this.currentSettings
|
||||||
|
});
|
||||||
|
|
||||||
|
// 시간 알람 설정 저장
|
||||||
|
await chrome.storage.local.set({
|
||||||
|
'time_alarm_settings': this.timeAlarmSettings
|
||||||
|
});
|
||||||
|
|
||||||
|
// Background script에 시간 알람 설정 변경 알림
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'updateTimeAlarmSettings',
|
||||||
|
settings: this.timeAlarmSettings
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('설정 저장 완료:', {
|
||||||
|
translation: this.currentSettings,
|
||||||
|
timeAlarm: this.timeAlarmSettings
|
||||||
|
});
|
||||||
|
|
||||||
|
this.showMessage('설정이 성공적으로 저장되었습니다.', 'success');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('설정 저장 실패:', error);
|
||||||
|
this.showMessage('설정 저장 중 오류가 발생했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
this.showLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetToDefaults() {
|
||||||
|
if (!confirm('모든 설정을 기본값으로 복원하시겠습니까?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.showLoading(true);
|
||||||
|
|
||||||
|
// 기본값으로 재설정 (구글만 활성화)
|
||||||
|
this.currentSettings = {
|
||||||
|
google: true,
|
||||||
|
mymemory: false,
|
||||||
|
deepl: false,
|
||||||
|
openai: false,
|
||||||
|
gemini: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 저장
|
||||||
|
await chrome.storage.local.set({
|
||||||
|
'translation_engine_settings': this.currentSettings
|
||||||
|
});
|
||||||
|
|
||||||
|
// UI 업데이트
|
||||||
|
this.updateUI();
|
||||||
|
|
||||||
|
// 시간 알람 설정 초기화
|
||||||
|
this.timeAlarmSettings = {
|
||||||
|
enabled: true,
|
||||||
|
workTime: 60,
|
||||||
|
restTime: 5,
|
||||||
|
autoZzim: false,
|
||||||
|
pomodoro: false,
|
||||||
|
cycle: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 저장소에서 시간 알람 설정 제거
|
||||||
|
await chrome.storage.local.remove('time_alarm_settings');
|
||||||
|
|
||||||
|
// Background script에 알림
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'updateTimeAlarmSettings',
|
||||||
|
settings: this.timeAlarmSettings
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('기본값 복원 완료');
|
||||||
|
this.showMessage('설정이 기본값으로 복원되었습니다.', 'success');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('기본값 복원 실패:', error);
|
||||||
|
this.showMessage('기본값 복원 중 오류가 발생했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
this.showLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showMessage(text, type = 'info') {
|
||||||
|
const messageElement = document.getElementById('message');
|
||||||
|
if (messageElement) {
|
||||||
|
messageElement.textContent = text;
|
||||||
|
messageElement.className = `message ${type}`;
|
||||||
|
messageElement.style.display = 'block';
|
||||||
|
|
||||||
|
// 3초 후 자동 숨김
|
||||||
|
setTimeout(() => {
|
||||||
|
messageElement.style.display = 'none';
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading(show) {
|
||||||
|
const loadingElement = document.getElementById('loading');
|
||||||
|
if (loadingElement) {
|
||||||
|
loadingElement.style.display = show ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 페이지 로드 시 초기화
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
console.log('설정 페이지 DOM 로드 완료');
|
||||||
|
new SettingsManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 창 닫기 전 확인
|
||||||
|
window.addEventListener('beforeunload', (e) => {
|
||||||
|
// 설정이 변경되었는지 확인하는 로직을 추가할 수 있음
|
||||||
|
// 현재는 단순히 로그만 출력
|
||||||
|
console.log('설정 페이지 종료');
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
npx web-ext run --target=chromium --start-url="https://www.naver.com"
|
||||||
|
|
@ -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,207 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>회원등급 및 가격안내</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: #181d27;
|
||||||
|
color: #f3f6fc;
|
||||||
|
font-family: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 30px 0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 650px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
background: #23293a;
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 8px 24px rgba(30,40,70,0.10);
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #38b6ff;
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 2em;
|
||||||
|
text-shadow: 1px 1px 7px #111b;
|
||||||
|
}
|
||||||
|
/* 등급 카드 영역 */
|
||||||
|
.grade-cards {
|
||||||
|
display: flex;
|
||||||
|
gap: 18px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.grade-card {
|
||||||
|
flex: 1 1 140px;
|
||||||
|
min-width: 180px;
|
||||||
|
background: #222b3d;
|
||||||
|
border-radius: 13px;
|
||||||
|
padding: 20px 18px 14px 18px;
|
||||||
|
box-shadow: 0 2px 12px rgba(56,182,255,0.10);
|
||||||
|
border: 2px solid #1b2a44;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 0.15s;
|
||||||
|
}
|
||||||
|
.grade-card:hover {
|
||||||
|
transform: translateY(-7px) scale(1.04);
|
||||||
|
border-color: #38b6ff;
|
||||||
|
box-shadow: 0 5px 26px #38b6ff33;
|
||||||
|
}
|
||||||
|
.grade-title {
|
||||||
|
font-size: 1.19em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.basic { color: #8efaff; }
|
||||||
|
.premium { color: #ffe066; }
|
||||||
|
.vip { color: #fc7676; }
|
||||||
|
.grade-detail {
|
||||||
|
font-size: 1em;
|
||||||
|
color: #c3d7ee;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.api-limit {
|
||||||
|
background: #172436;
|
||||||
|
border-radius: 9px;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 14px;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 0.97em;
|
||||||
|
color: #5be0ff;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 가격 안내 표 */
|
||||||
|
.price-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0 8px;
|
||||||
|
margin-top: 18px;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
.price-table th, .price-table td {
|
||||||
|
padding: 13px 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.price-table th {
|
||||||
|
background: #243350;
|
||||||
|
color: #88e6ff;
|
||||||
|
font-size: 1.08em;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
border-bottom: 2px solid #38b6ff99;
|
||||||
|
}
|
||||||
|
.price-table td {
|
||||||
|
background: #192339;
|
||||||
|
font-size: 1.07em;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.price-table .highlight {
|
||||||
|
font-weight: bold;
|
||||||
|
background: #222d48;
|
||||||
|
color: #38b6ff;
|
||||||
|
border-left: 5px solid #38b6ff99;
|
||||||
|
}
|
||||||
|
.bonus {
|
||||||
|
color: #ffe066;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
/* 모바일 대응 */
|
||||||
|
@media (max-width: 650px) {
|
||||||
|
.container { padding: 8px 2vw;}
|
||||||
|
.grade-cards {flex-direction: column; gap: 9px;}
|
||||||
|
.grade-card { min-width: 0; padding: 14px 10px 8px 10px;}
|
||||||
|
.price-table th, .price-table td { padding: 10px 5px; font-size: 0.98em;}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h2>회원등급 안내</h2>
|
||||||
|
<div class="grade-cards">
|
||||||
|
<div class="grade-card">
|
||||||
|
<div class="grade-title basic">Basic</div>
|
||||||
|
<div class="grade-detail">기본기능 제공</div>
|
||||||
|
<div class="grade-detail">API 20회/일</div>
|
||||||
|
<div class="api-limit">OCR 미포함</div>
|
||||||
|
</div>
|
||||||
|
<div class="grade-card">
|
||||||
|
<div class="grade-title premium">Premium</div>
|
||||||
|
<div class="grade-detail">Basic + OCR</div>
|
||||||
|
<div class="grade-detail">API 50회/일</div>
|
||||||
|
<div class="api-limit" style="color:#ffe066">OCR 포함</div>
|
||||||
|
</div>
|
||||||
|
<div class="grade-card">
|
||||||
|
<div class="grade-title vip">VIP</div>
|
||||||
|
<div class="grade-detail">Premium + API 한도 UP</div>
|
||||||
|
<div class="grade-detail">API 200회/일</div>
|
||||||
|
<div class="api-limit" style="color:#fc7676">모든 기능 무제한</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style="margin-top:38px;">가격 안내</h2>
|
||||||
|
<table class="price-table">
|
||||||
|
<tr>
|
||||||
|
<th>기간</th>
|
||||||
|
<th>등급</th>
|
||||||
|
<th>가격</th>
|
||||||
|
<th>혜택</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="3" class="highlight">1개월</td>
|
||||||
|
<td>Basic</td>
|
||||||
|
<td>5만원</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Premium</td>
|
||||||
|
<td>7만원</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>VIP</td>
|
||||||
|
<td>10만원</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="3" class="highlight">3개월</td>
|
||||||
|
<td>Basic</td>
|
||||||
|
<td>15만원</td>
|
||||||
|
<td class="bonus">+ 추가 15일 제공</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Premium</td>
|
||||||
|
<td>21만원</td>
|
||||||
|
<td class="bonus">+ 추가 15일 제공</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>VIP</td>
|
||||||
|
<td>30만원</td>
|
||||||
|
<td class="bonus">+ 추가 15일 제공</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="3" class="highlight">6개월</td>
|
||||||
|
<td>Basic</td>
|
||||||
|
<td>30만원</td>
|
||||||
|
<td class="bonus">+ 추가 1개월 제공</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Premium</td>
|
||||||
|
<td>42만원</td>
|
||||||
|
<td class="bonus">+ 추가 1개월 제공</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>VIP</td>
|
||||||
|
<td>60만원</td>
|
||||||
|
<td class="bonus">+ 추가 1개월 제공</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
-- public.events 테이블에 휴식 시간 추천활동 샘플 데이터 추가
|
||||||
|
-- event_type = 'rest_time', message 필드는 JSON 형식으로 저장
|
||||||
|
|
||||||
|
INSERT INTO public.events (event_type, message, created_at, event_start, amount_day) VALUES
|
||||||
|
('rest_time', '{"activity": "🚶♀️ 가벼운 산책을 하며 신선한 공기를 마셔보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "💧 물 한 잔을 마시며 수분을 보충하고 몸을 깨워보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🧘♀️ 5분간 심호흡을 하며 명상으로 마음을 정리해보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🤸♀️ 간단한 목과 어깨 스트레칭으로 뭉친 근육을 풀어보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "👀 20-20-20 법칙: 20초간 20피트(6m) 떨어진 곳을 보며 눈을 쉬게 해주세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🚽 화장실을 다녀오며 잠시 자리에서 일어나보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🌱 창밖을 보며 자연을 감상하고 마음의 평화를 찾아보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "📱 스마트폰을 내려놓고 디지털 디톡스 시간을 가져보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "☕ 따뜻한 차나 커피 한 잔으로 여유로운 시간을 만들어보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🎵 좋아하는 음악을 들으며 잠시 현실을 잊고 휴식해보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "📚 짧은 명언이나 좋은 글을 읽으며 마음에 영양을 공급해보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🤝 동료나 가족과 간단한 안부 인사를 나누며 소통해보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🧴 손목, 목, 어깨를 가볍게 마사지하며 피로를 풀어보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🏃♀️ 제자리에서 가볍게 몸을 움직이며 혈액순환을 도와보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🍎 건강한 간식(견과류, 과일)으로 에너지를 충전해보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🌬️ 발코니나 창가에서 신선한 공기를 마시며 기분전환을 해보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🧹 책상 주변을 정리하며 작업 환경을 깔끔하게 만들어보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🎯 오늘의 목표를 다시 한번 점검하며 동기부여를 해보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "💪 간단한 팔굽혀펴기나 스쿼트로 몸에 활력을 불어넣어보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🌟 지금까지 완료한 업무를 되돌아보며 성취감을 느껴보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🎨 간단한 낙서나 그림을 그리며 창의력을 자극해보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🧠 브레인 스토밍: 새로운 아이디어나 해결책을 자유롭게 생각해보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "📞 소중한 사람에게 안부 전화를 걸어 따뜻한 대화를 나누어보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🔥 업무 스트레스를 떨쳐내고 월매출 1억 달성 의지를 다져보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "💼 성공한 미래의 모습을 상상하며 동기부여 시간을 가져보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"text": "🏆 오늘 하루도 열심히 달려온 자신에게 박수를 보내세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "⚡ 5분간 파워 낮잠으로 에너지를 완전히 충전해보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🎪 좋아하는 유튜브 영상을 하나 보며 웃음으로 스트레스를 날려보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🌈 긍정적인 생각으로 마음을 채우고 다시 시작할 준비를 해보세요"}', NOW(), NOW(), 9999),
|
||||||
|
('rest_time', '{"activity": "🎲 새로운 도전을 위한 작은 계획을 세워보는 시간을 가져보세요"}', NOW(), NOW(), 9999);
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"deepl": {
|
||||||
|
"authKey": "6f07317d-f155-46f9-84a0-033ed942c9c6:fx"
|
||||||
|
},
|
||||||
|
"openai": {
|
||||||
|
"apiKey": "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"
|
||||||
|
},
|
||||||
|
"gemini": {
|
||||||
|
"apiKey": "AIzaSyByn419VWPyQlbdYJXderUEFz7_PeMvZhY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
// background.js (Service Worker)
|
|
||||||
|
|
||||||
// 1) 확장 프로그램 설치/업데이트 시 컨텍스트 메뉴 생성
|
|
||||||
chrome.runtime.onInstalled.addListener(() => {
|
|
||||||
chrome.contextMenus.create({
|
|
||||||
id: "searchTrademark",
|
|
||||||
title: "지재권 검색",
|
|
||||||
contexts: ["selection"] // 텍스트를 드래그 선택한 상태에서만 메뉴 표시
|
|
||||||
});
|
|
||||||
|
|
||||||
// (선택) 서비스 워커 keep-alive
|
|
||||||
chrome.alarms.create("keepAlive", { periodInMinutes: 4 });
|
|
||||||
});
|
|
||||||
|
|
||||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
||||||
if (alarm.name === "keepAlive") {
|
|
||||||
console.log("[background.js] 서비스 워커 유지 알람 실행됨");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2) 컨텍스트 메뉴 클릭 시 처리
|
|
||||||
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
|
||||||
if (info.menuItemId === "searchTrademark") {
|
|
||||||
// 선택된 텍스트(드래그된 내용)
|
|
||||||
const keyword = info.selectionText.trim();
|
|
||||||
if (!keyword) return;
|
|
||||||
|
|
||||||
console.log("[background.js] 컨텍스트 메뉴 검색 키워드:", keyword);
|
|
||||||
|
|
||||||
// MarkInfo 검색 URL (키워드 검색 예시)
|
|
||||||
const url = buildMarkInfoUrl(keyword);
|
|
||||||
|
|
||||||
// fetch로 마크인포 요청
|
|
||||||
fetch(url)
|
|
||||||
.then((resp) => {
|
|
||||||
if (!resp.ok) {
|
|
||||||
throw new Error(`네트워크 오류: ${resp.status}`);
|
|
||||||
}
|
|
||||||
return resp.text();
|
|
||||||
})
|
|
||||||
.then((html) => {
|
|
||||||
// __NUXT_DATA__ 추출 (정규표현식 예시)
|
|
||||||
const match = /<script[^>]*id="__NUXT_DATA__"[^>]*>([\s\S]*?)<\/script>/i.exec(html);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error("__NUXT_DATA__ 태그를 찾을 수 없습니다.");
|
|
||||||
}
|
|
||||||
const jsonString = match[1];
|
|
||||||
const globalData = JSON.parse(jsonString);
|
|
||||||
|
|
||||||
// 예: globalData 배열 중 첫 번째 아이템이 주요 정보라고 가정
|
|
||||||
let firstItem = null;
|
|
||||||
if (Array.isArray(globalData) && globalData.length > 0) {
|
|
||||||
firstItem = globalData[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 노티피케이션에 표시할 문자열 생성
|
|
||||||
let msg = "";
|
|
||||||
if (firstItem && typeof firstItem === "object") {
|
|
||||||
// 예: applicationNum, trademarkName 등 필요한 필드만 추출
|
|
||||||
const applicationNum = firstItem.applicationNum || "(출원번호 없음)";
|
|
||||||
const trademarkName = firstItem.trademarkName || "(상표명 없음)";
|
|
||||||
msg = `출원번호: ${applicationNum}\n상표명: ${trademarkName}`;
|
|
||||||
} else {
|
|
||||||
// globalData 전체 중 일부만 표시
|
|
||||||
msg = JSON.stringify(globalData).slice(0, 80) + "...";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 알림 표시
|
|
||||||
chrome.notifications.create({
|
|
||||||
type: "basic",
|
|
||||||
iconUrl: "icon.png",
|
|
||||||
title: "지재권 검색 결과",
|
|
||||||
message: `검색 키워드: ${keyword}\n결과: ${msg}`
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3) URL 생성 함수
|
|
||||||
function buildMarkInfoUrl(keyword) {
|
|
||||||
const encoded = encodeURIComponent(keyword);
|
|
||||||
return `https://markinfo.kr/search?page=1&size=20&sort=_score,desc&sort=applicationDate,desc&searchType=ST01&searchKeyword=${encoded}&statuses=APPLICATION&statuses=PUBLICATION&statuses=REGISTRATION`;
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 20 KiB |
|
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
"manifest_version": 3,
|
|
||||||
"name": "지재권 검색 확장 (컨텍스트 메뉴)",
|
|
||||||
"version": "1.0",
|
|
||||||
"description": "드래그한 텍스트를 우클릭 → '지재권 검색'으로 마크인포 검색을 수행합니다.",
|
|
||||||
"permissions": [
|
|
||||||
"contextMenus",
|
|
||||||
"storage",
|
|
||||||
"notifications",
|
|
||||||
"alarms"
|
|
||||||
],
|
|
||||||
"host_permissions": [
|
|
||||||
"https://markinfo.kr/*"
|
|
||||||
],
|
|
||||||
"background": {
|
|
||||||
"service_worker": "background.js"
|
|
||||||
},
|
|
||||||
"action": {
|
|
||||||
"default_icon": "icon.png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,827 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>찜관리</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
padding: 20px;
|
||||||
|
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;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 800;
|
||||||
|
font-family: 'Segoe UI', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-limit {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-form input[type="text"] {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-form input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3498db;
|
||||||
|
border-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #27ae60;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background: #219a52;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: #f39c12;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning:hover {
|
||||||
|
background: #e67e22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-large {
|
||||||
|
padding: 15px 30px;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-list {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-item {
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-item:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-checkboxes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-checkbox-input {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: #667eea;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-checkbox-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-info {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-name {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-nickname {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-url {
|
||||||
|
color: #888;
|
||||||
|
font-size: 12px;
|
||||||
|
word-break: break-all;
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e1e8ed;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zzim-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
background: #e8f4f8;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-status {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-status.visible {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-status.hidden {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.created-date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-actions .btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
min-width: 70px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-actions .btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
color: #3498db;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: #27ae60;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-prefix {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-right: none;
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-input {
|
||||||
|
border-radius: 0 4px 4px 0 !important;
|
||||||
|
border-left: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 모달 스타일 */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: white;
|
||||||
|
margin: 5% auto;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px 25px;
|
||||||
|
border-bottom: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 2px solid #e1e8ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
min-height: 80px;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 20px 25px;
|
||||||
|
border-top: 1px solid #e1e8ed;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-modal {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-modal-cancel {
|
||||||
|
background-color: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-modal-cancel:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-modal-save {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-modal-save:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-modal-save:disabled {
|
||||||
|
background: #cccccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-50px) scale(0.9);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 체크박스 스타일링 */
|
||||||
|
.checkbox-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #e1e8ed;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-container:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-checkmark {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
background-color: white;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox:hover input ~ .checkbox-checkmark {
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox input:checked ~ .checkbox-checkmark {
|
||||||
|
background-color: #667eea;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-checkmark:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox input:checked ~ .checkbox-checkmark:after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox .checkbox-checkmark:after {
|
||||||
|
left: 6px;
|
||||||
|
top: 2px;
|
||||||
|
width: 6px;
|
||||||
|
height: 10px;
|
||||||
|
border: solid white;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="stats-container">
|
||||||
|
<!-- 오늘 활동량 카드 (블루 테마) -->
|
||||||
|
<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">300</span></div>
|
||||||
|
</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">보유중 (품앗이 보상 +10%)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">
|
||||||
|
🏪 내 마켓 등록
|
||||||
|
</div>
|
||||||
|
<div class="market-form">
|
||||||
|
<span class="url-prefix">https://smartstore.naver.com/</span>
|
||||||
|
<input type="text" id="market-url" placeholder="마켓 주소 (예: abcd1234)" class="url-input">
|
||||||
|
<button class="btn btn-primary" id="add-market-btn">등록</button>
|
||||||
|
</div>
|
||||||
|
<div class="market-form">
|
||||||
|
<label>마켓 이름:</label>
|
||||||
|
<input type="text" id="market-name" placeholder="마켓 이름">
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div class="market-form">
|
||||||
|
<label>마켓 별명:</label>
|
||||||
|
<input type="text" id="market-nickname" placeholder="마켓 별명">
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="market-list">
|
||||||
|
<h4>내 마켓 목록</h4>
|
||||||
|
<div id="my-markets-list">
|
||||||
|
<!-- 마켓 목록이 여기에 표시됩니다 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">
|
||||||
|
💝 찜하기 작업
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 백그라운드 실행 옵션 추가 -->
|
||||||
|
<div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 6px;">
|
||||||
|
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
|
||||||
|
<input type="checkbox" id="background-mode" style="transform: scale(1.2);">
|
||||||
|
<span style="font-weight: bold; color: #2c3e50;">🔄 백그라운드 모드</span>
|
||||||
|
</label>
|
||||||
|
<div style="font-size: 12px; color: #666; margin-top: 5px; margin-left: 25px;">
|
||||||
|
체크하면 새 탭에서 백그라운드로 찜하기를 실행합니다.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 찜하기 옵션 설정 -->
|
||||||
|
<div style="margin-bottom: 20px; padding: 20px; background: #f8f9fa; border-radius: 8px; border: 2px solid #e1e8ed;">
|
||||||
|
<h4 style="margin: 0 0 15px 0; color: #2c3e50; font-size: 16px;">⚙️ 찜하기 옵션</h4>
|
||||||
|
|
||||||
|
<!-- 찜하기 속도 설정 -->
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: #2c3e50; font-size: 14px;">
|
||||||
|
⏱️ 찜하기 속도 (찜 간격)
|
||||||
|
</label>
|
||||||
|
<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="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>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; gap: 5px;">
|
||||||
|
<span style="font-size: 12px; color: #666;">+ 추가 간격:</span>
|
||||||
|
<input type="number" id="user-delay" value="0" min="0" max="9000" step="100"
|
||||||
|
style="width: 80px; padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px;">
|
||||||
|
<span style="font-size: 12px; color: #666;">ms</span>
|
||||||
|
</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;">300ms</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 11px; color: #888; margin-top: 5px;">
|
||||||
|
💡 찜과 찜 사이의 대기 시간을 설정합니다. (0.2초~10초)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 최신상품 우선 찜하기 옵션 -->
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
|
||||||
|
<input type="checkbox" id="latest-first" style="transform: scale(1.2);">
|
||||||
|
<span style="font-weight: 600; color: #2c3e50;">🆕 최신상품 우선 찜하기</span>
|
||||||
|
</label>
|
||||||
|
<div style="font-size: 12px; color: #666; margin-top: 5px; margin-left: 25px;">
|
||||||
|
체크하면 전체상품(/category/ALL) 대신 최신상품(/best)부터 찜합니다.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="btn btn-success btn-large" id="my-market-zzim-btn">
|
||||||
|
💝 내 마켓 찜하기
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-warning btn-large" id="mutual-zzim-btn">
|
||||||
|
🤝 품앗이 찜하기
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger btn-large" id="stop-zzim-btn" style="display: none;">
|
||||||
|
⏹️ 찜하기 중단
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 진행률 표시 -->
|
||||||
|
<div id="zzim-progress" style="display: none; margin: 20px 0;">
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
|
||||||
|
<span id="progress-text">진행 중...</span>
|
||||||
|
<span id="progress-percent">0%</span>
|
||||||
|
</div>
|
||||||
|
<div style="width: 100%; height: 8px; background: #e0e0e0; border-radius: 4px; overflow: hidden;">
|
||||||
|
<div id="progress-bar" style="height: 100%; background: #3498db; width: 0%; transition: width 0.3s ease;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="zzim-status" class="loading" style="display: none;"></div>
|
||||||
|
<div id="zzim-error" class="error" style="display: none;"></div>
|
||||||
|
<div id="zzim-success" class="success" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="zzim.js"></script>
|
||||||
|
|
||||||
|
<!-- 마켓 수정 모달 -->
|
||||||
|
<div id="edit-market-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">
|
||||||
|
✏️ 마켓 정보 수정
|
||||||
|
</h3>
|
||||||
|
<button class="modal-close" id="modal-close-btn">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">마켓 URL</label>
|
||||||
|
<input type="text" id="edit-market-url" class="form-input" placeholder="https://smartstore.naver.com/your-store">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">마켓 이름</label>
|
||||||
|
<input type="text" id="edit-market-name" class="form-input" placeholder="마켓 이름을 입력하세요">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">마켓 별명</label>
|
||||||
|
<input type="text" id="edit-market-nickname" class="form-input" placeholder="마켓 별명을 입력하세요">
|
||||||
|
</div>
|
||||||
|
<div class="checkbox-container">
|
||||||
|
<label class="custom-checkbox">
|
||||||
|
<input type="checkbox" id="edit-market-for-zzim">
|
||||||
|
<span class="checkbox-checkmark"></span>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<label class="checkbox-label" for="edit-market-for-zzim">내가 찜하기 할 때 포함</label>
|
||||||
|
<div class="checkbox-description">체크하면 내 마켓 찜하기 작업 시 이 마켓이 포함됩니다.</div>
|
||||||
|
</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>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn-modal btn-modal-cancel" id="modal-cancel-btn">취소</button>
|
||||||
|
<button class="btn-modal btn-modal-save" id="modal-save-btn">저장</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
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 |