Watch at Ease세상을 읽고, 일상의 답을 찾다.

기술 · 실용 가이드

학습 기록 없이 Anki 필터 덱 템플릿 공유하기

기존 검색 조건과 옵션을 별도의 JSON 파일로 내보낸 뒤 다른 Anki 컬렉션에서 다시 만드세요. 전체 코드가 제공되는 로컬 도우미 스크립트가 새 필터 덱을 만들기 전에 파일 전체를 점검합니다.

두 개의 별도 전달 경로를 보여 줍니다. 하나는 선택 사항인 원래 일반 덱의 APKG로 Include Scheduling을 끈 것이고, 다른 하나는 이름, 하나 또는 두 개의 검색 조건, 제한 수, 정렬 순서, 필터 옵션만 담은 JSON입니다. 대상 컬렉션은 조건에 맞는 자체 로컬 카드로 필터 덱을 만듭니다.
직접 제작한 개념 설명 일러스트입니다. 선택 사항인 원래 일반 덱의 APKG를 Include Scheduling을 끄고 내보내기 전에, 원본의 관련 필터 덱을 Empty로 비우세요. 별도의 JSON은 카드나 복습 로그 없이 설정을 전달하며, 이를 적용하면 조건에 맞는 대상 카드로 필터 덱이 만들어집니다. Watch at Ease

다른 사람이 재사용했으면 하는 Anki 필터 덱 모음이 있다면, 카드와 별도로 덱의 구성 정보를 공유하세요. 아래 도우미 스크립트는 이름, 검색 조건, 제한 수, 정렬 순서, 필터 덱 옵션을 JSON으로 내보냅니다. 받는 사람은 파일을 살펴본 뒤 자신의 컬렉션에서 조건에 맞는 카드로 여러 필터 덱을 다시 만들 수 있습니다. JSON을 통해 보낸 사람의 카드 레코드나 복습 로그를 받지는 않습니다.

이 절차는 Anki Desktop의 고급 사용법이며, 일회용 컬렉션에서 실제 Anki 26.09.3 Python/Rust 백엔드로 검증했습니다. apply=True로 설정을 가져오면 새 필터 덱이 즉시 만들어지고, 검색에 맞는 로컬 카드가 그 안으로 이동합니다. 먼저 사전 점검 보고서를 읽고 올바른 프로필인지 확인한 다음, 변경을 적용하기 전에 최신 백업을 보관하세요. 아래에 도우미 전체 코드가 제공되며 Python을 별도로 설치할 필요는 없습니다.

1. 실제로 필요한 전달 방식 선택하기

상대방이 학습 일정 데이터를 포함한 컬렉션 전체를 별도 프로필에서 사용하려는 경우에는 기존 방식인 컬렉션 패키지 .colpkg가 적합합니다. 이를 가져오면 현재 컬렉션의 카드가 교체됩니다. 필터 구성 정보만 보내거나 누적된 학습 기록을 제외하려는 목적에는 맞지 않습니다.

일반 덱 패키지 .apkg는 다른 컬렉션에 카드를 추가할 수 있으며, Anki에는 학습 일정 정보를 제외하는 옵션이 있습니다. 이 패키지는 카드를 전달하는 데 유용하지만, 필터 덱의 구성 정보를 확실하게 옮기는 수단으로 여겨서는 안 됩니다. CrowdAnki도 JSON 내보내기에서 필터 덱을 명시적으로 제외합니다. 태그로 필터를 만들거나 기존 필터를 다시 구성하는 도구는 작업의 일부를 해결해 줍니다. 하지만 어느 작업도 선택한 기존 필터 덱 여러 개의 설정 전체를 옮기는 것은 아닙니다.

재사용하려는 대상이 이미 설정된 필터 덱이라면 제공된 도우미를 사용하세요. 현재의 설정 필드 일곱 개를 보존하며, 두 검색 조건, 명시적인 false와 0 값, 현재의 미리 보기 지연 시간, 이전 방식의 저장된 지연 시간 필드도 포함합니다. 이전 필드를 복사한다고 예전 스케줄러가 활성화되지는 않습니다. 일반 덱의 프리셋, FSRS 설정, 추가 기능 설정, 미디어, 카드 내용은 이 JSON 형식에 포함되지 않습니다.

파일에는 선택한 덱 이름과 검색식 원문이 그대로 들어가며, 검색식에 넣은 ID도 포함됩니다. 공유하기 전에 읽어 보고 필요하면 비공개 이름이나 용어를 제거하세요. 카드와 복습 기록이 없다고 해서 이 문자열까지 익명화되는 것은 아닙니다.

2. 전체 도우미를 저장하고 Anki Desktop에서 불러오기

다음 exec(...) 한 줄 전체를 anki_filtered_templates.py라는 UTF-8 일반 텍스트 파일로 저장하세요. 이 줄에 프로그램 전체가 들어 있습니다. 줄바꿈과 공백을 이스케이프 표현으로 보존하므로 이 페이지에서 복사해도 Python 들여쓰기가 유지됩니다. 줄을 온전히 유지하고 일반 따옴표를 둥근 모양의 문장용 따옴표로 바꾸지 마세요. 함께 제공되는 소스는 일반적인 들여쓰기 형태로 보관해도 됩니다.

