김욱영
김욱영
Moderator
🌿 뉴비 파트너

서지관리 시스템(북엔즈)에서 메모를 작성하고, 단어장 앱(ANKI)에서 암기하기

이슈

논문을 관리하는 시스템의 가장 큰 문제점은 논문을 저장하고 PDF를 관리하는 앱(북엔즈, 조테로)와 논문에 대한 내용을 정리하는 앱(노션, 옵시디언)이 분리된다는 점이다.

그리고 이러한 서비스가 파편화되어 있을 수록 발생하는 또 다른 문제는, 정리한 논문을 다시는 살펴보지 않는다는 점이다.

다시 살펴보지 않은 메모는 필요 없다. 메모의 목적은 다시 살펴보고, 처음부터 시작하지 않기 위함이다.

이걸 해결하기 위해서는 2가지 문제를 해결해야 했다.

  1. 논문을 관리하는 시스템에서 메모를 해서, 메모가 파편화 되는 문제를 해결할 것

  2. 작성한 메모를 다시 살펴보는 구조를 만들 것

진행

논문을 관리하는 시스템 중 북엔즈(bookends)라는 서비스가 눈에 띄었다. PDF를 관리하기 적합해보였고, 메모를 작성하는 기능도 적게나마 있었다.

문장을 다시 암기하는 것을 위해서는 단어앱을 사용하는 것이 좋다고 판단했는데, 단어앱 중 자율성이 높은 ANKI를 사용하기로 했다.

즉, 북엔즈에서 메모를 작성하면 ANKI에서 메모를 암기할 수 있게 구조를 짜는 것이다. 이것이 꽤나 많은 문제를 해결한다고 생각했다.

기획

맥에는 script editor 라는 기능이 있는데, 맥에서 작동되는 앱들을 자동화할 수 있는 기능이다. (구글 앱스크립트와 유사하다.) 이 기능을 이용하여 Bookends를 ANKI와 연동하기로 했다. 챗GPT와 대화하면서 수식을 구체화 해나갔다.

간단한 기획은 이와 같다.

  1. 맥의 script editor를 이용, Bookends의 데이터를 가지고 오는 수식을 만든다.

    1. 데이터는 제목, 메모를 가지고 온다.

  2. 노트의 있는 내용을 #을 단위로 분할한다.

    1. #으로 분할한 내용도 같은 제목, 메모를 상속하게 만든다.

  3. ANKI Connect를 이용해 분할한 메모를 ANKI에 추가한다. 이 때 중복된 내용을 무시하도록 구성한다.

챗GPT와 구체화 한 코드:

(function () {
    var app = Application("Bookends");
    app.includeStandardAdditions = true;

    if (!app.running()) {
        console.log("Bookends가 실행 중이 아닙니다. Bookends를 실행합니다...");
        app.launch();
        delay(3);
    }

    function getPublicationsByGroup(groupName) {
        var frontWindow = app.libraryWindows[0];
        if (!frontWindow) {
            console.log("라이브러리 창이 열려 있지 않습니다.");
            return [];
        }

        var groups = frontWindow.groupItems().filter(function (group) {
            return group.name() === groupName;
        });

        if (groups.length === 0) {
            console.log(`그룹 '${groupName}'을(를) 찾을 수 없습니다.`);
            return [];
        }

        return groups[0].publicationItems().map(function (pub) {
            return {
                title: pub.title() || "",
                authors: pub.authors() || "",
                id: pub.id(),
                notes: pub.notes() || ""
            };
        });
    }

    function ankiConnectInvoke(action, params) {
        var request = {
            action: action,
            version: 6,
            params: params
        };
        var url = "http://127.0.0.1:8765";

        var app = Application.currentApplication();
        app.includeStandardAdditions = true;

        var json = JSON.stringify(request);
        var quotedJson = "'" + json.replace(/'/g, "'\\''") + "'";

        var cmd = "echo " + quotedJson + " | curl -s -X POST -H 'Content-Type: application/json' -d @- " + url;

        try {
            var response = app.doShellScript(cmd);
            var parsed = JSON.parse(response);
            if (parsed.error) {
                console.log("AnkiConnect 오류:", parsed.error);
                return { error: parsed.error };
            }
            return parsed.result;
        } catch (e) {
            console.log("쉘 스크립트 오류:", e.toString());
            return { error: e.toString() };
        }
    }

    // 기존 Anki 노트의 Front 필드를 가져오는 함수
    function getExistingAnkiFronts(deckName) {
        var query = `deck:"${deckName}"`;
        var noteIds = ankiConnectInvoke("findNotes", { query: query });
        if (noteIds.error) {
            console.log("AnkiConnect 오류 (findNotes):", noteIds.error);
            return new Set();
        }

        if (noteIds.length === 0) {
            return new Set();
        }

        var notesInfo = ankiConnectInvoke("notesInfo", { notes: noteIds });
        if (notesInfo.error) {
            console.log("AnkiConnect 오류 (notesInfo):", notesInfo.error);
            return new Set();
        }

        var fronts = notesInfo.map(note => note.fields.Front.value);
        return new Set(fronts);
    }

    try {
        var publications = getPublicationsByGroup("Done");
        console.log("Publications:", publications);

        var deckName = "bookends";
        var modelName = "Basic";
        var notesToAdd = [];

        // 기존 Anki 노트의 Front 필드를 가져와 Set으로 저장
        var existingFronts = getExistingAnkiFronts(deckName);
        console.log("기존 Anki Fronts 수:", existingFronts.size);

        publications.forEach(function (pub) {
            var parts = pub.notes.split("#").map(part => part.trim()).filter(Boolean);
            parts.forEach(function (note) {
                var front = note.replace(/\n/g, "<br>");

                // 중복 확인: Front 필드가 기존에 존재하지 않는 경우에만 추가
                if (existingFronts.has(front)) {
                    console.log("중복된 노트 발견. 건너뜁니다:", front);
                    return;
                }

                // 저자와 제목을 링크로 감싸기 위해 <a> 태그 사용
                var authors = pub.authors || "";
                var title = pub.title || "";
                var link = `bookends://sonnysoftware.com/ref/Library/${pub.id}`;
                var linkedText = `<a href="${link}">(${authors}, ${title})</a>`;
                var back = linkedText.replace(/\n/g, "<br>");

                notesToAdd.push({
                    deckName: deckName,
                    modelName: modelName,
                    fields: { Front: front, Back: back },
                    options: { allowDuplicate: false }
                });
            });
        });

        if (notesToAdd.length > 0) {
            var result = ankiConnectInvoke("addNotes", { notes: notesToAdd });
            if (result.error) {
                console.log("AnkiConnect 오류 (addNotes):", result.error);
            } else {
                console.log("추가된 노트 수:", result);
            }
            return result;
        } else {
            console.log("추가할 새로운 노트가 없습니다.");
            return "추가할 새로운 노트가 없습니다.";
        }
    } catch (e) {
        console.log("예상치 못한 오류:", e.toString());
        return { error: e.toString() };
    }
})();

완성

Mac OS X용 한국어 텍스트 편집기

코드를 실행하면, 북엔즈에 있는 논문에서 메모를 뽑아와, ANKi 앱에 자동으로 추가해 준다. 이 기능을 이용해, 단어 앱을 이용해 정리한 메모를, 논문을 매일 매일 살펴볼 수 있는 기회를 만들어준다.

내가 작성한 메모를 다시 살펴볼 수 있게 도와주는 시스템을 만들었다. 이가 논문 관리 시스템이 가지고 있던 큰 문제를 해결했다고 생각한다.

4

👉 이 게시글도 읽어보세요