- Introduced common.js for shared functions including API calls, error reporting, and UI helpers. - Implemented feedback.js for the feedback reporting page, including form handling and displaying feedback status. - Added layout.js for managing galaxy and section layouts in the visualization. - Created login.js for user authentication with session management. - Developed search.js for term searching based on user input and synonyms. - Implemented signup.js for user registration with validation and immediate login.
155 lines
5.8 KiB
JavaScript
155 lines
5.8 KiB
JavaScript
// 지도 배치와 그래프 계산 (DOM 없음). 좌표는 '세계 좌표'이며 화면에서 확대·이동을 적용한다.
|
|
|
|
const GOLDEN = Math.PI * (3 - Math.sqrt(5));
|
|
|
|
// 전체 우주: 은하 9개의 고정 배치 (지역사회 간호과정을 가운데에)
|
|
const UNIVERSE_SLOTS = {
|
|
g1: [-0.36, -0.30], g2: [0, -0.36], g4: [0.36, -0.30],
|
|
g5: [-0.40, 0.02], g3: [0, 0], g6: [0.40, 0.02],
|
|
g7: [-0.34, 0.33], g8: [0, 0.38], g9: [0.34, 0.33],
|
|
};
|
|
|
|
export function universeLayout(galaxies) {
|
|
const W = 1000, H = 760;
|
|
const pos = {};
|
|
galaxies.forEach((g, i) => {
|
|
const slot = UNIVERSE_SLOTS[g.galaxy_id] || [Math.cos(i) * 0.35, Math.sin(i) * 0.3];
|
|
pos[g.galaxy_id] = { x: slot[0] * W, y: slot[1] * H };
|
|
});
|
|
return { pos, bounds: { minX: -W / 2, maxX: W / 2, minY: -H / 2, maxY: H / 2 } };
|
|
}
|
|
|
|
/** 은하 안: 절(별자리)을 원형으로 */
|
|
export function galaxyLayout(sections, countOf) {
|
|
const n = sections.length;
|
|
const sizes = sections.map((s) => 128 + Math.sqrt(countOf(s.id)) * 7); // 지름
|
|
const gap = 34;
|
|
const circumference = sizes.reduce((a, b) => a + b + gap, 0);
|
|
const R = Math.max(240, circumference / (2 * Math.PI) / 0.9);
|
|
const pos = {};
|
|
let angle = -Math.PI / 2;
|
|
sections.forEach((s, i) => {
|
|
const share = ((sizes[i] + gap) / circumference) * Math.PI * 2;
|
|
const a = angle + share / 2;
|
|
pos[s.id] = { x: Math.cos(a) * R, y: Math.sin(a) * R * 0.86, r: sizes[i] / 2 };
|
|
angle += share;
|
|
});
|
|
const pad = Math.max(...sizes, 60) / 2 + 30;
|
|
return { pos, R, bounds: { minX: -R - pad, maxX: R + pad, minY: -R * 0.86 - pad, maxY: R * 0.86 + pad } };
|
|
}
|
|
|
|
/** 절 안: 항목별 작은 묶음 + 묶음 안 해바라기 배치 */
|
|
export function sectionLayout(groups) {
|
|
// groups: [{id, title, terms:[term]}]
|
|
const SPACING = 40;
|
|
const clusters = groups.map((g) => ({ ...g, r: Math.sqrt(g.terms.length) * SPACING * 0.62 + 34 }));
|
|
const pos = {};
|
|
const centers = {};
|
|
if (clusters.length === 1) {
|
|
centers[clusters[0].id] = { x: 0, y: 0, r: clusters[0].r };
|
|
} else {
|
|
const circumference = clusters.reduce((a, c) => a + c.r * 2 + 50, 0);
|
|
const R = Math.max(230, circumference / (2 * Math.PI));
|
|
let angle = -Math.PI / 2;
|
|
for (const c of clusters) {
|
|
const share = ((c.r * 2 + 50) / circumference) * Math.PI * 2;
|
|
const a = angle + share / 2;
|
|
centers[c.id] = { x: Math.cos(a) * R, y: Math.sin(a) * R, r: c.r };
|
|
angle += share;
|
|
}
|
|
}
|
|
for (const c of clusters) {
|
|
const ctr = centers[c.id];
|
|
// 이름순 정렬 후 해바라기 배치 (관계 많은 별은 가운데)
|
|
c.terms.forEach((t, k) => {
|
|
const rr = Math.sqrt(k + 0.6) * SPACING * 0.62;
|
|
const a = k * GOLDEN;
|
|
pos[t.i] = { x: ctr.x + Math.cos(a) * rr, y: ctr.y + Math.sin(a) * rr * 0.9 };
|
|
});
|
|
}
|
|
let minX = 0, maxX = 0, minY = 0, maxY = 0;
|
|
for (const c of Object.values(centers)) {
|
|
minX = Math.min(minX, c.x - c.r); maxX = Math.max(maxX, c.x + c.r);
|
|
minY = Math.min(minY, c.y - c.r); maxY = Math.max(maxY, c.y + c.r);
|
|
}
|
|
const pad = 70;
|
|
return { pos, centers, bounds: { minX: minX - pad, maxX: maxX + pad, minY: minY - pad - 20, maxY: maxY + pad } };
|
|
}
|
|
|
|
/** 화면 크기에 맞춰 보기 (scale, tx, ty) */
|
|
export function fit(bounds, w, h, { max = 1.4, min = 0.25, margin = 24 } = {}) {
|
|
const bw = bounds.maxX - bounds.minX, bh = bounds.maxY - bounds.minY;
|
|
const s = Math.max(min, Math.min(max, (w - margin * 2) / bw, (h - margin * 2) / bh));
|
|
const cx = (bounds.minX + bounds.maxX) / 2, cy = (bounds.minY + bounds.maxY) / 2;
|
|
return { s, tx: w / 2 - cx * s, ty: h / 2 - cy * s };
|
|
}
|
|
|
|
/** 라벨 겹침 제거: 우선순위 순으로 넣고 겹치면 숨김. items: [{id, x, y, w, h, prio}] → Set(보이는 id) */
|
|
export function cullLabels(items) {
|
|
const placed = [];
|
|
const visible = new Set();
|
|
const sorted = [...items].sort((a, b) => b.prio - a.prio);
|
|
const cell = 80;
|
|
const grid = new Map();
|
|
const key = (gx, gy) => gx + "," + gy;
|
|
for (const it of sorted) {
|
|
const box = { x0: it.x - it.w / 2, x1: it.x + it.w / 2, y0: it.y, y1: it.y + it.h };
|
|
let hit = false;
|
|
const gx0 = Math.floor(box.x0 / cell), gx1 = Math.floor(box.x1 / cell);
|
|
const gy0 = Math.floor(box.y0 / cell), gy1 = Math.floor(box.y1 / cell);
|
|
outer: for (let gx = gx0; gx <= gx1; gx++) {
|
|
for (let gy = gy0; gy <= gy1; gy++) {
|
|
for (const o of grid.get(key(gx, gy)) || []) {
|
|
if (box.x0 < o.x1 && box.x1 > o.x0 && box.y0 < o.y1 && box.y1 > o.y0) { hit = true; break outer; }
|
|
}
|
|
}
|
|
}
|
|
if (hit && !it.force) continue;
|
|
visible.add(it.id);
|
|
placed.push(box);
|
|
for (let gx = gx0; gx <= gx1; gx++) for (let gy = gy0; gy <= gy1; gy++) {
|
|
const k = key(gx, gy);
|
|
if (!grid.has(k)) grid.set(k, []);
|
|
grid.get(k).push(box);
|
|
}
|
|
}
|
|
return visible;
|
|
}
|
|
|
|
export function labelWidth(text, px = 12) {
|
|
let w = 0;
|
|
for (const ch of text) w += /[ㄱ-힣]/.test(ch) ? px * 1.1 : px * 0.6;
|
|
return w + 16;
|
|
}
|
|
|
|
/** 등록된 관계로 최단 경로 (방향 무시) — [{term, rel, forward}] 또는 null */
|
|
export function shortestPath(adj, from, to, { maxDepth = 8, allow = () => true } = {}) {
|
|
if (from === to) return [];
|
|
const prev = new Map([[from, null]]);
|
|
let frontier = [from];
|
|
for (let depth = 0; depth < maxDepth && frontier.length; depth++) {
|
|
const next = [];
|
|
for (const id of frontier) {
|
|
for (const r of adj.get(id) || []) {
|
|
if (!allow(r)) continue;
|
|
const other = r.s === id ? r.t : r.s;
|
|
if (prev.has(other)) continue;
|
|
prev.set(other, { id, r });
|
|
if (other === to) {
|
|
const path = [];
|
|
let cur = to;
|
|
while (prev.get(cur)) {
|
|
const p = prev.get(cur);
|
|
path.unshift({ from: p.id, to: cur, rel: p.r, forward: p.r.s === p.id });
|
|
cur = p.id;
|
|
}
|
|
return path;
|
|
}
|
|
next.push(other);
|
|
}
|
|
}
|
|
frontier = next;
|
|
}
|
|
return null;
|
|
}
|