exec("\"\"\"Filtered-deck\x20configuration\x20only;\x20tested\x20with\x20Anki\x2026.9.3's\x20real\x20backend.\n\nLoad\x20this\x20file\x20in\x20Anki\x20Desktop's\x20debug\x20console,\x20then\x20use\x20mw.col:\n\x20\x20\x20\x20export_filtered_templates(mw.col,\x20'/absolute/path/templates.json')\n\x20\x20\x20\x20import_filtered_templates(mw.col,\x20'/absolute/path/templates.json')\n\x20\x20\x20\x20import_filtered_templates(mw.col,\x20'/absolute/path/templates.json',\x20apply=True)\nUse\x20names=['Exact\x20name',\x20...]\x20to\x20export\x20a\x20selection.\x20None\x20exports\x20all\x20filtered\ndecks\x20in\x20name\x20order.\x20prefix\x20changes\x20destination\x20deck\x20names,\x20never\x20searches.\napply=True\x20immediately\x20builds\x20using\x20destination-local\x20cards,\x20in\x20file\x20order.\nEarlier\x20builds\x20take\x20precedence\x20for\x20overlapping\x20cards.\x20This\x20is\x20not\x20one\x20atomic\nbatch\x20or\x20one\x20promised\x20Undo.\x20Existing\x20filtered\x20decks\x20are\x20never\x20emptied\x20here.\nNo\x20source\x20deck-ID\x20metadata,\x20card/note\x20records\x20or\x20review\x20logs\x20are\x20exported.\nSearch\x20text\x20is\x20preserved\x20and\x20may\x20contain\x20source-specific\x20IDs.\n\"\"\"\nimport\x20json\nimport\x20math\nimport\x20unicodedata\nfrom\x20pathlib\x20import\x20Path\nfrom\x20anki.collection\x20import\x20Collection\nfrom\x20anki.decks\x20import\x20DeckId,\x20FilteredDeckConfig\nfrom\x20google.protobuf.json_format\x20import\x20ParseDict\n\nSCHEMA\x20=\x20'anki-filtered-templates'\nVERSION\x20=\x201\nCONFIG_KEYS\x20=\x20{'reschedule',\x20'search_terms',\x20'delays',\x20'preview_delay',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'preview_again_secs',\x20'preview_hard_secs',\x20'preview_good_secs'}\nDELAY_KEYS\x20=\x20('preview_delay',\x20'preview_again_secs',\x20'preview_hard_secs',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'preview_good_secs')\nORDER\x20=\x20FilteredDeckConfig.SearchTerm.Order\n\n\nclass\x20PartialImportError(RuntimeError):\n\x20\x20\x20\x20\"\"\"Inspect\x20.partial_report;\x20successful\x20earlier\x20creations\x20remain\x20present.\"\"\"\n\x20\x20\x20\x20def\x20__init__(self,\x20message,\x20report):\n\x20\x20\x20\x20\x20\x20\x20\x20self.partial_report\x20=\x20report\n\x20\x20\x20\x20\x20\x20\x20\x20ids\x20=\x20[row['id']\x20for\x20row\x20in\x20report['created']]\n\x20\x20\x20\x20\x20\x20\x20\x20super().__init__(f'{message}.\x20New\x20filtered\x20IDs\x20so\x20far:\x20{ids}.\x20'\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Inspect\x20exception.partial_report;\x20no\x20rollback\x20was\x20attempted.')\n\n\ndef\x20_object(value,\x20allowed,\x20required,\x20label):\n\x20\x20\x20\x20if\x20type(value)\x20is\x20not\x20dict:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}:\x20expected\x20an\x20object')\n\x20\x20\x20\x20unknown,\x20missing\x20=\x20set(value)\x20-\x20allowed,\x20required\x20-\x20set(value)\n\x20\x20\x20\x20if\x20unknown\x20or\x20missing:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}:\x20unknown\x20keys\x20{sorted(unknown)},\x20missing\x20keys\x20{sorted(missing)}')\n\n\ndef\x20_name(value):\n\x20\x20\x20\x20if\x20not\x20isinstance(value,\x20str)\x20or\x20not\x20value:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('Deck\x20name\x20must\x20be\x20a\x20nonempty\x20string')\n\x20\x20\x20\x20if\x20(any(ord(c)\x20\u003c\x2032\x20or\x20ord(c)\x20==\x20127\x20for\x20c\x20in\x20value)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20or\x20any(not\x20p\x20or\x20p\x20!=\x20p.strip()\x20for\x20p\x20in\x20value.split('::'))\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20or\x20unicodedata.normalize('NFC',\x20value)\x20!=\x20value):\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Name\x20needs\x20nonempty,\x20unpadded,\x20NFC-normalized\x20components:\x20{value!r}')\n\x20\x20\x20\x20return\x20value\n\n\ndef\x20_uint(value,\x20label):\n\x20\x20\x20\x20if\x20type(value)\x20is\x20not\x20int\x20or\x20not\x200\x20\u003c=\x20value\x20\u003c=\x204294967295:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}:\x20expected\x20an\x20integer\x20from\x200\x20to\x204294967295')\n\n\ndef\x20_config_json(config):\n\x20\x20\x20\x20#\x20Explicit\x20defaults\x20keep\x20false\x20and\x20zero\x20visible;\x20no\x20IDs\x20or\x20scheduling\x20data.\n\x20\x20\x20\x20result\x20=\x20{'reschedule':\x20config.reschedule,\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'search_terms':\x20[{'search':\x20t.search,\x20'limit':\x20t.limit,\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'order':\x20ORDER.Name(t.order)}\x20for\x20t\x20in\x20config.search_terms],\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'delays':\x20list(config.delays)}\n\x20\x20\x20\x20result.update({key:\x20getattr(config,\x20key)\x20for\x20key\x20in\x20DELAY_KEYS})\n\x20\x20\x20\x20return\x20result\n\n\ndef\x20_config(col,\x20value,\x20label):\n\x20\x20\x20\x20_object(value,\x20CONFIG_KEYS,\x20{'search_terms'},\x20label)\n\x20\x20\x20\x20if\x20'reschedule'\x20in\x20value\x20and\x20type(value['reschedule'])\x20is\x20not\x20bool:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}.reschedule:\x20expected\x20true\x20or\x20false')\n\x20\x20\x20\x20for\x20key\x20in\x20DELAY_KEYS:\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20key\x20in\x20value:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20_uint(value[key],\x20f'{label}.{key}')\n\x20\x20\x20\x20delays\x20=\x20value.get('delays',\x20[])\n\x20\x20\x20\x20if\x20type(delays)\x20is\x20not\x20list\x20or\x20any(type(v)\x20not\x20in\x20(int,\x20float)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20or\x20not\x20math.isfinite(v)\x20or\x20v\x20\u003c\x200\x20for\x20v\x20in\x20delays):\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}.delays:\x20expected\x20finite\x20nonnegative\x20numbers')\n\x20\x20\x20\x20terms\x20=\x20value['search_terms']\n\x20\x20\x20\x20if\x20type(terms)\x20is\x20not\x20list\x20or\x20not\x201\x20\u003c=\x20len(terms)\x20\u003c=\x202:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}:\x20requires\x20one\x20or\x20two\x20search\x20terms')\n\x20\x20\x20\x20normalized\x20=\x20[]\n\x20\x20\x20\x20for\x20i,\x20term\x20in\x20enumerate(terms):\n\x20\x20\x20\x20\x20\x20\x20\x20part\x20=\x20f'{label}.search_terms[{i}]'\n\x20\x20\x20\x20\x20\x20\x20\x20_object(term,\x20{'search',\x20'limit',\x20'order'},\x20{'search'},\x20part)\n\x20\x20\x20\x20\x20\x20\x20\x20search\x20=\x20term['search']\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20not\x20isinstance(search,\x20str)\x20or\x20not\x20search.strip():\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{part}:\x20blank\x20search\x20rejected;\x20use\x20explicit\x20deck:*\x20for\x20all\x20decks')\n\x20\x20\x20\x20\x20\x20\x20\x20_uint(term.get('limit',\x200),\x20part\x20+\x20'.limit')\n\x20\x20\x20\x20\x20\x20\x20\x20order\x20=\x20term.get('order',\x200)\n\x20\x20\x20\x20\x20\x20\x20\x20try:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20type(order)\x20is\x20str:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20ORDER.Value(order)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20elif\x20type(order)\x20is\x20int:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20ORDER.Name(order)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20else:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('not\x20an\x20enum\x20name\x20or\x20integer')\n\x20\x20\x20\x20\x20\x20\x20\x20except\x20(ValueError,\x20KeyError)\x20as\x20error:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{part}:\x20unknown\x20order\x20{order!r}')\x20from\x20error\n\x20\x20\x20\x20\x20\x20\x20\x20try:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20normalized.append(col.build_search_string(search))\n\x20\x20\x20\x20\x20\x20\x20\x20except\x20Exception\x20as\x20error:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{part}:\x20invalid\x20search:\x20{error}')\x20from\x20error\n\x20\x20\x20\x20result\x20=\x20FilteredDeckConfig()\n\x20\x20\x20\x20result.Clear()\n\x20\x20\x20\x20try:\n\x20\x20\x20\x20\x20\x20\x20\x20ParseDict(value,\x20result,\x20ignore_unknown_fields=False)\n\x20\x20\x20\x20except\x20Exception\x20as\x20error:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}:\x20invalid\x20configuration:\x20{error}')\x20from\x20error\n\x20\x20\x20\x20return\x20result,\x20normalized\n\n\ndef\x20_manifest(col,\x20value):\n\x20\x20\x20\x20_object(value,\x20{'schema',\x20'version',\x20'templates'},\x20{'schema',\x20'version',\x20'templates'},\x20'manifest')\n\x20\x20\x20\x20if\x20value['schema']\x20!=\x20SCHEMA\x20or\x20type(value['version'])\x20is\x20not\x20int\x20or\x20value['version']\x20!=\x20VERSION:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Expected\x20schema\x20{SCHEMA!r},\x20version\x20{VERSION}')\n\x20\x20\x20\x20if\x20type(value['templates'])\x20is\x20not\x20list\x20or\x20not\x20value['templates']:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('templates\x20must\x20be\x20a\x20nonempty\x20list')\n\x20\x20\x20\x20seen,\x20result\x20=\x20set(),\x20[]\n\x20\x20\x20\x20for\x20i,\x20item\x20in\x20enumerate(value['templates']):\n\x20\x20\x20\x20\x20\x20\x20\x20_object(item,\x20{'name',\x20'config'},\x20{'name',\x20'config'},\x20f'template\x20{i\x20+\x201}')\n\x20\x20\x20\x20\x20\x20\x20\x20name\x20=\x20_name(item['name'])\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20name.casefold()\x20in\x20seen:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Duplicate\x20template\x20name:\x20{name}')\n\x20\x20\x20\x20\x20\x20\x20\x20seen.add(name.casefold())\n\x20\x20\x20\x20\x20\x20\x20\x20config,\x20normalized\x20=\x20_config(col,\x20item['config'],\x20name)\n\x20\x20\x20\x20\x20\x20\x20\x20result.append((name,\x20config,\x20normalized))\n\x20\x20\x20\x20return\x20result\n\n\ndef\x20_pairs(pairs):\n\x20\x20\x20\x20result\x20=\x20{}\n\x20\x20\x20\x20for\x20key,\x20value\x20in\x20pairs:\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20key\x20in\x20result:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Duplicate\x20JSON\x20key:\x20{key}')\n\x20\x20\x20\x20\x20\x20\x20\x20result[key]\x20=\x20value\n\x20\x20\x20\x20return\x20result\n\n\ndef\x20_bad_constant(value):\n\x20\x20\x20\x20raise\x20ValueError(f'Nonfinite\x20JSON\x20constant:\x20{value}')\n\n\ndef\x20_counts(col):\n\x20\x20\x20\x20return\x20{'cards':\x20int(col.card_count()),\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'revlog':\x20int(col.db.scalar('select\x20count(*)\x20from\x20revlog'))}\n\n\ndef\x20export_filtered_templates(col,\x20path,\x20*,\x20names=None):\n\x20\x20\x20\x20\"\"\"Read\x20existing\x20filtered\x20configs,\x20validate\x20all,\x20then\x20exclusively\x20write\x20JSON.\n\n\x20\x20\x20\x20names=None\x20means\x20all\x20filtered\x20decks;\x20a\x20nonempty\x20list\x20gives\x20exact\x20selection\n\x20\x20\x20\x20and\x20order.\x20The\x20collection\x20is\x20not\x20written.\x20Existing\x20output\x20files\x20are\x20refused.\n\x20\x20\x20\x20Returns\x20the\x20manifest\x20that\x20was\x20written.\n\x20\x20\x20\x20\"\"\"\n\x20\x20\x20\x20available\x20=\x20{n.name:\x20n.id\x20for\x20n\x20in\x20col.decks.all_names_and_ids()\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20col.decks.get(n.id,\x20default=False)['dyn']}\n\x20\x20\x20\x20if\x20names\x20is\x20None:\n\x20\x20\x20\x20\x20\x20\x20\x20selected\x20=\x20list(available)\n\x20\x20\x20\x20elif\x20type(names)\x20is\x20list\x20and\x20names\x20and\x20all(isinstance(n,\x20str)\x20for\x20n\x20in\x20names):\n\x20\x20\x20\x20\x20\x20\x20\x20selected\x20=\x20names\n\x20\x20\x20\x20else:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('names\x20must\x20be\x20None\x20or\x20a\x20nonempty\x20list\x20of\x20exact\x20deck\x20names')\n\x20\x20\x20\x20if\x20len(selected)\x20!=\x20len(set(selected)):\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('Duplicate\x20names\x20in\x20selection')\n\x20\x20\x20\x20missing\x20=\x20[name\x20for\x20name\x20in\x20selected\x20if\x20name\x20not\x20in\x20available]\n\x20\x20\x20\x20if\x20missing:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Not\x20existing\x20filtered\x20deck\x20names:\x20{missing}')\n\x20\x20\x20\x20manifest\x20=\x20{'schema':\x20SCHEMA,\x20'version':\x20VERSION,\x20'templates':\x20[]}\n\x20\x20\x20\x20for\x20name\x20in\x20selected:\n\x20\x20\x20\x20\x20\x20\x20\x20saved\x20=\x20col.sched.get_or_create_filtered_deck(DeckId(available[name]))\n\x20\x20\x20\x20\x20\x20\x20\x20manifest['templates'].append({'name':\x20saved.name,\x20'config':\x20_config_json(saved.config)})\n\x20\x20\x20\x20_manifest(col,\x20manifest)\x20\x20#\x20No\x20incomplete\x20output\x20when\x20one\x20selection\x20is\x20invalid.\n\x20\x20\x20\x20text\x20=\x20json.dumps(manifest,\x20ensure_ascii=False,\x20allow_nan=False,\x20indent=2)\x20+\x20'\\n'\n\x20\x20\x20\x20with\x20Path(path).open('x',\x20encoding='utf-8')\x20as\x20stream:\n\x20\x20\x20\x20\x20\x20\x20\x20stream.write(text)\n\x20\x20\x20\x20return\x20manifest\n\n\ndef\x20import_filtered_templates(col,\x20path,\x20*,\x20prefix='',\x20apply=False):\n\x20\x20\x20\x20\"\"\"Validate\x20the\x20whole\x20file\x20first;\x20apply=False\x20never\x20creates/builds\x20decks.\n\n\x20\x20\x20\x20apply=True\x20creates\x20NEW\x20IDs\x20in\x20manifest\x20order\x20and\x20immediately\x20builds\x20from\n\x20\x20\x20\x20local\x20cards.\x20Prefix\x20alters\x20deck\x20names\x20only.\x20Backend-normalized\x20queries\x20are\n\x20\x20\x20\x20shown\x20in\x20the\x20plan\x20and\x20checked\x20on\x20readback;\x20original\x20query\x20text\x20is\x20submitted.\n\x20\x20\x20\x20On\x20execution\x20failure,\x20PartialImportError.partial_report\x20lists\x20created\x20IDs.\n\x20\x20\x20\x20Earlier\x20successful\x20operations\x20remain;\x20no\x20automatic\x20Empty/delete\x20is\x20done.\n\x20\x20\x20\x20\"\"\"\n\x20\x20\x20\x20if\x20type(apply)\x20is\x20not\x20bool\x20or\x20not\x20isinstance(prefix,\x20str):\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('apply\x20must\x20be\x20a\x20bool\x20and\x20prefix\x20must\x20be\x20a\x20string')\n\x20\x20\x20\x20with\x20Path(path).open(encoding='utf-8-sig')\x20as\x20stream:\n\x20\x20\x20\x20\x20\x20\x20\x20data\x20=\x20json.load(stream,\x20object_pairs_hook=_pairs,\x20parse_constant=_bad_constant)\n\x20\x20\x20\x20parsed\x20=\x20_manifest(col,\x20data)\n\x20\x20\x20\x20existing\x20=\x20{n.name.casefold():\x20n.id\x20for\x20n\x20in\x20col.decks.all_names_and_ids()}\n\x20\x20\x20\x20original_ids\x20=\x20set(existing.values())\n\x20\x20\x20\x20planned\x20=\x20[_name(prefix\x20+\x20name)\x20for\x20name,\x20_,\x20_\x20in\x20parsed]\n\x20\x20\x20\x20folded\x20=\x20{name.casefold()\x20for\x20name\x20in\x20planned}\n\x20\x20\x20\x20if\x20len(folded)\x20!=\x20len(planned):\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('Destination\x20names\x20collide\x20after\x20adding\x20prefix')\n\x20\x20\x20\x20for\x20name\x20in\x20planned:\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20col.decks.id_for_name(name)\x20is\x20not\x20None:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Destination\x20name\x20already\x20exists:\x20{name}')\n\x20\x20\x20\x20\x20\x20\x20\x20components\x20=\x20name.split('::')\n\x20\x20\x20\x20\x20\x20\x20\x20for\x20length\x20in\x20range(1,\x20len(components)):\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20parent\x20=\x20'::'.join(components[:length])\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20pid\x20=\x20col.decks.id_for_name(parent)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20parent.casefold()\x20in\x20folded\x20or\x20(pid\x20is\x20not\x20None\x20and\x20col.decks.get(pid)['dyn']):\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Filtered\x20decks\x20cannot\x20be\x20parents:\x20{parent}')\n\x20\x20\x20\x20report\x20=\x20{'applied':\x20False,\x20'templates':\x20[],\x20'created':\x20[],\x20'new_parent_decks':\x20[],\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'before':\x20_counts(col),\x20'warnings':\x20[\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Apply\x20immediately\x20builds\x20from\x20destination-local\x20cards\x20in\x20manifest\x20order.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Earlier\x20filtered\x20builds\x20take\x20precedence\x20for\x20overlapping\x20cards.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'The\x20batch\x20is\x20not\x20atomic;\x20no\x20automatic\x20rollback\x20or\x20promised\x20single\x20Undo.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Prefix\x20changes\x20filtered\x20names\x20only;\x20search\x20strings\x20keep\x20their\x20original\x20targets.'],\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'recovery':\x20[\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Inspect\x20created\x20IDs\x20and\x20names\x20before\x20any\x20recovery\x20action.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'To\x20return\x20cards,\x20use\x20Empty\x20on\x20ONLY\x20those\x20newly\x20created\x20filtered\x20decks.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Verify\x20each\x20is\x20empty,\x20then\x20delete\x20only\x20those\x20new\x20filtered\x20decks\x20if\x20unwanted.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Do\x20not\x20delete\x20normal\x20parent\x20decks\x20that\x20contain\x20other\x20decks\x20or\x20cards.']}\n\x20\x20\x20\x20for\x20name,\x20(_,\x20config,\x20normalized)\x20in\x20zip(planned,\x20parsed):\n\x20\x20\x20\x20\x20\x20\x20\x20report['templates'].append({'name':\x20name,\x20'config':\x20_config_json(config),\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'normalized_searches':\x20normalized})\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20normalized\x20!=\x20[term.search\x20for\x20term\x20in\x20config.search_terms]:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20report['warnings'].append(f'{name}:\x20backend\x20will\x20normalize\x20search\x20spelling;\x20see\x20normalized_searches.')\n\x20\x20\x20\x20if\x20not\x20apply:\n\x20\x20\x20\x20\x20\x20\x20\x20report['after']\x20=\x20_counts(col)\n\x20\x20\x20\x20\x20\x20\x20\x20return\x20report\n\x20\x20\x20\x20report['applied']\x20=\x20True\n\x20\x20\x20\x20try:\n\x20\x20\x20\x20\x20\x20\x20\x20for\x20item,\x20(_,\x20config,\x20normalized)\x20in\x20zip(report['templates'],\x20parsed):\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20col.decks.id_for_name(item['name'])\x20is\x20not\x20None:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Name\x20appeared\x20after\x20preflight:\x20{item[\"name\"]}')\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20new\x20=\x20col.sched.get_or_create_filtered_deck(DeckId(0))\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20new.name,\x20new.allow_empty\x20=\x20item['name'],\x20True\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20new.config.Clear()\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20new.config.CopyFrom(config)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20did\x20=\x20int(col.sched.add_or_update_filtered_deck(new).id)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20did\x20in\x20original_ids:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20RuntimeError(f'Backend\x20unexpectedly\x20reused\x20existing\x20ID\x20{did}')\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20row\x20=\x20{'id':\x20did,\x20'name':\x20item['name']}\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20report['created'].append(row)\x20\x20#\x20Retain\x20the\x20ID\x20even\x20if\x20verification\x20fails.\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20actual\x20=\x20col.sched.get_or_create_filtered_deck(DeckId(did))\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20row.update(name=actual.name,\x20config=_config_json(actual.config),\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20card_count=int(col.db.scalar('select\x20count(*)\x20from\x20cards\x20where\x20did=?',\x20did)))\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20expected\x20=\x20FilteredDeckConfig()\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20expected.CopyFrom(config)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20for\x20term,\x20search\x20in\x20zip(expected.search_terms,\x20normalized):\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20term.search\x20=\x20search\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20actual.name\x20!=\x20item['name']\x20or\x20actual.config\x20!=\x20expected:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20RuntimeError(f'Readback\x20mismatch\x20for\x20new\x20filtered\x20deck\x20{did};\x20inspect\x20partial_report')\n\x20\x20\x20\x20\x20\x20\x20\x20report['after']\x20=\x20_counts(col)\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20report['before']\x20!=\x20report['after']:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20RuntimeError('Card/revlog\x20totals\x20changed\x20during\x20import;\x20inspect\x20partial_report')\n\x20\x20\x20\x20except\x20Exception\x20as\x20error:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20PartialImportError(str(error),\x20report)\x20from\x20error\n\x20\x20\x20\x20finally:\n\x20\x20\x20\x20\x20\x20\x20\x20try:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20created_ids\x20=\x20{row['id']\x20for\x20row\x20in\x20report['created']}\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20report['new_parent_decks']\x20=\x20[{'id':\x20n.id,\x20'name':\x20n.name}\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20for\x20n\x20in\x20col.decks.all_names_and_ids()\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20n.id\x20not\x20in\x20original_ids\x20and\x20n.id\x20not\x20in\x20created_ids]\n\x20\x20\x20\x20\x20\x20\x20\x20except\x20Exception\x20as\x20error:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20report['inspection_error']\x20=\x20str(error)\n\x20\x20\x20\x20return\x20report\n")

