Compare commits

...

4 Commits
V1.0 ... master

Author SHA1 Message Date
9700X_PC 87da00a872 새 어록 감지 기능 추가 및 모달 스타일 개선, 자동 로그인 기능 구현 2025-06-22 23:58:16 +09:00
9700X_PC 035f045a75 로그인 구현 2025-06-22 12:02:47 +09:00
9700X_PC 05901f257a . 2025-03-05 17:12:58 +09:00
9700X_PC b787faec0c 제작자 추가 2025-03-05 16:52:14 +09:00
13 changed files with 5940 additions and 15 deletions

BIN
SearchTrademark_V1.1.zip Normal file

Binary file not shown.

View File

@ -7,11 +7,102 @@ chrome.runtime.onInstalled.addListener(() => {
contexts: ["selection"] contexts: ["selection"]
}); });
chrome.alarms.create("keepAlive", { periodInMinutes: 4 }); chrome.alarms.create("keepAlive", { periodInMinutes: 4 });
// 새 어록 감지 알람 생성 (1분마다)
chrome.alarms.create("checkNewSayings", { periodInMinutes: 1 });
// 초기 마지막 확인 시간 설정
chrome.storage.local.set({ lastSayingsCheck: Date.now() });
}); });
chrome.alarms.onAlarm.addListener((alarm) => { chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "keepAlive") { if (alarm.name === "keepAlive") {
console.log("[background.js] 서비스 워커 유지 알람 실행됨"); console.log("[background.js] 서비스 워커 유지 알람 실행됨");
} else if (alarm.name === "checkNewSayings") {
checkForNewSayings();
}
});
// 새 어록 확인 함수
async function checkForNewSayings() {
try {
console.log("[background.js] 새 어록 확인 시작");
// 마지막 확인 시간 가져오기
const { lastSayingsCheck } = await chrome.storage.local.get("lastSayingsCheck");
const lastCheckTime = lastSayingsCheck || Date.now() - 60000; // 기본값: 1분 전
// Supabase에서 새 어록 확인
const { access_token } = await chrome.storage.local.get("access_token");
if (!access_token) {
console.log("[background.js] 액세스 토큰이 없어 새 어록 확인을 건너뜁니다.");
return;
}
const response = await fetch('https://kbvpvbabvlzjfgcnfxsg.supabase.co/rest/v1/sayings?select=*,sayings_cat(name),sayings_target(name)&created_at=gte.' + new Date(lastCheckTime).toISOString() + '&order=created_at.desc', {
method: 'GET',
headers: {
'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtidnB2YmFidmx6amZnY25meHNnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzU2NDcxMjEsImV4cCI6MjA1MTIyMzEyMX0.BrPBMGI_zz6-UZpUJGQdJGCFKLEGJBE7CdNLKJgMZNM',
'Authorization': `Bearer ${access_token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const newSayings = await response.json();
if (newSayings && newSayings.length > 0) {
console.log(`[background.js] ${newSayings.length}개의 새 어록을 발견했습니다.`);
// 브라우저 알림 표시
chrome.notifications.create('newSayings', {
type: 'basic',
iconUrl: 'icon.png',
title: '새 어록 알림',
message: `${newSayings.length}개의 새로운 어록이 등록되었습니다.`,
buttons: [
{ title: '확인하기' },
{ title: '나중에' }
]
});
// 새 어록 데이터를 스토리지에 저장
chrome.storage.local.set({
pendingNewSayings: newSayings,
hasNewSayings: true
});
} else {
console.log("[background.js] 새 어록이 없습니다.");
}
// 마지막 확인 시간 업데이트
chrome.storage.local.set({ lastSayingsCheck: Date.now() });
} else {
console.error("[background.js] 새 어록 확인 실패:", response.status, response.statusText);
}
} catch (error) {
console.error("[background.js] 새 어록 확인 중 오류:", error);
}
}
// 알림 클릭 처리
chrome.notifications.onClicked.addListener((notificationId) => {
if (notificationId === 'newSayings') {
// 어록 관리 페이지 열기
chrome.tabs.create({ url: chrome.runtime.getURL('sayings.html') });
chrome.notifications.clear(notificationId);
}
});
// 알림 버튼 클릭 처리
chrome.notifications.onButtonClicked.addListener((notificationId, buttonIndex) => {
if (notificationId === 'newSayings') {
if (buttonIndex === 0) { // 확인하기
chrome.tabs.create({ url: chrome.runtime.getURL('sayings.html') });
}
chrome.notifications.clear(notificationId);
} }
}); });
@ -75,6 +166,38 @@ chrome.contextMenus.onClicked.addListener((info, tab) => {
}); });
}); });
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
const keyword = info.selectionText.trim();
if (!keyword) return;
const { access_token } = await chrome.storage.local.get("access_token");
if (!access_token) {
chrome.notifications.create({
type: "basic",
iconUrl: "icon.png",
title: "로그인 필요",
message: "기능을 사용하려면 먼저 로그인하세요."
});
return;
}
const response = await fetch("https://your-api.com/translate", {
method: "POST",
headers: {
"Authorization": `Bearer ${access_token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ text: keyword })
});
const result = await response.json();
chrome.tabs.sendMessage(tab.id, {
action: "showTooltip",
detailInfo: result,
keyword
});
});
// 키워드 검색 URL (기본 코드 그대로) // 키워드 검색 URL (기본 코드 그대로)
function buildMarkInfoUrl(keyword) { function buildMarkInfoUrl(keyword) {
const encoded = encodeURIComponent(keyword); const encoded = encodeURIComponent(keyword);

244
bannedWords.html Normal file
View File

@ -0,0 +1,244 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>금지어 관리</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: 'Segoe UI', sans-serif;
background: #f4f6f9;
padding: 20px;
}
h2 {
text-align: center;
margin-bottom: 20px;
color: #2c3e50;
}
/* 통계 정보 스타일 */
.stats-info {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
font-size: 14px;
font-weight: bold;
color: #495057;
text-align: center;
}
/* 테이블 스타일 */
.banned-words-table-container {
overflow-x: auto;
margin-top: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#banned-words-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
#banned-words-table th,
#banned-words-table td {
border: 1px solid #ddd;
padding: 12px 8px;
text-align: left;
white-space: nowrap;
}
#banned-words-table th {
background-color: #f8f9fa;
font-weight: bold;
position: sticky;
top: 0;
z-index: 1;
}
#banned-words-table tr:nth-child(even) {
background-color: #f9f9f9;
}
/* 순번 열 스타일 */
#banned-words-table th:first-child,
#banned-words-table td:first-child {
width: 60px;
text-align: center;
}
/* 액션 버튼 스타일 */
.action-btn {
padding: 6px 10px;
margin: 2px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
white-space: nowrap;
display: inline-block;
min-width: 40px;
}
.view-btn {
background: #3498db;
color: white;
}
.edit-btn {
background: #f39c12;
color: white;
}
.delete-btn {
background: #e74c3c;
color: white;
}
.action-btn:hover {
opacity: 0.8;
}
/* 등급 표시 스타일 */
.grade-display {
display: inline-block;
padding: 4px 8px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
color: #495057;
min-width: 60px;
text-align: center;
}
/* 로딩 표시 */
.loading {
text-align: center;
padding: 20px;
font-size: 14px;
color: #3498db;
}
/* 키프리스 모달 스타일 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
position: relative;
background-color: #fefefe;
margin: 15px auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 800px;
border-radius: 8px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #dee2e6;
}
.modal-header h3 {
margin: 0;
}
.close {
font-size: 24px;
font-weight: bold;
cursor: pointer;
color: #666;
}
.close:hover {
color: #000;
}
/* 디버그 정보 */
.debug-info {
margin-top: 20px;
padding: 10px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 12px;
color: #6c757d;
}
</style>
</head>
<body>
<h2>🚫 금지어 관리</h2>
<!-- 디버그 정보 -->
<div class="debug-info" id="debug-info" style="display: block;">
초기화 중...
<br>
<button id="token-check-btn" style="margin-top: 10px; padding: 5px 10px; font-size: 12px;">🔍 토큰 상태 확인</button>
</div>
<!-- 통계 정보 -->
<div id="banned-words-stats" class="stats-info">
<!-- 통계 정보가 여기에 표시됩니다 -->
</div>
<!-- 금지어 테이블 -->
<div class="banned-words-table-container">
<table id="banned-words-table">
<thead>
<tr>
<th>순번</th>
<th>금지어</th>
<th>등급</th>
<th>작업</th>
</tr>
</thead>
<tbody id="banned-words-tbody">
<!-- 동적으로 생성됨 -->
</tbody>
</table>
</div>
<div class="loading" id="banned-words-loading" style="display: none;">
🔄 금지어 목록을 불러오는 중...
</div>
<!-- 키프리스 결과 모달 -->
<div id="kipris-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>🔍 키프리스 검색 결과</h3>
<span class="close" id="close-kipris">&times;</span>
</div>
<div id="kipris-word-title"></div>
<div id="kipris-results">
<!-- 동적으로 생성됨 -->
</div>
<div class="loading" id="kipris-loading" style="display: none;">
🔄 키프리스 결과를 불러오는 중...
</div>
</div>
</div>
<script src="bannedWords.js"></script>
</body>
</html>

