Public Source Viewer

비나래아카이브 개발자 포털

실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.

Redacted View
view/hinana/discordSearchArchive.ejs
공개 가능
1 <!DOCTYPE html>
2 <html lang="ko">
3 <head>
4 <meta charset="utf-8">
5 <meta name="color-scheme" content="light dark">
6 <meta name="viewport" content="width=device-width,initial-scale=1">
7 <link rel="manifest" href="/manifest.json">
8 <meta name="theme-color" content="<%= theme === 'dark' ? '#0e1218' : '#f7f9fb' %>">
9 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
10 <title>아리스 검색 아카이브 - 비나래 아카이브</title>
11 <link rel="stylesheet" href="/vendors/bootstrap/css/bootstrap.min.css">
12 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css">
13 <script src="/js/popup.js"></script>
14 <style>
15 :root{--font:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans KR",sans-serif;--bg:#f7f9fb;--paper:#fff;--panel:rgba(255,255,255,.88);--text:#18202b;--muted:#687586;--accent:#c5a059;--accent2:#d45f86;--line:rgba(24,32,43,.12);--header:rgba(247,249,251,.86);--danger:#c43737;--shadow:0 20px 58px rgba(27,39,56,.1)}
16 body.dark-mode{--bg:#0e1218;--paper:#151b24;--panel:rgba(20,27,37,.9);--text:#eef4fb;--muted:#a3afbd;--accent:#d4b47a;--accent2:#ee89aa;--line:rgba(238,244,251,.13);--header:rgba(14,18,24,.88);--danger:#ff7777;--shadow:0 22px 64px rgba(0,0,0,.4)}
17 *{box-sizing:border-box}html,body{margin:0;min-height:100%;font-family:var(--font);background:var(--bg);color:var(--text)}body{padding-top:62px}a{color:inherit;text-decoration:none}button,input,select{font:inherit}
18 .global-header{position:fixed;inset:0 0 auto;z-index:3000;min-height:62px;padding:0 22px;display:flex;align-items:center;justify-content:space-between;gap:18px;background:var(--header);border-bottom:1px solid var(--line);backdrop-filter:blur(18px);box-shadow:0 10px 30px rgba(27,39,56,.07)}
19 .header-logo{height:30px;width:auto;mix-blend-mode:multiply}.dark-mode .header-logo{mix-blend-mode:screen}.header-nav{position:absolute;left:50%;transform:translateX(-50%);display:flex;gap:16px}.nav-link{padding:8px 10px;border-radius:7px;color:var(--muted);font-size:.9rem;font-weight:800}.nav-link:hover,.nav-link.active{color:var(--text);background:color-mix(in srgb,var(--paper) 70%,transparent)}
20 .header-controls{display:flex;align-items:center;gap:8px}.icon-btn{width:36px;height:36px;display:inline-grid;place-items:center;border:1px solid var(--line);border-radius:50%;background:var(--panel);color:var(--muted)}.account-link{padding:8px 11px;border:1px solid var(--line);border-radius:7px;font-size:.8rem;font-weight:800;color:var(--muted)}
21 .shell{width:min(1180px,calc(100% - 36px));margin:0 auto;padding:42px 0 70px}.page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;padding-bottom:22px;border-bottom:1px solid var(--line)}.eyebrow{font-size:.7rem;font-weight:900;color:var(--accent);text-transform:uppercase}.page-head h1{font-size:2rem;margin:5px 0 7px;font-weight:950}.page-head p{margin:0;color:var(--muted);font-size:.9rem}
22 .total-state{display:flex;align-items:center;gap:10px;padding:10px 13px;border:1px solid var(--line);background:var(--panel);border-radius:7px;font-size:.8rem}.total-state i{color:#5865f2;font-size:1.1rem}.total-state strong{display:block;color:var(--text)}
23 .workspace{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:20px;margin-top:22px;align-items:start}.list-area{min-width:0}.side-panel{background:var(--paper);border:1px solid var(--line);border-radius:7px;padding:17px;margin-bottom:12px}.panel-label{display:block;font-size:.69rem;font-weight:900;color:var(--accent);text-transform:uppercase;margin-bottom:8px}.side-panel h2{font-size:.95rem;font-weight:900;margin:0 0 8px}.side-panel p{font-size:.78rem;color:var(--muted);line-height:1.55;margin:0}
24 .filter-bar{display:flex;flex-wrap:wrap;gap:10px;align-items:flex-end;padding:15px;background:var(--paper);border:1px solid var(--line);border-radius:7px;margin-bottom:16px;box-shadow:var(--shadow)}
25 .filter-field{display:flex;flex-direction:column;gap:5px;min-width:0;flex:1 1 160px}.filter-field label{font-size:.69rem;font-weight:900;color:var(--accent);text-transform:uppercase}.filter-field select{width:100%;border:1px solid var(--line);border-radius:5px;background:var(--bg);color:var(--text);padding:9px 11px;font-size:.85rem}.filter-field select:focus{outline:2px solid color-mix(in srgb,var(--accent) 30%,transparent);border-color:var(--accent)}
26 .filter-apply{min-height:40px;border:0;border-radius:5px;padding:0 16px;background:var(--accent);color:#fff;font-weight:900;white-space:nowrap}.filter-reset{min-height:40px;display:inline-flex;align-items:center;padding:0 13px;border:1px solid var(--line);border-radius:5px;color:var(--muted);font-size:.82rem;font-weight:800}.filter-reset:hover{color:var(--text)}
27 .entry{background:var(--paper);border:1px solid var(--line);border-radius:7px;margin-bottom:12px;overflow:hidden}
28 .entry-head{display:flex;flex-wrap:wrap;align-items:center;gap:8px 14px;padding:12px 16px;border-bottom:1px solid var(--line);background:color-mix(in srgb,var(--accent) 4%,transparent);font-size:.78rem}
29 .entry-user{font-weight:900;color:var(--text)}.entry-guild{display:inline-flex;align-items:center;gap:5px;color:#5865f2;font-weight:800}.entry-meta{color:var(--muted);margin-left:auto;display:flex;flex-wrap:wrap;gap:8px 12px;align-items:center}
30 .badge-slot{padding:2px 8px;border:1px solid var(--line);border-radius:99px;font-size:.68rem;font-weight:800;color:var(--muted)}
31 .badge-hidden{padding:2px 8px;border:1px solid color-mix(in srgb,var(--danger) 45%,var(--line));border-radius:99px;font-size:.68rem;font-weight:800;color:var(--danger);background:color-mix(in srgb,var(--danger) 7%,transparent)}
32 .entry.hidden-entry{opacity:.6;border-style:dashed}
33 .entry-actions{display:flex;gap:6px;margin-left:10px}.action-btn{border:1px solid var(--line);background:var(--bg);color:var(--muted);border-radius:5px;padding:4px 10px;font-size:.72rem;font-weight:800;white-space:nowrap}.action-btn:hover{color:var(--text);border-color:var(--accent)}.action-btn.danger:hover{color:var(--danger);border-color:var(--danger)}
34 .entry-body{padding:14px 16px}.qa-label{font-size:.66rem;font-weight:900;color:var(--accent);text-transform:uppercase;margin-bottom:4px}.qa-label.answer{color:var(--accent2)}
35 .qa-text{white-space:pre-wrap;font-size:.86rem;line-height:1.65;word-break:break-word}
36 .qa-question{margin-bottom:12px;padding-bottom:12px;border-bottom:1px dashed var(--line)}
37 .qa-answer .qa-text{max-height:170px;overflow:hidden;position:relative}
38 .qa-answer.open .qa-text{max-height:none}
39 .qa-answer .qa-text.clamped::after{content:'';position:absolute;inset:auto 0 0;height:56px;background:linear-gradient(transparent,var(--paper))}
40 .qa-answer.open .qa-text.clamped::after{display:none}
41 .expand-btn{margin-top:8px;border:1px solid var(--line);background:var(--bg);color:var(--muted);border-radius:5px;padding:5px 12px;font-size:.75rem;font-weight:800}.expand-btn:hover{color:var(--text);border-color:var(--accent)}
42 .source-list{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px;padding-top:12px;border-top:1px dashed var(--line)}.source-link{display:inline-flex;align-items:center;gap:5px;max-width:100%;padding:5px 8px;border:1px solid var(--line);border-radius:5px;color:var(--muted);font-size:.72rem}.source-link span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-link:hover{color:var(--text);border-color:var(--accent)}
43 .usage-detail{display:flex;flex-wrap:wrap;gap:7px 14px;margin-top:12px;color:var(--muted);font-size:.71rem}.usage-detail strong{color:var(--text)}
44 .empty{padding:60px 20px;text-align:center;background:var(--paper);border:1px dashed var(--line);border-radius:7px;color:var(--muted)}.empty i{font-size:2rem;display:block;margin-bottom:12px;color:var(--accent)}
45 .pagination-bar{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:6px;margin-top:22px}
46 .page-link2{min-width:36px;height:36px;display:inline-grid;place-items:center;padding:0 10px;border:1px solid var(--line);border-radius:5px;background:var(--paper);color:var(--muted);font-size:.82rem;font-weight:800}
47 .page-link2:hover{color:var(--text);border-color:var(--accent)}.page-link2.current{background:var(--accent);border-color:var(--accent);color:#fff;pointer-events:none}.page-link2.disabled{opacity:.4;pointer-events:none}
48 .page-info{width:100%;text-align:center;margin-top:8px;color:var(--muted);font-size:.76rem}
49 .side-links{display:grid;gap:1px}.side-links a{display:flex;justify-content:space-between;align-items:center;padding:9px 0;border-bottom:1px solid var(--line);font-size:.8rem;color:var(--muted)}.side-links a:last-child{border:0}.side-links a:hover{color:var(--text)}
50 .stat-row{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--line);font-size:.8rem;color:var(--muted)}.stat-row:last-child{border:0}.stat-row strong{color:var(--text)}
51 .site-footer{width:min(1180px,calc(100% - 36px));margin:0 auto 22px;padding:25px 0 4px;border-top:1px solid var(--line);display:flex;align-items:flex-end;justify-content:space-between;gap:24px;color:var(--muted);font-size:.76rem}.footer-brand{display:flex;align-items:center;gap:13px}.footer-logo{width:90px;mix-blend-mode:multiply}.dark-mode .footer-logo{mix-blend-mode:screen}.footer-title{font-weight:950;color:var(--text);font-size:.9rem}.footer-links{display:flex;justify-content:flex-end;gap:14px;margin-bottom:7px}.footer-links a:hover{color:var(--text)}.footer-copy{text-align:right}.footer-version{margin-right:10px}
52 @media(max-width:850px){body{padding-top:108px}.global-header{min-height:108px;align-items:flex-start;padding:11px 16px}.header-nav{top:60px;width:100%;justify-content:center;border-top:1px solid var(--line);padding-top:7px}.workspace{grid-template-columns:1fr}.sidebar{display:grid;grid-template-columns:1fr 1fr;gap:10px}.side-panel{margin:0}.page-head{align-items:flex-start}.site-footer{align-items:flex-start}}
53 @media(max-width:600px){.shell,.site-footer{width:min(100% - 24px,1180px)}.shell{padding-top:28px}.page-head{display:block}.total-state{margin-top:15px}.page-head h1{font-size:1.55rem}.filter-bar{display:block}.filter-field{margin-bottom:10px}.filter-apply,.filter-reset{width:100%;justify-content:center;margin-top:6px}.entry-meta{margin-left:0}.form-foot,.site-footer{display:block}.sidebar{grid-template-columns:1fr}.footer-links{justify-content:flex-start;margin-top:18px}.footer-copy{text-align:left}.account-link{display:none}}
54 </style>
55 </head>
56 <body class="<%= theme === 'dark' ? 'dark-mode' : '' %>">
57 <header class="global-header">
58 <a href="/hinana/lounge"><img src="/image/<%= theme === 'dark' ? 'lounge1.png' : 'lounge.png' %>" alt="비나래 라운지" class="header-logo"></a>
59 <nav class="header-nav" aria-label="주요 메뉴"><a href="/hinana/index" class="nav-link">Archive</a><a href="/hinana/lounge" class="nav-link active">Lounge</a><a href="/hinana/gallery" class="nav-link">Gallery</a><a href="/hinana/plaza" class="nav-link">Plaza</a></nav>
60 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
61 </header>
62
63 <%
64 // 페이징 링크에 현재 필터를 유지하기 위한 쿼리스트링 생성기
65 function buildQuery(page) {
66 const params = new URLSearchParams();
67 if (selectedGuild) params.set('guild', selectedGuild);
68 if (selectedUser) params.set('user', selectedUser);
69 params.set('pageSize', String(result.pageSize));
70 params.set('page', String(page));
71 return '/hinana/discord-search-archive?' + params.toString();
72 }
73 %>
74
75 <main class="shell">
76 <section class="page-head">
77 <div><div class="eyebrow">Discord AI Search Archive</div><h1>Discord 검색 아카이브</h1><p>Discord /아리스 명령어의 웹 검색 기록과 출처를 서버별·사용자별로 열람합니다.</p></div>
78 <div class="total-state"><i class="bi bi-search"></i><div><strong><%= result.total.toLocaleString() %>건</strong><span>필터 결과 기준</span></div></div>
79 </section>
80
81 <div class="workspace">
82 <section class="list-area">
83 <form class="filter-bar" method="GET" action="/hinana/discord-search-archive">
84 <div class="filter-field">
85 <label for="guildFilter">서버</label>
86 <select id="guildFilter" name="guild">
87 <option value="">전체 서버</option>
88 <% filters.guilds.forEach(function(g){ %>
89 <option value="<%= g.id %>" <%= selectedGuild === g.id ? 'selected' : '' %>><%= g.name %> (<%= g.count %>건)</option>
90 <% }); %>
91 </select>
92 </div>
93 <div class="filter-field">
94 <label for="userFilter">사용자</label>
95 <select id="userFilter" name="user">
96 <option value="">전체 사용자</option>
97 <% filters.users.forEach(function(u){ %>
98 <option value="<%= u.id %>" <%= selectedUser === u.id ? 'selected' : '' %>><%= u.name %> (<%= u.count %>건)</option>
99 <% }); %>
100 </select>
101 </div>
102 <div class="filter-field" style="flex:0 1 130px">
103 <label for="pageSizeFilter">표시 건수</label>
104 <select id="pageSizeFilter" name="pageSize">
105 <% pageSizes.forEach(function(size){ %>
106 <option value="<%= size %>" <%= result.pageSize === size ? 'selected' : '' %>><%= size %>건씩</option>
107 <% }); %>
108 </select>
109 </div>
110 <button class="filter-apply" type="submit"><i class="bi bi-funnel-fill me-1"></i>적용</button>
111 <a class="filter-reset" href="/hinana/discord-search-archive">초기화</a>
112 </form>
113
114 <% if (result.entries.length === 0) { %>
115 <div class="empty"><i class="bi bi-search"></i>조건에 맞는 검색 기록이 없습니다.<br>아리스가 새 검색을 완료하면 이곳에 표시됩니다.</div>
116 <% } else { %>
117 <% result.entries.forEach(function(entry){ %>
118 <article class="entry <%= entry.isHidden ? 'hidden-entry' : '' %>">
119 <div class="entry-head">
120 <span class="entry-user"><i class="bi bi-person-fill me-1"></i><%= entry.discordUsername %></span>
121 <span class="entry-guild"><i class="bi bi-<%= entry.guildId ? 'server' : 'envelope-fill' %>"></i><%= entry.guildName || 'DM' %></span>
122 <span class="badge-slot"><%= entry.personaName || ('슬롯 ' + (entry.personaSlot || 1)) %></span>
123 <% if (entry.isHidden) { %><span class="badge-hidden"><i class="bi bi-eye-slash-fill me-1"></i>숨김</span><% } %>
124 <span class="entry-meta">
125 <span><i class="bi bi-cpu me-1"></i><%= entry.model %></span>
126 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
127 <span><i class="bi bi-clock me-1"></i><%= new Date(entry.createdAt).toLocaleString('ko-KR') %></span>
128 </span>
129 <% if (isAdmin) { %>
130 <span class="entry-actions">
131 <button type="button" class="action-btn hide-btn" data-id="<%= entry.id %>" data-hidden="<%= entry.isHidden ? '1' : '0' %>"><i class="bi bi-eye<%= entry.isHidden ? '' : '-slash' %> me-1"></i><%= entry.isHidden ? '숨김 해제' : '숨김' %></button>
132 <button type="button" class="action-btn danger delete-btn" data-id="<%= entry.id %>"><i class="bi bi-trash me-1"></i>삭제</button>
133 </span>
134 <% } %>
135 </div>
136 <div class="entry-body">
137 <div class="qa-question"><div class="qa-label">Question</div><div class="qa-text"><%= entry.question %></div></div>
138 <div class="qa-answer"><div class="qa-label answer">Answer</div><div class="qa-text"><%= entry.answer %></div><button type="button" class="expand-btn" hidden><i class="bi bi-chevron-down me-1"></i>전체 보기</button></div>
139 <% if (entry.sources && entry.sources.length) { %><div class="source-list" aria-label="검색 출처"><% entry.sources.forEach(function(source, index){ %><a class="source-link" href="<%= source.url %>" target="_blank" rel="noreferrer"><i class="bi bi-box-arrow-up-right"></i><span><%= index + 1 %>. <%= source.title %></span></a><% }); %></div><% } %>
140 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
141 </div>
142 </article>
143 <% }); %>
144
145 <nav class="pagination-bar" aria-label="페이지 이동">
146 <a class="page-link2 <%= result.page <= 1 ? 'disabled' : '' %>" href="<%= buildQuery(1) %>"><i class="bi bi-chevron-double-left"></i></a>
147 <a class="page-link2 <%= result.page <= 1 ? 'disabled' : '' %>" href="<%= buildQuery(result.page - 1) %>"><i class="bi bi-chevron-left"></i></a>
148 <%
149 const windowSize = 5;
150 let startPage = Math.max(1, result.page - Math.floor(windowSize / 2));
151 let endPage = Math.min(result.totalPages, startPage + windowSize - 1);
152 startPage = Math.max(1, endPage - windowSize + 1);
153 for (let p = startPage; p <= endPage; p++) { %>
154 <a class="page-link2 <%= p === result.page ? 'current' : '' %>" href="<%= buildQuery(p) %>"><%= p %></a>
155 <% } %>
156 <a class="page-link2 <%= result.page >= result.totalPages ? 'disabled' : '' %>" href="<%= buildQuery(result.page + 1) %>"><i class="bi bi-chevron-right"></i></a>
157 <a class="page-link2 <%= result.page >= result.totalPages ? 'disabled' : '' %>" href="<%= buildQuery(result.totalPages) %>"><i class="bi bi-chevron-double-right"></i></a>
158 <div class="page-info"><%= result.page %> / <%= result.totalPages %> 페이지 · 총 <%= result.total.toLocaleString() %>건</div>
159 </nav>
160 <% } %>
161 </section>
162
163 <aside class="sidebar">
164 <section class="side-panel">
165 <span class="panel-label">Statistics</span>
166 <h2>아카이브 현황</h2>
167 <div class="stat-row"><span>필터 결과</span><strong><%= result.total.toLocaleString() %>건</strong></div>
168 <div class="stat-row"><span>서버 수</span><strong><%= filters.guilds.length %>개</strong></div>
169 <div class="stat-row"><span>사용자 수</span><strong><%= filters.users.length %>명</strong></div>
170 </section>
171 <% if (isAdmin) { %>
172 <section class="side-panel">
173 <span class="panel-label">Admin</span>
174 <h2><%= username %></h2>
175 <div class="side-links">
176 <a href="/hinana/discord-archive"><span><i class="bi bi-chat-square-text me-1"></i> 일반 대화 아카이브</span><i class="bi bi-chevron-right"></i></a>
177 <a href="/hinana/admin"><span><i class="bi bi-gear me-1"></i> 관리자 페이지</span><i class="bi bi-chevron-right"></i></a>
178 <a href="/hinana/security"><span><i class="bi bi-shield-lock me-1"></i> 보안 대시보드</span><i class="bi bi-chevron-right"></i></a>
179 <a href="/hinana/backups"><span><i class="bi bi-archive me-1"></i> 백업 관리</span><i class="bi bi-chevron-right"></i></a>
180 </div>
181 </section>
182 <% } else { %>
183 <section class="side-panel">
184 <span class="panel-label">Account</span>
185 <h2><%= username || '게스트' %></h2>
186 <div class="side-links">
187 <% if (username) { %>
188 <a href="/hinana/userInfo"><span><i class="bi bi-person me-1"></i> 계정 정보</span><i class="bi bi-chevron-right"></i></a>
189 <% } else { %>
190 <a href="/login?redirect=<%= encodeURIComponent('/hinana/discord-search-archive') %>"><span><i class="bi bi-box-arrow-in-right me-1"></i> 로그인</span><i class="bi bi-chevron-right"></i></a>
191 <% } %>
192 <a href="/hinana/lounge"><span><i class="bi bi-grid me-1"></i> 라운지</span><i class="bi bi-chevron-right"></i></a>
193 <a href="/hinana/persona"><span><i class="bi bi-person-badge me-1"></i> 페르소나 설정</span><i class="bi bi-chevron-right"></i></a>
194 </div>
195 </section>
196 <% } %>
197 <section class="side-panel">
198 <span class="panel-label">Info</span>
199 <h2>기록 안내</h2>
200 <p>/아리스 명령어가 성공적으로 응답한 검색만 저장됩니다. 답변에 사용한 출처와 토큰 상세를 함께 확인할 수 있습니다.</p>
201 </section>
202 </aside>
203 </div>
204 </main>
205
206 <footer class="site-footer"><div class="footer-brand"><img src="/image/sign.png" alt="비나래" class="footer-logo"><div><div class="footer-title">비나래 라운지</div><div>Archive My Lounge</div></div></div><div><div class="footer-links"><a href="/hinana/index">Archive</a><a href="/hinana/lounge">Lounge</a><a href="/hinana/gallery">Gallery</a><a href="/hinana/plaza">Plaza</a><a href="https://x.com/NoctchillHinana" target="_blank" rel="noreferrer">X</a></div><div class="footer-copy"><span class="footer-version">Ver. 8.0.3.2-Utsumi Aoba</span>&copy; 2024~2026. 비나래 | hinana.moe</div></div></footer>
207
208 <script>
209 // 답변이 길어 잘린 경우에만 '전체 보기' 버튼 표시
210 document.querySelectorAll('.qa-answer').forEach(function(box){
211 const text = box.querySelector('.qa-text');
212 const btn = box.querySelector('.expand-btn');
213 if (!text || !btn) return;
214 if (text.scrollHeight > text.clientHeight + 4) {
215 text.classList.add('clamped');
216 btn.hidden = false;
217 btn.addEventListener('click', function(){
218 const open = box.classList.toggle('open');
219 btn.innerHTML = open
220 ? '<i class="bi bi-chevron-up me-1"></i>접기'
221 : '<i class="bi bi-chevron-down me-1"></i>전체 보기';
222 });
223 }
224 });
225 // 관리자 전용: 검색 기록 숨김/삭제
226 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
227 async function adminApi(url, body) {
228 const res = await fetch(url, {
229 method: 'POST',
230 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
231 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
232 });
233 const data = await res.json().catch(function(){ return {}; });
234 if (!res.ok || !data.success) throw new Error(data.message || '요청에 실패했습니다.');
235 return data;
236 }
237 document.querySelectorAll('.hide-btn').forEach(function(btn){
238 btn.addEventListener('click', async function(){
239 const isHidden = btn.dataset.hidden === '1';
240 try {
241 await adminApi('/api/admin/discord-search-archive/hide', { id: btn.dataset.id, hidden: !isHidden });
242 location.reload();
243 } catch (err) { showAlert(err.message); }
244 });
245 });
246 document.querySelectorAll('.delete-btn').forEach(function(btn){
247 btn.addEventListener('click', async function(){
248 if (!confirm('이 검색 기록을 완전히 삭제할까요? 되돌릴 수 없습니다.')) return;
249 try {
250 await adminApi('/api/admin/discord-search-archive/delete', { id: btn.dataset.id });
251 location.reload();
252 } catch (err) { showAlert(err.message); }
253 });
254 });
255 if ('serviceWorker' in navigator) navigator.serviceWorker.register('/sw.js');
256 </script>
257 </body>
258 </html>
259