Anki Desktop에서 원본 프로필을 열고 메인 창으로 돌아가세요. Windows/Linux에서는 Ctrl+Shift+;, macOS에서는 Command+Shift+;로 Debug Console을 엽니다. 일부 키보드 배열에서는 ; 대신 : 또는 +를 사용합니다. 위쪽 입력 창에 명령을 붙여 넣고 Shift 없이 Ctrl+Return 또는 Command+Return으로 실행하세요. 이는 공식 콘솔 사용 안내입니다. 아래 검증에서는 그래픽 콘솔 자체를 조작하지 않았습니다.

다음 명령으로 파일을 불러오되, 예시를 파일의 전체 경로로 바꾸세요. C:/Users/YOU/Downloads/anki_filtered_templates.py처럼 따옴표 안에 쓴 Windows 경로에서도 슬래시를 사용할 수 있습니다. mw.col은 이 Anki 프로필에서 열려 있는 컬렉션 객체입니다. 별도로 설치한 Python 프로세스에서 사용 중인 컬렉션 파일을 직접 열지 마세요.

from pathlib import Path; from aqt import mw; exec(Path("/FULL/PATH/anki_filtered_templates.py").read_text(encoding="utf-8"))

3. 카드를 준비하기 전에 필터 구성 정보 내보내기

