Add quick provider login buttons (Auth0 connections), an API to upsert users into Postgres and gate admin via can_manage, plus schema and Node server. Co-authored-by: Cursor <cursoragent@cursor.com>
335 lines
13 KiB
HTML
335 lines
13 KiB
HTML
<!doctype html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<meta name="color-scheme" content="light dark" />
|
||
<title>NCue | 개인 링크 홈</title>
|
||
<meta name="description" content="개인 서비스 링크를 모아 관리하는 홈 화면" />
|
||
<link rel="stylesheet" href="./styles.css" />
|
||
<!-- Auth0 SPA SDK (정적 사이트용) -->
|
||
<!-- jsDelivr 차단/실패 시 unpkg로 자동 대체 -->
|
||
<script
|
||
src="https://cdn.jsdelivr.net/npm/@auth0/auth0-spa-js@2/dist/auth0-spa-js.production.js"
|
||
onerror="this.onerror=null;this.src='https://unpkg.com/@auth0/auth0-spa-js@2/dist/auth0-spa-js.production.js';"
|
||
></script>
|
||
<script>
|
||
// 로그인 설정 (관리 기능 잠금용)
|
||
// 1) Auth0에서 Application(SPA) 생성 후 domain/clientId를 입력하세요.
|
||
// 2) Allowed Callback URLs / Allowed Logout URLs에 현재 사이트 주소를 등록하세요.
|
||
// 예: https://drive.daewoongai.com/apps/dashboard/
|
||
window.AUTH_CONFIG = {
|
||
auth0: {
|
||
domain: "",
|
||
clientId: "",
|
||
},
|
||
// 관리 허용 이메일(대소문자 무시)
|
||
allowedEmails: [],
|
||
};
|
||
</script>
|
||
<script defer src="./script.js"></script>
|
||
</head>
|
||
<body>
|
||
<a class="skip-link" href="#main">본문으로 건너뛰기</a>
|
||
|
||
<header class="topbar">
|
||
<div class="wrap">
|
||
<div class="brand">
|
||
<div class="logo" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none">
|
||
<path
|
||
d="M10.5 13.5l3-3m-1.24-3.13l-2.86-2.86a4 4 0 0 0-5.66 5.66l2.86 2.86"
|
||
stroke="currentColor"
|
||
stroke-width="1.8"
|
||
stroke-linecap="round"
|
||
/>
|
||
<path
|
||
d="M13.5 10.5l2.86 2.86a4 4 0 0 1-5.66 5.66l-2.86-2.86"
|
||
stroke="currentColor"
|
||
stroke-width="1.8"
|
||
stroke-linecap="round"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<div class="brand-text">
|
||
<div class="brand-title">NCue</div>
|
||
<div class="brand-sub" id="subtitle">개인 링크 관리</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="actions">
|
||
<button class="btn" id="btnAdd" type="button">
|
||
<span class="btn-ico" aria-hidden="true">+</span>
|
||
추가
|
||
</button>
|
||
<button class="btn" id="btnImport" type="button">가져오기</button>
|
||
<button class="btn" id="btnExport" type="button">내보내기</button>
|
||
<button class="btn" id="btnTheme" type="button" aria-pressed="false" title="테마 전환">
|
||
테마
|
||
</button>
|
||
<div class="user" id="user" hidden>
|
||
<span class="user-dot" aria-hidden="true"></span>
|
||
<span class="user-text" id="userText">로그인 필요</span>
|
||
</div>
|
||
<button class="btn" id="btnGoogle" type="button" hidden>구글</button>
|
||
<button class="btn" id="btnKakao" type="button" hidden>카카오</button>
|
||
<button class="btn" id="btnNaver" type="button" hidden>네이버</button>
|
||
<button class="btn" id="btnLogin" type="button">로그인</button>
|
||
<button class="btn" id="btnLogout" type="button" hidden>로그아웃</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="wrap" id="main">
|
||
<section class="panel">
|
||
<div class="controls">
|
||
<label class="field">
|
||
<span class="field-label">검색</span>
|
||
<input
|
||
id="q"
|
||
class="input"
|
||
type="search"
|
||
placeholder="제목/도메인/태그 검색…"
|
||
autocomplete="off"
|
||
spellcheck="false"
|
||
/>
|
||
</label>
|
||
|
||
<label class="field">
|
||
<span class="field-label">정렬</span>
|
||
<select id="sort" class="select">
|
||
<option value="json">파일 순서</option>
|
||
<option value="recent">최근 수정</option>
|
||
<option value="name">이름</option>
|
||
<option value="domain">도메인</option>
|
||
<option value="favorite">즐겨찾기 우선</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="check">
|
||
<input id="onlyFav" type="checkbox" />
|
||
<span>즐겨찾기만</span>
|
||
</label>
|
||
|
||
<div class="meta" id="meta" aria-live="polite"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="grid" id="grid" aria-label="링크 목록"></section>
|
||
|
||
<section class="empty" id="empty" hidden>
|
||
<div class="empty-title">표시할 링크가 없습니다.</div>
|
||
<div class="empty-sub">상단의 “추가” 버튼으로 새 링크를 등록하세요.</div>
|
||
</section>
|
||
</main>
|
||
|
||
<!-- Modal -->
|
||
<div class="modal" id="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" hidden>
|
||
<div class="modal-backdrop" data-close="1"></div>
|
||
<div class="modal-card" role="document">
|
||
<div class="modal-head">
|
||
<div class="modal-title" id="modalTitle">링크 추가</div>
|
||
<button class="icon-btn" type="button" id="btnClose" title="닫기" aria-label="닫기">×</button>
|
||
</div>
|
||
|
||
<form id="form" class="modal-body">
|
||
<input type="hidden" id="id" />
|
||
|
||
<label class="field">
|
||
<span class="field-label">제목</span>
|
||
<input id="title" class="input" type="text" required maxlength="80" placeholder="예: Git" />
|
||
</label>
|
||
|
||
<label class="field">
|
||
<span class="field-label">URL</span>
|
||
<input
|
||
id="url"
|
||
class="input"
|
||
type="url"
|
||
required
|
||
placeholder="예: https://example.com"
|
||
inputmode="url"
|
||
autocomplete="off"
|
||
spellcheck="false"
|
||
/>
|
||
<div class="hint">http(s) URL을 권장합니다. (미입력 시 자동으로 https://가 붙습니다)</div>
|
||
</label>
|
||
|
||
<label class="field">
|
||
<span class="field-label">설명</span>
|
||
<input id="description" class="input" type="text" maxlength="140" placeholder="선택" />
|
||
</label>
|
||
|
||
<label class="field">
|
||
<span class="field-label">태그</span>
|
||
<input id="tags" class="input" type="text" placeholder="예: ncue, dev (쉼표로 구분)" />
|
||
</label>
|
||
|
||
<label class="check">
|
||
<input id="favorite" type="checkbox" />
|
||
<span>즐겨찾기</span>
|
||
</label>
|
||
|
||
<div class="modal-foot">
|
||
<button class="btn btn-ghost" type="button" id="btnCancel">취소</button>
|
||
<button class="btn btn-primary" type="submit" id="btnSave">저장</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Auth Config Modal -->
|
||
<div class="modal" id="authModal" role="dialog" aria-modal="true" aria-labelledby="authModalTitle" hidden>
|
||
<div class="modal-backdrop" data-auth-close="1"></div>
|
||
<div class="modal-card" role="document">
|
||
<div class="modal-head">
|
||
<div class="modal-title" id="authModalTitle">로그인 설정</div>
|
||
<button class="icon-btn" type="button" id="btnAuthClose" title="닫기" aria-label="닫기">×</button>
|
||
</div>
|
||
|
||
<form id="authForm" class="modal-body">
|
||
<label class="field">
|
||
<span class="field-label">Auth0 Domain</span>
|
||
<input id="authDomain" class="input" type="text" placeholder="예: your-tenant.us.auth0.com" />
|
||
<div class="hint">Auth0 테넌트 도메인입니다. (비밀값 아님)</div>
|
||
</label>
|
||
|
||
<label class="field">
|
||
<span class="field-label">Auth0 Client ID</span>
|
||
<input id="authClientId" class="input" type="text" placeholder="예: AbCdEf..." />
|
||
<div class="hint">Auth0 SPA Application의 Client ID입니다. (비밀값 아님)</div>
|
||
</label>
|
||
|
||
<label class="field">
|
||
<span class="field-label">허용 이메일</span>
|
||
<input id="authAllowedEmails" class="input" type="text" placeholder="예: me@example.com, admin@example.com" />
|
||
<div class="hint">쉼표로 구분합니다. 비워두면 “로그인한 모든 계정”이 관리 가능해집니다.</div>
|
||
</label>
|
||
|
||
<label class="field">
|
||
<span class="field-label">Connection 이름(선택)</span>
|
||
<input id="authConnGoogle" class="input" type="text" placeholder="Google 예: google-oauth2" />
|
||
<input id="authConnKakao" class="input" type="text" placeholder="Kakao 예: kakao" />
|
||
<input id="authConnNaver" class="input" type="text" placeholder="Naver 예: naver" />
|
||
<div class="hint">Auth0에서 설정한 connection 이름입니다. 비우면 “로그인” 버튼으로 통합 로그인 화면을 띄웁니다.</div>
|
||
</label>
|
||
|
||
<div class="modal-foot">
|
||
<button class="btn btn-ghost" type="button" id="btnAuthReset">초기화</button>
|
||
<button class="btn btn-primary" type="submit" id="btnAuthSave">저장</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hidden file input for import -->
|
||
<input id="file" type="file" accept="application/json" hidden />
|
||
|
||
<!-- Toast -->
|
||
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
|
||
|
||
<!--
|
||
기본 링크 데이터(서버 없이 index.html만 열어도 동작)
|
||
이 배열 순서가 "파일 순서" 정렬 기준이 됩니다.
|
||
-->
|
||
<script id="linksData" type="application/json">
|
||
[
|
||
{
|
||
"id": "dsyoon-ncue-net",
|
||
"title": "DSYoon",
|
||
"url": "https://ncue.net/dsyoon",
|
||
"description": "개인 페이지",
|
||
"tags": ["personal", "ncue"],
|
||
"favorite": false,
|
||
"createdAt": "2026-02-07T00:00:00.000Z",
|
||
"updatedAt": "2026-02-07T00:00:00.000Z"
|
||
},
|
||
{
|
||
"id": "family-ncue-net",
|
||
"title": "Family",
|
||
"url": "https://ncue.net/family",
|
||
"description": "Family",
|
||
"tags": ["personal", "ncue"],
|
||
"favorite": false,
|
||
"createdAt": "2026-02-07T00:00:00.000Z",
|
||
"updatedAt": "2026-02-07T00:00:00.000Z"
|
||
},
|
||
{
|
||
"id": "git-ncue-net",
|
||
"title": "Git",
|
||
"url": "https://git.ncue.net/",
|
||
"description": "NCUE Git 서비스",
|
||
"tags": ["dev", "ncue"],
|
||
"favorite": false,
|
||
"createdAt": "2026-02-07T00:00:00.000Z",
|
||
"updatedAt": "2026-02-07T00:00:00.000Z"
|
||
},
|
||
{
|
||
"id": "mail-ncue-net",
|
||
"title": "Mail",
|
||
"url": "https://mail.ncue.net/",
|
||
"description": "NCUE 메일",
|
||
"tags": ["mail", "ncue"],
|
||
"favorite": false,
|
||
"createdAt": "2026-02-07T00:00:00.000Z",
|
||
"updatedAt": "2026-02-07T00:00:00.000Z"
|
||
},
|
||
{
|
||
"id": "tts-ncue-net",
|
||
"title": "TTS",
|
||
"url": "https://tts.ncue.net/",
|
||
"description": "입력한 text를 mp3로 변환",
|
||
"tags": ["text", "mp3", "ncue"],
|
||
"favorite": false,
|
||
"createdAt": "2026-02-07T00:00:00.000Z",
|
||
"updatedAt": "2026-02-07T00:00:00.000Z"
|
||
},
|
||
{
|
||
"id": "meeting-ncue-net",
|
||
"title": "Meeting",
|
||
"url": "https://meeting.ncue.net/",
|
||
"description": "NCUE 미팅",
|
||
"tags": ["meeting", "ncue"],
|
||
"favorite": false,
|
||
"createdAt": "2026-02-07T00:00:00.000Z",
|
||
"updatedAt": "2026-02-07T00:00:00.000Z"
|
||
},
|
||
{
|
||
"id": "openclaw-ncue-net",
|
||
"title": "OpenClaw",
|
||
"url": "https://openclaw.ncue.net/",
|
||
"description": "OpenClaw",
|
||
"tags": ["tool", "ncue"],
|
||
"favorite": false,
|
||
"createdAt": "2026-02-07T00:00:00.000Z",
|
||
"updatedAt": "2026-02-07T00:00:00.000Z"
|
||
},
|
||
{
|
||
"id": "link-ncue-net",
|
||
"title": "Link",
|
||
"url": "https://link.ncue.net/",
|
||
"description": "NCUE 링크 허브",
|
||
"tags": ["link", "ncue"],
|
||
"favorite": false,
|
||
"createdAt": "2026-02-07T00:00:00.000Z",
|
||
"updatedAt": "2026-02-07T00:00:00.000Z"
|
||
},
|
||
{
|
||
"id": "dreamgirl-ncue-net",
|
||
"title": "DreamGirl",
|
||
"url": "https://ncue.net/dreamgirl",
|
||
"description": "DreamGirl",
|
||
"tags": ["personal", "ncue"],
|
||
"favorite": false,
|
||
"createdAt": "2026-02-07T00:00:00.000Z",
|
||
"updatedAt": "2026-02-07T00:00:00.000Z"
|
||
}
|
||
]
|
||
</script>
|
||
|
||
<noscript>
|
||
<div class="noscript">이 페이지는 JavaScript가 필요합니다.</div>
|
||
</noscript>
|
||
</body>
|
||
</html>
|