링크 공유 저장(links.json) 지원
- Flask에 /api/links 추가: GET은 links.json 로드, PUT은 관리자만 links.json 저장 - 프론트는 /api/links 사용 가능 시 serverMode로 전환하여 추가/편집/삭제/즐겨찾기/가져오기를 서버에 저장 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
91
script.js
91
script.js
@@ -58,6 +58,7 @@
|
||||
sortKey: "json",
|
||||
onlyFav: false,
|
||||
canManage: false,
|
||||
serverMode: false, // true when /api/links is available
|
||||
};
|
||||
|
||||
// Access levels (open/copy)
|
||||
@@ -124,6 +125,7 @@
|
||||
ready: false,
|
||||
mode: "disabled", // enabled | misconfigured | sdk_missing | disabled
|
||||
serverCanManage: null,
|
||||
idTokenRaw: "",
|
||||
};
|
||||
|
||||
function nowIso() {
|
||||
@@ -294,6 +296,10 @@
|
||||
}
|
||||
|
||||
function getMergedLinks() {
|
||||
if (state.serverMode) {
|
||||
// In serverMode, baseLinks is the shared source of truth.
|
||||
return (state.baseLinks || []).map(normalizeLink);
|
||||
}
|
||||
const tomb = new Set(state.store.tombstones || []);
|
||||
const overrides = state.store.overridesById || {};
|
||||
|
||||
@@ -793,6 +799,7 @@
|
||||
|
||||
async function syncUserToServerWithIdToken(idToken) {
|
||||
try {
|
||||
auth.idTokenRaw = String(idToken || "");
|
||||
const cfg = getAuthConfig();
|
||||
const r = await fetch(apiUrl("/api/auth/sync"), {
|
||||
method: "POST",
|
||||
@@ -818,6 +825,47 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function saveLinksToServer(links) {
|
||||
if (!auth.idTokenRaw) return false;
|
||||
try {
|
||||
const cfg = getAuthConfig();
|
||||
const r = await fetch(apiUrl("/api/links"), {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${auth.idTokenRaw}`,
|
||||
"X-Auth0-Issuer": `https://${cfg.auth0.domain}/`,
|
||||
"X-Auth0-ClientId": cfg.auth0.clientId,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(Array.isArray(links) ? links : []),
|
||||
});
|
||||
return r.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLinksFromServer() {
|
||||
try {
|
||||
const r = await fetch(apiUrl("/api/links"), { cache: "no-store" });
|
||||
if (!r.ok) return null;
|
||||
const data = await r.json();
|
||||
const list = data && Array.isArray(data.links) ? data.links : null;
|
||||
if (!list) return null;
|
||||
return list.map(normalizeLink);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function persistLinksIfServerMode() {
|
||||
if (!state.serverMode) return;
|
||||
if (!state.canManage) return;
|
||||
const links = getMergedLinks(); // in serverMode this is baseLinks
|
||||
const ok = await saveLinksToServer(links);
|
||||
if (!ok) toastOnce("savefail", "서버 저장(links.json)에 실패했습니다. 권한/서버 로그를 확인하세요.");
|
||||
}
|
||||
|
||||
function sendLogoutToServer(idToken) {
|
||||
if (!idToken) return;
|
||||
const cfg = getAuthConfig();
|
||||
@@ -939,6 +987,7 @@
|
||||
auth.serverCanManage = null;
|
||||
const t = loadTokens();
|
||||
if (auth.user && t && t.id_token) {
|
||||
auth.idTokenRaw = String(t.id_token || "");
|
||||
const can = await syncUserToServerWithIdToken(t.id_token);
|
||||
if (typeof can === "boolean") {
|
||||
auth.serverCanManage = can;
|
||||
@@ -998,6 +1047,7 @@
|
||||
const claims = await auth.client.getIdTokenClaims();
|
||||
const raw = claims && claims.__raw ? String(claims.__raw) : "";
|
||||
if (raw) {
|
||||
auth.idTokenRaw = raw;
|
||||
const cfg = getAuthConfig();
|
||||
const r = await fetch(apiUrl("/api/auth/sync"), {
|
||||
method: "POST",
|
||||
@@ -1107,6 +1157,14 @@
|
||||
}
|
||||
|
||||
function upsertCustom(link) {
|
||||
if (state.serverMode) {
|
||||
const n = normalizeLink(link);
|
||||
const idx = state.baseLinks.findIndex((c) => c && c.id === n.id);
|
||||
if (idx >= 0) state.baseLinks[idx] = n;
|
||||
else state.baseLinks.push(n);
|
||||
state.baseOrder = new Map(state.baseLinks.map((l, i) => [l.id, i]));
|
||||
return;
|
||||
}
|
||||
const n = normalizeLink(link);
|
||||
const idx = state.store.custom.findIndex((c) => c && c.id === n.id);
|
||||
if (idx >= 0) state.store.custom[idx] = n;
|
||||
@@ -1115,6 +1173,13 @@
|
||||
}
|
||||
|
||||
function deleteLink(id) {
|
||||
if (state.serverMode) {
|
||||
const before = state.baseLinks.length;
|
||||
state.baseLinks = (state.baseLinks || []).filter((l) => l && l.id !== id);
|
||||
state.baseOrder = new Map(state.baseLinks.map((l, i) => [l.id, i]));
|
||||
if (state.baseLinks.length !== before) toast("삭제했습니다.");
|
||||
return;
|
||||
}
|
||||
if (isBaseId(id)) {
|
||||
const s = new Set(state.store.tombstones || []);
|
||||
s.add(id);
|
||||
@@ -1134,6 +1199,11 @@
|
||||
const link = getLinkById(id);
|
||||
if (!link) return;
|
||||
const next = !link.favorite;
|
||||
if (state.serverMode) {
|
||||
upsertCustom({ ...link, favorite: next, updatedAt: nowIso() });
|
||||
render();
|
||||
return;
|
||||
}
|
||||
if (isBaseId(id)) {
|
||||
setOverride(id, { favorite: next, updatedAt: nowIso() });
|
||||
} else {
|
||||
@@ -1231,10 +1301,12 @@
|
||||
}
|
||||
used.add(n.id);
|
||||
// 가져오기는 custom로 추가(기본과 충돌 방지)
|
||||
state.store.custom.push(n);
|
||||
if (state.serverMode) state.baseLinks.push(n);
|
||||
else state.store.custom.push(n);
|
||||
added++;
|
||||
}
|
||||
saveStore();
|
||||
if (!state.serverMode) saveStore();
|
||||
state.baseOrder = new Map(state.baseLinks.map((l, i) => [l.id, i]));
|
||||
toast(`가져오기 완료: ${added}개`);
|
||||
}
|
||||
|
||||
@@ -1259,6 +1331,7 @@
|
||||
}
|
||||
if (act === "fav") {
|
||||
toggleFavorite(id);
|
||||
persistLinksIfServerMode();
|
||||
return;
|
||||
}
|
||||
if (act === "copy") {
|
||||
@@ -1276,6 +1349,7 @@
|
||||
if (confirm(`삭제할까요?\n\n- ${name}`)) {
|
||||
deleteLink(id);
|
||||
render();
|
||||
persistLinksIfServerMode();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1323,6 +1397,7 @@
|
||||
closeModal();
|
||||
render();
|
||||
toast("저장했습니다.");
|
||||
persistLinksIfServerMode();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1340,6 +1415,7 @@
|
||||
closeModal();
|
||||
render();
|
||||
toast("추가했습니다.");
|
||||
persistLinksIfServerMode();
|
||||
}
|
||||
|
||||
function wire() {
|
||||
@@ -1415,7 +1491,16 @@
|
||||
wire();
|
||||
await hydrateAuthConfigFromServerIfNeeded();
|
||||
await initAuth();
|
||||
state.baseLinks = await loadBaseLinks();
|
||||
const serverLinks = await loadLinksFromServer();
|
||||
if (serverLinks) {
|
||||
state.serverMode = true;
|
||||
state.baseLinks = serverLinks;
|
||||
// serverMode에서는 localStorage 기반 커스텀/오버라이드는 사용하지 않음(공유 JSON이 진실)
|
||||
state.store = { overridesById: {}, tombstones: [], custom: [] };
|
||||
saveStore();
|
||||
} else {
|
||||
state.baseLinks = await loadBaseLinks();
|
||||
}
|
||||
state.baseOrder = new Map(state.baseLinks.map((l, i) => [l.id, i]));
|
||||
el.sort.value = state.sortKey;
|
||||
applyManageLock();
|
||||
|
||||
Reference in New Issue
Block a user