기존 폴더 안에서 새 JSON 파일 이름을 정하세요. 다음 명령은 현재의 모든 필터 덱을 이름순으로 내보내고 내보낸 개수를 출력합니다. 내보내기는 컬렉션을 읽기만 하며 기존 출력 파일을 덮어쓰지 않습니다. 경로는 자신의 컴퓨터에 맞게 바꾸세요.

manifest = export_filtered_templates(mw.col, "/FULL/PATH/templates.json"); print(len(manifest["templates"]))

일부만 선택하려면 만들고 싶은 순서대로 정확한 이름을 지정하세요. 예: manifest = export_filtered_templates(mw.col, "/FULL/PATH/selected-templates.json", names=["Morning", "Exam"]). 일반 덱 이름, 존재하지 않는 이름, 중복 이름, 빈 선택 목록은 거부됩니다. 이 예시의 이름은 실제 필터 덱 이름으로 바꿔야 하는 자리표시자입니다.

텍스트 편집기로 JSON을 여세요. 최상위 필드는 schema, version, templates이며 각 템플릿에는 name과 config가 있습니다. search_terms 아래에는 각 검색 조건의 검색식, limit, order가 들어 있습니다. 두 번째 조건이 있으면 그것도 확인하고, reschedule과 미리 보기 값도 확인하세요. limit가 0이면 0으로, reschedule=false이면 false로 유지됩니다. 기본값처럼 보인다는 이유로 값을 지우지 말고 생성된 필드를 보존하세요.

