Files
web_mindmap/script.js
2026-05-12 15:43:28 +09:00

1382 lines
43 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const webNodes = [
{
id: "root",
type: "root",
title: "웹 개발",
subtitle: "클릭해서 펼치기",
image: "imgs/category_web_overview.png",
category: "전체 개요",
focus: "웹 개발이 프론트엔드, 백엔드, 통신, 데이터 흐름으로 나뉘는 큰 그림을 봅니다.",
x: 1020,
y: 700,
children: ["structure", "frontend", "network", "backend", "workflow"],
},
{
id: "structure",
type: "category",
title: "웹의 뼈대",
subtitle: "브라우저와 화면 구성",
image: "imgs/category_web_structure.png",
category: "웹의 뼈대",
focus: "HTML, CSS, 브라우저, DOM이 화면을 구성하는 기본 흐름을 봅니다.",
x: 1020,
y: 420,
children: ["url-dns", "browser", "html-css", "css-layout", "dom", "devtools"],
},
{
id: "frontend",
type: "category",
title: "프론트엔드",
subtitle: "UI를 만드는 도구",
image: "imgs/category_frontend.png",
category: "프론트엔드",
focus: "사용자가 보는 화면과 백엔드가 만나는 경계를 먼저 파악합니다.",
x: 1360,
y: 560,
children: ["javascript", "typescript", "react", "front-back", "web-storage"],
},
{
id: "network",
type: "category",
title: "통신",
subtitle: "요청과 응답",
image: "imgs/category_network.png",
category: "통신",
focus: "요청과 응답, 상태 코드, 헤더, 바디가 웹 통신에서 맡는 역할을 봅니다.",
x: 1320,
y: 900,
children: ["auth", "cookie", "api", "http", "request-response", "cors", "xss-csrf"],
},
{
id: "backend",
type: "category",
title: "백엔드·데이터",
subtitle: "서버와 저장소",
image: "imgs/category_backend_data.png",
category: "백엔드·데이터",
focus: "서버가 요청을 처리하고 데이터를 저장·조회하는 큰 흐름을 봅니다.",
x: 1020,
y: 1030,
children: ["web-server", "database", "json", "env-vars"],
},
{
id: "workflow",
type: "category",
title: "프레임워크·도구",
subtitle: "개발 흐름",
image: "imgs/category_framework_tools.png",
category: "프레임워크·도구",
focus: "Next.js 같은 프레임워크가 라우팅, 렌더링, 개발 흐름을 어떻게 묶는지 봅니다.",
x: 660,
y: 900,
children: ["next", "next-structure", "npm-package", "git-version-control", "build-bundling", "deployment-hosting", "testing"],
},
{
id: "url-dns",
parent: "structure",
type: "term",
title: "URL · 도메인 · DNS",
subtitle: "웹의 뼈대",
image: "imgs/term_url_dns.png",
category: "웹의 뼈대",
focus: "주소창의 도메인이 DNS를 통해 IP로 바뀌고 서버에 연결되는 흐름을 봅니다.",
x: 650,
y: 170,
},
{
id: "browser",
parent: "structure",
type: "term",
title: "브라우저",
subtitle: "웹의 뼈대",
image: "imgs/term_browser.png",
category: "웹의 뼈대",
focus: "URL 요청, 리소스 해석, 렌더링, 개발자 도구를 연결해서 봅니다.",
x: 720,
y: 330,
},
{
id: "html-css",
parent: "structure",
type: "term",
title: "HTML / CSS",
subtitle: "웹의 뼈대",
image: "imgs/html_css.png",
category: "웹의 뼈대",
focus: "HTML은 구조, CSS는 표현을 담당한다는 기본 분업을 익힙니다.",
x: 1020,
y: 240,
},
{
id: "dom",
parent: "structure",
type: "term",
title: "DOM",
subtitle: "웹의 뼈대",
image: "imgs/term_dom.png",
category: "웹의 뼈대",
focus: "HTML이 객체 트리로 바뀌고 JavaScript가 이를 조작하는 흐름을 봅니다.",
x: 1320,
y: 330,
},
{
id: "css-layout",
parent: "structure",
type: "term",
title: "CSS 레이아웃",
subtitle: "웹의 뼈대",
image: "imgs/term_css_layout.png",
category: "웹의 뼈대",
focus: "Box Model, Flexbox, Grid, 반응형 규칙으로 화면을 배치하는 방법을 봅니다.",
x: 910,
y: 110,
},
{
id: "devtools",
parent: "structure",
type: "term",
title: "DevTools",
subtitle: "웹의 뼈대",
image: "imgs/term_devtools.png",
category: "웹의 뼈대",
focus: "Elements, Console, Network, Application 패널로 브라우저 안의 상태를 확인합니다.",
x: 1290,
y: 150,
},
{
id: "javascript",
parent: "frontend",
type: "term",
title: "JavaScript",
subtitle: "프론트엔드",
image: "imgs/term_javascript.png",
category: "프론트엔드",
focus: "이벤트, 상태, 비동기 처리로 화면이 움직이는 원리를 이해합니다.",
x: 1530,
y: 380,
},
{
id: "react",
parent: "frontend",
type: "term",
title: "React",
subtitle: "프론트엔드",
image: "imgs/React.png",
category: "프론트엔드",
focus: "컴포넌트, 상태, props를 중심으로 UI를 조립하는 방식을 봅니다.",
x: 1640,
y: 560,
},
{
id: "typescript",
parent: "frontend",
type: "term",
title: "TypeScript",
subtitle: "프론트엔드",
image: "imgs/term_typescript.png",
category: "프론트엔드",
focus: "JavaScript에 타입 검사를 더해 런타임 전에 오류를 줄이는 방법을 봅니다.",
x: 1720,
y: 410,
},
{
id: "front-back",
parent: "frontend",
type: "term",
title: "프론트엔드와 백엔드",
subtitle: "프론트엔드",
image: "imgs/frontend_backend.png",
category: "프론트엔드",
focus: "화면 담당 영역과 서버 담당 영역이 어디서 만나는지 확인합니다.",
x: 1530,
y: 740,
},
{
id: "web-storage",
parent: "frontend",
type: "term",
title: "localStorage · sessionStorage",
subtitle: "프론트엔드",
image: "imgs/term_web_storage.png",
category: "프론트엔드",
focus: "브라우저에 문자열 데이터를 저장하는 방식과 쿠키와의 차이를 봅니다.",
x: 1740,
y: 760,
},
{
id: "auth",
parent: "network",
type: "term",
title: "인증",
subtitle: "통신",
image: "imgs/term_authentication.png",
category: "통신",
focus: "로그인, 세션, 토큰, 권한 확인의 차이를 분리해서 봅니다.",
x: 1510,
y: 810,
},
{
id: "cookie",
parent: "network",
type: "term",
title: "쿠키",
subtitle: "통신",
image: "imgs/cookie.png",
category: "통신",
focus: "브라우저에 저장되는 작은 데이터가 세션과 추적에 쓰이는 방식을 익힙니다.",
x: 1600,
y: 940,
},
{
id: "api",
parent: "network",
type: "category",
title: "API",
subtitle: "통신",
image: "imgs/term_api.png",
category: "통신",
focus: "프론트엔드와 백엔드가 약속된 주소와 데이터 형식으로 대화하는 개념입니다.",
x: 1460,
y: 1080,
children: ["rest-crud"],
},
{
id: "http",
parent: "network",
type: "category",
title: "HTTP 프로토콜",
subtitle: "통신",
image: "imgs/http_protocol.png",
category: "통신",
focus: "웹에서 클라이언트와 서버가 약속된 방식으로 통신하는 규칙입니다.",
x: 1190,
y: 1160,
children: ["https-tls"],
},
{
id: "request-response",
parent: "network",
type: "term",
title: "HTTP 요청/응답",
subtitle: "통신",
image: "imgs/httpReq_Res.png",
category: "통신",
focus: "메서드, 헤더, 바디, 상태 코드가 실제 흐름에서 어떤 역할인지 봅니다.",
x: 930,
y: 1180,
},
{
id: "https-tls",
parent: "http",
type: "term",
title: "HTTPS · TLS",
subtitle: "통신",
image: "imgs/term_https_tls.png",
category: "통신",
focus: "TLS 암호화와 인증서로 HTTP 통신을 안전하게 보호하는 원리를 봅니다.",
x: 1120,
y: 1370,
},
{
id: "rest-crud",
parent: "api",
type: "term",
title: "REST · CRUD",
subtitle: "통신",
image: "imgs/term_rest_crud.png",
category: "통신",
focus: "리소스 중심 API와 생성/조회/수정/삭제 흐름을 HTTP 메서드와 연결합니다.",
x: 1620,
y: 1220,
},
{
id: "cors",
parent: "network",
type: "term",
title: "CORS",
subtitle: "통신",
image: "imgs/term_cors.png",
category: "통신",
focus: "다른 출처의 API 요청이 브라우저 보안 정책과 서버 헤더로 허용되는 방식을 봅니다.",
x: 1740,
y: 1040,
},
{
id: "xss-csrf",
parent: "network",
type: "term",
title: "XSS · CSRF",
subtitle: "통신",
image: "imgs/term_xss_csrf.png",
category: "통신",
focus: "스크립트 주입과 위조 요청을 구분하고 입력 검증, 토큰, 쿠키 옵션을 봅니다.",
x: 1790,
y: 1370,
},
{
id: "web-server",
parent: "backend",
type: "term",
title: "웹서버 프로그램",
subtitle: "백엔드·데이터",
image: "imgs/web_server_program.png",
category: "백엔드·데이터",
focus: "요청을 받아 정적 파일이나 애플리케이션 응답을 돌려주는 서버 역할입니다.",
x: 760,
y: 1010,
},
{
id: "database",
parent: "backend",
type: "category",
title: "데이터베이스",
subtitle: "백엔드·데이터",
image: "imgs/database.png",
category: "백엔드·데이터",
focus: "애플리케이션 데이터가 테이블, 문서, 키-값 등으로 저장되는 방식을 봅니다.",
x: 880,
y: 1280,
children: ["postgres", "orm-migration"],
},
{
id: "postgres",
parent: "database",
type: "term",
title: "PostgreSQL",
subtitle: "백엔드·데이터",
image: "imgs/postgress.png",
category: "백엔드·데이터",
focus: "관계형 데이터베이스와 SQL, 트랜잭션, 확장성 개념을 연결합니다.",
x: 1180,
y: 1300,
},
{
id: "orm-migration",
parent: "database",
type: "term",
title: "ORM · Migration",
subtitle: "백엔드·데이터",
image: "imgs/term_orm_migration.png",
category: "백엔드·데이터",
focus: "코드 모델과 DB 테이블을 연결하고 스키마 변경을 버전으로 관리하는 방법을 봅니다.",
x: 810,
y: 1500,
},
{
id: "json",
parent: "backend",
type: "term",
title: "JSON",
subtitle: "백엔드·데이터",
image: "imgs/term_json.png",
category: "백엔드·데이터",
focus: "API에서 주고받는 데이터가 키-값, 배열, 객체로 표현되는 형식입니다.",
x: 1420,
y: 1210,
},
{
id: "env-vars",
parent: "backend",
type: "term",
title: "환경 변수(.env)",
subtitle: "백엔드·데이터",
image: "imgs/term_env_vars.png",
category: "백엔드·데이터",
focus: "API Key, DB URL 같은 설정을 코드와 분리하고 Git에 올리지 않는 이유를 봅니다.",
x: 650,
y: 1210,
},
{
id: "next",
parent: "workflow",
type: "term",
title: "Next.js",
subtitle: "프레임워크·도구",
image: "imgs/nextJS.png",
category: "프레임워크·도구",
focus: "React 기반 프레임워크가 라우팅, 렌더링, API 경계를 어떻게 제공하는지 봅니다.",
x: 390,
y: 820,
},
{
id: "next-structure",
parent: "workflow",
type: "term",
title: "Next.js 디렉터리 구조",
subtitle: "프레임워크·도구",
image: "imgs/nextjs_directory_structure.png",
category: "프레임워크·도구",
focus: "app, page, layout, component, public 같은 폴더의 역할을 익힙니다.",
x: 420,
y: 1080,
},
{
id: "npm-package",
parent: "workflow",
type: "term",
title: "npm · package.json",
subtitle: "프레임워크·도구",
image: "imgs/term_npm_package.png",
category: "프레임워크·도구",
focus: "패키지 설치, scripts, dependencies가 프로젝트 실행 흐름을 잡는 방식을 봅니다.",
x: 260,
y: 940,
},
{
id: "git-version-control",
parent: "workflow",
type: "term",
title: "Git · 버전 관리",
subtitle: "프레임워크·도구",
image: "imgs/term_git_version_control.png",
category: "프레임워크·도구",
focus: "commit, branch, merge, remote를 통해 변경 이력과 협업을 관리하는 방법을 봅니다.",
x: 200,
y: 1190,
},
{
id: "build-bundling",
parent: "workflow",
type: "term",
title: "빌드 · 번들링",
subtitle: "프레임워크·도구",
image: "imgs/term_build_bundling.png",
category: "프레임워크·도구",
focus: "소스 코드를 배포 가능한 정적 파일로 최적화하는 과정을 봅니다.",
x: 610,
y: 1190,
},
{
id: "deployment-hosting",
parent: "workflow",
type: "category",
title: "배포 · 호스팅",
subtitle: "프레임워크·도구",
image: "imgs/term_deployment_hosting.png",
category: "프레임워크·도구",
focus: "빌드 결과물을 서버나 CDN에 올려 외부 사용자가 접근하게 만드는 흐름을 봅니다.",
x: 560,
y: 1410,
children: ["nginx-reverse-proxy"],
},
{
id: "nginx-reverse-proxy",
parent: "deployment-hosting",
type: "term",
title: "Nginx · Reverse Proxy",
subtitle: "프레임워크·도구",
image: "imgs/term_nginx_reverse_proxy.png",
category: "프레임워크·도구",
focus: "공개 요청을 받아 정적 파일과 API 서버로 나누어 전달하는 프록시 역할을 봅니다.",
x: 350,
y: 1620,
},
{
id: "testing",
parent: "workflow",
type: "term",
title: "테스트",
subtitle: "프레임워크·도구",
image: "imgs/term_testing.png",
category: "프레임워크·도구",
focus: "Unit, Integration, E2E, Smoke 테스트로 변경이 실제로 동작하는지 확인합니다.",
x: 190,
y: 1450,
},
];
const linuxNodes = [
{
id: "root",
type: "root",
title: "리눅스",
subtitle: "클릭해서 펼치기",
image: "imgs/linux_card_root.png",
category: "전체 개요",
focus: "리눅스를 터미널, 파일 시스템, 권한, 프로세스, 패키지, 네트워크 흐름으로 나누어 봅니다.",
x: 1020,
y: 700,
children: ["linux-basics", "linux-shell", "linux-filesystem", "linux-permissions", "linux-processes", "linux-packages-network"],
},
{
id: "linux-basics",
parent: "root",
type: "category",
title: "기초 개념",
subtitle: "운영체제와 도움말",
image: "imgs/linux_card_linux_basics.png",
category: "기초 개념",
focus: "리눅스가 커널, 배포판, 셸, 패키지 저장소로 구성된 운영체제 생태계라는 큰 그림을 봅니다.",
x: 1020,
y: 420,
children: ["linux-kernel", "linux-distro", "linux-terminal", "linux-man-pages", "linux-sudo-basics"],
},
{
id: "linux-shell",
parent: "root",
type: "category",
title: "셸 명령어",
subtitle: "명령 실행과 조합",
image: "imgs/linux_card_linux_shell.png",
category: "셸 명령어",
focus: "셸은 명령어, 옵션, 인자, 파이프, 리다이렉트를 조합해 작업을 자동화하는 인터페이스입니다.",
x: 1360,
y: 560,
children: ["linux-command-shape", "linux-pwd-ls-cd", "linux-file-ops", "linux-pipes", "linux-grep-find", "linux-history", "linux-env-path"],
},
{
id: "linux-filesystem",
parent: "root",
type: "category",
title: "파일 시스템",
subtitle: "경로와 디렉터리",
image: "imgs/linux_card_linux_filesystem.png",
category: "파일 시스템",
focus: "리눅스는 모든 경로가 루트(/)에서 시작하고, 설정·로그·사용자 파일이 관례적인 위치에 놓입니다.",
x: 1320,
y: 900,
children: ["linux-root-path", "linux-home-etc-var", "linux-hidden-files", "linux-mounts", "linux-links"],
},
{
id: "linux-permissions",
parent: "root",
type: "category",
title: "권한",
subtitle: "사용자와 접근 제어",
image: "imgs/linux_card_linux_permissions.png",
category: "권한",
focus: "사용자, 그룹, 읽기/쓰기/실행 권한을 이해하면 파일 접근과 관리자 작업을 안전하게 다룰 수 있습니다.",
x: 1020,
y: 1030,
children: ["linux-users-groups", "linux-rwx", "linux-chmod-chown", "linux-sudo", "linux-ssh-keys"],
},
{
id: "linux-processes",
parent: "root",
type: "category",
title: "프로세스",
subtitle: "실행 중인 작업",
image: "imgs/linux_card_linux_processes.png",
category: "프로세스",
focus: "프로세스와 서비스 상태를 보고, 필요하면 종료하거나 로그를 확인하고, 예약 실행까지 연결합니다.",
x: 660,
y: 900,
children: ["linux-ps-top", "linux-kill", "linux-systemd", "linux-logs", "linux-cron"],
},
{
id: "linux-packages-network",
parent: "root",
type: "category",
title: "패키지·네트워크",
subtitle: "설치와 원격 접속",
image: "imgs/linux_card_linux_packages_network.png",
category: "패키지·네트워크",
focus: "패키지 관리자로 도구를 설치하고, 네트워크 명령과 SSH로 서버 상태를 확인합니다.",
x: 660,
y: 560,
children: ["linux-apt-dnf", "linux-update-upgrade", "linux-ping-ss", "linux-curl-wget", "linux-ssh", "linux-firewall"],
},
{
id: "linux-kernel",
parent: "linux-basics",
type: "term",
title: "커널",
subtitle: "기초 개념",
image: "imgs/linux_card_linux_kernel.png",
category: "기초 개념",
focus: "커널은 CPU, 메모리, 파일, 네트워크 같은 하드웨어 자원을 프로그램이 안전하게 쓰도록 중재합니다.",
x: 650,
y: 170,
},
{
id: "linux-distro",
parent: "linux-basics",
type: "term",
title: "배포판",
subtitle: "기초 개념",
image: "imgs/linux_card_linux_distro.png",
category: "기초 개념",
focus: "Ubuntu, Debian, Fedora 같은 배포판은 커널 위에 기본 도구, 패키지 저장소, 설정 방식을 묶어 제공합니다.",
x: 720,
y: 330,
},
{
id: "linux-terminal",
parent: "linux-basics",
type: "term",
title: "터미널",
subtitle: "기초 개념",
image: "imgs/linux_card_linux_terminal.png",
category: "기초 개념",
focus: "터미널은 셸을 통해 명령을 입력하고 결과를 확인하는 창입니다.",
x: 1020,
y: 240,
},
{
id: "linux-man-pages",
parent: "linux-basics",
type: "term",
title: "man · --help",
subtitle: "기초 개념",
image: "imgs/linux_card_linux_man_pages.png",
category: "기초 개념",
focus: "명령어가 헷갈릴 때는 man, --help, tldr 같은 도움말로 옵션과 사용 예를 먼저 확인합니다.",
x: 1320,
y: 330,
},
{
id: "linux-sudo-basics",
parent: "linux-basics",
type: "term",
title: "관리자 작업",
subtitle: "기초 개념",
image: "imgs/linux_card_linux_sudo_basics.png",
category: "기초 개념",
focus: "시스템 설정, 패키지 설치, 서비스 제어처럼 영향 범위가 큰 작업은 관리자 권한이 필요합니다.",
x: 1290,
y: 150,
},
{
id: "linux-command-shape",
parent: "linux-shell",
type: "term",
title: "명령어 구조",
subtitle: "셸 명령어",
image: "imgs/linux_card_linux_command_shape.png",
category: "셸 명령어",
focus: "대부분의 명령은 command --option argument 형태이며, 옵션은 동작 방식을 바꾸고 인자는 대상을 지정합니다.",
x: 1530,
y: 380,
},
{
id: "linux-pwd-ls-cd",
parent: "linux-shell",
type: "term",
title: "pwd · ls · cd",
subtitle: "셸 명령어",
image: "imgs/linux_card_linux_pwd_ls_cd.png",
category: "셸 명령어",
focus: "pwd로 현재 위치를 보고, ls로 목록을 확인하고, cd로 디렉터리를 이동합니다.",
x: 1640,
y: 560,
},
{
id: "linux-file-ops",
parent: "linux-shell",
type: "term",
title: "cp · mv · rm",
subtitle: "셸 명령어",
image: "imgs/linux_card_linux_file_ops.png",
category: "셸 명령어",
focus: "cp는 복사, mv는 이동/이름 변경, rm은 삭제입니다. 삭제 명령은 되돌리기 어렵기 때문에 대상 경로를 먼저 확인합니다.",
x: 1720,
y: 410,
},
{
id: "linux-pipes",
parent: "linux-shell",
type: "term",
title: "파이프 · 리다이렉트",
subtitle: "셸 명령어",
image: "imgs/linux_card_linux_pipes.png",
category: "셸 명령어",
focus: "파이프(|)는 한 명령의 출력을 다음 명령으로 넘기고, > 또는 >>는 출력을 파일에 저장합니다.",
x: 1530,
y: 740,
},
{
id: "linux-grep-find",
parent: "linux-shell",
type: "term",
title: "grep · find",
subtitle: "셸 명령어",
image: "imgs/linux_card_linux_grep_find.png",
category: "셸 명령어",
focus: "grep은 텍스트 안에서 패턴을 찾고, find는 파일 시스템에서 조건에 맞는 파일과 디렉터리를 찾습니다.",
x: 1740,
y: 760,
},
{
id: "linux-history",
parent: "linux-shell",
type: "term",
title: "history",
subtitle: "셸 명령어",
image: "imgs/linux_card_linux_history.png",
category: "셸 명령어",
focus: "history와 방향키, Ctrl+R 검색을 쓰면 이전 명령을 빠르게 재사용할 수 있습니다.",
x: 1780,
y: 620,
},
{
id: "linux-env-path",
parent: "linux-shell",
type: "term",
title: "환경 변수 · PATH",
subtitle: "셸 명령어",
image: "imgs/linux_card_linux_env_path.png",
category: "셸 명령어",
focus: "환경 변수는 현재 셸과 프로그램에 전달되는 설정이고, PATH는 명령어 실행 파일을 찾는 디렉터리 목록입니다.",
x: 1470,
y: 240,
},
{
id: "linux-root-path",
parent: "linux-filesystem",
type: "term",
title: "루트 경로 /",
subtitle: "파일 시스템",
image: "imgs/linux_card_linux_root_path.png",
category: "파일 시스템",
focus: "리눅스의 모든 절대 경로는 루트(/)에서 시작합니다.",
x: 1510,
y: 810,
},
{
id: "linux-home-etc-var",
parent: "linux-filesystem",
type: "term",
title: "/home · /etc · /var",
subtitle: "파일 시스템",
image: "imgs/linux_card_linux_home_etc_var.png",
category: "파일 시스템",
focus: "/home은 사용자 파일, /etc는 시스템 설정, /var는 로그와 변하는 데이터를 주로 담습니다.",
x: 1600,
y: 940,
},
{
id: "linux-hidden-files",
parent: "linux-filesystem",
type: "term",
title: "숨김 파일",
subtitle: "파일 시스템",
image: "imgs/linux_card_linux_hidden_files.png",
category: "파일 시스템",
focus: ".bashrc처럼 점(.)으로 시작하는 파일은 기본 목록에서 숨겨지며, 보통 사용자 설정을 담습니다.",
x: 1460,
y: 1080,
},
{
id: "linux-mounts",
parent: "linux-filesystem",
type: "term",
title: "mount",
subtitle: "파일 시스템",
image: "imgs/linux_card_linux_mounts.png",
category: "파일 시스템",
focus: "디스크, USB, 네트워크 저장소는 특정 디렉터리에 마운트되어 파일 시스템 일부처럼 보입니다.",
x: 1190,
y: 1160,
},
{
id: "linux-links",
parent: "linux-filesystem",
type: "term",
title: "심볼릭 링크",
subtitle: "파일 시스템",
image: "imgs/linux_card_linux_links.png",
category: "파일 시스템",
focus: "심볼릭 링크는 다른 파일이나 디렉터리를 가리키는 바로가기이며 ln -s로 만듭니다.",
x: 1740,
y: 1040,
},
{
id: "linux-users-groups",
parent: "linux-permissions",
type: "term",
title: "사용자 · 그룹",
subtitle: "권한",
image: "imgs/linux_card_linux_users_groups.png",
category: "권한",
focus: "리눅스는 사용자와 그룹 단위로 파일 접근, 서비스 실행, 관리자 작업 가능 여부를 나눕니다.",
x: 760,
y: 1010,
},
{
id: "linux-rwx",
parent: "linux-permissions",
type: "term",
title: "rwx 권한",
subtitle: "권한",
image: "imgs/linux_card_linux_rwx.png",
category: "권한",
focus: "r은 읽기, w는 쓰기, x는 실행 권한입니다. 디렉터리의 x는 내부로 들어갈 수 있는 권한에 가깝습니다.",
x: 880,
y: 1280,
},
{
id: "linux-chmod-chown",
parent: "linux-permissions",
type: "term",
title: "chmod · chown",
subtitle: "권한",
image: "imgs/linux_card_linux_chmod_chown.png",
category: "권한",
focus: "chmod는 권한 비트를 바꾸고, chown은 파일의 소유자나 그룹을 바꿉니다.",
x: 1180,
y: 1300,
},
{
id: "linux-sudo",
parent: "linux-permissions",
type: "term",
title: "sudo",
subtitle: "권한",
image: "imgs/linux_card_linux_sudo.png",
category: "권한",
focus: "sudo는 허용된 사용자가 한 명령을 관리자 권한으로 실행하게 해 줍니다.",
x: 810,
y: 1500,
},
{
id: "linux-ssh-keys",
parent: "linux-permissions",
type: "term",
title: "SSH Key 권한",
subtitle: "권한",
image: "imgs/linux_card_linux_ssh_keys.png",
category: "권한",
focus: "개인 키는 너무 열려 있으면 SSH가 거부할 수 있으므로 보통 chmod 600처럼 제한적으로 둡니다.",
x: 1420,
y: 1210,
},
{
id: "linux-ps-top",
parent: "linux-processes",
type: "term",
title: "ps · top",
subtitle: "프로세스",
image: "imgs/linux_card_linux_ps_top.png",
category: "프로세스",
focus: "ps는 프로세스 목록을 출력하고, top/htop은 CPU와 메모리 사용량을 실시간으로 보여 줍니다.",
x: 390,
y: 820,
},
{
id: "linux-kill",
parent: "linux-processes",
type: "term",
title: "kill",
subtitle: "프로세스",
image: "imgs/linux_card_linux_kill.png",
category: "프로세스",
focus: "kill은 프로세스에 종료 신호를 보내며, 강제 종료 전에 정상 종료 신호를 먼저 고려합니다.",
x: 420,
y: 1080,
},
{
id: "linux-systemd",
parent: "linux-processes",
type: "term",
title: "systemd · service",
subtitle: "프로세스",
image: "imgs/linux_card_linux_systemd.png",
category: "프로세스",
focus: "systemctl로 서비스를 시작, 중지, 재시작, 자동 실행 설정할 수 있습니다.",
x: 260,
y: 940,
},
{
id: "linux-logs",
parent: "linux-processes",
type: "term",
title: "로그",
subtitle: "프로세스",
image: "imgs/linux_card_linux_logs.png",
category: "프로세스",
focus: "journalctl과 /var/log는 서비스 오류와 시스템 이벤트를 추적하는 첫 번째 단서입니다.",
x: 200,
y: 1190,
},
{
id: "linux-cron",
parent: "linux-processes",
type: "term",
title: "cron",
subtitle: "프로세스",
image: "imgs/linux_card_linux_cron.png",
category: "프로세스",
focus: "cron은 정해진 시간에 백업, 정리, 점검 스크립트를 반복 실행할 때 사용합니다.",
x: 610,
y: 1190,
},
{
id: "linux-apt-dnf",
parent: "linux-packages-network",
type: "term",
title: "apt · dnf",
subtitle: "패키지·네트워크",
image: "imgs/linux_card_linux_apt_dnf.png",
category: "패키지·네트워크",
focus: "apt는 Debian/Ubuntu 계열, dnf는 Fedora/RHEL 계열에서 패키지를 설치하고 제거하는 대표 도구입니다.",
x: 560,
y: 310,
},
{
id: "linux-update-upgrade",
parent: "linux-packages-network",
type: "term",
title: "update · upgrade",
subtitle: "패키지·네트워크",
image: "imgs/linux_card_linux_update_upgrade.png",
category: "패키지·네트워크",
focus: "update는 패키지 목록을 새로 받고, upgrade는 설치된 패키지를 새 버전으로 올립니다.",
x: 390,
y: 470,
},
{
id: "linux-ping-ss",
parent: "linux-packages-network",
type: "term",
title: "ping · ss",
subtitle: "패키지·네트워크",
image: "imgs/linux_card_linux_ping_ss.png",
category: "패키지·네트워크",
focus: "ping은 연결 가능성을 확인하고, ss는 열린 포트와 소켓 상태를 확인합니다.",
x: 780,
y: 420,
},
{
id: "linux-curl-wget",
parent: "linux-packages-network",
type: "term",
title: "curl · wget",
subtitle: "패키지·네트워크",
image: "imgs/linux_card_linux_curl_wget.png",
category: "패키지·네트워크",
focus: "curl은 HTTP 요청과 응답 확인에 강하고, wget은 파일 다운로드에 자주 쓰입니다.",
x: 350,
y: 650,
},
{
id: "linux-ssh",
parent: "linux-packages-network",
type: "term",
title: "ssh",
subtitle: "패키지·네트워크",
image: "imgs/linux_card_linux_ssh.png",
category: "패키지·네트워크",
focus: "ssh는 원격 서버의 셸에 안전하게 접속하는 기본 도구입니다.",
x: 780,
y: 660,
},
{
id: "linux-firewall",
parent: "linux-packages-network",
type: "term",
title: "방화벽",
subtitle: "패키지·네트워크",
image: "imgs/linux_card_linux_firewall.png",
category: "패키지·네트워크",
focus: "ufw, firewalld, iptables 같은 도구로 외부에서 접근 가능한 포트와 규칙을 제한합니다.",
x: 480,
y: 210,
},
];
const maps = {
web: {
eyebrow: "Web Learning Map",
title: "웹 개발 용어 마인드맵",
ariaLabel: "접을 수 있는 웹 개발 용어 지도",
nodes: webNodes,
},
linux: {
eyebrow: "Linux Learning Map",
title: "리눅스 기초 마인드맵",
ariaLabel: "접을 수 있는 리눅스 기초 개념 지도",
nodes: linuxNodes,
},
};
const viewport = document.querySelector("#viewport");
const world = document.querySelector("#world");
const nodeLayer = document.querySelector("#nodeLayer");
const connectorLayer = document.querySelector("#connectorLayer");
const visibleCount = document.querySelector("#visibleCount");
const pageTitle = document.querySelector("#page-title");
const mapEyebrow = document.querySelector("#mapEyebrow");
const lightbox = document.querySelector("#lightbox");
const lightboxImage = document.querySelector("#lightboxImage");
const lightboxCaption = document.querySelector("#lightboxCaption");
const lightboxCategory = document.querySelector("#lightboxCategory");
const lightboxFocus = document.querySelector("#lightboxFocus");
const zoomStage = document.querySelector("#zoomStage");
const zoomLevel = document.querySelector("#zoomLevel");
const zoomInButton = document.querySelector("#zoomIn");
const zoomOutButton = document.querySelector("#zoomOut");
const zoomResetButton = document.querySelector("#zoomReset");
const fullscreenToggle = document.querySelector("#fullscreenToggle");
let activeMapId = "web";
let nodes = maps[activeMapId].nodes;
let nodeMap = new Map(nodes.map((node) => [node.id, node]));
const expanded = new Set();
let pan = { x: window.innerWidth / 2 - 1136, y: window.innerHeight / 2 - 764 };
let activePointer = null;
let zoomState = { scale: 1, x: 0, y: 0, dragging: false, pointerId: null, startX: 0, startY: 0, startOffsetX: 0, startOffsetY: 0 };
connectorLayer.setAttribute("viewBox", "0 0 2400 1900");
connectorLayer.setAttribute("width", "2400");
connectorLayer.setAttribute("height", "1900");
function visibleNodeIds() {
const visible = ["root"];
for (const node of nodes) {
if (node.id === "root") continue;
const parent = node.parent ?? "root";
if (expanded.has(parent)) visible.push(node.id);
}
return visible;
}
function setWorldTransform() {
world.style.transform = `translate(${pan.x}px, ${pan.y}px)`;
}
function syncMapChrome() {
const activeMap = maps[activeMapId];
pageTitle.textContent = activeMap.title;
mapEyebrow.textContent = activeMap.eyebrow;
nodeLayer.setAttribute("aria-label", activeMap.ariaLabel);
for (const button of document.querySelectorAll(".map-tab")) {
const isSelected = button.dataset.map === activeMapId;
button.classList.toggle("is-selected", isSelected);
button.setAttribute("aria-pressed", String(isSelected));
}
}
function switchMap(mapId) {
if (!maps[mapId] || mapId === activeMapId) return;
activeMapId = mapId;
nodes = maps[activeMapId].nodes;
nodeMap = new Map(nodes.map((node) => [node.id, node]));
expanded.clear();
syncMapChrome();
resetView();
render();
}
function render() {
const visible = new Set(visibleNodeIds());
nodeLayer.innerHTML = "";
for (const node of nodes) {
if (!visible.has(node.id)) continue;
const element = document.createElement("div");
element.className = `node ${node.type} node-enter${node.image ? " has-image" : ""}`;
element.dataset.id = node.id;
element.tabIndex = 0;
element.setAttribute("role", node.image ? "button" : "group");
element.setAttribute("aria-label", node.image ? `${node.title} 이미지 열기` : node.title);
element.style.left = `${node.x}px`;
element.style.top = `${node.y}px`;
element.innerHTML = `
<span class="node-title">${node.title}</span>
<span class="node-subtitle">${node.subtitle}</span>
${
node.children
? `<button class="node-badge" type="button" aria-label="${node.title} ${expanded.has(node.id) ? "접기" : "펼치기"}">${expanded.has(node.id) ? "" : "+"}</button>`
: `<span class="node-badge" aria-hidden="true">↗</span>`
}
`;
if (expanded.has(node.id)) element.classList.add("is-open");
element.addEventListener("pointerdown", onNodePointerDown);
element.addEventListener("keydown", onNodeKeyDown);
const toggleButton = element.querySelector("button.node-badge");
if (toggleButton) {
toggleButton.addEventListener("pointerdown", (event) => event.stopPropagation());
toggleButton.addEventListener("click", (event) => {
event.stopPropagation();
toggleNode(node.id);
});
}
nodeLayer.appendChild(element);
}
requestAnimationFrame(() => drawConnectors(visible));
visibleCount.textContent = `${visible.size}개 노드`;
}
function nodeBox(node) {
const element = nodeLayer.querySelector(`[data-id="${node.id}"]`);
const width = element?.offsetWidth ?? (node.type === "root" ? 232 : node.type === "term" ? 176 : 184);
const height = element?.offsetHeight ?? (node.type === "root" ? 128 : 76);
return {
left: node.x,
top: node.y,
width,
height,
cx: node.x + width / 2,
cy: node.y + height / 2,
};
}
function boxEdgePoint(box, toward) {
const dx = toward.cx - box.cx;
const dy = toward.cy - box.cy;
if (dx === 0 && dy === 0) return { x: box.cx, y: box.cy };
const halfW = box.width / 2 + 8;
const halfH = box.height / 2 + 8;
const scale = Math.min(Math.abs(halfW / dx || Infinity), Math.abs(halfH / dy || Infinity));
return {
x: box.cx + dx * scale,
y: box.cy + dy * scale,
};
}
function drawConnectors(visible) {
connectorLayer.innerHTML = "";
for (const node of nodes) {
if (node.id === "root" || !visible.has(node.id)) continue;
const parent = nodeMap.get(node.parent ?? "root");
if (!parent || !visible.has(parent.id)) continue;
const parentBox = nodeBox(parent);
const childBox = nodeBox(node);
const from = boxEdgePoint(parentBox, childBox);
const to = boxEdgePoint(childBox, parentBox);
const dx = to.x - from.x;
const dy = to.y - from.y;
const horizontalPull = Math.max(120, Math.abs(dx) * 0.48);
const verticalPull = Math.max(40, Math.abs(dy) * 0.16);
const c1 = {
x: from.x + Math.sign(dx || 1) * horizontalPull,
y: from.y + Math.sign(dy || 1) * verticalPull,
};
const c2 = {
x: to.x - Math.sign(dx || 1) * horizontalPull,
y: to.y - Math.sign(dy || 1) * verticalPull,
};
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", `M ${from.x} ${from.y} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${to.x} ${to.y}`);
connectorLayer.appendChild(path);
}
}
function toggleNode(id) {
const node = nodeMap.get(id);
if (!node) return;
if (node.children) {
if (expanded.has(id)) collapseBranch(id);
else expanded.add(id);
render();
return;
}
openLightbox(node);
}
function collapseBranch(id) {
const node = nodeMap.get(id);
expanded.delete(id);
for (const childId of node.children ?? []) {
collapseBranch(childId);
}
}
function onNodePointerDown(event) {
if (event.target.closest(".node-badge")) return;
event.preventDefault();
event.stopPropagation();
const element = event.currentTarget;
const node = nodeMap.get(element.dataset.id);
element.setPointerCapture(event.pointerId);
element.classList.add("is-dragging");
activePointer = {
mode: "node",
pointerId: event.pointerId,
node,
element,
startClientX: event.clientX,
startClientY: event.clientY,
startX: node.x,
startY: node.y,
moved: false,
};
}
function onViewportPointerDown(event) {
if (event.target.closest(".node, .hud, .canvas-help, .lightbox")) return;
viewport.setPointerCapture(event.pointerId);
viewport.classList.add("is-panning");
activePointer = {
mode: "pan",
pointerId: event.pointerId,
startClientX: event.clientX,
startClientY: event.clientY,
startPanX: pan.x,
startPanY: pan.y,
moved: false,
};
}
function onPointerMove(event) {
if (!activePointer || activePointer.pointerId !== event.pointerId) return;
const dx = event.clientX - activePointer.startClientX;
const dy = event.clientY - activePointer.startClientY;
if (Math.hypot(dx, dy) > 5) activePointer.moved = true;
if (activePointer.mode === "pan") {
pan = { x: activePointer.startPanX + dx, y: activePointer.startPanY + dy };
setWorldTransform();
return;
}
activePointer.node.x = activePointer.startX + dx;
activePointer.node.y = activePointer.startY + dy;
activePointer.element.style.left = `${activePointer.node.x}px`;
activePointer.element.style.top = `${activePointer.node.y}px`;
drawConnectors(new Set(visibleNodeIds()));
}
function onPointerUp(event) {
if (!activePointer || activePointer.pointerId !== event.pointerId) return;
if (activePointer.mode === "node") {
activePointer.element.classList.remove("is-dragging");
if (!activePointer.moved && activePointer.node.image) openLightbox(activePointer.node);
}
viewport.classList.remove("is-panning");
activePointer = null;
}
function onNodeKeyDown(event) {
const node = nodeMap.get(event.currentTarget.dataset.id);
if (!node?.image || (event.key !== "Enter" && event.key !== " ")) return;
event.preventDefault();
openLightbox(node);
}
function openLightbox(node) {
resetZoom();
lightboxImage.src = encodeURI(node.image);
lightboxImage.alt = `${node.title} 인포그래픽`;
lightboxCaption.textContent = node.title;
lightboxCategory.textContent = node.category;
lightboxFocus.textContent = node.focus;
lightbox.hidden = false;
}
function closeLightbox() {
setFullscreenMode(false);
lightbox.hidden = true;
}
function setFullscreenMode(enabled) {
lightbox.classList.toggle("is-fullscreen", enabled);
fullscreenToggle.textContent = enabled ? "전체화면 종료" : "전체화면";
}
function toggleFullscreen() {
setFullscreenMode(!lightbox.classList.contains("is-fullscreen"));
resetZoom();
}
function clampZoom(value) {
return Math.min(5, Math.max(0.5, value));
}
function applyZoom() {
zoomState.scale = clampZoom(zoomState.scale);
lightboxImage.style.transform = `translate(${zoomState.x}px, ${zoomState.y}px) scale(${zoomState.scale})`;
zoomLevel.textContent = `${Math.round(zoomState.scale * 100)}%`;
}
function resetZoom() {
zoomState = { scale: 1, x: 0, y: 0, dragging: false, pointerId: null, startX: 0, startY: 0, startOffsetX: 0, startOffsetY: 0 };
zoomStage?.classList.remove("is-dragging");
if (lightboxImage) applyZoom();
}
function setZoom(nextScale) {
const previous = zoomState.scale;
zoomState.scale = clampZoom(nextScale);
if (zoomState.scale <= 1 && !lightbox.classList.contains("is-fullscreen")) {
zoomState.x = 0;
zoomState.y = 0;
} else if (previous <= 1) {
zoomState.x = 0;
zoomState.y = 0;
}
applyZoom();
}
function onZoomWheel(event) {
event.preventDefault();
const direction = event.deltaY > 0 ? -1 : 1;
setZoom(zoomState.scale + direction * 0.15);
}
function onZoomPointerDown(event) {
const canPanImage = zoomState.scale > 1 || lightbox.classList.contains("is-fullscreen");
if (!canPanImage || event.button !== 0) return;
event.preventDefault();
zoomStage.setPointerCapture(event.pointerId);
zoomStage.classList.add("is-dragging");
zoomState.dragging = true;
zoomState.pointerId = event.pointerId;
zoomState.startX = event.clientX;
zoomState.startY = event.clientY;
zoomState.startOffsetX = zoomState.x;
zoomState.startOffsetY = zoomState.y;
}
function onZoomPointerMove(event) {
if (!zoomState.dragging || zoomState.pointerId !== event.pointerId) return;
zoomState.x = zoomState.startOffsetX + event.clientX - zoomState.startX;
zoomState.y = zoomState.startOffsetY + event.clientY - zoomState.startY;
applyZoom();
}
function onZoomPointerUp(event) {
if (!zoomState.dragging || zoomState.pointerId !== event.pointerId) return;
zoomState.dragging = false;
zoomState.pointerId = null;
zoomStage.classList.remove("is-dragging");
}
function resetView() {
pan = { x: window.innerWidth / 2 - 1136, y: window.innerHeight / 2 - 764 };
setWorldTransform();
}
function foldAll() {
expanded.clear();
render();
}
viewport.addEventListener("pointerdown", onViewportPointerDown);
window.addEventListener("pointermove", onPointerMove);
window.addEventListener("pointerup", onPointerUp);
window.addEventListener("pointercancel", onPointerUp);
document.querySelector("#resetView").addEventListener("click", resetView);
document.querySelector("#foldAll").addEventListener("click", foldAll);
for (const button of document.querySelectorAll(".map-tab")) {
button.addEventListener("click", () => switchMap(button.dataset.map));
}
document.querySelector(".lightbox-backdrop").addEventListener("click", closeLightbox);
document.querySelector("#closeLightbox").addEventListener("click", closeLightbox);
zoomInButton.addEventListener("click", () => setZoom(zoomState.scale + 0.25));
zoomOutButton.addEventListener("click", () => setZoom(zoomState.scale - 0.25));
zoomResetButton.addEventListener("click", resetZoom);
fullscreenToggle.addEventListener("click", toggleFullscreen);
zoomStage.addEventListener("wheel", onZoomWheel, { passive: false });
zoomStage.addEventListener("dblclick", () => setZoom(zoomState.scale > 1 ? 1 : 2));
zoomStage.addEventListener("pointerdown", onZoomPointerDown);
zoomStage.addEventListener("pointermove", onZoomPointerMove);
zoomStage.addEventListener("pointerup", onZoomPointerUp);
zoomStage.addEventListener("pointercancel", onZoomPointerUp);
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") closeLightbox();
if (lightbox.hidden) return;
if (event.key === "+" || event.key === "=") setZoom(zoomState.scale + 0.25);
if (event.key === "-") setZoom(zoomState.scale - 0.25);
if (event.key === "0") resetZoom();
if (event.key.toLowerCase() === "f") toggleFullscreen();
});
window.addEventListener("resize", resetView);
setWorldTransform();
syncMapChrome();
render();