로그인·네비·F-Scan 로고, favicon, 페이지 타이틀, 인증 메일 브랜딩을 NCue로 통일. Co-authored-by: Cursor <cursoragent@cursor.com>
956 lines
47 KiB
Plaintext
956 lines
47 KiB
Plaintext
<!doctype html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<%- include('partials/favicon') %>
|
|
<title>프롬프트 라이브러리 - NCue</title>
|
|
<link rel="stylesheet" href="/public/styles.css" />
|
|
</head>
|
|
<body>
|
|
<div class="app-shell">
|
|
<%- include('partials/nav', { activeMenu: typeof activeMenu !== 'undefined' ? activeMenu : 'prompts', adminMode: typeof adminMode !== 'undefined' ? adminMode : false, opsUserEmail: typeof opsUserEmail !== 'undefined' ? opsUserEmail : false }) %>
|
|
<div class="content-area">
|
|
<header class="topbar">
|
|
<h1>프롬프트</h1>
|
|
<a class="top-action-link" href="/ai-explore">AI 목록</a>
|
|
</header>
|
|
<main class="container container-ai-full prompt-lib">
|
|
<a href="/ai-explore" class="prompts-back" aria-label="AI 탐색으로 돌아가기">← AI</a>
|
|
|
|
<section class="prompts-hero">
|
|
<h1>프롬프트 라이브러리</h1>
|
|
<p class="prompts-lead">
|
|
공식 템플릿을 복사하거나, 팀이 공유한 프롬프트를 고르고 좋아요로 인기를 표시하세요. 워크플로 도움으로 본인 업무에 맞는 지시문 초안을 만들 수 있습니다.
|
|
</p>
|
|
<% if (typeof openaiPolishAvailable !== "undefined" && !openaiPolishAvailable) { %>
|
|
<p class="prompts-stats prompts-stats--info">「워크플로」의 일부 기능은 서버에 OpenAI 키가 있을 때 사용할 수 있습니다.</p>
|
|
<% } else { %>
|
|
<p class="prompts-stats">공식 <strong><%= (officialPrompts && officialPrompts.length) || 0 %></strong>종 · 팀 공유
|
|
<% if (libData && libData.hasDb) { %><strong><%= (libData.community && libData.community.length) || 0 %></strong>건<% } else { %><span>DB 연결 시 활성</span><% } %>
|
|
</p>
|
|
<% } %>
|
|
</section>
|
|
|
|
<% if (!libData && (!officialPrompts || !officialPrompts.length)) { %>
|
|
<section class="panel">
|
|
<p class="empty">프롬프트 데이터를 불러오지 못했습니다.</p>
|
|
</section>
|
|
<% } else { %>
|
|
<div class="prompt-lib-tabs" role="tablist" aria-label="프롬프트 메뉴">
|
|
<button type="button" class="prompt-lib-tab is-active" data-tab="library" role="tab" aria-selected="true">라이브러리</button>
|
|
<button type="button" class="prompt-lib-tab" data-tab="workflow" role="tab" aria-selected="false">워크플로 → 프롬프트</button>
|
|
<button type="button" class="prompt-lib-tab" data-tab="submit" role="tab" aria-selected="false">공유하기</button>
|
|
<button type="button" class="prompt-lib-tab" data-tab="mine" role="tab" aria-selected="false">내가 올린 것</button>
|
|
</div>
|
|
|
|
<p class="prompt-lib-login-hint" id="promptLibLoginHint" hidden>
|
|
좋아요·공유·삭제는 <strong>회사 이메일(OPS) 로그인</strong> 후 사용할 수 있습니다.
|
|
</p>
|
|
|
|
<section class="panel prompt-lib-panel" data-panel="library">
|
|
<div class="prompt-lib-toolbar">
|
|
<div class="prompt-lib-filters">
|
|
<label class="visually-hidden" for="promptFilterSource">출처</label>
|
|
<select id="promptFilterSource" class="prompt-lib-select">
|
|
<option value="all">전체</option>
|
|
<option value="official">공식 템플릿만</option>
|
|
<option value="community" <%= libData && libData.hasDb ? "" : "disabled" %>>팀 공유만</option>
|
|
</select>
|
|
<label class="visually-hidden" for="promptSort">정렬</label>
|
|
<select id="promptSort" class="prompt-lib-select">
|
|
<option value="popular">인기(좋아요)</option>
|
|
<option value="recent">최신</option>
|
|
</select>
|
|
</div>
|
|
<div class="prompt-lib-search">
|
|
<label class="visually-hidden" for="promptSearchQ">검색</label>
|
|
<input type="search" id="promptSearchQ" class="prompt-lib-search-input" placeholder="제목·설명·태그 검색" autocomplete="off" />
|
|
</div>
|
|
</div>
|
|
<% if (!libData || !libData.hasDb) { %>
|
|
<p class="prompt-lib-db-hint">PostgreSQL이 연결되지 않은 환경입니다. 공식 템플릿과 복사만 사용할 수 있으며, 팀 공유·좋아요는 DB 연결 후 사용할 수 있습니다.</p>
|
|
<% } %>
|
|
|
|
<div class="prompts-layout prompt-lib-main">
|
|
<div class="prompt-lib-list-wrap">
|
|
<h2 class="prompts-grid-title">시나리오 · 공유</h2>
|
|
<div class="prompts-grid prompt-lib-grid" id="promptCardList" role="list"></div>
|
|
<p class="empty" id="promptListEmpty" hidden>조건에 맞는 항목이 없습니다.</p>
|
|
</div>
|
|
<div class="prompts-preview-panel prompt-lib-preview">
|
|
<h2 id="previewTitle">프롬프트 미리보기</h2>
|
|
<p class="prompts-preview-empty" id="previewEmpty">왼쪽에서 항목을 선택하세요.</p>
|
|
<div id="previewActive" hidden>
|
|
<div class="prompts-preview-toolbar prompt-lib-preview-toolbar">
|
|
<button type="button" class="prompt-lib-like" id="promptLikeBtn" title="로그인 후 사용">
|
|
<span class="prompt-lib-like-icon" aria-hidden="true">♥</span>
|
|
<span id="promptLikeCount">0</span>
|
|
</button>
|
|
<button type="button" class="prompts-copy-btn" id="copyPromptBtn" disabled>클립보드에 복사</button>
|
|
</div>
|
|
<p class="prompt-lib-meta" id="previewMeta"></p>
|
|
<div class="prompt-lib-preview-files" id="previewFileWrap" hidden></div>
|
|
<label class="visually-hidden" for="promptBody">선택한 프롬프트 전문</label>
|
|
<textarea id="promptBody" class="prompts-body-textarea" readonly spellcheck="false" aria-describedby="previewHint"></textarea>
|
|
<p class="prompts-hint" id="previewHint">대괄호 [ ] 안은 상황에 맞게 바꾼 뒤 사용하세요.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel prompt-lib-panel prompt-lib-panel--submit" data-panel="submit" hidden>
|
|
<div class="prompt-lib-submit-hero">
|
|
<div class="prompt-lib-submit-hero-icon" aria-hidden="true">✦</div>
|
|
<h2>팀에 프롬프트 공개하기</h2>
|
|
<p>동료들이 참고할 수 있도록 지시문과(선택) <strong>참고·결과 파일</strong>을 함께 올릴 수 있습니다.</p>
|
|
</div>
|
|
<p class="prompt-lib-policy">
|
|
작성자는 로그인 이메일로 저장됩니다. <strong>민감·개인정보·기밀</strong>이 포함되지 않게 해 주세요. 첨부는 항목당 최대 5개, 파일당 20MB입니다.
|
|
</p>
|
|
<form id="promptSubmitForm" class="prompt-lib-form" novalidate enctype="multipart/form-data">
|
|
<div class="prompt-lib-fieldset">
|
|
<h3 class="prompt-lib-h3">기본 정보</h3>
|
|
<div class="prompt-lib-input-row">
|
|
<label for="pcTitle" class="prompt-lib-label">제목 <span class="req">*</span></label>
|
|
<input type="text" id="pcTitle" name="title" class="prompt-lib-input" maxlength="500" required placeholder="예: 주간 CS 리포트 요약" />
|
|
</div>
|
|
<div class="prompt-lib-input-row prompt-lib-input-row--2col">
|
|
<div>
|
|
<label for="pcTag" class="prompt-lib-label">태그</label>
|
|
<input type="text" id="pcTag" name="tag" class="prompt-lib-input" maxlength="100" placeholder="예: CS, 요약" />
|
|
</div>
|
|
<div>
|
|
<label for="pcDesc" class="prompt-lib-label">한줄 설명</label>
|
|
<input type="text" id="pcDesc" name="description" class="prompt-lib-input" maxlength="2000" placeholder="이 프롬프트로 어떤 일을 하는지" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="prompt-lib-fieldset">
|
|
<h3 class="prompt-lib-h3">프롬프트 본문 <span class="req">*</span></h3>
|
|
<p class="prompt-lib-hintline">채팅에 그대로 붙여 넣을 수 있는 <strong>지시문 전체</strong>를 적어 주세요.</p>
|
|
<div class="prompt-lib-body-editor-wrap">
|
|
<div class="prompt-lib-body-toolbar" role="tablist" aria-label="프롬프트 본문 표시">
|
|
<button
|
|
type="button"
|
|
class="prompt-lib-body-mode-btn is-active"
|
|
id="pcBodyModeRaw"
|
|
role="tab"
|
|
aria-selected="true"
|
|
aria-controls="pcBody"
|
|
>
|
|
원문 보기
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="prompt-lib-body-mode-btn"
|
|
id="pcBodyModeMd"
|
|
role="tab"
|
|
aria-selected="false"
|
|
aria-controls="pcBodyMdPreview"
|
|
>
|
|
마크다운 보기
|
|
</button>
|
|
</div>
|
|
<textarea
|
|
id="pcBody"
|
|
name="body"
|
|
class="prompt-lib-textarea prompt-lib-textarea--body"
|
|
rows="14"
|
|
minlength="10"
|
|
required
|
|
placeholder="ChatGPT, Claude, 자사 채팅 등에 사용할 지시문을 작성합니다."
|
|
aria-label="프롬프트 본문"
|
|
></textarea>
|
|
<div
|
|
id="pcBodyMdPreview"
|
|
class="prompt-lib-md-preview"
|
|
hidden
|
|
role="region"
|
|
aria-label="마크다운 미리보기"
|
|
tabindex="0"
|
|
>
|
|
<div class="prompt-lib-md-body" id="pcBodyMdInner"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="prompt-lib-fieldset">
|
|
<h3 class="prompt-lib-h3">첨부 (선택)</h3>
|
|
<div class="prompt-lib-upload-grid">
|
|
<div class="prompt-lib-drop prompt-lib-drop--prompt">
|
|
<span class="prompt-lib-drop-eyebrow">PR</span>
|
|
<strong>프롬프트와 함께 쓰는 자료</strong>
|
|
<p>예: 참고 PDF, 사내 양식, 키워드 목록</p>
|
|
<input type="file" id="pcPromptFiles" name="promptFiles" class="prompt-lib-file-native" multiple />
|
|
<label for="pcPromptFiles" class="prompt-lib-file-btn">파일 선택</label>
|
|
<p class="prompt-lib-file-list" id="pcPromptFileNames" aria-live="polite"></p>
|
|
</div>
|
|
<div class="prompt-lib-drop prompt-lib-drop--result">
|
|
<span class="prompt-lib-drop-eyebrow">OUT</span>
|
|
<strong>결과(샘플) 파일</strong>
|
|
<p>예: AI로 만든 산출물 예시, 기대하는 출력 형식</p>
|
|
<input type="file" id="pcResultFiles" name="resultFiles" class="prompt-lib-file-native" multiple />
|
|
<label for="pcResultFiles" class="prompt-lib-file-btn">파일 선택</label>
|
|
<p class="prompt-lib-file-list" id="pcResultFileNames" aria-live="polite"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="prompt-lib-submit-bar">
|
|
<button type="submit" class="prompt-lib-submit-btn" id="pcSubmit">공개하기</button>
|
|
</div>
|
|
<p class="form-message" id="pcMsg" hidden></p>
|
|
</form>
|
|
</section>
|
|
|
|
<section class="panel prompt-lib-panel prompt-lib-panel--workflow" data-panel="workflow" hidden>
|
|
<div class="prompt-lib-wf-hero">
|
|
<div class="prompt-lib-wf-hero-icon" aria-hidden="true">◇</div>
|
|
<h2>워크플로 → 프롬프트</h2>
|
|
<p>업무를 한눈에 정리한 뒤, <strong>한 덩어리 초안</strong>을 만듭니다. 필요하면 <strong>AI로 다듬기</strong>로 정리한 지시문을 얻을 수 있습니다.</p>
|
|
</div>
|
|
|
|
<aside class="prompt-lib-wf-api-note" role="note">
|
|
<strong>동작 방식</strong>
|
|
<ul>
|
|
<li>
|
|
<span class="prompt-lib-wf-k">초안 합치기</span> — ①~④에 적은 내용을 이 브라우저에서만 이어 붙입니다. <em>인터넷 밖으로 보내지 않습니다</em> (OpenAI/ChatGPT API 호출 없음).<% if (typeof openaiPolishAvailable !== "undefined" && !openaiPolishAvailable) { %>
|
|
<br /><span class="prompt-lib-wf-warn">「AI로 다듬기」는 현재 환경에서 API 키가 없어 사용할 수 없습니다.</span><% } %>
|
|
</li>
|
|
</ul>
|
|
</aside>
|
|
|
|
<div class="prompt-lib-wf-body">
|
|
<div class="prompt-lib-wf-fieldset">
|
|
<h3 class="prompt-lib-wf-h3">①~④단계 입력</h3>
|
|
<div class="prompt-lib-wf-row">
|
|
<label for="wfGoal" class="prompt-lib-wf-label"
|
|
>맡기려는 일 <span class="prompt-lib-wf-badge">1</span> <span class="req">· 한 문장</span></label
|
|
>
|
|
<input type="text" id="wfGoal" class="prompt-lib-wf-input" placeholder="예: 거래처 견적 이메일 초안" autocomplete="off" />
|
|
</div>
|
|
<div class="prompt-lib-wf-row">
|
|
<label for="wfIn" class="prompt-lib-wf-label"
|
|
>AI에게 줄 자료 <span class="prompt-lib-wf-badge">2</span></label
|
|
>
|
|
<textarea id="wfIn" class="prompt-lib-wf-textarea" rows="4" placeholder="붙일 메모, 표, 키워드…" spellcheck="false"></textarea>
|
|
</div>
|
|
<div class="prompt-lib-wf-row">
|
|
<label for="wfOut" class="prompt-lib-wf-label"
|
|
>원하는 출력·톤 <span class="prompt-lib-wf-badge">3</span></label
|
|
>
|
|
<textarea id="wfOut" class="prompt-lib-wf-textarea" rows="3" placeholder="불릿, 표, 격식체, 분량…" spellcheck="false"></textarea>
|
|
</div>
|
|
<div class="prompt-lib-wf-row">
|
|
<label for="wfRule" class="prompt-lib-wf-label"
|
|
>제약·주의 <span class="prompt-lib-wf-badge">4</span> <span class="opt">선택</span></label
|
|
>
|
|
<input type="text" id="wfRule" class="prompt-lib-wf-input" placeholder="개인정보 제외, 한국어, …" autocomplete="off" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="prompt-lib-wf-fieldset prompt-lib-wf-fieldset--actions">
|
|
<h3 class="prompt-lib-wf-h3">가공하기</h3>
|
|
<p class="prompt-lib-wf-lead">먼저 <strong>초안 합치기</strong>로 아래에 텍스트를 만든 뒤, 필요할 때만 <strong>AI로 다듬기</strong>를 누르세요.</p>
|
|
<div class="prompt-lib-wf-actions">
|
|
<div class="prompt-lib-wf-action">
|
|
<button
|
|
type="button"
|
|
class="prompt-lib-wf-btn prompt-lib-wf-btn--merge"
|
|
id="wfMerge"
|
|
title="1~4번에 적은 내용을 브라우저에서만 이어 붙여, 아래 결과 칸에 한 덩어리로 넣습니다. 서버·OpenAI로 전송하지 않습니다."
|
|
aria-describedby="wfHelpMerge"
|
|
>
|
|
초안 합치기
|
|
</button>
|
|
<p id="wfHelpMerge" class="prompt-lib-wf-action-desc">
|
|
로컬에서만 병합 · API 미사용
|
|
<span class="prompt-lib-wf-tip" title="PC에서만: 마우스를 버튼 위에 잠시 올리면 전체 설명이 툴팁으로 보입니다." tabindex="0" role="img" aria-label="툴팁 안내">ⓘ</span>
|
|
</p>
|
|
</div>
|
|
<div class="prompt-lib-wf-action">
|
|
<button
|
|
type="button"
|
|
class="prompt-lib-wf-btn prompt-lib-wf-btn--ai"
|
|
id="wfPolish"
|
|
<% if (typeof openaiPolishAvailable === "undefined" || !openaiPolishAvailable) { %>disabled<% } %>
|
|
>
|
|
AI로 다듬기
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="prompt-lib-wf-fieldset">
|
|
<h3 class="prompt-lib-wf-h3">결과(복사해 공유 탭에 붙여 넣을 수 있음)</h3>
|
|
<textarea
|
|
id="wfResult"
|
|
class="prompt-lib-wf-textarea prompt-lib-wf-textarea--result"
|
|
rows="12"
|
|
placeholder="「초안 합치기」 또는 「AI로 다듬기」 후 여기에 표시됩니다."
|
|
spellcheck="false"
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
<p class="form-message" id="wfMsg" hidden></p>
|
|
</section>
|
|
|
|
<section class="panel prompt-lib-panel" data-panel="mine" hidden>
|
|
<h2>내가 올린 프롬프트</h2>
|
|
<p class="field-hint" style="margin: 0 0 8px" id="mineEmptyHint">로그인 시 목록이 표시됩니다.</p>
|
|
<ul class="prompt-lib-mine-list" id="mineList"></ul>
|
|
</section>
|
|
<% } %>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
<script type="application/json" id="prompt-lib-boot">
|
|
<%- JSON.stringify({
|
|
official: officialPrompts || [],
|
|
lib: libData || { hasDb: false, community: [], likeCountOfficial: {}, likeCountCommunity: {}, myOfficialLikes: [], myCommunityLikes: [], mySubmissions: [] },
|
|
loggedIn: !!userEmail,
|
|
userEmail: userEmail || "",
|
|
polish: !!openaiPolishAvailable
|
|
}).replace(/</g, "\\u003c") %>
|
|
</script>
|
|
<script src="/vendor/marked/marked.umd.js"></script>
|
|
<script src="/vendor/dompurify/purify.min.js"></script>
|
|
<script>
|
|
(function () {
|
|
var boot = document.getElementById("prompt-lib-boot");
|
|
var C = { official: [], lib: { hasDb: false, community: [] }, loggedIn: false, userEmail: "", polish: false };
|
|
try {
|
|
C = JSON.parse(boot ? boot.textContent : "{}");
|
|
} catch (e) {}
|
|
|
|
var cardList = document.getElementById("promptCardList");
|
|
if (!cardList) return;
|
|
|
|
var filterEl = document.getElementById("promptFilterSource");
|
|
var sortEl = document.getElementById("promptSort");
|
|
var searchEl = document.getElementById("promptSearchQ");
|
|
var listEmpty = document.getElementById("promptListEmpty");
|
|
var titleEl = document.getElementById("previewTitle");
|
|
var bodyEl = document.getElementById("promptBody");
|
|
var copyBtn = document.getElementById("copyPromptBtn");
|
|
var emptyEl = document.getElementById("previewEmpty");
|
|
var activeWrap = document.getElementById("previewActive");
|
|
var previewMeta = document.getElementById("previewMeta");
|
|
var likeBtn = document.getElementById("promptLikeBtn");
|
|
var likeCountEl = document.getElementById("promptLikeCount");
|
|
var loginHint = document.getElementById("promptLibLoginHint");
|
|
var previewFileWrap = document.getElementById("previewFileWrap");
|
|
|
|
var byKey = {};
|
|
var currentKey = null;
|
|
var lo = (C.lib && C.lib.likeCountOfficial) || {};
|
|
var lc = (C.lib && C.lib.likeCountCommunity) || {};
|
|
var myO = new Set((C.lib && C.lib.myOfficialLikes) || []);
|
|
var myC = new Set((C.lib && C.lib.myCommunityLikes) || []);
|
|
|
|
function keyOf(src, id) {
|
|
return src + ":" + id;
|
|
}
|
|
|
|
function buildMerged() {
|
|
var list = [];
|
|
(C.official || []).forEach(function (p, i) {
|
|
if (!p || !p.id) return;
|
|
list.push({
|
|
source: "official",
|
|
id: p.id,
|
|
title: p.title,
|
|
description: p.description,
|
|
body: p.body,
|
|
tag: p.tag,
|
|
likeCount: lo[p.id] != null ? lo[p.id] : 0,
|
|
myLike: myO.has(p.id),
|
|
order: i,
|
|
createdT: 0
|
|
});
|
|
});
|
|
(C.lib.community || []).forEach(function (c) {
|
|
list.push({
|
|
source: "community",
|
|
id: c.id,
|
|
title: c.title,
|
|
description: c.description,
|
|
body: c.body,
|
|
tag: c.tag,
|
|
likeCount: c.likeCount != null ? c.likeCount : (lc[c.id] || 0),
|
|
myLike: myC.has(c.id),
|
|
authorLabel: c.authorLabel,
|
|
promptFiles: c.promptFiles || [],
|
|
resultFiles: c.resultFiles || [],
|
|
order: 10000,
|
|
createdT: c.createdAt ? new Date(c.createdAt).getTime() : 0
|
|
});
|
|
});
|
|
return list;
|
|
}
|
|
|
|
function filterAndSort() {
|
|
var all = buildMerged();
|
|
var src = filterEl && filterEl.value;
|
|
var q = (searchEl && searchEl.value ? searchEl.value : "").trim().toLowerCase();
|
|
var sort = sortEl && sortEl.value;
|
|
var out = all.filter(function (p) {
|
|
if (src === "official" && p.source !== "official") return false;
|
|
if (src === "community" && p.source !== "community") return false;
|
|
if (!q) return true;
|
|
var t =
|
|
(p.title + " " + (p.description || "") + " " + (p.tag || "") + " " + (p.source === "community" ? p.authorLabel || "" : "")).toLowerCase();
|
|
return t.indexOf(q) !== -1;
|
|
});
|
|
if (sort === "popular") {
|
|
out.sort(function (a, b) {
|
|
if (b.likeCount !== a.likeCount) return b.likeCount - a.likeCount;
|
|
if (a.source !== b.source) return a.source === "official" ? -1 : 1;
|
|
return String(a.title).localeCompare(String(b.title), "ko");
|
|
});
|
|
} else {
|
|
out.sort(function (a, b) {
|
|
if (a.source !== b.source) return a.source === "community" ? -1 : 1;
|
|
if (a.source === "community" && b.source === "community") return b.createdT - a.createdT;
|
|
return (a.order || 0) - (b.order || 0);
|
|
});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function renderList() {
|
|
var rows = filterAndSort();
|
|
cardList.innerHTML = "";
|
|
if (!rows.length) {
|
|
if (listEmpty) listEmpty.hidden = false;
|
|
return;
|
|
}
|
|
if (listEmpty) listEmpty.hidden = true;
|
|
rows.forEach(function (p) {
|
|
var btn = document.createElement("button");
|
|
btn.type = "button";
|
|
btn.className = "prompt-template-card";
|
|
btn.setAttribute("data-key", keyOf(p.source, p.id));
|
|
btn.setAttribute("role", "listitem");
|
|
var srcLabel = p.source === "official" ? "공식" : "팀";
|
|
var likes = p.likeCount != null ? p.likeCount : 0;
|
|
var authorHtml =
|
|
p.source === "community" && p.authorLabel
|
|
? "<span class=\"prompt-card-author\" title=\"작성\">" + escapeHtml(p.authorLabel) + "</span>"
|
|
: "";
|
|
btn.innerHTML =
|
|
"<h3>" + escapeHtml(p.title) + "</h3><p>" + escapeHtml(p.description || "") + "</p>" +
|
|
"<div class=\"prompt-card-footer\"><span class=\"prompt-mini-tag\">" + escapeHtml(p.tag || "") + "</span>" +
|
|
authorHtml +
|
|
"<span class=\"prompt-card-src\">" + srcLabel + "</span>" +
|
|
"<span class=\"prompt-card-likes\" aria-label=\"좋아요\">♥ " + likes + "</span></div>";
|
|
btn.addEventListener("click", function () {
|
|
selectKey(btn.getAttribute("data-key"));
|
|
});
|
|
cardList.appendChild(btn);
|
|
});
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
if (s == null) return "";
|
|
return String(s)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
function formatFileSize(n) {
|
|
if (n == null || n <= 0) return "";
|
|
if (n < 1024) return n + " B";
|
|
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
|
|
return (n / 1024 / 1024).toFixed(1) + " MB";
|
|
}
|
|
function renderPreviewFiles(p) {
|
|
if (!previewFileWrap) return;
|
|
previewFileWrap.innerHTML = "";
|
|
if (p.source !== "community" || (!(p.promptFiles && p.promptFiles.length) && !(p.resultFiles && p.resultFiles.length))) {
|
|
previewFileWrap.hidden = true;
|
|
return;
|
|
}
|
|
previewFileWrap.hidden = false;
|
|
function addBlock(title, files) {
|
|
if (!files || !files.length) return;
|
|
var d = document.createElement("div");
|
|
d.className = "prompt-lib-preview-file-block";
|
|
var h = document.createElement("h4");
|
|
h.textContent = title;
|
|
d.appendChild(h);
|
|
var ul = document.createElement("ul");
|
|
ul.className = "prompt-lib-file-link-list";
|
|
files.forEach(function (f) {
|
|
if (!f || !f.relativePath) return;
|
|
var li = document.createElement("li");
|
|
var a = document.createElement("a");
|
|
a.href = f.relativePath;
|
|
a.target = "_blank";
|
|
a.rel = "noopener noreferrer";
|
|
a.textContent = f.originalName || "파일";
|
|
var z = f.size != null ? formatFileSize(f.size) : "";
|
|
if (z) a.setAttribute("title", z);
|
|
li.appendChild(a);
|
|
if (z) {
|
|
var sp = document.createElement("span");
|
|
sp.className = "prompt-lib-file-size";
|
|
sp.textContent = z;
|
|
li.appendChild(sp);
|
|
}
|
|
ul.appendChild(li);
|
|
});
|
|
d.appendChild(ul);
|
|
previewFileWrap.appendChild(d);
|
|
}
|
|
addBlock("프롬프트 관련 자료", p.promptFiles);
|
|
addBlock("결과(샘플) 파일", p.resultFiles);
|
|
}
|
|
function selectKey(k) {
|
|
currentKey = k;
|
|
var p = byKey[k];
|
|
if (!p) {
|
|
if (emptyEl) emptyEl.hidden = false;
|
|
if (activeWrap) activeWrap.hidden = true;
|
|
if (copyBtn) copyBtn.disabled = true;
|
|
if (titleEl) titleEl.textContent = "프롬프트 미리보기";
|
|
if (likeBtn) likeBtn.classList.remove("is-liked");
|
|
if (previewFileWrap) {
|
|
previewFileWrap.hidden = true;
|
|
previewFileWrap.innerHTML = "";
|
|
}
|
|
return;
|
|
}
|
|
document.querySelectorAll(".prompt-template-card").forEach(function (el) {
|
|
el.classList.toggle("is-selected", el.getAttribute("data-key") === k);
|
|
el.setAttribute("aria-pressed", el.getAttribute("data-key") === k ? "true" : "false");
|
|
});
|
|
if (emptyEl) emptyEl.hidden = true;
|
|
if (activeWrap) activeWrap.hidden = false;
|
|
if (titleEl) titleEl.textContent = p.title;
|
|
if (bodyEl) bodyEl.value = p.body;
|
|
if (copyBtn) copyBtn.disabled = false;
|
|
if (likeCountEl) likeCountEl.textContent = String(p.likeCount != null ? p.likeCount : 0);
|
|
if (likeBtn) {
|
|
likeBtn.classList.toggle("is-liked", !!p.myLike);
|
|
likeBtn.disabled = !C.loggedIn || !(C.lib && C.lib.hasDb);
|
|
}
|
|
if (previewMeta) {
|
|
var srcLabel =
|
|
p.source === "official" ? "공식 템플릿" : "팀 공유 · " + (p.authorLabel || "팀원");
|
|
previewMeta.textContent = srcLabel;
|
|
}
|
|
renderPreviewFiles(p);
|
|
}
|
|
|
|
function rebuildIndex() {
|
|
byKey = {};
|
|
buildMerged().forEach(function (p) {
|
|
byKey[keyOf(p.source, p.id)] = p;
|
|
});
|
|
}
|
|
|
|
rebuildIndex();
|
|
renderList();
|
|
var firstKey = null;
|
|
if (C.official && C.official[0] && C.official[0].id) {
|
|
firstKey = keyOf("official", C.official[0].id);
|
|
} else if (C.lib && C.lib.community && C.lib.community[0] && C.lib.community[0].id) {
|
|
firstKey = keyOf("community", C.lib.community[0].id);
|
|
}
|
|
if (firstKey && byKey[firstKey]) {
|
|
selectKey(firstKey);
|
|
} else {
|
|
if (emptyEl) emptyEl.hidden = false;
|
|
if (activeWrap) activeWrap.hidden = true;
|
|
}
|
|
var urlParams = new URLSearchParams(window.location.search);
|
|
var deepId = urlParams.get("id");
|
|
if (deepId) {
|
|
var ko = keyOf("official", deepId);
|
|
var kc = keyOf("community", deepId);
|
|
if (byKey[ko]) selectKey(ko);
|
|
else if (byKey[kc]) selectKey(kc);
|
|
}
|
|
|
|
if (filterEl) filterEl.addEventListener("change", renderList);
|
|
if (sortEl) sortEl.addEventListener("change", renderList);
|
|
if (searchEl) searchEl.addEventListener("input", function () { renderList(); if (currentKey && byKey[currentKey]) selectKey(currentKey); });
|
|
|
|
if (likeBtn) {
|
|
likeBtn.addEventListener("click", function () {
|
|
if (!C.loggedIn || !(C.lib && C.lib.hasDb) || !currentKey) return;
|
|
var p = byKey[currentKey];
|
|
if (!p) return;
|
|
fetch("/api/prompts/likes/toggle", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "same-origin",
|
|
body: JSON.stringify({ kind: p.source, id: p.id })
|
|
})
|
|
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); })
|
|
.then(function (o) {
|
|
if (o.ok && o.j && o.j.ok) {
|
|
p.likeCount = o.j.likeCount;
|
|
p.myLike = o.j.liked;
|
|
if (p.source === "official") {
|
|
lo[p.id] = o.j.likeCount;
|
|
if (p.myLike) myO.add(p.id); else myO.delete(p.id);
|
|
} else {
|
|
lc[p.id] = o.j.likeCount;
|
|
if (p.myLike) myC.add(p.id); else myC.delete(p.id);
|
|
}
|
|
if (likeCountEl) likeCountEl.textContent = String(p.likeCount);
|
|
if (likeBtn) likeBtn.classList.toggle("is-liked", p.myLike);
|
|
renderList();
|
|
selectKey(currentKey);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
if (copyBtn) {
|
|
copyBtn.addEventListener("click", function () {
|
|
if (!bodyEl || !bodyEl.value) return;
|
|
navigator.clipboard.writeText(bodyEl.value).then(
|
|
function () {
|
|
var t = copyBtn.textContent;
|
|
copyBtn.textContent = "복사됨";
|
|
setTimeout(function () { copyBtn.textContent = t; }, 1600);
|
|
},
|
|
function () { bodyEl.select(); try { document.execCommand("copy"); } catch (e) {} }
|
|
);
|
|
});
|
|
}
|
|
|
|
if (loginHint) {
|
|
loginHint.hidden = C.loggedIn;
|
|
}
|
|
|
|
// Tabs
|
|
var tabs = document.querySelectorAll(".prompt-lib-tab");
|
|
var panels = document.querySelectorAll(".prompt-lib-panel");
|
|
function showTab(name) {
|
|
tabs.forEach(function (t) {
|
|
var on = t.getAttribute("data-tab") === name;
|
|
t.classList.toggle("is-active", on);
|
|
t.setAttribute("aria-selected", on ? "true" : "false");
|
|
});
|
|
panels.forEach(function (p) {
|
|
p.hidden = p.getAttribute("data-panel") !== name;
|
|
});
|
|
}
|
|
tabs.forEach(function (t) {
|
|
t.addEventListener("click", function () {
|
|
showTab(t.getAttribute("data-tab"));
|
|
if (t.getAttribute("data-tab") === "mine") renderMine();
|
|
});
|
|
});
|
|
showTab("library");
|
|
|
|
function renderMine() {
|
|
var ul = document.getElementById("mineList");
|
|
if (!ul) return;
|
|
ul.innerHTML = "";
|
|
var hint = document.getElementById("mineEmptyHint");
|
|
if (!C.loggedIn) {
|
|
if (hint) { hint.textContent = "로그인 후에만 볼 수 있습니다."; }
|
|
return;
|
|
}
|
|
var mine = (C.lib && C.lib.mySubmissions) || [];
|
|
if (hint) { hint.textContent = mine.length ? "삭제 시 목록·라이브러리에서 사라집니다." : "아직 올린 프롬프트가 없습니다."; }
|
|
mine.forEach(function (m) {
|
|
var li = document.createElement("li");
|
|
li.className = "prompt-lib-mine-item";
|
|
li.innerHTML = "<span class=\"mine-title\">" + escapeHtml(m.title) + "</span>" +
|
|
"<span class=\"mine-date\">" + escapeHtml((m.createdAt || "").slice(0, 10)) + "</span>" +
|
|
"<button type=\"button\" class=\"btn-ghost mine-del\" data-id=\"" + escapeHtml(m.id) + "\">삭제</button>";
|
|
li.querySelector(".mine-del").addEventListener("click", function () {
|
|
if (!confirm("이 공유를 삭제할까요?")) return;
|
|
fetch("/api/prompts/community/" + encodeURIComponent(m.id), { method: "DELETE", credentials: "same-origin" })
|
|
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); })
|
|
.then(function (o) {
|
|
if (o.ok && o.j && o.j.ok) {
|
|
C.lib.mySubmissions = (C.lib.mySubmissions || []).filter(function (x) { return x.id !== m.id; });
|
|
C.lib.community = (C.lib.community || []).filter(function (x) { return x.id !== m.id; });
|
|
rebuildIndex();
|
|
renderList();
|
|
renderMine();
|
|
if (currentKey === keyOf("community", m.id)) {
|
|
if (emptyEl) emptyEl.hidden = false;
|
|
if (activeWrap) activeWrap.hidden = true;
|
|
if (titleEl) titleEl.textContent = "프롬프트 미리보기";
|
|
if (previewFileWrap) {
|
|
previewFileWrap.hidden = true;
|
|
previewFileWrap.innerHTML = "";
|
|
}
|
|
}
|
|
} else {
|
|
alert((o.j && o.j.error) || "삭제에 실패했습니다.");
|
|
}
|
|
});
|
|
});
|
|
ul.appendChild(li);
|
|
});
|
|
}
|
|
if (C.loggedIn) renderMine();
|
|
|
|
// Submit
|
|
var pcForm = document.getElementById("promptSubmitForm");
|
|
if (pcForm) {
|
|
pcForm.addEventListener("submit", function (ev) {
|
|
ev.preventDefault();
|
|
if (!C.loggedIn) {
|
|
alert("로그인이 필요합니다.");
|
|
return;
|
|
}
|
|
if (!(C.lib && C.lib.hasDb)) {
|
|
alert("DB에 연결된 환경에서만 공유할 수 있습니다.");
|
|
return;
|
|
}
|
|
var msg = document.getElementById("pcMsg");
|
|
var body = (document.getElementById("pcBody") && document.getElementById("pcBody").value) || "";
|
|
var title = (document.getElementById("pcTitle") && document.getElementById("pcTitle").value) || "";
|
|
var descVal = (document.getElementById("pcDesc") && document.getElementById("pcDesc").value) || "";
|
|
var tagVal = (document.getElementById("pcTag") && document.getElementById("pcTag").value) || "기타";
|
|
if (body.length < 10) {
|
|
if (msg) { msg.textContent = "본문은 10자 이상 입력해 주세요."; msg.hidden = false; msg.style.color = "#b91c1c"; }
|
|
return;
|
|
}
|
|
if (msg) msg.hidden = true;
|
|
var prIn = document.getElementById("pcPromptFiles");
|
|
var reIn = document.getElementById("pcResultFiles");
|
|
if (prIn && prIn.files && prIn.files.length > 5) {
|
|
if (msg) { msg.textContent = "프롬프트 첨부는 5개 이하로 올려 주세요."; msg.hidden = false; msg.style.color = "#b91c1c"; }
|
|
return;
|
|
}
|
|
if (reIn && reIn.files && reIn.files.length > 5) {
|
|
if (msg) { msg.textContent = "결과(샘플) 첨부는 5개 이하로 올려 주세요."; msg.hidden = false; msg.style.color = "#b91c1c"; }
|
|
return;
|
|
}
|
|
var fd = new FormData();
|
|
fd.append("title", title);
|
|
fd.append("tag", tagVal);
|
|
fd.append("description", descVal);
|
|
fd.append("body", body);
|
|
if (prIn && prIn.files) {
|
|
for (var pi = 0; pi < prIn.files.length; pi++) fd.append("promptFiles", prIn.files[pi]);
|
|
}
|
|
if (reIn && reIn.files) {
|
|
for (var ri = 0; ri < reIn.files.length; ri++) fd.append("resultFiles", reIn.files[ri]);
|
|
}
|
|
var subBtn = document.getElementById("pcSubmit");
|
|
if (subBtn) subBtn.disabled = true;
|
|
fetch("/api/prompts/community", {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
body: fd
|
|
})
|
|
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); })
|
|
.then(function (o) {
|
|
if (subBtn) subBtn.disabled = false;
|
|
if (o.ok && o.j && o.j.ok) {
|
|
if (msg) { msg.textContent = "공개되었습니다. 라이브러리에서 확인하세요."; msg.hidden = false; msg.style.color = "#047857"; }
|
|
pcForm.reset();
|
|
if (typeof window.promptLibResetPcBodyView === "function") window.promptLibResetPcBodyView();
|
|
var pfn = document.getElementById("pcPromptFileNames");
|
|
var rfn = document.getElementById("pcResultFileNames");
|
|
if (pfn) pfn.textContent = "";
|
|
if (rfn) rfn.textContent = "";
|
|
C.lib.hasDb = true;
|
|
C.lib.community = C.lib.community || [];
|
|
C.lib.community.unshift({
|
|
id: o.j.id,
|
|
title: title.trim(),
|
|
description: descVal,
|
|
body: body,
|
|
tag: tagVal || "기타",
|
|
authorLabel: (function (em) {
|
|
if (!em) return "팀원";
|
|
var a = em.indexOf("@");
|
|
if (a < 1) return "팀원";
|
|
var loc = em.slice(0, a).trim();
|
|
return loc || "팀원";
|
|
})(C.userEmail),
|
|
likeCount: 0,
|
|
createdAt: o.j.createdAt || new Date().toISOString(),
|
|
promptFiles: o.j.promptFiles || [],
|
|
resultFiles: o.j.resultFiles || []
|
|
});
|
|
C.lib.mySubmissions = C.lib.mySubmissions || [];
|
|
C.lib.mySubmissions.unshift({ id: o.j.id, title: title.trim(), createdAt: o.j.createdAt || "" });
|
|
rebuildIndex();
|
|
renderList();
|
|
showTab("library");
|
|
selectKey("community:" + o.j.id);
|
|
} else {
|
|
if (msg) { msg.textContent = (o.j && o.j.error) || "저장에 실패했습니다."; msg.hidden = false; msg.style.color = "#b91c1c"; }
|
|
}
|
|
})
|
|
.catch(function () {
|
|
if (subBtn) subBtn.disabled = false;
|
|
if (msg) { msg.textContent = "요청에 실패했습니다."; msg.hidden = false; msg.style.color = "#b91c1c"; }
|
|
});
|
|
});
|
|
}
|
|
function wirePromptFileList(inputId, nameId) {
|
|
var inp = document.getElementById(inputId);
|
|
var out = document.getElementById(nameId);
|
|
if (!inp || !out) return;
|
|
inp.addEventListener("change", function () {
|
|
out.textContent = "";
|
|
if (!inp.files || !inp.files.length) return;
|
|
var names = [];
|
|
for (var i = 0; i < inp.files.length; i++) names.push(inp.files[i].name);
|
|
out.textContent = names.join(" · ");
|
|
});
|
|
}
|
|
wirePromptFileList("pcPromptFiles", "pcPromptFileNames");
|
|
wirePromptFileList("pcResultFiles", "pcResultFileNames");
|
|
|
|
// Workflow
|
|
var wfMerge = document.getElementById("wfMerge");
|
|
var wfPolish = document.getElementById("wfPolish");
|
|
var wfRes = document.getElementById("wfResult");
|
|
var wfMsg = document.getElementById("wfMsg");
|
|
if (wfMerge) {
|
|
wfMerge.addEventListener("click", function () {
|
|
var g = (document.getElementById("wfGoal") && document.getElementById("wfGoal").value) || "";
|
|
var i = (document.getElementById("wfIn") && document.getElementById("wfIn").value) || "";
|
|
var o = (document.getElementById("wfOut") && document.getElementById("wfOut").value) || "";
|
|
var r = (document.getElementById("wfRule") && document.getElementById("wfRule").value) || "";
|
|
var t =
|
|
"다음 조건에 따라 결과를 한국어로 생성해 주세요.\n\n" +
|
|
"【맡기려는 일】\n" + g + "\n\n" +
|
|
"【입력·자료】\n" + i + "\n\n" +
|
|
"【원하는 출력·형식·톤】\n" + o + "\n\n" +
|
|
(r ? "【제약·주의】\n" + r : "");
|
|
if (wfRes) wfRes.value = t;
|
|
if (wfMsg) wfMsg.hidden = true;
|
|
});
|
|
}
|
|
if (wfPolish) {
|
|
wfPolish.addEventListener("click", function () {
|
|
if (!C.loggedIn) {
|
|
alert("로그인이 필요합니다.");
|
|
return;
|
|
}
|
|
if (!C.polish) {
|
|
if (wfMsg) { wfMsg.textContent = "OpenAI API가 설정되지 않았습니다."; wfMsg.hidden = false; wfMsg.style.color = "#b91c1c"; }
|
|
return;
|
|
}
|
|
var draft = (wfRes && wfRes.value) || "";
|
|
if (draft.length < 5) {
|
|
if (wfMsg) { wfMsg.textContent = "먼저 아래에 초안을 넣거나 「초안 합치기」를 누르세요."; wfMsg.hidden = false; wfMsg.style.color = "#b91c1c"; }
|
|
return;
|
|
}
|
|
wfPolish.disabled = true;
|
|
fetch("/api/prompts/polish-workflow", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "same-origin",
|
|
body: JSON.stringify({ draft: draft })
|
|
})
|
|
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); })
|
|
.then(function (o) {
|
|
wfPolish.disabled = false;
|
|
if (o.ok && o.j && o.j.text) {
|
|
if (wfRes) wfRes.value = o.j.text;
|
|
if (wfMsg) { wfMsg.textContent = "다듬기 완료. 필요하면 수동으로 수정하세요."; wfMsg.hidden = false; wfMsg.style.color = "#047857"; }
|
|
} else {
|
|
if (wfMsg) { wfMsg.textContent = (o.j && o.j.error) || "실패"; wfMsg.hidden = false; wfMsg.style.color = "#b91c1c"; }
|
|
}
|
|
})
|
|
.catch(function () {
|
|
wfPolish.disabled = false;
|
|
if (wfMsg) { wfMsg.textContent = "요청에 실패했습니다."; wfMsg.hidden = false; wfMsg.style.color = "#b91c1c"; }
|
|
});
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
<script>
|
|
(function () {
|
|
var pcBody = document.getElementById("pcBody");
|
|
var pcBodyMdPreview = document.getElementById("pcBodyMdPreview");
|
|
var pcBodyMdInner = document.getElementById("pcBodyMdInner");
|
|
var btnRaw = document.getElementById("pcBodyModeRaw");
|
|
var btnMd = document.getElementById("pcBodyModeMd");
|
|
if (!pcBody || !pcBodyMdPreview || !pcBodyMdInner || !btnRaw || !btnMd) return;
|
|
|
|
function renderMd() {
|
|
var text = (pcBody && pcBody.value) || "";
|
|
if (typeof marked === "undefined" || typeof DOMPurify === "undefined") {
|
|
pcBodyMdInner.innerHTML =
|
|
'<p class="prompt-lib-md-fallback">마크다운 뷰어를 불러오지 못했습니다. 원문 보기로 편집하세요.</p>';
|
|
return;
|
|
}
|
|
if (!String(text).trim()) {
|
|
pcBodyMdInner.innerHTML =
|
|
'<p class="prompt-lib-md-empty">입력한 내용이 없습니다. 원문 보기에서 지시문을 입력하세요.</p>';
|
|
return;
|
|
}
|
|
try {
|
|
if (typeof marked.setOptions === "function") {
|
|
marked.setOptions({ breaks: true, gfm: true, async: false });
|
|
}
|
|
var rawHtml = marked.parse(String(text), { async: false });
|
|
pcBodyMdInner.innerHTML = DOMPurify.sanitize(rawHtml, { USE_PROFILES: { html: true } });
|
|
} catch (e) {
|
|
pcBodyMdInner.innerHTML = '<p class="prompt-lib-md-fallback">렌더링에 실패했습니다.</p>';
|
|
}
|
|
}
|
|
|
|
function setMode(isRaw) {
|
|
if (isRaw) {
|
|
btnRaw.classList.add("is-active");
|
|
btnMd.classList.remove("is-active");
|
|
btnRaw.setAttribute("aria-selected", "true");
|
|
btnMd.setAttribute("aria-selected", "false");
|
|
pcBody.removeAttribute("hidden");
|
|
pcBody.removeAttribute("aria-hidden");
|
|
pcBodyMdPreview.setAttribute("hidden", "");
|
|
pcBodyMdPreview.setAttribute("aria-hidden", "true");
|
|
} else {
|
|
renderMd();
|
|
btnRaw.classList.remove("is-active");
|
|
btnMd.classList.add("is-active");
|
|
btnRaw.setAttribute("aria-selected", "false");
|
|
btnMd.setAttribute("aria-selected", "true");
|
|
pcBody.setAttribute("hidden", "");
|
|
pcBody.setAttribute("aria-hidden", "true");
|
|
pcBodyMdPreview.removeAttribute("hidden");
|
|
pcBodyMdPreview.removeAttribute("aria-hidden");
|
|
}
|
|
}
|
|
|
|
btnRaw.addEventListener("click", function () {
|
|
setMode(true);
|
|
});
|
|
btnMd.addEventListener("click", function () {
|
|
setMode(false);
|
|
});
|
|
|
|
window.promptLibResetPcBodyView = function () {
|
|
if (pcBodyMdInner) pcBodyMdInner.innerHTML = "";
|
|
setMode(true);
|
|
};
|
|
})();
|
|
</script>
|
|
<style>
|
|
.visually-hidden {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
padding: 0;
|
|
margin: -1px;
|
|
overflow: hidden;
|
|
clip: rect(0, 0, 0, 0);
|
|
white-space: nowrap;
|
|
border: 0;
|
|
}
|
|
</style>
|
|
</body>
|
|
</html>
|