원래 파일의 사본을 보관하세요. 템플릿 순서를 바꾸면 덱을 만드는 순서가 달라지고, 검색식을 바꾸면 대상 컬렉션에서 모을 수 있는 카드가 달라집니다. 원본 템플릿의 검색식이 비어 있으면 내보내기가 중단되고 명시적인 검색식을 요구합니다. 모든 덱을 대상으로 삼으려는 경우에만 deck:*를 사용하세요. 오류를 없애기 위해 무조건 넣지 마세요.

4. 필요하면 학습 일정 정보를 제외한 카드 패키지 추가하기

받는 사람에게 이미 적합한 카드가 있다면 이 단계를 건너뛰세요. 그렇지 않다면 JSON을 내보낸 다음, 전달하려는 일반 덱의 카드를 담고 있는 원본 필터 덱마다 Empty를 사용하세요. Empty는 카드를 삭제하지 않고 원래 덱으로 돌려보냅니다. 이 작업은 원본 컬렉션에서 카드의 현재 덱 위치를 의도적으로 바꾸며, 도우미가 대신 수행하지는 않습니다.

카드의 원래 일반 덱을 필요한 하위 덱과 함께 Anki Deck Package(.apkg)로 내보내세요. Include Scheduling을 끄고, 카드에 필요한 미디어를 선택하세요. 전체 컬렉션 .colpkg로 대신하지 마세요. Anki의 일반 덱 프리셋 옵션은 JSON으로 제공하는 필터 구성 정보와 별개입니다.