1615
bannedWords.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
tooltipEl.style.color = "#333"; tooltipEl.style.color = "#333";
tooltipEl.style.maxWidth = "600px"; tooltipEl.style.maxWidth = "600px";
tooltipEl.style.maxHeight = "500px"; tooltipEl.style.maxHeight = "500px";
// 전체 툴팁 컨테이너에 flex 컬럼 레이아웃 // flex 컬럼 레이아웃으로 구성
tooltipEl.style.display = "flex"; tooltipEl.style.display = "flex";
tooltipEl.style.flexDirection = "column"; tooltipEl.style.flexDirection = "column";
@ -35,9 +35,10 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
headerDiv.style.background = "#fff"; headerDiv.style.background = "#fff";
headerDiv.style.padding = "12px 16px"; headerDiv.style.padding = "12px 16px";
headerDiv.style.borderBottom = "1px solid #ccc"; headerDiv.style.borderBottom = "1px solid #ccc";
headerDiv.style.display = "flex"; // 헤더 내부: 검색 키워드와 제작자 정보를 수직 정렬
headerDiv.style.justifyContent = "space-between"; const headerContent = document.createElement("div");
headerDiv.style.alignItems = "center"; headerContent.style.display = "flex";
headerContent.style.flexDirection = "column";
// 검색 키워드 제목 // 검색 키워드 제목
const titleElem = document.createElement("h2"); const titleElem = document.createElement("h2");
@ -45,9 +46,19 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
titleElem.style.margin = "0"; titleElem.style.margin = "0";
titleElem.style.fontSize = "20px"; titleElem.style.fontSize = "20px";
titleElem.style.color = "#2c3e50"; titleElem.style.color = "#2c3e50";
headerDiv.appendChild(titleElem); headerContent.appendChild(titleElem);
// 내부 닫기 버튼 (헤더 내) // 제작자 정보 (작은 글씨)
const creatorElem = document.createElement("span");
creatorElem.id = "tooltip-creator";
creatorElem.textContent = "내차는언제타냐: 지재권 검색기";
creatorElem.style.fontSize = "12px";
creatorElem.style.color = "#7f8c8d";
headerContent.appendChild(creatorElem);
headerDiv.appendChild(headerContent);
// 내부 닫기 버튼 (헤더 우측)
const headerCloseBtn = document.createElement("button"); const headerCloseBtn = document.createElement("button");
headerCloseBtn.textContent = "닫기"; headerCloseBtn.textContent = "닫기";
headerCloseBtn.style.padding = "6px 10px"; headerCloseBtn.style.padding = "6px 10px";
@ -59,16 +70,16 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
headerCloseBtn.onclick = removeTooltip; headerCloseBtn.onclick = removeTooltip;
headerDiv.appendChild(headerCloseBtn); headerDiv.appendChild(headerCloseBtn);
// 스크롤되는 본문 영역 // 본문 영역 (스크롤 가능)
const bodyDiv = document.createElement("div"); const bodyDiv = document.createElement("div");
bodyDiv.id = "markinfo-tooltip-body"; bodyDiv.id = "markinfo-tooltip-body";
bodyDiv.style.padding = "16px"; bodyDiv.style.padding = "16px";
bodyDiv.style.overflowY = "auto"; bodyDiv.style.overflowY = "auto";
// 본문 영역은 전체 높이에서 헤더 높이를 뺀 만큼 사용
bodyDiv.style.flex = "1 1 auto"; bodyDiv.style.flex = "1 1 auto";
tooltipEl.appendChild(headerDiv); tooltipEl.appendChild(headerDiv);
tooltipEl.appendChild(bodyDiv); tooltipEl.appendChild(bodyDiv);
document.body.appendChild(tooltipEl); document.body.appendChild(tooltipEl);
// 글로벌 닫기 버튼 (항상 보이는 우측 상단) // 글로벌 닫기 버튼 (항상 보이는 우측 상단)
@ -105,7 +116,6 @@ function removeTooltip() {
tooltipEl.parentNode.removeChild(tooltipEl); tooltipEl.parentNode.removeChild(tooltipEl);
} }
tooltipEl = null; tooltipEl = null;
// 글로벌 닫기 버튼 제거
const globalClose = document.getElementById("tooltip-global-close"); const globalClose = document.getElementById("tooltip-global-close");
if (globalClose && globalClose.parentNode) { if (globalClose && globalClose.parentNode) {
globalClose.parentNode.removeChild(globalClose); globalClose.parentNode.removeChild(globalClose);
@ -136,10 +146,11 @@ function renderDetailInfo(results, keyword) {
const bodyDiv = document.getElementById("markinfo-tooltip-body"); const bodyDiv = document.getElementById("markinfo-tooltip-body");
if (!bodyDiv) return; if (!bodyDiv) return;
let html = `<div style="line-height: 1.6;">`; let html = `<div style="line-height: 1.6;">`;
// 결과가 없을 경우 안전한 단어 표시
if (results.error) { if (results.error) {
html += `<p style="color: red; font-weight: bold;">오류: ${results.error}</p>`; html += `<p style="color: red; font-weight: bold;">오류: ${results.error}</p>`;
} else if (!Array.isArray(results) || results.length === 0) { } else if (!Array.isArray(results) || results.length === 0) {
html += `<p style="color: #7f8c8d;">검색 결과가 없습니다.</p>`; html += `<p style="color: #7f8c8d; font-style: italic;">지식재산권이 없는 안전한 단어</p>`;
} else { } else {
results.forEach((result, idx) => { results.forEach((result, idx) => {
html += `<div style="margin-bottom: 24px; padding-bottom: 12px; border-bottom: 1px solid #ecf0f1;">`; html += `<div style="margin-bottom: 24px; padding-bottom: 12px; border-bottom: 1px solid #ecf0f1;">`;

View File

@ -1,16 +1,18 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "지재권 검색 확장 (컨텍스트 메뉴)", "name": "내차는언제타냐 - 지재권 검색 확장 (컨텍스트 메뉴)",
"version": "1.0", "version": "1.2",
"description": "드래그한 텍스트를 우클릭 → '지재권 검색'으로 MarkInfo 검색을 수행하고 결과를 툴팁으로 표시합니다.", "description": "드래그한 텍스트를 우클릭 → '지재권 검색'으로 MarkInfo 검색을 수행하고 결과를 툴팁으로 표시합니다.",
"permissions": [ "permissions": [
"contextMenus", "contextMenus",
"storage", "storage",
"notifications", "notifications",
"alarms" "alarms",
"activeTab"
], ],
"host_permissions": [ "host_permissions": [
"https://markinfo.kr/*" "https://markinfo.kr/*",
"http://146.56.101.199:8000/*"
], ],
"background": { "background": {
"service_worker": "background.js" "service_worker": "background.js"
@ -23,6 +25,13 @@
} }
], ],
"action": { "action": {
"default_popup": "popup.html",
"default_icon": "icon.png" "default_icon": "icon.png"
} },
"web_accessible_resources": [
{
"resources": ["bannedWords.html", "bannedWords.js"],
"matches": ["<all_urls>"]
}
]
} }

465
popup.html Normal file
View File

@ -0,0 +1,465 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>내차는언제타냐 통합확장기</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: 'Segoe UI', sans-serif;
background: #f4f6f9;
padding: 20px;
width: 320px;
height: 100vh;
overflow: hidden;
}
h2 {
text-align: center;
margin-bottom: 20px;
color: #2c3e50;
}
.form-group {
margin-bottom: 12px;
position: relative;
}
input[type="email"], input[type="password"] {
width: 100%;
padding: 10px 40px 10px 10px;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 14px;
}
.form-group input:focus {
border-color: #3498db;
outline: none;
}
.tooltip {
font-size: 11px;
color: #999;
margin-top: 3px;
}
.error {
color: red;
font-size: 12px;
margin-top: 4px;
}
.checkbox-group {
display: flex;
align-items: center;
margin-top: 6px;
font-size: 13px;
}
.checkbox-group input {
margin-right: 6px;
}
.btn {
width: 100%;
padding: 10px;
background: #3498db;
border: none;
color: white;
font-weight: bold;
border-radius: 6px;
cursor: pointer;
margin-top: 10px;
}
.btn:disabled {
background: #bdc3c7;
}
#password-toggle {
position: absolute;
right: 10px;
top: 10px;
cursor: pointer;
color: #555;
user-select: none;
}
.status {
margin-top: 10px;
text-align: center;
font-size: 13px;
}
.loading {
text-align: center;
font-size: 14px;
color: #3498db;
}
.level {
margin-top: 10px;
text-align: center;
font-weight: bold;
color: #27ae60;
}
.debug-info {
margin-top: 10px;
padding: 10px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 12px;
color: #6c757d;
}
.debug-info button {
display: block;
margin-top: 8px;
padding: 6px 12px;
font-size: 11px;
background: #007acc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
width: 100%;
transition: background-color 0.2s;
}
.debug-info button:hover {
background: #005a9e;
}
/* 관리 버튼 스타일 */
.management-buttons {
margin: 15px 0;
}
.management-btn {
background: #27ae60;
margin-bottom: 8px;
}
.management-btn:hover {
background: #219a52;
}
/* 모달 스타일 수정 */
.modal {
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0,0,0,0.5);
display: none;
}
.modal-content {
position: relative;
background-color: #fefefe;
margin: 15px auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 900px;
min-width: 400px;
min-height: 300px;
border-radius: 5px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.modal-header {
cursor: move;
padding: 10px;
margin: -20px -20px 20px -20px;
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
border-radius: 5px 5px 0 0;
user-select: none;
}
.resizer {
width: 10px;
height: 10px;
background: #6c757d;
position: absolute;
right: 0;
bottom: 0;
cursor: se-resize;
border-radius: 0 0 5px 0;
}
.resizer:hover {
background: #5a6268;
}
.close {
font-size: 24px;
font-weight: bold;
cursor: pointer;
color: white;
}
.close:hover {
opacity: 0.7;
}
.modal-body {
padding: 20px;
height: calc(100% - 56px); /* 헤더 높이를 제외한 나머지 */
overflow-y: auto;
}
/* 테이블 컨테이너 높이 조정 */
.banned-words-table-container {
height: calc(100% - 80px); /* 통계 정보 높이를 제외한 나머지 */
overflow: auto;
}
#banned-words-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
font-size: 14px;
}
/* 테이블 헤더 고정 */
#banned-words-table thead {
position: sticky;
top: 0;
background: #f8f9fa;
z-index: 1;
}
/* 통계 정보 스타일 */
.stats-info {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
font-size: 14px;
font-weight: bold;
color: #495057;
text-align: center;
}
/* 테이블 스타일 */
.banned-words-table-container {
overflow-x: auto;
}
#banned-words-table th,
#banned-words-table td {
border: 1px solid #ddd;
padding: 12px 8px;
text-align: left;
white-space: nowrap;
}
#banned-words-table th {
background-color: #f8f9fa;
font-weight: bold;
}
#banned-words-table tr:nth-child(even) {
background-color: #f9f9f9;
}
/* 순번 열 스타일 */
#banned-words-table th:first-child,
#banned-words-table td:first-child {
width: 60px;
text-align: center;
}
/* 액션 버튼 스타일 */
.action-btn {
padding: 6px 10px;
margin: 2px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
white-space: nowrap;
display: inline-block;
min-width: 40px;
}
.view-btn {
background: #3498db;
color: white;
}
.edit-btn {
background: #f39c12;
color: white;
}
.delete-btn {
background: #e74c3c;
color: white;
}
.action-btn:hover {
opacity: 0.8;
}
/* 키프리스 결과 스타일 */
.kipris-results-container {
max-height: 500px;
overflow-y: auto;
}
.kipris-item {
border: 1px solid #ddd;
margin-bottom: 15px;
padding: 15px;
border-radius: 6px;
background: #f9f9f9;
}
.kipris-item h4 {
margin: 0 0 10px 0;
color: #2c3e50;
}
.kipris-field {
margin-bottom: 8px;
}
.kipris-field strong {
color: #34495e;
}
.kipris-drawing {
max-width: 100%;
height: auto;
margin-top: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
/* 등급 표시 스타일 */
.grade-display {
display: inline-block;
padding: 4px 8px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
color: #495057;
min-width: 60px;
text-align: center;
}
</style>
</head>
<body>
<h2>내차는언제타냐 통합확장 로그인</h2>
<!-- 디버그 정보 -->
<div class="debug-info" id="debug-info">
초기화 중...
</div>
<!-- 로그인 화면 -->
<div id="login-section">
<div class="form-group">
<input type="email" id="email" placeholder="이메일 주소" />
<div class="tooltip">가입한 이메일 주소를 입력하세요</div>
<div class="error" id="email-error"></div>
</div>
<div class="form-group">
<input type="password" id="password" placeholder="비밀번호" />
<span id="password-toggle">👁️</span>
<div class="tooltip">영문, 숫자 포함 6자 이상 입력</div>
<div class="error" id="password-error"></div>
</div>
<div class="checkbox-group">
<input type="checkbox" id="save-login" />
<label for="save-login">로그인 정보 저장</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="auto-login" />
<label for="auto-login">자동 로그인</label>
</div>
<button class="btn" id="login-btn">로그인</button>
<div class="loading" id="loading" style="display: none;">🔄 로그인 중입니다...</div>
<div class="status" id="status"></div>
<div class="level" id="membership-level"></div>
</div>
<!-- 로그인 후 사용자 정보 출력 화면 -->
<div id="user-info-section" style="display: none;">
<h3>👋 내차는언제타냐 통합확장기</h3>
<p><strong>이메일:</strong> <span id="user-email"></span></p>
<p><strong>회원등급:</strong> <span id="user-level"></span></p>
<p><strong>오늘 호출량:</strong> <span id="user-usage"></span></p>
<p><strong>등급 만료일:</strong> <span id="user-expire"></span></p>
<!-- 관리 버튼들 -->
<div class="management-buttons">
<button class="btn management-btn" id="banned-words-btn">🚫 금지어 관리</button>
<button class="btn management-btn" id="sayings-btn">💬 어록보기</button>
</div>
<button class="btn" id="logout-btn">로그아웃</button>
</div>
<!-- 스크립트를 일반 스크립트로 변경 -->
<script src="popup.js"></script>
<!-- 금지어 관리 모달 -->
<div id="banned-words-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>🚫 금지어 관리</h3>
<span class="close" id="close-banned-words">&times;</span>
</div>
<div class="modal-body">
<div id="banned-words-stats" class="stats-info">
<!-- 통계 정보가 여기에 표시됩니다 -->
</div>
<div class="banned-words-table-container">
<table id="banned-words-table">
<thead>
<tr>
<th>순번</th>
<th>금지어</th>
<th>등급</th>
<th>작업</th>
</tr>
</thead>
<tbody id="banned-words-tbody">
<!-- 동적으로 생성됨 -->
</tbody>
</table>
</div>
<div class="loading" id="banned-words-loading" style="display: none;">
🔄 금지어 목록을 불러오는 중...
</div>
</div>
</div>
</div>
<!-- 키프리스 결과 모달 -->
<div id="kipris-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>🔍 키프리스 검색 결과</h3>
<span class="close" id="close-kipris">&times;</span>
</div>
<div class="modal-body">
<div id="kipris-word-title"></div>
<div class="kipris-results-container">
<div id="kipris-results">
<!-- 동적으로 생성됨 -->
</div>
</div>
<div class="loading" id="kipris-loading" style="display: none;">
🔄 키프리스 결과를 불러오는 중...
</div>
</div>
</div>
</div>
</body>
</html>

1071
popup.js Normal file

File diff suppressed because it is too large Load Diff

688
sayings.html Normal file
View File

@ -0,0 +1,688 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>어록 관리</title>
<style>
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background-color: #f8f9fa;
line-height: 1.6;
}
h2 {
text-align: center;
margin-bottom: 20px;
color: #2c3e50;
}
/* 필터 섹션 스타일 */
.filter-section {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.filter-row {
display: flex;
gap: 15px;
align-items: center;
margin-bottom: 15px;
flex-wrap: wrap;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.filter-group label {
font-size: 14px;
font-weight: bold;
color: #555;
}
.filter-group select,
.filter-group input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.filter-buttons {
display: flex;
gap: 10px;
align-items: end;
}
.filter-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
}
.filter-btn.primary {
background: #3498db;
color: white;
}
.filter-btn.secondary {
background: #95a5a6;
color: white;
}
.filter-btn:hover {
opacity: 0.8;
}
/* 통계 정보 스타일 */
.stats-info {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
font-size: 14px;
font-weight: bold;
color: #495057;
display: flex;
justify-content: space-between;
align-items: center;
}
/* 어록 추가 버튼 스타일 */
.add-saying-btn {
padding: 10px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.2s ease;
}
.add-saying-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
}
.add-saying-btn:active {
transform: translateY(0);
}
/* 어록 카드 컨테이너 */
.sayings-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
margin-top: 20px;
}
/* 어록 카드 스타일 */
.saying-card {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border-left: 4px solid #007bff;
transition: all 0.3s ease;
}
.saying-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
.saying-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.saying-title {
font-size: 18px;
font-weight: 600;
color: #2c3e50;
margin: 0;
flex: 1;
}
.saying-meta {
display: flex;
gap: 8px;
flex-shrink: 0;
margin-left: 12px;
}
.saying-category, .saying-target {
background: #f8f9fa;
color: #495057;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.saying-category {
background: #e3f2fd;
color: #1976d2;
}
.saying-target {
background: #f3e5f5;
color: #7b1fa2;
}
.saying-content {
margin: 12px 0;
line-height: 1.6;
color: #2c3e50;
}
.saying-content h1, .saying-content h2, .saying-content h3 {
margin-top: 16px;
margin-bottom: 8px;
}
.saying-content p {
margin-bottom: 8px;
}
.saying-content blockquote {
border-left: 4px solid #007bff;
padding-left: 16px;
margin: 12px 0;
color: #6c757d;
font-style: italic;
}
.saying-content code {
background: #f8f9fa;
padding: 2px 4px;
border-radius: 4px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.9em;
}
.saying-content pre {
background: #f8f9fa;
padding: 12px;
border-radius: 8px;
overflow-x: auto;
margin: 12px 0;
}
.saying-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e9ecef;
font-size: 14px;
color: #6c757d;
}
.saying-author {
font-weight: 500;
}
.saying-date {
color: #adb5bd;
}
/* 로딩 표시 */
.loading {
text-align: center;
padding: 40px;
font-size: 16px;
color: #3498db;
}
/* 빈 상태 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #666;
}
.empty-state .icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-state .title {
font-size: 18px;
margin-bottom: 8px;
}
.empty-state .description {
font-size: 14px;
color: #999;
}
/* 모달 스타일 */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
min-width: 500px;
max-width: 700px;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.modal-title {
font-size: 20px;
font-weight: bold;
color: #2c3e50;
margin: 0;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-close:hover {
color: #333;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #555;
}
.form-input {
width: 100%;
padding: 10px 12px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.form-textarea {
width: 100%;
padding: 10px 12px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
min-height: 120px;
resize: vertical;
font-family: inherit;
}
.form-select {
width: 100%;
padding: 10px 12px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 14px;
background: white;
}
.form-help {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.markdown-preview {
border: 2px solid #ddd;
border-radius: 4px;
padding: 10px 12px;
min-height: 120px;
background: #f9f9f9;
margin-top: 10px;
}
.preview-tabs {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.preview-tab {
padding: 6px 12px;
border: 1px solid #ddd;
background: #f8f9fa;
cursor: pointer;
border-radius: 4px;
font-size: 12px;
}
.preview-tab.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.modal-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: background 0.2s;
}
.btn-primary {
background: #28a745;
color: white;
}
.btn-primary:hover {
background: #218838;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
/* 디버그 정보 */
.debug-info {
margin-top: 20px;
padding: 10px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 12px;
color: #6c757d;
}
.debug-info button {
margin-top: 10px;
padding: 5px 10px;
font-size: 12px;
background: #007acc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.debug-info button:hover {
background: #005a9e;
}
/* 반응형 디자인 */
@media (max-width: 768px) {
.sayings-container {
grid-template-columns: 1fr;
}
.filter-row {
flex-direction: column;
align-items: stretch;
}
.filter-buttons {
justify-content: center;
}
.stats-info {
flex-direction: column;
gap: 15px;
text-align: center;
}
.modal-content {
min-width: 90%;
margin: 20px;
}
.saying-meta {
justify-content: center;
}
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header h1 {
margin: 0;
color: #2c3e50;
font-size: 24px;
}
.header-right {
display: flex;
align-items: center;
gap: 15px;
}
.current-user {
padding: 8px 12px;
background: #e3f2fd;
border: 1px solid #2196f3;
border-radius: 20px;
font-size: 14px;
color: #1976d2;
font-weight: 500;
}
</style>
</head>
<body>
<div class="header">
<h1>📚 어록 관리</h1>
<div class="header-right">
<div id="current-user" class="current-user">👤 로딩 중...</div>
<button id="add-saying-btn" class="add-saying-btn">✨ 어록 등록</button>
</div>
</div>
<!-- 디버그 정보 -->
<div class="debug-info" id="debug-info" style="display: block;">
초기화 중...
<br>
<button id="log-check-btn" style="margin-top: 10px; padding: 5px 10px; font-size: 12px;">📋 로그 확인</button>
</div>
<!-- 필터 섹션 -->
<div class="filter-section">
<div class="filter-row">
<div class="filter-group">
<label for="category-filter">카테고리</label>
<select id="category-filter" class="filter-select">
<option value="all">모든 카테고리</option>
<!-- 동적으로 추가됨 -->
</select>
</div>
<div class="filter-group">
<label for="target-filter">타겟</label>
<select id="target-filter" class="filter-select">
<option value="all">모든 타겟</option>
<!-- 동적으로 추가됨 -->
</select>
</div>
<div class="filter-group">
<label for="date-filter">기간</label>
<select id="date-filter" class="filter-select">
<option value="all">전체 기간</option>
<option value="today">오늘</option>
<option value="week">최근 1주일</option>
<option value="month">최근 1개월</option>
</select>
</div>
<div class="filter-group">
<label for="search-input">검색</label>
<input type="text" id="search-input" placeholder="어록 검색..." class="search-input">
</div>
<div class="filter-buttons">
<button class="filter-btn primary" id="apply-filter">🔍 필터 적용</button>
<button class="filter-btn secondary" id="reset-filters">🔄 초기화</button>
</div>
</div>
</div>
<!-- 통계 정보 -->
<div id="sayings-stats" class="stats-info">
<div>
<!-- 통계 정보가 여기에 표시됩니다 -->
</div>
<button class="add-saying-btn" id="add-saying-btn">
어록 등록
</button>
</div>
<!-- 어록 등록 모달 -->
<div id="add-saying-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title"> 새 어록 등록</h3>
<button class="modal-close" id="modal-close">&times;</button>
</div>
<form id="saying-form">
<div class="form-group">
<label class="form-label" for="saying-title">어록 제목</label>
<input type="text" id="saying-title" class="form-input" placeholder="어록의 제목을 입력하세요" required>
<div class="form-help">어록을 대표하는 제목을 입력해주세요.</div>
</div>
<div class="form-group">
<label class="form-label" for="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>
<textarea id="saying-content" class="form-textarea" placeholder="어록 내용을 입력하세요 (마크다운 지원)" required></textarea>
<div id="markdown-preview" class="markdown-preview" style="display: none;"></div>
<div class="form-help">
💡 마크다운 문법을 사용할 수 있습니다.
<strong>**굵게**</strong>, <em>*기울임*</em>,
<code>`코드`</code>, 링크 등을 지원합니다.
</div>
</div>
<div class="form-group">
<label class="form-label" for="saying-category">카테고리</label>
<select id="saying-category" class="form-select" required>
<option value="">카테고리를 선택하세요</option>
<!-- 동적으로 로드됩니다 -->
</select>
<div class="form-help">어록의 성격에 맞는 카테고리를 선택해주세요.</div>
</div>
<div class="form-group">
<label class="form-label" for="saying-target">타겟</label>
<select id="saying-target" class="form-select" required>
<option value="">타겟을 선택하세요</option>
<!-- 동적으로 로드됩니다 -->
</select>
<div class="form-help">어록의 대상을 선택해주세요. (기본값: 누구나)</div>
</div>
<div class="form-group">
<label class="form-label">등록자</label>
<div id="modal-current-user" style="padding: 10px; background: #f8f9fa; border-radius: 4px; color: #666;">
로그인된 사용자 정보를 가져오는 중...
</div>
<div class="form-help">현재 로그인한 사용자 이름이 자동으로 등록됩니다.</div>
</div>
</form>
<div class="modal-buttons">
<button type="button" class="btn btn-secondary" id="cancel-btn">취소</button>
<button type="submit" class="btn btn-primary" id="submit-btn">📝 등록하기</button>
</div>
</div>
</div>
<!-- 어록 컨테이너 -->
<div id="sayings-container" class="sayings-container">
<!-- 동적으로 생성됨 -->
</div>
<div class="loading" id="sayings-loading" style="display: none;">
🔄 어록을 불러오는 중...
</div>
<!-- 마크다운 라이브러리를 직접 로드 -->
<script src="https://cdn.jsdelivr.net/npm/marked@9.1.6/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js"></script>
<script src="sayings.js"></script>
</body>
</html>

1699
sayings.js Normal file

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB