현재 위키 편집 화면의 기능과 구현 위치를 정리한다.
app/views/Wiki/edit.scala.html - 편집 화면 마크업, 툴바, AhaWikiEditConfig (CodeMirror 스타일은 app/assets/edit.less)public/js/Wiki/edit.page.js - 편집 화면의 대부분의 클라이언트 동작public/js/AhaWiki.Editor.js - 입력 보조 규칙과 CodeMirror/textarea 공통 keydown 처리public/js/AhaWiki.CodeMirror.AhaMark.js - AhaMark용 CodeMirror modepublic/js/Wiki/edit.hotkeyAlt.js - 편집 화면 Alt 단축키 dispatcherpublic/js/Wiki/edit.divider.js - editor/preview 컬럼 divider drag resizepublic/js/AhaWiki.WikipediaToSchema.js - Wikipedia InfoBox를 Schema 블록으로 변환Word Wrap 체크박스는 CodeMirror의 lineWrapping 옵션을 토글한다.CodeMirror 5를 사용하며 mode 이름은 ahamark이다.
현재 하이라이트 대상:
=, ==, ..., ======----[[[#!...]]]와 블록 내부#!...[Page], [Page Name], [Page|Alias], URL[[Macro(...)]]{{name}}[YYYY-MM-DD], [YYYY-MM-DD|YYYY-MM-DDTHH:mm:ss]inline Copied!!Style class:
ahamark-interpreter - [[[#!...]]] interpreter blockahamark-interpreter-shebang - #!... directive lineAhaWiki.Editor는 CodeMirror와 textarea 모두에 같은 입력 규칙을 적용한다.
(), {}, [], "", '', `Backspace는 커서 양쪽의 matching pair 를 지운다(사이에 선택 영역이 있으면 감싼 괄호만 벗긴다). [[[ 바로 뒤에서 누르면 \n]]] wrapper 를 통째로 지운다.Enter는 현재 줄이 list이면 list prefix를 다음 줄로 이어준다.Tab은 list line에서는 indent/outdent, 일반 위치에서는 tab 문자를 삽입한다.--[ 입력 흐름에서는 현재 날짜/시간 link를 만든다 — [YYYY-MM-DD|YYYY-MM-DDTHH:mm:ss], 날짜 페이지로 가고 시각이 별칭이다. 2026-09-14 전에는 둘 사이가 공백이었는데, 지금 문법에서 공백은 이름의 일부다.[[ / [[[ 주변 입력은 interpreter/block wrapper 작성을 돕는다.Process로 들어오는 경우를 보정한다.자동완성 popup은 CodeMirror cursor 위치 또는 textarea 위치 기준으로 표시한다.
지원 context:
[[Ma - macro name[Pa 또는 ["Pa - page name[[[#!Ta - interpreter name[[[#!table ts - table option[[[#!Schema Pe - schema.org class[[[#!Schema Person 블록 내부 property line - schema.org propertySource API:
GET /api/macroNamesGET /api/interpreterNamesGET /api/pageNamesGET /api/schemaClassNamesGET /api/schemaPropertyNames동작:
ArrowUp / ArrowDown으로 선택을 이동한다.Enter / Tab으로 선택 항목을 적용한다.Escape, blur, editor 외부 click/scroll에서 popup을 닫는다.MacroName()을 삽입하고 cursor를 괄호 안에 둔다.POST /preview로 미리보기를 갱신한다.name, text, lineStart, lineEnd를 함께 보낸다..previewPane에 넣은 뒤 Mermaid 초기화를 수행한다..pageOverflow class를 붙인다.data-line-start / data-line-end를 이용해 대응 위치로 scroll한다.previewSyncBlink animation을 적용한다.편집 URL에 lineStart와 lineEnd가 있으면 해당 line range만 editor에 넣는다.
Switch to … 링크로 raw editor 와 오간다.편집 화면을 열 때 본문이 #!gantt 블록이면 Gantt 편집기를, partial edit 이거나 본문 전체가 #!table 블록이면 Table Editor 를 자동으로 연다. partial edit 의 Switch to … 링크로 raw editor 와 오간다.
tsv, csv, tsv 1, tsv 1 1, tsv 1 1 tablesorter 를 제안하는 것은 raw editor 의 [[[#!table 뒤다(아래 자동완성 참고)tablesorter 는 아무 일도 하지 않는다. InterpreterTable.Shebang.getClasses 가 모든 표에 tablesorter 클래스를 붙이고 .distinct 로 중복을 지우므로, 적어도 안 적어도 결과가 같다. 정렬을 켜는 스위치처럼 보이지만 아니다 — 목록에서 뺄지는 정하지 않았다. --2026-08-31주의:
편집 화면에서 현재 page의 attachment를 조회하고 관리한다.
GET /api/pageAttachments로 기존 첨부 목록을 읽는다.POST /api/uploadAttachment에 multipart upload한다.POST /api/uploadClipboardImage에 multipart upload한다.[[Attachment(...)]] macro를 cursor 위치에 삽입한다.POST /api/deleteAttachment를 호출한다.integrityStatus가 S3_ONLY인 항목은 insert/delete action을 숨긴다.InfoBox 버튼은 Wikipedia page name을 입력받아 en, ko Wikipedia에서 infobox를 조회한다.
[[[#!Schema ... ]]] block으로 삽입한다.# API URL과 sameAs 값을 함께 넣는다.# 라벨\t값 주석으로 남는다. 매핑이 값을 줄이더라도 원문은 문서에 남는다.[[[#!Schema 뒤의 인자가 클래스인데, 가져오기는 이 자리를 늘 비워 두었고 Corporation·WebSite·SoftwareApplication 은 사람이 나중에 쳐 넣었다. 그 일을 Wikidata 가 대신한다.
문서의 Wikidata 항목이 무엇의 인스턴스인지(P31) 말하고, 그 클래스 상당수가 schema.org 대응 클래스(P1709 equivalent class, P2888 exact match)를 달고 있다. 극장(Q24354)은 PerformingArtsTheater 를 직접 가리킨다. 그 직접 대응만 쓴다.
PerformingArtsTheater, GitHub → WebApplication WebSite, 트루먼 쇼 → Movie.Q494895)은 있지만 P31 이 「마천루」라 대응이 없다 → 인자를 비운다.P279)로 한 단계 올라가는 것은 하지 않는다. 문서 열 개로 해 보니 Git 이 Service, Python 이 잡음 셋이었다. 틀린 클래스는 맞아 보이면서 틀리므로 빈 것보다 나쁘다.출처는 # Wikidata\thttps://www.wikidata.org/wiki/Q… 주석으로 블록 안에 남는다. 언어판이 달라도 항목은 하나라, 인포박스가 잡힌 언어판을 차례로 물어 항목이 나오는 첫 언어판에서 멈춘다.
실제 API 로 확인한 결과(2026-09-10): Seoul → City, 봉준호 → Person, Breaking Bad → TVSeries, 기생충 → Movie. PHP 와 Amazon Web Services 는 항목은 찾지만 P31 의 클래스에 직접 대응이 없어 인자가 빈다. 브라우저 밖(Node)에서 재현할 때는 User-Agent 를 줘야 한다 — Wikimedia API 는 UA 가 없으면 403, node 면 429 를 답하고, 스크립트는 그것을 조용히 빈 결과로 삼킨다.
클래스가 둘 이상이면 인자도 둘 이상이다. InterpreterSchema 는 이제 인자를 전부 읽는다(2026-09-04 이전에는 첫 것만 읽었다) — JSON-LD @type 은 배열, RDFa typeof 는 공백 구분, 색인에는 양쪽에 오른다.
소유는 매핑하지 않는다. schema.org에는 「이 장소를 X가 소유한다」에 해당하는 property가 없다. owns는 그 반대로, domain이 Person·Organization이고 range가 Product다. 여기 연결하면 맞아 보이면서 뜻이 뒤집히므로 Owner·소유·소유주·소유기관은 라벨 그대로 통과시킨다.
yearBuilt는 연도만 남는다. range가 Number라 1978-04-14를 담을 수 없다. 어휘 전체에서 range가 Date·DateTime이면서 domain에 Place 계열이 있는 property는 하나도 없다 — 건물의 완공일을 온전히 담을 자리가 schema.org에 없다. 월·일은 위의 # 주석에 남는다.
반면 조직은 다르다. 설립일은 foundingDate(domain Organization, range Date)로 가므로 1963-01-01이 그대로 남는다.
좌표는 latitude·longitude 두 줄로 쪼갠다. geo의 range는 GeoCoordinates 객체라 문자열 한 줄로는 맞지 않는다. 읽지 못한 값은 geo인 채로 둔다 — 지어내지 않는다.
37°33′36″N)와 한국어(북위 37° 34′ 21″)를 모두 읽는다. 방위가 숫자 뒤·앞 어디에 오든 된다.… / 37.5725; 126.9756)으로 넘어간다.좌표와 지리 좌표계를 모두 본다. ko.wikipedia 건물 인포박스는 후자를 쓴다.한 칸이 여러 값으로 쪼개져 온다. Wikipedia가 April 11, 1931; 95 years ago (…)를 쉼표에서 나누므로 값 하나씩 보는 변환은 yearBuilt를 April 11\t1931로 만든다. 그래서 변환은 행 전체를 본다.
CalculatedSchemaOrg 의 (클래스, 속성) 조합 전부를 배포된 어휘 파일과 대조했다. 550여 문서.
고친 것
writer 137건 → author. schema.org 에 writer 는 없다. 가져오기가 만든 게 아니라 Writer 라벨이 미매핑으로 통과한 것을 사람이 소문자로 바꾼 것이었다. 라벨은 이제 author 로 간다.politicalParty 4건 → memberOf(Person → Organization). 정당·Political party 라벨도 같이.Cognag → Cognac(Rémy Martin). 커스텀 클래스지만 철자.SoftwareSourceCode ↔ SoftwareApplication — 한쪽 속성이 다른 쪽 블록에 있던 것. 클래스를 바꾸거나 속성을 옮기지 않고 짝 클래스를 인자에 더했다 (20건). 라이브러리는 코드이면서 쓰는 물건이고, 여섯 문서(Git·bash·ncdu…)는 이미 그렇게 적혀 있었다. scripts/add-paired-class.mjs. 소프트웨어 문서 47개가 전부 정합해졌다.SoftwareApplication 블록과 코드용 SoftwareSourceCode 블록을 따로 두고 있었고, 두 번째의 applicationCategory 는 첫 번째와 값까지 같은 사본이었다. 클래스를 넓히면 한 문서가 애플리케이션을 두 번 선언한다. 그래서 그 줄을 지웠다.두기로 한 것 — 대안이 없거나, 고치면 더 틀려진다.
TVSeries.duration 29건. duration 은 에피소드의 속성이라 시리즈에는 안 맞지만, 시리즈 길이를 담을 자리가 schema.org 에 없다. numberOfEpisodes 는 다른 사실이다.ComputerLanguage.datePublished 16건, designer 15건. ComputerLanguage 는 Intangible 이라 CreativeWork 속성이 붙지 않고, designer 는 속성 자체가 없다. 언어의 설계자·발표일을 담을 자리가 없다.Cinematography 4건, volume 3건 — 촬영감독·권 수에 해당하는 속성이 없다. 라벨 그대로 통과한 정직한 상태다.Corporation.industry 2건 — industry 의 domain 은 JobPosting 이다. 기업의 업종을 담을 속성이 없다.Standard·Poem·Whiskey·Cognac — 커스텀 클래스. 색인의 「Custom」 절이 이를 위한 자리다.logo 가 소프트웨어 블록에 있는 것 — domain 은 Organization·Brand·Product 이지만 InterpreterSchema.imageKeys 가 클래스와 무관하게 그림으로 그린다. 표시 관례다.검산은 파생 테이블이 아니라 페이지를 다시 읽어서 한다. 테이블은 재계산이 밀려, writer 를 전부 바꾼 직후에도 121건이 남았다고 답했다.
저장이 재계산을 부른다. 그래서 고친 문서는 색인도 따라온다. 반대로 고칠 게 없던 문서는 안 따라온다 — Git·bash·ncdu·ClamAv·Mermaid·uBlock Origin 은 원래 두 클래스를 적고 있어 편집하지 않았고, 색인은 아직 한 클래스로만 보여 준다. 렌더링(typeof·JSON-LD @type)은 이미 둘 다 맞다. 다음 편집 때 맞춰진다. POST /api/Admin/Site/:seq/Calculate?pageName=... 로 한 문서만 재계산할 수 있지만 API 키로는 403 이고 세션 관리자여야 한다.
comment는 필수 입력이다.minorEdit 체크박스를 지원한다.ahawiki-recaptcha-site-key meta 가 실리고, 그때만 서버가 token 없는 저장을 거절한다. 키 하나만 있으면 끈 것과 같다 — 하나만으로 켜면 token 을 받을 길 없이 저장이 전부 거절된다.AhaWiki.ReCaptcha.js 하나이고, InterpreterKanban 도 이것을 쓴다. Google 의 스크립트는 처음 필요할 때 읽는다(편집 화면은 열 때 미리).saveSenderId를 hidden field에 넣어 WebSocket page update broadcast에서 자기 tab의 저장 알림을 제외한다.오류 처리:
403 - permission denied403 reCAPTCHA token is required - reCAPTCHA 가 켜져 있는데 token 이 없다. 2026-09-13 전에는 빈 token 이면 검사를 건너뛰었다 — 어느 클라이언트든 token 을 빼면 검사 없이 저장할 수 있었고, Kanban 이 그렇게 저장하고 있었다.403 reCAPTCHA failed - Google 이 token 을 받아들이지 않았다. 만료되었거나 이미 쓴 token 도 여기로 온다.409 - revision conflict400 body == latestText - 변경 없는 저장편집 화면에서는 Alt와 다음 key 조합으로 toolbar action을 실행한다. edit.hotkeyAlt.js 가 .edit 안에서 kbd[data-hotkey-alt=키] 를 찾아 클릭한다.
2026-09-10 까지는 문서 전체에서 찾았다. 상단 내비게이션(_base.scala.html)의 S·R·W·H·B·E 힌트도 같은 속성을 쓰므로, Alt S 는 저장과 함께 검색 창을 열고 Alt R 은 Rename 과 Random(/r) 두 링크를 다 따라갔으며, 아래 목록에 없는 Alt H·Alt B·Alt E 는 History·Blame·Edit 링크로 이동해 편집 중인 내용을 잃었다. test/manual/editor-hotkeys/ 가 브라우저에서 키를 눌러 이것을 재현하고, 고쳐진 것을 확인한다.
Alt W - Word WrapAlt I - InfoBoxAlt A - AttachAlt C - revision comment input focusAlt M - Minor editAlt S - SaveAlt R - RenameAlt T - Delete문단마다 붙는 편집 링크는 logics.wikis.PartialEdit 한 곳에서 만듭니다.
revision — 지금 보고 있는 리비전. 쿼리스트링이 우선입니다. 옛 리비전을 보는 중이라면 편집 링크도 그 리비전을 가리켜야 하기 때문입니다. 링크에 리비전을 실어 두는 덕에, 문단을 열어 둔 사이 페이지가 바뀌면 저장 시점에 알아챌 수 있습니다.editUrl — /w/{페이지}?action=edit&revision=N&lineStart=N&lineEnd=N이 둘이 인터프리터 블록용(ExtractConvertInjectInterpreter)과 제목용(InterpreterWiki)에 각각 한 벌씩, 글자 하나 다르지 않게 있었습니다. 두 렌더러는 페이지를 나누는 방식이 달라서 따로 있는 게 맞지만, **링크 모양은 달라질 수 없는 것** 입니다.
편집 툴바(toolbarLeft)에 서식 버튼을 둔다 — 굵게·기울임·밑줄·취소선·인라인 코드·제목·불릿 목록·링크. 선택을 감싸거나(굵게 → '''…''') 줄 앞에 붙인다(제목 == , 목록 * ). 빈 선택이면 커서를 프리픽스와 서픽스 사이에 둔다.
AhaWiki.Format.wrap / prefixLines(public/js/AhaWiki.Format.js)다 — «전체 텍스트 + 선택»을 받아 «최소 편집(범위·대체·새 선택)»을 돌려준다. DOM 을 모르므로 단위테스트되고(test/ahawiki.format.test.mjs), CodeMirror·textarea 양쪽이 같은 결과를 적용한다.replaceRange(undo 보존), textarea 는 값 치환. 배선은 edit.page.js 의 .formatButton 클릭 핸들러. 버튼은 data-format 으로 동작을 고른다.ToDo 의 «WYSIWYG editing» 설계안이다. 접근 3(서식 툴바)의 첫걸음은 2026-09-16 에 만들어 배포했다 — 위 «서식 툴바» 절. 지금 편집기는 CodeMirror 소스 + 라이브 미리보기 + (표·간트) 구조 편집기 + 서식 툴바다. 진짜 WYSIWYG(접근 2 하이브리드)은 아래 열린 결정 을 소유자가 정한 뒤 단계별로 만든다.
렌더는 AhaMark → HTML 단방향이다. WYSIWYG 은 반대로 «편집한 리치 문서 → AhaMark» 직렬화가 필요하고, 그게 어렵다.
[[[#!Table/Vim/Kanban/Map/Schema/Gantt/Markdown …]]]), 매크로([[…]]), 커스텀 링크 문법([a b]·따옴표), 디렉티브(#!var …), 주석 — 대부분 리치로 «직접» 편집할 수 없고 그대로 보존돼야 한다.[[[블록]]] 과 [[매크로]] 는 «아톰 노드»로 렌더 결과만 보여 주고, 편집은 기존 표·간트 편집기나 소스 모달로 연다. 지금의 표·간트 편집기 패턴을 그대로 넓히는 방향이다.'''…''' 삽입 등) + 블록별 편집기 추가. 진짜 WYSIWYG 은 아니지만 노력·위험 최소이고 UX 이득의 상당수를 준다.권장: 2(하이브리드), 단계별. 3 은 «꼭 진짜 WYSIWYG 이 아니어도 되면» 의 저비용 대안.
saveSenderId).Similar pages by cosine similarity. Words after page name are term frequency.