이 순서가 중요합니다. 실제 백엔드 테스트에서는 카드가 원래 덱 계층 밖의 필터에 들어 있는 상태에서 원래 덱을 내보내자, 전달하려던 카드 세 장 중 한 장이 누락됐습니다. 카드가 들어 있는 하위 필터 덱은 첫 패키지에 일반 덱으로 포함됐습니다. 원본 필터 덱들을 비운 뒤 새로 만든 패키지에는 세 장 모두 원래의 일반 덱에 포함됐고, 비어 있는 필터 덱 구성 정보는 빠졌습니다. 두 패키지의 내부 컬렉션 데이터베이스를 확인했을 때 복습 로그 행은 모두 0개였습니다.

받는 사람은 JSON보다 먼저 APKG를 가져온 뒤 예상한 카드, 태그, 원래 덱 이름을 확인해야 합니다. 병합한다고 기존 노트가 그대로 유지되는 것은 아닙니다. GUID가 일치하는 테스트에서는 원본 노트가 더 최신일 때 가져오기 설정에 따라 필드와 태그가 갱신됐지만, 대상의 기존 카드 레코드와 복습 로그 행은 바뀌지 않았습니다. 받는 사람에게 이미 관련 카드가 있다면 해당 설정과 결과를 확인하세요. 설정만 담은 JSON의 영향 범위는 별도의 카드 패키지를 가져오는 작업보다 좁습니다.

5. 받는 사람의 프로필에서 파일 전체 사전 점검하기

설정을 적용할 대상 프로필로 전환하고 콘솔을 다시 연 뒤, 2단계의 명령으로 도우미를 불러오세요. 새 필터 덱들을 구분하기 쉽도록 필요하면 접두어를 정하세요. Shared::를 쓰면 Shared::Morning 같은 이름이 만들어집니다. Shared는 일반 상위 덱이어야 하며, 실제 가져오기 과정에서 새 일반 상위 덱이 생성될 수도 있습니다. 사전 점검 자체는 아무 덱도 만들지 않습니다.

plan = import_filtered_templates(mw.col, "/FULL/PATH/templates.json", prefix="Shared::"); pp(plan)

계획에 나온 모든 name과 config, normalized_searches, warnings를 읽어 보세요. 도우미는 덱을 하나라도 만들기 전에 모든 항목을 검증합니다. 잘못된 JSON, 중복 키, 알 수 없는 필드나 order 값, 잘못된 검색식, 빈 검색식, 잘못된 이름, 대상 덱 이름 충돌을 거부합니다. 각 템플릿에는 비어 있지 않은 검색 조건이 하나 또는 두 개 있어야 합니다. 기존 이름을 조용히 덮어쓰거나 자동으로 +를 붙여 받아들이지 않고 충돌로 거부합니다.

접두어는 deck:Course, tag:chapter1 또는 검색식의 다른 부분을 바꾸지 않습니다. cid: 카드 ID 검색이나 nid: 노트 ID 검색에는 원본 ID가 그대로 남으며, 다른 컬렉션에서는 그 ID가 의도한 레코드를 가리키지 않을 수 있습니다. 대상 컬렉션의 원하는 레코드를 참조하는지 확인하세요. 필요하면 JSON 작업 사본을 직접 수정한 뒤 사전 점검을 다시 실행하세요. Anki는 유효한 검색식의 표기를 정규화할 수 있습니다. 계획에 그 결과가 표시되며, 가져오기 도우미는 생성 후 저장된 검색식이 그 결과와 맞는지 확인합니다.

사전 점검은 설정과 검색 문법을 확인하며, 실제로 모일 카드나 스케줄러별 모든 생성 조건을 확인하지는 않습니다. 유효한 검색식이어도 조건에 맞는 카드가 없을 수 있습니다. 받는 사람의 스케줄러 설정이 보낸 사람과 다를 수도 있습니다. 회상 가능성에 따른 정렬에서 확인된 제약은 아래에 설명합니다. apply=True를 사용하기 전에 이름이나 문법 오류를 해결하세요.

6. 한 번 적용한 뒤 생성된 덱 확인하기

보고서 내용이 맞으면 같은 JSON 경로와 접두어로 다음 명령을 실행하세요. 이 래퍼는 완료 결과 또는 부분 실패 보고서를 출력하고 Anki 화면 새로 고침을 요청합니다. 적용은 대상 컬렉션을 의도적으로 변경하는 작업입니다. 실수로 원본 프로필에서 실행하지 마세요.

exec("try:\n\x20\x20\x20\x20result\x20=\x20import_filtered_templates(mw.col,\x20'/FULL/PATH/templates.json',\x20prefix='Shared::',\x20apply=True)\nexcept\x20PartialImportError\x20as\x20error:\n\x20\x20\x20\x20pp(error.partial_report)\n\x20\x20\x20\x20mw.reset()\n\x20\x20\x20\x20raise\nelse:\n\x20\x20\x20\x20pp(result)\n\x20\x20\x20\x20mw.reset()\n")

created의 각 항목에서 id, name, card_count, config를 확인하세요. 도우미는 항상 새 필터 덱 ID를 요청하고 결과를 다시 읽습니다. 계획에 표시된 검색식 정규화를 허용하면서 저장된 이름과 설정이 맞는지 확인합니다. before와 after의 카드 및 revlog 총수는 같아야 합니다. 이 작업은 조건에 맞는 로컬 카드를 재배치하며, 원본 카드나 복습 기록을 가져오지 않습니다.

새 필터 덱을 열고 검색 조건, 제한 수, 카드 선택 순서, 일정 재조정 옵션, 미리 보기 지연 시간을 JSON과 비교하세요. 검색에 맞는다고 알고 있는 카드 하나와 그 카드의 원래 덱을 확인하세요. 컬렉션 전체 개수가 같다는 사실만으로 검색식이 의도한 카드를 골랐다고 입증할 수는 없습니다. 도우미가 빈 결과를 허용하므로, 필터 덱이 비어 있어도 정상적인 결과일 수 있습니다.

JSON과 출력 결과를 함께 보관하세요. 나중에 다른 모음을 만들려면 새 이름이나 새 접두어를 선택하고 사전 점검을 다시 실행하세요. 같은 대상 이름이 그대로 있는 상태에서 동일한 가져오기를 반복하면 거부됩니다. 이 도우미는 기존 덱을 제자리에서 갱신하는 도구가 아닙니다.

7. 겹치는 카드, Empty, 부분 실패 이해하기

먼저 구성한 덱이 겹치는 카드에 우선권을 가집니다. Morning이 카드 1, 2, 3을 모았다면, 그 카드들이 Morning에 있는 동안 나중에 만드는 Exam 필터가 2와 3을 다시 가져갈 수는 없습니다. 그림에서는 Exam이 카드 4를 받습니다. 이 번호는 합성 예시의 카드 표시이며, 실행 가능한 검색식이나 실제 Anki 카드 ID가 아닙니다. 기존 필터 덱도 새 필터가 검색할 수 있었던 카드를 이미 담고 있을 수 있습니다.

일반적으로 일시 중단된 카드, 미뤄 둔 카드, 다른 필터 덱에 이미 들어 있는 카드는 제외됩니다. 그 밖의 대상 조건도 적용될 수 있습니다. 도우미는 카드의 일시 중단을 해제하거나 기존 필터를 비우지 않습니다. 결과가 적거나 0개라고 해서 데이터가 사라졌다고 판단하기 전에 실제 검색식, 제한 수, 대상 조건, 덱을 구성한 순서를 확인하세요.

reschedule 설정은 모인 카드를 학습할 때 어떤 결과가 생기는지 결정합니다. 일정 재조정이 켜져 있으면 답변에 따라 카드의 평소 학습 일정이 바뀔 수 있습니다. 미리 보기 모드에서는 미리 보기 방식이 적용됩니다. 템플릿을 전달하는 것과 이후 그 템플릿으로 학습하는 것은 별개의 작업입니다. 복습을 시작하기 전에 이 옵션을 확인하세요.

앞선 템플릿이 성공한 뒤 apply가 실패해도 이미 만든 새 덱들은 남습니다. 출력된 부분 보고서의 created와 new_parent_decks를 읽고 대상 컬렉션을 확인하세요. 새 필터 덱을 사용하지 않기로 했다면, 보고서에 표시된 이번에 새로 만든 필터 덱에만 Empty를 사용하세요. 카드가 원래 덱으로 돌아갔고 새 필터 덱이 비어 있는지 확인한 뒤, 필요 없는 새 필터 덱을 삭제하세요. 다른 카드나 덱을 담고 있는 일반 상위 덱은 삭제하지 마세요.

전체 작업을 자동으로 롤백하지 않으며, 한 번의 Undo로 되돌릴 수 있다고 보장하지도 않습니다. 새 필터 덱을 비우면 카드가 돌아가지만, 새로 만든 덱 레코드가 삭제되거나 이후의 학습이 지워지지는 않습니다. 이전 APKG 가져오기가 갱신한 노트를 되돌리거나 컬렉션의 모든 설정을 복원하지도 않습니다. 같은 이름으로 무작정 다시 실행하지 말고 원인을 해결한 다음 남은 작업을 사전 점검하세요.

8. 검증한 내용과 버전에 따라 달라지는 부분

일반 형태의 도우미는 임시 컬렉션에서 실제 Anki 26.9.3 Python/Rust 백엔드로 실행한 테스트 21개를 통과했습니다. 백엔드가 보고하는 애플리케이션 버전은 26.09.3입니다. 테스트에는 템플릿 20개의 내보내기/가져오기/다시 내보내기, 두 검색 조건, 명시적인 false/0 값, 정확한 새 ID 요청, 이름 및 문법 오류, 기록이 있는 원본 컬렉션이 포함됐습니다. 읽기 전용 내보내기와 사전 점검에는 전체 SQLite 논리 스냅샷을 사용했습니다. 백엔드로 카드에 답하여 실제 복습 로그 데이터를 생성했습니다.

카드가 겹치는 경우와 Empty 검사는 실제 백엔드 작업으로 수행했습니다. 부분 실패 테스트는 첫 번째 실제 생성이 끝난 뒤 두 번째 생성에 의도적으로 오류를 주입했습니다. 이 검사는 보고서와 앞서 수행한 작업의 보존을 확인하며, 자연스럽게 발생하는 실패의 빈도를 측정하지는 않습니다. 별도의 검토자는 백엔드 사례 네 가지를 추가로 확인했습니다. 두 번째 항목에서 회상 가능성 정렬 값 8을 사용했을 때 자연스럽게 발생한 실패, 뒤쪽 항목에서 필터 덱을 상위 덱으로 지정한 충돌, 뒤쪽 항목의 알 수 없는 열거형 값, 합성 원본 컬렉션의 실제 카드 ID가 들어 있는 검색식입니다. 자연 발생한 실패에서도 처음 만든 덱은 남았고 보고서에 정확히 표시됐습니다. 두 가지 사전 점검 실패에서는 모든 SQLite 테이블이 그대로 유지됐습니다. 카드 ID 사례는 cid: 검색식 원문을 유지한 채 빈 대상 컬렉션에 정상적으로 가져와졌지만 모인 카드는 0장이었습니다. 이는 문법이 유효해도 원하는 대상 카드가 선택된다는 보장은 없음을 보여 줍니다. 별도의 실제 APKG 테스트에서는 일반 카드 공유, GUID 일치 시 병합, Empty를 먼저 수행하도록 수정한 내보내기 순서를 확인한 뒤, 그 패키지를 이 도우미의 정확히 같은 버전과 함께 사용했습니다.

초기 테스트 준비 과정에서 구체적인 호환성 한계가 드러났습니다. FSRS를 사용하지 않는 새 컬렉션에서 원본 테스트 데이터를 만들 때 RETRIEVABILITY_ASCENDING, 정렬 값 8이 도우미 실행 전에 Anki 내부에서 실패했습니다. 최종 템플릿 20개 왕복 테스트에는 정상 동작한 정렬 값 0부터 7까지를 사용했습니다. 정의된 정렬 값 11개의 이름과 숫자는 모두 읽기 전용 사전 점검을 통과했습니다. 그렇다고 모든 스케줄러 설정에서 11개가 모두 덱을 성공적으로 구성한다는 뜻은 아닙니다. 선택한 정렬에서 실패하면 오류와 앞서 만들어진 덱을 확인한 뒤, 호환되는 정렬을 고르거나 대상 설정을 의도적으로 맞추고 다시 시도하세요.

프로그램은 Anki의 스케줄러 및 설정 API를 사용하며, 이 API는 버전에 따라 바뀔 수 있습니다. 테스트한 버전에서 정의된 필드 일곱 개를 복사하며, 앞으로 추가될 알 수 없는 옵션까지 지원한다고 주장하지 않습니다. Anki GUI, 사용자 개인 프로필, 모바일 앱, 여러 운영체제의 조합, 대규모 실사용 컬렉션은 테스트하지 않았습니다. 소스, JSON, 결과를 보관하고 Anki 버전을 바꿀 때 필요한 검사를 다시 수행하세요.

합성 예시에서 Morning을 먼저 만들어 카드 1, 2, 3을 모읍니다. 그다음 만든 Exam은 카드 4만 받으며, 겹치는 카드 2와 3은 Morning에 남습니다. Empty는 모인 카드를 삭제하지 않고 원래 덱으로 돌려보냅니다.
합성 카드 표시를 사용해 직접 제작한 일러스트입니다. 먼저 만든 덱이 겹치는 카드를 유지하며, 그 카드가 원래 덱으로 돌아갈 때까지 나중의 필터는 가져갈 수 없습니다. 각 템플릿은 검색 조건을 하나 또는 두 개 지원합니다. Empty는 카드를 원래 덱으로 돌려보내지만 필터 덱의 구성 정보는 남겨 둡니다. Watch at Ease