From 232f453e1599827b334e8c567d4843efe54d76c2 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Mon, 1 Jun 2026 11:38:05 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EB=B3=80=ED=99=98=20=EA=B7=B8=EB=9E=98?= =?UTF-8?q?=ED=94=84=20OS=20=EB=A7=88=EC=8A=A4=ED=84=B0=ED=94=8C=EB=9E=9C?= =?UTF-8?q?=20SSOT=20(=EB=A9=80=ED=8B=B0=EC=97=90=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=ED=8A=B8=20=EB=B6=84=EC=84=9D+=EB=A6=AC=EC=84=9C=EC=B9=98=20?= =?UTF-8?q?=EC=A2=85=ED=95=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 전체 소스 심층 분석(5) + 인터넷 리서치(7) → 3 독립 아키텍트 → 심사 랭킹 → 마스터플랜 종합을 Workflow로 오케스트레이션해 생성. - docs/ssot/_data/*.json : SSOT 원천 데이터 (master/analyses/researches/designs/ranking) - docs/ssot/build.py : JSON → index.html(웹 대시보드) + PLAN.md 제너레이터 - 9 ADR · 8단계 로드맵(P1 그래프엔진+PDF압축 → P8 굳히기) - 양방향/다방향 그래프 라우팅, Codex OAuth+API, FFmpeg/Ghostscript 미디어 레이어 --- docs/ssot/PLAN.md | 361 ++++++++++++++ docs/ssot/README.md | 38 ++ docs/ssot/_data/analyses.json | 576 +++++++++++++++++++++++ docs/ssot/_data/designs.json | 252 ++++++++++ docs/ssot/_data/master.json | 552 ++++++++++++++++++++++ docs/ssot/_data/meta.json | 15 + docs/ssot/_data/ranking.json | 40 ++ docs/ssot/_data/researches.json | 698 +++++++++++++++++++++++++++ docs/ssot/_data/status.json | 11 + docs/ssot/build.py | 720 ++++++++++++++++++++++++++++ docs/ssot/index.html | 807 ++++++++++++++++++++++++++++++++ 11 files changed, 4070 insertions(+) create mode 100644 docs/ssot/PLAN.md create mode 100644 docs/ssot/README.md create mode 100644 docs/ssot/_data/analyses.json create mode 100644 docs/ssot/_data/designs.json create mode 100644 docs/ssot/_data/master.json create mode 100644 docs/ssot/_data/meta.json create mode 100644 docs/ssot/_data/ranking.json create mode 100644 docs/ssot/_data/researches.json create mode 100644 docs/ssot/_data/status.json create mode 100644 docs/ssot/build.py create mode 100644 docs/ssot/index.html diff --git a/docs/ssot/PLAN.md b/docs/ssot/PLAN.md new file mode 100644 index 0000000..ec46bf2 --- /dev/null +++ b/docs/ssot/PLAN.md @@ -0,0 +1,361 @@ +# Everything2Everything — 변환 그래프 OS 마스터플랜 + +> 전체 소스 심층 분석 · 방법론 인터넷 리서치 · 아키텍처 종합 (Single Source of Truth) + +**생성** 2026-06-01 · **방법** Workflow 멀티에이전트 오케스트레이션 (5 코드분석 + 7 인터넷리서치 → 3 독립 아키텍트 → 심사 랭킹 → 마스터플랜 종합) +**규모** 17 agents · 1,426,291 subagent tokens · 367 tool calls +**종합** idx2(AI·미디어 우선, 89점) 실행 골격 + idx0(그래프 코어, 84점) 아키텍처 영혼 + +> 이 문서는 SSOT다. `docs/ssot/_data/*.json` 을 갱신하고 `python docs/ssot/build.py` 로 재생성한다. 웹 버전은 `docs/ssot/index.html`. + +--- +## 엘리베이터 피치 + +손으로 짠 변환 switch를 자동 경로 탐색 그래프로 교체하고, 그 위에 PDF 압축·영상·HWP·AI를 엣지로 얹어, 코드 한 줄당 N×M 매트릭스가 발현하는 '변환하면서 더 좋아지는' 만능 변환기. + +## 북극성 비전 + +Everything2Everything의 북극성은 "세상의 모든 변환을 원자(atomic) 엣지로 등록하면, 엔진이 그 조합으로 임의의 A→Z를 스스로 합성하고, 변환하면서 AI가 결과를 더 좋게 만드는 변환 그래프 OS"다. 핵심 통찰은 현재 DocumentProvider.RouteAsync(92-205)가 사실상 '사람이 손으로 그린 Dijkstra'(md→html→docx, docx→html→md, hwp→html→md를 switch에 박아넣음)라는 점이며, 이 손그림을 삭제하고 엔진이 같은 경로를 '계산'하게 만드는 것이 모든 확장의 열쇠다. 그래프가 코어가 되면 FFmpeg(미디어), PDF 압축, HWP 양방향, AI 요약/번역/캡션이 전부 '엣지 추가'로 환원되고, OutputsForInput은 1-hop 직접 출력에서 도달 가능한 모든 포맷(transitive closure)으로 폭발한다. 동시에 사용자가 명시한 신규 가치(PDF 압축·HWP·영상·AI)를 인프라 완성을 기다리지 않고 빠르게 출시해 체감 차별화를 먼저 만든다. AI는 핵심 엔진이 아니라 '키 없으면 조용히 비활성되는 부가가치 엣지'로, 변환의 로컬 예측가능성이라는 신뢰를 절대 깨지 않는다. + +## 설계 원칙 + +1. **변환은 엣지, 엔진은 라우터** — 모든 Provider는 단일 홉 원자 변환(md→html, png→pdf)만 선언한다. 멀티홉(md→docx)은 절대 Provider 내부에 손으로 짜지 않고 엔진의 그래프 탐색이 자동 합성한다. DocumentProvider.RouteAsync의 switch 지옥이 재발하지 않도록 이를 불변식으로 강제한다. +2. **기능이 그래프를 견인하되, 그래프가 기능을 받친다** — 사용자 체감 가치(PDF 압축·HWP·영상·AI)를 빠르게 출시하되, 신규 기능은 반드시 '그래프 엣지'로만 추가한다. Phase 0에 심은 그래프 코어가 하드코딩 유혹을 구조적으로 차단한다. +3. **손실은 가중치다** — 품질 손실을 ConversionPair.LossClass(Lossless/Container/Recode/Rasterize)로 SSOT화하고 -log(보존율)+홉페널티로 환산한다. 멀티홉 경로 선택과 UI '손실 변환' 경고 배지가 모두 이 단일 출처를 소비한다. +4. **AI는 끄면 사라지는 부가 엣지** — AI는 절대 기본 경로를 점유하지 않는다. 키가 없으면 모든 기존 변환은 100% 동작하고 AI 페어만 자동 비활성(NotReady)되며 ✨AI 배지로만 opt-in 노출된다. 변환의 로컬 예측가능성 신뢰를 깨지 않는다. +5. **무거운 외부 도구는 분리 호출로만** — FFmpeg(GPL 정적링크 금지·LGPL 분리호출만), Ghostscript/MuPDF(AGPL·사용자 설치본 감지만), H2Orestart/Calibre(GPL·외부 프로세스 분리)를 본체에 절대 정적 링크하지 않는다. 라이선스 경계를 코드 리뷰 게이트로 강제해 상업 배포 오염을 원천 차단한다. +6. **순수 .NET 우선, 외부 바이너리 차선** — 단일 포터블 EXE 부담을 줄이기 위해 SharpCompress·Parquet.Net·PDFsharp·Svg.Skia 같은 순수 관리 코드를 EXE에 직접 포함하고, FFmpeg/Pandoc/Calibre 같은 무거운 바이너리는 '외부 설치 감지 + 미설치 시 안내/자동조달' 모델로만 통합한다. +7. **점진 마이그레이션, 무중단** — IConverterProvider/ConvertResult/ConvertOptions 일반화는 기존 8개 Provider를 어댑터로 감싸 한 번에 깨지지 않게 한다. 모든 코어 변경은 회귀 테스트(현재 0개에서 출발)로 '동일 동작'을 객관 증명한다. + +## 타깃 아키텍처 + +4계층 변환 그래프 아키텍처. (1) Abstractions 계층이 Provider 계약을 담고, (2) 그래프 코어가 모든 원자 변환을 방향 그래프로 합성해 Dijkstra로 멀티홉 경로를 푼다. (3) Provider 계층은 in-box 코드 Provider(이미지/문서/미디어/AI)와 manifest 기반 외부 도구 어댑터로 나뉘며, (4) 실행 계층(ExternalProcessRunner·ISettingsStore)이 외부 프로세스·설정·키를 횡단 관리한다. 핵심은 ProviderRegistry를 단일 홉 딕셔너리에서 ConversionGraph로 승격하는 것이다. + +- **Abstractions 계층 (Everything2Everything.Abstractions)** — Provider 계약을 별도 어셈블리로 분리해 타입 동일성을 보장하고 향후 플러그인의 안정적 참조점을 제공 + `IConverterProvider`, `ConvertRequest/ConvertContext`, `ConvertResult(비파일 산출물 포함)`, `ProviderCapability`, `ConversionPair+LossClass`, `ExternalDependency` +- **그래프 코어 계층 (Core.Graph)** — 모든 Provider Capability를 순회해 방향 그래프(노드=확장자, 엣지=Provider+LossClass 가중치)를 빌드하고, 자체 Dijkstra로 최저손실 멀티홉 경로를 탐색·실행 + `ConversionGraph`, `PathFinder(자체 Dijkstra)`, `ChainExecutor(ExecuteChainAsync)`, `ConversionEngine(라우터로 축소)`, `ProviderRegistry(증분 등록 Register/Rebuild)` +- **Provider 계층** — 단일 홉 원자 변환 능력을 선언·실행. in-box 코드 Provider와 manifest 어댑터 Provider 공존 + `MagickProvider/PdfProvider/HtmlProvider(기존)`, `LlmProvider(AI)`, `FfmpegProvider(미디어)`, `PdfToolProvider(압축)`, `ImageCombineProvider(N→1)`, `ExternalToolProvider(manifest 어댑터 베이스)` +- **실행/인프라 계층** — 외부 프로세스 실행·설정 영속화·키 보안·미리보기를 횡단 제공 + `ExternalProcessRunner(CliWrap, 타임아웃+stderr+Kill)`, `ISettingsStore(DPAPI 암호화)`, `ExternalToolDetector(번들 경로 폴백)`, `IPreviewRenderer(프리뷰 캐시)`, `ManifestLoader` + +**데이터 흐름:** 파일 입력 → ConversionEngine.ConvertOneAsync가 입력/출력 확장자 정규화 → ConversionGraph.FindBestPath(in,out,options)로 경로 탐색(직접 엣지 있으면 1홉, 없으면 손실가중치 기반 멀티홉) → ChainExecutor가 경로의 각 홉을 순차 실행하며 중간 산출물을 공용 workDir(Temp/e2e_{Guid})에 체이닝 → 각 홉은 Provider.ConvertAsync(ConvertRequest) 호출, 진행률은 홉 수로 분할 매핑 → 마지막 홉 산출물을 OutputPathHelper로 충돌 해결 후 최종 출력 → ConvertResult(출력 경로 + 비파일 산출물) 반환, 중간 산출물 정리. AI/외부도구 엣지는 CheckAvailabilityAsync 게이트를 먼저 통과해야 그래프에 활성 노드로 참여. + +## 핵심 아키텍처 결정 (ADR) + +### ADR-1 · ProviderRegistry를 단일 홉 딕셔너리에서 ConversionGraph로 승격 +- **결정:** _byPair 단일 룩업(ProviderRegistry.cs:6,43)을 유지하되 그 위에 인접 리스트 그래프(Dictionary>)를 빌드하고, 외부 의존성 없는 자체 Dijkstra(80~120줄, .NET 9 PriorityQueue 사용)로 멀티홉 경로를 탐색한다. DocumentProvider.RouteAsync의 손그림 멀티홉을 엔진 합성으로 대체. +- **근거:** 현재 멀티홉이 Provider 내부 switch에 하드코딩되어 형식 N개에 O(N²)로 수동 증식한다. NCSA Polyglot 모델(노드=포맷, 엣지=Provider, 가중치=손실)은 학계 검증된 best practice이며, 그래프가 수십 노드·수백 엣지 규모라 성능 이슈가 없다. +- **대안:** QuikGraph(MS-PL, 2022 이후 정체)·Pandoc식 단일 AST 허브(이질적 도메인에 부적합). 자체 구현이 단일 EXE/AOT/라이선스 검토 모두 무부담이라 1순위. +- **트레이드오프:** 멀티홉은 중간 임시파일 I/O가 늘고 손실이 누적될 수 있다. 완화: 직접 엣지 우선, MaxHops=3 제한, 손실 블랙리스트, 손실 경로 UI 경고 배지. + +### ADR-2 · 손실을 ConversionPair.LossClass 가중치로 SSOT화 +- **결정:** ConversionPair에 LossClass(Lossless=0/Container=0.05/Recode=0.4/Rasterize=0.8) 필드를 추가하고, 엣지 가중치를 -log(품질보존율)+홉페널티+실행비용 합산으로 계산한다. UI '손실 변환' 배지도 이 가중치를 소비. +- **근거:** 손실은 본래 곱셈적(0.9×0.8)이므로 -log 변환으로 덧셈 최단경로(Dijkstra)가 곧 최대 품질보존 경로가 된다. 래스터화(텍스트/벡터→PNG)는 단방향 손실 절벽이므로 큰 페널티로 자연 회피. +- **대안:** 동적 손실 측정(Versus식 실측). 초기엔 정적 가중치 테이블로 시작하고 동적 측정은 처음부터 넣지 않는다(과도한 복잡도). +- **트레이드오프:** 정적 가중치는 추정값이라 일부 쌍에서 비최적 경로 가능. 완화: 보수적으로 직접 엣지 우선, 멀티홉은 fallback으로만 운영. + +### ADR-3 · 레지스트리 충돌을 조용한 first-wins에서 Priority 기반 명시 선택으로 교체 +- **결정:** _byPair.TryAdd(ProviderRegistry.cs:22)의 '조용한 첫 등록자 우선'을 ProviderCapability.Priority 필드 + 다중 Provider 공존 모델 + 충돌 시 진단 경고로 교체한다. 같은 (input,output)에 빠른변환/고품질/AI 등 복수 전략 등록 허용. +- **근거:** PDF압축 vs PDF렌더, AI변환 vs 일반변환처럼 한 쌍에 복수 전략이 필연적으로 생긴다. 현재는 부트스트랩 순서에 따라 비결정적으로 한쪽이 조용히 사라져 데이터 손실이다. +- **대안:** 현 first-wins 유지(확장 불가). 비용 기반 자동 선택만(사용자 전략 선택 불가). Priority+공존이 그래프 가중치와도 자연 연결. +- **트레이드오프:** 같은 쌍에 복수 Provider가 등록되면 UI에서 전략 선택지를 노출해야 하는 추가 복잡도. 완화: 기본은 최저비용 자동 선택, 고급 모드에서만 명시 선택. + +### ADR-4 · IConverterProvider 시그니처를 ConvertRequest/ConvertContext로 일반화 +- **결정:** 단일 sourcePath/단일 outputExtension/IProgress 고정 시그니처(IConverterProvider.cs:9-15)를 ConvertRequest(다중 입력·옵션 백·미디어 메타) + ConvertContext로 일반화하고, ConvertResult(ConvertResult.cs)에 ExtractedText/AiResponse/Metadata/IntermediateArtifacts 필드를 추가한다. 기존 8개 Provider는 어댑터로 감싸 무중단 마이그레이션. +- **근거:** 현 시그니처는 N→1 결합, AI 비파일 응답, 영상 메타데이터 프로빙, 멀티홉 중간 컨텍스트를 표현할 수 없다. 미디어/AI 엣지가 들어올 '그릇'을 코어에 먼저 판다. +- **대안:** 시그니처 유지하고 옵션에 모든 것 욱여넣기(갓 오브젝트 가속). 점진 어댑터 전략이 8개 Provider 동시 파괴를 방지. +- **트레이드오프:** 어댑터 계층이 일시적 중복을 만든다. 완화: 회귀 테스트로 동일 동작 보장 후 어댑터를 점진 제거. + +### ADR-5 · AI는 IAiProvider 특수 인터페이스가 아니라 그래프의 부가 엣지로 편입 +- **결정:** LlmProvider를 일반 IConverterProvider로 구현하고 Microsoft.Extensions.AI(IChatClient) 추상화 위에 OpenAI/Anthropic 공식 SDK를 연결한다. AI는 로컬 변환이 없는 신규 페어(요약/번역/캡션/메타데이터)에만 노출되고, 키 부재 시 CheckAvailabilityAsync가 NotReady를 반환해 그래프에서 자동 비활성된다. +- **근거:** AI를 특수 카테고리로 두면 그래프·레지스트리 밖에 별도 배관이 생긴다. 엣지로 환원하면 OcrProvider가 Windows OCR을 흡수한 선례처럼 매트릭스에 자연 편입되고, AI 후처리 파이프(OCR→LLM 교정)도 멀티홉으로 자동 합성된다. +- **대안:** 별도 IAiConverterProvider 확장(추상화 분기 증가). 통합 IConverterProvider가 단순하고 그래프와 정합. +- **트레이드오프:** AI는 비결정적·유료·네트워크 의존이라 '재현 가능한 변환'과 충돌. 완화: ✨AI 배지·기본 경로 불점유·키 없으면 비활성 불변식. + +### ADR-6 · 무거운 외부 도구는 분리 프로세스 호출 + 라이선스 게이트로만 통합 +- **결정:** FFmpeg는 BtbN lgpl-shared 빌드를 별도 프로세스로 호출(LGPL 준수), Ghostscript/MuPDF는 AGPL이라 사용자 설치본 감지만, H2Orestart/Calibre/Pandoc은 GPL이라 외부 프로세스 분리. 공통 ExternalProcessRunner(CliWrap, 타임아웃+stderr+Kill)로 통일하고, 라이선스 경계를 코드 리뷰 게이트로 강제한다. +- **근거:** 단일 포터블 EXE 상업 배포에서 GPL/AGPL 바이너리 정적 링크는 즉시 라이선스 오염이다. 이미 LibreOffice를 외부 도구로 다루는 검증된 패턴을 그대로 확장. +- **대안:** GPL 빌드 번들(라이선스 위반)·상업 라이선스 구매(비용). 분리 호출 + 사용자 설치 감지/LGPL 자동조달이 안전. +- **트레이드오프:** 진정한 자족 EXE가 아니라 외부 의존 체인이 길어진다. 완화: 순수 .NET 라이브러리 우선, 외부 도구는 NotReady로 친절히 안내. + +### ADR-7 · CombineAsync를 ImageCombineProvider(N→1 엣지)로 분리 +- **결정:** ConversionEngine.CombineAsync의 ImageMagick 직접 의존(ConversionEngine.cs:2,164-257)과 정적 HashSet(CombinableInputs/Outputs:14-23)을 IMultiInputProvider 추상화로 분리한다. 엔진은 라이브러리 중립이 되고 결합 가능 형식은 Provider 능력 선언으로 통합. +- **근거:** 현재 '결합'이 Provider 추상화 밖에 있어 엔진이 ImageMagick에 결합되고, PDF 병합·동영상 concat·오디오 믹스 같은 비이미지 결합으로 확장 불가하다. 정적 HashSet과 능력 선언의 이중 관리도 해소. +- **대안:** 현 구조 유지(이미지 결합만 영구 고착). N→1 추상화가 모든 결합을 동일 패턴으로 흡수. +- **트레이드오프:** 결합 진행률 보고가 단일 출력 가정과 달라 재설계 필요. 완화: ConvertProgress를 N→1 케이스로 확장. + +### ADR-8 · ConvertOptions 갓 오브젝트를 그래프 옵션 + 형식별 옵션 백으로 분해 +- **결정:** 11개 sub-record 갓 오브젝트(ConvertOptions.cs:35-55)를 그래프 옵션(AllowMultiHop/MaxHops/AvoidLossy) + 형식별 옵션 백(IReadOnlyDictionary 또는 Provider 선언형 스키마)으로 분해한다. Video/Audio/Ai/PdfCompress를 sub-record 증식 없이 수용하고 ISettingsStore(DPAPI 암호화)로 영속화. +- **근거:** 형식 추가마다 sub-record가 비대해지고 모든 Provider가 무관한 옵션을 끌고 다닌다. 영상 코덱·AI 프롬프트·PDF 압축 레벨을 담을 자리가 코어 record 증식 없이 필요하다. +- **대안:** sub-record 계속 추가(god object 가속). 옵션 백이 형식별 옵션만 주입해 확장성 확보. +- **트레이드오프:** 강타입 안전성이 약화된다. 완화: Provider가 옵션 스키마(이름/타입/범위/기본값)를 선언하고 UI가 동적 생성·검증. + +### ADR-9 · manifest는 풀 DSL이 아니라 단순 CLI용 선언적 인자 템플릿으로 제한 채택 +- **결정:** manifest를 ExternalProcessRunner 위의 '선언적 인자 템플릿({input}/{output}/{outdir}/{format})'으로만 좁게 채택해 qpdf/Ghostscript 같은 단순 CLI 압축 도구를 코드 없이 추가한다. 복잡 로직(FFmpeg HW가속 폴백·AI)은 in-box 코드 Provider 원칙을 P1부터 못박는다. +- **근거:** Provider 8개·테스트 0개 단일 개발자 프로젝트에 풀 manifest DSL·동적 ALC 로더는 ROI가 낮다. FFmpeg의 nvenc→AV1 조건부 폴백은 manifest로 표현 불가하므로 하이브리드 경계가 필수. +- **대안:** 풀 플러그인 생태계(과잉 엔지니어링)·전부 코드(확장 비용). 좁은 manifest가 단순 도구 추가 비용만 제거. +- **트레이드오프:** manifest가 또 다른 갓 오브젝트가 될 위험. 완화: '90% 단순 CLI만 manifest, 복잡 로직은 in-box' 경계를 P1 불변식으로 명문화. + +## 실행 로드맵 + +### P1 · 그래프 엔진 도입 + 즉시 체감 가치(PDF 압축) `effort:L` `risk:medium` `status:planned` +**목표:** ProviderRegistry를 ConversionGraph로 승격하고 멀티홉 경로 탐색을 엔진에 내장한다. 동시에 PDF 압축이라는 즉시 체감 신기능을 출시해 '보이지 않는 리팩터링의 함정'을 회피한다. + +**산출물:** +- ConversionGraph + 자체 Dijkstra PathFinder(외부 의존성 0, .NET 9 PriorityQueue) +- ConversionPair.LossClass 필드 + 정적 가중치 테이블 +- ConversionEngine.ConvertOneAsync 그래프 위임 + ChainExecutor(공용 workDir 헬퍼) +- PdfToolProvider 신설: PDF 압축(Light=PDFsharp 구조최적화, Strong=PDFium 렌더+Magick 재인코딩, Max=Ghostscript 외부폴백) + 병합/분할 +- xUnit 테스트 프로젝트 신설(현재 0개) + 그래프 경로탐색 회귀 테스트 + +**핵심 코드 변경:** +- `ProviderRegistry.cs` — _byPair 위에 인접 리스트 그래프 빌드, 증분 등록 Register/Rebuild 추가 +- `ConversionEngine.cs:91` — TryGet 직접 매핑에서 그래프 FindBestPath→ExecuteChainAsync 위임으로 전환 +- `ConversionPair` — LossClass 필드 추가, 엣지 가중치 SSOT +- `신규 PdfToolProvider` — 동일포맷 pdf→pdf Skip(ConversionEngine.cs:88) 우회, 3단계 압축 + +**Exit Criteria:** 기존 모든 변환이 그래프 경로로 동일 동작(회귀 테스트 통과)하고, PDF 파일을 3단계 레벨로 압축해 출력 용량 감소를 GUI에서 확인 가능. + +### P2 · 손그림 멀티홉 제거 + HWP 한글 양방향 `effort:M` `risk:medium` `status:planned` · depends: P1 +**목표:** DocumentProvider.RouteAsync의 손코딩 switch를 삭제하고 원자 엣지만 선언하게 해 그래프를 도그푸딩한다. HWP→DOCX/HTML/TXT 출력 매트릭스를 확장해 한글 사용자 핵심 요구를 충족. + +**산출물:** +- DocumentProvider.RouteAsync(92-205) 삭제 → md→html, html→docx 등 원자 엣지만 선언, md→docx는 엔진 자동 합성 +- HWP/HWPX 출력 확장: HwpxProvider Outputs에 .docx/.html/.txt/.odt 추가(soffice --convert-to 파라미터화) +- .hwp 입력 시 --infilter='Hwp2002_File' 조건부 지정 + 함초롬/맑은고딕 폰트 누락 감지 경고 +- DocumentProvider 입력에 .pdf 추가 → pdf→docx/html/txt 역변환(soffice) + pdf→txt 무외부 폴백(PdfPig) +- RouteAsync 삭제가 손그림과 동일 동작함을 회귀 테스트로 증명 + +**핵심 코드 변경:** +- `DocumentProvider.cs:92-205` — 멀티홉 switch 삭제, 단일 홉 원자 변환만 선언 +- `HwpxProvider` — Outputs 배열에 .docx/.html/.txt/.odt 추가, soffice 타깃 파라미터화 +- `DocumentProvider Inputs` — .pdf 추가로 PDF 역변환 엣지 개통 + +**Exit Criteria:** HWP/HWPX 파일을 DOCX/HTML/TXT/PDF로 변환 가능하고, md→docx 같은 멀티홉이 RouteAsync 없이 그래프 합성으로 동일하게 동작. + +### P3 · 외부 프로세스 통합 + 인터페이스 일반화 `effort:L` `risk:medium` `status:planned` · depends: P2 +**목표:** 3중 복제된 LibreOffice 호출을 단일 ExternalProcessRunner로 통합하고(타임아웃·stderr·Kill), IConverterProvider/ConvertResult를 일반화해 미디어·AI 엣지가 들어올 그릇을 판다. + +**산출물:** +- ExternalProcessRunner(CliWrap): 타임아웃+stderr수집+Kill 통합, LibreOffice 3중 복제 흡수 +- Abstractions 어셈블리 분리(IConverterProvider/ConvertResult 이전, 타입 동일성) +- IConverterProvider→ConvertRequest/ConvertContext 일반화, 기존 8개 Provider 어댑터로 무중단 마이그레이션 +- ConvertResult에 ExtractedText/AiResponse/Metadata/IntermediateArtifacts 필드 +- ISettingsStore(DPAPI 암호화) 신설 — API 키·도구 경로 영속화 토대 +- Priority 기반 충돌 모델로 _byPair.TryAdd first-wins 교체 + +**핵심 코드 변경:** +- `3개 Provider` — ConvertWithLibreOfficeAsync 복붙을 ExternalProcessRunner로 통합 +- `IConverterProvider.cs:9-15` — ConvertRequest/ConvertContext로 일반화 +- `ConvertResult.cs` — 비파일 산출물 필드 추가 +- `신규 ISettingsStore` — DPAPI ProtectedData 암호화 JSON 영속화 + +**Exit Criteria:** LibreOffice가 멈춰도 타임아웃으로 복구되고 stderr가 에러 메시지에 포함되며, 기존 변환이 일반화된 시그니처로 무중단 동작(회귀 테스트 통과). + +### P4 · 미디어 레이어 — 영상/오디오 코덱·압축 `effort:XL` `risk:high` `status:planned` · depends: P3 +**목표:** FFmpeg로 카테고리를 '미디어 변환기'로 점프시킨다. 영상/오디오 N×M 코덱·압축을 라이선스 안전하게 통합하고 배치 병렬화로 트랜스코딩 병목을 해소. + +**산출물:** +- FfmpegProvider: FFMpegCore(MIT) + 영상(mp4/mkv/webm/mov/avi/gif)·오디오(mp3/aac/m4a/opus/flac/wav) N×M +- 바이너리 조달: ExternalToolDetector.TryFindFfmpeg + BtbN lgpl-shared 자동 다운로드(SHA256 검증), GlobalFFOptions 경로 고정 +- HW 인코더(nvenc/qsv/amf) 우선 + SW 폴백, NotifyOnProgress→IProgress 직결, CancellableThrough(ct) +- 배치 병렬화: ConvertManyAsync 순차 for-loop(57-70)를 Parallel.ForEachAsync로 교체(MaxDegreeOfParallelism) +- PreviewService→IPreviewRenderer 추상화 + FFmpeg 프레임 추출 + 프리뷰 캐시 +- ImageMagick ResourceLimits 전역 설정(decompression bomb 방어) + NU190x 취약점 경고 재활성화 + +**핵심 코드 변경:** +- `신규 FfmpegProvider` — FFMpegCore 래퍼, HW 가속 폴백, RequiresExternal +- `ConversionEngine.cs:57-70` — 순차 for-loop를 Parallel.ForEachAsync로 교체 +- `PreviewService.cs:23` — 닫힌 switch를 IPreviewRenderer 레지스트리로, 영상 프레임 추출 추가 + +**신규 Provider:** FfmpegProvider + +**Exit Criteria:** mp4→webm, wav→mp3 등 영상/오디오 변환이 HW 가속으로 동작하고 진행률·취소가 정확하며, 100개 배치가 멀티코어를 활용. + +### P5 · AI 부가가치 레이어 — Codex OAuth + API `effort:L` `risk:high` `status:planned` · depends: P3 +**목표:** 변환에 'AI가 더 좋게 만든다'는 해자를 얹는다. 기본은 API 키 + 공식 SDK, Codex CLI는 구독자용 opt-in. 키 없으면 AI 페어만 비활성, 기존 변환 무영향. + +**산출물:** +- LlmProvider: Microsoft.Extensions.AI(IChatClient)로 OpenAI/Anthropic 공식 SDK 연결 + Codex CLI opt-in(codex exec --json --output-schema) +- AI 매트릭스: 요약(pdf/docx/txt→txt/md), 번역(→대상언어), OCR교정(OcrProvider 출력 2단계 파이프), 이미지 캡션(png/jpg→txt 비전), 메타데이터(→json Structured Output) +- 키 관리: ISettingsStore DPAPI 암호화 + OPENAI_API_KEY/ANTHROPIC_API_KEY 환경변수 폴백, CheckAvailabilityAsync 게이트 +- UI: AI 출력 페어에 ✨AI 배지(종량과금·네트워크 명시) + 설정에서 백엔드/모델/키 입력 +- Codex 경로 SemaphoreSlim(1) 직렬화(auth.json refresh 토큰 race 방지) 또는 --ephemeral + +**핵심 코드 변경:** +- `신규 LlmProvider` — IConverterProvider로 구현, AI는 로컬 변환 없는 신규 엣지로만 +- `CheckAvailabilityAsync` — 키/codex --version 게이트로 키 부재 시 NotReady→그래프 자동 비활성 +- `UI` — AI 페어 ✨ 배지, 등록 순서로 기본 경로 불점유 보장 + +**신규 Provider:** LlmProvider + +**Exit Criteria:** API 키 입력 시 PDF 요약·이미지 캡션·번역이 동작하고, 키가 없으면 AI 페어만 사라지고 모든 기존 변환은 100% 동작. + +### P6 · 매트릭스 자동 극대화 + 헤드리스 CLI `effort:L` `risk:medium` `status:planned` · depends: P5 +**목표:** 앞 단계에서 쌓인 모든 엣지를 그래프가 자동 합성해 진짜 N×M·다방향을 완성하고(video→mp3→txt AI전사 등), 헤드리스 CLI로 자동화·스크립팅을 개방한다. + +**산출물:** +- OutputsForInput을 transitive closure로 확장 — '이 파일로 만들 수 있는 모든 포맷' UI 노출 + 손실 경로 경고 배지 +- 멀티홉 도그푸딩 검증: hwp→pdf→png, video→mp3→txt(AI) 같은 신규 합성 경로 동작 확인 +- 헤드리스 CLI 분리: --json/--output-dir/--quality/--prompt/--codec/--recursive 플래그 + stdout JSON 결과 + exit code +- 워치폴더 모드(FileSystemWatcher + 디바운스 + 파일잠금 재시도, 출력 디렉터리 분리로 무한루프 방지) +- QuickProgressWindow 취소 토큰 전파 + 케이퍼빌리티 사전 점검 + +**핵심 코드 변경:** +- `ProviderRegistry.cs:52` — OutputsForInput을 그래프 reachability로 확장 +- `CliRouter.cs:21` — 옵션 플래그 파싱 + stdout JSON + exit code +- `App.xaml.cs:96` — Quick 경로에 취소 토큰 전파 + +**Exit Criteria:** HWP 파일에서 PNG까지(멀티홉) 변환 가능하고, CLI가 WPF 창 없이 JSON 결과를 stdout으로 반환해 스크립트가 파싱 가능. + +### P7 · 순수 .NET 카테고리 보강 + manifest 어댑터 `effort:L` `risk:low` `status:planned` · depends: P6 +**목표:** EXE 번들 가능한 순수 관리 라이브러리로 빈 카테고리를 채우고, 단순 CLI 도구를 코드 없이 추가하는 좁은 manifest 어댑터를 도입한다. + +**산출물:** +- ArchiveProvider(SharpCompress, 순수관리) — zip/7z/tar/gz/bz2 +- DataProvider(Parquet.Net/ClosedXML/CsvHelper) — csv↔json↔xlsx↔parquet +- VectorProvider(Svg.Skia) — svg→png/jpg/webp/pdf, EPS는 Magick+Ghostscript +- PandocProvider(외부 CLI) — md/rst/latex/ipynb/epub 마크업 매트릭스, LibreOffice 겹침은 Priority 라우팅 +- EbookProvider(Calibre ebook-convert, 외부) — epub↔mobi↔azw3↔pdf +- manifest 어댑터(ExternalProcessRunner 위 인자 템플릿): qpdf/gs 같은 단순 CLI 코드 없이 추가 + +**핵심 코드 변경:** +- `신규 4-5개 Provider` — 순수 .NET은 EXE 직접 포함, 외부 CLI는 분리 호출 +- `ManifestLoader` — tools/*.manifest.json으로 단순 CLI 엣지 추가 +- `Bootstrap` — 하드코딩 배열에 신규 Provider 등록 + manifest 동적 등록 + +**신규 Provider:** ArchiveProvider, DataProvider, VectorProvider, PandocProvider, EbookProvider + +**Exit Criteria:** zip 압축/해제, csv→xlsx, svg→png가 외부 도구 없이 동작하고, manifest 파일 하나로 새 CLI 변환 도구를 코어 재컴파일 없이 추가 가능. + +### P8 · 확장성·신뢰성·배포 굳히기 `effort:L` `risk:low` `status:planned` · depends: P7 +**목표:** 기능이 다 들어온 뒤 회귀 방지·UI 분해·배포를 다진다. 차별화는 끝났으니 여기서부터는 깨지지 않게 유지. + +**산출물:** +- UI MVVM 분해(MainWindow.xaml.cs 1171줄) + Provider 선언형 옵션 스키마 기반 동적 옵션 UI 생성 +- 히스토리 도메인 로직을 Core로 분리 + 데모 시드 제거 + 스트리밍 로드/회전 정책 +- CI 강화: NuGet 캐시 + self-contained portable EXE 산출 + 외부 바이너리 번들링 파이프라인(FFmpeg LGPL 고지) + dotnet test 게이트 +- 출력 형식 매트릭스 3중 중복(AllFormats/PopularOutputs/파일다이얼로그)을 단일 FormatCatalog로 통합 +- 테스트 확대: OutputPathHelper 충돌·결합 로직·JSONL round-trip 순수 함수 커버 + +**핵심 코드 변경:** +- `MainWindow.xaml.cs` — MVVM 분해, 동적 옵션 UI +- `BuildMsix.ps1` — 외부 바이너리 번들 + 라이선스 고지 단계 +- `build.yml/release.yml` — 캐시+테스트 게이트+self-contained 산출물 + +**Exit Criteria:** PR마다 테스트가 게이트로 동작하고, self-contained portable EXE가 자동 산출되며, 새 형식 추가가 단일 FormatCatalog 한 곳 수정으로 끝남. + +## 변환 매트릭스 · 그래프 라우팅 + +- **현재:** 8개 Provider가 PairsFromMatrix로 N×M 쌍을 선언하지만 ProviderRegistry는 (input,output) 단일 홉 딕셔너리(_byPair)만 매핑한다. 멀티홉(md→docx)은 DocumentProvider.RouteAsync(92-205)에 손코딩되어 형식 N개에 O(N²)로 수동 증식한다. 매트릭스는 '거의 모든 것→이미지/PDF/텍스트' 단방향으로만 풍부하고, 역방향(이미지/PDF→편집문서, HWP 출력, 미디어/아카이브)이 구조적으로 비어 있다. 동일포맷(pdf→pdf 압축)은 ConversionEngine.cs:88에서 무조건 Skip된다. +- **목표:** ConversionGraph가 모든 Provider Capability를 순회해 방향 그래프를 빌드하고, Dijkstra가 임의의 A→Z를 원자 엣지 조합으로 자동 합성한다. OutputsForInput은 transitive closure로 확장되어 '이 파일로 만들 수 있는 모든 포맷'을 노출한다. PDF/HWP 양방향, 영상/오디오, 아카이브/데이터/벡터, AI 후처리가 모두 엣지로 편입되고, 동일포맷 압축(pdf→pdf)도 옵션으로 허용되는 엣지가 된다. + +**구조적 공백:** +- PDF 압축(pdf→pdf): 어떤 Provider도 수행 못 함 — PdfToolProvider 신설 필요 +- PDF→DOCX/HTML 역변환: 편집가능 역변환 경로 전무 — LibreOffice 경유 추가 +- HWP/HWPX 출력: H2Orestart import 전용이라 →HWP 불가, →DOCX/HTML/TXT도 미노출 +- 영상/오디오 전 카테고리: mp4/mp3/flac 등 미디어 Provider 0개 +- 아카이브/폰트/벡터/데이터/전자책: 빈 카테고리(ComingSoon enum 미사용) +- AI 변환(요약/번역/캡션): 추상화·옵션·Provider 어디에도 자리 없음 +- 동일포맷 최적화(이미지 리인코딩, PDF 압축): ConversionEngine.cs:88에서 Skip되어 표현 불가 + +**그래프 라우팅 설계:** 1) 그래프 빌드(앱 시작 1회): ProviderRegistry 생성자 루프(16-30)에서 각 Provider의 Capability.SupportedConversions를 순회해 인접 리스트 Dictionary>를 구축한다. 노드=정규화된 확장자(.png/.pdf/.docx), 엣지=Edge{Provider, ConversionPair, Weight}. 노드 수십·엣지 수백 규모라 그래프는 매우 작다. 2) 가중치: 각 ConversionPair.LossClass(Lossless=0/Container=0.05/Recode=0.4/Rasterize=0.8)를 -log(품질보존율)로 환산하고 홉페널티(작은 상수)와 실행비용(외부 프로세스>in-process)을 가중합한다. 손실은 곱셈적이므로 -log 변환으로 덧셈 최단경로가 곧 최대 품질보존 경로가 된다. 3) 탐색: .NET 9 System.Collections.Generic.PriorityQueue로 Dijkstra(O(E log V), 80~120줄)를 자체 구현한다. ConversionEngine.ConvertOneAsync(91)에서 직접 엣지가 있으면 1홉(기존 동작 호환), 없으면 FindBestPath(inExt,outExt,options)로 멀티홉 경로를 구한다. AllowMultiHop(기본 true)/MaxHops(기본 3)/AvoidLossy 옵션으로 게이트. 4) 실행: ChainExecutor가 경로의 각 홉을 순차 실행하며 중간 산출물을 공용 workDir(Temp/e2e_{Guid})에 체이닝하고, 진행률을 홉 수로 분할해 IProgress에 매핑한다. 각 홉은 기존 provider.ConvertAsync를 그대로 호출(인터페이스 변경 불필요). 5) 안전장치: 멀티홉은 직접 엣지가 없을 때만 발동, 손실 블랙리스트(텍스트→래스터 같은 도메인 경계 전이 통제), 한 홉 실패 시 어느 홉에서 실패했는지 사용자에게 전달. 6) UI: OutputsForInput을 reachability(transitive closure)로 확장하고, 손실 경로로만 도달하는 출력에 '손실 변환' 경고 배지를 붙인다. + +## AI 통합 + +- **Codex OAuth:** Codex CLI를 PATH에서 감지될 때만 활성화되는 구독자용 opt-in 보조 백엔드로 둔다. 핵심 제약: ChatGPT 구독 OAuth 토큰(auth.json의 access/refresh)은 Codex 백엔드 전용이라 api.openai.com에 직접 Bearer로 붙일 수 없다 — 구독 재사용은 오직 codex CLI 프로세스 호출로만 가능. 실행은 ExternalProcessRunner로 `codex exec --skip-git-repo-check --json --output-schema schema.json -o out.json --cd "<프롬프트 + 파일경로>"` 형태. --skip-git-repo-check는 변환 앱에 필수(git 저장소 아닌 폴더 허용), --output-schema로 응답을 JSON Schema로 강제해 메타데이터 추출, --json으로 JSONL 이벤트 스트림 파싱. CheckAvailabilityAsync에서 `codex --version` 프로브 + auth.json 존재 확인. auth.json refresh 토큰 race를 막기 위해 SemaphoreSlim(1) 직렬화 또는 --ephemeral 사용. +- **API 모드:** 기본 경로는 API 키 + 공식 SDK다. Microsoft.Extensions.AI(IChatClient, MIT) 단일 추상화로 OpenAI(공식 OpenAI 패키지, MIT)와 Anthropic(공식 Anthropic 패키지, MIT)을 동일 인터페이스로 다룬다. 사용자는 설정에서 'OpenAI / Claude / Codex CLI / auto'를 고르고 API 키만 입력한다. 키는 ISettingsStore에서 System.Security.Cryptography.ProtectedData(DPAPI, CurrentUser)로 암호화해 %LOCALAPPDATA%에 저장하고, OPENAI_API_KEY/ANTHROPIC_API_KEY 환경변수도 폴백으로 읽어 CI/파워유저 친화. CheckAvailabilityAsync가 키 부재 시 NotReady(키 발급 URL을 ExternalDependency로 안내)를 반환해 그래프에서 자동 비활성. +- **아키텍처:** LlmProvider를 별도 IAiProvider가 아닌 일반 IConverterProvider로 구현해 그래프의 부가 엣지로 편입한다(ADR-5). AI는 로컬 변환이 없는 신규 페어(요약/번역/캡션/메타데이터)에만 노출되며, 등록 순서로 '로컬 변환이 이미 있는 페어는 로컬 Provider가 우선, AI는 신규 페어만'을 보장한다(Priority 충돌 모델). 불변식: 키가 없어도 모든 기존 변환은 100% 동작하고 AI 페어만 비활성, AI는 절대 기본 경로를 점유하지 않으며 UI에 ✨AI 배지(종량과금·네트워크 명시)로만 opt-in 노출된다. 텍스트 추출이 필요하면 DocumentProvider/PdfProvider/OcrProvider를 주입받아 '추출→LLM' 2단계로 구성(OcrProvider가 PdfProvider를 주입받는 선례). 프라이버시: 로컬 문서가 외부 서버로 전송되므로 명시적 동의 토글 필수(기본 OFF), 미래에 Ollama 로컬 모델 경로를 IChatClient로 열어둔다. +- **활용 사례:** + - 요약: pdf/docx/txt/md → txt/md (긴 문서를 LLM이 요약) + - 번역: txt/docx/md → txt/docx (대상 언어는 옵션, 비파일 입력 LLM 왕복) + - OCR 교정: OcrProvider 출력(.txt)을 받아 LLM이 오탈자/줄바꿈 정리 (그래프가 OCR→LLM 2단계 멀티홉으로 자동 합성) + - 이미지 캡션/대체텍스트: png/jpg → txt (비전 모델) + - 문서 언어 번역 + 포맷 정규화: csv→md(표), txt→md + - 메타데이터 생성: 임의 입력 → json (제목/태그/요약, Structured Outputs로 구조화) + +## 미디어 레이어 + +- **영상:** FfmpegProvider(FFMpegCore 5.4.0, MIT)로 mp4/mkv/webm/mov/avi/gif N×M 트랜스코딩. H.264/H.265는 HW 인코더(h264_nvenc/qsv/amf) 우선, LGPL 빌드엔 libx264/x265(GPL)가 없으므로 HW 미지원 시 AV1(libaom)/VP9(libvpx, 둘 다 BSD-like royalty-free)로 폴백. FFprobe로 duration 확보 후 NotifyOnProgress(Action,TimeSpan)을 IProgress에 직결, CancellableThrough(ct)로 취소. +- **오디오:** 오디오는 mp3/aac/m4a/opus/ogg/flac/wav N×M. AAC는 FFmpeg 네이티브 aac 인코더(LGPL, libfdk-aac=nonfree 회피), Opus/FLAC/MP3는 LGPL 빌드로 직접 처리. 오디오 전용 출력(flac/mp3)은 영상 입력에서 오디오 트랙만 추출. +- **PDF 압축:** PdfToolProvider 3단계: Light=PDFsharp(MIT, in-process) 또는 qpdf(Apache 2.0) 구조 최적화(object stream 압축·linearize), Strong=PDFium 렌더+ImageMagick 재인코딩(텍스트 선택성 잃지만 라이선스 안전), Max=Ghostscript(-dPDFSETTINGS /screen)는 AGPL이라 번들 금지·사용자 설치본 감지만. 병합/분할/암호화는 PDFsharp 또는 qpdf. +- **이미지 최적화:** 기존 MagickProvider의 ApplyEncoding(jpg/png/webp/avif/tiff 품질·알파평탄화·MaxLongEdge)을 공용 ImageEncoder 헬퍼로 추출해 PdfProvider/HtmlProvider/CombineAsync의 4중 복제를 제거. 동일포맷 이미지 리인코딩(품질 조절)도 엣지로 허용. +- **외부 바이너리·라이선스 전략:** 단일 포터블 EXE 부담을 줄이기 위해 무거운 바이너리(FFmpeg ~100MB)는 절대 번들하지 않고 'RequiresExternal + 최초 사용 시 자동 다운로드' 모델. 라이선스 게이트(코드 리뷰 강제): FFmpeg는 BtbN lgpl-shared 빌드(--enable-gpl/nonfree 없음)를 별도 프로세스로 호출(동적 분리)해 LGPL 준수 — gyan.dev/BtbN gpl 빌드(GPLv3) 번들 절대 금지. ExternalToolDetector.TryFindFfmpeg가 (a)%LOCALAPPDATA%\Everything2Everything\ffmpeg, (b)시스템 PATH 순 탐지, 없으면 lgpl-shared zip을 SHA256 검증 후 다운로드. GlobalFFOptions.Configure로 경로 고정. NVENC는 LGPL 빌드에서 --enable-nonfree 없이 합법 사용 가능(NVIDIA 공식 확인). About 화면에 'uses FFmpeg under LGPLv2.1' 고지 + 소스 다운로드 링크(LGPL 의무). MSIX 변형에서는 샌드박스 정책상 lgpl-shared DLL을 패키지 동봉(여전히 LGPL 준수). Ghostscript/MuPDF(AGPL)는 사용자 설치본 감지만, codec 특허(H.264/AAC) 위험을 줄이려 AV1/VP9/Opus/FLAC(royalty-free)를 기본 권장 출력으로. + +## 리스크 레지스터 + +| 리스크 | 발생 | 영향 | 완화책 | +|---|---|---|---| +| '보이지 않는 리팩터링의 함정' — 그래프 코어 재설계가 사용자 체감 변화 0인 상태로 길어짐 | medium | high | P1에서 그래프 도입과 PDF 압축(즉시 체감 신기능)을 묶고, transitive closure로 늘어나는 '만들 수 있는 포맷 목록'을 가시 성과로 노출. DocumentProvider.RouteAsync 삭제를 회귀 테스트로 동일 동작 증명. | +| '최단 경로' 압박으로 LlmProvider/FfmpegProvider를 또 하드코딩 switch로 끼워넣어 RouteAsync 지옥 재생산 | medium | high | P1에 그래프 코어를 먼저 심어 하드코딩을 구조적으로 차단. '신규 기능은 그래프 엣지로만 추가'를 불변식으로 명문화하고 코드 리뷰 게이트로 강제. | +| GPL/AGPL 바이너리(FFmpeg gpl빌드·Ghostscript·H2Orestart) 정적 링크로 상업 배포 라이선스 오염 | medium | high | 모든 무거운 외부 도구를 별도 프로세스 분리 호출 + 사용자 설치 감지/LGPL 빌드 자동조달로만 통합. 라이선스 경계를 코드 리뷰 게이트로 강제(ADR-6). | +| 멀티홉 손실 누적·은폐 — HWP→PDF(래스터화)→DOCX가 '편집가능'을 약속하나 이미지 덩어리 반환 | medium | medium | LossClass 가중치로 래스터화에 큰 페널티, 멀티홉은 직접 엣지 없을 때만, MaxHops=3, 손실 블랙리스트, 손실 경로 UI 경고 배지 3겹 가드레일. | +| 인터페이스 일반화(ConvertRequest)가 8개 기존 Provider를 한 번에 깸 | medium | high | 기존 시그니처를 어댑터로 감싸 점진 마이그레이션, 무중단을 회귀 테스트로 보장. P3에 배치해 미디어/AI 동기가 코드에 들어온 뒤 일반화. | +| AI 비결정성·종량과금·네트워크 의존이 '로컬 예측가능 변환' 신뢰를 깸 | high | medium | AI는 기본 경로 불점유, ✨AI 배지 opt-in, 키 없으면 조용히 비활성을 설계 불변식으로 박음. 토큰/비용 표시, 사용자 확인 게이트, 재시도·백오프. | +| 테스트 0개 상태에서 대규모 코어 변경이 회귀를 탐지 못 함 | high | high | P1에서 xUnit 테스트 프로젝트를 최우선 신설하고 그래프 경로탐색·DocumentProvider 회귀를 첫 안전망으로. CI에 dotnet test 게이트 추가. | +| manifest가 또 다른 갓 오브젝트화 — FFmpeg HW가속 폴백 같은 복잡 로직을 manifest로 표현 시도 | low | medium | manifest는 '90% 단순 CLI(qpdf/gs)만, 복잡 로직은 in-box 코드 Provider' 경계를 P1부터 불변식으로 명문화. | + +## 성공 지표 + +| 지표 | 현재 | 목표 | +|---|---|---| +| 멀티홉 경로 자동 합성 | DocumentProvider.RouteAsync에 손코딩된 3-4개 체인만 동작 | 엔진이 임의 A→Z를 그래프 탐색으로 자동 합성, RouteAsync 0줄 | +| 입력당 도달 가능 출력 포맷 수 | 1-hop 직접 출력만(OutputsForInput 직접 매핑) | transitive closure로 확장된 도달 가능 전체 포맷 + 손실 배지 | +| 지원 카테고리 수 | 이미지/PDF/문서/HEIC/OCR (약 5) | +영상/오디오/아카이브/데이터/벡터/전자책/AI (약 12) | +| PDF 압축 기능 | 어떤 Provider도 수행 불가 | 3단계 레벨(Light/Strong/Max) 압축 + 병합/분할 | +| HWP 출력 매트릭스 | →PDF/이미지만, →DOCX/HTML/TXT 미노출 | HWP→DOCX/HTML/TXT/PDF 완성 | +| 코어 테스트 커버리지 | 테스트 프로젝트 0개 | 그래프 탐색·OutputPathHelper·결합·JSONL round-trip 커버 + CI 게이트 | +| 배치 처리 동시성 | 순차 for-loop(코어 1개만 사용) | Parallel.ForEachAsync(MaxDegreeOfParallelism)로 멀티코어 활용 | +| CLI 자동화 가능성 | WPF 창만 띄우고 stdout 무반환 | --json/--codec/--prompt 플래그 + stdout JSON + exit code | + +## 다음 세션 인계 노트 (Handoff) + +다음 세션은 P1(그래프 엔진 도입 + PDF 압축)부터 시작한다. 시작 순서와 검증 포인트: + +1) 가장 먼저 xUnit 테스트 프로젝트를 신설하라(현재 0개). 이게 모든 코어 변경의 안전망이며, 특히 DocumentProvider.RouteAsync 삭제가 '손그림과 동일 동작'임을 증명할 회귀 테스트의 전제다. 먼저 현재 RouteAsync의 모든 경로(md→docx, docx→md, hwp→html 등)에 대한 골든 테스트를 작성해 baseline을 고정하라. + +2) ConversionGraph + 자체 Dijkstra를 ProviderRegistry 옆에 얇게 얹어라. ProviderRegistry.cs:16-30 생성자 루프에 그래프 빌드 한 단계만 추가. _byPair는 유지(직접 엣지 1홉 호환). ConversionPair에 LossClass 필드 추가가 선결. + +3) 첫 검증: ConversionEngine.ConvertOneAsync(91)를 그래프 위임으로 바꾼 뒤, 기존 모든 변환이 동일 동작하는지 회귀 테스트로 확인. 그 다음에야 RouteAsync를 삭제하고 원자 엣지만 선언하게 바꿔 md→docx가 그래프 합성으로 동일하게 나오는지 검증. + +4) PDF 압축(PdfToolProvider)은 ConversionEngine.cs:88의 동일포맷 Skip을 우회해야 한다 — pdf→pdf를 엣지로 허용하는 메커니즘이 그래프 도입과 함께 필요. PDFsharp(MIT) in-process 압축부터 시작하면 외부 의존성 0으로 즉시 체감 가치. + +먼저 검증할 불변식: (a) 기존 8개 Provider 변환이 그래프 경로로 100% 동일 동작, (b) 멀티홉은 직접 엣지 없을 때만 발동, (c) 손실 경로에 가중치가 정확히 반영되는지. 라이선스 게이트(GPL/AGPL 분리 호출)는 P4(미디어)부터 본격 적용되지만, P1의 Ghostscript 폴백에서도 '사용자 설치본 감지만, 번들 금지' 원칙을 처음부터 지켜라. + +참고: 빌드 후에는 메모리의 project_build_pipeline(publish + 카스케이드 재등록 PowerShell 시퀀스)를 따르고, 사용자가 직접 push & GUI 검증하는 워크플로이므로 큰 결정은 빠른 승인 후 단일 commit으로 진행. + +--- +## 심사 종합 권고 + +승자는 idx 2(AI·미디어 기능 우선, 89점)를 '실행 골격'으로, idx 0(그래프 코어, 84점)을 '아키텍처 영혼'으로 삼아 종합한다. 단독 채택이 아니라 두 안의 합성이 정답이다. + +핵심 통찰: 세 안의 기술적 부품(자체 Dijkstra 멀티홉, ExternalProcessRunner 통합, ConvertRequest/ConvertResult 일반화, LossClass 가중치, Priority 충돌 모델, AI=엣지, DPAPI 키저장)은 사실상 동일하다. 진짜 차이는 '무엇을 북극성으로 삼아 순서를 짜느냐' 하나뿐이다. 이 프로젝트는 단일 개발자가 직접 push하고 GUI로 검증하며 큰 결정을 빠르게 승인하는 워크플로(프로젝트 메모리)이고, 사용자가 명시적으로 요구한 것은 AI/미디어/HWP/PDF압축이라는 '기능'이다. 따라서 '보이지 않는 리팩터링의 함정'(idx 0 본인이 인정한 최대 리스크)에 빠지는 그래프-우선 순서는 이 맥락에서 부적합하다. + +그러나 idx 2의 최대 리스크('최단 경로 압박으로 LlmProvider/FfmpegProvider를 또 하드코딩 switch로 끼워넣어 RouteAsync 지옥 재생산')는 실재하고, idx 0의 그래프 코어가 바로 이 리스크의 백신이다. 그래서 둘을 봉합하는 마스터플랜은 다음 순서다: + +Phase 0 (기반, 그러나 즉시 가치와 묶기 — idx 0의 자기 완화책 채택): ExternalProcessRunner 통합(3중 복제 제거) + Priority 충돌 모델 + ConvertRequest/ConvertResult 일반화(어댑터로 무중단). 동시에 DocumentProvider.RouteAsync를 원자 엣지로 분해하고 자체 Dijkstra를 넣어 '손그림=그래프 동일 동작'을 회귀 테스트로 증명(테스트 0개 탈출의 첫걸음). 이 단계의 가시 성과는 transitive closure로 늘어나는 '만들 수 있는 포맷 목록'. + +Phase 1 (즉시 체감 — idx 2 순서): HWP 출력 매트릭스 확장(이미 깔린 LibreOffice+H2Orestart 배관 재사용 → DOCX/HTML/TXT) + PDF 압축. 이때 신규 기능은 반드시 '그래프 엣지'로만 추가한다는 것을 불변식으로 박아 idx 2의 하드코딩 유혹을 Phase 0 그래프가 구조적으로 차단. + +Phase 2~3 (미디어 + AI 부가가치 레이어 — idx 2): FFmpeg/Ghostscript/qpdf를 idx 2의 라이선스 게이트(분리 프로세스·LGPL/AGPL 경계) 하에 엣지로 추가. AI는 idx 0의 '엣지' + idx 2의 '✨AI 배지·키 없으면 비활성·기본 경로 불점유' 이중 불변식으로 통합. 배치 병렬화는 이 시점에 필수. + +idx 1(플러그인 생태계, 71점)은 베이스로는 과잉 엔지니어링이라 탈락하지만, 두 아이디어는 흡수한다: (1) Priority 기반 충돌 모델(이미 Phase 0에 편입), (2) manifest를 '풀 DSL'이 아니라 'ExternalProcessRunner 위 선언적 인자 템플릿'으로 축소해 qpdf/gs 같은 단순 CLI를 코드 없이 추가하는 좁은 용도로만 채택. 복잡 로직(FFmpeg HW가속·AI)은 in-box 코드 원칙을 P1부터 못박아 manifest 갓오브젝트화를 방지한다(idx 1 본인의 하이브리드 경계 그대로). + +한 줄 요약: idx 2의 '기능이 견인하는 로드맵'에 idx 0의 '그래프가 받치는 코어'를 Phase 0에 심어, 사용자 체감 가치를 빠르게 내면서도 RouteAsync 지옥의 재발을 그래프로 원천 차단한다. + +### 마스터플랜에 흡수한 최고의 아이디어 + +- [idx 0의 핵심] DocumentProvider.RouteAsync(92-205) 손그림 멀티홉을 '삭제'하고 엔진이 Dijkstra로 동일 경로를 계산하게 만드는 도그푸딩 — 이것을 회귀 테스트로 '그래프=손그림 동일 동작' 객관 증명. 테스트 0개인 현 상태에서 이 변환의 첫 안전망이 된다. 어떤 마스터플랜이 채택되든 이 검증 루프는 필수. +- [idx 0의 핵심] 손실을 -log(보존율)+홉페널티 단일 가중치로 ConversionPair.LossClass(Lossless=0/Container=0.05/Recode=0.4/Rasterize=0.8) 필드에 SSOT화. 이것이 멀티홉 경로 선택과 UI '⚠손실' 배지의 단일 출처. idx 2의 '손실 변환 경고 배지'도 이 가중치를 그대로 소비. +- [idx 0의 핵심] AI를 IAiProvider 특수 인터페이스가 아니라 '로컬 변환이 없는 신규 엣지(요약/번역/캡션)'로 그래프에 환원 — idx 2의 '후처리 부가가치 레이어'와 결합하면, AI는 그래프상 엣지이면서 동시에 키 없으면 자동 비활성 노드 + ✨AI 배지로 노출되는 이중 안전장치를 얻는다. +- [idx 2의 핵심] AI 불변식: 키가 없어도 모든 기존 변환 100% 동작, AI는 절대 기본 경로를 점유하지 않고 ✨AI 배지 페어로만 opt-in, 키 부재 시 등록 순서·게이트로 조용히 비활성. '변환은 로컬에서 예측가능' 신뢰를 깨지 않는 설계 불변식. +- [idx 2의 핵심] 라이선스 경계를 코드 리뷰 게이트로 강제: FFmpeg는 GPL 정적링크 금지·LGPL 분리호출만, Ghostscript/MuPDF는 AGPL이라 사용자 설치본 감지만, H2Orestart/Calibre는 GPL이라 외부 프로세스 분리. 모든 무거운 외부 도구 = '별도 프로세스 분리 호출 + 사용자 설치 감지 또는 LGPL 빌드 자동조달'. .NET 9 단일 EXE 상업 배포 오염 방지의 핵심. +- [idx 2의 핵심] 단계 순서: PDF 압축 + HWP 출력 매트릭스 확장을 최우선 출시(이미 HwpxProvider의 LibreOffice+H2Orestart 배관 존재 → DOCX/HTML/TXT 출력만 추가하면 즉시 신규 가치). 초기 체감 가치를 그래프 리팩터링보다 먼저. +- [idx 2의 핵심] 배치 병렬화: ConversionEngine 순차 for-loop(57-70)를 Parallel.ForEachAsync(동시성 제한 포함)로 교체. AI 네트워크 왕복·영상 트랜스코딩의 치명적 병목 해소. 더불어 ImageMagick ResourceLimits 전역 설정 + decompression bomb 방어로 미디어 공격면 차단. +- [idx 1의 핵심] 레지스트리 충돌 모델 교체: _byPair.TryAdd(22)의 조용한 first-wins를 ProviderCapability.Priority + 다중 Provider 공존으로 교체 + 충돌 시 진단 경고. 같은 (input,output)에 빠른변환/고품질/AI 등 복수 전략 등록 가능. idx 0/2 모두 이 교체가 전제 조건. +- [idx 1의 부분 채택] manifest는 '풀 생태계 비전'이 아니라 'ExternalProcessRunner 위의 선언적 인자 템플릿({input}/{output}/{outdir}/{format})'으로만 제한 채택 — qpdf/Ghostscript 같은 단순 CLI 압축 도구를 코드 없이 추가하는 용도. 단, 복잡 로직(FFmpeg HW가속 폴백/AI)은 in-box 코드 Provider 원칙을 P1부터 못박아 manifest 갓오브젝트화 방지. +- [3안 공통] ExternalProcessRunner 단일 추상화로 LibreOffice 3중 복제(DocumentProvider:238-281 / DocxProvider:113-157 / HwpxProvider:107-151) 통합 — 타임아웃·stderr 수집·Kill 일원화. 이후 모든 외부 엣지(FFmpeg/Ghostscript/qpdf/Codex CLI)가 이 러너 하나 공유. 세 안이 만장일치로 지목한 가장 안전하고 즉시 실행가능한 첫 리팩터링. +- [3안 공통] IConverterProvider 시그니처(9-15)를 ConvertRequest/ConvertContext로 일반화 + ConvertResult(10)에 비파일 산출물 필드(추출 텍스트·AI 응답·미디어 메타데이터) 추가. 단, 기존 8개 Provider는 어댑터로 감싸 점진 마이그레이션 + 회귀 테스트로 무중단 보장(idx 0의 마이그레이션 전략 채택). +- [3안 공통] ConvertOptions 갓 오브젝트(14+ sub-record)를 그래프 옵션(AllowMultiHop/MaxHops/AvoidLossy) + 형식별 옵션 백(IReadOnlyDictionary 또는 Provider 선언형 스키마)으로 분해 → Video/Audio/Ai/PdfCompress를 sub-record 증식 없이 수용. DPAPI(ProtectedData) 기반 ISettingsStore 신설로 API 키·도구 경로 안전 저장. \ No newline at end of file diff --git a/docs/ssot/README.md b/docs/ssot/README.md new file mode 100644 index 0000000..ada2b69 --- /dev/null +++ b/docs/ssot/README.md @@ -0,0 +1,38 @@ +# Everything2Everything — SSOT (Single Source of Truth) + +이 폴더는 프로젝트의 **장기 마스터플랜 SSOT**다. "변환 프로그램의 극한 — 진짜 양방향·다방향 변환 + AI + 미디어"라는 목표를 향한 아키텍처 비전·핵심 결정(ADR)·8단계 로드맵·리스크·근거(인터넷 리서치 + 코드 분석)를 한곳에 모았다. + +생성 방법: 멀티에이전트 Workflow 오케스트레이션 (5 코드분석 + 7 인터넷리서치 → 3 독립 아키텍트 → 심사 랭킹 → 마스터플랜 종합). 2026-06-01, 17 agents / 1.43M tokens. + +## 파일 구조 + +``` +docs/ssot/ +├─ index.html ← 웹 대시보드 (브라우저로 열기). _data 에서 생성됨, 직접 편집 금지 +├─ PLAN.md ← 마크다운 SSOT (사람·다음 세션이 읽는 텍스트). 생성됨, 직접 편집 금지 +├─ build.py ← 제너레이터. _data/*.json → index.html + PLAN.md +├─ README.md ← 이 파일 +└─ _data/ ← ★진짜 SSOT 원천 데이터 (여기를 고친다) + ├─ master.json ← 마스터플랜 (비전·원칙·아키텍처·ADR·로드맵·매트릭스·AI·미디어·리스크·지표·인계노트) + ├─ status.json ← 로드맵 단계별 진행 상태 (planned|in_progress|done) + ├─ analyses.json ← 5개 서브시스템 코드 심층 분석 + ├─ researches.json← 7개 토픽 인터넷 리서치 (라이브러리·라이선스·출처) + ├─ designs.json ← 3개 독립 설계안 + ├─ ranking.json ← 설계안 심사 점수 + 흡수 아이디어 + 종합 권고 + └─ meta.json ← 생성 메타데이터 +``` + +## 갱신 워크플로 (다음 세션) + +1. **진행 표시**: 단계를 시작/완료하면 `_data/status.json` 의 해당 단계를 `in_progress` / `done` 으로 바꾼다. +2. **계획 수정**: 로드맵·ADR이 바뀌면 `_data/master.json` 을 편집한다 (HTML/MD를 직접 고치지 말 것). +3. **재생성**: `python docs/ssot/build.py` → `index.html` 과 `PLAN.md` 가 다시 만들어진다. + +## 핵심 요약 (TL;DR) + +- **승자 종합**: idx2(AI·미디어 우선, 89점)를 실행 골격 + idx0(그래프 코어, 84점)을 아키텍처 영혼. +- **북극성**: 모든 변환을 단일 홉 "엣지"로 등록 → 엔진(자체 Dijkstra)이 멀티홉 A→Z를 자동 합성하는 변환 그래프 OS. +- **P1부터**: ① xUnit 테스트 프로젝트 신설(현재 0개) → ② `ProviderRegistry`를 `ConversionGraph`로 승격 + 자체 Dijkstra → ③ `DocumentProvider.RouteAsync`(손그림 멀티홉) 삭제를 회귀 테스트로 "동일 동작" 증명 → ④ PDF 압축(PDFsharp) 즉시 체감 가치. +- **불변식**: AI는 키 없으면 조용히 비활성(기존 변환 100% 동작), 무거운 외부 도구(FFmpeg/Ghostscript)는 GPL/AGPL 라이선스 게이트로 분리 프로세스 호출만. + +자세한 내용은 `PLAN.md` 또는 `index.html` 참조. diff --git a/docs/ssot/_data/analyses.json b/docs/ssot/_data/analyses.json new file mode 100644 index 0000000..19bda2b --- /dev/null +++ b/docs/ssot/_data/analyses.json @@ -0,0 +1,576 @@ +[ + { + "key": "core-engine", + "subsystem": "Core 추상화 & 변환 엔진 (Everything2Everything.Core)", + "summary": "변환 능력을 IConverterProvider로 추상화하고, ProviderRegistry가 (입력확장자, 출력확장자) 쌍을 단일 홉 딕셔너리로 매핑하며, ConversionEngine이 단일/배치/결합 변환을 오케스트레이션하는 구조다. 양방향 N×M 매트릭스는 ProviderCapability.PairsFromMatrix로 각 Provider가 자기 입력·출력의 데카르트 곱을 선언해 표현하지만, 멀티홉 경로는 엔진이 아니라 각 Provider 내부에 하드코딩(DocxProvider/HwpxProvider→PdfProvider, HeicProvider→MagickProvider, DocumentProvider의 거대 switch)되어 있다. 이미지 중심으로 설계가 견고하게 동작하지만, 진정한 다방향 변환·AI·영상/오디오로 확장하려면 핵심 추상화 자체의 재설계가 필요하다.", + "strengths": [ + "ConversionPair.Normalize(ConversionPair.cs:8)로 확장자 정규화를 한 곳에 모으고, 모든 룩업·비교를 OrdinalIgnoreCase로 일관되게 처리해 대소문자/점 누락 버그를 구조적으로 차단함", + "ProviderCapability(ProviderCapability.cs:18)가 능력 선언(SupportedConversions)·상태(ProviderStatus)·외부 의존성(ExternalDependency)·로드맵을 하나의 불변 record로 캡슐화 — UI가 메뉴/가용성을 메타데이터만으로 생성할 수 있는 데이터 주도 설계", + "CheckAvailabilityAsync/ProviderAvailability(IConverterProvider.cs:7,18) 패턴으로 외부 도구(LibreOffice·WebView2·Windows OCR) 부재를 변환 실행 전에 친절한 사유·다운로드 URL과 함께 보고함", + "ConvertResult(ConvertResult.cs:10)의 Ok/Fail/Skip 팩토리와 ConvertStatus 3분류로 부분 실패/건너뜀을 일관되게 표현하고, 예외를 결과 객체로 변환(ConversionEngine.cs:121)해 배치 중 한 파일 실패가 전체를 중단시키지 않음", + "IProgress 진행률을 멀티홉 구간별로 분할 합성(DocxProvider.cs:103 `0.55+p*0.45`, HeicProvider.cs:61)해 체인 변환에서도 매끄러운 진행 표시를 제공함", + "CancellationToken이 엔진→Provider→외부 프로세스(proc.Kill)까지 일관되게 전파되어 취소가 실제로 동작함" + ], + "weaknesses": [ + "멀티홉 경로 탐색의 부재가 가장 큰 부채: ProviderRegistry는 단일 (input,output) 룩업만 하고(ProviderRegistry.cs:43,49) 그래프가 없어, A→B→C 같은 경로는 매번 Provider 내부에 손으로 짜야 한다. DocumentProvider.RouteAsync(DocumentProvider.cs:92-205)는 사실상 사람이 손으로 그린 경로 그래프이며, 형식이 늘어날수록 switch가 조합 폭발한다", + "Provider 간 체이닝이 생성자 주입으로 하드와이어됨: Bootstrap이 new HeicProvider(magick), new DocxProvider(pdf), new OcrProvider(pdf)처럼 의존성을 수동 결선(Everything2EverythingBootstrap.cs:9-21)한다. 새 중간 형식(예: DOCX→PDF→이미지 외에 DOCX→HTML→이미지)을 자동으로 발견할 방법이 없다", + "ProviderRegistry._byPair.TryAdd(ProviderRegistry.cs:22)는 같은 (input,output)을 여러 Provider가 선언하면 '먼저 등록된 것이 이긴다'를 조용히 적용한다. 우선순위/품질 기반 선택이 불가능하고, 충돌이 경고 없이 묻힌다(예: docx→png을 DocxProvider와 잠재적 다른 Provider가 동시 주장 시)", + "ConversionEngine이 ImageMagick에 직접 의존(ConversionEngine.cs:2)하고 CombineAsync/LoadImageForCombine/ApplyCombineEncoding(164-257)에서 MagickImageCollection을 직접 조작 — '결합'이 Provider 추상화 밖에 있어 엔진이 특정 라이브러리에 결합(coupling)되고, 이미지 외 결합(PDF 병합·동영상 concat)으로 확장 불가", + "ConvertOptions(ConvertOptions.cs:17-58)가 11개 sub-record를 가진 갓 오브젝트로, 형식 추가마다 sub-record가 늘어난다. 모든 Provider가 동일한 거대 옵션을 받지만 대부분 무시하고, 영상/오디오/코덱/AI 관련 옵션을 담을 자리가 없다", + "IConverterProvider(IConverterProvider.cs:9-15)가 '단일 파일 경로 in → outputDirectory에 파일 out, IProgress'로 고정되어 있어 스트리밍, 다중 입력(N→1 결합), AI 프롬프트/모델 파라미터, 미디어 메타데이터(코덱·비트레이트·길이) 프로빙을 표현할 수 없다", + "Provider 목록이 컴파일타임 고정(Bootstrap의 배열). 플러그인 DLL 동적 로딩, ProviderStatus.ComingSoon을 실제 구현으로 교체할 확장 지점이 없다", + "결합 가능 형식이 ConversionEngine의 정적 HashSet(CombinableInputs/Outputs, ConversionEngine.cs:14-23)에 박혀 있어 Provider의 능력 선언과 이중 관리되고 동기화가 깨지기 쉽다" + ], + "extensibilityBlockers": [ + "ProviderRegistry.cs:6,43,49 — 매칭이 Dictionary<(Input,Output)> 단일 홉뿐. 멀티홉 경로 탐색(BFS/Dijkstra) API가 전혀 없어 양방향·다방향 극대화의 근본 한계", + "ProviderRegistry.cs:22 — `_byPair.TryAdd`가 동일 변환쌍 충돌을 조용히 첫 등록자 우선으로 삼킴. 비용/품질 가중치 기반 경로 선택 불가", + "ConversionEngine.cs:2 + 164-257 — 엔진이 ImageMagick(MagickImageCollection)에 직접 의존. 결합 로직이 Provider 밖에 있어 추상화 누수, 비이미지 결합 확장 차단", + "IConverterProvider.cs:9-15 — 시그니처가 (sourcePath, outputDirectory, outputExtension, ConvertOptions, IProgress) 단일 파일·단일 출력 디렉터리·실수 진행률로 고정. 다중 입력·스트리밍·AI 파라미터·미디어 프로빙 표현 불가", + "Everything2EverythingBootstrap.cs:9-21 — Provider 인스턴스와 체이닝 의존성이 컴파일타임 하드코딩. 동적 플러그인 로딩/등록 지점 부재", + "ConvertOptions.cs:35-55 — 11개 이미지/문서 중심 sub-record. 영상 코덱/비트레이트/프레임레이트, 오디오, AI(모델·프롬프트·온도) 옵션을 담을 구조가 없고 형식마다 sub-record 증식", + "DocumentProvider.cs:92-205 — 멀티홉 라우팅이 손으로 짠 switch 그래프. 형식 N개에 대해 경로가 O(N^2)로 수동 증식, 새 형식 추가 시 모든 분기 갱신 필요", + "ConvertResult.cs:10 — 결과가 출력 파일 경로 리스트만 담음. 추출 텍스트·AI 응답·메타데이터·중간 산출물 같은 비파일 결과를 표현할 필드 없음" + ], + "improvementOpportunities": [ + { + "title": "ProviderRegistry를 변환 그래프로 승격하고 엔진에 멀티홉 경로 탐색(BFS/Dijkstra) 추가", + "rationale": "확장자를 노드, ConversionPair를 간선, Provider 비용/품질을 가중치로 하는 유향 그래프를 구성하면 DocxProvider→PdfProvider, DocumentProvider.RouteAsync 같은 손코딩 체인을 엔진이 자동 합성한다. 임시 파일 체이닝을 표준화하면 '진짜 다방향 변환 극대화'가 코드 추가 없이 새 형식 조합으로 폭발적으로 늘어난다 — 프로젝트 핵심 목표의 직접 달성", + "impact": "high", + "effort": "high" + }, + { + "title": "CombineAsync를 IMultiInputProvider(또는 N→1 Provider 추상화)로 분리해 엔진의 ImageMagick 직접 의존 제거", + "rationale": "ConversionEngine.cs:164-257의 ImageMagick 결합 로직을 ImageCombineProvider로 옮기면 엔진이 라이브러리 중립이 되고, PDF 병합·동영상 concat·오디오 믹스 같은 다른 N→1 결합을 동일 추상화로 추가할 수 있다. CombinableInputs/Outputs 정적 HashSet과 Provider 능력 선언의 이중 관리도 해소", + "impact": "high", + "effort": "medium" + }, + { + "title": "IConverterProvider 시그니처를 ConvertRequest/ConvertContext 객체로 일반화하고 다중 입력·비파일 결과·미디어 메타데이터 지원", + "rationale": "현재 (sourcePath,outputDir,ext,options,progress) 고정 시그니처는 N→1 결합, 스트리밍, OCR 텍스트/AI 응답 같은 비파일 산출물, 영상 프로빙을 표현 못 한다. 요청/컨텍스트 객체로 감싸면 인터페이스 파괴 없이 영상·오디오·AI 변환을 같은 추상화로 흡수 가능", + "impact": "high", + "effort": "high" + }, + { + "title": "AI/LLM 변환을 위한 IAiConverterProvider 확장과 ConvertOptions.Ai 옵션 도입", + "rationale": "Codex/LLM 통합(요약·번역·이미지 캡션·문서 재구성)은 모델 ID·프롬프트·온도·API 키 같은 파라미터가 필요한데 현재 ConvertOptions와 IConverterProvider에는 자리가 없다. OcrProvider가 Windows OCR을 Provider로 깔끔히 흡수한 선례가 있으므로 AI도 Provider로 표현하면 변환 매트릭스에 자연스럽게 편입된다", + "impact": "high", + "effort": "medium" + }, + { + "title": "Provider 동적 로딩(플러그인) 도입 — Bootstrap 하드코딩 제거", + "rationale": "Everything2EverythingBootstrap.cs:9-21의 컴파일타임 배열을 어셈블리 스캔/MEF/DI 기반 등록으로 바꾸면 영상(FFmpeg)·AI·신규 코덱 Provider를 본체 재컴파일 없이 추가할 수 있다. ProviderStatus.ComingSoon 항목을 별도 플러그인으로 점진 구현하는 길도 열림", + "impact": "medium", + "effort": "high" + }, + { + "title": "ProviderRegistry 변환쌍 충돌을 명시적 우선순위/진단으로 전환", + "rationale": "_byPair.TryAdd(ProviderRegistry.cs:22)의 '첫 등록자 승리'는 조용한 데이터 손실이다. Provider에 우선순위/품질 점수를 부여하고 충돌 시 로그·진단을 남기면, 같은 변환을 더 빠르거나 고품질로 하는 Provider를 의도적으로 선택할 수 있어 경로 비용 가중치(첫 개선안)와도 연결된다", + "impact": "medium", + "effort": "low" + }, + { + "title": "ConvertOptions를 형식별 옵션 백(IReadOnlyDictionary 또는 옵션 프로바이더)으로 분해", + "rationale": "11개 sub-record 갓 오브젝트(ConvertOptions.cs)는 형식 추가마다 비대해지고 모든 Provider가 무관한 옵션을 끌고 다닌다. Provider가 자기 옵션 스키마를 선언하고 엔진이 형식별 옵션만 주입하면 영상/오디오/AI 옵션을 코어 record 증식 없이 수용", + "impact": "medium", + "effort": "medium" + } + ], + "keyFiles": [ + { + "path": "src/Everything2Everything.Core/Providers/IConverterProvider.cs", + "role": "변환 능력의 핵심 인터페이스. 단일 파일 in/out·IProgress로 고정되어 영상/AI/스트리밍 확장의 1차 제약 지점" + }, + { + "path": "src/Everything2Everything.Core/Providers/ProviderRegistry.cs", + "role": "(input,output) 단일 홉 딕셔너리 매칭. 멀티홉 그래프 탐색 부재와 TryAdd 충돌 무시의 근원" + }, + { + "path": "src/Everything2Everything.Core/ConversionEngine.cs", + "role": "변환 오케스트레이터. 단일/배치는 Provider에 위임하나 결합(CombineAsync)에서 ImageMagick에 직접 의존하는 추상화 누수" + }, + { + "path": "src/Everything2Everything.Core/ConvertOptions.cs", + "role": "11개 sub-record 갓 오브젝트. 이미지/문서 중심이며 영상·오디오·AI 옵션 자리 부재" + }, + { + "path": "src/Everything2Everything.Core/Converters/DocumentProvider.cs", + "role": "RouteAsync(92-205)가 손코딩 멀티홉 경로 그래프. 그래프 탐색 부재를 Provider 내부 switch로 메우는 패턴의 대표 사례" + }, + { + "path": "src/Everything2Everything.Core/Converters/DocxProvider.cs", + "role": "PdfProvider를 생성자 주입받아 DOCX→PDF→이미지 체인을 수동 결선(104). Provider 간 하드와이어 체이닝의 전형" + }, + { + "path": "src/Everything2Everything.Core/Everything2EverythingBootstrap.cs", + "role": "Provider 인스턴스·의존성을 컴파일타임 하드코딩. 동적 플러그인 로딩 부재의 진원지" + }, + { + "path": "src/Everything2Everything.Core/Providers/ProviderCapability.cs", + "role": "능력·상태·외부의존성 메타데이터 캡슐화. PairsFromMatrix로 N×M 양방향 매트릭스를 선언적으로 표현하는 강점 지점" + } + ] + }, + { + "key": "providers", + "subsystem": "Provider 매트릭스 전체 (8개 IConverterProvider + ProviderRegistry 디스패치)", + "summary": "8개 Provider(Magick/Heic/Pdf/Docx/Html/Hwpx/Ocr/Document)가 `IConverterProvider`를 구현하고, 각자 `PairsFromMatrix(inputs, outputs)`로 N×M 변환 쌍을 카르테시안 곱으로 선언하면 `ProviderRegistry`가 `(input,output)→provider` 딕셔너리로 평탄화해 디스패치한다. 실제 변환은 대부분 \"중간 포맷으로 정규화 후 위임\"하는 파이프라인 — 이미지는 Magick로, 문서/한글은 LibreOffice→PDF→Magick로, HTML은 WebView2→PNG/PDF로 수렴한다. 결과적으로 매트릭스는 \"거의 모든 것 → 이미지/PDF/텍스트\" 방향으로만 풍부하고, 그 역방향(이미지/PDF → 편집가능 문서, HWP 출력, 미디어/아카이브 전 카테고리)이 구조적으로 비어 있다. 양방향·다방향·AI·미디어 코덱이라는 프로젝트 목표 대비 현재는 단방향 래스터라이저 집합에 가깝다.", + "strengths": [ + "일관된 추상화: 모든 Provider가 `IConverterProvider`(Capability/CheckAvailability/ConvertAsync) 단일 인터페이스를 구현하고, `ProviderCapability.PairsFromMatrix`(ProviderCapability.cs:62-71)로 변환 쌍을 선언적으로 생성 → 신규 Provider 추가 시 보일러플레이트 최소.", + "레지스트리 기반 O(1) 디스패치: `ProviderRegistry`가 `(input,output)` 키 딕셔너리(_byPair, ProviderRegistry.cs:6,21-22)로 평탄화해 ConversionEngine이 입력 확장자만으로 즉시 Provider를 찾는다. 입력→가능출력 역인덱스(_outputsByInput)도 미리 구축해 UI 셀렉터/컨텍스트메뉴에 바로 공급.", + "중간 포맷 위임 패턴으로 코드 재사용: Heic→(PNG)→Magick, Docx/Hwpx→(PDF)→PdfProvider.ConvertCore, OCR→(PDF페이지 PNG)→Windows OCR. PdfProvider/MagickProvider가 공용 백엔드로 재활용되어 신규 입력 포맷이 이미지 출력 전체 매트릭스를 거의 공짜로 획득.", + "외부 의존성을 1급 모델로 표현: `ExternalDependency`(이름/설명/다운로드URL/필수여부)와 `ProviderAvailability.NotReady(reason, missing)`로 미설치 상태를 구조화해 사용자에게 안내 가능 — DocxProvider는 Word COM 우선/LibreOffice 폴백까지 이중화.", + "취소·실패·충돌 처리가 표준화: 모든 Provider가 CancellationToken을 전파하고 외부 프로세스는 `proc.Kill(true)`로 정리, `OutputPathHelper.ResolveOutputPath/ShouldSkip`으로 파일명 충돌(AppendNumber/Overwrite/Skip)을 통일 처리, 임시파일은 finally에서 삭제.", + "HTML 렌더링의 STA 디스패처 격리(HtmlProvider.cs:153-181): WebView2를 별도 STA 스레드+자체 Dispatcher 루프에서 구동해 메인 UI 스레드와 분리 — 배치 변환 중 UI 프리징/COM 아파트먼트 충돌 회피." + ], + "weaknesses": [ + "매트릭스가 사실상 단방향: 거의 모든 입력이 이미지/PDF/텍스트로 '나가기만' 한다. 편집가능 포맷으로 되돌아오는 경로가 OCR(이미지/PDF→txt/docx, 그것도 레이아웃 손실 평문)뿐이고, 진짜 구조 보존 역변환(PDF→DOCX, 이미지→벡터/문서)이 전무.", + "PDF가 입력으로만 풍부하고 출력이 빈약: PdfProvider는 PDF→이미지 렌더링만 한다(PdfProvider.cs:11-12). 이미지/HTML/DOCX→PDF는 각기 다른 Provider가 따로 만들지만, PDF→PDF(압축/병합/분할/회전)와 PDF→DOCX가 없어 프로젝트 핵심 목표인 'PDF 압축'을 어떤 Provider도 수행하지 못함.", + "HWP는 출력 불가가 구조적 한계로 고착: DocumentProvider가 HWP/HWPX를 입력으로만 받고 출력 Outputs 배열에 .hwp가 없다(DocumentProvider.cs:24, RoadmapNote:40). H2Orestart가 쓰기를 지원 안 해 'HWP↔DOCX/PDF/HTML' 목표의 절반(→HWP)이 막혀 있음.", + "DOC/DOCX 출력의 정의 충돌·왕복 불가능: DocxProvider는 .docx를 입력으로만(→PDF/이미지), DocumentProvider는 .docx를 입력이자 출력으로 선언하지만 OCR도 .docx를 출력한다. 'DOCX 편집본을 다시 받는' 일관된 단일 경로가 없고, DOCX→DOCX 같은 동일포맷은 ConversionEngine에서 Skip 처리됨.", + "외부 프로세스 안정성 취약점: LibreOffice 호출이 3곳(DocxProvider/HwpxProvider/DocumentProvider)에 복붙되어 있고 타임아웃이 전혀 없다 — soffice가 멈추면 WaitForExitAsync가 무한 대기(취소 토큰에만 의존). 또한 soffice는 동일 사용자 프로필을 공유해 동시 인스턴스 충돌 위험이 있으나 직렬화/락이 없음.", + "AI/LLM 통합 지점 부재: ConvertOptions에 OCR 백엔드 문자열('auto')만 있을 뿐, Codex/LLM 호출을 위한 추상화(요약/번역/생성형 변환/캡셔닝)가 인터페이스·옵션·Provider 어디에도 없음. 현재 구조에 AI를 끼워넣을 확장점이 설계되지 않음.", + "미디어·아카이브·폰트·CAD 카테고리 전무: 영상(mp4/mov/webm), 오디오(mp3/wav/flac), 아카이브(zip/7z/tar), 폰트(ttf/otf/woff), 벡터(svg/eps), CAD(dwg/dxf), 전자책(epub/mobi)을 다루는 Provider가 0개. `ProviderStatus.ComingSoon`은 enum에만 존재하고 이를 사용하는 Provider가 하나도 없어 로드맵 UI가 빈 상태.", + "레지스트리의 조용한 우선순위 함정: `_byPair.TryAdd`(ProviderRegistry.cs:22)는 첫 등록 Provider가 승리하고 후속은 '말없이 무시'된다. 향후 같은 (input,output) 쌍을 두 Provider가 선언하면(예: PDF 압축 vs PDF 렌더) 부트스트랩 순서에 따라 비결정적으로 한쪽이 사라짐 — 경고도 없음." + ], + "extensibilityBlockers": [ + "IConverterProvider.cs:9-15 — ConvertAsync 시그니처가 단일 sourcePath/단일 outputExtension에 고정. 다방향(N입력→1출력 병합, 1입력→N출력 동시) 변환과 '입력=출력 동일포맷 최적화(PDF압축, 이미지 리인코딩)'가 인터페이스 레벨에서 표현 불가 (동일포맷은 ConversionEngine.cs:88-89에서 무조건 Skip됨).", + "DocumentProvider.cs:24 `Outputs = {.html,.docx,.md,.txt}` 및 :40 RoadmapNote — HWP/HWPX가 출력 배열에서 영구 제외. 양방향 HWP를 위해서는 H2Orestart 쓰기 대체재(또는 자체 HWP writer)가 필요하나 코드 구조상 출력 라우트(RouteAsync, :92-205)에 .hwp 분기 자체가 없음.", + "PdfProvider.cs:11-12 PdfRenderOutputs에 .pdf·.docx 부재 — PDF→PDF(압축/병합) 및 PDF→편집문서 경로가 Provider 차원에서 차단. PDFtoImage 라이브러리는 렌더 전용이라 PDF 쓰기/조작 백엔드(예: PdfPig/QuestPDF/iText) 도입 전까지 확장 불가.", + "ConvertOptions.cs 전체 — 옵션 클래스가 이미지·PDF렌더·HTML렌더·OCR로 한정. Video/Audio/Archive/Ai 옵션 그룹이 없어, 미디어 코덱(비트레이트/해상도/fps)이나 LLM 프롬프트/모델 선택을 전달할 통로가 없음. 새 카테고리는 옵션 모델 확장이 선행되어야 함.", + "MagickProvider.cs:8-19, HeicProvider.cs:11-12, PdfProvider.cs:11-12 등 — 각 Provider가 지원 확장자 배열을 하드코딩. ffmpeg 같은 '수백 포맷 양방향' 백엔드를 PairsFromMatrix로 표현하면 조합 폭발(예: 50입력×50출력=2500쌍)로 레지스트리 메모리·UI 매트릭스가 비현실적 — 능력 기반(capability predicate) 표현이 없음.", + "ProviderRegistry.cs:22 `_byPair.TryAdd` — 동일 (input,output)에 복수 Provider(예: 빠른 변환 vs 고품질 vs AI 변환)를 '선택지'로 공존시키는 모델 부재. 한 쌍당 정확히 하나의 Provider만 허용해 변환 전략 다중화(품질/속도/AI 토글)가 불가능.", + "Everything2EverythingBootstrap.cs:11-21 — Provider 목록이 컴파일타임 하드코딩 배열. 플러그인/동적 등록(외부 DLL, ffmpeg 유무에 따른 조건부 등록)이 없어 신규 카테고리 추가가 항상 코어 재컴파일을 요구." + ], + "improvementOpportunities": [ + { + "title": "LibreOffice 호출 로직을 단일 SofficeRunner 서비스로 통합 + 타임아웃·직렬화 추가", + "rationale": "DocxProvider.ConvertWithLibreOfficeAsync(113-157), HwpxProvider.ConvertWithLibreOfficeAsync(107-151), DocumentProvider.SofficeConvertAsync(238-281)가 거의 동일한 ProcessStartInfo+--headless 인자+산출물 이동 로직을 3중 복제. 공용 서비스로 추출하면 중복 제거와 함께 (현재 전무한) 프로세스 타임아웃·동시성 락·재시도를 한 곳에서 추가해 soffice 행(hang)/프로필 충돌 안정성을 크게 높일 수 있음.", + "impact": "high", + "effort": "low" + }, + { + "title": "이미지 인코딩(ApplyEncoding/ApplyTransforms) 공용 ImageEncoder 헬퍼로 추출", + "rationale": "jpg/png/webp/avif/tiff 인코딩 + 알파 평탄화 + MaxLongEdge 리사이즈 로직이 MagickProvider(144-208), PdfProvider(99-149), HtmlProvider(104-137), ConversionEngine.CombineAsync(215-257)에 4중 복제됨. 단일 헬퍼로 모으면 새 출력 포맷/품질 옵션을 한 번만 추가해 전 Provider가 일관되게 획득 — 현재는 한 곳만 고치면 나머지가 누락되는 드리프트 위험.", + "impact": "medium", + "effort": "low" + }, + { + "title": "FFmpeg 기반 미디어 Provider(영상·오디오) 신설 + 능력기반 매트릭스 표현 도입", + "rationale": "프로젝트 목표의 핵심인 영상/오디오 코덱·압축·포맷 변환을 담당하는 Provider가 전무. ffmpeg는 양방향 N×M이 본질이므로, PairsFromMatrix 카르테시안 방식 대신 '입력군↔출력군 + 코덱 옵션'을 표현하는 capability predicate 모델을 함께 도입해야 조합 폭발을 피하면서 진짜 다방향을 달성. ExternalToolDetector에 ffmpeg 탐지 추가로 기존 의존성 패턴 재사용 가능.", + "impact": "high", + "effort": "high" + }, + { + "title": "PDF 쓰기/조작 백엔드 도입으로 PDF→PDF(압축/병합/분할)·PDF→DOCX 경로 개통", + "rationale": "현재 PdfProvider는 렌더 전용이라 매트릭스 최대 갭이자 명시적 목표인 'PDF 압축'과 'PDF→편집문서' 역방향이 비어 있음. QuestPDF/PdfPig/Ghostscript 등 쓰기 백엔드를 추가하면 PdfProvider 출력 배열에 .pdf를 더해 압축·병합을 제공하고, LibreOffice 경유 PDF→DOCX 역라우트도 DocumentProvider에 추가 가능(소스가 PDF면 soffice가 draw로 변환). 매트릭스 양방향성을 가장 크게 끌어올리는 단일 작업.", + "impact": "high", + "effort": "high" + }, + { + "title": "AI/LLM 변환 확장점 설계: IConverterProvider에 AI Provider 카테고리 + ConvertOptions.Ai 옵션 그룹", + "rationale": "Codex/LLM 통합 목표를 위한 추상화가 코드에 전혀 없음. 'OCR 후 LLM 교정', 'PDF→요약 MD', '이미지→캡션/번역', '문서 언어 번역' 같은 생성형 변환을 담을 AiProvider 인터페이스 확장과 옵션(모델/프롬프트/API키)을 먼저 설계하면, 레지스트리의 다중 Provider 공존 모델 개선과 맞물려 동일 (input,output) 쌍에 '일반/AI' 전략을 선택지로 노출 가능. 현 구조는 끼워넣을 자리조차 없어 선제적 설계가 필요.", + "impact": "high", + "effort": "high" + }, + { + "title": "레지스트리 다중 Provider 공존 + 명시적 우선순위/충돌 경고 모델", + "rationale": "_byPair.TryAdd(ProviderRegistry.cs:22)가 동일 쌍 중복 시 후속 Provider를 조용히 버림. PDF압축 vs PDF렌더, AI변환 vs 일반변환처럼 한 쌍에 복수 전략이 필연적으로 생기는데 현재는 표현 불가하고 버그 유발. (input,output)→List로 바꾸고 우선순위/품질태그를 부여하면 UI에서 변환 전략을 선택하게 하고, 등록 충돌을 진단창에 경고로 노출 가능.", + "impact": "medium", + "effort": "medium" + }, + { + "title": "아카이브(zip/7z) 및 폰트(ttf↔woff2) Provider 추가로 빈 카테고리 보강", + "rationale": "아카이브 압축/해제와 웹폰트 변환은 비교적 독립적이고 성숙한 .NET 라이브러리(System.IO.Compression, SharpCompress, FontTools류)로 빠르게 채울 수 있는 저비용 갭. ComingSoon enum이 미사용 상태이므로, 단계적 출시를 위해 먼저 ComingSoon로 등록해 로드맵 UI를 실제로 활성화하는 부수 효과도 얻음.", + "impact": "medium", + "effort": "medium" + } + ], + "keyFiles": [ + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/Providers/ProviderRegistry.cs", + "role": "(input,output)→Provider 평탄화 디스패치 테이블. TryAdd(:22)의 first-wins 정책이 향후 다중 전략 확장의 핵심 병목." + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/Providers/ProviderCapability.cs", + "role": "PairsFromMatrix(:62-71) 카르테시안 곱으로 변환 쌍 선언. ffmpeg류 대규모 매트릭스에서 조합 폭발 유발 지점이자 capability-predicate 도입 후보." + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/Providers/IConverterProvider.cs", + "role": "단일입력→단일출력 고정 인터페이스. 다방향/동일포맷최적화/AI 변환의 표현 한계 근원." + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/Everything2EverythingBootstrap.cs", + "role": "8개 Provider 하드코딩 등록 + 등록 순서 결정(:11-21). 플러그인/동적등록 부재 지점." + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/Converters/DocumentProvider.cs", + "role": "텍스트 5종 매트릭스 라우터(RouteAsync:92-205). HWP 입력전용·출력불가(:24,:40), LibreOffice 경유 라우팅의 중심." + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/Converters/PdfProvider.cs", + "role": "PDF→이미지 렌더 전용(:11-12). PDF 출력/압축/PDF→DOCX 역방향 부재의 핵심 갭. 다른 Provider들이 ConvertCore를 공용 백엔드로 재사용." + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/Converters/ExternalToolDetector.cs", + "role": "LibreOffice/Word COM/H2Orestart 탐지. ffmpeg 등 신규 외부도구 탐지 확장의 표준 패턴 제공." + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/ConvertOptions.cs", + "role": "이미지/PDF/HTML/OCR 한정 옵션 모델. Video/Audio/Archive/Ai 옵션 그룹 부재 — 신규 카테고리 확장의 선행 작업 대상." + } + ] + }, + { + "key": "app-ui", + "subsystem": "App UI / CLI / Shell (Everything2Everything WPF .NET 9)", + "summary": "WPF FluentWindow 기반 데스크톱 UI로, MainWindow가 큐 관리·매트릭스 출력 필터링·진행 표시·프리뷰·이력을 모두 코드비하인드(MainWindow.xaml.cs 1171줄)에서 직접 처리한다. 출력 형식은 큐 내 모든 입력의 OutputsForFile 교집합으로 1-hop 직접 변환만 필터링하며(RefreshAvailableOutputFormats:836), 멀티홉/체이닝/AI/미디어 개념은 코드 어디에도 없다. CLI(CliRouter)는 5개 verb를 파싱하지만 register/diagnose를 제외하면 모두 GUI 창을 띄우는 런처에 불과해 stdout/exit-code 기반 자동화·파이프라인이 불가능하다. ContextMenuRegistrar는 12개 PopularOutputs를 별도 하드코딩하여 ProviderRegistry 매트릭스와 부분적으로만 동기화된다.", + "strengths": [ + "매트릭스 출력 필터링이 ProviderRegistry를 단일 진실원천(SSOT)으로 삼아 동적으로 동작 — 큐 변경 시마다 OutputsForFile 교집합을 재계산(RefreshAvailableOutputFormats MainWindow.xaml.cs:849-858)하므로 Provider만 추가하면 1-hop 변환 UI는 자동 확장된다", + "컨텍스트 메뉴 카스케이드가 OutputsForInput 매트릭스를 조회해 입력 확장자별로 가능한 출력만 노출(ContextMenuRegistrar.cs:37-45), 우클릭→'to %1' CLI 호출로 GUI 없이 즉시 변환되는 깔끔한 셸 통합", + "진행 표시·취소 UX가 IProgress + CancellationTokenSource로 일관되게 구현 — 사이드바 전체 진행바(ProcessingProgressPanel)와 큐 row별 inline 진행바(QueueItem.ProgressValue)가 동시 갱신되고 취소 버튼이 즉각 상태를 'cancelling'으로 전환(OnCancelProcessingClick:619)", + "전역 예외 로깅(WireGlobalExceptionLogging App.xaml.cs:122)이 AppDomain/Dispatcher/TaskScheduler 3채널을 모두 후킹해 안정성 확보, Quick/Register 모드는 임시폴더에 타임스탬프 로그를 남겨 디버깅 가능", + "외부 도구 의존 Provider(RequiresExternal)를 시작 시 비동기 점검(RefreshCapabilityStatusAsync:66)해 준비 안 된 형식을 사이드바에 경고 표시 — 케이퍼빌리티 인식형 UI의 기반은 마련됨", + "CombineToSingle UX가 입력/출력 양쪽 조건(CanCombineInput/CanCombine)과 큐 개수를 종합해 버튼 활성화 및 5가지 분기 힌트 메시지 제공(UpdateCombineState:909-931)" + ], + "weaknesses": [ + "MVVM 전무: MainWindow.xaml.cs 1171줄에 View 로직·ViewModel(QueueItem/DateGroup/HistoryRow)·Service 호출(PreviewService/HistoryStorage/engine.ConvertManyAsync)·CSV/JSON 익스포트가 한 파일에 혼재. 명령은 일부 RelayCommand(25-38)지만 대부분 코드비하인드 이벤트 핸들러(OnProcessQueueClick 등)라 테스트·재사용 불가", + "변환 옵션 UI가 형식별로 확장 불가능한 구조: 사이드바에는 Quality 슬라이더 하나만 있고 ConvertOptions의 10여 종 옵션(PngCompression, AvifSpeed, PdfRender.Dpi, Ocr.Language, HtmlRender.Viewport 등)이 전혀 노출되지 않음. BuildOptions(259-281)는 JPEG/WebP/AVIF Quality만 슬라이더에서 읽고 나머지는 모두 기본값 하드코딩", + "CLI가 진정한 자동화에 부적합: dialog/quick/showmain 모드가 모두 WPF 창을 띄우며(App.xaml.cs:39-57), stdout으로 결과(출력 경로/성공·실패 카운트)를 반환하지 않고 exit code도 Quick 실패 시 1만 반환. JSON 출력·배치 매니페스트·표준입력 파이프·진행 스트리밍이 없어 AI/스크립트가 결과를 파싱 불가", + "출력 형식 매트릭스가 3곳에 중복 하드코딩: MainWindow.AllFormats(813-827, 12개), ContextMenuRegistrar.PopularOutputs(14-28, 12개), OpenFileDialog 입력 필터(96). 새 형식 추가 시 세 곳을 수동 동기화해야 하며 라벨·정렬·색상 리소스 키가 분산", + "멀티홉 경로 표현 수단이 UI/엔진 양쪽에 전무: ComboBox는 1-hop 직접 변환만 나열하고, 멀티홉이 생기면 '직접 vs 경유' 구분, 경로 미리보기(예: HWP→PDF→PNG), 중간 형식 선택, 품질 누적 손실 경고를 표현할 자리가 없음", + "QuickProgressWindow는 취소 버튼이 없어(취소 토큰을 ConvertManyAsync에 전달조차 안 함, App.xaml.cs:96) CLI/우클릭 경로의 긴 변환을 중단할 방법이 없음 — 메인 창과 취소 UX가 비대칭", + "케이퍼빌리티 상태가 시작 시 1회만 점검되고(RefreshCapabilityStatusAsync) 결과를 캐시하지 않아, 형식 선택 시점에 해당 출력이 외부 도구 부재로 실패할지 사전 차단하지 못함 — 변환 실행 후에야 실패 확인", + "데모 시드 데이터(SeedDemoHistory:667-708)가 프로덕션 코드에 하드코딩되어 첫 실행 시 가짜 14.2MB PNG 등을 이력에 주입, 통계(EST. SPACE SAVED)를 오염시킴" + ], + "extensibilityBlockers": [ + "MainWindow.xaml.cs:849-858 — RefreshAvailableOutputFormats가 OutputsForFile(1-hop 직접 변환)의 단순 교집합만 계산. 멀티홉 그래프 탐색(BFS/DFS over ConversionPair)이 없어 'HWP→DOCX 경유 PDF' 같은 간접 경로가 출력 목록에 절대 나타나지 않음", + "ProviderRegistry.cs:52-61 — OutputsForInput이 _outputsByInput 직접 매핑만 반환. 도달 가능 그래프(transitive closure)·경로 비용·중간 형식 개념이 없어 다방향 변환 확장의 근본 차단점", + "MainWindow.xaml.cs:963-976 — UpdateQualityPanelForFormat이 ext를 switch로 하드코딩(.jpg/.webp/.avif만 quality 패널 표시). 형식별 옵션 스키마가 Provider에서 선언적으로 오지 않아, 새 옵션(코덱·비트레이트·OCR 언어·LLM 프롬프트)을 추가하려면 XAML+코드비하인드를 직접 수정해야 함", + "MainWindow.xaml.cs:259-281 — BuildOptions가 ConvertOptions의 일부 필드만 수동 채움. 미디어/AI Provider가 요구할 옵션(영상 코덱, CRF, 프레임레이트, AI 모델명, API 키)을 받을 동적 옵션 바인딩 메커니즘 부재", + "IConverterProvider.cs:9-15 — ConvertAsync 시그니처가 단일 sourcePath→단일 outputExtension 동기 변환 전제. 스트리밍 입력, 다중 출력 산출물(예: 영상→썸네일+자막+트랜스코드), AI 비동기 잡, 진행 중 부분 결과를 표현할 수 없음", + "ContextMenuRegistrar.cs:14-28 — PopularOutputs 배열이 정적이라 AI/미디어 형식(.mp4/.webm 등) 추가 시 카스케이드에 자동 반영되지 않고, 멀티홉 출력도 우클릭 메뉴에 노출 불가", + "App.xaml.cs:39-57 & CliRouter.cs:21-49 — CLI 파서가 옵션 플래그(--quality, --output-dir, --json, --recursive)를 전혀 받지 않고 verb+files 구조만 지원. AI 통합(LLM 프롬프트 전달)·배치 스크립팅을 위한 인자 확장 여지가 구조적으로 막힘" + ], + "improvementOpportunities": [ + { + "title": "엔진에 변환 그래프 + 멀티홉 경로 탐색 도입", + "rationale": "ProviderRegistry에 ConversionPair 그래프를 구성하고 BFS로 도달 가능 출력·최단 경로를 계산하는 PathFinder를 추가. RefreshAvailableOutputFormats(MainWindow.xaml.cs:836)가 직접+간접 출력을 모두 나열하고, 선택 시 경로(HWP→PDF→PNG)를 미리보기로 보여주면 '진짜 다방향 변환 극대화' 목표의 핵심 기반이 된다. ConvertManyAsync는 경로를 받아 중간 산출물을 임시폴더에 체이닝.", + "impact": "high", + "effort": "high" + }, + { + "title": "Provider 선언형 옵션 스키마 + 동적 옵션 UI 생성", + "rationale": "ProviderCapability에 옵션 디스크립터(이름/타입/범위/기본값/조건)를 선언하고, 사이드바가 선택된 출력 형식에 맞춰 컨트롤을 동적 생성하도록 전환. 현재 하드코딩된 UpdateQualityPanelForFormat(963)·BuildOptions(259)를 대체하면 미디어 코덱·OCR 언어·AI 프롬프트 등 어떤 옵션도 코드 수정 없이 노출 가능.", + "impact": "high", + "effort": "high" + }, + { + "title": "헤드리스 CLI 모드 분리 (stdout JSON + exit code + 옵션 플래그)", + "rationale": "to/quick 모드에 --headless/--json을 추가해 WPF 창 없이 변환하고, 결과(출력 경로·바이트·성공·실패)를 JSON으로 stdout 출력, 적절한 exit code 반환. CliRouter.Parse(21)를 플래그 파싱으로 확장하고 ConsoleHelper로 스트리밍. AI 에이전트·배치 스크립트·CI 파이프라인 통합의 전제 조건이며 effort 대비 자동화 가치가 매우 큼.", + "impact": "high", + "effort": "medium" + }, + { + "title": "출력 형식 매트릭스 단일 SSOT로 통합", + "rationale": "AllFormats(MainWindow:813)·PopularOutputs(ContextMenuRegistrar:14)·OpenFileDialog 필터(96)의 3중 중복을 Core의 단일 FormatCatalog(확장자·라벨·색상키·정렬)로 통합하고 ProviderRegistry와 교차검증. 새 형식 추가 시 한 곳만 수정하면 UI·우클릭·파일다이얼로그가 동시 반영되어 미디어/AI 형식 추가 비용이 급감.", + "impact": "medium", + "effort": "low" + }, + { + "title": "MainWindow를 MVVM으로 분해", + "rationale": "1171줄 코드비하인드를 MainWindowViewModel + 서비스(QueueService/HistoryService/PreviewService 호출)로 분리하고 RelayCommand를 전면 적용. 멀티홉·AI·미디어 기능이 추가될수록 코드비하인드 비대화가 가속되므로, 지금 분해해야 형식별 옵션 패널·경로 선택 등 신규 UI를 독립 테스트 가능한 단위로 붙일 수 있다.", + "impact": "medium", + "effort": "high" + }, + { + "title": "QuickProgressWindow에 취소 + 케이퍼빌리티 사전 점검", + "rationale": "App.RunQuickAsync(82)가 CancellationTokenSource를 만들어 ConvertManyAsync에 전달하고 QuickProgressWindow에 취소 버튼을 추가해 CLI/우클릭 경로의 긴 변환(특히 미래의 영상·AI 잡)을 중단 가능하게. 또한 형식 선택 시점에 CheckAvailabilityAsync 캐시를 조회해 외부도구 부재 출력을 사전 비활성화하면 실패 후 확인하는 현재 UX를 개선.", + "impact": "medium", + "effort": "medium" + } + ], + "keyFiles": [ + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.App/Views/MainWindow.xaml.cs", + "role": "메인 UI 코드비하인드(1171줄): 큐 관리, 매트릭스 출력 필터링(RefreshAvailableOutputFormats:836), 옵션 빌드(BuildOptions:259), 진행/취소, 프리뷰, 이력, CSV/JSON 익스포트가 모두 혼재 — MVVM 부재의 진앙" + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.App/Cli/CliRouter.cs", + "role": "CLI 파서: verb+files만 인식, 옵션 플래그·JSON 출력·stdout 결과 없음 — 자동화/AI 통합 차단점" + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.App/App.xaml.cs", + "role": "진입점: CLI 모드를 GUI 창 생성으로 라우팅(39-57), 헤드리스 실행 경로 부재, 전역 예외 로깅" + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.App/Shell/ContextMenuRegistrar.cs", + "role": "우클릭 카스케이드 등록: PopularOutputs 12개 하드코딩(14-28)을 OutputsForInput 매트릭스와 교차해 입력별 메뉴 생성 — 매트릭스 동기화가 부분적" + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/Providers/ProviderRegistry.cs", + "role": "변환 매트릭스 SSOT: _byPair/_outputsByInput 직접 매핑만 보유(52-61), 그래프 탐색·멀티홉 도달성 부재 — 다방향 확장의 근본 차단점" + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/ConversionEngine.cs", + "role": "변환 실행: ConvertOneAsync는 1-hop 직접 변환만(91), Combine은 이미지→PDF/TIFF/GIF 특수 경로, 멀티홉 체이닝 없음" + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/ConvertOptions.cs", + "role": "형식별 옵션 모델(10여 종): UI에서 Quality 3종만 바인딩되고 나머지는 미노출 — 옵션 UI 확장성 격차의 증거" + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/Providers/IConverterProvider.cs", + "role": "Provider 계약: 단일입력→단일출력 ConvertAsync(9), 다중산출물·스트리밍·AI 비동기잡·선언형 옵션 스키마 표현 불가" + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.App/Views/QuickProgressWindow.xaml.cs", + "role": "CLI/우클릭용 진행 창: 취소 버튼·취소 토큰 부재(메인 창과 비대칭), 결과 요약만 표시" + } + ] + }, + { + "key": "infra", + "subsystem": "인프라 (히스토리 / 프리뷰 / 빌드 / 패키징 / CI-CD / 설정 영속화)", + "summary": "히스토리는 `%LocalAppData%`에 append-only JSONL로 저장하는 정적 헬퍼(HistoryStorage)와 UI 전용 인메모리 컬렉션(HistoryStore)으로 단순 분리돼 있고, 프리뷰는 이미지/PDF/HEIC만 동기 렌더하며 문서·영상은 명시적으로 스텁 처리한다. 빌드/패키징은 framework-dependent MSIX 단일 산출물에 최적화돼 있어 .NET DLL과 C++ Shell DLL만 Layout에 복사하고 FFmpeg/Ghostscript 같은 대형 외부 바이너리 번들링 메커니즘이 전혀 없다. 가장 큰 구조적 공백은 설정 영속화 계층의 완전 부재로, 변환 옵션은 매번 UI에서 재구성되고(BuildOptions) AI API 키·도구 경로·사용자 기본값을 저장할 곳이 없으며, 테스트 인프라도 0이다.", + "strengths": [ + "히스토리 저장이 손상 내성을 가진 JSONL(줄 단위) 포맷이라 한 줄이 깨져도 나머지는 로드됨 — HistoryStorage.cs:30-39의 라인별 try/catch로 파싱 격리. 영상/배치 같은 대량 엔트리가 쌓여도 append-only라 쓰기 비용은 O(1)", + "HistoryStorage의 모든 I/O가 try/catch로 감싸져 영속화 실패가 인메모리 동작을 막지 않음(HistoryStorage.cs:56-59, 64) — 권한 없는 환경/네트워크 드라이브에서도 앱이 죽지 않는 graceful degradation", + "PreviewService가 CancellationToken을 끝까지 전파하고 무거운 디코딩을 Task.Run으로 오프로드(PreviewService.cs:25-30) — UI 스레드 블로킹 방지, 빠른 큐 전환 시 이전 프리뷰 취소 가능", + "ExternalToolDetector(ExternalToolDetector.cs:7-37)가 ProgramFiles 다중 경로 + 레지스트리(UNO\\InstallPath) 폴백으로 외부 도구를 탐지하는 견고한 패턴을 이미 보유 — FFmpeg/Ghostscript 탐지로 그대로 확장 가능한 선례", + "출력 디렉토리 전략이 enum(OutputLocation)으로 추상화돼 서브폴더/동일폴더/커스텀 3종을 깔끔히 분기(ConversionEngine.cs:259-282), suffix가 출력 확장자 기반 자동 생성(`_pdf`, `_png`)이라 양방향 매트릭스 확장 시 충돌 없음", + "CI가 build.yml(PR 검증)과 release.yml(태그 기반 릴리즈 + 조건부 PFX 서명)로 역할 분리돼 있고, release.yml:35-49가 secret 유무에 따라 서명/미서명을 graceful하게 분기" + ], + "weaknesses": [ + "설정 영속화 계층이 전무함 — 저장되는 영구 상태는 history.jsonl 단 하나. ConvertOptions는 MainWindow.BuildOptions()(MainWindow.xaml.cs:259-281)에서 매 실행마다 UI 컨트롤로부터 새로 생성되고 _conflictRule·QualitySlider 값은 앱 재시작 시 소실. AI(Codex/LLM) API 키, LibreOffice/FFmpeg 경로, 사용자 기본 출력 형식을 저장할 메커니즘이 없어 AI 통합·도구 경로 캐싱의 토대가 없음", + "PreviewService가 문서·영상을 원천 미지원 — 문서는 '다음 업데이트에서 지원'(PreviewService.cs:28-29), 영상은 case 자체가 없어 RenderViaMagick(PreviewService.cs:30)로 폴백→MagickImage가 mp4/mov를 못 열어 예외. HWP↔DOCX, 영상 코덱 변환 목표 대비 프리뷰가 핵심 입력 타입을 커버 못함", + "프리뷰 캐싱 부재 — 같은 파일을 다시 선택할 때마다 디코딩/리사이즈/temp PNG 왕복(RenderHeic의 PreviewService.cs:67-76)을 재수행. 대용량/배치 큐에서 동일 항목 반복 클릭 시 매번 풀 디코딩", + "히스토리 전체 로드가 비확장적 — File.ReadAllLines(HistoryStorage.cs:27)로 전 파일을 메모리에 적재 후 OrderBy(MainWindow.xaml.cs:663)로 전량 정렬. 영상/배치 변환으로 엔트리가 수만 줄 쌓이면 시작 시 전부 파싱·정렬해야 함(페이징·tail read·만료 정책 없음)", + "히스토리 도메인 로직이 UI에 결합 — 로드/그룹화/데모 시드(SeedDemoHistory MainWindow.xaml.cs:667-708)가 MainWindow.xaml.cs에 박혀 있고, 실측 데이터가 없으면 가짜 '842.4 MB 절약' 데모가 통계에 섞임(MainWindow.xaml.cs:685,704). 헤드리스/CLI 배치 경로에서 히스토리 기록 재사용 불가", + "빌드/패키징이 대형 외부 바이너리 번들을 전혀 가정하지 않음 — BuildMsix.ps1:108-109가 publish 산출물 + Shell DLL만 Layout에 복사. FFmpeg(~80MB)/Ghostscript를 동봉하려면 Layout 복사 단계, MSIX 용량(현재 200MB+ 압축 한계 고려), 라이선스(FFmpeg LGPL/GPL) 처리 로직이 모두 없음", + "테스트 인프라 0 — 솔루션에 테스트 프로젝트가 없음(Glob **/*Test* 무결과). OutputPathHelper 충돌 해결, ConversionEngine 결합 로직, JSONL round-trip 같은 순수 함수조차 자동 검증 없어 양방향 매트릭스 확장 시 회귀 탐지 불가", + "CI가 self-contained/portable EXE를 빌드·게시하지 않음 — README는 portable EXE를 'A) 가장 가벼움'으로 권장하나(README.md:140), 두 워크플로 모두 MSIX만 산출(build.yml:42, release.yml:75). .NET 9 Desktop Runtime 미설치 환경용 self-contained 배포 산출물이 자동화에 없음", + "CI에 NuGet/빌드 캐시가 없어(build.yml 전체) 매 실행마다 Magick.NET·WebView2 등 대형 패키지를 재복원 — 외부 바이너리까지 더해지면 빌드 시간 선형 증가" + ], + "extensibilityBlockers": [ + "PreviewService.cs:23-31 — 확장자 switch가 닫힌 구조이고 default가 RenderViaMagick(MagickImage)로 폴백. 영상(.mp4/.mov/.mkv) 프리뷰를 추가하려면 FFmpeg 프레임 추출 case를 직접 삽입해야 하며, 현재는 영상 입력 시 MagickImage 생성자에서 예외 발생", + "PreviewService 전체가 static 클래스 + 하드코딩된 디코더 의존성(ImageMagick/PDFtoImage/Libheif) — 영상/AI 썸네일을 위한 디코더 플러그인 주입(DI) 지점이 없어 IPreviewRenderer 추상화 없이는 확장 불가", + "ConvertOptions(ConvertOptions.cs:17-58)에 영상(코덱/비트레이트/fps)·AI(모델/프롬프트/API키)·압축(PDF 압축 레벨) 옵션 sub-record가 없고, 영속화 대상이 아니라 [JsonSerializable] 직렬화 속성도 없음 — AI 기능 설정과 도구 경로를 담을 영구 설정 모델이 부재", + "BuildMsix.ps1:102-115 Layout 구성 단계가 publish 출력 + 단일 Shell DLL만 복사하도록 하드코딩 — FFmpeg/Ghostscript/Tesseract 같은 외부 바이너리를 동봉하는 Copy-Item 단계와 그 경로를 런타임에 해석하는 메커니즘이 없음", + "Package.appxmanifest:175-177이 runFullTrust 단일 capability만 선언 — AI 기능의 네트워크 호출(internetClient)이나 추가 파일 타입(.mp4/.mov/.webm/.mkv) ItemType 등록(현재 manifest:69-168에 영상 확장자 전무)이 없어 영상 우클릭 메뉴 노출 불가", + "HistoryEntry(HistoryStore.cs:5-15)가 영상/배치 변환 메타데이터(코덱, 해상도, duration, 다중 출력 통계)를 표현할 필드가 부족 — MetaLine 단일 문자열에 의존(HistoryStore.cs:13)해 구조화된 영상 변환 이력 질의 불가", + "설정/키 저장소가 없어 AI API 키를 DPAPI(ProtectedData)로 암호화 저장할 진입점 자체가 부재 — HistoryStorage.cs의 LocalAppData 패턴은 있으나 평문 JSONL이라 비밀 저장에 부적합" + ], + "improvementOpportunities": [ + { + "title": "ISettingsStore(설정 영속화 계층) 도입 — DPAPI 암호화 + JSON", + "rationale": "AI(Codex/LLM) API 키, 외부 도구 경로(FFmpeg/Ghostscript/LibreOffice 캐시), 사용자 기본 ConvertOptions를 %LocalAppData%에 영속화. 비밀(API 키)은 System.Security.Cryptography.ProtectedData(DPAPI)로 암호화. HistoryStorage.cs의 LocalAppData+try/catch 패턴을 그대로 재사용하면 일관성 확보. AI 통합·도구 경로 캐싱·옵션 기억의 공통 토대가 되어 가장 높은 레버리지를 가짐.", + "impact": "high", + "effort": "medium" + }, + { + "title": "IPreviewRenderer 추상화 + 영상 프레임 추출(FFmpeg) + 프리뷰 캐시", + "rationale": "PreviewService.cs:23-31의 닫힌 switch를 확장자→IPreviewRenderer 레지스트리로 교체하고, 영상은 FFmpeg로 중간 프레임을 추출해 썸네일화. (path,mtime,maxEdge) 키로 LRU 캐시를 두면 배치 큐 반복 클릭 시 재디코딩 제거. 영상 변환 목표의 전제 조건이며 프리뷰 미지원 입력(문서/영상)의 핵심 공백을 메움.", + "impact": "high", + "effort": "high" + }, + { + "title": "외부 바이너리 번들링 파이프라인 (BuildMsix.ps1 확장 + ExternalToolDetector 폴백)", + "rationale": "BuildMsix.ps1:102-115 Layout 단계에 FFmpeg/Ghostscript를 다운로드·검증(체크섬)·복사하는 단계를 추가하고, ExternalToolDetector.cs에 '번들 경로 우선, 없으면 시스템 탐지' 폴백을 더함. MSIX 용량 증가 대비 self-contained와 별개 산출물로 분리. 영상/PDF 압축 기능 구현의 필수 인프라.", + "impact": "high", + "effort": "high" + }, + { + "title": "히스토리 영속화의 스트리밍 로드 + 만료/회전 정책", + "rationale": "HistoryStorage.cs:27의 File.ReadAllLines 전량 적재를 마지막 N줄 tail-read 또는 페이징으로 교체하고, 줄 수/일수 기반 회전(rotation)을 추가. 영상/배치로 이력이 수만 줄 쌓일 때 시작 지연을 방지. HistoryEntry에 구조화된 영상 메타(코덱/해상도/duration) 필드 추가도 병행.", + "impact": "medium", + "effort": "medium" + }, + { + "title": "히스토리 도메인 로직을 UI에서 Core로 분리 + 데모 시드 제거", + "rationale": "LoadHistory/AddToHistoryGroups/SeedDemoHistory(MainWindow.xaml.cs:652-708)가 UI에 결합돼 CLI/헤드리스 배치 경로에서 이력 기록을 재사용 못함. HistoryService를 Core로 올리고 가짜 통계(MainWindow.xaml.cs:685,704)를 제거하면 배치 파이프라인·테스트·정확한 통계가 모두 가능.", + "impact": "medium", + "effort": "low" + }, + { + "title": "테스트 프로젝트 신설 (xUnit) — 순수 함수 우선", + "rationale": "OutputPathHelper 충돌 해결(AppendNumber/Skip/Overwrite 경계), ConversionEngine.IsCombineSupported, HistoryEntry JSONL round-trip, ExternalToolDetector 경로 우선순위 등 외부 의존성 없는 순수 로직부터 커버. 양방향 N×M 매트릭스가 200+ 쌍으로 늘 때 회귀 안전망 확보. CI(build.yml)에 dotnet test 단계 1줄 추가로 게이트화.", + "impact": "high", + "effort": "medium" + }, + { + "title": "CI 강화 — NuGet 캐시 + self-contained portable 산출물 + 테스트 게이트", + "rationale": "build.yml에 actions/cache(NuGet) 추가로 빌드 시간 단축(외부 바이너리 추가 시 효과 증대), README가 권장하는 portable EXE(README.md:140)를 self-contained로 빌드해 release.yml 산출물에 동봉(.NET 런타임 미설치 환경 지원), dotnet test 단계로 PR 품질 게이트. 현재 두 워크플로(build.yml/release.yml) 모두 MSIX만 산출하는 공백 해소.", + "impact": "medium", + "effort": "low" + }, + { + "title": "OutputPathHelper 충돌 처리 강건화 + 결합 출력 경로 일관화", + "rationale": "OutputPathHelper.cs:27의 10000회 선형 File.Exists 스캔은 동일 폴더 대량 출력(영상 프레임 추출 시 수천 장) 시 O(n^2)에 가깝게 느려짐 — 디렉토리 1회 열거 후 최대 인덱스+1 방식으로 개선. Overwrite/Skip이 모두 fullPath를 반환(OutputPathHelper.cs:21-24)하는 점은 ShouldSkip와 분리돼 있어 호출자가 둘 다 호출해야 하는 암묵 계약 — 의도를 한 메서드로 응집.", + "impact": "low", + "effort": "low" + } + ], + "keyFiles": [ + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/HistoryStorage.cs", + "role": "유일한 영구 상태 저장소 — append-only JSONL, 손상 내성 라인별 파싱, 전량 로드(비페이징). 설정 영속화 계층의 재사용 가능한 패턴 원천" + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/HistoryStore.cs", + "role": "인메모리 ObservableCollection + HistoryEntry record. 영상/배치 메타데이터 필드 부족(MetaLine 단일 문자열 의존)" + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/PreviewService.cs", + "role": "static 프리뷰 렌더러 — 이미지/PDF/HEIC만 지원, 문서·영상 스텁/미지원, 캐싱 없음. 영상/AI 확장의 핵심 블로커" + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/OutputPathHelper.cs", + "role": "출력 경로/충돌 해결 — 10000회 선형 스캔, Overwrite/Skip 동일 반환 + ShouldSkip 분리 계약" + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/ConvertOptions.cs", + "role": "형식별 옵션 sub-record 집합 — 영상/AI/압축 옵션 부재, 직렬화/영속화 미지원" + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.Core/Converters/ExternalToolDetector.cs", + "role": "ProgramFiles+레지스트리 폴백 도구 탐지 — FFmpeg/Ghostscript 탐지로 확장 가능한 선례, 번들 경로 폴백 추가 지점" + }, + { + "path": "D:/workspace/Everything2Everthing/packaging/BuildMsix.ps1", + "role": "MSIX 빌드 파이프라인 — Layout에 publish+Shell DLL만 복사(BuildMsix.ps1:108-109), 외부 바이너리 번들 단계 없음" + }, + { + "path": "D:/workspace/Everything2Everthing/packaging/Package.appxmanifest", + "role": "MSIX 매니페스트 — runFullTrust만 선언, 영상 확장자/네트워크 capability 미등록" + }, + { + "path": "D:/workspace/Everything2Everthing/.github/workflows/build.yml", + "role": "PR CI — 캐시·테스트·portable 산출물 없음, MSIX만 빌드" + }, + { + "path": "D:/workspace/Everything2Everthing/.github/workflows/release.yml", + "role": "릴리즈 CI — 태그 기반, 조건부 PFX 서명, MSIX만 산출(self-contained 없음)" + }, + { + "path": "D:/workspace/Everything2Everthing/src/Everything2Everything.App/Views/MainWindow.xaml.cs", + "role": "히스토리 도메인 로직(LoadHistory/SeedDemoHistory:652-708)과 옵션 빌드(BuildOptions:259-281)가 UI에 결합 — 영속화·재사용 차단" + } + ] + }, + { + "key": "quality", + "subsystem": "횡단 품질 (에러 처리 / 동시성 / 취소 / 보안 / 메모리 / 테스트)", + "summary": "변환 파이프라인의 횡단 관심사는 비교적 일관된 패턴(IProgress 보고, OperationCanceledException 재던짐, try/finally 임시정리, ConvertResult.Fail 래핑)으로 손코딩되어 있고, 외부 프로세스 호출은 ProcessStartInfo.ArgumentList를 사용해 셸 인젝션을 구조적으로 회피한다. 그러나 배치 변환이 전적으로 순차 for-loop(ConversionEngine.cs:57)라 멀티코어/외부프로세스 대기 시간을 전혀 활용하지 못하고, 취소 토큰 전파가 라이브러리 경계(Magick.NET, Windows OCR, CDP)에서 끊기며, 메모리는 전 페이지/전 프레임을 한꺼번에 디코딩하는 비스트리밍 구조다. 테스트 프로젝트가 전무(0개)하고 NuGet 취약점 경고가 csproj에서 통째로 억제(NU1901-1904)되어, AI/영상/대용량 기능으로 확장하기 전에 품질 안전망이 먼저 필요하다.", + "strengths": [ + "외부 프로세스 호출이 전부 ProcessStartInfo.ArgumentList 기반(DocumentProvider.cs:254-261, HwpxProvider.cs:118-125, DocxProvider.cs:124-131)이라 경로에 공백/특수문자/`&`가 있어도 셸 인젝션이 구조적으로 불가능하다. UseShellExecute=false + CreateNoWindow=true로 일관됨.", + "취소 시 외부 프로세스를 확실히 죽이는 패턴이 일관적이다: WaitForExitAsync(ct) → catch(OperationCanceledException){ proc.Kill(true); throw; } (DocumentProvider.cs:266-267, HwpxProvider.cs:134-137, DocxProvider.cs:140-143). Kill(true)로 자식 트리까지 종료.", + "임시 파일/디렉터리를 Guid 기반 고유 이름으로 만들고 try/finally로 정리한다(OcrProvider.cs:174, DocumentProvider.cs:71/88, HwpxProvider.cs:70/103, DocxProvider.cs:57/109). 동시 실행 시 충돌이 없도록 Guid.NewGuid():N를 씀.", + "ConversionEngine.ConvertOneAsync가 OperationCanceledException은 재던지고 그 외 예외만 ConvertResult.Fail로 래핑(ConversionEngine.cs:117-124)해, 취소와 실패가 의미상 구분된다. 한 파일 실패가 배치 전체를 죽이지 않는 격리 구조.", + "전역 예외 안전망 3종(AppDomain.UnhandledException, DispatcherUnhandledException, TaskScheduler.UnobservedTaskException)을 App 시작 시 배선하고 temp 로그에 append (App.xaml.cs:122-154). UI 스레드 미처리 예외는 사용자에게 메시지박스로 노출 후 Handled 처리.", + "HTML 렌더링을 전용 STA 스레드 + 독립 Dispatcher로 격리하고 finally에서 controller.Close() + Dispatcher 셧다운(HtmlProvider.cs:162-181, 245-249)해 WebView2 COM 자원 수명을 명확히 관리한다." + ], + "weaknesses": [ + "배치가 100% 순차 for-loop(ConversionEngine.cs:57-70)다. LibreOffice/OCR/WebView2처럼 대기 시간이 긴 변환에서 단일 파일씩만 처리해 멀티코어를 전혀 못 쓴다. 100장 이미지 변환도 코어 1개만 사용.", + "취소 토큰이 라이브러리 경계에서 단절된다. Magick.NET의 image.Write/Resize/collection.Coalesce(MagickProvider.cs:84-94, PdfProvider.cs:84-87)는 ct를 받지 않아, 거대 이미지 1장 인코딩 중에는 ThrowIfCancellationRequested 체크 지점 사이에서 취소가 지연된다. Windows OCR engine.RecognizeAsync(OcrProvider.cs:164)도 ct 미전달.", + "CLI quick 경로가 아예 취소 불가능하다: App.RunQuickAsync가 ConvertManyAsync를 CancellationToken 없이 호출(App.xaml.cs:96)하고 QuickProgressWindow에 취소 UI도 없다. 컨텍스트 메뉴 대량 변환을 중단할 방법이 없음.", + "에러 메시지 채널이 비일관적이다. 코어는 ConvertResult.Message(구조화)로 반환하지만, MainWindow는 catch에서 MessageBox.Show(ex.Message)로만 노출(MainWindow.xaml.cs:569)하고 OnProcessQueueClick은 개별 result.Status가 Failed/Skipped여도 그냥 히스토리에만 적고(MainWindow.xaml.cs:551-561) 사용자에게 실패를 표면화하지 않는다. 실패 파일도 done처럼 큐에서 제거됨(line 563).", + "메모리가 비스트리밍이다. PDF는 페이지마다 PNG 전체를 MemoryStream에 디코딩(PdfProvider.cs:79-87), OCR은 파일을 MemoryStream→ToArray()→InMemoryRandomAccessStream으로 3중 복사(OcrProvider.cs:148-159), HTML 캡처는 base64 PNG 전체를 메모리에 들고 Magick으로 재디코딩(HtmlProvider.cs:76-98). 8000x8000 같은 대용량/멀티프레임 GIF는 collection.Coalesce()로 전 프레임을 동시에 메모리에 적재(MagickProvider.cs:84).", + "ImageMagick 리소스 한계(ResourceLimits.Memory/Width/Height)가 어디에도 설정돼 있지 않다. 악의적/손상 이미지(decompression bomb)나 거대 RAW가 프로세스 메모리를 무제한 점유 가능. MagickProvider/PdfProvider/HtmlProvider/CombineAsync 전부 무방비.", + "테스트가 0개다(test 프로젝트/파일 없음 — Glob *Test* 결과 없음). 12종×N 양방향 매트릭스, 라우팅 분기(DocumentProvider.RouteAsync), 충돌 규칙, 취소 경로 모두 회귀 검증이 불가능.", + "csproj가 NuGet 취약점 경고를 통째로 억제한다: NoWarn에 NU1901;NU1902;NU1903;NU1904 (Everything2Everything.Core.csproj:11). 알려진 CVE가 있는 패키지가 들어와도 빌드가 침묵한다. WebView2/OpenXML/Magick은 외부 미디어를 파싱하는 공격면이 큰 라이브러리들이라 위험.", + "_cts 접근에 동시성 보호가 없다. OnProcessQueueClick은 _cts.Token을 await 호출 인자로 직접 읽고(MainWindow.xaml.cs:541) finally에서 _cts=null로 set(line 574)하는데, OnCancelProcessingClick(line 621-622)이 다른 시점에 _cts.Cancel()을 호출한다. UI 스레드 단일 진입으로 대체로 안전하나 _cts 수명/dispose가 명시적이지 않고 CancellationTokenSource.Dispose()가 한 번도 호출되지 않음(누수)." + ], + "extensibilityBlockers": [ + "ConversionEngine.cs:57-70 — ConvertManyAsync의 순차 for-loop가 하드코딩됨. 영상 트랜스코딩(파일당 수십 초~분)이나 AI 호출(LLM 왕복 지연)을 추가하면 순차 처리가 치명적 병목이 된다. 병렬도(MaxDegreeOfParallelism) 옵션이 ConvertOptions에 없음(ConvertOptions.cs 전체).", + "IProgress 단일 스칼라 진행 모델(IConverterProvider.ConvertAsync 시그니처)은 영상(프레임/시간코드), AI(토큰 스트리밍), 다단계 파이프라인의 진행을 표현 못 한다. ConvertProgress(Index,Total,CurrentPath,FileProgress) (ConversionEngine.cs:285)도 단일 파일=단일 출력 가정에 묶여 있어 1→N 페이지 분할의 부분 진행을 못 담는다.", + "ConvertResult가 동기 완료 모델(ConvertResult.cs:10-25)이라 스트리밍/증분 출력(영상 인코딩 중 부분 미리보기, LLM 토큰 스트림)을 표현할 타입이 없다. OutputPaths는 변환 끝난 뒤에야 채워짐.", + "외부 프로세스 실행 로직(ConvertWithLibreOfficeAsync)이 DocxProvider/HwpxProvider/DocumentProvider에 거의 동일하게 3중 복제됨(DocxProvider.cs:113-157, HwpxProvider.cs:107-151, DocumentProvider.cs:238-281). FFmpeg/ghostscript(PDF압축)/codex CLI 같은 새 외부도구를 추가할 때마다 stderr 수집·타임아웃·종료처리·결과검증 보일러플레이트를 또 복붙해야 한다. 공통 ExternalProcessRunner 추상화 부재.", + "외부 프로세스에 타임아웃이 없다(WaitForExitAsync(ct)만, DocumentProvider.cs:266 등). LibreOffice/Word COM(DocxProvider.cs:159-186)이 hang하면 취소하기 전까지 영원히 대기. 영상/대용량 작업에선 walltime 한계가 필수.", + "stderr를 RedirectStandardError=true로 켜두고도 한 번도 읽지 않는다(DocumentProvider.cs:252, HwpxProvider.cs:116, DocxProvider.cs:122). 파이프 버퍼가 가득 차면 자식 프로세스가 블록될 수 있고, 실패 시 LibreOffice의 실제 오류 사유를 버려 ConvertResult가 'exit N'만 남긴다(DocumentProvider.cs:270). AI/코덱 도구 디버깅이 불가능." + ], + "improvementOpportunities": [ + { + "title": "배치 변환에 제한된 병렬 처리(Parallel.ForEachAsync) 도입", + "rationale": "ConvertManyAsync의 순차 for-loop(ConversionEngine.cs:57)를 MaxDegreeOfParallelism 옵션을 가진 Parallel.ForEachAsync로 교체하면 멀티코어 + 외부프로세스 대기 시간을 활용해 처리량이 대폭 향상된다. 단, IProgress 보고를 인덱스 기반에서 '완료 카운터'(Interlocked) 기반으로 바꾸고, MainWindow의 snapshot[i] 인덱스 매핑(MainWindow.xaml.cs:525-530)을 QueueItem별 개별 progress로 재설계해야 한다. 영상/AI 확장의 선결 과제.", + "impact": "high", + "effort": "medium" + }, + { + "title": "공통 ExternalProcessRunner 추상화 (타임아웃 + stderr 수집 + Kill 통합)", + "rationale": "세 Provider에 복제된 LibreOffice 실행 코드(DocxProvider.cs:113, HwpxProvider.cs:107, DocumentProvider.cs:238)를 하나의 헬퍼로 통합하고, 거기에 walltime 타임아웃·stderr 비동기 읽기·실패 시 stderr를 ConvertResult.Message에 포함하는 기능을 추가한다. FFmpeg/ghostscript/codex CLI 등 향후 외부도구를 일관되게 붙일 토대가 되고, 현재의 stderr 미수집 데드락 위험과 hang 위험을 동시에 해소한다.", + "impact": "high", + "effort": "medium" + }, + { + "title": "ImageMagick ResourceLimits 전역 설정 + decompression bomb 방어", + "rationale": "부트스트랩(Everything2EverythingBootstrap.cs)에서 ResourceLimits.Memory/Width/Height/Area에 합리적 상한을 설정해 손상·악성 이미지나 거대 RAW가 프로세스를 OOM시키는 것을 막는다. 신뢰할 수 없는 입력을 파싱하는 변환기의 기본 안전장치이며, 대용량 영상/이미지 기능 추가 시 더 중요해진다.", + "impact": "high", + "effort": "low" + }, + { + "title": "코어 단위 테스트 프로젝트 신설 (xUnit) — 순수 로직 우선", + "rationale": "외부 의존성 없이 결정적으로 검증 가능한 부분이 풍부하다: OutputPathHelper(충돌/AppendNumber/Sanitize, OutputPathHelper.cs), CliRouter.Parse(CliRouter.cs:21), DocumentProvider 라우팅 분기(별칭 정규화·NotSupported 경로, DocumentProvider.cs:92-204), ConversionEngine.IsCombineSupported. Markdig/ReverseMarkdown round-trip(md→html→md)도 인메모리로 검증 가능. 12종 매트릭스 회귀를 막는 최소 안전망.", + "impact": "high", + "effort": "medium" + }, + { + "title": "실패/건너뜀 결과를 UI에 일관되게 표면화", + "rationale": "OnProcessQueueClick(MainWindow.xaml.cs:543-564)이 result.Status가 Failed여도 done처럼 큐에서 제거하고 사용자에게 알리지 않는다. result.Status별로 QueueItem 상태(failed/skipped)를 시각화하고, 실패 항목은 큐에 남기거나 요약 토스트로 보고하도록 바꿔 '조용한 실패'를 없앤다. ConvertResult.Message가 이미 구조화돼 있어 채널만 연결하면 된다.", + "impact": "medium", + "effort": "low" + }, + { + "title": "CLI quick 경로에 취소 토큰 전파 + NuGet 취약점 경고 재활성화", + "rationale": "App.RunQuickAsync(App.xaml.cs:96)가 토큰 없이 ConvertManyAsync를 호출해 컨텍스트 메뉴 대량 변환을 중단 불가하다. CancellationTokenSource를 QuickProgressWindow의 취소 버튼과 연결한다. 동시에 csproj의 NoWarn NU1901-1904(Everything2Everything.Core.csproj:11)를 제거해, 외부 미디어를 파싱하는 WebView2/OpenXML/Magick 의존성의 알려진 CVE가 빌드에서 드러나게 한다.", + "impact": "medium", + "effort": "low" + }, + { + "title": "대용량 입력 스트리밍/페이지 단위 메모리 관리", + "rationale": "PDF 페이지 전체를 MemoryStream에 디코딩(PdfProvider.cs:79)하고 OCR이 파일을 3중 복사(OcrProvider.cs:148-159)하며 멀티프레임 GIF를 통째로 Coalesce(MagickProvider.cs:84)하는 구조는 영상/대용량 PDF에서 메모리 폭증을 부른다. 페이지/프레임 단위로 디코딩-인코딩-해제하는 스트리밍 루프와, OCR의 불필요한 ToArray() 복사 제거가 필요. 영상 코덱·PDF 압축 기능의 전제 조건.", + "impact": "medium", + "effort": "high" + }, + { + "title": "외부 프로세스 입력 검증 강화 (LibreOffice 출력 파일명 충돌)", + "rationale": "세 Provider가 LibreOffice 출력 파일명을 GetFileNameWithoutExtension(sourcePath)+'.'+fmt로 예측(DocumentProvider.cs:272 등)하는데, 병렬화하거나 동명이파일을 같은 outDir로 변환하면 결과물이 서로 덮어쓴다. 인젝션은 ArgumentList로 막혀 있으나, 출력 파일명 격리(작업별 고유 폴더)가 병렬화 도입 시 필수 선결 조건이다.", + "impact": "low", + "effort": "low" + } + ], + "keyFiles": [ + { + "path": "src/Everything2Everything.Core/ConversionEngine.cs", + "role": "배치 오케스트레이션 핵심. 순차 for-loop(57), 취소 체크(59), 예외→ConvertResult 격리(117-124), CombineAsync의 Task.Run 래핑(186-200). 병렬화/진행모델 재설계의 진원지." + }, + { + "path": "src/Everything2Everything.Core/Converters/DocumentProvider.cs", + "role": "외부 프로세스(LibreOffice) 호출 + 임시작업폴더 + 라우팅 분기의 대표. ArgumentList 안전 패턴(254-261), Kill on cancel(266-267), stderr 미수집(252), 타임아웃 부재." + }, + { + "path": "src/Everything2Everything.Core/Converters/OcrProvider.cs", + "role": "메모리 3중 복사(148-159)·라이브러리 경계 취소 단절(RecognizeAsync ct 미전달, 164)·임시 PDF 페이지 정리(111-119)의 표본." + }, + { + "path": "src/Everything2Everything.Core/Converters/HtmlProvider.cs", + "role": "STA 스레드+Dispatcher 격리(162-181), CDP base64 전량 메모리 적재(76-98, 252-280), 취소 등록(227)·자원 정리 finally(245-249). WebView2 COM 수명 관리의 모범이자 메모리 비스트리밍의 예." + }, + { + "path": "src/Everything2Everything.App/Views/MainWindow.xaml.cs", + "role": "UI 측 취소(OnCancelProcessingClick 619)·_cts 수명(516/574)·실패결과 조용한 제거(543-564)·MessageBox 단일 에러채널(569). 에러 표면화/취소 UX 개선 지점." + }, + { + "path": "src/Everything2Everything.App/App.xaml.cs", + "role": "전역 예외 안전망 3종 배선(122-154)과 CLI quick의 취소토큰 누락(96). 에러 처리 일관성/취소 완전성의 양면." + }, + { + "path": "src/Everything2Everything.Core/Everything2Everything.Core.csproj", + "role": "NuGet 취약점 경고 전체 억제(NoWarn NU1901-1904, line 11). ResourceLimits 미설정. 보안/공급망 위생의 출발점." + }, + { + "path": "src/Everything2Everything.Core/OutputPathHelper.cs", + "role": "충돌 해소·파일명 살균 순수 로직. 외부 의존성 없는 단위 테스트의 1순위 대상이자 병렬화 시 race 검토 지점." + } + ] + } +] \ No newline at end of file diff --git a/docs/ssot/_data/designs.json b/docs/ssot/_data/designs.json new file mode 100644 index 0000000..dfc0fc3 --- /dev/null +++ b/docs/ssot/_data/designs.json @@ -0,0 +1,252 @@ +[ + { + "angle": "변환 그래프 코어 우선 — \"모든 변환은 엣지(edge)이고, 엔진은 라우터(router)다.\" Provider를 손으로 체이닝하는 대신, 모든 변환을 그래프의 단방향 엣지로 등록하고 멀티홉 경로 탐색(Dijkstra)이 N×M·다방향을 자동으로 합성하게 만든다. UI/AI/미디어/PDF/HWP는 전부 이 그래프 위에 '엣지를 더하는 것'으로 환원된다.", + "vision": "Everything2Everything의 북극성은 \"원자적 변환(atomic conversion)의 조합으로 임의의 A→Z를 자동 합성하는 변환 그래프 OS\"다.\n\n현재 코드의 가장 강력한 증거는 DocumentProvider.RouteAsync(DocumentProvider.cs:92-205)다. 이건 사람이 손으로 그린 Dijkstra다 — `md→docx`를 `md→html→docx`로, `docx→md`를 `docx→html→md`로 중첩 switch에 박아넣었다. 형식이 N개로 늘면 이 손그림 그래프는 O(N²)로 폭발하고, 새 형식 하나가 모든 분기를 건드린다. 비전의 핵심은 이 손그림을 '삭제'하고 엔진이 그래프 탐색으로 같은 경로를 '계산'하게 만드는 것이다.\n\n원칙은 세 가지다.\n(1) 모든 Provider는 '단일 홉 원자 변환'만 선언한다 (md→html, html→docx, png→pdf). 멀티홉은 절대 Provider 안에 손으로 짜지 않는다.\n(2) ProviderRegistry는 (input,output) 딕셔너리 룩업에서 → '방향 그래프(노드=확장자, 엣지=Provider×ConversionPair×손실가중치)'로 승격된다. 앱 시작 시 1회 그래프 빌드, Dijkstra 1회로 최적 경로를 푼다(NCSA Polyglot 모델, 외부 의존성 없는 자체 구현 80~120줄).\n(3) 손실은 가중치다. lossless=0, 컨테이너변환=0.05, lossy재인코딩=0.4, 래스터화=0.8. `-log(보존율)+홉페널티`로 손실 경로를 자연스럽게 회피하고, UI는 손실 경로에 '⚠ 손실' 배지를 붙인다.\n\n이 그래프가 코어가 되면 모든 신기능이 '엣지 추가'로 환원된다. FFmpeg = 미디어 엣지 수백 개, PDF 압축 = pdf→pdf 엣지(동일포맷도 엣지로 허용), HWP→DOCX = 이미 있는 LibreOffice 엣지, AI 요약/번역/캡션 = '신규 엣지(로컬 변환이 없는 페어)'. 그래프가 풍부해질수록 OutputsForInput은 1-hop 직접 출력(현 ProviderRegistry.cs:52-58)에서 → '도달 가능한 모든 포맷(transitive closure)'으로 폭발적으로 확장된다. 이것이 진짜 N×M·다방향의 의미다 — 매트릭스를 손으로 채우는 게 아니라, 작은 엣지들의 조합으로 매트릭스가 '발현'된다.", + "keyMoves": [ + "ProviderRegistry를 그래프로 승격: _byPair 단일 룩업(ProviderRegistry.cs:6,43) 옆에 ConversionGraph(인접 리스트 Dictionary>)를 빌드하고, 자체 Dijkstra FindBestPath(in,out,options)를 추가. 외부 라이브러리 없이 80~120줄, 단일 EXE/AOT 친화.", + "ConversionPair에 LossClass(Lossless/Container/Recode/Rasterize) 필드 추가 → 엣지 가중치의 단일 출처(SSOT). 충돌 시 _byPair.TryAdd의 '조용한 첫 등록자 우선'(ProviderRegistry.cs:22)을 비용/우선순위 기반 명시 선택 + 진단 경고로 교체.", + "ConversionEngine.ConvertOneAsync(ConversionEngine.cs:91)를 그래프 위임으로 전환: 직접 엣지가 없거나 더 싼 멀티홉이 있으면 ExecuteChainAsync가 경로의 각 홉을 순차 실행(중간 산출물은 DocumentProvider의 workDir 패턴을 공용 헬퍼로 승격해 재사용). 진행률은 홉 수로 분할 매핑.", + "DocumentProvider.RouteAsync의 손그림 멀티홉(:92-205)을 삭제 → DocumentProvider는 md→html, html→docx 같은 원자 엣지만 선언. md→docx는 엔진이 자동 합성. 이것이 그래프 코어의 첫 검증(dogfooding).", + "IConverterProvider 시그니처(IConverterProvider.cs:9-15)를 ConvertRequest/ConvertContext 객체로 일반화 — 다중 입력(N→1 결합), 비파일 결과(추출 텍스트·AI 응답·미디어 메타데이터), 멀티홉 중간 컨텍스트, 풍부한 진행률을 표현. ConvertResult(ConvertResult.cs:10)에 비파일 산출물 필드 추가.", + "CombineAsync의 ImageMagick 직접 의존(ConversionEngine.cs:2,164-257)을 IMultiInputProvider(N→1 엣지)로 분리 → 엔진의 라이브러리 결합 제거. 결합 가능 형식의 정적 HashSet(ConversionEngine.cs:14-23)을 Provider 능력 선언으로 통합.", + "ExternalProcessRunner 단일 추상화로 LibreOffice 호출 3중 복제(DocxProvider/HwpxProvider/DocumentProvider) 통합 + 타임아웃·stderr 수집·Kill. 이후 모든 외부 엣지(FFmpeg/Ghostscript/qpdf/Codex CLI)가 이 러너 하나를 공유.", + "ConvertOptions(ConvertOptions.cs:17-58)의 갓 오브젝트를 그래프 옵션(AllowMultiHop/MaxHops/AvoidLossy) + 형식별 옵션 백(IReadOnlyDictionary 또는 Provider 선언형 스키마)으로 분해 → 미디어 코덱·AI 프롬프트·PDF 압축 레벨을 담을 자리 확보.", + "AI는 '특수 엣지'일 뿐: IAiConverterProvider가 아니라 LlmProvider도 동일한 그래프 엣지로 편입(요약/번역/캡션은 로컬 변환이 없는 신규 페어). Codex non-interactive(codex exec --json)와 API 키(Microsoft.Extensions.AI IChatClient + DPAPI 키저장) 둘 다 동일 엣지의 백엔드 선택지로." + ], + "roadmap": [ + { + "phase": "Phase 0", + "title": "그래프 코어 골격 — 손그림 Dijkstra를 진짜 Dijkstra로", + "goal": "ProviderRegistry를 변환 그래프로 승격하고, 멀티홉 경로 탐색을 엔진에 내장한다. 기능 추가 0, 순수 기반 재설계. DocumentProvider.RouteAsync 삭제로 그래프를 dogfooding 검증.", + "deliverables": [ + "ConversionGraph 클래스: ProviderRegistry 생성자 루프(ProviderRegistry.cs:16-30)에서 인접 리스트(노드=확장자, Edge={Provider,ConversionPair,Weight}) 빌드", + "자체 Dijkstra FindBestPath(inExt,outExt,options) — 외부 의존성 0, 80~120줄, 최대 홉 수(기본 3) + 손실 블랙리스트 안전장치", + "ConversionPair에 LossClass 필드 추가 + 정적 가중치 테이블(lossless=0/container=0.05/recode=0.4/rasterize=0.8)", + "ConversionEngine.ConvertOneAsync를 그래프 위임으로 전환 + ExecuteChainAsync(홉 순차 실행, 공용 workDir 헬퍼, 진행률 분할)", + "DocumentProvider.RouteAsync(:92-205) 삭제 → 원자 엣지만 선언. md→docx 등 멀티홉이 엔진 합성으로 동일 동작함을 검증", + "_byPair.TryAdd 충돌(ProviderRegistry.cs:22)을 비용 기반 선택 + 진단 경고로 교체", + "xUnit 테스트 프로젝트 신설: 그래프 경로 탐색·손실 가중치·DocumentProvider 회귀를 순수 함수로 검증(현재 테스트 0개)" + ] + }, + { + "phase": "Phase 1", + "title": "인터페이스 일반화 — 엣지가 무엇이든 표현 가능하게", + "goal": "IConverterProvider/ConvertResult/ConvertOptions를 일반화해 다중입력·비파일결과·형식별옵션을 표현. 미디어·AI·PDF 엣지가 들어올 '그릇'을 코어에 먼저 판다.", + "deliverables": [ + "IConverterProvider 시그니처를 ConvertRequest/ConvertContext 객체로 일반화(IConverterProvider.cs:9-15) — 다중 입력, 미디어 메타데이터 프로빙, 멀티홉 컨텍스트", + "ConvertResult에 비파일 산출물 필드(추출 텍스트·메타데이터·중간 산출물) 추가(ConvertResult.cs:10)", + "ConvertOptions 분해: 그래프 옵션(AllowMultiHop/MaxHops/AvoidLossy) + 형식별 옵션 백(IReadOnlyDictionary) → 갓 오브젝트(ConvertOptions.cs:17-58) 해소", + "CombineAsync를 IMultiInputProvider(N→1 엣지)로 분리 → 엔진의 ImageMagick 직접 의존(ConversionEngine.cs:2) 제거, 결합 형식 정적 HashSet을 Provider 능력으로 통합", + "ExternalProcessRunner 단일 추상화(타임아웃+stderr수집+Kill) → LibreOffice 호출 3중 복제 통합", + "UI/CLI 도달성 노출: OutputsForInput을 transitive closure로 확장(ProviderRegistry.cs:52) → '이 파일로 만들 수 있는 모든 포맷'에 손실 배지 표시" + ] + }, + { + "phase": "Phase 2", + "title": "엣지 폭발 1 — PDF·HWP 양방향 + 한글 문서", + "goal": "그래프에 PDF 조작 엣지와 한글 문서 양방향 엣지를 추가. 사용자 핵심 요구(PDF 압축, HWP→DOCX/PDF/HTML)를 그래프 합성으로 충족.", + "deliverables": [ + "PdfToolProvider 신설: pdf→pdf 동일포맷 엣지 허용(압축/병합/분할/회전). 압축 3단계(PDFsharp 구조최적화 / PDFium+ImageMagick 재인코딩 / Ghostscript 외부폴백). 동일포맷 Skip(ConversionEngine.cs:88) 우회", + "PDF 역변환 엣지: DocumentProvider 입력에 .pdf 추가 → pdf→{docx,html,txt}(soffice), pdf→txt 무외부 폴백(PdfPig), pdf→md(pdf→html→md 자동 합성)", + "HWP/HWPX 출력 매트릭스 확장: HwpxProvider Hant Outputs에 .docx/.html/.txt/.odt 추가(soffice --convert-to 파라미터화), .hwp 입력에 --infilter 명시", + "ConvertOptions에 PdfCompress 옵션 그룹 추가, 한글 폰트(함초롬) 누락 감지 경고", + "역방향 HWP(→hwpx)는 hwpxlib 베스트에포트 엣지로 ComingSoon 등록(라이선스 격리: 외부 프로세스 분리 호출)" + ] + }, + { + "phase": "Phase 3", + "title": "엣지 폭발 2 — 미디어·아카이브·범용 문서", + "goal": "FFmpeg 미디어 엣지 수백 개를 그래프에 주입. 능력 기반(capability predicate) 표현으로 N×M 조합 폭발을 회피하면서 '진짜 모든 것'에 근접.", + "deliverables": [ + "FfmpegProvider: FFMpegCore(MIT) + LGPL 바이너리(RequiresExternal+자동 다운로드). 영상/오디오 N×M, HW 인코더 폴백, NotifyOnProgress→IProgress 직결, ExternalProcessRunner 재사용", + "능력 기반 엣지 표현: ffmpeg처럼 50×50=2500쌍이 비현실적인 백엔드는 PairsFromMatrix 대신 capability predicate로 그래프에 lazy 편입", + "ArchiveProvider(SharpCompress, 순수관리) + DataProvider(Parquet.Net/ClosedXML/CsvHelper) + 벡터(Svg.Skia) — EXE 번들 가능 순수 .NET 우선", + "PandocProvider(외부 CLI) — md/rst/latex/ipynb/epub 등 마크업 거대 매트릭스를 그래프에 합류, LibreOffice와 겹치는 페어는 비용 기반 우선순위로 라우팅", + "PreviewService를 IPreviewRenderer로 추상화(PreviewService.cs:23) + FFmpeg 프레임 추출 + 프리뷰 캐시" + ] + }, + { + "phase": "Phase 4", + "title": "AI 엣지 + 헤드리스 자동화 + 플러그인", + "goal": "AI를 '그래프의 특수 엣지'로 편입(핵심 엔진이 아닌 부가 엣지). 헤드리스 CLI와 manifest 기반 외부 도구 어댑터로 확장을 코어 재컴파일 없이.", + "deliverables": [ + "LlmProvider: Codex non-interactive(codex exec --skip-git-repo-check --json --output-schema) + API 키(Microsoft.Extensions.AI IChatClient, OpenAI/Anthropic 공식 SDK) 둘 다 백엔드 선택지. 요약/번역/캡션/메타데이터를 '로컬 변환 없는 신규 엣지'로 그래프 편입", + "ISettingsStore(DPAPI 암호화) 신설 → AI API 키·외부도구 경로·기본 옵션 영속화(현재 영속 상태는 history.jsonl 단 하나)", + "헤드리스 CLI 분리: CliRouter에 --json/--output-dir/--quality/--recursive 플래그(CliRouter.cs:21) + stdout JSON 결과 + exit code → AI/스크립트가 파싱 가능", + "manifest 기반 ExternalToolProvider 어댑터(CliWrap) — 새 외부 도구를 JSON manifest만으로 엣지 추가, 코어 재컴파일 0. 진짜 동적 .NET 플러그인은 collectible ALC로 후속", + "배치 병렬화(Parallel.ForEachAsync, ConversionEngine.cs:57) + ImageMagick ResourceLimits(decompression bomb 방어) + NuGet 취약점 경고 재활성화" + ] + } + ], + "biggestRisk": "그래프 코어 재설계가 '엔진은 깔끔해졌는데 사용자에게 보이는 변화가 0'인 상태로 Phase 0~1을 길게 끄는 '보이지 않는 리팩터링의 함정'이 가장 큰 위험이다. 그래프 엔진은 그 자체로는 데모할 게 없다 — DocumentProvider.RouteAsync를 삭제해도 사용자는 똑같은 md→docx를 볼 뿐이다. 완화책: (1) Phase 0를 '순수 기반'이 아니라 '즉시 가치'와 묶는다 — 그래프가 켜지는 순간 OutputsForInput이 transitive closure로 확장되어 '만들 수 있는 포맷 목록'이 눈에 띄게 늘어나는 것을 Phase 0 산출물에 포함시켜 가시적 성과를 만든다. (2) DocumentProvider.RouteAsync 삭제를 회귀 테스트로 강제 검증해 '그래프가 손그림과 동일 동작함'을 객관적으로 증명(테스트 0개인 현 상태가 이 검증의 선결 조건). (3) 멀티홉 손실 누적의 신뢰성 위험 — 잘못된 가중치는 lossy 경로를 최적으로 오판한다. 초기엔 '멀티홉은 직접 엣지가 없을 때만 발동' + MaxHops=3 + 손실 블랙리스트로 보수적으로 게이트하고, 동적 손실 측정(Versus식)은 처음부터 넣지 않는다. 2차 위험은 인터페이스 일반화(Phase 1)가 8개 기존 Provider를 전부 건드려 한 번에 깨질 수 있다는 점 — ConvertRequest 일반화는 기존 시그니처를 어댑터로 감싸 점진 마이그레이션하고, 무중단을 회귀 테스트로 보장한다." + }, + { + "angle": "플러그인 생태계 우선 (Plugin-Ecosystem-First): 코어를 변환 호스트(host)로 축소하고, 모든 변환 능력을 선언적 manifest + 어댑터 Provider로 외부화한다. \"코드를 늘려 포맷을 늘리는\" 모델에서 \"manifest를 늘려 포맷을 늘리는\" 모델로 전환.", + "vision": "Everything2Everything을 단일 모놀리식 변환기가 아니라 \"변환 능력의 OS\"로 재정의한다. 코어는 더 이상 변환을 '아는' 주체가 아니라, 변환 능력을 선언받아 조합·라우팅·실행하는 얇은 호스트가 된다. 세상의 모든 변환 도구(FFmpeg/Ghostscript/Pandoc/LibreOffice/Calibre/qpdf/LLM)는 코드가 아닌 선언적 manifest(JSON)로 등록되며, 코어는 이 능력들을 방향 그래프로 합성해 manifest 작성자가 한 번도 명시하지 않은 멀티홉 경로(HWP→PDF→DOCX, PNG→PDF 압축→DOCX)까지 자동으로 발견한다. 결과적으로 새 포맷·새 도구 추가가 코어 재컴파일 없이 manifest 파일 하나로 끝나고, 커버리지가 manifest 수에 비례해 폭발적으로 증가한다. AI(Codex/LLM)조차 '특별한 기능'이 아니라 manifest로 선언된 또 하나의 어댑터일 뿐이며, 키가 없으면 그래프에서 자동으로 비활성 노드가 된다. 이것이 진정한 N×M×멀티홉 — 사람이 손으로 짠 switch가 아니라, 능력 선언의 자동 합성으로 달성하는 '변환 프로그램의 극한'이다.", + "keyMoves": [ + "코어 분리 + 어댑터 베이스 추출: IConverterProvider를 Everything2Everything.Abstractions 별도 어셈블리로 분리(타입 동일성 보장)하고, 7개 in-box Provider의 외부 프로세스 호출 로직(현재 DocumentProvider.SofficeConvertAsync:238-281이 3개 Provider에 복붙됨)을 단일 ExternalToolProvider 추상 베이스 + ProcessRunner(CliWrap 기반, 타임아웃·stderr 수집·Kill 통합)로 통합. 이 베이스가 manifest 어댑터의 실행 엔진이 된다.", + "선언적 Manifest + 동적 등록 파이프라인: tools/*.manifest.json 스키마를 기존 ProviderCapability/ConversionPair/ExternalDependency 모양 그대로 직렬화한 형태로 정의. manifest는 (tool id, 실행파일 탐지 규칙, 입력×출력 매트릭스, argument 템플릿 {input}/{output}/{outdir}/{format}, 성공 판정, LossClass)를 선언. ManifestLoader가 런타임에 읽어 ExternalToolProvider 인스턴스로 합성. ProviderRegistry를 '닫힌 생성자'에서 Register/RegisterRange/Rebuild가 가능한 '증분 등록' 구조로 개조(현재 생성자 17-30행 인덱싱 로직을 private Index(provider)로 추출).", + "ProviderRegistry → ConversionGraph 승격 + 멀티홉 경로 탐색: 모든 Provider의 Capability.SupportedConversions를 순회해 방향 그래프(노드=확장자, 엣지=Provider+LossClass 가중치) 구축. 외부 의존성 없는 자체 Dijkstra(~100줄)로 최저손실 경로 탐색. ConversionEngine.ConvertOneAsync(91행)에서 직접 엣지가 없으면 그래프 탐색으로 폴백, 경로의 각 홉을 ExecuteChainAsync로 순차 실행(중간 산출물은 DocumentProvider의 workDir 패턴 재사용). DocumentProvider의 손으로 짠 RouteAsync switch는 '단일 홉 원자 변환'만 선언하도록 분해 → md→docx 같은 경로는 엔진이 자동 합성.", + "레지스트리 충돌 모델 교체: _byPair.TryAdd(22행)의 '조용한 first-wins'를 ProviderCapability.Priority 필드 + 명시적 다중 Provider 공존 모델로 교체. 같은 (input,output)에 빠른변환/고품질/AI 등 복수 전략을 등록하고 우선순위·진단으로 선택. 이로써 manifest 어댑터가 in-box Provider를 덮어쓰는 사고를 방지하고 변환 전략 다중화 가능.", + "ConvertRequest/ConvertResult 일반화 + 옵션 백 분해: IConverterProvider 시그니처를 단일 sourcePath/outputExtension(IConverterProvider.cs:9-15)에서 ConvertRequest(다중 입력·미디어 메타·옵션 백)/ConvertContext로 일반화. ConvertResult(현재 OutputPaths만, ConvertResult.cs:10)에 추출 텍스트·AI 응답·메타데이터 필드 추가. ConvertOptions 갓 오브젝트(11개 sub-record)를 manifest별 IReadOnlyDictionary 옵션 백으로 분해해 Video/Audio/Ai 옵션을 sub-record 증식 없이 수용.", + "AI는 또 하나의 manifest 어댑터: LlmProvider를 Microsoft.Extensions.AI(IChatClient) 추상화 위에 구축. 기본 경로는 API 키(OpenAI/Anthropic 공식 SDK), opt-in 보조 경로는 Codex CLI(codex exec --json --output-schema, PATH 감지 시만 활성). 키는 DPAPI(ProtectedData)로 암호화 저장하는 ISettingsStore 신설. CheckAvailabilityAsync가 키/CLI 부재 시 NotReady→그래프에서 자동 비활성 노드 처리. AI는 로컬 변환이 없는 신규 페어(요약/번역/캡션/메타데이터)에만 노출, UI에 'AI' 배지." + ], + "roadmap": [ + { + "phase": "P0", + "title": "기반 해체 — 어댑터 베이스 + 증분 레지스트리 + 외부프로세스 통합", + "goal": "플러그인 아키텍처가 얹힐 '얇은 호스트' 토대를 만든다. 사용자 체감 변화는 없지만 이후 모든 단계의 전제조건. 동시에 가장 위험한 부채(타임아웃 없는 soffice, stderr 미수집, 3중 복붙)를 제거한다.", + "deliverables": [ + "Everything2Everything.Abstractions 어셈블리 분리 (IConverterProvider/ProviderCapability/ConversionPair/ConvertResult 이전, 플러그인이 false>로 참조해 타입 동일성 보장)", + "ProcessRunner 추상화 (CliWrap 기반: 타임아웃 + stderr 수집 + 취소 시 Kill(true) 통합). DocumentProvider.SofficeConvertAsync(238-281)·HwpxProvider·DocxProvider의 복붙 제거", + "ExternalToolProvider 추상 베이스 추출 (manifest 어댑터와 in-box 외부도구 Provider의 공통 실행 엔진)", + "ProviderRegistry 증분 등록 개조: 생성자 인덱싱(17-30행)을 private Index(provider)로 추출 + public Register/RegisterRange/Rebuild 추가", + "Everything2Everything.Core.Tests (xUnit) 신설 + OutputPathHelper·ProviderRegistry·PairsFromMatrix 순수함수 회귀 테스트 (현재 테스트 0개)", + "ISettingsStore 도입 (DPAPI 암호화 JSON) — AI 키·도구 경로 영속화 토대" + ] + }, + { + "phase": "P1", + "title": "선언적 Manifest 생태계 — 동적 등록으로 커버리지 폭발", + "goal": "내 관점의 핵심. 새 외부 도구를 코드 없이 manifest 한 장으로 등록하는 파이프라인을 완성하고, 이를 통해 FFmpeg(미디어)·qpdf/Ghostscript(PDF 압축)·Pandoc(학술문서)·Calibre(전자책)를 즉시 추가해 커버리지를 폭발시킨다.", + "deliverables": [ + "tools/*.manifest.json 스키마 정의 (tool id, 탐지 규칙, 입력×출력 매트릭스, argument 템플릿, 성공판정, LossClass, ExternalDependency)", + "ManifestLoader + ExternalToolDetector 일반화 (TryFindFfmpeg/Pandoc/Qpdf/Ghostscript/CalibreEbookConvert를 manifest의 toolDetect 규칙으로 데이터화)", + "FFmpeg manifest (FFMpegCore + BtbN lgpl-shared 빌드, RequiresExternal + 최초사용 시 자동조달): 영상 mp4/mkv/webm/mov + 오디오 mp3/aac/opus/flac/wav N×M, NotifyOnProgress→IProgress 연결", + "PDF 압축/유틸 manifest: qpdf(Apache, 구조최적화) + PDFsharp(in-box, 병합/분할) + Ghostscript(사용자 설치 감지, 고급압축) 2계층. 'pdf→pdf 동일포맷'을 옵션으로 구분하는 PdfToolProvider", + "Pandoc + Calibre manifest (md/rst/latex/epub/mobi 등 마크업·전자책 매트릭스)", + "ConvertOptions 옵션 백 분해 (manifest별 IReadOnlyDictionary로 코덱/비트레이트/압축레벨 수용, sub-record 증식 차단)" + ] + }, + { + "phase": "P2", + "title": "그래프 합성 — 진짜 멀티홉 N×M", + "goal": "manifest로 폭증한 노드들을 자동 합성해, 누구도 명시하지 않은 경로(HWP→PDF→DOCX, PNG→PDF→압축)를 엔진이 스스로 발견하게 한다. 양방향·다방향 극대화의 근본 해결.", + "deliverables": [ + "ConversionGraph 신설 (Capability 순회로 방향 그래프 빌드, 노드=확장자/엣지=Provider+LossClass)", + "자체 Dijkstra 경로 탐색 (~100줄, 외부 의존성 0, 비용=−log(품질보존율)+홉페널티+실행비용). ConvertOptions에 AllowMultiHop/MaxHops/AvoidLossy 추가", + "ConversionEngine.ConvertOneAsync(91행) 멀티홉 폴백 + ExecuteChainAsync (중간 산출물 workDir 패턴 재사용, 진행률 홉 분할)", + "DocumentProvider.RouteAsync(92-205) 분해: 손으로 짠 switch 그래프 제거 → md→html/html→docx 같은 '단일 홉 원자 변환'만 선언, 멀티홉은 그래프가 자동 합성", + "레지스트리 충돌 모델 교체 (ProviderCapability.Priority + 다중 Provider 공존 + 충돌 진단, TryAdd 조용한 first-wins 폐기)", + "OutputsForInput을 transitive closure로 확장 — 'HWP 출력 가능 전체 포맷' UI 표시 + 손실 경로 경고 배지", + "배치 제한 병렬화 (Parallel.ForEachAsync, 현재 순차 for-loop:57-70 대체)" + ] + }, + { + "phase": "P3", + "title": "AI 어댑터 + 한글/역변환 강화", + "goal": "AI를 manifest 생태계의 일급 시민으로 편입하고(특별 취급 없음), 한글 문서 양방향과 PDF 역변환의 실용 품질을 끌어올린다.", + "deliverables": [ + "LlmProvider (Microsoft.Extensions.AI/IChatClient): OpenAI/Anthropic 공식 SDK 경로 + Codex CLI opt-in 경로(codex exec --json --output-schema)", + "ConvertOptions.Llm 옵션 백 + DPAPI 키 저장(ISettingsStore 활용) + 환경변수 fallback. CheckAvailability 게이트로 키 부재 시 그래프 자동 비활성", + "AI 변환 매트릭스: 요약/번역/포맷정규화/OCR교정(OcrProvider 출력 2단계 파이프)/이미지캡션/메타데이터(Structured Outputs). 로컬 변환 없는 신규 페어에만 노출 + 'AI' 배지", + "HWP→DOCX/HTML/TXT/ODT 정방향 확장 (LibreOffice 단일 엔진, HwpOutputs 배열에 .docx/.html/.txt/.odt 추가 + .hwp 입력 시 --infilter='Hwp2002_File')", + "PDF→DOCX/HTML/TXT 역변환 (DocumentProvider Inputs에 .pdf 추가 + soffice 경로, PdfPig 무외부 폴백)", + "함초롬/맑은고딕 폰트 누락 감지 경고 + JRE 미설치 안내 (HWP 레이아웃 깨짐 예방)" + ] + }, + { + "phase": "P4", + "title": "헤드리스 CLI + ConvertRequest 일반화 + 배포", + "goal": "manifest 생태계를 자동화·스크립팅·AI 파이프라인에서 쓸 수 있게 개방하고, 외부 바이너리 번들 배포를 확립한다.", + "deliverables": [ + "ConvertRequest/ConvertContext 일반화 (다중 입력 N→1, 미디어 메타 프로빙, 비파일 결과). ConvertResult에 ExtractedText/AiResponse/Metadata 필드", + "헤드리스 CLI 모드 분리 (stdout JSON + exit code + --quality/--output-dir/--json/--recursive 플래그). 현재 CLI는 WPF 창만 띄우고 stdout 무반환", + "워치폴더 모드 (FileSystemWatcher + 디바운스 + 파일잠금 재시도) — 핫폴더→큐 자동적재", + "BuildMsix.ps1 외부 바이너리 번들 파이프라인 (FFmpeg lgpl-shared 동봉 + 라이선스 고지, MSIX 용량/internetClient capability)", + "CI 강화 (NuGet 캐시 + self-contained portable EXE 산출 + 테스트 게이트)", + "UI MVVM 분해 + 선언형 옵션 스키마 기반 동적 옵션 UI 생성 (manifest가 옵션 스키마도 선언 → XAML 하드코딩 제거)" + ] + } + ], + "biggestRisk": "manifest 추상화의 '표현력 천장'과 멀티홉 신뢰성이 동시에 무너지는 것. (1) 선언적 argument 템플릿({input}/{output}/{format})은 단순 CLI 도구에는 완벽하지만, FFmpeg의 코덱별 HW가속 폴백(nvenc 실패→AV1)이나 조건부 인자처럼 '로직이 필요한' 변환은 manifest로 표현 불가능 — 결국 manifest 어댑터에 escape hatch(코드 후크)를 열어줘야 하고, 그 순간 '코드 없는 확장'이라는 핵심 약속이 부분적으로 깨진다. 경계 설계(어디까지 manifest, 어디부터 코드)를 P1에서 명확히 긋지 못하면 manifest가 또 다른 갓 오브젝트가 된다. (2) 멀티홉 그래프 합성은 강력하지만 품질 손실이 누적·은폐된다 — HWP→PDF(래스터화)→DOCX는 '편집가능 DOCX'를 약속하지만 실제로는 이미지 덩어리를 반환할 수 있다. LossClass 가중치가 부정확하면 사용자가 인지하지 못한 채 최악 경로를 탄다. 완화책: manifest는 '90% 도구(단순 CLI)'만 커버하고 복잡 로직 도구(FFmpeg/AI)는 명시적으로 in-box 코드 Provider로 유지하는 하이브리드를 P1부터 원칙으로 못박을 것, 그리고 멀티홉은 '직접 엣지가 없을 때만' 발동 + 손실 경로 UI 경고 배지 + MaxHops 제한 + 금지 전이 블랙리스트로 가드레일을 세 겹으로 두는 것." + }, + { + "angle": "AI·미디어 기능 우선 — 사용자가 명시한 신규 가치(Codex/API AI 통합, 영상/오디오/PDF 압축, HWP 한글 변환)를 최단 경로로 출시하고, 그래프/레지스트리 리팩터링은 '그 기능을 켜기 위한 최소 인프라'로만 취급한다. 인프라 완성도가 아니라 사용자 체감 차별화가 북극성이다.", + "vision": "Everything2Everything을 '무엇이든 → 무엇이든, 그리고 변환하면서 더 좋아지는' 도구로 만든다. 핵심 차별화는 두 가지다. (1) 변환이 단순 포맷 치환이 아니라 'AI 부가가치 레이어'를 거친다 — 영상을 mp4로 바꾸면서 자동으로 자막을 뽑고, PDF를 압축하면서 OCR 오탈자를 LLM이 교정하고, HWP를 DOCX로 풀면서 요약·번역을 곁들인다. AI는 변환의 핵심 엔진이 아니라 '후처리 부가가치 단계'로 배치해, API 키가 없어도 모든 기존 변환은 100% 동작하고 AI 페어에만 '✨ AI' 배지가 붙는다. (2) 미디어를 1급 시민으로 — FFmpeg(영상/오디오 코덱·압축), Ghostscript/PDFsharp(PDF 압축), LibreOffice+H2Orestart(HWP→DOCX/PDF/HTML)를 외부 도구 분리-호출 모델로 합법적으로 통합해 '이미지 변환기'에서 '미디어·문서·AI 통합 변환기'로 카테고리를 점프시킨다. 멀티홉 그래프는 이 모든 걸 '손으로 짠 switch' 없이 자동 합성하는 배관일 뿐, 사용자에게는 '이 파일로 만들 수 있는 모든 포맷'이라는 풍부한 출력 목록과 '손실 변환' 경고 배지로만 드러난다. 즉 N×M 매트릭스 극대화 + AI 부가가치 = 경쟁 변환기가 흉내 못 내는 해자.", + "keyMoves": [ + "ConvertContext/ConvertRequest 도입으로 IConverterProvider 시그니처 일반화 — 단일 sourcePath/단일 outputExtension/IProgress 고정(IConverterProvider.cs:9-15)을 다중 입력·비파일 결과(추출 텍스트·AI 응답·미디어 메타데이터)·다단계 진행률을 담는 컨텍스트 객체로 교체. AI·미디어·압축 Provider가 요구하는 모든 표현을 인터페이스 레벨에서 한 번에 연다.", + "ConvertOptions 갓-오브젝트(ConvertOptions.cs)를 형식별 옵션 백으로 분해하고 Ai/Media/PdfCompress 옵션 그룹 신설 — 영상 코덱·CRF·fps, 오디오 비트레이트, AI 모델·프롬프트·온도·백엔드(openai|anthropic|codex-cli|auto), PDF 압축 레벨(Light/Strong/Max)을 담을 자리를 만들고 DPAPI 암호화 설정 영속화 계층(ISettingsStore)을 신설해 API 키·도구 경로를 안전 저장.", + "ProviderRegistry를 얇은 ConversionGraph로 승격 — _byPair 단일홉(ProviderRegistry.cs:6,43) 위에 인접 리스트 그래프를 얹고 외부 의존성 없는 자체 Dijkstra(80~120줄, 비용=손실가중+홉페널티)로 멀티홉 경로를 자동 합성. DocumentProvider.RouteAsync(DocumentProvider.cs:92-205)의 손으로 짠 md→html→docx switch를 '원자 변환 선언 + 엔진 자동 합성'으로 대체해 형식 추가 시 O(N²) 수동 증식을 제거.", + "공통 ExternalProcessRunner + 외부 도구 어댑터 추상화 신설 — 3곳에 복붙된 LibreOffice 호출(DocumentProvider/DocxProvider/HwpxProvider)을 타임아웃·stderr 수집·Kill 통합 단일 러너로 합치고, FFmpeg·Ghostscript·qpdf·codex CLI를 manifest 기반으로 동일 패턴 재사용. 신규 외부 도구 추가가 보일러플레이트 복붙 없이 끝나게.", + "신규 1급 Provider 4종 추가 — LlmProvider(요약·번역·캡션·OCR교정·메타데이터, MEAI IChatClient 추상화 + Codex CLI opt-in), FfmpegProvider(영상/오디오 N×M 코덱·압축, RequiresExternal + LGPL 빌드 자동조달), PdfToolProvider(PDF→PDF 압축/병합/분할, PDFsharp in-process + gs 고급압축 폴백), 그리고 DocumentProvider 출력 매트릭스 확장으로 HWP→DOCX/HTML/TXT/PDF 완성.", + "배치 병렬화 + 헤드리스 CLI — 순차 for-loop(ConversionEngine.cs:57-70)를 Parallel.ForEachAsync로 교체(AI 왕복·영상 트랜스코딩의 치명적 병목 해소)하고, stdout JSON + exit code + 옵션 플래그(--quality/--prompt/--codec/--json)를 받는 headless 모드를 분리해 AI 스크립팅·배치 자동화를 가능케.", + "ImageMagick ResourceLimits 전역 설정 + decompression bomb 방어 + NuGet 취약점 경고(NU190x) 재활성화 — AI/미디어로 공격면이 커지는 만큼 외부 미디어 파싱 라이브러리(WebView2/OpenXML/Magick/FFmpeg)의 위험을 횡단적으로 차단." + ], + "roadmap": [ + { + "phase": "Phase 0", + "title": "AI·미디어를 담을 그릇 만들기 (Enablement, 1~1.5주)", + "goal": "신규 기능을 끼워넣기 전, 인터페이스·옵션·설정·프로세스 실행이라는 4개 병목을 최소한으로 일반화한다. 이 단계 자체는 사용자에게 안 보이지만, 이걸 건너뛰면 AI/미디어 Provider가 또 하드코딩 switch로 변질된다.", + "deliverables": [ + "IConverterProvider를 ConvertContext(다중 입력·CancellationToken·다단계 IProgress) + ConvertRequest로 일반화하고, 기존 7개 Provider를 어댑터로 무중단 마이그레이션 (IConverterProvider.cs:9-15)", + "ConvertResult에 비파일 결과 필드 추가 — ExtractedText, AiResponse, Metadata(Dictionary), IntermediateArtifacts (ConvertResult.cs:10)", + "ConvertOptions 분해 + 신규 옵션 그룹 Ai/Media/PdfCompress 골격 추가 (ConvertOptions.cs)", + "ISettingsStore 신설 — DPAPI(ProtectedData) 암호화 JSON으로 API 키·LibreOffice/FFmpeg/gs 경로·기본 출력 형식 영속화 (현재 history.jsonl 외 영속 상태 전무)", + "공통 ExternalProcessRunner 추상화 — 타임아웃·stderr 수집·Kill 통합, DocumentProvider.SofficeConvertAsync(238-281) 등 3중 복제를 흡수" + ] + }, + { + "phase": "Phase 1", + "title": "헤드라인 기능 1탄: PDF 압축 + HWP 한글 변환 (가장 빠른 체감 가치, 1.5~2주)", + "goal": "외부 의존성이 이미 검증된(LibreOffice) 영역부터 친다. PDF 압축은 사용자가 명시한 핵심 목표이고, HWP→DOCX/PDF/HTML은 기존 코드 확장만으로 절반이 완성된다 — 최소 노력 대비 최대 차별화.", + "deliverables": [ + "PdfToolProvider 신설 — .pdf→.pdf 압축(Light=PDFsharp/qpdf 구조최적화, Strong=PDFium 렌더+Magick 재인코딩, Max=Ghostscript /screen 외부폴백) + 병합/분할. PdfProvider의 렌더 로직·ApplyEncoding 재사용", + "DocumentProvider 출력 매트릭스에 .pdf 추가 + HWP/HWPX → DOCX/HTML/TXT/PDF 완성 (soffice --convert-to 파라미터화, .hwp에 --infilter=\"Hwp2002_File\" 조건부 지정)", + "PDF → DOCX/HTML/TXT 역변환 — DocumentProvider Inputs에 .pdf 추가(soffice가 Draw로 열어 변환) + PdfPig 무외부 txt 폴백", + "Ghostscript/qpdf를 ExternalToolDetector로 감지(미설치 시 NotReady) — AGPL 전염 회피, LibreOffice 패턴 그대로", + "ImageMagick ResourceLimits 전역 설정 + NU190x 경고 재활성화 (보안 횡단)" + ] + }, + { + "phase": "Phase 2", + "title": "헤드라인 기능 2탄: 미디어 레이어 (영상·오디오 코덱·압축, 2~2.5주)", + "goal": "FFmpeg로 카테고리를 '미디어 변환기'로 점프시킨다. 이게 경쟁 이미지 변환기와의 가장 가시적인 차별화이자 사용자가 명시한 영상/오디오/압축 요구의 본체.", + "deliverables": [ + "FfmpegProvider 신설 — FFMpegCore(MIT) + 영상(mp4/mkv/webm/mov/avi/gif)·오디오(mp3/aac/m4a/opus/flac/wav) N×M 매트릭스, HW 인코더(nvenc/qsv/amf) 우선 + LGPL 빌드 SW 폴백", + "바이너리 조달 — ExternalToolDetector.TryFindFfmpeg + %LOCALAPPDATA% 자동 다운로드(BtbN lgpl-shared, 라이선스 안전), GlobalFFOptions 경로 고정", + "진행률·취소 — FFprobe duration 기반 NotifyOnProgress를 다단계 IProgress에 연결, CancellableThrough(ct)로 취소 직결", + "PreviewService에 영상 프레임 추출(FFmpeg) case 추가 + IPreviewRenderer 추상화 + 프리뷰 캐시 (현재 mp4 입력 시 MagickImage 예외)", + "배치 병렬화 — ConvertManyAsync 순차 for-loop를 Parallel.ForEachAsync로 교체 (영상 트랜스코딩 병목 해소, ConvertOptions에 MaxDegreeOfParallelism)" + ] + }, + { + "phase": "Phase 3", + "title": "헤드라인 기능 3탄: AI 부가가치 레이어 (Codex OAuth + API, 2~2.5주)", + "goal": "변환에 'AI가 더 좋게 만든다'는 해자를 얹는다. 기본은 API 키 + 공식 SDK, Codex CLI는 구독자용 opt-in 보조 경로. 키가 없으면 AI 페어만 비활성, 기존 변환은 무영향.", + "deliverables": [ + "LlmProvider 신설 — MEAI IChatClient 추상화로 OpenAI/Anthropic 연결 + Codex CLI 감지 시 opt-in 백엔드(codex exec --json --output-schema)", + "AI 변환 매트릭스 — 요약(pdf/docx/txt→txt/md), 번역(→대상언어), OCR교정(OcrProvider 출력 2단계 파이프라인), 이미지 캡션(png/jpg→txt 비전), 메타데이터 생성(→json Structured Output)", + "키 관리 — ISettingsStore DPAPI 암호화 저장 + OPENAI_API_KEY/ANTHROPIC_API_KEY 환경변수 폴백, CheckAvailabilityAsync가 키/codex --version 게이트", + "UI — AI 출력 페어에 '✨ AI' 배지(종량과금·네트워크 명시) + 설정에서 백엔드/모델/키 입력, 등록 순서로 'AI는 로컬 변환 없는 신규 페어에만 노출'", + "헤드리스 CLI 확장 — --prompt/--codec/--quality/--json 플래그로 AI·미디어 스크립팅 가능" + ] + }, + { + "phase": "Phase 4", + "title": "멀티홉 그래프로 매트릭스 자동 극대화 (1.5~2주)", + "goal": "앞 단계에서 쌓인 모든 원자 변환을 그래프가 자동 합성해 '진짜 N×M·다방향'을 완성한다. AI/미디어가 먼저 들어와 있어야 그래프의 가치가 폭발한다 (예: hwp→pdf→png, video→mp3→txt(AI전사)).", + "deliverables": [ + "ConversionGraph 신설 — Provider Capability 순회로 인접 리스트 빌드 + 자체 Dijkstra(비용=손실가중+홉페널티), 직접 엣지 우선·최대 홉 3·손실 블랙리스트 안전장치", + "ConvertOptions에 AllowMultiHop/MaxHops/AvoidLossy 추가 + ConvertOneAsync가 직접 엣지 없을 때 FindBestPath→ExecuteChainAsync 위임 (중간산출물 workDir 패턴 재사용)", + "DocumentProvider.RouteAsync(92-205) 손코딩 멀티홉 제거 — 원자 변환만 선언, md→docx는 엔진이 md→html→docx 자동 합성", + "UI — OutputsForInput을 그래프 reachability(transitive closure)로 확장해 '이 파일로 만들 수 있는 모든 포맷' 노출 + 손실 경로 '손실 변환' 경고 배지", + "ProviderRegistry 충돌을 명시적 우선순위/진단으로 전환 (_byPair.TryAdd 조용한 first-wins 제거, ProviderRegistry.cs:22)" + ] + }, + { + "phase": "Phase 5", + "title": "확장성·신뢰성 굳히기 (지속)", + "goal": "기능이 다 들어온 뒤 회귀 방지·확장 지점·배포를 다진다. 차별화는 끝났으니 여기서부터는 '깨지지 않게' 유지하는 단계.", + "deliverables": [ + "테스트 프로젝트 신설(xUnit) — 그래프 경로탐색·OutputPathHelper 충돌·결합 로직·JSONL round-trip 등 순수 함수 우선 (현재 테스트 0개)", + "manifest 기반 외부 도구 어댑터 + source generator 자동 등록 — Bootstrap 하드코딩 배열(9-21) 제거, 새 CLI 도구를 코드 빌드 없이 추가", + "CombineAsync를 IMultiInputProvider로 분리 — ConversionEngine의 ImageMagick 직접 의존(164-257) 제거, PDF병합·영상concat으로 결합 확장", + "CI 강화 — NuGet 캐시 + self-contained portable 산출물 + 외부 바이너리 번들링 파이프라인(FFmpeg LGPL 고지) + 테스트 게이트", + "아카이브(SharpCompress)·데이터(Parquet/ClosedXML/CsvHelper)·벡터(Svg.Skia) 등 순수 .NET 카테고리 추가로 빈 카테고리 보강" + ] + } + ], + "biggestRisk": "AI·미디어 기능을 최단 경로로 밀다 보면 'Phase 0 인프라 일반화'를 건너뛰고 LlmProvider/FfmpegProvider를 또 하드코딩으로 끼워넣으려는 유혹이 가장 크다 — 그러면 DocumentProvider.RouteAsync처럼 새 switch 지옥이 카테고리마다 생겨 6개월 뒤 멀티홉 그래프(Phase 4)를 얹을 수 없게 된다. 두 번째 리스크는 라이선스: FFmpeg(GPL 빌드 번들 금지, LGPL 분리호출만)·Ghostscript/MuPDF(AGPL, 사용자 설치본 감지만)·H2Orestart/Calibre(GPL, 외부 프로세스 분리)를 본체에 정적 링크하면 상업 배포가 즉시 오염된다. 모든 무거운 외부 도구는 반드시 '별도 프로세스 분리 호출 + 사용자 설치 감지 또는 LGPL 빌드 자동조달'로만 통합해야 하며, 이 경계를 코드 리뷰 게이트로 강제해야 한다. 세 번째는 AI의 비결정성·종량과금·네트워크 의존이 '변환은 로컬에서 예측가능하게 동작한다'는 사용자 신뢰를 깨뜨릴 수 있다는 점 — 그래서 AI는 절대 기본 경로를 점유하지 않고 opt-in '✨ AI' 배지 페어로만 노출하며, 키 없으면 조용히 비활성되도록 등록 순서·게이트를 설계의 불변식으로 박아야 한다." + } +] \ No newline at end of file diff --git a/docs/ssot/_data/master.json b/docs/ssot/_data/master.json new file mode 100644 index 0000000..9bf1a54 --- /dev/null +++ b/docs/ssot/_data/master.json @@ -0,0 +1,552 @@ +{ + "vision": "Everything2Everything의 북극성은 \"세상의 모든 변환을 원자(atomic) 엣지로 등록하면, 엔진이 그 조합으로 임의의 A→Z를 스스로 합성하고, 변환하면서 AI가 결과를 더 좋게 만드는 변환 그래프 OS\"다. 핵심 통찰은 현재 DocumentProvider.RouteAsync(92-205)가 사실상 '사람이 손으로 그린 Dijkstra'(md→html→docx, docx→html→md, hwp→html→md를 switch에 박아넣음)라는 점이며, 이 손그림을 삭제하고 엔진이 같은 경로를 '계산'하게 만드는 것이 모든 확장의 열쇠다. 그래프가 코어가 되면 FFmpeg(미디어), PDF 압축, HWP 양방향, AI 요약/번역/캡션이 전부 '엣지 추가'로 환원되고, OutputsForInput은 1-hop 직접 출력에서 도달 가능한 모든 포맷(transitive closure)으로 폭발한다. 동시에 사용자가 명시한 신규 가치(PDF 압축·HWP·영상·AI)를 인프라 완성을 기다리지 않고 빠르게 출시해 체감 차별화를 먼저 만든다. AI는 핵심 엔진이 아니라 '키 없으면 조용히 비활성되는 부가가치 엣지'로, 변환의 로컬 예측가능성이라는 신뢰를 절대 깨지 않는다.", + "elevatorPitch": "손으로 짠 변환 switch를 자동 경로 탐색 그래프로 교체하고, 그 위에 PDF 압축·영상·HWP·AI를 엣지로 얹어, 코드 한 줄당 N×M 매트릭스가 발현하는 '변환하면서 더 좋아지는' 만능 변환기.", + "designPrinciples": [ + { + "name": "변환은 엣지, 엔진은 라우터", + "description": "모든 Provider는 단일 홉 원자 변환(md→html, png→pdf)만 선언한다. 멀티홉(md→docx)은 절대 Provider 내부에 손으로 짜지 않고 엔진의 그래프 탐색이 자동 합성한다. DocumentProvider.RouteAsync의 switch 지옥이 재발하지 않도록 이를 불변식으로 강제한다." + }, + { + "name": "기능이 그래프를 견인하되, 그래프가 기능을 받친다", + "description": "사용자 체감 가치(PDF 압축·HWP·영상·AI)를 빠르게 출시하되, 신규 기능은 반드시 '그래프 엣지'로만 추가한다. Phase 0에 심은 그래프 코어가 하드코딩 유혹을 구조적으로 차단한다." + }, + { + "name": "손실은 가중치다", + "description": "품질 손실을 ConversionPair.LossClass(Lossless/Container/Recode/Rasterize)로 SSOT화하고 -log(보존율)+홉페널티로 환산한다. 멀티홉 경로 선택과 UI '손실 변환' 경고 배지가 모두 이 단일 출처를 소비한다." + }, + { + "name": "AI는 끄면 사라지는 부가 엣지", + "description": "AI는 절대 기본 경로를 점유하지 않는다. 키가 없으면 모든 기존 변환은 100% 동작하고 AI 페어만 자동 비활성(NotReady)되며 ✨AI 배지로만 opt-in 노출된다. 변환의 로컬 예측가능성 신뢰를 깨지 않는다." + }, + { + "name": "무거운 외부 도구는 분리 호출로만", + "description": "FFmpeg(GPL 정적링크 금지·LGPL 분리호출만), Ghostscript/MuPDF(AGPL·사용자 설치본 감지만), H2Orestart/Calibre(GPL·외부 프로세스 분리)를 본체에 절대 정적 링크하지 않는다. 라이선스 경계를 코드 리뷰 게이트로 강제해 상업 배포 오염을 원천 차단한다." + }, + { + "name": "순수 .NET 우선, 외부 바이너리 차선", + "description": "단일 포터블 EXE 부담을 줄이기 위해 SharpCompress·Parquet.Net·PDFsharp·Svg.Skia 같은 순수 관리 코드를 EXE에 직접 포함하고, FFmpeg/Pandoc/Calibre 같은 무거운 바이너리는 '외부 설치 감지 + 미설치 시 안내/자동조달' 모델로만 통합한다." + }, + { + "name": "점진 마이그레이션, 무중단", + "description": "IConverterProvider/ConvertResult/ConvertOptions 일반화는 기존 8개 Provider를 어댑터로 감싸 한 번에 깨지지 않게 한다. 모든 코어 변경은 회귀 테스트(현재 0개에서 출발)로 '동일 동작'을 객관 증명한다." + } + ], + "targetArchitecture": { + "overview": "4계층 변환 그래프 아키텍처. (1) Abstractions 계층이 Provider 계약을 담고, (2) 그래프 코어가 모든 원자 변환을 방향 그래프로 합성해 Dijkstra로 멀티홉 경로를 푼다. (3) Provider 계층은 in-box 코드 Provider(이미지/문서/미디어/AI)와 manifest 기반 외부 도구 어댑터로 나뉘며, (4) 실행 계층(ExternalProcessRunner·ISettingsStore)이 외부 프로세스·설정·키를 횡단 관리한다. 핵심은 ProviderRegistry를 단일 홉 딕셔너리에서 ConversionGraph로 승격하는 것이다.", + "layers": [ + { + "name": "Abstractions 계층 (Everything2Everything.Abstractions)", + "responsibility": "Provider 계약을 별도 어셈블리로 분리해 타입 동일성을 보장하고 향후 플러그인의 안정적 참조점을 제공", + "components": [ + "IConverterProvider", + "ConvertRequest/ConvertContext", + "ConvertResult(비파일 산출물 포함)", + "ProviderCapability", + "ConversionPair+LossClass", + "ExternalDependency" + ] + }, + { + "name": "그래프 코어 계층 (Core.Graph)", + "responsibility": "모든 Provider Capability를 순회해 방향 그래프(노드=확장자, 엣지=Provider+LossClass 가중치)를 빌드하고, 자체 Dijkstra로 최저손실 멀티홉 경로를 탐색·실행", + "components": [ + "ConversionGraph", + "PathFinder(자체 Dijkstra)", + "ChainExecutor(ExecuteChainAsync)", + "ConversionEngine(라우터로 축소)", + "ProviderRegistry(증분 등록 Register/Rebuild)" + ] + }, + { + "name": "Provider 계층", + "responsibility": "단일 홉 원자 변환 능력을 선언·실행. in-box 코드 Provider와 manifest 어댑터 Provider 공존", + "components": [ + "MagickProvider/PdfProvider/HtmlProvider(기존)", + "LlmProvider(AI)", + "FfmpegProvider(미디어)", + "PdfToolProvider(압축)", + "ImageCombineProvider(N→1)", + "ExternalToolProvider(manifest 어댑터 베이스)" + ] + }, + { + "name": "실행/인프라 계층", + "responsibility": "외부 프로세스 실행·설정 영속화·키 보안·미리보기를 횡단 제공", + "components": [ + "ExternalProcessRunner(CliWrap, 타임아웃+stderr+Kill)", + "ISettingsStore(DPAPI 암호화)", + "ExternalToolDetector(번들 경로 폴백)", + "IPreviewRenderer(프리뷰 캐시)", + "ManifestLoader" + ] + } + ], + "dataFlow": "파일 입력 → ConversionEngine.ConvertOneAsync가 입력/출력 확장자 정규화 → ConversionGraph.FindBestPath(in,out,options)로 경로 탐색(직접 엣지 있으면 1홉, 없으면 손실가중치 기반 멀티홉) → ChainExecutor가 경로의 각 홉을 순차 실행하며 중간 산출물을 공용 workDir(Temp/e2e_{Guid})에 체이닝 → 각 홉은 Provider.ConvertAsync(ConvertRequest) 호출, 진행률은 홉 수로 분할 매핑 → 마지막 홉 산출물을 OutputPathHelper로 충돌 해결 후 최종 출력 → ConvertResult(출력 경로 + 비파일 산출물) 반환, 중간 산출물 정리. AI/외부도구 엣지는 CheckAvailabilityAsync 게이트를 먼저 통과해야 그래프에 활성 노드로 참여." + }, + "coreDecisions": [ + { + "id": "ADR-1", + "title": "ProviderRegistry를 단일 홉 딕셔너리에서 ConversionGraph로 승격", + "decision": "_byPair 단일 룩업(ProviderRegistry.cs:6,43)을 유지하되 그 위에 인접 리스트 그래프(Dictionary>)를 빌드하고, 외부 의존성 없는 자체 Dijkstra(80~120줄, .NET 9 PriorityQueue 사용)로 멀티홉 경로를 탐색한다. DocumentProvider.RouteAsync의 손그림 멀티홉을 엔진 합성으로 대체.", + "rationale": "현재 멀티홉이 Provider 내부 switch에 하드코딩되어 형식 N개에 O(N²)로 수동 증식한다. NCSA Polyglot 모델(노드=포맷, 엣지=Provider, 가중치=손실)은 학계 검증된 best practice이며, 그래프가 수십 노드·수백 엣지 규모라 성능 이슈가 없다.", + "alternatives": "QuikGraph(MS-PL, 2022 이후 정체)·Pandoc식 단일 AST 허브(이질적 도메인에 부적합). 자체 구현이 단일 EXE/AOT/라이선스 검토 모두 무부담이라 1순위.", + "tradeoffs": "멀티홉은 중간 임시파일 I/O가 늘고 손실이 누적될 수 있다. 완화: 직접 엣지 우선, MaxHops=3 제한, 손실 블랙리스트, 손실 경로 UI 경고 배지." + }, + { + "id": "ADR-2", + "title": "손실을 ConversionPair.LossClass 가중치로 SSOT화", + "decision": "ConversionPair에 LossClass(Lossless=0/Container=0.05/Recode=0.4/Rasterize=0.8) 필드를 추가하고, 엣지 가중치를 -log(품질보존율)+홉페널티+실행비용 합산으로 계산한다. UI '손실 변환' 배지도 이 가중치를 소비.", + "rationale": "손실은 본래 곱셈적(0.9×0.8)이므로 -log 변환으로 덧셈 최단경로(Dijkstra)가 곧 최대 품질보존 경로가 된다. 래스터화(텍스트/벡터→PNG)는 단방향 손실 절벽이므로 큰 페널티로 자연 회피.", + "alternatives": "동적 손실 측정(Versus식 실측). 초기엔 정적 가중치 테이블로 시작하고 동적 측정은 처음부터 넣지 않는다(과도한 복잡도).", + "tradeoffs": "정적 가중치는 추정값이라 일부 쌍에서 비최적 경로 가능. 완화: 보수적으로 직접 엣지 우선, 멀티홉은 fallback으로만 운영." + }, + { + "id": "ADR-3", + "title": "레지스트리 충돌을 조용한 first-wins에서 Priority 기반 명시 선택으로 교체", + "decision": "_byPair.TryAdd(ProviderRegistry.cs:22)의 '조용한 첫 등록자 우선'을 ProviderCapability.Priority 필드 + 다중 Provider 공존 모델 + 충돌 시 진단 경고로 교체한다. 같은 (input,output)에 빠른변환/고품질/AI 등 복수 전략 등록 허용.", + "rationale": "PDF압축 vs PDF렌더, AI변환 vs 일반변환처럼 한 쌍에 복수 전략이 필연적으로 생긴다. 현재는 부트스트랩 순서에 따라 비결정적으로 한쪽이 조용히 사라져 데이터 손실이다.", + "alternatives": "현 first-wins 유지(확장 불가). 비용 기반 자동 선택만(사용자 전략 선택 불가). Priority+공존이 그래프 가중치와도 자연 연결.", + "tradeoffs": "같은 쌍에 복수 Provider가 등록되면 UI에서 전략 선택지를 노출해야 하는 추가 복잡도. 완화: 기본은 최저비용 자동 선택, 고급 모드에서만 명시 선택." + }, + { + "id": "ADR-4", + "title": "IConverterProvider 시그니처를 ConvertRequest/ConvertContext로 일반화", + "decision": "단일 sourcePath/단일 outputExtension/IProgress 고정 시그니처(IConverterProvider.cs:9-15)를 ConvertRequest(다중 입력·옵션 백·미디어 메타) + ConvertContext로 일반화하고, ConvertResult(ConvertResult.cs)에 ExtractedText/AiResponse/Metadata/IntermediateArtifacts 필드를 추가한다. 기존 8개 Provider는 어댑터로 감싸 무중단 마이그레이션.", + "rationale": "현 시그니처는 N→1 결합, AI 비파일 응답, 영상 메타데이터 프로빙, 멀티홉 중간 컨텍스트를 표현할 수 없다. 미디어/AI 엣지가 들어올 '그릇'을 코어에 먼저 판다.", + "alternatives": "시그니처 유지하고 옵션에 모든 것 욱여넣기(갓 오브젝트 가속). 점진 어댑터 전략이 8개 Provider 동시 파괴를 방지.", + "tradeoffs": "어댑터 계층이 일시적 중복을 만든다. 완화: 회귀 테스트로 동일 동작 보장 후 어댑터를 점진 제거." + }, + { + "id": "ADR-5", + "title": "AI는 IAiProvider 특수 인터페이스가 아니라 그래프의 부가 엣지로 편입", + "decision": "LlmProvider를 일반 IConverterProvider로 구현하고 Microsoft.Extensions.AI(IChatClient) 추상화 위에 OpenAI/Anthropic 공식 SDK를 연결한다. AI는 로컬 변환이 없는 신규 페어(요약/번역/캡션/메타데이터)에만 노출되고, 키 부재 시 CheckAvailabilityAsync가 NotReady를 반환해 그래프에서 자동 비활성된다.", + "rationale": "AI를 특수 카테고리로 두면 그래프·레지스트리 밖에 별도 배관이 생긴다. 엣지로 환원하면 OcrProvider가 Windows OCR을 흡수한 선례처럼 매트릭스에 자연 편입되고, AI 후처리 파이프(OCR→LLM 교정)도 멀티홉으로 자동 합성된다.", + "alternatives": "별도 IAiConverterProvider 확장(추상화 분기 증가). 통합 IConverterProvider가 단순하고 그래프와 정합.", + "tradeoffs": "AI는 비결정적·유료·네트워크 의존이라 '재현 가능한 변환'과 충돌. 완화: ✨AI 배지·기본 경로 불점유·키 없으면 비활성 불변식." + }, + { + "id": "ADR-6", + "title": "무거운 외부 도구는 분리 프로세스 호출 + 라이선스 게이트로만 통합", + "decision": "FFmpeg는 BtbN lgpl-shared 빌드를 별도 프로세스로 호출(LGPL 준수), Ghostscript/MuPDF는 AGPL이라 사용자 설치본 감지만, H2Orestart/Calibre/Pandoc은 GPL이라 외부 프로세스 분리. 공통 ExternalProcessRunner(CliWrap, 타임아웃+stderr+Kill)로 통일하고, 라이선스 경계를 코드 리뷰 게이트로 강제한다.", + "rationale": "단일 포터블 EXE 상업 배포에서 GPL/AGPL 바이너리 정적 링크는 즉시 라이선스 오염이다. 이미 LibreOffice를 외부 도구로 다루는 검증된 패턴을 그대로 확장.", + "alternatives": "GPL 빌드 번들(라이선스 위반)·상업 라이선스 구매(비용). 분리 호출 + 사용자 설치 감지/LGPL 자동조달이 안전.", + "tradeoffs": "진정한 자족 EXE가 아니라 외부 의존 체인이 길어진다. 완화: 순수 .NET 라이브러리 우선, 외부 도구는 NotReady로 친절히 안내." + }, + { + "id": "ADR-7", + "title": "CombineAsync를 ImageCombineProvider(N→1 엣지)로 분리", + "decision": "ConversionEngine.CombineAsync의 ImageMagick 직접 의존(ConversionEngine.cs:2,164-257)과 정적 HashSet(CombinableInputs/Outputs:14-23)을 IMultiInputProvider 추상화로 분리한다. 엔진은 라이브러리 중립이 되고 결합 가능 형식은 Provider 능력 선언으로 통합.", + "rationale": "현재 '결합'이 Provider 추상화 밖에 있어 엔진이 ImageMagick에 결합되고, PDF 병합·동영상 concat·오디오 믹스 같은 비이미지 결합으로 확장 불가하다. 정적 HashSet과 능력 선언의 이중 관리도 해소.", + "alternatives": "현 구조 유지(이미지 결합만 영구 고착). N→1 추상화가 모든 결합을 동일 패턴으로 흡수.", + "tradeoffs": "결합 진행률 보고가 단일 출력 가정과 달라 재설계 필요. 완화: ConvertProgress를 N→1 케이스로 확장." + }, + { + "id": "ADR-8", + "title": "ConvertOptions 갓 오브젝트를 그래프 옵션 + 형식별 옵션 백으로 분해", + "decision": "11개 sub-record 갓 오브젝트(ConvertOptions.cs:35-55)를 그래프 옵션(AllowMultiHop/MaxHops/AvoidLossy) + 형식별 옵션 백(IReadOnlyDictionary 또는 Provider 선언형 스키마)으로 분해한다. Video/Audio/Ai/PdfCompress를 sub-record 증식 없이 수용하고 ISettingsStore(DPAPI 암호화)로 영속화.", + "rationale": "형식 추가마다 sub-record가 비대해지고 모든 Provider가 무관한 옵션을 끌고 다닌다. 영상 코덱·AI 프롬프트·PDF 압축 레벨을 담을 자리가 코어 record 증식 없이 필요하다.", + "alternatives": "sub-record 계속 추가(god object 가속). 옵션 백이 형식별 옵션만 주입해 확장성 확보.", + "tradeoffs": "강타입 안전성이 약화된다. 완화: Provider가 옵션 스키마(이름/타입/범위/기본값)를 선언하고 UI가 동적 생성·검증." + }, + { + "id": "ADR-9", + "title": "manifest는 풀 DSL이 아니라 단순 CLI용 선언적 인자 템플릿으로 제한 채택", + "decision": "manifest를 ExternalProcessRunner 위의 '선언적 인자 템플릿({input}/{output}/{outdir}/{format})'으로만 좁게 채택해 qpdf/Ghostscript 같은 단순 CLI 압축 도구를 코드 없이 추가한다. 복잡 로직(FFmpeg HW가속 폴백·AI)은 in-box 코드 Provider 원칙을 P1부터 못박는다.", + "rationale": "Provider 8개·테스트 0개 단일 개발자 프로젝트에 풀 manifest DSL·동적 ALC 로더는 ROI가 낮다. FFmpeg의 nvenc→AV1 조건부 폴백은 manifest로 표현 불가하므로 하이브리드 경계가 필수.", + "alternatives": "풀 플러그인 생태계(과잉 엔지니어링)·전부 코드(확장 비용). 좁은 manifest가 단순 도구 추가 비용만 제거.", + "tradeoffs": "manifest가 또 다른 갓 오브젝트가 될 위험. 완화: '90% 단순 CLI만 manifest, 복잡 로직은 in-box' 경계를 P1 불변식으로 명문화." + } + ], + "roadmap": [ + { + "phase": "P1", + "title": "그래프 엔진 도입 + 즉시 체감 가치(PDF 압축)", + "goal": "ProviderRegistry를 ConversionGraph로 승격하고 멀티홉 경로 탐색을 엔진에 내장한다. 동시에 PDF 압축이라는 즉시 체감 신기능을 출시해 '보이지 않는 리팩터링의 함정'을 회피한다.", + "deliverables": [ + "ConversionGraph + 자체 Dijkstra PathFinder(외부 의존성 0, .NET 9 PriorityQueue)", + "ConversionPair.LossClass 필드 + 정적 가중치 테이블", + "ConversionEngine.ConvertOneAsync 그래프 위임 + ChainExecutor(공용 workDir 헬퍼)", + "PdfToolProvider 신설: PDF 압축(Light=PDFsharp 구조최적화, Strong=PDFium 렌더+Magick 재인코딩, Max=Ghostscript 외부폴백) + 병합/분할", + "xUnit 테스트 프로젝트 신설(현재 0개) + 그래프 경로탐색 회귀 테스트" + ], + "keyChanges": [ + { + "area": "ProviderRegistry.cs", + "change": "_byPair 위에 인접 리스트 그래프 빌드, 증분 등록 Register/Rebuild 추가" + }, + { + "area": "ConversionEngine.cs:91", + "change": "TryGet 직접 매핑에서 그래프 FindBestPath→ExecuteChainAsync 위임으로 전환" + }, + { + "area": "ConversionPair", + "change": "LossClass 필드 추가, 엣지 가중치 SSOT" + }, + { + "area": "신규 PdfToolProvider", + "change": "동일포맷 pdf→pdf Skip(ConversionEngine.cs:88) 우회, 3단계 압축" + } + ], + "effort": "L", + "risk": "medium", + "exitCriteria": "기존 모든 변환이 그래프 경로로 동일 동작(회귀 테스트 통과)하고, PDF 파일을 3단계 레벨로 압축해 출력 용량 감소를 GUI에서 확인 가능." + }, + { + "phase": "P2", + "title": "손그림 멀티홉 제거 + HWP 한글 양방향", + "goal": "DocumentProvider.RouteAsync의 손코딩 switch를 삭제하고 원자 엣지만 선언하게 해 그래프를 도그푸딩한다. HWP→DOCX/HTML/TXT 출력 매트릭스를 확장해 한글 사용자 핵심 요구를 충족.", + "deliverables": [ + "DocumentProvider.RouteAsync(92-205) 삭제 → md→html, html→docx 등 원자 엣지만 선언, md→docx는 엔진 자동 합성", + "HWP/HWPX 출력 확장: HwpxProvider Outputs에 .docx/.html/.txt/.odt 추가(soffice --convert-to 파라미터화)", + ".hwp 입력 시 --infilter='Hwp2002_File' 조건부 지정 + 함초롬/맑은고딕 폰트 누락 감지 경고", + "DocumentProvider 입력에 .pdf 추가 → pdf→docx/html/txt 역변환(soffice) + pdf→txt 무외부 폴백(PdfPig)", + "RouteAsync 삭제가 손그림과 동일 동작함을 회귀 테스트로 증명" + ], + "keyChanges": [ + { + "area": "DocumentProvider.cs:92-205", + "change": "멀티홉 switch 삭제, 단일 홉 원자 변환만 선언" + }, + { + "area": "HwpxProvider", + "change": "Outputs 배열에 .docx/.html/.txt/.odt 추가, soffice 타깃 파라미터화" + }, + { + "area": "DocumentProvider Inputs", + "change": ".pdf 추가로 PDF 역변환 엣지 개통" + } + ], + "dependsOn": "P1", + "effort": "M", + "risk": "medium", + "exitCriteria": "HWP/HWPX 파일을 DOCX/HTML/TXT/PDF로 변환 가능하고, md→docx 같은 멀티홉이 RouteAsync 없이 그래프 합성으로 동일하게 동작." + }, + { + "phase": "P3", + "title": "외부 프로세스 통합 + 인터페이스 일반화", + "goal": "3중 복제된 LibreOffice 호출을 단일 ExternalProcessRunner로 통합하고(타임아웃·stderr·Kill), IConverterProvider/ConvertResult를 일반화해 미디어·AI 엣지가 들어올 그릇을 판다.", + "deliverables": [ + "ExternalProcessRunner(CliWrap): 타임아웃+stderr수집+Kill 통합, LibreOffice 3중 복제 흡수", + "Abstractions 어셈블리 분리(IConverterProvider/ConvertResult 이전, 타입 동일성)", + "IConverterProvider→ConvertRequest/ConvertContext 일반화, 기존 8개 Provider 어댑터로 무중단 마이그레이션", + "ConvertResult에 ExtractedText/AiResponse/Metadata/IntermediateArtifacts 필드", + "ISettingsStore(DPAPI 암호화) 신설 — API 키·도구 경로 영속화 토대", + "Priority 기반 충돌 모델로 _byPair.TryAdd first-wins 교체" + ], + "keyChanges": [ + { + "area": "3개 Provider", + "change": "ConvertWithLibreOfficeAsync 복붙을 ExternalProcessRunner로 통합" + }, + { + "area": "IConverterProvider.cs:9-15", + "change": "ConvertRequest/ConvertContext로 일반화" + }, + { + "area": "ConvertResult.cs", + "change": "비파일 산출물 필드 추가" + }, + { + "area": "신규 ISettingsStore", + "change": "DPAPI ProtectedData 암호화 JSON 영속화" + } + ], + "dependsOn": "P2", + "effort": "L", + "risk": "medium", + "exitCriteria": "LibreOffice가 멈춰도 타임아웃으로 복구되고 stderr가 에러 메시지에 포함되며, 기존 변환이 일반화된 시그니처로 무중단 동작(회귀 테스트 통과)." + }, + { + "phase": "P4", + "title": "미디어 레이어 — 영상/오디오 코덱·압축", + "goal": "FFmpeg로 카테고리를 '미디어 변환기'로 점프시킨다. 영상/오디오 N×M 코덱·압축을 라이선스 안전하게 통합하고 배치 병렬화로 트랜스코딩 병목을 해소.", + "newProviders": [ + "FfmpegProvider" + ], + "deliverables": [ + "FfmpegProvider: FFMpegCore(MIT) + 영상(mp4/mkv/webm/mov/avi/gif)·오디오(mp3/aac/m4a/opus/flac/wav) N×M", + "바이너리 조달: ExternalToolDetector.TryFindFfmpeg + BtbN lgpl-shared 자동 다운로드(SHA256 검증), GlobalFFOptions 경로 고정", + "HW 인코더(nvenc/qsv/amf) 우선 + SW 폴백, NotifyOnProgress→IProgress 직결, CancellableThrough(ct)", + "배치 병렬화: ConvertManyAsync 순차 for-loop(57-70)를 Parallel.ForEachAsync로 교체(MaxDegreeOfParallelism)", + "PreviewService→IPreviewRenderer 추상화 + FFmpeg 프레임 추출 + 프리뷰 캐시", + "ImageMagick ResourceLimits 전역 설정(decompression bomb 방어) + NU190x 취약점 경고 재활성화" + ], + "keyChanges": [ + { + "area": "신규 FfmpegProvider", + "change": "FFMpegCore 래퍼, HW 가속 폴백, RequiresExternal" + }, + { + "area": "ConversionEngine.cs:57-70", + "change": "순차 for-loop를 Parallel.ForEachAsync로 교체" + }, + { + "area": "PreviewService.cs:23", + "change": "닫힌 switch를 IPreviewRenderer 레지스트리로, 영상 프레임 추출 추가" + } + ], + "dependsOn": "P3", + "effort": "XL", + "risk": "high", + "exitCriteria": "mp4→webm, wav→mp3 등 영상/오디오 변환이 HW 가속으로 동작하고 진행률·취소가 정확하며, 100개 배치가 멀티코어를 활용." + }, + { + "phase": "P5", + "title": "AI 부가가치 레이어 — Codex OAuth + API", + "goal": "변환에 'AI가 더 좋게 만든다'는 해자를 얹는다. 기본은 API 키 + 공식 SDK, Codex CLI는 구독자용 opt-in. 키 없으면 AI 페어만 비활성, 기존 변환 무영향.", + "newProviders": [ + "LlmProvider" + ], + "deliverables": [ + "LlmProvider: Microsoft.Extensions.AI(IChatClient)로 OpenAI/Anthropic 공식 SDK 연결 + Codex CLI opt-in(codex exec --json --output-schema)", + "AI 매트릭스: 요약(pdf/docx/txt→txt/md), 번역(→대상언어), OCR교정(OcrProvider 출력 2단계 파이프), 이미지 캡션(png/jpg→txt 비전), 메타데이터(→json Structured Output)", + "키 관리: ISettingsStore DPAPI 암호화 + OPENAI_API_KEY/ANTHROPIC_API_KEY 환경변수 폴백, CheckAvailabilityAsync 게이트", + "UI: AI 출력 페어에 ✨AI 배지(종량과금·네트워크 명시) + 설정에서 백엔드/모델/키 입력", + "Codex 경로 SemaphoreSlim(1) 직렬화(auth.json refresh 토큰 race 방지) 또는 --ephemeral" + ], + "keyChanges": [ + { + "area": "신규 LlmProvider", + "change": "IConverterProvider로 구현, AI는 로컬 변환 없는 신규 엣지로만" + }, + { + "area": "CheckAvailabilityAsync", + "change": "키/codex --version 게이트로 키 부재 시 NotReady→그래프 자동 비활성" + }, + { + "area": "UI", + "change": "AI 페어 ✨ 배지, 등록 순서로 기본 경로 불점유 보장" + } + ], + "dependsOn": "P3", + "effort": "L", + "risk": "high", + "exitCriteria": "API 키 입력 시 PDF 요약·이미지 캡션·번역이 동작하고, 키가 없으면 AI 페어만 사라지고 모든 기존 변환은 100% 동작." + }, + { + "phase": "P6", + "title": "매트릭스 자동 극대화 + 헤드리스 CLI", + "goal": "앞 단계에서 쌓인 모든 엣지를 그래프가 자동 합성해 진짜 N×M·다방향을 완성하고(video→mp3→txt AI전사 등), 헤드리스 CLI로 자동화·스크립팅을 개방한다.", + "deliverables": [ + "OutputsForInput을 transitive closure로 확장 — '이 파일로 만들 수 있는 모든 포맷' UI 노출 + 손실 경로 경고 배지", + "멀티홉 도그푸딩 검증: hwp→pdf→png, video→mp3→txt(AI) 같은 신규 합성 경로 동작 확인", + "헤드리스 CLI 분리: --json/--output-dir/--quality/--prompt/--codec/--recursive 플래그 + stdout JSON 결과 + exit code", + "워치폴더 모드(FileSystemWatcher + 디바운스 + 파일잠금 재시도, 출력 디렉터리 분리로 무한루프 방지)", + "QuickProgressWindow 취소 토큰 전파 + 케이퍼빌리티 사전 점검" + ], + "keyChanges": [ + { + "area": "ProviderRegistry.cs:52", + "change": "OutputsForInput을 그래프 reachability로 확장" + }, + { + "area": "CliRouter.cs:21", + "change": "옵션 플래그 파싱 + stdout JSON + exit code" + }, + { + "area": "App.xaml.cs:96", + "change": "Quick 경로에 취소 토큰 전파" + } + ], + "dependsOn": "P5", + "effort": "L", + "risk": "medium", + "exitCriteria": "HWP 파일에서 PNG까지(멀티홉) 변환 가능하고, CLI가 WPF 창 없이 JSON 결과를 stdout으로 반환해 스크립트가 파싱 가능." + }, + { + "phase": "P7", + "title": "순수 .NET 카테고리 보강 + manifest 어댑터", + "goal": "EXE 번들 가능한 순수 관리 라이브러리로 빈 카테고리를 채우고, 단순 CLI 도구를 코드 없이 추가하는 좁은 manifest 어댑터를 도입한다.", + "newProviders": [ + "ArchiveProvider", + "DataProvider", + "VectorProvider", + "PandocProvider", + "EbookProvider" + ], + "deliverables": [ + "ArchiveProvider(SharpCompress, 순수관리) — zip/7z/tar/gz/bz2", + "DataProvider(Parquet.Net/ClosedXML/CsvHelper) — csv↔json↔xlsx↔parquet", + "VectorProvider(Svg.Skia) — svg→png/jpg/webp/pdf, EPS는 Magick+Ghostscript", + "PandocProvider(외부 CLI) — md/rst/latex/ipynb/epub 마크업 매트릭스, LibreOffice 겹침은 Priority 라우팅", + "EbookProvider(Calibre ebook-convert, 외부) — epub↔mobi↔azw3↔pdf", + "manifest 어댑터(ExternalProcessRunner 위 인자 템플릿): qpdf/gs 같은 단순 CLI 코드 없이 추가" + ], + "keyChanges": [ + { + "area": "신규 4-5개 Provider", + "change": "순수 .NET은 EXE 직접 포함, 외부 CLI는 분리 호출" + }, + { + "area": "ManifestLoader", + "change": "tools/*.manifest.json으로 단순 CLI 엣지 추가" + }, + { + "area": "Bootstrap", + "change": "하드코딩 배열에 신규 Provider 등록 + manifest 동적 등록" + } + ], + "dependsOn": "P6", + "effort": "L", + "risk": "low", + "exitCriteria": "zip 압축/해제, csv→xlsx, svg→png가 외부 도구 없이 동작하고, manifest 파일 하나로 새 CLI 변환 도구를 코어 재컴파일 없이 추가 가능." + }, + { + "phase": "P8", + "title": "확장성·신뢰성·배포 굳히기", + "goal": "기능이 다 들어온 뒤 회귀 방지·UI 분해·배포를 다진다. 차별화는 끝났으니 여기서부터는 깨지지 않게 유지.", + "deliverables": [ + "UI MVVM 분해(MainWindow.xaml.cs 1171줄) + Provider 선언형 옵션 스키마 기반 동적 옵션 UI 생성", + "히스토리 도메인 로직을 Core로 분리 + 데모 시드 제거 + 스트리밍 로드/회전 정책", + "CI 강화: NuGet 캐시 + self-contained portable EXE 산출 + 외부 바이너리 번들링 파이프라인(FFmpeg LGPL 고지) + dotnet test 게이트", + "출력 형식 매트릭스 3중 중복(AllFormats/PopularOutputs/파일다이얼로그)을 단일 FormatCatalog로 통합", + "테스트 확대: OutputPathHelper 충돌·결합 로직·JSONL round-trip 순수 함수 커버" + ], + "keyChanges": [ + { + "area": "MainWindow.xaml.cs", + "change": "MVVM 분해, 동적 옵션 UI" + }, + { + "area": "BuildMsix.ps1", + "change": "외부 바이너리 번들 + 라이선스 고지 단계" + }, + { + "area": "build.yml/release.yml", + "change": "캐시+테스트 게이트+self-contained 산출물" + } + ], + "dependsOn": "P7", + "effort": "L", + "risk": "low", + "exitCriteria": "PR마다 테스트가 게이트로 동작하고, self-contained portable EXE가 자동 산출되며, 새 형식 추가가 단일 FormatCatalog 한 곳 수정으로 끝남." + } + ], + "conversionMatrix": { + "currentState": "8개 Provider가 PairsFromMatrix로 N×M 쌍을 선언하지만 ProviderRegistry는 (input,output) 단일 홉 딕셔너리(_byPair)만 매핑한다. 멀티홉(md→docx)은 DocumentProvider.RouteAsync(92-205)에 손코딩되어 형식 N개에 O(N²)로 수동 증식한다. 매트릭스는 '거의 모든 것→이미지/PDF/텍스트' 단방향으로만 풍부하고, 역방향(이미지/PDF→편집문서, HWP 출력, 미디어/아카이브)이 구조적으로 비어 있다. 동일포맷(pdf→pdf 압축)은 ConversionEngine.cs:88에서 무조건 Skip된다.", + "targetState": "ConversionGraph가 모든 Provider Capability를 순회해 방향 그래프를 빌드하고, Dijkstra가 임의의 A→Z를 원자 엣지 조합으로 자동 합성한다. OutputsForInput은 transitive closure로 확장되어 '이 파일로 만들 수 있는 모든 포맷'을 노출한다. PDF/HWP 양방향, 영상/오디오, 아카이브/데이터/벡터, AI 후처리가 모두 엣지로 편입되고, 동일포맷 압축(pdf→pdf)도 옵션으로 허용되는 엣지가 된다.", + "gaps": [ + "PDF 압축(pdf→pdf): 어떤 Provider도 수행 못 함 — PdfToolProvider 신설 필요", + "PDF→DOCX/HTML 역변환: 편집가능 역변환 경로 전무 — LibreOffice 경유 추가", + "HWP/HWPX 출력: H2Orestart import 전용이라 →HWP 불가, →DOCX/HTML/TXT도 미노출", + "영상/오디오 전 카테고리: mp4/mp3/flac 등 미디어 Provider 0개", + "아카이브/폰트/벡터/데이터/전자책: 빈 카테고리(ComingSoon enum 미사용)", + "AI 변환(요약/번역/캡션): 추상화·옵션·Provider 어디에도 자리 없음", + "동일포맷 최적화(이미지 리인코딩, PDF 압축): ConversionEngine.cs:88에서 Skip되어 표현 불가" + ], + "graphRoutingPlan": "1) 그래프 빌드(앱 시작 1회): ProviderRegistry 생성자 루프(16-30)에서 각 Provider의 Capability.SupportedConversions를 순회해 인접 리스트 Dictionary>를 구축한다. 노드=정규화된 확장자(.png/.pdf/.docx), 엣지=Edge{Provider, ConversionPair, Weight}. 노드 수십·엣지 수백 규모라 그래프는 매우 작다. 2) 가중치: 각 ConversionPair.LossClass(Lossless=0/Container=0.05/Recode=0.4/Rasterize=0.8)를 -log(품질보존율)로 환산하고 홉페널티(작은 상수)와 실행비용(외부 프로세스>in-process)을 가중합한다. 손실은 곱셈적이므로 -log 변환으로 덧셈 최단경로가 곧 최대 품질보존 경로가 된다. 3) 탐색: .NET 9 System.Collections.Generic.PriorityQueue로 Dijkstra(O(E log V), 80~120줄)를 자체 구현한다. ConversionEngine.ConvertOneAsync(91)에서 직접 엣지가 있으면 1홉(기존 동작 호환), 없으면 FindBestPath(inExt,outExt,options)로 멀티홉 경로를 구한다. AllowMultiHop(기본 true)/MaxHops(기본 3)/AvoidLossy 옵션으로 게이트. 4) 실행: ChainExecutor가 경로의 각 홉을 순차 실행하며 중간 산출물을 공용 workDir(Temp/e2e_{Guid})에 체이닝하고, 진행률을 홉 수로 분할해 IProgress에 매핑한다. 각 홉은 기존 provider.ConvertAsync를 그대로 호출(인터페이스 변경 불필요). 5) 안전장치: 멀티홉은 직접 엣지가 없을 때만 발동, 손실 블랙리스트(텍스트→래스터 같은 도메인 경계 전이 통제), 한 홉 실패 시 어느 홉에서 실패했는지 사용자에게 전달. 6) UI: OutputsForInput을 reachability(transitive closure)로 확장하고, 손실 경로로만 도달하는 출력에 '손실 변환' 경고 배지를 붙인다." + }, + "aiIntegration": { + "codexOAuth": "Codex CLI를 PATH에서 감지될 때만 활성화되는 구독자용 opt-in 보조 백엔드로 둔다. 핵심 제약: ChatGPT 구독 OAuth 토큰(auth.json의 access/refresh)은 Codex 백엔드 전용이라 api.openai.com에 직접 Bearer로 붙일 수 없다 — 구독 재사용은 오직 codex CLI 프로세스 호출로만 가능. 실행은 ExternalProcessRunner로 `codex exec --skip-git-repo-check --json --output-schema schema.json -o out.json --cd \"<프롬프트 + 파일경로>\"` 형태. --skip-git-repo-check는 변환 앱에 필수(git 저장소 아닌 폴더 허용), --output-schema로 응답을 JSON Schema로 강제해 메타데이터 추출, --json으로 JSONL 이벤트 스트림 파싱. CheckAvailabilityAsync에서 `codex --version` 프로브 + auth.json 존재 확인. auth.json refresh 토큰 race를 막기 위해 SemaphoreSlim(1) 직렬화 또는 --ephemeral 사용.", + "apiMode": "기본 경로는 API 키 + 공식 SDK다. Microsoft.Extensions.AI(IChatClient, MIT) 단일 추상화로 OpenAI(공식 OpenAI 패키지, MIT)와 Anthropic(공식 Anthropic 패키지, MIT)을 동일 인터페이스로 다룬다. 사용자는 설정에서 'OpenAI / Claude / Codex CLI / auto'를 고르고 API 키만 입력한다. 키는 ISettingsStore에서 System.Security.Cryptography.ProtectedData(DPAPI, CurrentUser)로 암호화해 %LOCALAPPDATA%에 저장하고, OPENAI_API_KEY/ANTHROPIC_API_KEY 환경변수도 폴백으로 읽어 CI/파워유저 친화. CheckAvailabilityAsync가 키 부재 시 NotReady(키 발급 URL을 ExternalDependency로 안내)를 반환해 그래프에서 자동 비활성.", + "useCases": [ + "요약: pdf/docx/txt/md → txt/md (긴 문서를 LLM이 요약)", + "번역: txt/docx/md → txt/docx (대상 언어는 옵션, 비파일 입력 LLM 왕복)", + "OCR 교정: OcrProvider 출력(.txt)을 받아 LLM이 오탈자/줄바꿈 정리 (그래프가 OCR→LLM 2단계 멀티홉으로 자동 합성)", + "이미지 캡션/대체텍스트: png/jpg → txt (비전 모델)", + "문서 언어 번역 + 포맷 정규화: csv→md(표), txt→md", + "메타데이터 생성: 임의 입력 → json (제목/태그/요약, Structured Outputs로 구조화)" + ], + "architecture": "LlmProvider를 별도 IAiProvider가 아닌 일반 IConverterProvider로 구현해 그래프의 부가 엣지로 편입한다(ADR-5). AI는 로컬 변환이 없는 신규 페어(요약/번역/캡션/메타데이터)에만 노출되며, 등록 순서로 '로컬 변환이 이미 있는 페어는 로컬 Provider가 우선, AI는 신규 페어만'을 보장한다(Priority 충돌 모델). 불변식: 키가 없어도 모든 기존 변환은 100% 동작하고 AI 페어만 비활성, AI는 절대 기본 경로를 점유하지 않으며 UI에 ✨AI 배지(종량과금·네트워크 명시)로만 opt-in 노출된다. 텍스트 추출이 필요하면 DocumentProvider/PdfProvider/OcrProvider를 주입받아 '추출→LLM' 2단계로 구성(OcrProvider가 PdfProvider를 주입받는 선례). 프라이버시: 로컬 문서가 외부 서버로 전송되므로 명시적 동의 토글 필수(기본 OFF), 미래에 Ollama 로컬 모델 경로를 IChatClient로 열어둔다." + }, + "mediaLayer": { + "video": "FfmpegProvider(FFMpegCore 5.4.0, MIT)로 mp4/mkv/webm/mov/avi/gif N×M 트랜스코딩. H.264/H.265는 HW 인코더(h264_nvenc/qsv/amf) 우선, LGPL 빌드엔 libx264/x265(GPL)가 없으므로 HW 미지원 시 AV1(libaom)/VP9(libvpx, 둘 다 BSD-like royalty-free)로 폴백. FFprobe로 duration 확보 후 NotifyOnProgress(Action,TimeSpan)을 IProgress에 직결, CancellableThrough(ct)로 취소.", + "audio": "오디오는 mp3/aac/m4a/opus/ogg/flac/wav N×M. AAC는 FFmpeg 네이티브 aac 인코더(LGPL, libfdk-aac=nonfree 회피), Opus/FLAC/MP3는 LGPL 빌드로 직접 처리. 오디오 전용 출력(flac/mp3)은 영상 입력에서 오디오 트랙만 추출.", + "pdfCompression": "PdfToolProvider 3단계: Light=PDFsharp(MIT, in-process) 또는 qpdf(Apache 2.0) 구조 최적화(object stream 압축·linearize), Strong=PDFium 렌더+ImageMagick 재인코딩(텍스트 선택성 잃지만 라이선스 안전), Max=Ghostscript(-dPDFSETTINGS /screen)는 AGPL이라 번들 금지·사용자 설치본 감지만. 병합/분할/암호화는 PDFsharp 또는 qpdf.", + "imageOptim": "기존 MagickProvider의 ApplyEncoding(jpg/png/webp/avif/tiff 품질·알파평탄화·MaxLongEdge)을 공용 ImageEncoder 헬퍼로 추출해 PdfProvider/HtmlProvider/CombineAsync의 4중 복제를 제거. 동일포맷 이미지 리인코딩(품질 조절)도 엣지로 허용.", + "approach": "단일 포터블 EXE 부담을 줄이기 위해 무거운 바이너리(FFmpeg ~100MB)는 절대 번들하지 않고 'RequiresExternal + 최초 사용 시 자동 다운로드' 모델. 라이선스 게이트(코드 리뷰 강제): FFmpeg는 BtbN lgpl-shared 빌드(--enable-gpl/nonfree 없음)를 별도 프로세스로 호출(동적 분리)해 LGPL 준수 — gyan.dev/BtbN gpl 빌드(GPLv3) 번들 절대 금지. ExternalToolDetector.TryFindFfmpeg가 (a)%LOCALAPPDATA%\\Everything2Everything\\ffmpeg, (b)시스템 PATH 순 탐지, 없으면 lgpl-shared zip을 SHA256 검증 후 다운로드. GlobalFFOptions.Configure로 경로 고정. NVENC는 LGPL 빌드에서 --enable-nonfree 없이 합법 사용 가능(NVIDIA 공식 확인). About 화면에 'uses FFmpeg under LGPLv2.1' 고지 + 소스 다운로드 링크(LGPL 의무). MSIX 변형에서는 샌드박스 정책상 lgpl-shared DLL을 패키지 동봉(여전히 LGPL 준수). Ghostscript/MuPDF(AGPL)는 사용자 설치본 감지만, codec 특허(H.264/AAC) 위험을 줄이려 AV1/VP9/Opus/FLAC(royalty-free)를 기본 권장 출력으로." + }, + "riskRegister": [ + { + "risk": "'보이지 않는 리팩터링의 함정' — 그래프 코어 재설계가 사용자 체감 변화 0인 상태로 길어짐", + "likelihood": "medium", + "impact": "high", + "mitigation": "P1에서 그래프 도입과 PDF 압축(즉시 체감 신기능)을 묶고, transitive closure로 늘어나는 '만들 수 있는 포맷 목록'을 가시 성과로 노출. DocumentProvider.RouteAsync 삭제를 회귀 테스트로 동일 동작 증명." + }, + { + "risk": "'최단 경로' 압박으로 LlmProvider/FfmpegProvider를 또 하드코딩 switch로 끼워넣어 RouteAsync 지옥 재생산", + "likelihood": "medium", + "impact": "high", + "mitigation": "P1에 그래프 코어를 먼저 심어 하드코딩을 구조적으로 차단. '신규 기능은 그래프 엣지로만 추가'를 불변식으로 명문화하고 코드 리뷰 게이트로 강제." + }, + { + "risk": "GPL/AGPL 바이너리(FFmpeg gpl빌드·Ghostscript·H2Orestart) 정적 링크로 상업 배포 라이선스 오염", + "likelihood": "medium", + "impact": "high", + "mitigation": "모든 무거운 외부 도구를 별도 프로세스 분리 호출 + 사용자 설치 감지/LGPL 빌드 자동조달로만 통합. 라이선스 경계를 코드 리뷰 게이트로 강제(ADR-6)." + }, + { + "risk": "멀티홉 손실 누적·은폐 — HWP→PDF(래스터화)→DOCX가 '편집가능'을 약속하나 이미지 덩어리 반환", + "likelihood": "medium", + "impact": "medium", + "mitigation": "LossClass 가중치로 래스터화에 큰 페널티, 멀티홉은 직접 엣지 없을 때만, MaxHops=3, 손실 블랙리스트, 손실 경로 UI 경고 배지 3겹 가드레일." + }, + { + "risk": "인터페이스 일반화(ConvertRequest)가 8개 기존 Provider를 한 번에 깸", + "likelihood": "medium", + "impact": "high", + "mitigation": "기존 시그니처를 어댑터로 감싸 점진 마이그레이션, 무중단을 회귀 테스트로 보장. P3에 배치해 미디어/AI 동기가 코드에 들어온 뒤 일반화." + }, + { + "risk": "AI 비결정성·종량과금·네트워크 의존이 '로컬 예측가능 변환' 신뢰를 깸", + "likelihood": "high", + "impact": "medium", + "mitigation": "AI는 기본 경로 불점유, ✨AI 배지 opt-in, 키 없으면 조용히 비활성을 설계 불변식으로 박음. 토큰/비용 표시, 사용자 확인 게이트, 재시도·백오프." + }, + { + "risk": "테스트 0개 상태에서 대규모 코어 변경이 회귀를 탐지 못 함", + "likelihood": "high", + "impact": "high", + "mitigation": "P1에서 xUnit 테스트 프로젝트를 최우선 신설하고 그래프 경로탐색·DocumentProvider 회귀를 첫 안전망으로. CI에 dotnet test 게이트 추가." + }, + { + "risk": "manifest가 또 다른 갓 오브젝트화 — FFmpeg HW가속 폴백 같은 복잡 로직을 manifest로 표현 시도", + "likelihood": "low", + "impact": "medium", + "mitigation": "manifest는 '90% 단순 CLI(qpdf/gs)만, 복잡 로직은 in-box 코드 Provider' 경계를 P1부터 불변식으로 명문화." + } + ], + "successMetrics": [ + { + "metric": "멀티홉 경로 자동 합성", + "current": "DocumentProvider.RouteAsync에 손코딩된 3-4개 체인만 동작", + "target": "엔진이 임의 A→Z를 그래프 탐색으로 자동 합성, RouteAsync 0줄" + }, + { + "metric": "입력당 도달 가능 출력 포맷 수", + "current": "1-hop 직접 출력만(OutputsForInput 직접 매핑)", + "target": "transitive closure로 확장된 도달 가능 전체 포맷 + 손실 배지" + }, + { + "metric": "지원 카테고리 수", + "current": "이미지/PDF/문서/HEIC/OCR (약 5)", + "target": "+영상/오디오/아카이브/데이터/벡터/전자책/AI (약 12)" + }, + { + "metric": "PDF 압축 기능", + "current": "어떤 Provider도 수행 불가", + "target": "3단계 레벨(Light/Strong/Max) 압축 + 병합/분할" + }, + { + "metric": "HWP 출력 매트릭스", + "current": "→PDF/이미지만, →DOCX/HTML/TXT 미노출", + "target": "HWP→DOCX/HTML/TXT/PDF 완성" + }, + { + "metric": "코어 테스트 커버리지", + "current": "테스트 프로젝트 0개", + "target": "그래프 탐색·OutputPathHelper·결합·JSONL round-trip 커버 + CI 게이트" + }, + { + "metric": "배치 처리 동시성", + "current": "순차 for-loop(코어 1개만 사용)", + "target": "Parallel.ForEachAsync(MaxDegreeOfParallelism)로 멀티코어 활용" + }, + { + "metric": "CLI 자동화 가능성", + "current": "WPF 창만 띄우고 stdout 무반환", + "target": "--json/--codec/--prompt 플래그 + stdout JSON + exit code" + } + ], + "ssotNotes": "다음 세션은 P1(그래프 엔진 도입 + PDF 압축)부터 시작한다. 시작 순서와 검증 포인트:\\n\\n1) 가장 먼저 xUnit 테스트 프로젝트를 신설하라(현재 0개). 이게 모든 코어 변경의 안전망이며, 특히 DocumentProvider.RouteAsync 삭제가 '손그림과 동일 동작'임을 증명할 회귀 테스트의 전제다. 먼저 현재 RouteAsync의 모든 경로(md→docx, docx→md, hwp→html 등)에 대한 골든 테스트를 작성해 baseline을 고정하라.\\n\\n2) ConversionGraph + 자체 Dijkstra를 ProviderRegistry 옆에 얇게 얹어라. ProviderRegistry.cs:16-30 생성자 루프에 그래프 빌드 한 단계만 추가. _byPair는 유지(직접 엣지 1홉 호환). ConversionPair에 LossClass 필드 추가가 선결.\\n\\n3) 첫 검증: ConversionEngine.ConvertOneAsync(91)를 그래프 위임으로 바꾼 뒤, 기존 모든 변환이 동일 동작하는지 회귀 테스트로 확인. 그 다음에야 RouteAsync를 삭제하고 원자 엣지만 선언하게 바꿔 md→docx가 그래프 합성으로 동일하게 나오는지 검증.\\n\\n4) PDF 압축(PdfToolProvider)은 ConversionEngine.cs:88의 동일포맷 Skip을 우회해야 한다 — pdf→pdf를 엣지로 허용하는 메커니즘이 그래프 도입과 함께 필요. PDFsharp(MIT) in-process 압축부터 시작하면 외부 의존성 0으로 즉시 체감 가치.\\n\\n먼저 검증할 불변식: (a) 기존 8개 Provider 변환이 그래프 경로로 100% 동일 동작, (b) 멀티홉은 직접 엣지 없을 때만 발동, (c) 손실 경로에 가중치가 정확히 반영되는지. 라이선스 게이트(GPL/AGPL 분리 호출)는 P4(미디어)부터 본격 적용되지만, P1의 Ghostscript 폴백에서도 '사용자 설치본 감지만, 번들 금지' 원칙을 처음부터 지켜라.\\n\\n참고: 빌드 후에는 메모리의 project_build_pipeline(publish + 카스케이드 재등록 PowerShell 시퀀스)를 따르고, 사용자가 직접 push & GUI 검증하는 워크플로이므로 큰 결정은 빠른 승인 후 단일 commit으로 진행." +} \ No newline at end of file diff --git a/docs/ssot/_data/meta.json b/docs/ssot/_data/meta.json new file mode 100644 index 0000000..1bb5c27 --- /dev/null +++ b/docs/ssot/_data/meta.json @@ -0,0 +1,15 @@ +{ + "project": "Everything2Everything", + "title": "Everything2Everything — 변환 그래프 OS 마스터플랜", + "subtitle": "전체 소스 심층 분석 · 방법론 인터넷 리서치 · 아키텍처 종합 (Single Source of Truth)", + "generatedDate": "2026-06-01", + "method": "Workflow 멀티에이전트 오케스트레이션 (5 코드분석 + 7 인터넷리서치 → 3 독립 아키텍트 → 심사 랭킹 → 마스터플랜 종합)", + "usage": { + "agents": 17, + "subagentTokens": 1426291, + "toolUses": 367, + "durationMs": 907053 + }, + "repo": "https://github.com/yunchan8804-blip/Everything2Everthing.git", + "winningSynthesis": "idx2(AI·미디어 우선, 89점) 실행 골격 + idx0(그래프 코어, 84점) 아키텍처 영혼" +} diff --git a/docs/ssot/_data/ranking.json b/docs/ssot/_data/ranking.json new file mode 100644 index 0000000..4c6750e --- /dev/null +++ b/docs/ssot/_data/ranking.json @@ -0,0 +1,40 @@ +{ + "rankings": [ + { + "angle": "변환 그래프 코어 우선 (idx 0) — 모든 변환은 엣지, 엔진은 라우터. Dijkstra 멀티홉 자동 합성이 북극성.", + "score": 84, + "strengths": "세 안 중 가장 정확하게 '진짜 아키텍처 부채'를 짚었다. DocumentProvider.RouteAsync(92-205)의 손그림 Dijkstra와 OutputsForInput(52-58)의 1-hop 한계를 정확히 인용했고, 이 손그림을 '삭제하고 엔진이 계산하게 한다'는 도그푸딩 검증(회귀 테스트로 동일 동작 증명)은 기술적으로 가장 견고하다. transitive closure로 OutputsForInput이 폭발한다는 통찰은 N×M·다방향의 본질을 정확히 포착한 것이고, 손실을 -log(보존율)+홉페널티 가중치로 SSOT화한 설계는 정교하다. 외부 의존성 없는 80~120줄 자체 Dijkstra는 .NET 9 단일 EXE/AOT 제약과 완벽히 양립한다. AI를 IAiProvider 특수 인터페이스가 아니라 '로컬 변환이 없는 신규 엣지'로 환원한 것은 세 안 중 가장 우아하다. biggestRisk('보이지 않는 리팩터링의 함정')를 스스로 정직하게 진단하고 완화책(Phase 0를 가시적 성과와 묶기)까지 제시한 자기인식이 뛰어나다.", + "weaknesses": "치명적 약점은 단계적 가치 전달이다. 사용자가 명시한 신규 가치(AI, 미디어, PDF 압축, HWP)가 로드맵 3~5단계로 밀린다. 그래프 코어가 완성돼도 사용자는 똑같은 md→docx만 본다 — 본인도 인정한 '데모할 게 없는' 상태다. transitive closure 확장을 가시 성과로 내세우지만, md→docx 같은 경로는 이미 RouteAsync에 손으로 박혀 동작 중이므로 '새로 생기는 도달 가능 포맷'이 실제로 사용자에게 체감될 만큼 많지 않다(현 Provider 구성상 신규 합성 경로가 제한적). 또한 인터페이스 일반화(Phase 1)를 그래프 코어 직후에 두어 8개 Provider를 조기에 건드리는데, 이는 AI/미디어라는 '왜 일반화가 필요한가'의 동기가 코드에 들어오기 전이라 추상화가 추측에 기반할 위험이 있다.", + "weaknessesNote": "" + }, + { + "angle": "플러그인 생태계 우선 (idx 1) — 코어를 얇은 호스트로 축소, 변환 능력을 선언적 manifest JSON으로 외부화.", + "score": 71, + "strengths": "'manifest를 늘려 포맷을 늘린다'는 비전은 장기 확장성의 천장이 가장 높다. LibreOffice 호출 복제(실측: DocumentProvider/DocxProvider/HwpxProvider 3곳, 거의 동일한 ConvertWithLibreOfficeAsync)를 ExternalToolProvider 추상 베이스로 통합하고 그것을 manifest 어댑터의 실행 엔진으로 재사용하는 설계는 영리하다. Abstractions를 별도 어셈블리로 분리해 타입 동일성을 보장한다는 점, 충돌 모델을 Priority 기반 다중 Provider 공존으로 교체하는 점은 견고하다. biggestRisk에서 '표현력 천장'(FFmpeg HW가속 폴백은 manifest로 불가)을 스스로 진단하고 하이브리드(90% 단순 도구만 manifest, 복잡 로직은 in-box) 경계를 P1 원칙으로 못박은 것은 성숙한 자기방어다.", + "weaknesses": "프로젝트 현 단계에 대한 과잉 엔지니어링이 가장 크다. Provider가 8개뿐이고 테스트 0개인 단일 개발자 프로젝트에서 manifest DSL·동적 로더·별도 어셈블리 분리는 ROI가 가장 낮다. 본인이 인정하듯 FFmpeg/AI 같은 '진짜로 추가하고 싶은' 무거운 도구는 결국 in-box 코드로 남아 manifest 밖에 있으므로, manifest가 실제로 커버하는 건 이미 LibreOffice 하나로 다 되는 '단순 CLI 90%'뿐이다 — 즉 가장 매력적인 신규 가치(AI/미디어)는 manifest 혜택을 거의 못 받는다. 단계적 가치 전달이 idx 0보다도 늦다: Phase 1 전체가 '기반 해체'로, 사용자가 보이는 변화 0인 구간이 가장 길다. '코드 없는 확장'이라는 핵심 약속이 escape hatch로 부분적으로 깨진다고 본인이 인정한 시점에서, 이 안의 차별화 명분이 상당 부분 증발한다.", + "weaknessesNote": "" + }, + { + "angle": "AI·미디어 기능 우선 (idx 2) — 사용자 명시 신규 가치를 최단 경로로 출시, 그래프는 '기능을 켜는 최소 인프라'로만 취급.", + "score": 89, + "strengths": "단계적 가치 전달과 사용자 요구 충족도에서 압도적이다. PDF 압축 + HWP 한글 변환을 Phase 1(가장 빠른 체감)로 배치한 판단은 정확하다 — HWP는 이미 LibreOffice+H2Orestart 배관이 깔려 있어(HwpxProvider 실측) DOCX/HTML/TXT 출력 매트릭스 확장만으로 즉시 신규 가치가 나온다. '인프라가 아니라 사용자 체감 차별화가 북극성'이라는 프레이밍은 단일 개발자·GUI 검증 워크플로(메모리상 사용자 패턴)와 가장 잘 맞는다. AI를 '핵심 엔진이 아니라 후처리 부가가치 레이어'로, 키 없으면 모든 변환 100% 동작 + ✨AI 배지로만 노출하는 불변식은 idx 0/1의 'AI=엣지'보다 사용자 신뢰 측면에서 한 수 위다. 라이선스 분석(FFmpeg GPL 정적링크 금지/LGPL 분리호출, Ghostscript/MuPDF AGPL 감지만, H2Orestart GPL 외부프로세스)이 세 안 중 가장 구체적이고 실행가능하며 .NET 9 단일 EXE 번들 제약을 정면으로 다룬다. 배치 병렬화(순차 for-loop 57-70 실측 → Parallel.ForEachAsync)는 AI 왕복·트랜스코딩 병목을 정확히 짚었다. 결정적으로, idx 0의 핵심 자산(멀티홉 그래프, 자체 Dijkstra, ExternalProcessRunner 통합, ConvertRequest 일반화, 손실 배지)을 전부 흡수하되 Phase 0와 Phase 4에 적절히 배치해 '기능으로 그래프를 정당화'한다.", + "weaknesses": "biggestRisk가 정확히 이 안의 아킬레스건이다 — '최단 경로' 압박이 LlmProvider/FfmpegProvider를 또 하드코딩 switch로 끼워넣어 카테고리마다 RouteAsync 지옥을 재생산할 유혹. 본인이 이를 명시하고 'Phase 0 인터페이스 일반화를 불변식으로 박는다'고 방어하지만, 로드맵상 그래프 자동 합성이 Phase 4(맨 끝)라서 Phase 1~3 동안 멀티홉 없이 기능이 쌓이면 나중에 그래프를 얹을 때 이미 작성된 기능 Provider들이 그래프 친화적이지 않게 굳어질 구조적 위험이 idx 0보다 크다. 또한 신규 1급 Provider 4종을 동시에 여는 야심은 단일 개발자 기준 Phase별 범위가 다소 낙관적이다(주 단위 추정이 공격적). 아키텍처 순수성 면에서는 idx 0에 명백히 뒤진다.", + "weaknessesNote": "" + } + ], + "bestIdeasToGraft": [ + "[idx 0의 핵심] DocumentProvider.RouteAsync(92-205) 손그림 멀티홉을 '삭제'하고 엔진이 Dijkstra로 동일 경로를 계산하게 만드는 도그푸딩 — 이것을 회귀 테스트로 '그래프=손그림 동일 동작' 객관 증명. 테스트 0개인 현 상태에서 이 변환의 첫 안전망이 된다. 어떤 마스터플랜이 채택되든 이 검증 루프는 필수.", + "[idx 0의 핵심] 손실을 -log(보존율)+홉페널티 단일 가중치로 ConversionPair.LossClass(Lossless=0/Container=0.05/Recode=0.4/Rasterize=0.8) 필드에 SSOT화. 이것이 멀티홉 경로 선택과 UI '⚠손실' 배지의 단일 출처. idx 2의 '손실 변환 경고 배지'도 이 가중치를 그대로 소비.", + "[idx 0의 핵심] AI를 IAiProvider 특수 인터페이스가 아니라 '로컬 변환이 없는 신규 엣지(요약/번역/캡션)'로 그래프에 환원 — idx 2의 '후처리 부가가치 레이어'와 결합하면, AI는 그래프상 엣지이면서 동시에 키 없으면 자동 비활성 노드 + ✨AI 배지로 노출되는 이중 안전장치를 얻는다.", + "[idx 2의 핵심] AI 불변식: 키가 없어도 모든 기존 변환 100% 동작, AI는 절대 기본 경로를 점유하지 않고 ✨AI 배지 페어로만 opt-in, 키 부재 시 등록 순서·게이트로 조용히 비활성. '변환은 로컬에서 예측가능' 신뢰를 깨지 않는 설계 불변식.", + "[idx 2의 핵심] 라이선스 경계를 코드 리뷰 게이트로 강제: FFmpeg는 GPL 정적링크 금지·LGPL 분리호출만, Ghostscript/MuPDF는 AGPL이라 사용자 설치본 감지만, H2Orestart/Calibre는 GPL이라 외부 프로세스 분리. 모든 무거운 외부 도구 = '별도 프로세스 분리 호출 + 사용자 설치 감지 또는 LGPL 빌드 자동조달'. .NET 9 단일 EXE 상업 배포 오염 방지의 핵심.", + "[idx 2의 핵심] 단계 순서: PDF 압축 + HWP 출력 매트릭스 확장을 최우선 출시(이미 HwpxProvider의 LibreOffice+H2Orestart 배관 존재 → DOCX/HTML/TXT 출력만 추가하면 즉시 신규 가치). 초기 체감 가치를 그래프 리팩터링보다 먼저.", + "[idx 2의 핵심] 배치 병렬화: ConversionEngine 순차 for-loop(57-70)를 Parallel.ForEachAsync(동시성 제한 포함)로 교체. AI 네트워크 왕복·영상 트랜스코딩의 치명적 병목 해소. 더불어 ImageMagick ResourceLimits 전역 설정 + decompression bomb 방어로 미디어 공격면 차단.", + "[idx 1의 핵심] 레지스트리 충돌 모델 교체: _byPair.TryAdd(22)의 조용한 first-wins를 ProviderCapability.Priority + 다중 Provider 공존으로 교체 + 충돌 시 진단 경고. 같은 (input,output)에 빠른변환/고품질/AI 등 복수 전략 등록 가능. idx 0/2 모두 이 교체가 전제 조건.", + "[idx 1의 부분 채택] manifest는 '풀 생태계 비전'이 아니라 'ExternalProcessRunner 위의 선언적 인자 템플릿({input}/{output}/{outdir}/{format})'으로만 제한 채택 — qpdf/Ghostscript 같은 단순 CLI 압축 도구를 코드 없이 추가하는 용도. 단, 복잡 로직(FFmpeg HW가속 폴백/AI)은 in-box 코드 Provider 원칙을 P1부터 못박아 manifest 갓오브젝트화 방지.", + "[3안 공통] ExternalProcessRunner 단일 추상화로 LibreOffice 3중 복제(DocumentProvider:238-281 / DocxProvider:113-157 / HwpxProvider:107-151) 통합 — 타임아웃·stderr 수집·Kill 일원화. 이후 모든 외부 엣지(FFmpeg/Ghostscript/qpdf/Codex CLI)가 이 러너 하나 공유. 세 안이 만장일치로 지목한 가장 안전하고 즉시 실행가능한 첫 리팩터링.", + "[3안 공통] IConverterProvider 시그니처(9-15)를 ConvertRequest/ConvertContext로 일반화 + ConvertResult(10)에 비파일 산출물 필드(추출 텍스트·AI 응답·미디어 메타데이터) 추가. 단, 기존 8개 Provider는 어댑터로 감싸 점진 마이그레이션 + 회귀 테스트로 무중단 보장(idx 0의 마이그레이션 전략 채택).", + "[3안 공통] ConvertOptions 갓 오브젝트(14+ sub-record)를 그래프 옵션(AllowMultiHop/MaxHops/AvoidLossy) + 형식별 옵션 백(IReadOnlyDictionary 또는 Provider 선언형 스키마)으로 분해 → Video/Audio/Ai/PdfCompress를 sub-record 증식 없이 수용. DPAPI(ProtectedData) 기반 ISettingsStore 신설로 API 키·도구 경로 안전 저장." + ], + "recommendation": "승자는 idx 2(AI·미디어 기능 우선, 89점)를 '실행 골격'으로, idx 0(그래프 코어, 84점)을 '아키텍처 영혼'으로 삼아 종합한다. 단독 채택이 아니라 두 안의 합성이 정답이다.\\n\\n핵심 통찰: 세 안의 기술적 부품(자체 Dijkstra 멀티홉, ExternalProcessRunner 통합, ConvertRequest/ConvertResult 일반화, LossClass 가중치, Priority 충돌 모델, AI=엣지, DPAPI 키저장)은 사실상 동일하다. 진짜 차이는 '무엇을 북극성으로 삼아 순서를 짜느냐' 하나뿐이다. 이 프로젝트는 단일 개발자가 직접 push하고 GUI로 검증하며 큰 결정을 빠르게 승인하는 워크플로(프로젝트 메모리)이고, 사용자가 명시적으로 요구한 것은 AI/미디어/HWP/PDF압축이라는 '기능'이다. 따라서 '보이지 않는 리팩터링의 함정'(idx 0 본인이 인정한 최대 리스크)에 빠지는 그래프-우선 순서는 이 맥락에서 부적합하다.\\n\\n그러나 idx 2의 최대 리스크('최단 경로 압박으로 LlmProvider/FfmpegProvider를 또 하드코딩 switch로 끼워넣어 RouteAsync 지옥 재생산')는 실재하고, idx 0의 그래프 코어가 바로 이 리스크의 백신이다. 그래서 둘을 봉합하는 마스터플랜은 다음 순서다:\\n\\nPhase 0 (기반, 그러나 즉시 가치와 묶기 — idx 0의 자기 완화책 채택): ExternalProcessRunner 통합(3중 복제 제거) + Priority 충돌 모델 + ConvertRequest/ConvertResult 일반화(어댑터로 무중단). 동시에 DocumentProvider.RouteAsync를 원자 엣지로 분해하고 자체 Dijkstra를 넣어 '손그림=그래프 동일 동작'을 회귀 테스트로 증명(테스트 0개 탈출의 첫걸음). 이 단계의 가시 성과는 transitive closure로 늘어나는 '만들 수 있는 포맷 목록'.\\n\\nPhase 1 (즉시 체감 — idx 2 순서): HWP 출력 매트릭스 확장(이미 깔린 LibreOffice+H2Orestart 배관 재사용 → DOCX/HTML/TXT) + PDF 압축. 이때 신규 기능은 반드시 '그래프 엣지'로만 추가한다는 것을 불변식으로 박아 idx 2의 하드코딩 유혹을 Phase 0 그래프가 구조적으로 차단.\\n\\nPhase 2~3 (미디어 + AI 부가가치 레이어 — idx 2): FFmpeg/Ghostscript/qpdf를 idx 2의 라이선스 게이트(분리 프로세스·LGPL/AGPL 경계) 하에 엣지로 추가. AI는 idx 0의 '엣지' + idx 2의 '✨AI 배지·키 없으면 비활성·기본 경로 불점유' 이중 불변식으로 통합. 배치 병렬화는 이 시점에 필수.\\n\\nidx 1(플러그인 생태계, 71점)은 베이스로는 과잉 엔지니어링이라 탈락하지만, 두 아이디어는 흡수한다: (1) Priority 기반 충돌 모델(이미 Phase 0에 편입), (2) manifest를 '풀 DSL'이 아니라 'ExternalProcessRunner 위 선언적 인자 템플릿'으로 축소해 qpdf/gs 같은 단순 CLI를 코드 없이 추가하는 좁은 용도로만 채택. 복잡 로직(FFmpeg HW가속·AI)은 in-box 코드 원칙을 P1부터 못박아 manifest 갓오브젝트화를 방지한다(idx 1 본인의 하이브리드 경계 그대로).\\n\\n한 줄 요약: idx 2의 '기능이 견인하는 로드맵'에 idx 0의 '그래프가 받치는 코어'를 Phase 0에 심어, 사용자 체감 가치를 빠르게 내면서도 RouteAsync 지옥의 재발을 그래프로 원천 차단한다." +} \ No newline at end of file diff --git a/docs/ssot/_data/researches.json b/docs/ssot/_data/researches.json new file mode 100644 index 0000000..4826f36 --- /dev/null +++ b/docs/ssot/_data/researches.json @@ -0,0 +1,698 @@ +[ + { + "key": "conversion-graph", + "topic": "그래프 기반 멀티홉 변환 경로 탐색 아키텍처 (Everything2Everything 적용)", + "keyFindings": [ + "**Pandoc = 단일 AST 허브-앤-스포크**: 모든 포맷을 하나의 중립 AST(Pandoc AST)로 파싱(reader)하고 거기서 각 포맷으로 직렬화(writer)한다. M개 reader + N개 writer만 구현하면 M×N 변환을 자동 커버하고, 새 포맷은 reader/writer 1개 추가로 끝. 단 이 모델은 '한 도메인 안에서 의미가 보존되는 공통 표현'이 존재할 때(텍스트/마크업)만 성립한다. 이미지/오디오/문서를 하나의 AST로 묶는 건 불가능 — Everything2Everything처럼 도메인이 이질적이면 단일 허브가 아니라 '여러 허브를 가진 변환 그래프'가 정답이다.", + "**NCSA Polyglot / Conversion Software Registry(CSR)가 이 프로젝트의 정확한 청사진**: 노드=파일 포맷(확장자), 엣지=특정 소프트웨어를 통한 (입력→출력) 변환, 가중치=변환 시 '정보 보존량(information retained)'. 입력→출력 최단 경로를 탐색해 멀티홉 체인을 자동 생성한다. 손실 정량화는 별도 프레임워크 Versus(file-to-file 비교)로 측정해 엣지 가중치로 환산 → '정보 손실이 가장 적은 경로'를 고른다. 즉 노드=포맷 / 엣지=Provider / 가중치=손실 모델은 학계에서 이미 검증된 best practice다.", + "**가중치 모델링 best practice = 비용을 곱셈이 아니라 덧셈으로 만들기**: Dijkstra/A*는 경로비용이 엣지비용의 '합'일 때 동작한다. 손실은 본래 곱셈적(0.9 × 0.8...)이므로 `weight = -log(품질보존율)` 형태로 변환하면 합산 최단경로가 곧 '최대 품질보존 경로'가 된다. 여기에 lossy 엣지에 큰 페널티, lossless(컨테이너 재포장·무손실 코덱)에 0에 가까운 비용, 외부 도구 실행/렌더링 속도 비용을 가중합으로 섞는다. 홉 수 자체에도 작은 상수 페널티를 줘 '불필요하게 긴 체인'을 억제한다.", + "**손실 경로 회피 핵심 규칙들**: (1) 같은 lossy 인코딩을 두 번 거치지 않게 한다(JPEG→PNG→JPEG 같은 generation loss는 누적·비가역). (2) lossy→lossless 변환은 데이터를 복원하지 못하므로(이미 버려진 정보) 가중치에 반영. (3) 래스터화는 '단방향 손실 절벽' — 벡터/텍스트(PDF·SVG·DOCX)를 PNG로 한 번 떨구면 텍스트·벡터 정보가 영구 소실되므로, 래스터를 중간 허브로 쓰는 경로는 '꼭 필요할 때만(예: OCR, 썸네일)' 허용하고 가중치를 매우 높게 준다.", + "**중간 포맷(허브) 선택 전략**: 문서 도메인은 HTML/Markdown 또는 OOXML(DOCX)을 허브로(현재 DocumentProvider가 이미 HTML을 사실상 허브로 사용 중). 인쇄·레이아웃 보존이 중요하면 PDF가 허브(이미 PDFium 보유). 이미지 도메인은 무손실 중간 포맷(PNG/TIFF, 또는 ImageMagick 내부의 MIFF)을 허브로 써 generation loss를 막는다. 즉 '하나의 글로벌 허브'가 아니라 '도메인별 허브 + 도메인 간 경계는 의도적 손실 게이트(PDF, PNG)'로 설계하는 게 핵심.", + "**ImageMagick delegate = 이미 멀티홉 엔진**: decode/encode만 지정된 delegate를 자동으로 이어붙여 중간 포맷 체인을 만든다(예: BPG→PNG(중간)→내부표현→출력). %i(입력)/%o(출력)/%u(고유 임시파일) 토큰으로 중간 임시파일을 관리. 같은 변환에 delegate 여러 개면 선언 순서대로 시도하다 성공하는 것을 채택(우선순위=순서 + 가용성 fallback). Everything2Everything의 MagickProvider는 이 체인을 라이브러리 내부에서 이미 활용 중이므로, 이미지 노드 사이는 사실상 단일 '슈퍼노드'로 묶어도 된다.", + "**FFmpeg filtergraph = DAG 기반, format negotiation**: 노드=필터, pad=타입 있는 입출력 포트, 엣지=프레임 흐름. source/sink 개념, 사이클·다중 링크 허용. 핵심 시사점은 '인접 노드 간 포맷 협상(format negotiation)' — 변환 그래프에서도 각 Provider가 받을 수 있는/내보낼 수 있는 포맷 집합을 선언하고 엔진이 그 교집합으로 연결을 결정하는 구조가 견고하다.", + "**현재 코드 상태**: ProviderRegistry는 `(Input,Output)→Provider` 단일 홉 딕셔너리(`_byPair`)만 갖고, 멀티홉 경로 탐색이 전혀 없다. 멀티홉은 DocumentProvider.RouteAsync 안에 `md→html→docx`, `docx→html→md` 식으로 하드코딩되어 Provider 내부에 묻혀 있다. 이 하드코딩 분기들이 바로 '그래프로 끌어올려야 할' 멀티홉 로직이다." + ], + "recommendedApproach": "단일 글로벌 AST 허브(Pandoc식)는 도메인이 이질적인 이 프로젝트에 부적합하다. 대신 **NCSA Polyglot 모델(노드=포맷, 엣지=Provider, 가중치=손실)을 ProviderRegistry 위에 얇은 그래프 레이어로 얹는 것**을 권장한다.\n\n**1) 그래프 빌드 (앱 시작 시 1회)**: 모든 Provider의 Capability.SupportedConversions를 순회해 방향 그래프를 만든다. 노드=정규화된 확장자, 엣지=해당 Provider+ConversionPair. 노드 수가 수십 개, 엣지 수가 수백 개 수준이므로 그래프는 매우 작다.\n\n**2) 경로 탐색**: Dijkstra(또는 A*) 1회로 충분. 비용은 엣지 가중합 = `α·(-log 품질보존율) + β·홉상수 + γ·실행비용`. lossy 게이트(래스터화, lossy 재인코딩)에 큰 가중치를 줘 손실 경로를 자연스럽게 회피한다. 직접 엣지(단일 홉)는 항상 비용이 낮아 기존 동작과 호환된다.\n\n**3) 라이브러리 선택**: 그래프가 작고 알고리즘이 표준적이므로 **외부 의존성 없이 자체 Dijkstra 약 80~120줄로 구현하는 것을 1순위로 권장**한다. 단일 포터블 EXE/MSIX 배포에 유리하고(트리밍·AOT 충돌 없음), MS-PL 같은 라이선스 검토도 불필요하다. 직접 구현이 부담되면 QuikGraph(MS-PL, net5~net10 호환)를 쓰되 2022년 이후 릴리스가 없는 점을 감안한다.\n\n**4) 손실 정량화(선택적 고도화)**: 초기에는 포맷 쌍별 정적 가중치 테이블(lossless=0.0, 컨테이너변환=0.05, lossy재인코딩=0.4, 래스터화=0.8 등)로 시작하고, 추후 Versus처럼 실제 결과물을 비교해 가중치를 보정하는 단계로 확장한다. 처음부터 동적 측정을 넣을 필요는 없다.\n\n**5) 안전장치**: 멀티홉은 '직접 엣지가 없을 때만' 발동하게 하고, 최대 홉 수(예: 3)와 '명시적으로 금지된 손실 전이' 블랙리스트를 둔다. 중간 산출물은 임시 폴더에 만들고 마지막에 정리(DocumentProvider의 workDir 패턴 그대로 재사용).", + "libraries": [ + { + "name": "자체 Dijkstra 구현 (직접 작성)", + "purpose": "ProviderRegistry 위에 포맷 그래프 + 가중치 최단경로 탐색을 직접 구현 (PriorityQueue는 .NET 9 BCL에 내장)", + "license": "N/A (프로젝트 코드)", + "maturity": "production (표준 알고리즘, 그래프 규모가 작아 검증 부담 낮음)", + "notes": "1순위 권장. 외부 의존성 0 → 단일 포터블 EXE/MSIX, 트리밍/AOT, 라이선스 검토 모두 무부담. 노드 수십·엣지 수백 규모라 성능 이슈 없음. .NET 9의 System.Collections.Generic.PriorityQueue로 O(E log V) Dijkstra를 간단히 작성 가능." + }, + { + "name": "QuikGraph", + "purpose": "방향 그래프 자료구조 + Dijkstra/A*/k-shortest path/BFS 등 알고리즘 제공", + "license": "MS-PL (Microsoft Public License, 상업적 사용 가능)", + "maturity": "active이나 정체 (최신 2.5.0이 2022-07 릴리스, 이후 신규 릴리스 없음 / 다운로드 1300만+)", + "notes": "NuGet target에 net5.0~net10.0 포함되어 .NET 9에서 동작. 자체 구현이 부담될 때 2순위. k-shortest path가 내장이라 '대안 경로 N개 제시' 같은 고급 UX에 유리. 다만 유지보수 정체와 추가 의존성(트리밍 설정)을 감수해야 함." + }, + { + "name": "Kemsekov.GraphSharp", + "purpose": "Dijkstra·그래프 컬러링·컴포넌트 등 알고리즘, QuikGraph 어댑터 제공", + "license": "확인 필요 (NuGet/리포 라이선스 확인 권장)", + "maturity": "active (3.1.x 최근 업데이트, QuikGraph보다 활발)", + "notes": "QuikGraph보다 유지보수가 활발하고 QuikGraph 그래프 어댑터를 제공. 다만 이 정도 규모 문제에는 기능 과잉. 라이선스를 반드시 확인한 뒤에만 채택." + }, + { + "name": "Dijkstra.NET", + "purpose": "우선순위 큐 기반 Dijkstra(O(E log V)) 단일 목적 라이브러리", + "license": "MIT (리포 확인 권장)", + "maturity": "beta/소규모 (단순·경량, 업데이트 빈도 낮음)", + "notes": "오직 Dijkstra만 필요하고 외부 패키지를 굳이 쓰겠다면 가장 가벼운 선택. 기능이 적어 가중치 모델 커스터마이즈는 직접 해야 함." + }, + { + "name": "Pandoc (외부 CLI, 참조 아키텍처)", + "purpose": "문서 도메인 단일 AST 허브 변환 엔진. 라이브러리가 아니라 '허브-앤-스포크' 설계 참조 + 선택적 외부 도구", + "license": "GPL-2.0+ (CLI를 번들 없이 외부 호출하면 프로젝트 라이선스에 영향 없음)", + "maturity": "production (업계 표준, 활발)", + "notes": "DocumentProvider의 LibreOffice/Markdig/ReverseMarkdown 조합 대신 Pandoc CLI를 옵션 백엔드로 두면 텍스트 도메인 변환 품질·범위가 크게 향상. 단 GPL이라 EXE에 정적 번들은 피하고, 사용자 설치 도구로 감지·호출하는 패턴(현재 LibreOffice 감지 패턴과 동일)을 권장." + } + ], + "integrationNotes": "현재 ProviderRegistry는 `_byPair`(단일 홉)와 `_outputsByInput`만 갖고 있고, 멀티홉은 DocumentProvider.RouteAsync에 하드코딩돼 있다. 다음 단계로 그래프 레이어를 얇게 얹는 것을 권장한다.\n\n**1) ConversionGraph (신규, ProviderRegistry 내부 또는 옆에)**: 생성자에서 모든 Provider의 Capability.SupportedConversions를 순회해 `Dictionary>`(노드=확장자, Edge={Provider, ConversionPair, Weight})로 인접 리스트를 만든다. ProviderRegistry 생성자 루프(현재 17~30행)에 그래프 빌드 한 단계만 추가하면 된다.\n\n**2) 가중치 부여**: ProviderCapability에 정적 손실 등급을 노출하는 게 깔끔하다. 예) `ConversionPair`에 선택적 `LossClass`(Lossless/Container/Recode/Rasterize) 필드를 추가하거나, Provider가 `double EstimateCost(ConversionPair)`를 구현(IConverterProvider 확장). 가중치는 `-log(보존율)+홉페널티` 합산. 기존 Provider는 기본값(직접 변환=저비용)으로 두면 무중단 마이그레이션 가능.\n\n**3) ConvertOptions 확장**: `bool AllowMultiHop`(기본 true), `int MaxHops`(기본 3), `bool AvoidLossy`(true면 래스터화·lossy 재인코딩 엣지를 큰 페널티/제외) 옵션을 추가. 현재 ConvertOptions 패턴(섹션별 옵션 객체)에 자연스럽게 들어간다.\n\n**4) ConversionEngine.ConvertOneAsync 수정**: 현재 91행 `_registry.TryGet`이 직접 매핑만 본다. 여기서 직접 엣지가 없으면(또는 더 저비용 멀티홉이 있으면) `ConversionGraph.FindBestPath(inExt, outExt, options)`로 경로를 구해, 경로의 각 홉을 순차 실행하는 `ExecuteChainAsync`로 위임한다. 중간 산출물은 DocumentProvider가 이미 쓰는 `workDir = Temp/e2e_..._{Guid}` 패턴을 공용 헬퍼로 올려 재사용하고, 각 홉은 기존 `provider.ConvertAsync`를 그대로 호출(인터페이스 변경 불필요). 진행률은 홉 수로 분할해 IProgress에 매핑.\n\n**5) DocumentProvider 단순화(점진적)**: 그래프 레이어가 안정화되면 RouteAsync의 `md→html→docx` 같은 하드코딩 멀티홉 분기를 제거하고, DocumentProvider는 '단일 홉 원자 변환'(md→html, html→docx 등)만 선언하게 만든다. 그러면 md→docx는 엔진의 그래프 탐색이 자동으로 md→html→docx로 합성한다. 이게 Pandoc식 '작은 변환의 조합' 철학을 레지스트리 수준에서 실현하는 것.\n\n**6) UI/탐색 표시**: OutputsForInput가 지금은 직접 출력만 반환한다. 멀티홉을 켜면 도달 가능한 모든 출력(그래프 reachability)으로 확장할 수 있어, '이 파일로 만들 수 있는 모든 포맷' 목록이 훨씬 풍부해진다. 단, 손실 경로로만 도달하는 출력은 UI에서 경고 배지(예: '손실 변환')로 구분해 사용자에게 알리는 걸 권장(Versus 철학의 경량판).", + "risks": [ + "멀티홉은 중간 산출물마다 손실이 누적될 수 있다(특히 lossy 또는 래스터화 경유). 가중치 모델이 부실하면 '동작은 하지만 품질이 나쁜' 경로를 선택할 위험 → 손실 등급 테이블과 lossy 게이트 페널티를 신중히 설정해야 한다.", + "초기 가중치는 추정값(정적 테이블)이라 실제 결과와 어긋날 수 있다. Versus식 실측 보정 없이 운영하면 일부 쌍에서 비최적 경로가 나올 수 있다 → 우선 보수적으로(직접 엣지 우선, 멀티홉은 fallback) 운영 권장.", + "QuikGraph는 2022년 이후 릴리스가 없어(정체) .NET 신버전·트리밍/AOT에서 미세 이슈 가능성. 단일 포터블 EXE/MSIX·트리밍을 쓴다면 자체 구현이 더 안전하다.", + "Pandoc을 백엔드로 넣을 경우 GPL-2.0이므로 EXE에 정적 번들 금지. 외부 설치 도구로 감지·호출하는 방식만 허용(LibreOffice 감지 패턴 준수). 잘못 번들하면 라이선스 위반.", + "멀티홉 체인 실행은 중간 임시파일 I/O가 늘어 성능·디스크 사용이 증가하고, 한 홉 실패 시 부분 산출물 정리/에러 메시지 매핑이 복잡해진다(어느 홉에서 실패했는지 사용자에게 전달 필요).", + "도메인 경계(예: DOCX→PNG)에서 그래프가 의도치 않게 '텍스트→래스터' 같은 손실 경로를 자동 선택할 수 있다. 도메인 간 전이는 명시적 화이트리스트/블랙리스트로 통제하지 않으면 예상 밖 결과가 나온다." + ], + "sources": [ + "https://pandoc.org/MANUAL.html", + "https://pandoc.org/using-the-pandoc-api.html", + "https://deepwiki.com/jgm/pandoc", + "https://ssa.ncsa.illinois.edu/isda/software/polyglot/", + "https://www.archives.gov/files/applied-research/ncsa/5-toward-a-universal-quantifiable-and-scalable-file-format-converter.pdf", + "https://www.archives.gov/files/applied-research/papers/conversion-software-registry.pdf", + "https://ssa.ncsa.illinois.edu/isda/software/archived/conversion-software-registry-csr/", + "https://imagemagick.org/source/delegates.xml", + "https://usage.imagemagick.org/files/", + "https://deepwiki.com/FFmpeg/FFmpeg/8.1-filter-architecture", + "https://ffmpeg.org/ffmpeg-filters.html", + "https://github.com/KeRNeLith/QuikGraph", + "https://www.nuget.org/packages/QuikGraph", + "https://www.nuget.org/packages/Kemsekov.GraphSharp/", + "https://github.com/matiii/Dijkstra.NET", + "https://theimagecdn.com/docs/lossy-vs-lossless-compression", + "https://en.wikipedia.org/wiki/Lossy_compression" + ] + }, + { + "key": "codex-ai", + "topic": "Codex non-interactive OAuth + API를 .NET 9/WPF 데스크톱 파일 변환기(Everything2Everything)에 통합", + "keyFindings": [ + "[가장 중요] ChatGPT 구독 OAuth 토큰은 Codex 백엔드 전용이다. auth.json에 저장된 access/refresh 토큰은 codex CLI가 자체 백엔드를 호출할 때만 유효하며, 이 토큰을 꺼내 api.openai.com(공식 OpenAI API)에 직접 Bearer로 붙여도 동작하지 않는다. 따라서 '구독으로 API를 공짜로 쓰는' 경로는 오직 codex CLI 프로세스를 외부 실행하는 방법뿐이고, SDK를 직접 호출하려면 반드시 별도의 종량제 API 키(CODEX_API_KEY 또는 OPENAI_API_KEY)가 필요하다. 이 둘은 과금 모델이 완전히 분리된 별개 경로다.", + "Codex CLI 인증은 3가지: (1) 브라우저 ChatGPT OAuth(`codex login`) — 구독 사용, (2) 디바이스 코드 플로우(`codex login --device-auth`, 2026년 3월 추가, 헤드리스/원격용 베타) — URL+코드를 다른 기기에서 입력, (3) API 키(`CODEX_API_KEY`/`OPENAI_API_KEY` 환경변수) — CI/CD·프로그래매틱 권장. 토큰은 기본 `~/.codex/auth.json`(Windows는 `%USERPROFILE%\\.codex\\auth.json`)에 평문 JSON으로 저장되며, config의 `cli_auth_credentials_store`를 file/keyring/auto로 바꿀 수 있다.", + "비대화형 실행은 `codex exec \"<프롬프트>\"`. 주요 플래그: `--json`(stdout이 JSONL 이벤트 스트림), `--output-schema `(응답을 JSON Schema로 강제 — 메타데이터/구조화 추출에 핵심), `-o/--output-last-message `(최종 메시지를 파일로), `--model `, `--cd `(작업 디렉터리), `--skip-git-repo-check`(git 저장소 아닌 폴더 허용 — 변환 앱에 필수), `--sandbox read-only|workspace-write|danger-full-access`, `--ephemeral`(세션 미저장). 프롬프트 안에 파일/이미지 경로를 직접 적으면 Codex가 읽어들인다.", + "헤드리스 부트스트랩: 브라우저 있는 PC에서 `codex login` 후 생성된 auth.json을 헤드리스 머신으로 복사하거나, `printenv CODEX_ACCESS_TOKEN | codex login --with-access-token`로 토큰 주입 가능. CI에서는 auth.json을 매 실행 덮어쓰면 refresh된 토큰이 stale해지는 race가 있어 'if [ ! -f ] 가드'와 concurrency 직렬화가 권장된다 — 데스크톱 앱이 동시 변환 다건을 돌릴 때 동일 문제 발생 가능(아래 risks 참조).", + "공식 OpenAI .NET SDK는 NuGet `OpenAI`(v2.10.0, 2026-04-04), MIT 라이선스, netstandard2.0 타깃이라 .NET 9에서 문제없이 동작. Chat Completions·비전(이미지 입력)·Structured Outputs(JSON Schema, gpt-4o 계열 이상) 모두 지원, 활발히 유지보수 중. Azure 전용 확장은 `Azure.AI.OpenAI`(공식 OpenAI 패키지 위에 얹힘). `OpenAI-DotNet`(8.x), `tryAGI.OpenAI`(4.x)는 커뮤니티 대안.", + "Anthropic Claude는 2026년부터 공식 .NET SDK가 NuGet `Anthropic` 패키지(v12.23.0, 2026-05-21, netstandard2.0)로 제공 — anthropics/anthropic-sdk-csharp. 이름이 비슷한 `Anthropic.SDK`(tghamm, 5.x)와 `tryAGI.Anthropic`은 비공식이다. 공식 패키지를 쓰는 것이 권장.", + "Microsoft.Extensions.AI(MEAI) 1.0이 2026-04-03 정식 출시(stable, MIT). `IChatClient` 단일 추상화로 OpenAI/Anthropic/Ollama/Bedrock/Gemini를 한 인터페이스로 다루며, 멀티모달(텍스트+이미지) 메시지를 지원. 프로바이더 교체가 한 줄 변경이라 이 프로젝트의 Provider/Registry 'OpenAI냐 Claude냐를 사용자가 선택' 요구에 정확히 들어맞는다." + ], + "recommendedApproach": "단일 포터블 EXE / MSIX 배포라는 제약이 결정적이다. codex CLI는 별도 설치가 필요한 외부 Node 기반 바이너리이고, 포터블 EXE에 번들하기 어렵고(수백 MB), OAuth 토큰을 API로 재사용할 수 없으므로 '비용 절감' 명분도 사라진다. 따라서 기본 경로는 SDK 직접 호출로 가는 것이 맞다.\n\n권장 아키텍처(2층):\n1) 추상화 층 — Microsoft.Extensions.AI의 `IChatClient`를 내부 LLM 게이트웨이로 채택. OpenAI는 공식 `OpenAI`(MIT) + MEAI OpenAI 커넥터, Claude는 공식 `Anthropic` 패키지(MEAI Anthropic 커넥터)로 연결. 사용자는 설정에서 'OpenAI / Claude / (옵션)Codex CLI'를 고르고 API 키만 입력하면 된다. 키는 Windows DPAPI(ProtectedData)로 암호화해 로컬 저장 — 포터블 EXE에서도 사용자별 암호화 가능.\n\n2) 선택적 Codex CLI 백엔드 — ChatGPT Pro/Plus 구독을 이미 보유한 파워유저를 위해, codex가 PATH에 감지될 때만 활성화되는 보조 백엔드로 둔다. `CheckAvailabilityAsync`에서 `codex --version` 프로브 → 실패 시 RequiresExternal로 다운로드 안내. 실행은 `codex exec --skip-git-repo-check --json --output-schema schema.json -o out.json --cd \"<프롬프트 + 파일경로>\"` 형태로 Process 호출, JSONL 마지막 메시지 파싱.\n\n즉 '기본은 API 키 + 공식 SDK(MEAI 추상화), Codex CLI는 구독자용 opt-in 보조 경로'의 하이브리드가 이 프로젝트에 최적이다. LLM 자체는 변환의 '핵심 엔진'이 아니라 '후처리/부가가치 단계'(요약·번역·포맷 정규화·OCR 교정·이미지 캡션·메타데이터)로 배치해, 키가 없어도 기존 변환은 100% 동작하고 LLM 기능만 비활성(ComingSoon/RequiresExternal 스타일)되게 한다.", + "libraries": [ + { + "name": "OpenAI (공식 .NET SDK)", + "purpose": "OpenAI API 직접 호출 — Chat Completions, 비전(이미지 입력), Structured Outputs(JSON Schema). 문서 요약/번역/메타데이터 생성/이미지 캡션의 기본 엔진", + "license": "MIT", + "maturity": "production (v2.10.0, 2026-04-04, 활발히 유지보수, netstandard2.0이라 .NET 9 호환)" + }, + { + "name": "Anthropic (공식 Claude .NET SDK)", + "purpose": "Claude API 직접 호출 — OpenAI 대안. 긴 문서 요약/번역에 강점, 비전 지원", + "license": "MIT (anthropics/anthropic-sdk-csharp)", + "maturity": "production (v12.23.0, 2026-05-21, 공식). 주의: 비공식 'Anthropic.SDK'(tghamm)·'tryAGI.Anthropic'과 혼동 금지" + }, + { + "name": "Microsoft.Extensions.AI / .Abstractions", + "purpose": "IChatClient 단일 추상화로 OpenAI·Claude·Ollama를 동일 인터페이스로 — Provider/Registry의 LLM 백엔드 선택 계층", + "license": "MIT", + "maturity": "production (1.0 정식, 2026-04-03, 멀티모달 지원). 본 프로젝트 추상화와 가장 정합" + }, + { + "name": "OpenAI Codex CLI", + "purpose": "외부 프로세스(codex exec)로 ChatGPT 구독 OAuth를 재사용해 LLM 호출 — 구독 보유자 opt-in 보조 경로", + "license": "Apache-2.0 (openai/codex 리포)", + "maturity": "active (2026년 활발, GPT-5.5 에이전틱). 단 별도 설치 필요·OAuth 토큰 API 재사용 불가·포터블 번들 부적합" + }, + { + "name": "Azure.AI.OpenAI", + "purpose": "Azure OpenAI Service를 쓸 경우의 확장(공식 OpenAI 패키지 위에 얹힘). 일반 OpenAI만 쓸 거면 불필요", + "license": "MIT", + "maturity": "production (v2.1.0). 본 프로젝트엔 선택적" + }, + { + "name": "tryAGI.OpenAI / OpenAI-DotNet", + "purpose": "OpenAI 비공식 커뮤니티 SDK 대안", + "license": "MIT", + "maturity": "active (각각 v4.2.0 / v8.8.x). 공식 OpenAI 패키지가 있으므로 우선순위 낮음" + } + ], + "integrationNotes": "현 추상화(IConverterProvider / ProviderCapability / ProviderRegistry / ConvertOptions)에 자연스럽게 끼워넣는 방법:\n\n1) 신규 `LlmProvider : IConverterProvider` 추가 (OcrProvider와 동일한 패턴). SupportedConversions를 LLM 후처리 매트릭스로 정의:\n - 요약: .pdf/.docx/.txt/.md → .txt/.md (summary)\n - 번역: .txt/.docx/.md → .txt/.docx (대상 언어는 옵션)\n - 포맷 정규화: .txt → .md, .csv → .md(표)\n - OCR 교정: OcrProvider 출력(.txt)을 받아 LLM이 오탈자/줄바꿈 정리 (파이프라인 2단계)\n - 이미지 캡션/대체텍스트: .png/.jpg → .txt (비전)\n - 메타데이터 생성: 임의 입력 → .json (제목/태그/요약, Structured Outputs)\n OcrProvider가 PdfProvider를 생성자 주입으로 재사용하듯, LlmProvider도 텍스트 추출이 필요하면 DocumentProvider/PdfProvider/OcrProvider를 주입받아 '추출→LLM' 2단계로 구성한다.\n\n2) `ConvertOptions`에 `LlmOptions Llm { get; set; } = new();` 추가. 필드 예: Backend(\\\"openai\\\"|\\\"anthropic\\\"|\\\"codex-cli\\\"|\\\"auto\\\"), Model, ApiKey(또는 키 저장소 참조), Task(Summarize/Translate/Normalize/Caption/Metadata), TargetLanguage, MaxTokens, Temperature. 기존 OcrOptions와 동일한 스타일이라 직렬화·UI 바인딩 일관성 유지.\n\n3) `CheckAvailabilityAsync`가 게이트 역할:\n - SDK 경로: API 키(설정 또는 OPENAI_API_KEY/ANTHROPIC_API_KEY 환경변수) 존재 확인 → 없으면 ProviderAvailability.NotReady(\\\"API 키 미설정\\\", MissingDependencies). ExternalDependency로 키 발급 URL 안내.\n - Codex 경로: `codex --version` 프로세스 프로브 + auth.json 존재 확인 → 없으면 ProviderStatus.RequiresExternal로 다운로드/로그인 안내.\n 기존 OcrProvider가 OCR 언어팩 유무로 NotReady를 반환하는 패턴을 그대로 따른다.\n\n4) `ConvertAsync` 구현: IProgress / CancellationToken 시그니처 그대로 유지. SDK 경로는 IChatClient.GetResponseAsync(스트리밍 시 진행률 갱신), Codex 경로는 Process.Start로 `codex exec --json --output-schema ... -o out.json` 실행 후 표준출력 JSONL 파싱. 출력 파일은 기존 OutputPathHelper.ResolveOutputPath + OnCollision 규칙을 재사용. 실패 시 ConvertResult.Fail, 건너뛰기 ConvertResult.Skip로 통일.\n\n5) ProviderRegistry는 수정 불필요 — 생성자에서 providers 목록에 LlmProvider 인스턴스만 추가하면 _byPair 매트릭스에 자동 편입된다. 단 LLM은 비결정적·유료·네트워크 의존이므로, 동일 (input,output) 페어를 로컬 변환 Provider가 이미 점유한 경우 TryAdd가 먼저 등록된 쪽을 유지하는 현 동작 덕분에 'LLM은 로컬 변환이 없는 신규 페어(요약/번역 등)에만 노출'되도록 등록 순서를 조정하면 된다. UI에서는 LLM 출력 페어에 '✨ AI' 배지를 붙여 종량 과금/네트워크 사용을 사용자에게 명시할 것.\n\n6) 키 보안: 포터블 EXE에서도 System.Security.Cryptography.ProtectedData(DPAPI, CurrentUser)로 키를 암호화해 %APPDATA% 또는 앱 폴더에 저장. 환경변수 OPENAI_API_KEY/ANTHROPIC_API_KEY도 fallback으로 읽어 CI/파워유저 친화.", + "risks": [ + "[핵심 오해 차단] ChatGPT 구독 OAuth 토큰을 OpenAI API에 직접 재사용하려는 설계는 불가능하다. auth.json의 access_token은 codex 백엔드 전용. 'API 직접 호출'을 원하면 반드시 종량제 API 키가 필요하고, '구독 재사용'을 원하면 codex CLI 프로세스 호출 외 방법이 없다. 이 둘을 혼동하면 아키텍처가 무너진다.", + "Codex CLI를 단일 포터블 EXE에 번들하기는 비현실적(외부 Node 런타임·수백 MB·자동 업데이트). 사용자가 별도 설치+로그인해야 하므로 일반 사용자 대상 기본 경로로는 부적합. 구독 파워유저용 opt-in으로만 다뤄야 한다.", + "auth.json refresh 토큰 race: 다건 동시 변환에서 codex 프로세스를 병렬 실행하면 토큰 갱신 충돌로 stale 토큰이 덮어써질 수 있다. Codex 경로는 SemaphoreSlim(1)로 직렬화하거나 --ephemeral 사용 권장.", + "비용·레이트리밋·네트워크 의존: LLM 변환은 종량 과금이라 대량 배치 변환 시 비용 폭증·429 가능. 변환 큐 row마다 토큰/예상비용 표시, 사용자 확인 게이트, 재시도·백오프 필요. 오프라인에서는 LLM 페어가 NotReady가 되어야 하며 기존 로컬 변환은 영향받지 않게 격리.", + "프라이버시: 사용자의 로컬 문서/이미지가 OpenAI/Anthropic 서버로 전송된다. 명시적 동의 UI·설정 토글 필수, 기본 OFF 권장. 기업/민감 데이터 사용자를 위해 로컬 모델(Ollama via IChatClient) 경로를 미래 옵션으로 열어두면 좋다.", + "비결정성·환각: 요약/번역/메타데이터는 결과가 매번 다를 수 있어 '재현 가능한 파일 변환'이라는 앱 정체성과 충돌. LLM 출력 페어는 UI에서 'AI/실험적'으로 분명히 구분하고 결과 미리보기·재생성 버튼을 제공할 것.", + "라이선스 주의: 공식 'Anthropic' 패키지와 비공식 'Anthropic.SDK'(tghamm)를 혼동하면 안 됨. 둘 다 동작하나 공식 패키지가 장기 지원에 유리. codex 리포는 Apache-2.0(상업 사용 가능)이지만 CLI는 OpenAI 계정·약관에 종속.", + "공식 SDK·MEAI 모두 netstandard2.0 타깃이라 .NET 9 호환은 문제없으나, MEAI 1.0의 일부 커넥터 API가 빠르게 진화 중이라 버전 핀 고정(예: 10.x 라인)과 회귀 테스트 권장." + ], + "sources": [ + "https://developers.openai.com/codex/auth", + "https://developers.openai.com/codex/noninteractive", + "https://developers.openai.com/codex/cli/reference", + "https://codex.danielvaughan.com/2026/04/01/codex-cli-authentication-flows-credential-management/", + "https://github.com/openai/codex/issues/3820", + "https://github.com/openai/openai-dotnet", + "https://www.nuget.org/packages/OpenAI", + "https://www.nuget.org/packages/Anthropic", + "https://github.com/anthropics/anthropic-sdk-csharp", + "https://platform.claude.com/docs/en/api/sdks/csharp", + "https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai", + "https://www.nuget.org/packages/Microsoft.Extensions.AI.Abstractions/", + "https://www.nuget.org/packages/Azure.AI.OpenAI/", + "https://developers.openai.com/codex/models", + "https://tosea.ai/blog/openai-codex-complete-guide-2026" + ] + }, + { + "key": "ffmpeg", + "topic": "FFmpeg .NET 통합 (영상/오디오/코덱 변환) — Everything2Everything 적용 리서치", + "keyFindings": [ + "라이브러리 선택은 명확하다: FFMpegCore가 정답이다. v5.4.0 (2025-10-27 릴리스, 누적 600만 다운로드, 일 3K, MIT 라이선스, .NET Standard 2.0+ → .NET 9 호환). 라이브러리 자체가 MIT이므로 상업적 사용에 제약이 전혀 없다.", + "Xabe.FFmpeg는 라이브러리 코드 자체가 CC BY-NC-SA 3.0 (비상업) 라이선스다. 상업적 사용은 별도 유료 상업 라이선스 구매가 필요하다. 인용: 'You may use Software under Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0) license for non commercial projects.' → 상업 사용 선호 방침상 탈락.", + "핵심 라이선스 함정은 라이브러리가 아니라 FFmpeg 바이너리 자체다. FFmpeg는 기본 LGPL 2.1+이지만 --enable-gpl(libx264/libx265 H.264/H.265 인코더 포함) 또는 --enable-nonfree(libfdk-aac) 빌드는 GPL/비배포 라이선스로 바뀐다. 폐쇄소스 상업 EXE에 가장 흔한 gyan.dev 빌드(GPLv3)나 BtbN gpl 빌드를 번들하면 안 된다.", + "결정적 발견: 폐쇄소스 상업 배포에는 BtbN의 'lgpl-shared' 빌드를 써야 한다. LGPL 준수 조건은 (1) --enable-gpl/--enable-nonfree 없이 빌드, (2) 동적 링크(여기선 별도 ffmpeg.exe 프로세스 호출이 가장 안전한 동적 분리), (3) FFmpeg 소스 코드 제공 의무(또는 다운로드 링크), (4) 다운로드 페이지/앱 내 FFmpeg 사용 고지(attribution)다. 이미 LibreOffice를 외부 도구로 호출하는 본 프로젝트 구조와 정확히 일치한다.", + "NVENC(h264_nvenc/hevc_nvenc)는 LGPL 빌드에서 --enable-nonfree 없이 사용 가능하다. NVIDIA의 FFmpeg/NVENC 인터페이스 작성자(Philip Lachsinger)가 직접 'turning it on did not stop your ffmpeg build from being lgpl compliant (it does not require the non-free flag)'라고 확인. 헤더는 MIT, 런타임은 GPU 드라이버의 system library exception 적용. → LGPL-shared 빌드 + NVENC HW 가속이 상업 배포에 합법적으로 양립한다.", + "코덱 라이선스 매트릭스(상업 배포 관점): H.264/H.265 인코딩은 libx264/libx265=GPL(폐쇄소스 불가) 대신 하드웨어 인코더(h264_nvenc/hevc_nvenc, h264_qsv/hevc_qsv, h264_amf)를 쓰면 LGPL 유지. AV1=libaom/libsvtav1(BSD-like, royalty-free), VP9=libvpx(BSD-like), Opus=libopus(BSD), FLAC(Xiph BSD), AAC=FFmpeg 네이티브 aac 인코더(LGPL, libfdk-aac는 nonfree라 회피). 즉 LGPL-shared 빌드만으로 AV1/VP9/Opus/FLAC/AAC/MP3는 모두 커버되고, H.264/H.265는 HW 인코더로 우회.", + "FFMpegCore는 본 프로젝트의 IConverterProvider 패턴에 매끄럽게 들어간다. 진행률은 NotifyOnProgress(Action onPercentageProgress, TimeSpan totalTimeSpan) → IProgress로 직결, 취소는 CancellableThrough(CancellationToken token, int timeout=0) → 기존 ct 직결, HW 가속은 WithHardwareAcceleration(HardwareAccelerationDevice) (enum: Auto/D3D11VA/DXVA2/QSV/CUVID/CUDA/VDPAU/VAAPI/LibMFX)로 지원.", + "바이너리 크기 현실: FFmpeg essentials 정적 빌드 ffmpeg.exe는 압축 32MB / 압축해제 ~103MB. 단일 포터블 EXE에 내장하면 100MB가 더해진다. .NET single-file publish는 ffmpeg.exe를 임베드해 자가추출(IncludeNativeLibrariesForSelfExtract)할 수 있으나 시작 시 temp 추출 비용이 크다. 더 나은 전략은 런타임 다운로드(FFMpegDownloader.DownloadFFMpegSuite())다." + ], + "recommendedApproach": "FFMpegCore 5.4.0(MIT) + FFprobe를 채택하고, FFmpeg 바이너리는 EXE에 번들하지 말고 'RequiresExternal + 최초 사용 시 LGPL-shared 빌드 자동 다운로드' 방식을 권장한다. 근거: (1) 라이선스 — BtbN lgpl-shared 빌드(GPL/nonfree 없음)는 폐쇄소스 상업 EXE에 합법적이며, ffmpeg.exe를 별도 프로세스로 호출(=동적 분리)하면 본 앱 바이너리는 FFmpeg와 분리되어 LGPL 전염 없음. gyan.dev나 BtbN gpl 빌드(GPLv3, libx264/x265 포함)는 절대 번들 금지. (2) 배포 크기 — FFmpeg ~100MB를 단일 EXE에 박으면 다운로드/시작이 무거워지므로, 이미 LibreOffice를 외부 의존성으로 두는 본 프로젝트 철학대로 FFmpeg도 외부 도구로 취급. 구체적으로 FfmpegProvider를 ProviderStatus.RequiresExternal로 등록하고, CheckAvailabilityAsync에서 (a) 시스템 PATH의 ffmpeg.exe, (b) 앱 데이터 폴더(%LOCALAPPDATA%\\\\Everything2Everything\\\\ffmpeg)에 받아둔 바이너리, (c) FFMpegCore의 FFMpegDownloader로 최초 1회 자동 다운로드 순으로 탐지/조달. GlobalFFOptions.Configure(new FFOptions{ BinaryFolder = <앱데이터 ffmpeg 경로> })로 경로를 고정. (3) 코덱 전략 — H.264/H.265는 HW 인코더(h264_nvenc/qsv/amf, 미지원 시 mpeg4/우회) 우선, AV1/VP9/Opus/FLAC/AAC(네이티브)/MP3는 LGPL 빌드로 직접 처리. (4) 진행률은 FFprobe로 duration을 먼저 구해 NotifyOnProgress(Action, TimeSpan)을 IProgress에 연결, 취소는 CancellableThrough(CancellationToken)에 기존 ct 연결. MSIX 배포 시에는 자동 다운로드가 샌드박스/네트워크 정책에 걸릴 수 있으니, MSIX 변형에서는 lgpl-shared DLL/EXE를 앱 패키지에 동봉(여전히 LGPL 준수: 동적 호출 + 소스 제공 링크 + 고지)하는 분기 권장.", + "libraries": [ + { + "name": "FFMpegCore", + "purpose": "FFmpeg/FFprobe CLI를 감싸는 .NET fluent wrapper. 트랜스코딩, 압축, 포맷 변환, 미디어 분석, 진행률/취소/HW가속 지원. 영상·오디오 Provider의 핵심 엔진.", + "license": "MIT (라이브러리 코드 — 상업 사용 자유. FFmpeg 바이너리 라이선스는 별개)", + "maturity": "production — v5.4.0(2025-10-27), 누적 600만 다운로드/일 3K, 활발한 유지보수. NuGet rosenbjergsoftworks", + "notes": "GlobalFFOptions.Configure로 BinaryFolder 지정. FFMpegDownloader.DownloadFFMpegSuite()로 ffbinaries API에서 런타임 다운로드. NotifyOnProgress / CancellableThrough / WithHardwareAcceleration 제공. 본 프로젝트 채택 권장." + }, + { + "name": "Xabe.FFmpeg", + "purpose": "FFmpeg .NET wrapper (대안 후보). 비슷한 트랜스코딩/변환 API.", + "license": "CC BY-NC-SA 3.0 (비상업 전용). 상업 사용은 유료 상업 라이선스 필수", + "maturity": "active — 유지되나 라이선스 모델이 상업 프로젝트에 부적합", + "notes": "코드 자체가 비상업 라이선스라 상업 EXE에는 탈락. 상업 라이선스 비용은 공개 안 됨(별도 문의)." + }, + { + "name": "직접 Process 호출 (System.Diagnostics.Process로 ffmpeg.exe 실행)", + "purpose": "의존성 0, ffmpeg CLI를 직접 ProcessStartInfo로 실행하고 stderr를 파싱해 진행률 추출.", + "license": "N/A (본인 코드, ffmpeg 바이너리만 라이선스 대상)", + "maturity": "production — 가장 단순/투명하지만 인자 빌드·진행률 파싱(time=/duration 정규식)·에러 처리를 직접 구현해야 함", + "notes": "본 프로젝트는 이미 DocumentProvider에서 soffice를 ProcessStartInfo+WaitForExitAsync(ct)로 호출하는 패턴이 정립됨. FFMpegCore가 부담스러우면 이 패턴 재사용 가능하나, 진행률/HW가속 추상화를 직접 짜야 해 FFMpegCore 대비 이득 적음." + }, + { + "name": "FFmpeg 바이너리 (BtbN lgpl-shared 빌드)", + "purpose": "실제 트랜스코딩을 수행하는 네이티브 엔진. GPL/nonfree 미포함 LGPL 빌드.", + "license": "LGPL 2.1+ (--enable-gpl, --enable-nonfree 없음). 폐쇄소스 상업 배포 가능 — 단 동적 분리 호출 + 소스 제공 + 고지 필요", + "maturity": "production — BtbN/FFmpeg-Builds, 7.1.x 정기 릴리스(2025). winget: BtbN.FFmpeg.LGPL.Shared.7.1", + "notes": "상업 배포에 권장하는 바이너리. NVENC/QSV/AMF HW 인코더 + AV1/VP9/Opus/FLAC/AAC(네이티브) 포함. H.264/H.265 SW(libx264/x265=GPL)는 빠짐 → HW 인코더로 우회." + }, + { + "name": "FFmpeg 바이너리 (gyan.dev essentials/full)", + "purpose": "가장 널리 쓰이는 Windows 정적 빌드. libx264/x265 H.264/H.265 SW 인코딩 포함.", + "license": "GPLv3 (essentials/full 모두 --enable-gpl). 폐쇄소스 상업 EXE에 번들 금지", + "maturity": "production — 사실상 표준, 정기 갱신", + "notes": "개발/테스트엔 편하나 GPL이라 상업 배포 바이너리로 부적합. ffmpeg.exe ~103MB(압축해제). 라이선스 때문에 BtbN lgpl-shared로 교체 필요." + } + ], + "integrationNotes": "새 FfmpegProvider : IConverterProvider를 src/Everything2Everything.Core/Converters/에 추가하고 ProviderRegistry에 등록한다(기존 7개 Provider와 동일). 구체 설계:\\n\\n1) Capability: Status=ProviderStatus.RequiresExternal, ExternalDependencies에 new ExternalDependency(Name:\\\"FFmpeg\\\", Description:\\\"영상/오디오 트랜스코딩 엔진(LGPL 빌드)\\\", DownloadUrl:\\\"https://github.com/BtbN/FFmpeg-Builds/releases\\\"). SupportedConversions는 ProviderCapability.PairsFromMatrix로 영상(mp4/mkv/webm/mov/avi/gif)·오디오(mp3/aac/m4a/opus/ogg/flac/wav) 입출력 N×M 구성. 단 코덱 호환 안 되는 쌍(예: → flac은 오디오 전용)은 매트릭스 후 필터링하거나 PairsFromMatrix 대신 명시적 ConversionPair 리스트로 정밀 제어 권장.\\n\\n2) 바이너리 탐지: ExternalToolDetector에 TryFindFfmpeg(out string ffmpegPath) 추가 — (a) %LOCALAPPDATA%\\\\Everything2Everything\\\\ffmpeg\\\\ffmpeg.exe, (b) 시스템 PATH(where ffmpeg), 순으로 탐지. CheckAvailabilityAsync에서 못 찾으면 NotReady 반환하되, 선택적으로 FFMpegDownloader.DownloadFFMpegSuite(new FFOptions{ BinaryFolder=<앱데이터경로> })로 자동 조달 후 GlobalFFOptions.Configure로 경로 고정. (FFMpegDownloader 기본 소스는 ffbinaries=gyan GPL 빌드일 수 있으니, 상업 배포에선 BtbN lgpl-shared zip을 직접 받는 커스텀 다운로더를 쓰거나 동봉 권장 — 라이선스 검증 필수.)\\n\\n3) ConvertAsync 본문(FFMpegCore 사용):\\n var media = await FFProbe.AnalyseAsync(sourcePath, cancellationToken: ct); // duration 확보\\n await FFMpegArguments\\n .FromFileInput(sourcePath)\\n .OutputToFile(outputPath, overwrite:true, opt => opt\\n .WithHardwareAcceleration(HardwareAccelerationDevice.Auto) // NVENC/QSV 자동\\n .WithVideoCodec(\\\"h264_nvenc\\\") // 또는 av1/libaom, vp9, opus 등 outExt별 분기\\n .WithAudioCodec(\\\"aac\\\"))\\n .NotifyOnProgress(p => progress?.Report(p/100.0), media.Duration) // IProgress 직결\\n .CancellableThrough(ct) // 기존 ct 직결\\n .ProcessAsynchronously();\\n 진행률 NotifyOnProgress(Action, TimeSpan)의 0~100을 /100.0해 기존 IProgress 계약(0~1)에 맞춘다. 예외는 기존 Provider처럼 try/catch로 ConvertResult.Fail, OperationCanceledException은 rethrow.\\n\\n4) HW 가속 폴백: CheckAvailabilityAsync 또는 첫 변환 시 ffmpeg -encoders로 h264_nvenc/h264_qsv/h264_amf 가용성을 탐지해 ConvertOptions에 저장하고, 없으면 SW 인코더로 폴백하되 H.264/H.265 SW는 GPL이라 LGPL 빌드엔 없음 → 폴백을 AV1/VP9(libaom/libvpx, LGPL) 또는 mpeg4로 잡거나 사용자에게 HW 미지원 안내. ConvertOptions에 코덱/품질(CRF) 옵션 필드 추가 고려.\\n\\n5) 라이선스 고지: 본 프로젝트 어딘가(About/설정)에 'This software uses libraries from the FFmpeg project under the LGPLv2.1' 문구 + FFmpeg 소스 다운로드 링크 추가(LGPL 의무). ExternalDependency.DownloadUrl을 통해 UI에서 안내 가능.\\n\\n6) csproj: Everything2Everything.Core.csproj ItemGroup에 추가. 자동 다운로더를 쓸 경우 FFMpegCore에 내장된 FFMpegDownloader 사용(별도 패키지 불필요). 단일 EXE에 번들 시 ffmpeg.exe를 None/Content로 추가하고 single-file에서 SelfExtract 메타데이터로 제외/비압축 처리(시작 성능).", + "risks": [ + "라이선스 사고 위험(최대): 개발 편의로 gyan.dev(GPLv3) 빌드를 받아 그대로 상업 EXE에 번들하면 GPL 위반. FFMpegDownloader.DownloadFFMpegSuite() 기본 소스(ffbinaries)가 GPL 빌드를 받아올 수 있어, 자동 다운로더를 쓰더라도 받아오는 빌드의 라이선스를 반드시 검증/고정해야 한다. 상업 배포는 BtbN lgpl-shared로 명시 고정 권장.", + "LGPL 고지/소스제공 의무 누락: LGPL은 동적 호출이어도 (1) FFmpeg 사용 고지, (2) FFmpeg 소스 코드 입수 경로 제공이 필요. About 화면/배포 페이지에 문구·링크가 없으면 위반.", + "H.264/H.265 SW 인코딩 공백: LGPL 빌드엔 libx264/x265가 없어 HW 인코더(NVENC/QSV/AMF)가 없는 머신(예: 구형/가상 환경)에서는 H.264/H.265 출력이 불가. 폴백 코덱(AV1/VP9) 또는 명확한 사용자 안내 필요. 무신경하면 '변환 실패'로 보임.", + "코덱별 특허/로열티 별도 위험: 라이선스(코드 배포)와 특허(코덱 사용)는 별개. H.264/H.265/AAC는 MPEG-LA/Access Advance 등 특허 풀 대상으로, FFmpeg.org도 'commercial use carries higher risk'라고 경고. AV1/VP9/Opus/FLAC(royalty-free)을 기본/권장 출력으로 두면 위험 최소화.", + "배포 크기/UX: ffmpeg.exe ~100MB. 단일 EXE 내장 시 다운로드/시작 지연, 자동 다운로드 시 최초 변환 전 네트워크 의존 + 실패 처리(오프라인/방화벽) 필요. MSIX 샌드박스에선 임의 경로 다운로드/실행이 막힐 수 있어 동봉 전략으로 분기해야 함.", + "자동 다운로드 보안: 외부 URL에서 실행 파일을 받아 실행하므로 HTTPS·체크섬(해시) 검증 없이 받으면 공급망 공격 표면. 다운로드 후 SHA256 검증 권장.", + "프로세스 호출 안정성: ffmpeg는 stderr로 진행률을 흘리고 비정상 종료 시 좀비 프로세스/임시파일이 남을 수 있음. 기존 DocumentProvider처럼 ct 취소 시 proc.Kill(true)와 temp 정리(finally)를 FFMpegCore의 CancellableThrough가 처리하는지 확인하고, 미흡하면 보강 필요." + ], + "sources": [ + "https://github.com/rosenbjerg/FFMpegCore", + "https://github.com/rosenbjerg/FFMpegCore/blob/main/LICENSE", + "https://www.nuget.org/packages/FFMpegCore", + "https://github.com/rosenbjerg/FFMpegCore/blob/main/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs", + "https://github.com/rosenbjerg/FFMpegCore/issues/277", + "https://github.com/rosenbjerg/FFMpegCore/issues/279", + "https://ffmpeg.xabe.net/license.html", + "https://ffmpeg.xabe.net/index.html", + "https://github.com/tomaszzmuda/Xabe.FFmpeg", + "https://www.ffmpeg.org/legal.html", + "https://ffmpeg.org/general.html", + "https://trac.ffmpeg.org/wiki/Encode/AV1", + "https://trac.ffmpeg.org/wiki/HWAccelIntro", + "https://forums.developer.nvidia.com/t/lgpl-ffmpeg-and-nvenc-in-a-closed-source-commercial-application/51169", + "https://www.gyan.dev/ffmpeg/builds/", + "https://github.com/BtbN/FFmpeg-Builds", + "https://github.com/BtbN/FFmpeg-Builds/releases", + "https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview", + "https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/design.md" + ] + }, + { + "key": "pdf-compress", + "topic": "PDF 압축 및 PDF↔문서 양방향 변환 (Everything2Everything .NET 9 / WPF 통합 관점)", + "keyFindings": [ + "압축 엔진 라이선스가 핵심 갈림길이다. Ghostscript(-dPDFSETTINGS /screen·/ebook·/printer·/prepress)는 압축 품질이 가장 우수하지만 AGPL v3 듀얼 라이선스다. 포터블 EXE에 gs 바이너리를 동봉/배포하면 AGPL 전염 의무(소스 공개)가 발생하므로, 상업적 사용을 원하면 Artifex 상업 라이선스 구매가 필요하다. MuPDF/mutool clean도 동일하게 AGPL이라 같은 문제가 있다.", + "qpdf(Apache 2.0, 최신 12.4.0 / 2026-04, 매우 활발)가 라이선스 안전성 측면에서 1순위 무료 옵션이다. object stream 압축(--object-streams=generate), 스트림 재압축, linearize(웹 최적화), 암호화/복호화/비밀번호 변경, 페이지 분할·병합을 모두 CLI로 제공한다. 단, qpdf는 이미지 다운샘플링(리샘플링)은 하지 않는다 — 구조 최적화/무손실 위주라 압축률이 Ghostscript보다 낮다.", + "최대 압축률(이미지 다운샘플링)은 라이선스 안전한 무료 도구만으로는 약하다. qpdf로 구조 최적화 + 프로젝트가 이미 보유한 ImageMagick/PDFium으로 이미지 페이지를 재인코딩하는 하이브리드가 AGPL을 피하면서 실용적 압축을 내는 최선의 무료 경로다.", + "PDF→DOCX 레이아웃 보존은 순수 .NET 라이브러리로는 사실상 불가능하다. PdfPig(Apache 2.0, v0.1.14 / 2026-03, 활발)는 텍스트·글자 위치(page.Letters)·단어(GetWords)·이미지 추출까지 가능하지만 DOCX 재구성(레이아웃 엔진)은 설계 범위 밖이다. Docnet.Core(MIT, PDFium 래퍼)도 렌더/텍스트 추출 전용이다.", + "PDF→DOCX/편집가능 변환의 현실적 최선은 이미 통합된 LibreOffice headless(soffice --convert-to docx)다. 무료(MPL/LGPL)이고 단락·표를 어느 정도 복원하지만, 스캔/복잡 레이아웃 PDF는 충실도가 낮다. 고충실도 상업 변환이 필요하면 Nutrient(PSPDFKit)/IronPDF 등 유료 SDK가 있으나 라이선스 비용이 든다.", + "PDF→텍스트/HTML은 순수 .NET로 충분하다. PdfPig(텍스트, 무료)와 PDFium(Docnet) 또는 mutool로 텍스트/구조 추출이 가능하다. PDF→HTML 레이아웃 보존은 LibreOffice 또는 pdf2htmlEX(외부)이 현실적이다.", + "PDF/A 변환은 Ghostscript(-dPDFA, AGPL) 또는 LibreOffice PDF export(SelectPdfVersion=1 → PDF/A-1)로 가능하며, 검증은 veraPDF(반사실적 표준 검증기, 무료)로 한다. 라이선스 안전을 원하면 LibreOffice 경로 + veraPDF 검증 조합이 적합하다.", + "암호화/복호화/병합/분할은 qpdf(Apache 2.0) 단독으로 전부 커버 가능하며 가장 가벼운 단일 실행 파일이다. 순수 .NET 대안으로 PDFsharp 6.2.x(MIT, AES-128/256, 병합·분할·암호화 지원, .NET 9 호환)가 외부 의존성 없이 In-Process로 동작해 포터블 EXE에 가장 잘 맞는다." + ], + "recommendedApproach": "이 프로젝트(단일 포터블 EXE / 상업적 사용 가능 라이선스 우선)에는 'AGPL 회피 + 가능한 한 in-process .NET' 원칙을 권장한다.\n\n1) PDF 압축: 1차로 PDFsharp(MIT) 또는 qpdf(Apache 2.0)로 구조 최적화(object stream 압축, linearize, 중복 객체 제거)를 in-process/경량 CLI로 수행한다. 이미지 다운샘플링이 필요한 '강한 압축' 모드는 이미 보유한 PDFium(Docnet/PDFtoImage)로 페이지를 렌더 후 ImageMagick로 JPEG/품질 조절 재인코딩하여 새 PDF를 만드는 하이브리드를 별도 옵션으로 제공한다(텍스트 선택성은 잃지만 라이선스 안전). Ghostscript는 '고급 압축' 옵션으로만 노출하되, 번들하지 말고 사용자가 별도 설치한 gs를 ExternalDependency로 감지해 쓰는 방식(LibreOffice/H2Orestart와 동일 패턴)으로 AGPL 배포 의무를 회피한다.\n\n2) PDF→DOCX/HTML(역변환): 기존 DocumentProvider의 LibreOffice 경로를 그대로 확장해 입력 매트릭스에 .pdf를 추가한다(soffice --convert-to docx/html/txt). 순수 .NET PdfPig는 'PDF→txt' 같은 빠른 무외부 경로와 텍스트 추출 폴백으로 사용한다.\n\n3) PDF/A·암호화·병합·분할: PDFsharp(MIT, in-process)를 기본 엔진으로, qpdf(Apache 2.0)를 무거운 작업(linearize/복잡 암호)의 외부 폴백으로 둔다. PDF/A 검증은 선택적 veraPDF 연동.\n\n전체적으로 '무료 기본(PDFsharp/qpdf/PDFium/LibreOffice) + 선택적 고급(Ghostscript/유료 SDK, 사용자 설치 감지)' 2계층 전략이 라이선스·포터블성·품질의 균형점이다.", + "libraries": [ + { + "name": "qpdf (CLI + libqpdf)", + "purpose": "PDF 구조 최적화/object stream 압축, linearize(웹 최적화), 암호화·복호화·비밀번호 변경, 페이지 분할·병합. 이미지 다운샘플링은 안 함(무손실/구조 위주).", + "license": "Apache License 2.0 (v7+ 재라이선스)", + "maturity": "production / 매우 활발 (v12.4.0, 2026-04; v12.3.2, 2026-01). Windows MSVC 32/64bit 빌드 제공", + "notes": "상업적 사용 완전 안전. 단일 정적 빌드 EXE로 번들 가능. .NET에서는 Process 호출 또는 libqpdf P/Invoke. 압축률은 이미지 미리샘플링이 없어 Ghostscript보다 낮음." + }, + { + "name": "Ghostscript (gswin64c) + Ghostscript.NET 래퍼", + "purpose": "-dPDFSETTINGS(/screen·/ebook·/printer·/prepress) 프리셋 + -dDownsampleColorImages/-dColorImageResolution 등 이미지 다운샘플링으로 최고 압축률. -dPDFA로 PDF/A 변환.", + "license": "AGPL v3 (또는 Artifex 상업 라이선스). Ghostscript.NET 래퍼는 MIT지만 네이티브 gs는 AGPL", + "maturity": "production / 활발 (10.x). Ghostscript.NET 래퍼 NuGet은 1.3.3로 다소 정체", + "notes": "주의: 포터블 EXE에 gs 동봉·배포 시 AGPL 전염. 상업 배포하려면 Artifex 상업 라이선스 필요. '사용자가 별도 설치한 gs를 감지'하는 ExternalDependency 패턴으로 쓰면 배포 의무 회피 가능." + }, + { + "name": "MuPDF / mutool (clean)", + "purpose": "mutool clean으로 폰트/이미지 스트림 압축·garbage collect. 렌더링/멀티포맷(EPUB/XPS) 강점.", + "license": "AGPL v3 (또는 Artifex 상업 라이선스)", + "maturity": "production / 활발 (1.27.x)", + "notes": "Ghostscript와 동일한 AGPL 이슈. 상업 포터블 배포에는 부적합(상업 라이선스 없으면). 굳이 채택 이유 없음 — qpdf로 대체 권장." + }, + { + "name": "PdfPig (UglyToad.PdfPig)", + "purpose": "순수 .NET PDF 읽기/텍스트·글자 위치(Letters)·단어(GetWords/NearestNeighbour)·이미지(GetImages) 추출, 기본 PDF 생성·병합. PDF→txt/구조 분석에 적합.", + "license": "Apache License 2.0", + "maturity": "production / 활발 (v0.1.14, 2026-03; PDFBox 포팅). netstandard2.0", + "notes": "외부 의존성 없는 in-process. 단, PDF→DOCX 레이아웃 재구성은 설계 범위 밖. 텍스트 추출·OCR 전처리·검색 폴백 용도." + }, + { + "name": "Docnet.Core", + "purpose": "PDFium(Apache 2.0) .NET Standard 래퍼. 페이지 렌더(비트맵), 텍스트/메타데이터 추출.", + "license": "MIT (네이티브 PDFium은 Apache 2.0)", + "maturity": "active / 안정 (유지보수 보통)", + "notes": "이미 프로젝트가 PDFium(PDFtoImage)를 쓰므로 중복 가능. 이미지 다운샘플링 기반 압축의 렌더 단계에 활용 가능." + }, + { + "name": "PDFsharp 6.x", + "purpose": "순수 .NET PDF 생성·수정·병합·분할, AES-128/256 암호화·복호화, PDF/A·PDF/UA 일부 지원.", + "license": "MIT (상업적 자유, 저작권 고지 유지 시)", + "maturity": "production / 활발 (6.2.4, .NET 9/10 호환)", + "notes": "외부 바이너리 0개 → 포터블 EXE에 최적. 병합·분할·암호화의 기본 in-process 엔진으로 1순위. 단 이미지 재압축/다운샘플링 기능은 약함." + }, + { + "name": "DocumentFormat.OpenXml + OpenXmlPowerTools(Clippit)", + "purpose": "DOCX in-process 생성/편집. PdfPig 추출 텍스트로 간단한 DOCX 조립 시 사용.", + "license": "MIT (둘 다)", + "maturity": "production / 활발 (OpenXml 3.5.1; PowerTools 4.5.x / Clippit 2026 유지)", + "notes": "고충실도 PDF→DOCX 변환 엔진은 아님. 텍스트만 담는 단순 DOCX 폴백 생성에 한정 사용." + }, + { + "name": "LibreOffice (soffice headless)", + "purpose": "PDF→DOCX/HTML/TXT 역변환, PDF/A export, 문서 상호변환. 이미 통합됨.", + "license": "MPL 2.0 / LGPL 3.0 (상업 사용 가능)", + "maturity": "production / 매우 활발", + "notes": "PDF→편집가능 변환의 현실적 무료 최선. 복잡/스캔 레이아웃은 충실도 한계. 기존 DocumentProvider 패턴 그대로 .pdf 입력 추가." + }, + { + "name": "veraPDF", + "purpose": "PDF/A·PDF/UA 적합성 검증(생성 아님). 변환 후 검증 단계.", + "license": "이중(GPLv3+ / MPLv2+), 둘 다 무료 사용 가능", + "maturity": "production / 활발 (PDF Association·OPF 유지, ISO 참조 검증기)", + "notes": "Java 기반 CLI. PDF/A 출력 보증이 필요할 때만 선택적 ExternalDependency로 연동." + }, + { + "name": "IronPDF / Nutrient(PSPDFKit) .NET SDK", + "purpose": "고충실도 PDF→Word/Excel/PPT 변환, HTML↔PDF, 압축 등 올인원 상업 SDK.", + "license": "상업(유료, 무료 아님)", + "maturity": "production / 활발", + "notes": "레이아웃 보존 PDF→DOCX가 사업적으로 꼭 필요할 때의 유료 대안. 라이선스 비용·런타임 크기 고려. 기본 무료 전략과 별도 옵션." + } + ], + "integrationNotes": "기존 추상화에 자연스럽게 끼워넣을 수 있다. 핵심은 IConverterProvider / ProviderCapability / ProviderRegistry(N×M 매트릭스)와 ExternalDependency 감지 패턴이 이미 LibreOffice·H2Orestart에서 검증되어 있다는 점이다.\n\n1) PDF 역변환(DOCX/HTML/TXT): 가장 저비용 통합. DocumentProvider.cs의 Inputs 배열에 \\\".pdf\\\"를 추가하고 RouteAsync에 PDF 분기를 넣으면 된다. .pdf→{docx,html,txt}는 그대로 SofficeConvertAsync 재사용(soffice는 PDF 입력을 Draw로 열어 변환). .pdf→txt 무외부 폴백은 PdfPig로 page.Text를 모아 쓰는 별도 경로를 추가하면 LibreOffice 없이도 동작. .pdf→md는 기존 패턴대로 pdf→html(soffice)→ReverseMarkdown 체인으로 처리. RoadmapNote/ExternalDependencies는 기존 LibreOffice 의존성 항목 그대로 재사용.\n\n2) PDF 압축/유틸리티(압축·암호화·병합·분할·PDF/A): 같은 \\\".pdf\\\"→\\\".pdf\\\" 변환은 현 ProviderRegistry가 (input==output)을 ConversionEngine 단계에서 걸러낼 가능성이 높으므로, '동일 확장자 변환'을 옵션 파라미터로 구분하는 새 PdfToolProvider(예: Id \\\"pdf-tools\\\")를 신설하는 편이 깔끔하다. ConvertOptions에 PdfCompress(레벨: Light=PDFsharp/qpdf 구조 최적화, Strong=PDFium 렌더+ImageMagick 재인코딩, Max=Ghostscript /screen) 같은 옵션 그룹을 추가한다(기존 Jpeg/Webp/Avif/Tiff/PdfRender 옵션 그룹과 동일한 record 스타일). 압축 강도에 따라 내부적으로 (a) PDFsharp in-process, (b) PDFium+ImageMagick 하이브리드(PdfProvider의 렌더 로직과 ApplyEncoding 재사용 가능), (c) gs CLI 폴백으로 디스패치.\n\n3) 라이선스/배포 경계: PDFsharp·PdfPig·qpdf는 번들 가능(MIT/Apache). Ghostscript·MuPDF·veraPDF·LibreOffice는 절대 EXE에 정적 링크/동봉하지 말고, CheckAvailabilityAsync에서 ExternalToolDetector로 사용자 설치본을 감지해 ProviderAvailability.NotReady(미설치 시) 또는 Ready로 분기한다(현 DocumentProvider.CheckAvailabilityAsync와 동일 패턴). 이렇게 하면 고급 압축/PDF-A는 '사용자가 직접 설치한 도구로만' 동작하여 AGPL 배포 전염을 구조적으로 회피한다. ExternalDependency.DownloadUrl에 gs/veraPDF 다운로드 링크를 넣어 안내.\n\n4) qpdf 통합 방식: SofficeConvertAsync와 동일한 ProcessStartInfo/ArgumentList 패턴으로 QpdfRunner 헬퍼를 하나 만들어 압축(--object-streams=generate --compress-streams=y --recompress-flate), 암호화(--encrypt), 복호화(--decrypt), 분할(--split-pages), 병합(--pages)을 공통 호출. libqpdf P/Invoke는 복잡도가 높아 CLI 호출이 통합 비용 대비 합리적.", + "risks": [ + "Ghostscript/MuPDF AGPL 전염: 단일 포터블 EXE에 gs/mutool 바이너리를 동봉해 배포하면 전체 애플리케이션 소스 공개 의무가 생길 수 있다. 상업 배포 시 반드시 '사용자 설치본 감지' 방식으로만 쓰거나 Artifex 상업 라이선스를 구매해야 한다.", + "PDF→DOCX 충실도 한계: LibreOffice·PdfPig 등 무료 경로는 표·다단·스캔 PDF에서 레이아웃이 깨지기 쉽다. 사용자 기대치를 UI에 명확히 표기(텍스트 위주 복원)하지 않으면 품질 클레임 위험.", + "이미지 다운샘플링 압축의 부작용: PDFium 렌더+ImageMagick 재인코딩 방식은 강한 압축률을 내지만 텍스트 선택성/벡터를 잃고 전 페이지가 래스터화된다. 검색 가능한 PDF가 깨지므로 '강한/최대' 모드는 별도 옵션으로 분리하고 경고 필요.", + "qpdf 압축률 기대치: qpdf는 이미지 다운샘플링을 안 해 '구조 최적화'만으로는 사용자가 기대하는 수십% 감소가 안 나올 수 있다. 압축 레벨별 실제 효과를 벤치마크로 검증 후 UI 문구를 맞춰야 함.", + "외부 도구 버전·경로 다양성: LibreOffice/gs/veraPDF의 설치 경로·버전 차이로 인자 비호환(예: soffice 7.3 이상에서만 PDF export 파라미터화)이 발생할 수 있어 ExternalToolDetector의 버전 체크/오류 처리 강화 필요.", + "PDF/A 보증: -dPDFA나 soffice export가 '시도'는 하지만 실제 적합성은 보장 못 한다. veraPDF 검증을 붙이지 않으면 'PDF/A 변환' 라벨이 사실과 다를 수 있는 법적/신뢰 리스크." + ], + "sources": [ + "https://ghostscript.com/licensing/", + "https://ghostscript.readthedocs.io/en/latest/VectorDevices.html", + "https://ghostscript.com/blog/optimizing-pdfs.html", + "https://www.nuget.org/packages/Ghostscript.NET", + "https://github.com/qpdf/qpdf", + "https://qpdf.readthedocs.io/en/stable/cli.html", + "https://en.wikipedia.org/wiki/MuPDF", + "https://mupdf.readthedocs.io/en/latest/tools/mutool-clean.html", + "https://artifex.com/blog/choosing-between-ghostscript-and-mupdf", + "https://github.com/UglyToad/PdfPig", + "https://uglytoad.github.io/PdfPig/", + "https://github.com/GowenGit/docnet", + "https://docs.pdfsharp.net/PDFsharp/Topics/PDF-Features/Encryption.html", + "https://www.nuget.org/packages/PDFSharp", + "https://github.com/ststeiger/PdfSharpCore", + "https://www.nuget.org/packages/documentformat.openxml", + "https://sergey-tihon.github.io/Clippit/", + "https://github.com/matteosecli/pdf2archive", + "https://verapdf.org/", + "https://www.nutrient.io/blog/convert-pdf-to-word/", + "https://www.pikepdf.org/", + "https://pikepdf.readthedocs.io/" + ] + }, + { + "key": "hwp", + "topic": "HWP/HWPX ↔ DOCX/PDF/HTML 양방향 변환 — Everything2Everything (.NET 9 / WPF) 통합 리서치", + "keyFindings": [ + "**정방향(HWP/HWPX → PDF/이미지)은 이미 동작 중이며 최선의 경로다.** 프로젝트는 HwpxProvider.cs에서 LibreOffice headless(`soffice --convert-to pdf`) + H2Orestart 확장으로 PDF 변환 후 PdfProvider로 위임한다. H2Orestart는 2026년에도 활발히 유지보수 중(v0.7.12, 2026-05-10 릴리스). 이 구조는 옳다.", + "**H2Orestart는 GPLv3 + Java 의존이다.** 라이선스가 GPLv3(LGPL 아님)이므로 EXE에 정적/동적 번들하면 전염성(copyleft) 문제가 생긴다. 단, 현재처럼 별도 .oxt 확장으로 사용자가 LibreOffice에 설치 → 별도 프로세스(soffice.exe)를 외부 호출하는 방식이면 E2E 본체 코드와 라이선스가 분리되어 상업 배포에 안전하다. 추가로 H2Orestart는 내부적으로 JRE/JDK가 필요하다(LibreOffice의 Java 통합 활성화 필수). LibreOffice 빌드에 따라 별도 JRE 설치/설정이 필요할 수 있어 '단일 포터블 EXE' 자족성에 마이너스 요인.", + "**H2Orestart는 import(읽기) 전용이다 — 저장은 ODT로만 가능, HWP/HWPX 쓰기 불가.** 따라서 LibreOffice 경로로는 역방향(DOCX→HWP) 출력이 불가능하다. LibreOffice에 HWP export 필터가 없다.", + "**역방향 HWP/HWPX '쓰기'는 hwplib(HWP) / hwpxlib(HWPX)만이 현실적 오픈소스 해법이다.** 둘 다 neolord0 작성, Apache-2.0(상업 친화적), 2026년까지 활발(hwplib v1.1.5+ 2026-02-04, hwpxlib v1.0.8 2025-11-14). hwplib은 BlankFileMaker/HWPWriter로 빈 HWP 생성·텍스트·표·이미지 삽입까지 가능. **그러나 둘 다 Java 라이브러리이며 '포맷 변환' 기능이 없다** — DOCX를 파싱해서 hwplib 객체 모델로 매핑하는 변환 로직을 직접 구현해야 한다. 고품질 레이아웃 보존 역변환은 사실상 새 변환 엔진을 작성하는 수준이라 비용이 매우 크다.", + "**pyhwp/hwp5(hwp5html)는 AGPLv3 + 반쯤 정체 상태다.** 마지막 안정 릴리스 0.1b15(2020-05-30). HWP5→HTML/ODT/txt 추출은 되지만 HWPX 미지원이고, AGPLv3는 SaaS·배포에 전염성이 강해 상업 배포기에 부적합. 이미 LibreOffice 경로가 있으므로 채택 이점 없음.", + "**hwp.js는 사실상 abandoned다(Apache-2.0, v0.0.3 2020-10, 2020년 유지보수 중단 공지).** 브라우저 HWP 뷰어/파서로 WebView2와 결합 가능성은 있으나, 미완성·구버전이라 신뢰성 낮음. 동일 저자(hahnlee)의 Rust 계열(hwp-rs)과 신생 Rust 생태계(openhwp, HwpForge, hwpers, unhwp)가 더 활발하지만 .NET 직접 연동은 FFI 작업이 추가로 필요.", + "**한컴 공식 경로(HwpAutomation COM / 한글 SDK / Docs Converter)는 품질이 가장 높지만 상업 라이선스 유료다.** HwpCtrl ActiveX/COM은 개인·비상업은 무료이나 '판매되는 솔루션'에 쓰려면 한컴 승인 + 별도 라이선스 필요(contact_sdk@hancom.com). 한글 SDK는 한글 프로그램 설치 없이 HWP/HWPX↔HTML/PDF/ODF 변환 및 약 1,000개 기능 제공, .NET에서 호출 가능하나 유료. 역방향(→HWP)을 네이티브 품질로 보장하는 유일한 길이지만 비용·배포(런타임 동봉) 제약이 큼.", + "**LibreOffice headless 한글 변환 안정성/폰트 이슈가 존재한다.** Old Hangul(옛한글)·제주어 음절, Noto Sans 일부 글리프가 PDF에서 깨지는 보고가 있고, 시스템에 한글 폰트(맑은 고딕/함초롬바탕 등)가 없으면 폰트 치환으로 레이아웃이 틀어진다. 레거시 HWP는 `--infilter=\"Hwp2002_File\"` 지정이 도움이 됨. 복잡한 표·다단·머리말은 레이아웃 손실 가능." + ], + "recommendedApproach": "단일 포터블 EXE / 상업 라이선스 선호라는 제약을 고려하면 **계층적 전략**이 최적이다.\n\n1) 정방향(HWP·HWPX → PDF/PNG/JPG 등): **현재 HwpxProvider의 LibreOffice + H2Orestart 경로를 유지·강화**한다. 이미 구현되어 있고 H2Orestart가 2026년에도 활발하다. 강화 포인트: (a) `--infilter=\\\"Hwp2002_File\\\"`를 .hwp에 한해 추가 지정해 import 필터 명시, (b) 한글 폰트(함초롬·맑은 고딕) 번들 또는 폰트 누락 감지 경고, (c) JRE 미설치 시 명확한 안내. H2Orestart는 GPLv3이지만 '사용자가 LibreOffice에 설치한 확장을 외부 프로세스로 호출'하는 분리 모델이라 E2E 본체 라이선스에 전염되지 않는다.\n\n2) 정방향 HWPX → DOCX/HTML/TXT '편집 가능 포맷' 출력: PDF 외에 DOCX/ODT/HTML 출력 수요가 있으면, **LibreOffice의 `--convert-to docx`/`html`/`txt`** 를 동일 파이프라인에서 노출하면 된다(soffice가 ODT 경유로 DOCX/HTML export 지원). 이게 hwp5html/pyhwp(AGPL) 채택보다 라이선스·유지보수 면에서 우수하다. HwpxProvider의 HwpOutputs 배열에 .docx/.html/.txt/.odt를 추가하고 변환 분기만 PDF 대신 해당 포맷으로 바꾸면 즉시 매트릭스가 확장된다.\n\n3) 역방향(DOCX/PDF/HTML → HWP·HWPX): **현실적으로 고품질 무료 OSS 경로는 없다.** 단기적으로는 'Coming Soon' 또는 미지원으로 두고, 정말 필요하면 두 가지 옵션 — (a) **저품질 수용형**: HTML/DOCX 텍스트·표를 추출해 hwpxlib(Apache-2.0)로 최소 구조의 HWPX를 생성(레이아웃 보존 낮음, Java 브리지 필요), (b) **고품질 유료형**: 한컴 한글 SDK/Docs Converter 상업 라이선스로 별도 Provider 구성. 무료 단일 EXE 원칙을 우선한다면 역방향은 '로드맵'으로 남기고, HWPX 출력만 hwpxlib 기반 베스트에포트로 제공하는 것을 권한다.\n\n요약: 정방향은 LibreOffice 단일 엔진으로 PDF·이미지·DOCX·HTML까지 모두 커버(추가 의존성 0). 역방향은 무료로는 베스트에포트 HWPX만, 네이티브 품질은 유료 한컴 SDK로 분리.", + "libraries": [ + { + "name": "H2Orestart (ebandal)", + "purpose": "LibreOffice 확장. HWP/HWPX를 LibreOffice에서 import(읽기) → ODT/PDF/DOCX/HTML로 headless 변환. 정방향 변환의 핵심 엔진.", + "license": "GPLv3", + "maturity": "active (v0.7.12, 2026-05-10)", + "notes": "Java 작성, JRE 필요. import 전용 — HWP/HWPX 쓰기 불가, 저장은 ODT만. 별도 .oxt 확장+외부 프로세스 호출이라 본체 코드에 GPL 전염 안 됨. 현재 프로젝트가 이미 사용 중." + }, + { + "name": "hwplib (neolord0 / kr.dogfoot)", + "purpose": "HWP 5.0 바이너리 읽기 AND 쓰기(BlankFileMaker로 새 파일 생성, HWPWriter로 저장). 역방향(→HWP) 출력의 유일한 무료 경로.", + "license": "Apache-2.0", + "maturity": "active (v1.1.5+, 2026-02-04)", + "notes": "Java 전용. 포맷 변환 기능 없음(DOCX/PDF/HTML export 불가) — 변환 매핑 로직을 직접 구현해야 함. 암호화 HWP 미지원. .NET에서 쓰려면 JVM 브리지(IKVM 또는 자식 프로세스 JAR) 필요." + }, + { + "name": "hwpxlib (neolord0)", + "purpose": "HWPX(OWPML, ZIP+XML) 읽기/쓰기. HWPX 직접 생성·수정. 역방향 HWPX 출력 후보.", + "license": "Apache-2.0", + "maturity": "active (v1.0.8, 2025-11-14)", + "notes": "Java 전용(Java 7+). 다른 포맷으로의 변환 기능은 없음(읽기/쓰기 라이브러리). HWPX가 ZIP+XML이라 사실 System.IO.Compression + XML로 .NET에서 직접 파싱·생성도 가능하나, 스키마(OWPML, KS X 6101)가 방대해 라이브러리 사용이 현실적." + }, + { + "name": "pyhwp / hwp5 (hwp5html, mete0r)", + "purpose": "HWP5 파서. HWP5 → HTML/ODT/txt 추출.", + "license": "AGPLv3+", + "maturity": "semi-abandoned (마지막 안정 0.1b15, 2020-05-30)", + "notes": "AGPLv3 전염성으로 상업 배포·SaaS에 부적합. HWPX 미지원. Python 의존. LibreOffice 경로 대비 채택 이점 없음." + }, + { + "name": "hwp.js (hahnlee)", + "purpose": "웹 기술 기반 HWP 뷰어/파서(브라우저 렌더링).", + "license": "Apache-2.0", + "maturity": "abandoned (v0.0.3, 2020-10, 2020년 유지보수 중단 공지)", + "notes": "미완성, 배포용 문서 파싱 실패 보고. HWPX 미지원. WebView2 결합은 이론상 가능하나 신뢰성 낮음. 동저자 Rust 후속작(hwp-rs)이 더 활발." + }, + { + "name": "Rust 생태계 (hwp-rs, openhwp, HwpForge, hwpers, unhwp)", + "purpose": "HWP/HWPX 파싱·렌더·Markdown 추출. openhwp는 읽기/쓰기 지향.", + "license": "대부분 MIT/Apache-2.0 (크레이트별 상이, 확인 필요)", + "maturity": "active/beta (2025-2026 신생, 성숙도 편차 큼)", + "notes": ".NET 연동에 FFI(C ABI) 래핑 필요. 단일 EXE에 네이티브 .dll 동봉 형태로 통합 가능하나 현재로선 미성숙·리스크. 향후 옵션." + }, + { + "name": "한컴 한글 SDK / HwpAutomation(COM) / Docs Converter", + "purpose": "한컴 네이티브 변환. HWP/HWPX ↔ HTML/PDF/ODF/DOCX, 약 1,000개 한글 기능. 역방향(→HWP) 네이티브 품질 보장.", + "license": "상업(유료, 별도 라이선스). ActiveX/COM은 개인·비상업만 무료", + "maturity": "production (한컴 공식)", + "notes": ".NET/C#에서 COM 또는 SDK 호출 가능. 가장 높은 품질·유일한 고품질 역변환 경로지만 유료 + 한글/SDK 런타임 동봉 필요로 '무료 단일 포터블 EXE'와 상충. contact_sdk@hancom.com 라이선스 문의." + } + ], + "integrationNotes": "**Provider/Registry 추상화에 끼우는 구체안 (D:\\\\workspace\\\\Everything2Everthing\\\\src\\\\Everything2Everything.Core)**\n\n현재 HwpxProvider.cs는 IConverterProvider를 구현하고, Capability.SupportedConversions = PairsFromMatrix(HwpInputs, HwpOutputs)로 (입력×출력) 쌍을 선언, ProviderRegistry가 (Input,Output)→Provider 딕셔너리로 라우팅한다. DocxProvider가 동일 패턴(LibreOffice/Word로 PDF→PdfProvider 위임)이라 이를 그대로 따른다.\n\n1) **정방향 출력 포맷 확장 (가장 비용 낮고 효과 큼)**: HwpxProvider의 `HwpOutputs` 배열에 `.docx`, `.html`, `.odt`, `.txt`를 추가한다. ConvertAsync의 분기에서 outExt가 이미지/PDF가 아니면 ConvertWithLibreOfficeAsync의 `--convert-to pdf`를 해당 필터(`docx:\\\"MS Word 2007 XML\\\"`, `html:HTML (StarWriter)`, `txt:Text` 등)로 파라미터화한다. 현재 메서드는 pdf 하드코딩이므로 target 포맷·확장자를 인자로 받도록 일반화하면 된다. .hwp 입력 시 `--infilter=\\\"Hwp2002_File\\\"`를 ArgumentList에 조건부 추가하면 import 안정성이 오른다. 이렇게 하면 HWP/HWPX → DOCX/HTML/TXT/ODT/PDF/이미지 매트릭스가 한 Provider·한 외부 의존(LibreOffice+H2Orestart)으로 완성된다.\n\n2) **availability 메시지 보강**: CheckAvailabilityAsync에 JRE 미설치 시 안내를 추가(H2Orestart는 LibreOffice의 Java 통합이 꺼져 있으면 동작 안 함). ExternalToolDetector에 한글 폰트(함초롬/맑은 고딕) 존재 여부 체크를 추가해 폰트 누락 시 ProviderAvailability에 경고 Reason을 실어주면 레이아웃 깨짐 사고를 예방.\n\n3) **역방향 Provider 신설(선택)**: 별도 `HwpReverseProvider`(또는 HwpxProvider에 입력 .docx/.html/.pdf → 출력 .hwpx 쌍 추가)를 만들되, 무료 경로는 hwpxlib 기반 베스트에포트로 한정. .NET↔Java 브리지가 필요하므로 (a) IKVM.NET으로 hwpxlib JAR을 .NET 어셈블리화, 또는 (b) 번들한 JRE로 hwpxlib 래퍼 JAR을 자식 프로세스 실행(현재 LibreOffice를 외부 프로세스로 부르는 패턴과 동일해 일관적). 단일 EXE 원칙상 (b)가 기존 외부-프로세스 모델과 잘 맞는다. Capability.Status는 ComingSoon 또는 RequiresExternal로 두고, RoadmapNote에 '레이아웃 보존 제한적, 고품질은 한컴 SDK 필요' 명시.\n\n4) **유료 고품질 역변환은 별도 옵셔널 Provider**: 한컴 SDK가 설치·라이선스된 환경에서만 활성화되는 `HancomSdkProvider`를 COM(dynamic)로 구현(DocxProvider의 ConvertWithWordCom이 Type.GetTypeFromProgID로 Word COM을 dynamic 호출하는 패턴과 동일). CheckAvailabilityAsync에서 ProgID/SDK DLL 존재로 가용성 판단, 없으면 NotReady로 빠지므로 무료 배포본에는 영향 없음.\n\n라이선스 격리 원칙: GPLv3(H2Orestart)·Java 라이브러리(hwplib/hwpxlib)·한컴 SDK 모두 '외부 프로세스 또는 사용자 설치 확장'으로 분리 호출하여 E2E 본체(상업 배포)와 라이선스 경계를 유지한다. 본체에 GPL/AGPL 코드를 링크하지 않는다.", + "risks": [ + "H2Orestart는 GPLv3 — 라이브러리로 링크/번들하면 copyleft 전염. 반드시 별도 .oxt 확장 + soffice.exe 외부 프로세스 호출로 분리 유지해야 상업 배포 안전. EXE에 H2Orestart 코드를 포함하지 말 것.", + "LibreOffice headless 한글 변환은 폰트 의존적 — 함초롬/맑은 고딕 등이 시스템에 없으면 폰트 치환으로 레이아웃·줄바꿈이 틀어지고, 옛한글/일부 글리프는 PDF에서 깨질 수 있음. '단일 포터블 EXE'라도 LibreOffice·H2Orestart·JRE·한글 폰트가 환경에 없으면 변환 불가 — 진정한 자족 EXE가 아니라 외부 의존 체인이 김.", + "H2Orestart 동작에 JRE/JDK 필요. LibreOffice의 Java 통합이 비활성/미설치면 HWP import 실패. 사용자 환경 편차로 인한 실패율 존재.", + "역방향(DOCX/PDF/HTML → HWP/HWPX)은 무료로는 고품질 불가. hwplib/hwpxlib은 '쓰기'는 되나 '변환' 매핑 로직을 직접 구현해야 하며 레이아웃 보존이 매우 어려움 — 사실상 새 변환 엔진 개발 비용. PDF→HWP는 더 비현실적(레이아웃 재구성 필요).", + "hwplib/hwpxlib은 Java 전용 — .NET 통합에 IKVM 또는 자식 프로세스 JAR + 번들 JRE 필요로 배포 용량·복잡도 증가. IKVM는 .NET 9 호환성·유지보수 상태를 별도 검증해야 함.", + "한컴 SDK/COM은 상업 라이선스 유료 + 한글/SDK 런타임 동봉 필요 — 무료 단일 EXE 컨셉과 정면 충돌. 라이선스 비용·계약 협의(contact_sdk@hancom.com) 필요.", + "pyhwp(AGPLv3)·hwp.js(abandoned) 모두 채택 부적합 — AGPL 전염성, 유지보수 중단. 후보에서 제외 권장.", + "HWPX를 .NET에서 ZIP+XML로 직접 파싱·생성하는 것은 이론상 가능하나 OWPML(KS X 6101) 스키마가 방대해 부분 지원에 그치기 쉬움. 자체 구현은 유지보수 부담 큼." + ], + "sources": [ + "https://github.com/ebandal/H2Orestart", + "https://extensions.libreoffice.org/en/extensions/show/27504", + "https://github.com/neolord0/hwplib", + "https://github.com/neolord0/hwpxlib", + "https://mvnrepository.com/artifact/kr.dogfoot/hwplib", + "https://github.com/mete0r/pyhwp", + "https://pyhwp.readthedocs.io/en/latest/converters.html", + "https://github.com/hahnlee/hwp.js", + "https://github.com/hahnlee/hwp-rs", + "https://github.com/openhwp/openhwp", + "https://developer.hancom.com/hwpautomation", + "https://developer.hancom.com/docsconverter/guide/api", + "https://www.hancom.com/product/sdk/hwpSdk", + "https://forum.developer.hancom.com/t/topic/2879", + "https://help.libreoffice.org/latest/en-US/text/shared/guide/convertfilters.html", + "https://bugs.documentfoundation.org/show_bug.cgi?id=128907", + "https://ask.libreoffice.org/t/korean-fonts-are-not-rendered-during-document-conversion/29178", + "https://packages.debian.org/sid/text/libreoffice-h2orestart", + "https://manpages.debian.org/testing/libreoffice-common/unopkg.1.en.html", + "https://github.com/chrisryugj/kordoc" + ] + }, + { + "key": "plugin-arch", + "topic": ".NET 9 확장 가능 변환기 플러그인 아키텍처 — Everything2Everything용 best practice 리서치", + "keyFindings": [ + "기존 추상화는 이미 잘 설계된 정적 매트릭스다. IConverterProvider(Capability + CheckAvailabilityAsync + ConvertAsync), ProviderRegistry((input,output) 쌍 사전), ProviderCapability(ConversionPair 리스트)로 구성. 단, 현재 ProviderRegistry는 생성자에서 IEnumerable를 받아 컴파일 타임에 고정된다 — 동적 등록/언로딩 진입점이 없다. 확장성의 첫 단계는 '런타임에 Provider를 추가하는 RegisterDynamic / Rebuild' 메서드 도입이다.", + "Microsoft 공식 .NET 플러그인 튜토리얼(2026-02 갱신)의 핵심 패턴: (1) 공유 계약 어셈블리(PluginBase)를 별도 프로젝트로 분리, (2) 플러그인 프로젝트는 계약을 false + runtime로 참조해 계약 DLL 중복 로드를 방지(이게 빠지면 같은 인터페이스가 서로 다른 타입으로 인식되어 캐스팅 실패), (3) 플러그인 csproj에 true를 넣어 의존성을 출력으로 복사, (4) 플러그인당 별도 AssemblyLoadContext(ALC) + AssemblyDependencyResolver로 의존성 충돌 격리. Load 오버라이드에서 계약 어셈블리는 null 반환해 default ALC로 fall back시켜야 타입 동일성이 유지된다.", + "보안/신뢰 경계에 대한 Microsoft의 명시적 경고: '신뢰할 수 없는 코드는 신뢰된 .NET 프로세스에 안전하게 로드할 수 없다. 보안/안정성 경계가 필요하면 OS 또는 가상화 플랫폼이 제공하는 기술을 사용하라.' 즉 in-process ALC는 '버전 격리/핫리로드'용이지 '샌드박스'가 아니다. 진짜 격리(서드파티 untrusted 플러그인, 네이티브 크래시 차단)는 out-of-process 호스트(별도 .exe + IPC) 또는 Windows AppContainer/Job Object로만 달성된다.", + "언로딩(hot-reload)은 협조적(cooperative)이며 footgun이 많다. collectible ALC를 써도 (a) 정적 캐시(직렬화기/DI 컨테이너가 플러그인 타입 캐시), (b) 해지 안 한 이벤트 핸들러, (c) 살아있는 Timer/Task/Thread, (d) 플러그인 타입이 host-scope 인프라로 누출되면 절대 언로드되지 않는다. 검증은 WeakReference + 반복 GC로만 가능. 네이티브 라이브러리(ImageMagick/PDFium/LibreOffice)는 ALC 경계를 무시하므로 in-process 언로딩 대상에서 제외해야 한다.", + "MEF2(System.Composition)는 죽지 않았고 활발히 유지보수 중이다 — 최신 10.0.7(2025), .NET 9/10과 호환(.NET Core 2.0/Standard 2.0 타겟). 다만 이 프로젝트에는 권장하지 않는다: MEF은 런타임 리플렉션 기반 attribute 스캔이라 (a) 단일 포터블 EXE/AOT 친화성이 낮고, (b) 7개뿐인 1급 Provider에는 과도하다. 동적 서드파티 플러그인을 정말 열 때만 가치가 있다.", + "Source generator 방식이 이 프로젝트에 가장 적합한 '자동 등록' 수단이다. AutoRegisterInject(v1.4.1, MIT, netstandard2.0, .NET 9 호환)가 [RegisterScoped]/[RegisterSingleton] 등 attribute로 DI 등록 코드를 컴파일 타임에 생성 — 리플렉션 0, AOT 친화. 1급(in-box) Provider들은 source generator로 자동 등록하고, 동적 외부 플러그인만 ALC로 로드하는 하이브리드가 이상적.", + "외부 도구 어댑터 패턴은 이미 코드에 존재한다(DocumentProvider.SofficeConvertAsync가 LibreOffice를 ProcessStartInfo로 감쌈). 이를 일반화한 'ExternalToolProvider' 베이스 클래스 + JSON manifest로 FFmpeg/Ghostscript/Pandoc을 균일 인터페이스로 흡수할 수 있다. CliWrap(v3.10.1, MIT, 2026-03 갱신)이 ProcessStartInfo 보일러플레이트(스트림 리다이렉트/취소/exit code/진행률)를 대체할 fluent 래퍼로 강력 추천.", + "라이선스 함정 2가지가 단일 포터블 EXE 배포에 치명적이다. (1) Ghostscript: AGPL/상업 듀얼 라이선스 — 닫힌 소스 포터블 EXE에 번들하려면 Artifex 상업 라이선스 필요. PDF 처리는 이미 가진 PDFium으로 대체하는 게 안전. (2) FFmpeg: libx264/libx265 등 인기 코덱은 GPL이라 번들 시 앱 전체가 GPL 전염. 반드시 --enable-gpl 없이 빌드한 LGPL 빌드를 '동적 링크'(별도 exe 호출/별도 DLL)로 사용하고 소스 오퍼/저작권 고지를 포함해야 함. Pandoc도 GPL이라 같은 '동적 = 별도 프로세스 호출' 원칙 적용. LibreOffice(MPL-2.0)와 CliWrap/AutoRegisterInject(MIT)는 번들 안전." + ], + "recommendedApproach": "단일 포터블 EXE/WPF/Windows 11 제약에서는 '3계층 하이브리드'가 최적이다. (계층 1 — In-box Provider, 지금처럼) 7개 1급 Provider는 컴파일 타임에 고정. 단 등록 보일러플레이트를 줄이려면 AutoRegisterInject source generator를 도입해 [RegisterE2EProvider] attribute만 붙이면 자동 등록되게 한다. 리플렉션 없고 AOT/트리밍 친화라 단일 EXE에 이상적. (계층 2 — 외부 도구 어댑터 Provider) 이미 있는 SofficeConvertAsync 패턴을 'ExternalToolProvider' 추상 베이스로 일반화한다. 각 외부 도구(LibreOffice/FFmpeg/Ghostscript/Pandoc)는 코드가 아니라 JSON manifest(tool id, 실행 파일 탐지 경로, 입력/출력 확장자 매트릭스, argument 템플릿, 성공 판정 규칙)로 선언하고, 런타임에 manifest를 읽어 ProviderRegistry에 합성한다. 프로세스 실행은 CliWrap으로 통일(취소/진행률/exit code 처리 일원화). 이렇게 하면 새 도구 추가가 '코드 빌드 없이 manifest + 탐지기 추가'로 끝난다. 외부 프로세스 호출 자체가 천연 격리 경계라서 도구가 크래시해도 앱은 살아있다(가장 가성비 좋은 안정성/보안 경계). (계층 3 — 진짜 동적 플러그인, 필요할 때만) 서드파티가 .NET DLL Provider를 끼우는 시나리오가 생기면 그때 collectible ALC + AssemblyDependencyResolver를 도입한다. 단 이건 untrusted 격리가 아님을 명심하고, 신뢰할 수 없는 플러그인은 out-of-process 워커(별도 exe)로 돌린다. ImageMagick/PDFium/WebView2 같은 네이티브 의존 Provider는 절대 언로드 대상으로 만들지 않는다(네이티브가 ALC 경계를 무시). 결론: 지금 당장 필요한 건 계층 1의 source generator 자동 등록과 계층 2의 manifest 기반 외부 도구 어댑터다. ALC 동적 로딩은 '서드파티 플러그인 마켓'을 실제로 열 때까지 미루는 게 복잡도 대비 합리적이다.", + "libraries": [ + { + "name": "System.Runtime.Loader.AssemblyLoadContext + AssemblyDependencyResolver (BCL 내장)", + "purpose": "플러그인 DLL을 격리된 컨텍스트에 동적 로드/언로드, 의존성 충돌 해결. .NET 공식 플러그인 메커니즘", + "license": "MIT (.NET 런타임)", + "maturity": "production (BCL 내장, .NET Core 3.0~.NET 9/10 안정)", + "notes": "collectible 옵션으로 hot-reload 가능하나 언로드는 협조적이라 footgun 많음. untrusted 코드 샌드박스 아님 — 격리는 OS/가상화로. 네이티브 의존 Provider에는 부적합. 계층 3에서만 필요" + }, + { + "name": "AutoRegisterInject", + "purpose": "attribute 기반으로 IConverterProvider 구현체를 컴파일 타임에 DI 자동 등록 (리플렉션/스캔 제거)", + "license": "MIT", + "maturity": "active (v1.4.1, netstandard2.0, .NET 9 호환)", + "notes": "단일 포터블 EXE/AOT/트리밍 친화. 계층 1 in-box Provider 자동 등록에 권장. 대안: Jab(소스 제너레이터 DI 컨테이너, MIT) 또는 직접 만든 소형 source generator" + }, + { + "name": "System.Composition (MEF2)", + "purpose": "attribute 기반 런타임 플러그인 발견/합성 (Export/Import)", + "license": "MIT", + "maturity": "active (최신 10.0.7, 2025, .NET 9 호환)", + "notes": "'죽었다'는 오래된 오해 — 현재도 유지보수됨. 그러나 런타임 리플렉션 의존이라 AOT/단일 EXE 친화성 낮고 7개 Provider엔 과함. 본 프로젝트엔 비권장, 동적 서드파티 플러그인 다수일 때만 고려" + }, + { + "name": "CliWrap", + "purpose": "외부 CLI 도구(LibreOffice/FFmpeg/Ghostscript/Pandoc)를 fluent하게 실행 — 인자/스트림/취소/진행률/exit code 일원화", + "license": "MIT", + "maturity": "production (v3.10.1, 2026-03 갱신, 활발)", + "notes": "현재 ProcessStartInfo 보일러플레이트(SofficeConvertAsync 등)를 대체. 어댑터 Provider 베이스의 실행 엔진으로 강력 추천. net6.0/netstandard2.0 타겟이라 .NET 9 OK" + }, + { + "name": "LibreOffice (soffice)", + "purpose": "문서(DOCX/HTML/TXT/HWP) 변환 외부 엔진 (이미 사용 중)", + "license": "MPL-2.0", + "maturity": "production", + "notes": "MPL이라 별도 프로세스 호출 시 번들/상업 사용 안전. H2Orestart로 HWP 입력 지원" + }, + { + "name": "FFmpeg", + "purpose": "오디오/비디오 변환 어댑터 Provider 후보", + "license": "LGPL-2.1+ (코어) / 일부 코덱 GPL / nonfree", + "maturity": "production", + "notes": "반드시 --enable-gpl 없이 빌드한 LGPL 빌드를 별도 프로세스로 호출(동적 사용). libx264/x265(GPL) 포함 빌드를 번들하면 앱 전체 GPL 전염. 저작권 고지 + 소스 오퍼 필요" + }, + { + "name": "Ghostscript", + "purpose": "PDF/PostScript 변환 어댑터 후보", + "license": "AGPL-3.0 / 상업 듀얼 (Artifex)", + "maturity": "production", + "notes": "닫힌 소스 포터블 EXE 번들엔 상업 라이선스 필수 — 위험. 가능하면 기존 PDFium으로 PDF 처리 대체 권장. 꼭 필요하면 사용자가 직접 설치한 외부 도구로만 탐지(번들 안 함)" + }, + { + "name": "Pandoc", + "purpose": "마크다운/문서 포맷 광범위 변환 어댑터 후보", + "license": "GPL-2.0+", + "maturity": "production", + "notes": "GPL이라 별도 프로세스 호출(동적)로만 사용. EXE에 정적 결합/번들 시 GPL 전염 위험. 사용자 설치 도구로 탐지하는 방식이 안전" + }, + { + "name": "System.Reflection.Metadata", + "purpose": "어셈블리를 실제 로드하지 않고 PE/메타데이터만 읽어 플러그인 manifest/attribute를 스캔 (reflection-free discovery)", + "license": "MIT (.NET 런타임)", + "maturity": "production", + "notes": "의존성 누락된 플러그인도 파일 락 없이 안전 스캔. 계층 3 ALC 도입 시 발견 단계에 사용" + } + ], + "integrationNotes": "현재 추상화에 끼워넣는 구체 단계: (1) ProviderRegistry를 '닫힌 생성자'에서 '증분 등록 가능' 구조로 확장. 현재 생성자가 한 번에 _byPair 사전을 빌드하므로, 동일 인덱싱 로직을 private void Index(IConverterProvider) 로 빼고 public void Register(IConverterProvider)/RegisterRange/Rebuild를 추가한다. 이걸로 source generator 등록 + manifest 기반 어댑터 등록 둘 다 같은 진입점을 쓴다. (2) 계층1 자동등록: IConverterProvider 구현체(HeicProvider/PdfProvider 등)에 [RegisterE2EProvider] 같은 마커를 붙이고 AutoRegisterInject(또는 소형 자작 generator)로 'IEnumerable GetBuiltInProviders()'를 컴파일 타임 생성. App 시작 시 registry.RegisterRange(GetBuiltInProviders()). (3) 계층2 어댑터: 새 추상 클래스 ExternalToolProvider : IConverterProvider 를 만든다. 이 클래스가 (a) ProviderCapability를 manifest의 입력×출력 매트릭스에서 ProviderCapability.PairsFromMatrix로 생성(기존 헬퍼 재사용), (b) CheckAvailabilityAsync는 manifest의 toolDetect 규칙(현 ExternalToolDetector 패턴 일반화)으로 실행 파일 탐지, (c) ConvertAsync는 manifest의 argument 템플릿({input}/{output}/{outdir}/{format} 토큰 치환)을 CliWrap으로 실행하고 결과 파일 존재로 성공 판정. 기존 ExternalDependency 레코드를 manifest의 dependency 섹션과 그대로 매핑. (4) Manifest 로더: tools/*.manifest.json 을 읽어 ExternalToolProvider 인스턴스들을 만들고 registry.RegisterRange. manifest 스키마는 기존 ProviderCapability/ConversionPair/ExternalDependency 모양을 그대로 직렬화한 형태로 잡으면 추상화 변경 최소. (5) 충돌 우선순위: _byPair.TryAdd는 first-wins라 in-box Provider가 동일 쌍을 가지면 manifest 어댑터보다 먼저 등록해 우선권을 준다(현재 동작 유지). 향후 우선순위 필드가 필요하면 ProviderCapability에 Priority(int) 추가 후 Index에서 비교. (6) 안정성: 외부 도구는 CliWrap의 ExecuteAsync에 CancellationToken과 타임아웃을 걸고, 크래시해도 ConvertResult.Fail로 흡수(이미 DocumentProvider가 try/catch로 처리하는 패턴 유지). 진짜 in-process .NET 플러그인(계층3)을 열 때만 collectible ALC를 도입하되, IConverterProvider 계약을 담은 별도 Everything2Everything.Abstractions 어셈블리를 만들고 플러그인은 그것을 false로 참조하게 해 타입 동일성을 보장한다.", + "risks": [ + "in-process ALC를 '샌드박스'로 오해하면 안 됨 — Microsoft가 명시적으로 untrusted 코드 격리 불가라고 경고. 서드파티 플러그인은 반드시 out-of-process로.", + "네이티브 의존 Provider(ImageMagick/PDFium/LibreOffice/WebView2)를 collectible ALC에 넣으면 네이티브 핸들이 ALC 경계를 무시해 언로드 실패/프로세스 누수 발생. 이들은 default ALC 고정.", + "Ghostscript(AGPL)와 GPL 코덱 포함 FFmpeg를 단일 포터블 EXE에 번들하면 라이선스 전염/위반. 상업 사용 전제와 충돌 — 번들 대신 '사용자 설치 외부 도구 탐지' 또는 LGPL-only 빌드 별도 프로세스 호출로 회피.", + "Pandoc(GPL)도 EXE에 정적 결합 시 전염. 별도 프로세스 호출(동적)로만 사용해야 함.", + "ALC 언로드 footgun(정적 캐시/이벤트/Timer/타입 누출)으로 메모리 누수 — hot-reload를 실제로 구현하면 WeakReference+반복 GC 검증 테스트가 필수.", + "MEF2 도입은 리플렉션 의존으로 단일 EXE 트리밍/AOT와 충돌 가능 — 이 프로젝트엔 과한 선택지.", + "manifest의 argument 템플릿에 사용자 제어 경로를 치환할 때 인자 인젝션 위험 — CliWrap의 ArgumentsBuilder(자동 이스케이프)를 쓰고 shell 문자열 결합을 피해야 함." + ], + "sources": [ + "https://learn.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support", + "https://learn.microsoft.com/en-us/dotnet/standard/assembly/unloadability", + "https://jordansrowles.medium.com/real-plugin-systems-in-net-assemblyloadcontext-unloadability-and-reflection-free-discovery-81f920c83644", + "https://www.devleader.ca/2026/04/09/plugin-loading-in-net-assemblyloadcontext-with-dependency-injection", + "https://learn.microsoft.com/en-us/dotnet/standard/mef/", + "https://www.nuget.org/packages/System.Composition/", + "https://github.com/patrickklaeren/AutoRegisterInject", + "https://www.nuget.org/packages/AutoRegisterInject", + "https://github.com/pakrym/jab", + "https://github.com/Tyrrrz/CliWrap", + "https://www.nuget.org/packages/CliWrap/", + "https://www.ffmpeg.org/legal.html", + "https://wip.co/posts/ffmpeg-license-can-we-legally-sell-closed-source-apps-that-use-it-ap32zk", + "https://ghostscript.com/licensing/", + "https://ghostscript.com/docs/9.54.0/Commprod.htm", + "https://www.nuget.org/packages/Microsoft.DeclarativeAgents.Manifest/3.5.0" + ] + }, + { + "key": "doc-pipeline", + "topic": "범용 문서/아카이브/폰트/CAD 변환 커버리지 확장 + 변환기 UX 패턴 (Everything2Everything, .NET 9/WPF/Windows 11)", + "keyFindings": [ + "Pandoc은 사실상의 'universal document 허브'다. pandoc 3.x 기준 43개 입력 / 57개 출력 포맷(마크다운 계열·HTML·LaTeX·DOCX·EPUB·RST·MediaWiki·Org·Textile·JATS 등)을 지원하며, 단일 도구로 N×M 마크업/문서 매트릭스를 한 번에 커버한다. .NET 통합은 SimonCropp의 PandocNet(MIT, v4.0.0 / 2026-04, CliWrap 기반 강타입 래퍼)이 가장 성숙하다. 단 pandoc.exe를 번들하지 않고 PATH 또는 명시 경로로 외부 설치를 요구한다 — 프로젝트가 LibreOffice를 외부 의존성으로 다루는 패턴과 정확히 동일.", + "프로젝트는 이미 LibreOffice로 DOCX↔HTML↔TXT를 처리하므로, Pandoc은 LibreOffice가 약한 '마크업/경량 텍스트 포맷'(rst, org, latex, mediawiki, asciidoc, textile, ipynb, epub→md 등)을 채우는 보완재로 배치하는 것이 최적이다. 둘은 경쟁이 아니라 라우팅 분담(LibreOffice=오피스 바이너리, Pandoc=마크업/학술)이다.", + "CloudConvert가 '극한' 변환기의 레퍼런스 매트릭스: 212포맷 / 13카테고리(문서23·이미지42·비디오28·오디오21·스프레드시트8·슬라이드11·전자책22·아카이브39·벡터10·CAD3·폰트5·데이터·해시). Everything2Everything의 현재 커버리지(이미지·PDF·문서5종·HEIC·OCR)와 비교하면 아카이브·폰트·전자책·데이터·벡터·오디오/비디오가 미개척 영역.", + "'극한' UX를 만드는 4대 공통 패턴: (1) HandBrake/XnConvert식 배치 큐(queue) — 수백 파일을 한 번에 넣고 순차/병렬 처리 + per-row 진행률(프로젝트는 이미 큐 inline progress bar 보유), (2) 액션 체이닝(XnConvert: resize→watermark→convert를 한 파이프라인으로), (3) 워치/핫 폴더 자동화(폴더에 떨어뜨리면 자동 변환), (4) 명확한 드롭존 + 클릭 업로드 병행 + hover 시 시각 피드백.", + "라이선스 함정 2건 확인: (a) SixLabors.Fonts는 3.0.0부터 'Split License'로 빌드 타임 라이선스 검증을 강제 — 연매출 1M USD 이상 + 클로즈드소스면 상업 라이선스 구매 필수. 폰트 변환에는 라이선스 비용 없는 LayoutFarm/Typography(MIT 계열) 또는 Aspose.Font(상용) 검토 권장. (b) Xabe.FFmpeg는 CC BY-NC-SA(비상업 전용)라 상업 배포 불가 — 오디오/비디오는 반드시 FFMpegCore(MIT) + FFmpeg 바이너리(LGPL 빌드)로 가야 한다.", + "CAD는 순수 .NET 솔루션이 아직 미성숙: ACadSharp(MIT, v3.6.x)는 DXF/DWG 읽기/쓰기가 가능하나 공식적으로 alpha(일부 엔티티 미구현). 실무 DWG↔DXF 변환은 ODA File Converter(무료, 비오픈소스, 재배포 제약)나 Aspose.CAD(상용)에 의존. LibreDWG는 GPLv3+라 클로즈드소스 EXE에 부적합. CAD는 우선순위 후순위 권장.", + "아카이브는 SharpCompress(MS-PL/유사 permissive, v0.4x, 2026년에도 활발히 유지보수, 5일 전 커밋)가 단연 최적 — 순수 C#, 무의존, zip/tar/gzip/bzip2/lzip/zstd/7z 쓰기 + RAR/arj/arc 읽기, non-seekable 스트림 + async 지원. 단일 EXE에 그대로 포함 가능." + ], + "recommendedApproach": "단계적 카테고리 확장을 권장한다. 핵심 원칙은 '프로젝트가 이미 검증한 외부-CLI 호출 패턴(DocumentProvider→soffice --headless --convert-to)을 그대로 복제'하는 것과 '단일 포터블 EXE에 부담을 주지 않도록 순수 관리 코드 라이브러리를 1순위로 채택'하는 것이다.\\n\\n[1순위 — 순수 .NET, EXE에 바로 포함, 라이선스 깨끗]\\n- 아카이브: SharpCompress 추가 → ArchiveProvider 신설. zip/7z/tar/gz/bz2 ↔ (압축/해제). 무의존 순수 C#라 포터블 EXE에 이상적.\\n- 데이터: Parquet.Net(MIT, v6.0.3, 순수관리, .NET8/10) + ClosedXML(MIT) + CsvHelper로 csv↔json↔xlsx↔parquet DataProvider. 전부 순수 관리 코드.\\n- 벡터: 이미 SkiaSharp 생태계에 가까우므로 Svg.Skia(MIT, v5.0.0)로 svg→png/jpg/webp/pdf. EPS는 ImageMagick+Ghostscript 델리게이트 활용(이미 ImageMagick 보유).\\n\\n[2순위 — 외부 CLI 의존, LibreOffice와 동일한 ExternalToolDetector 패턴]\\n- 마크업/학술 문서: PandocNet(MIT) + pandoc.exe 외부 의존 → PandocProvider. md/rst/org/latex/mediawiki/asciidoc/textile/ipynb/epub 등을 매트릭스로 노출.\\n- 전자책: Calibre의 ebook-convert.exe(GPLv3, 별도 프로세스 호출이므로 GPL 전파 없음) → EbookProvider. epub↔mobi↔azw3↔pdf↔docx.\\n- 오디오/비디오: FFMpegCore(MIT) + FFmpeg LGPL 바이너리 → MediaProvider. mp4/mkv/mp3/wav/flac/webm 등. (Xabe.FFmpeg는 비상업 라이선스이므로 배제)\\n\\n[3순위 — 보류/조건부]\\n- 폰트: 라이선스 비용 없는 LayoutFarm/Typography로 ttf↔woff/woff2 읽기, woff2 쓰기는 Brotli 압축 필요. SixLabors.Fonts 3.x는 빌드타임 라이선스 강제로 회피. 수요 확인 후 진행.\\n- CAD: ACadSharp가 alpha라 신중. ODA File Converter는 재배포 제약. 수요 검증 전까지 보류.\\n\\nUX는 워치폴더(FileSystemWatcher + 500ms 디바운스 + 파일 잠금 재시도) + CLI 자동화(이미 CliRouter 존재)를 더해 '배치/자동화 3종 세트(드래그앤드롭·핫폴더·CLI)'를 완성하는 것을 권장.", + "libraries": [ + { + "name": "PandocNet (SimonCropp)", + "purpose": "Pandoc CLI 강타입 .NET 래퍼 — md/rst/org/latex/mediawiki/asciidoc/ipynb/epub 등 40+ 마크업·문서 상호변환의 허브", + "license": "MIT", + "maturity": "active (v4.0.0, 2026-04, CliWrap 기반). pandoc.exe 번들 안 함 — 외부 설치 필요", + "notes": "PandocInstance.ConvertToText() / Convert(in,out) API. 강타입 InFormat/OutFormat 클래스. 프로젝트의 ExternalToolDetector + soffice 호출 패턴과 동일하게 pandoc.exe 탐지 추가하면 됨. LibreOffice가 못 하는 마크업/학술 포맷 보완재." + }, + { + "name": "SharpCompress", + "purpose": "아카이브 압축/해제 — zip/7z/tar/gzip/bzip2/lzip/zstd 쓰기 + rar/arj/arc 읽기", + "license": "MS-PL 계열 permissive (상업 사용 가능)", + "maturity": "production (v0.48.x, 2026년 5일 전 커밋, 활발). 순수 C#, 무의존, async 지원", + "notes": "단일 포터블 EXE에 그대로 포함 가능. non-seekable 스트림 지원으로 대용량 OK. ArchiveProvider 신설 1순위." + }, + { + "name": "Parquet.Net (aloneguid)", + "purpose": "Apache Parquet 읽기/쓰기 (데이터 카테고리)", + "license": "MIT", + "maturity": "production (v6.0.3, 2026-05, 27M 다운로드). 순수 관리 코드, 무의존, .NET8/10", + "notes": "ParquetSharp(C++ PInvoke)보다 포터블 EXE에 적합 — 네이티브 의존 없음. csv/json/xlsx↔parquet DataProvider에 사용." + }, + { + "name": "ClosedXML + CsvHelper + ExcelDataReader", + "purpose": "xlsx/csv 읽기·쓰기, json 변환 (데이터 카테고리)", + "license": "MIT (ClosedXML, CsvHelper) / MS-PL (ExcelDataReader)", + "maturity": "production, 모두 순수 관리 코드", + "notes": "ClosedXML은 OpenXML 래퍼로 xlsx 쓰기, ExcelDataReader는 구형 xls 읽기. CsvHelper로 csv↔json. 전부 EXE 포함 가능." + }, + { + "name": "Svg.Skia (wieslawsoltes)", + "purpose": "SVG → PNG/JPG/WebP/PDF/XPS 래스터화 (벡터 카테고리)", + "license": "MIT", + "maturity": "active (v5.0.0, 2026-05). SkiaSharp 백엔드 의존(네이티브 자산 필요)", + "notes": "SVG Full 1.1 + SVG2 정적 기능. SkiaSharp 네이티브 dll이 포터블 EXE 크기를 늘리지만 단일파일 publish 시 추출 가능. EPS는 ImageMagick+Ghostscript로 보완." + }, + { + "name": "Calibre ebook-convert (CLI)", + "purpose": "전자책 변환 — epub↔mobi↔azw3↔pdf↔docx, --output-profile kindle 등", + "license": "GPLv3 (별도 프로세스 호출이므로 앱에 GPL 전파 안 됨)", + "maturity": "production (Calibre 9.x, 문서 2026-05 갱신). 외부 설치 필요", + "notes": "LibreOffice/pandoc과 동일하게 ProcessStartInfo로 ebook-convert.exe 호출. EbookProvider 신설. 출력 확장자로 포맷 자동 추론." + }, + { + "name": "FFMpegCore (rosenbjerg)", + "purpose": "오디오/비디오 변환 — mp4/mkv/webm/mp3/wav/flac 등 (FFmpeg/FFProbe 래퍼)", + "license": "MIT (래퍼) + FFmpeg는 LGPL 빌드 사용", + "maturity": "production, fluent 인자 빌더, sync/async", + "notes": "Xabe.FFmpeg(CC BY-NC-SA, 비상업)는 배제 — 상업 배포 불가. FFMpegCore + LGPL FFmpeg 바이너리 조합이 라이선스상 안전. MediaProvider로 대형 카테고리 확장." + }, + { + "name": "Magick.NET (이미 보유)", + "purpose": "이미지 + 벡터(SVG/EPS/AI/PS) + PSD/HEIC. Ghostscript 델리게이트로 EPS/AI/PS 래스터화", + "license": "Apache-2.0", + "maturity": "production, 이미 프로젝트에서 사용 중", + "notes": "추가 도입 없이 EPS/AI/PS 입력을 커버 가능(Ghostscript 외부 의존만 추가). 벡터 카테고리의 일부를 기존 MagickProvider 확장으로 흡수 가능." + }, + { + "name": "LayoutFarm/Typography", + "purpose": "폰트 읽기/변환 — ttf/otf/ttc/woff/woff2 읽기, 글리프 레이아웃", + "license": "MIT/Apache 계열 (permissive, 라이선스 비용 없음)", + "maturity": "active이나 변환 API는 SixLabors/Aspose보다 저수준", + "notes": "SixLabors.Fonts 3.x의 빌드타임 라이선스 강제를 피하는 무료 대안. woff2 쓰기는 Brotli 압축 별도 필요. 폰트 카테고리는 수요 검증 후 진행 권장." + }, + { + "name": "ACadSharp (DomCR)", + "purpose": "CAD — DXF/DWG 읽기/쓰기", + "license": "MIT", + "maturity": "alpha (v3.6.x, 일부 엔티티 미구현·버그 가능)", + "notes": "순수 .NET이라 매력적이나 성숙도 부족. 실무 DWG↔DXF는 ODA File Converter(무료·비오픈·재배포 제약) 의존. CAD는 후순위/조건부 권장. LibreDWG(GPLv3)는 클로즈드 EXE에 부적합." + } + ], + "integrationNotes": "프로젝트의 추상화는 신규 카테고리 추가에 매우 친화적이다. 각 신규 Provider는 IConverterProvider 3개 멤버(Capability getter, CheckAvailabilityAsync, ConvertAsync)만 구현하면 되고, Everything2EverythingBootstrap.CreateDefault()의 providers 배열에 한 줄 추가하면 ProviderRegistry가 N×M 매트릭스를 자동 인덱싱한다(_byPair / _outputsByInput).\\n\\n구체적 통합 방법:\\n\\n1) 매트릭스 선언: ProviderCapability.PairsFromMatrix(Inputs, Outputs)를 그대로 재사용. 예) ArchiveProvider는 Inputs={zip,7z,tar,gz,...}, Outputs={zip,7z,tar,gz}로 선언. PandocProvider는 마크업 포맷 배열로 거대 매트릭스 자동 생성. (단 Pandoc/LibreOffice 매트릭스가 겹치는 pair는 ProviderRegistry가 _byPair.TryAdd로 '먼저 등록된 Provider 우선'이므로 Bootstrap 배열 순서로 라우팅 우선순위 제어 — LibreOffice를 오피스 바이너리에, Pandoc을 마크업에 우선시키려면 순서 조정).\\n\\n2) 외부 도구 탐지: DocumentProvider.CheckAvailabilityAsync가 ExternalToolDetector.TryFindLibreOfficeSoffice(out _)를 호출하고 NotReady(reason, ExternalDependencies)를 반환하는 패턴을 그대로 복제. ExternalToolDetector에 TryFindPandoc / TryFindCalibreEbookConvert / TryFindFfmpeg / TryFindGhostscript 메서드를 추가하면 됨. ExternalDependency 레코드(Name/Description/DownloadUrl/IsRequired)로 미설치 시 다운로드 안내 UI가 기존 DiagnoseWindow와 연동된다.\\n\\n3) 외부 CLI 변환: DocumentProvider.SofficeConvertAsync의 ProcessStartInfo(UseShellExecute=false, CreateNoWindow=true, ArgumentList, WaitForExitAsync(ct) + proc.Kill(true) on cancel) 패턴이 pandoc/ebook-convert/ffmpeg에 그대로 적용된다. 출력물 검증(File.Exists(produced)) + targetPath로 Move하는 흐름도 동일. progress?.Report()는 ffmpeg의 경우 stderr의 time= 파싱으로 실제 진행률 산출 가능(FFMpegCore가 OnProgress 콜백 제공).\\n\\n4) 순수 관리 코드 Provider(SharpCompress/Parquet.Net/ClosedXML/Svg.Skia)는 외부 프로세스 없이 ConvertAsync 내부에서 직접 호출 — CheckAvailabilityAsync는 항상 ProviderAvailability.Ready 반환(Status=Available). MagickProvider가 이미 이 형태이므로 동일 스타일.\\n\\n5) ConvertOptions 확장: 카테고리별 인코딩 옵션 클래스(JpegEncodingOptions 등)가 이미 있는 패턴을 따라 ArchiveOptions(압축레벨), EbookOptions(output-profile), MediaOptions(코덱/비트레이트), PandocOptions(standalone/toc) 등을 추가. ConvertOptions에 프로퍼티로 노출하면 UI가 자동 바인딩 가능.\\n\\n6) UX 확장: CliRouter(이미 존재)에 워치폴더 모드 추가 — FileSystemWatcher를 IHosted/백그라운드로 띄우고 Created 이벤트에 500ms 디바운스 타이머 + 파일 잠금 재시도(IOException 시 짧은 지연 후 재시도) + InternalBufferSize 64KB 상향. 변환은 기존 ConversionEngine + ProviderRegistry.TryGet으로 재사용. 큐 inline progress bar(이미 보유)와 결합하면 핫폴더→큐 자동 적재 UX 완성. XnConvert식 액션 체이닝은 ConvertOptions에 후처리 파이프라인(resize→convert) 추가로 확장 가능.", + "risks": [ + "단일 포터블 EXE 부담: pandoc(~150MB)·calibre·ffmpeg·ghostscript는 모두 외부 바이너리라 EXE에 번들하면 크기가 폭증한다. LibreOffice처럼 '외부 설치 의존 + 미설치 시 안내' 모델을 유지해야 함. 순수 관리 라이브러리(SharpCompress/Parquet.Net/Svg.Skia)만 EXE에 직접 포함 권장. MSIX 배포라면 의존성 번들이 다소 수월하나 여전히 용량 이슈.", + "라이선스 지뢰: (1) SixLabors.Fonts 3.0+는 빌드타임 라이선스 검증 강제 — 무심코 NuGet 추가 시 빌드 실패 또는 라이선스 위반. (2) Xabe.FFmpeg는 CC BY-NC-SA(비상업)라 상업 배포 시 위반 — FFMpegCore로 대체 필수. (3) LibreDWG는 GPLv3+라 클로즈드소스 EXE에 링크 불가. (4) FFmpeg는 빌드에 따라 GPL/LGPL이 갈리므로 LGPL 빌드 바이너리만 동봉해야 함.", + "Pandoc/LibreOffice 매트릭스 중복 라우팅: docx/html 등 겹치는 pair에서 어느 엔진이 더 좋은 결과를 내는지 케이스마다 다름(Pandoc은 시맨틱 변환에 강하나 복잡한 레이아웃은 LibreOffice가 충실). _byPair.TryAdd의 '선등록 우선' 규칙만으로는 품질 최적화가 안 됨 — pair별 선호 엔진 매핑이나 사용자 선택 옵션 필요.", + "ACadSharp alpha 성숙도: DWG 일부 엔티티 미구현으로 실제 도면이 깨질 수 있음. CAD를 정식 기능으로 내세우면 신뢰도 리스크. ODA File Converter는 무료지만 재배포 라이선스 제약이 있어 사용자가 직접 설치하도록 안내해야 함.", + "워치폴더 안정성: 파일이 아직 쓰기 중일 때 Created 이벤트가 발생하면 IOException(파일 잠금). 디바운스 + 재시도 로직을 견고하게 짜지 않으면 자동 변환이 깨진 파일을 생성. 또 변환 출력이 같은 워치폴더에 떨어지면 무한 루프 위험 — 출력 디렉터리 분리 필수.", + "전자책/CAD 외부 도구의 콜드스타트 지연: calibre·LibreOffice·pandoc은 첫 프로세스 기동이 느림(특히 LibreOffice headless). 배치 처리 시 프로세스 재사용(서버 모드)이나 사용자에게 진행 표시로 체감 지연 완화 필요." + ], + "sources": [ + "https://pandoc.org/MANUAL.html", + "https://github.com/SimonCropp/PandocNet", + "https://www.nuget.org/packages/Pandoc/", + "https://github.com/jgm/pandoc/wiki/Pandoc-wrappers-and-interfaces", + "https://github.com/adamhathcock/sharpcompress", + "https://www.nuget.org/packages/sharpcompress/", + "https://github.com/G-Research/ParquetSharp", + "https://www.nuget.org/packages/Parquet.Net", + "https://github.com/wieslawsoltes/Svg.Skia", + "https://sixlabors.com/pricing/", + "https://github.com/SixLabors/Fonts/blob/main/LICENSE", + "https://github.com/LayoutFarm/Typography", + "https://products.aspose.com/font/net/conversion/woff2-to-ttf/", + "https://manual.calibre-ebook.com/generated/en/ebook-convert.html", + "https://github.com/DomCR/ACadSharp", + "https://www.opendesign.com/guestfiles/oda_file_converter", + "https://www.gnu.org/software/libredwg/", + "https://ezdxf.readthedocs.io/en/stable/addons/odafc.html", + "https://github.com/rosenbjerg/FFMpegCore", + "https://ffmpeg.xabe.net/license.html", + "https://imagemagick.org/include/formats.php", + "https://github.com/dlemstra/Magick.NET/blob/main/docs/Readme.md", + "https://cloudconvert.com/", + "https://www.spotsaas.com/blog/cloudconvert-software-review", + "https://www.eleken.co/blog-posts/file-upload-ui", + "https://howtoconvert.co/blog/best-open-source-file-converter-apps", + "https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-io-filesystemwatcher", + "https://www.jeremyknight.me/2026/01/17/filewatcher-worker/", + "https://www.nuget.org/packages/ClosedXML" + ] + } +] \ No newline at end of file diff --git a/docs/ssot/_data/status.json b/docs/ssot/_data/status.json new file mode 100644 index 0000000..784e757 --- /dev/null +++ b/docs/ssot/_data/status.json @@ -0,0 +1,11 @@ +{ + "_comment": "로드맵 단계별 진행 상태. 값: planned | in_progress | done. 갱신 후 `python build.py`로 index.html/PLAN.md 재생성.", + "P1": "planned", + "P2": "planned", + "P3": "planned", + "P4": "planned", + "P5": "planned", + "P6": "planned", + "P7": "planned", + "P8": "planned" +} diff --git a/docs/ssot/build.py b/docs/ssot/build.py new file mode 100644 index 0000000..470aab6 --- /dev/null +++ b/docs/ssot/build.py @@ -0,0 +1,720 @@ +# -*- coding: utf-8 -*- +""" +Everything2Everything SSOT 제너레이터. + +_data/*.json (마스터플랜 + 분석 + 리서치 + 설계안 + 심사 + 메타) 을 읽어 +- index.html : 단일 파일 다크테마 대시보드 (외부 의존성 0, 폰트만 CDN fallback) +- PLAN.md : 사람이 읽고 다음 세션이 참조하는 마크다운 SSOT +를 생성한다. + +다음 세션 사용법: + 1) _data/*.json 을 갱신 (로드맵 단계 status 등) + 2) python build.py + 3) index.html / PLAN.md 가 재생성됨 +""" +import json +import os +import html + +HERE = os.path.dirname(os.path.abspath(__file__)) +DATA = os.path.join(HERE, "_data") + + +def load(name): + with open(os.path.join(DATA, name), encoding="utf-8") as f: + return json.load(f) + + +master = load("master.json") +analyses = load("analyses.json") +researches = load("researches.json") +designs = load("designs.json") +ranking = load("ranking.json") +meta = load("meta.json") + +# 로드맵 진행 상태(다음 세션이 갱신). 없으면 모두 'planned'. +status_path = os.path.join(DATA, "status.json") +if os.path.exists(status_path): + with open(status_path, encoding="utf-8") as f: + STATUS = json.load(f) +else: + STATUS = {} + + +def e(s): + return html.escape(str(s if s is not None else "")) + + +def nl2br(s): + return e(s).replace("\\n", "
").replace("\n", "
") + + +# ------------------------------------------------------------------ CSS +CSS = r""" +:root{ + --bg:#080b11; --bg1:#0d121b; --card:#121a26; --card2:#16202e; + --line:#22304333; --line2:#2a3a50; --tx:#e7eef6; --dim:#9fb2c8; --faint:#65788f; + --cy:#22d3ee; --em:#34d399; --vi:#a78bfa; --am:#fbbf24; --rs:#fb7185; --bl:#60a5fa; + --r:16px; --rs2:10px; + --mono:"JetBrains Mono",ui-monospace,"SFMono-Regular",Consolas,"Cascadia Code",monospace; + --sans:"Pretendard Variable",Pretendard,-apple-system,"Segoe UI","Malgun Gothic",sans-serif; +} +*{box-sizing:border-box} +html{scroll-behavior:smooth} +body{margin:0;background:var(--bg);color:var(--tx);font-family:var(--sans); + font-size:15.5px;line-height:1.72;-webkit-font-smoothing:antialiased} +body::before{content:"";position:fixed;inset:0;z-index:-2; + background: + radial-gradient(60% 50% at 78% -8%, #1b3a4a55, transparent 70%), + radial-gradient(55% 45% at 12% 4%, #2c224a55, transparent 70%), + var(--bg);} +body::after{content:"";position:fixed;inset:0;z-index:-1;opacity:.4; + background-image:linear-gradient(#ffffff05 1px,transparent 1px),linear-gradient(90deg,#ffffff05 1px,transparent 1px); + background-size:54px 54px;mask-image:radial-gradient(circle at 50% 0,#000,transparent 80%)} +a{color:var(--cy);text-decoration:none} +a:hover{text-decoration:underline} +.wrap{display:grid;grid-template-columns:248px minmax(0,1fr);gap:0;max-width:1320px;margin:0 auto} +/* nav */ +nav{position:sticky;top:0;align-self:start;height:100vh;overflow-y:auto;padding:30px 18px 40px; + border-right:1px solid var(--line2)} +nav .brand{font-family:var(--mono);font-weight:700;font-size:15px;letter-spacing:-.3px;line-height:1.35; + background:linear-gradient(92deg,var(--cy),var(--vi));-webkit-background-clip:text;background-clip:text;color:transparent} +nav .brsub{color:var(--faint);font-size:11.5px;margin:6px 0 22px;font-family:var(--mono)} +nav a{display:block;color:var(--dim);font-size:13.5px;padding:6px 11px;border-radius:8px;margin:1px 0; + border-left:2px solid transparent;transition:.15s} +nav a:hover{background:#ffffff08;color:var(--tx);text-decoration:none} +nav a.on{color:var(--tx);background:#22d3ee14;border-left-color:var(--cy)} +nav .nsec{color:var(--faint);font-size:10.5px;text-transform:uppercase;letter-spacing:.12em;margin:18px 0 6px 11px} +main{padding:0 clamp(20px,4vw,60px) 120px;min-width:0} +/* hero */ +.hero{padding:64px 0 40px} +.kick{display:inline-flex;gap:9px;align-items:center;font-family:var(--mono);font-size:12px;color:var(--cy); + border:1px solid var(--cy);border-color:#22d3ee44;background:#22d3ee0e;padding:5px 13px;border-radius:999px} +.kick b{color:var(--em)} +h1{font-size:clamp(30px,5vw,50px);line-height:1.08;letter-spacing:-1.4px;margin:22px 0 0;font-weight:800} +h1 .g{background:linear-gradient(96deg,var(--cy) 10%,var(--em) 50%,var(--vi));-webkit-background-clip:text;background-clip:text;color:transparent} +.pitch{font-size:18.5px;color:var(--dim);max-width:62ch;margin:20px 0 0;line-height:1.6} +.metarow{display:flex;flex-wrap:wrap;gap:10px;margin-top:30px} +.chip{font-family:var(--mono);font-size:12px;color:var(--dim);background:var(--card);border:1px solid var(--line2); + padding:7px 13px;border-radius:9px} +.chip b{color:var(--tx)} +.chip .k{color:var(--faint)} +/* sections */ +section{padding:50px 0;border-top:1px solid var(--line);scroll-margin-top:18px} +.sh{display:flex;align-items:baseline;gap:14px;margin:0 0 8px} +.sh .no{font-family:var(--mono);font-size:13px;color:var(--cy);font-weight:700} +h2{font-size:27px;letter-spacing:-.7px;margin:0;font-weight:750} +.sub{color:var(--dim);font-size:15px;margin:0 0 26px;max-width:78ch} +.lead{font-size:17px;color:var(--tx);max-width:80ch;line-height:1.7} +.dim{color:var(--dim)} +/* grid + cards */ +.grid{display:grid;gap:14px} +.g2{grid-template-columns:repeat(2,1fr)} +.g3{grid-template-columns:repeat(3,1fr)} +.g4{grid-template-columns:repeat(4,1fr)} +@media(max-width:900px){.g2,.g3,.g4{grid-template-columns:1fr}} +.card{background:linear-gradient(180deg,var(--card),var(--bg1));border:1px solid var(--line2);border-radius:var(--r); + padding:20px 22px;position:relative;transition:.18s} +.card:hover{border-color:#3a5170;transform:translateY(-2px);box-shadow:0 14px 40px -22px #000} +.card h3{margin:0 0 8px;font-size:16.5px;letter-spacing:-.2px} +.card p{margin:0;color:var(--dim);font-size:14px} +.num{font-family:var(--mono);color:var(--vi);font-size:13px;font-weight:700} +code,.mono{font-family:var(--mono);font-size:.88em} +:not(pre)>code{background:#ffffff0d;border:1px solid var(--line);padding:1.5px 6px;border-radius:6px;color:#bfe6ef} +/* badges */ +.b{display:inline-flex;align-items:center;gap:5px;font-family:var(--mono);font-size:11px;font-weight:700; + padding:3px 9px;border-radius:7px;letter-spacing:.02em;white-space:nowrap} +.b-xl{background:#fb71851f;color:var(--rs);border:1px solid #fb718544} +.b-l{background:#fbbf241f;color:var(--am);border:1px solid #fbbf2444} +.b-m{background:#22d3ee1f;color:var(--cy);border:1px solid #22d3ee44} +.b-s{background:#34d3991f;color:var(--em);border:1px solid #34d39944} +.b-high{background:#fb71851f;color:var(--rs);border:1px solid #fb718544} +.b-medium{background:#fbbf241f;color:var(--am);border:1px solid #fbbf2444} +.b-low{background:#34d3991f;color:var(--em);border:1px solid #34d39944} +.b-gh{background:#a78bfa1f;color:var(--vi);border:1px solid #a78bfa44} +.b-done{background:#34d39926;color:var(--em);border:1px solid #34d39955} +.b-prog{background:#60a5fa22;color:var(--bl);border:1px solid #60a5fa55} +.b-plan{background:#ffffff0a;color:var(--faint);border:1px solid var(--line2)} +/* roadmap timeline */ +.tl{position:relative;margin-top:8px;padding-left:0} +.ph{position:relative;border:1px solid var(--line2);border-radius:var(--r);margin:0 0 14px;overflow:hidden; + background:linear-gradient(180deg,var(--card),var(--bg1))} +.ph>summary{list-style:none;cursor:pointer;padding:18px 22px;display:flex;align-items:center;gap:16px} +.ph>summary::-webkit-details-marker{display:none} +.ph .pno{font-family:var(--mono);font-weight:800;font-size:15px;min-width:42px;height:42px;display:grid;place-items:center; + border-radius:11px;background:#22d3ee14;color:var(--cy);border:1px solid #22d3ee3a} +.ph[open] .pno{background:linear-gradient(135deg,var(--cy),var(--vi));color:#04121a;border-color:transparent} +.ph .pti{flex:1;min-width:0} +.ph .pti b{font-size:16.5px;letter-spacing:-.2px} +.ph .pti .pg{color:var(--dim);font-size:13.5px;margin-top:3px} +.ph .pbadges{display:flex;gap:7px;flex-wrap:wrap;align-items:center} +.ph .body{padding:4px 22px 22px 80px;border-top:1px solid var(--line)} +.ph .body h4{margin:18px 0 9px;font-size:12px;text-transform:uppercase;letter-spacing:.11em;color:var(--faint);font-family:var(--mono)} +.lst{list-style:none;padding:0;margin:0;display:grid;gap:7px} +.lst li{position:relative;padding-left:20px;color:var(--dim);font-size:14px} +.lst li::before{content:"";position:absolute;left:2px;top:9px;width:7px;height:7px;border-radius:2px; + background:linear-gradient(135deg,var(--cy),var(--em))} +.kc{display:grid;gap:6px} +.kc .row{display:grid;grid-template-columns:minmax(120px,210px) 1fr;gap:12px;font-size:13.5px; + padding:8px 0;border-bottom:1px dashed var(--line)} +.kc .row .a{font-family:var(--mono);font-size:12.5px;color:var(--am)} +.kc .row .c{color:var(--dim)} +.exit{margin-top:16px;background:#34d3990d;border:1px solid #34d39933;border-radius:var(--rs2);padding:12px 15px; + font-size:13.5px;color:var(--tx)} +.exit b{color:var(--em);font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.08em} +.provtag{font-family:var(--mono);font-size:11.5px;color:var(--vi);background:#a78bfa12;border:1px solid #a78bfa33; + padding:3px 9px;border-radius:7px;display:inline-block;margin:3px 5px 0 0} +/* ADR */ +.adr{border:1px solid var(--line2);border-radius:var(--r);overflow:hidden;background:linear-gradient(180deg,var(--card),var(--bg1))} +.adr>summary{cursor:pointer;list-style:none;padding:16px 20px;display:flex;gap:14px;align-items:center} +.adr>summary::-webkit-details-marker{display:none} +.adr .aid{font-family:var(--mono);font-weight:800;color:var(--vi);font-size:13px;min-width:52px} +.adr .atit{flex:1;font-weight:650;font-size:15.5px} +.adr .arrow{color:var(--faint);transition:.2s} +.adr[open] .arrow{transform:rotate(90deg);color:var(--cy)} +.adr .abody{padding:2px 20px 20px 20px;border-top:1px solid var(--line);display:grid;gap:12px} +.adr .field>span{font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.1em;color:var(--faint);display:block;margin-bottom:3px} +.adr .field.dec p{color:var(--tx)} +.adr .field p{margin:0;color:var(--dim);font-size:14px} +.adr .two{display:grid;grid-template-columns:1fr 1fr;gap:14px} +@media(max-width:760px){.adr .two{grid-template-columns:1fr}} +/* arch layers */ +.layer{display:grid;grid-template-columns:max-content 1fr;gap:18px;align-items:start; + border:1px solid var(--line2);border-left:3px solid var(--cy);border-radius:12px;padding:16px 20px;margin:0 0 12px; + background:linear-gradient(180deg,var(--card),var(--bg1))} +.layer:nth-child(2){border-left-color:var(--em)} +.layer:nth-child(3){border-left-color:var(--vi)} +.layer:nth-child(4){border-left-color:var(--am)} +.layer .ln{font-weight:700;font-size:15px;max-width:210px} +.layer .lr{color:var(--dim);font-size:13.5px;margin-top:4px} +.layer .comp{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px} +.layer .comp span{font-family:var(--mono);font-size:11.5px;color:var(--dim);background:#ffffff08;border:1px solid var(--line2);padding:3px 9px;border-radius:7px} +.flow{margin-top:14px;background:var(--card2);border:1px solid var(--line2);border-radius:var(--r);padding:18px 20px} +.flow b{color:var(--cy);font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase} +.flow p{margin:8px 0 0;color:var(--dim);font-size:14px} +/* matrix */ +.mtx{display:grid;grid-template-columns:1fr 1fr;gap:14px} +@media(max-width:760px){.mtx{grid-template-columns:1fr}} +.mbox{border:1px solid var(--line2);border-radius:var(--r);padding:18px 20px;background:linear-gradient(180deg,var(--card),var(--bg1))} +.mbox.cur{border-color:#fb718533}.mbox.tgt{border-color:#34d39933} +.mbox .t{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.1em;margin-bottom:8px} +.mbox.cur .t{color:var(--rs)}.mbox.tgt .t{color:var(--em)} +.mbox p{margin:0;color:var(--dim);font-size:14px} +.gaps{display:grid;gap:8px;margin-top:14px} +.gaps li{list-style:none;display:flex;gap:11px;align-items:flex-start;font-size:14px;color:var(--dim); + border:1px solid var(--line2);border-radius:10px;padding:11px 14px;background:var(--card)} +.gaps li::before{content:"GAP";font-family:var(--mono);font-size:10px;font-weight:700;color:var(--rs); + background:#fb71851a;border:1px solid #fb718540;padding:2px 7px;border-radius:6px;margin-top:1px;flex-shrink:0} +.plan-box{margin-top:14px;border:1px solid #22d3ee33;background:#22d3ee08;border-radius:var(--r);padding:18px 20px} +.plan-box .t{font-family:var(--mono);font-size:11px;color:var(--cy);text-transform:uppercase;letter-spacing:.09em;margin-bottom:8px} +.plan-box p{margin:0;color:var(--dim);font-size:14px;line-height:1.7} +/* use case pills */ +.uc{display:grid;gap:8px} +.uc li{list-style:none;font-size:14px;color:var(--dim);padding-left:26px;position:relative} +.uc li::before{content:"AI";position:absolute;left:0;top:1px;font-family:var(--mono);font-size:9.5px;font-weight:800; + color:var(--vi);background:#a78bfa18;border:1px solid #a78bfa44;border-radius:6px;padding:2px 5px} +/* table */ +.tbl{width:100%;border-collapse:collapse;font-size:13.8px;margin-top:6px} +.tbl th{text-align:left;font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.07em; + color:var(--faint);padding:9px 12px;border-bottom:1px solid var(--line2)} +.tbl td{padding:11px 12px;border-bottom:1px solid var(--line);color:var(--dim);vertical-align:top} +.tbl tr:hover td{background:#ffffff04} +.tbl td b{color:var(--tx)} +.arrow-c{color:var(--em);font-family:var(--mono)} +/* metrics */ +.met{display:grid;grid-template-columns:1.3fr auto 1.3fr;gap:0;align-items:center; + border:1px solid var(--line2);border-radius:12px;padding:14px 18px;margin-bottom:10px;background:var(--card)} +@media(max-width:760px){.met{grid-template-columns:1fr}} +.met .mn{font-weight:700;font-size:14.5px;grid-column:1/-1;margin-bottom:6px} +.met .from{font-size:13px;color:var(--rs)} +.met .to{font-size:13px;color:var(--em);text-align:right} +.met .ar{color:var(--faint);font-family:var(--mono);padding:0 16px} +@media(max-width:760px){.met .to{text-align:left}.met .ar{padding:4px 0}} +/* research / analysis */ +.rcard{border:1px solid var(--line2);border-radius:var(--r);overflow:hidden;margin-bottom:12px;background:linear-gradient(180deg,var(--card),var(--bg1))} +.rcard>summary{cursor:pointer;list-style:none;padding:16px 20px;display:flex;gap:13px;align-items:center} +.rcard>summary::-webkit-details-marker{display:none} +.rcard .rt{flex:1;font-weight:650;font-size:15px} +.rcard .rdot{width:9px;height:9px;border-radius:50%;background:linear-gradient(135deg,var(--cy),var(--vi));flex-shrink:0} +.rcard .rbody{padding:4px 20px 20px;border-top:1px solid var(--line);display:grid;gap:14px} +.rcard h4{margin:14px 0 8px;font-size:11.5px;text-transform:uppercase;letter-spacing:.1em;color:var(--faint);font-family:var(--mono)} +.srcs{display:flex;flex-wrap:wrap;gap:7px} +.srcs a{font-family:var(--mono);font-size:11.5px;background:#ffffff08;border:1px solid var(--line2);padding:4px 9px;border-radius:7px;color:var(--cy)} +.weak li::before{background:var(--rs)!important} +.blk li::before{background:var(--am)!important} +.opp li::before{background:var(--em)!important} +/* score bars */ +.score{display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center;margin:4px 0 14px} +.score .bar{height:9px;border-radius:6px;background:#ffffff0d;overflow:hidden} +.score .fill{height:100%;border-radius:6px;background:linear-gradient(90deg,var(--cy),var(--em))} +.score .v{font-family:var(--mono);font-weight:800;font-size:18px} +.win{border-color:#34d39955!important;box-shadow:0 0 0 1px #34d39933, 0 18px 50px -30px #34d39966} +.winbadge{font-family:var(--mono);font-size:10px;font-weight:800;color:#04121a;background:var(--em);padding:3px 8px;border-radius:6px} +/* handoff */ +.handoff{border:1px solid #fbbf2440;background:linear-gradient(180deg,#fbbf240a,var(--bg1));border-radius:var(--r);padding:24px 26px} +.handoff .t{font-family:var(--mono);color:var(--am);font-size:12px;letter-spacing:.08em;text-transform:uppercase} +.handoff p{color:var(--tx);font-size:15px;line-height:1.78;margin:12px 0 0} +.next{margin-top:18px;display:grid;gap:9px} +.next li{list-style:none;display:flex;gap:12px;align-items:flex-start;font-size:14.5px;color:var(--dim)} +.next li .n{font-family:var(--mono);font-weight:800;color:var(--am);background:#fbbf2418;border:1px solid #fbbf2440; + min-width:26px;height:26px;border-radius:8px;display:grid;place-items:center;font-size:12px;flex-shrink:0} +footer{border-top:1px solid var(--line2);margin-top:60px;padding:30px 0;color:var(--faint);font-size:12.5px;font-family:var(--mono)} +.toggle-all{font-family:var(--mono);font-size:12px;color:var(--cy);background:transparent;border:1px solid #22d3ee44; + padding:6px 13px;border-radius:8px;cursor:pointer;margin-bottom:14px} +.toggle-all:hover{background:#22d3ee12} +@media(max-width:980px){.wrap{grid-template-columns:1fr}nav{display:none}} +""" + +# ------------------------------------------------------------------ JS +JS = r""" +const secs=[...document.querySelectorAll('section[id]')]; +const links=[...document.querySelectorAll('nav a')]; +const byId={};links.forEach(a=>byId[a.getAttribute('href').slice(1)]=a); +const io=new IntersectionObserver((es)=>{es.forEach(en=>{if(en.isIntersecting){ + links.forEach(l=>l.classList.remove('on'));const a=byId[en.target.id];if(a)a.classList.add('on');}});}, + {rootMargin:'-12% 0px -78% 0px'}); +secs.forEach(s=>io.observe(s)); +document.querySelectorAll('[data-toggle]').forEach(btn=>{ + btn.addEventListener('click',()=>{ + const sel=btn.getAttribute('data-toggle'); + const ds=document.querySelectorAll(sel); + const anyClosed=[...ds].some(d=>!d.open); + ds.forEach(d=>d.open=anyClosed); + btn.textContent=anyClosed?btn.dataset.close:btn.dataset.open; + }); +}); +""" + + +# ------------------------------------------------------------------ builders +def badge_effort(x): + return f'{e(x)}' + + +def badge_risk(x): + return f'RISK {e(x)}' + + +def status_badge(phase): + st = STATUS.get(phase, "planned") + cls = {"done": "b-done", "in_progress": "b-prog", "planned": "b-plan"}.get(st, "b-plan") + lab = {"done": "완료", "in_progress": "진행중", "planned": "예정"}.get(st, "예정") + return f'{lab}' + + +def principles(): + cards = "" + for i, p in enumerate(master["designPrinciples"], 1): + cards += f"""
P{i:02d}
+

{e(p['name'])}

{e(p['description'])}

""" + return f'
{cards}
' + + +def architecture(): + layers = "" + for L in master["targetArchitecture"]["layers"]: + comps = "".join(f"{e(c)}" for c in L["components"]) + layers += f"""
{e(L['name'])}
+
{e(L['responsibility'])}
{comps}
""" + flow = master["targetArchitecture"]["dataFlow"] + return f"""

{e(master['targetArchitecture']['overview'])}

+
{layers}
+
데이터 흐름

{e(flow)}

""" + + +def adrs(): + out = "" + for a in master["coreDecisions"]: + out += f"""
+ {e(a['id'])}{e(a['title'])} + +
+
결정

{e(a['decision'])}

+
근거

{e(a['rationale'])}

+
+
대안

{e(a.get('alternatives',''))}

+
트레이드오프

{e(a['tradeoffs'])}

+
+
""" + return out + + +def roadmap(): + out = "" + for i, r in enumerate(master["roadmap"]): + deliv = "".join(f"
  • {e(d)}
  • " for d in r.get("deliverables", [])) + kc = "".join( + f'
    {e(c["area"])}
    {e(c["change"])}
    ' + for c in r.get("keyChanges", []) + ) + provs = "".join(f'+ {e(p)}' for p in r.get("newProviders", [])) + provs_html = f'

    신규 Provider

    {provs}
    ' if provs else "" + dep = r.get("dependsOn") + dep_html = f'depends · {e(dep)}' if dep else "" + open_attr = " open" if i == 0 else "" + out += f"""
    +
    {e(r['phase'])}
    +
    {e(r['title'])}
    {e(r['goal'])}
    +
    {status_badge(r['phase'])}{badge_effort(r['effort'])}{badge_risk(r['risk'])}{dep_html}
    +
    +
    +

    산출물

      {deliv}
    + {('

    핵심 코드 변경

    '+kc+'
    ') if kc else ''} + {provs_html} +
    Exit Criteria
    {e(r['exitCriteria'])}
    +
    """ + return f'
    {out}
    ' + + +def matrix(): + m = master["conversionMatrix"] + gaps = "".join(f"
  • {e(g)}
  • " for g in m["gaps"]) + return f"""
    +
    현재 상태

    {e(m['currentState'])}

    +
    목표 상태

    {e(m['targetState'])}

    +
    +

    구조적 공백

    +
      {gaps}
    +
    그래프 라우팅 설계 — ProviderRegistry → ConversionGraph

    {e(m['graphRoutingPlan'])}

    """ + + +def ai_section(): + ai = master["aiIntegration"] + uc = "".join(f"
  • {e(u)}
  • " for u in ai["useCases"]) + return f"""
    +

    Codex non-interactive OAuth

    {e(ai['codexOAuth'])}

    +

    API 키 모드 (기본 경로)

    {e(ai['apiMode'])}

    +
    +

    활용 사례 (AI 전용 신규 엣지)

      {uc}
    +

    아키텍처 — AI는 끄면 사라지는 부가 엣지

    {e(ai['architecture'])}

    """ + + +def media_section(): + m = master["mediaLayer"] + rows = [ + ("영상 (Video)", m["video"], "--cy"), + ("오디오 (Audio)", m["audio"], "--em"), + ("PDF 압축", m["pdfCompression"], "--vi"), + ("이미지 최적화", m["imageOptim"], "--am"), + ] + cards = "".join( + f'

    {e(t)}

    {e(d)}

    ' + for t, d, c in rows + ) + return f"""
    {cards}
    +
    외부 바이너리 · 라이선스 게이트 전략

    {e(m['approach'])}

    """ + + +def risks(): + rows = "" + for r in master["riskRegister"]: + rows += f"""{e(r['risk'])} + {badge_risk(r['likelihood'])}{badge_risk(r['impact'])} + {e(r['mitigation'])}""" + return f""" + {rows}
    리스크발생가능영향완화책
    """ + + +def metrics(): + out = "" + for m in master["successMetrics"]: + out += f"""
    {e(m['metric'])}
    +
    {e(m['current'])}
    {e(m['target'])}
    """ + return out + + +def research_section(): + out = "" + for r in researches: + libs = "" + for L in r.get("libraries", []): + libs += f"""{e(L.get('name'))}{e(L.get('purpose'))} + {e(L.get('license'))}{e(L.get('maturity'))}""" + libtbl = (f'

    라이브러리 · 도구

    {libs}
    이름용도라이선스성숙도
    ') if libs else "" + finds = "".join(f"
  • {e(k)}
  • " for k in r.get("keyFindings", [])) + srcs = "".join( + f'{e(s.split("//")[-1][:46])}' + for s in r.get("sources", []) if str(s).startswith("http") + ) + out += f"""
    {e(r['topic'])} +
    +

    핵심 발견

      {finds}
    +

    권장 접근 (.NET 9 / 단일 EXE)

    {e(r.get('recommendedApproach'))}

    + {libtbl} +

    통합 노트

    {e(r.get('integrationNotes'))}

    + {('

    출처

    '+srcs+'
    ') if srcs else ''} +
    """ + return out + + +def analysis_section(): + out = "" + for a in analyses: + weak = "".join(f"
  • {e(w)}
  • " for w in a.get("weaknesses", [])) + blk = "".join(f"
  • {e(b)}
  • " for b in a.get("extensibilityBlockers", [])) + opp = "".join( + f'
  • {e(o["title"])} impact {e(o["impact"])} effort {e(o["effort"])}
  • ' + for o in a.get("improvementOpportunities", []) + ) + out += f"""
    {e(a['subsystem'])} +
    +

    {e(a.get('summary'))}

    +

    설계 약점

      {weak}
    +

    확장성 차단 요소 (file:line)

      {blk}
    +

    개선 기회

      {opp}
    +
    """ + return out + + +def designs_section(): + rk = {r["angle"].split("(")[0].strip(): r for r in ranking["rankings"]} + # map by index hint in angle text + cards = "" + for idx, d in enumerate(designs): + # find matching ranking by 'idx N' + rinfo = next((r for r in ranking["rankings"] if f"idx {idx}" in r["angle"]), None) + score = rinfo["score"] if rinfo else "-" + is_win = (rinfo and rinfo["score"] == max(rr["score"] for rr in ranking["rankings"])) + pct = (score if isinstance(score, (int, float)) else 0) + moves = "".join(f"
  • {e(mv)}
  • " for mv in d.get("keyMoves", [])[:6]) + win_html = 'SELECTED BASE' if is_win else "" + cards += f"""
    +
    +

    {e(d['angle'])}

    {win_html}
    +
    {e(score)}
    +

    {e(d['vision'])[:340]}…

    +

    핵심 변경

    +
      {moves}
    +

    최대 리스크 · {e(d.get('biggestRisk',''))[:170]}

    +
    """ + graft = "".join(f"
  • {e(g)}
  • " for g in ranking["bestIdeasToGraft"]) + return f"""
    {cards}
    +
    심사 종합 권고

    {nl2br(ranking['recommendation'])}

    +

    마스터플랜에 흡수한 최고의 아이디어

    +
      {graft}
    """ + + +# nav +NAV_ITEMS = [ + ("개요", [("vision", "비전"), ("principles", "설계 원칙"), ("architecture", "타깃 아키텍처")]), + ("설계", [("adr", "핵심 결정 (ADR)"), ("matrix", "변환 매트릭스"), ("ai", "AI 통합"), ("media", "미디어 레이어")]), + ("실행", [("roadmap", "로드맵 P1–P8"), ("metrics", "성공 지표"), ("risks", "리스크 레지스터"), ("handoff", "다음 세션 인계")]), + ("근거", [("designs", "설계안 · 심사"), ("research", "인터넷 리서치"), ("analysis", "코드 분석")]), +] + + +def nav(): + out = "" + for grp, items in NAV_ITEMS: + out += f'
    {grp}
    ' + for hid, lab in items: + out += f'{lab}' + return out + + +u = meta["usage"] +metarow = f"""
    +
    생성 {e(meta['generatedDate'])}
    +
    방법 멀티에이전트 Workflow
    +
    에이전트 {u['agents']}
    +
    서브에이전트 토큰 {u['subagentTokens']:,}
    +
    도구 호출 {u['toolUses']}
    +
    종합 {e(meta['winningSynthesis'])}
    +
    """ + +HTML = f""" + +{e(meta['title'])} + + + + +
    + +
    +
    +
    MASTER PLAN · Single Source of Truth
    +

    모든 변환을 엣지로, 엔진이 경로를 합성하는 만능 변환기

    +

    {e(master['elevatorPitch'])}

    + {metarow} +
    + +
    00

    북극성 비전

    +

    {e(master['vision'])}

    +
    + +
    01

    설계 원칙

    +

    아키텍처를 관통하는 불변식. 모든 코드 변경은 이 원칙을 위배하지 않아야 한다.

    + {principles()} +
    + +
    02

    타깃 아키텍처

    +

    4계층 변환 그래프 아키텍처. 핵심은 ProviderRegistry를 단일 홉 딕셔너리에서 ConversionGraph로 승격하는 것.

    + {architecture()} +
    + +
    03

    핵심 아키텍처 결정 (ADR)

    +

    현재 코드의 구체적 한계를 직접 겨냥한 결정. 클릭하면 근거·대안·트레이드오프가 펼쳐진다.

    + +
    {adrs()}
    +
    + +
    04

    변환 매트릭스 · 그래프 라우팅

    +

    단일 홉 → 멀티홉 자동 합성. transitive closure로 "이 파일로 만들 수 있는 모든 포맷"이 폭발한다.

    + {matrix()} +
    + +
    05

    AI 통합 — Codex OAuth + API

    +

    키가 없어도 모든 기존 변환은 100% 동작. AI는 ✨ 배지로만 opt-in 노출되는 부가가치 엣지.

    + {ai_section()} +
    + +
    06

    미디어 레이어 — 영상·오디오·PDF 압축

    +

    FFmpeg(LGPL 분리 호출)·Ghostscript(AGPL 감지만)로 카테고리를 미디어 변환기로 점프.

    + {media_section()} +
    + +
    07

    실행 로드맵 · P1 → P8

    +

    각 단계가 독립적으로 가치를 전달하고 이전 단계에 의존한다. P1은 그래프 코어 + 즉시 체감(PDF 압축)을 함께 심는다.

    + + {roadmap()} +
    + +
    08

    성공 지표

    +

    현재 → 목표. 각 지표가 로드맵 완료를 객관적으로 측정한다.

    + {metrics()} +
    + +
    09

    리스크 레지스터

    +

    가장 큰 위협은 "보이지 않는 리팩터링의 함정"과 GPL/AGPL 라이선스 오염.

    + {risks()} +
    + +
    10

    다음 세션 인계 노트

    +

    이 문서가 SSOT다. 다음 세션은 아래 순서대로 시작한다.

    +
    ▶ Handoff — 어디서부터 시작하고 무엇을 먼저 검증할지
    +

    {nl2br(master['ssotNotes'])}

    +
    +
    + +
    11

    설계안 비교 · 심사

    +

    3개 독립 아키텍트가 서로 다른 각도에서 제안했고, 심사가 점수화·종합했다.

    + {designs_section()} +
    + +
    12

    인터넷 리서치 (7개 토픽)

    +

    2026년 6월 기준 WebSearch로 조사한 라이브러리·방법론·라이선스. 클릭하면 출처까지 펼쳐진다.

    + + {research_section()} +
    + +
    13

    코드 심층 분석 (5개 서브시스템)

    +

    현재 코드를 직접 읽고 file:line으로 인용한 약점·확장 차단 요소·개선 기회.

    + {analysis_section()} +
    + +
    + Everything2Everything SSOT · 생성 {e(meta['generatedDate'])} · {u['agents']} agents · {u['subagentTokens']:,} tokens
    + 이 페이지는 docs/ssot/_data/*.json 에서 python build.py 로 재생성됩니다. · {e(meta['repo'])} +
    +
    +
    + +""" + +with open(os.path.join(HERE, "index.html"), "w", encoding="utf-8") as f: + f.write(HTML) + + +# ------------------------------------------------------------------ Markdown SSOT +def md(): + L = [] + L.append(f"# {meta['title']}\n") + L.append(f"> {meta['subtitle']}\n") + L.append(f"**생성** {meta['generatedDate']} · **방법** {meta['method']} ") + L.append(f"**규모** {u['agents']} agents · {u['subagentTokens']:,} subagent tokens · {u['toolUses']} tool calls ") + L.append(f"**종합** {meta['winningSynthesis']}\n") + L.append("> 이 문서는 SSOT다. `docs/ssot/_data/*.json` 을 갱신하고 `python docs/ssot/build.py` 로 재생성한다. 웹 버전은 `docs/ssot/index.html`.\n") + L.append("---\n## 엘리베이터 피치\n") + L.append(master["elevatorPitch"] + "\n") + L.append("## 북극성 비전\n") + L.append(master["vision"] + "\n") + L.append("## 설계 원칙\n") + for i, p in enumerate(master["designPrinciples"], 1): + L.append(f"{i}. **{p['name']}** — {p['description']}") + L.append("\n## 타깃 아키텍처\n") + L.append(master["targetArchitecture"]["overview"] + "\n") + for Lr in master["targetArchitecture"]["layers"]: + L.append(f"- **{Lr['name']}** — {Lr['responsibility']} \n `{'`, `'.join(Lr['components'])}`") + L.append(f"\n**데이터 흐름:** {master['targetArchitecture']['dataFlow']}\n") + L.append("## 핵심 아키텍처 결정 (ADR)\n") + for a in master["coreDecisions"]: + L.append(f"### {a['id']} · {a['title']}") + L.append(f"- **결정:** {a['decision']}") + L.append(f"- **근거:** {a['rationale']}") + if a.get("alternatives"): + L.append(f"- **대안:** {a['alternatives']}") + L.append(f"- **트레이드오프:** {a['tradeoffs']}\n") + L.append("## 실행 로드맵\n") + for r in master["roadmap"]: + dep = f" · depends: {r['dependsOn']}" if r.get("dependsOn") else "" + st = STATUS.get(r["phase"], "planned") + L.append(f"### {r['phase']} · {r['title']} `effort:{r['effort']}` `risk:{r['risk']}` `status:{st}`{dep}") + L.append(f"**목표:** {r['goal']}\n") + L.append("**산출물:**") + for d in r.get("deliverables", []): + L.append(f"- {d}") + if r.get("keyChanges"): + L.append("\n**핵심 코드 변경:**") + for c in r["keyChanges"]: + L.append(f"- `{c['area']}` — {c['change']}") + if r.get("newProviders"): + L.append(f"\n**신규 Provider:** {', '.join(r['newProviders'])}") + L.append(f"\n**Exit Criteria:** {r['exitCriteria']}\n") + L.append("## 변환 매트릭스 · 그래프 라우팅\n") + m = master["conversionMatrix"] + L.append(f"- **현재:** {m['currentState']}") + L.append(f"- **목표:** {m['targetState']}\n") + L.append("**구조적 공백:**") + for g in m["gaps"]: + L.append(f"- {g}") + L.append(f"\n**그래프 라우팅 설계:** {m['graphRoutingPlan']}\n") + L.append("## AI 통합\n") + ai = master["aiIntegration"] + L.append(f"- **Codex OAuth:** {ai['codexOAuth']}") + L.append(f"- **API 모드:** {ai['apiMode']}") + L.append(f"- **아키텍처:** {ai['architecture']}") + L.append("- **활용 사례:**") + for x in ai["useCases"]: + L.append(f" - {x}") + L.append("\n## 미디어 레이어\n") + ml = master["mediaLayer"] + for k, lab in [("video", "영상"), ("audio", "오디오"), ("pdfCompression", "PDF 압축"), ("imageOptim", "이미지 최적화"), ("approach", "외부 바이너리·라이선스 전략")]: + L.append(f"- **{lab}:** {ml[k]}") + L.append("\n## 리스크 레지스터\n") + L.append("| 리스크 | 발생 | 영향 | 완화책 |\n|---|---|---|---|") + for r in master["riskRegister"]: + L.append(f"| {r['risk']} | {r['likelihood']} | {r['impact']} | {r['mitigation']} |") + L.append("\n## 성공 지표\n") + L.append("| 지표 | 현재 | 목표 |\n|---|---|---|") + for x in master["successMetrics"]: + L.append(f"| {x['metric']} | {x['current']} | {x['target']} |") + L.append("\n## 다음 세션 인계 노트 (Handoff)\n") + L.append(master["ssotNotes"].replace("\\n", "\n") + "\n") + L.append("---\n## 심사 종합 권고\n") + L.append(ranking["recommendation"].replace("\\n", "\n") + "\n") + L.append("### 마스터플랜에 흡수한 최고의 아이디어\n") + for g in ranking["bestIdeasToGraft"]: + L.append(f"- {g}") + return "\n".join(L) + + +with open(os.path.join(HERE, "PLAN.md"), "w", encoding="utf-8") as f: + f.write(md()) + +print("OK index.html", os.path.getsize(os.path.join(HERE, "index.html")), "bytes") +print("OK PLAN.md", os.path.getsize(os.path.join(HERE, "PLAN.md")), "bytes") diff --git a/docs/ssot/index.html b/docs/ssot/index.html new file mode 100644 index 0000000..afea5cd --- /dev/null +++ b/docs/ssot/index.html @@ -0,0 +1,807 @@ + + +Everything2Everything — 변환 그래프 OS 마스터플랜 + + + + +
    + +
    +
    +
    MASTER PLAN · Single Source of Truth
    +

    모든 변환을 엣지로, 엔진이 경로를 합성하는 만능 변환기

    +

    손으로 짠 변환 switch를 자동 경로 탐색 그래프로 교체하고, 그 위에 PDF 압축·영상·HWP·AI를 엣지로 얹어, 코드 한 줄당 N×M 매트릭스가 발현하는 '변환하면서 더 좋아지는' 만능 변환기.

    +
    +
    생성 2026-06-01
    +
    방법 멀티에이전트 Workflow
    +
    에이전트 17
    +
    서브에이전트 토큰 1,426,291
    +
    도구 호출 367
    +
    종합 idx2(AI·미디어 우선, 89점) 실행 골격 + idx0(그래프 코어, 84점) 아키텍처 영혼
    +
    +
    + +
    00

    북극성 비전

    +

    Everything2Everything의 북극성은 "세상의 모든 변환을 원자(atomic) 엣지로 등록하면, 엔진이 그 조합으로 임의의 A→Z를 스스로 합성하고, 변환하면서 AI가 결과를 더 좋게 만드는 변환 그래프 OS"다. 핵심 통찰은 현재 DocumentProvider.RouteAsync(92-205)가 사실상 '사람이 손으로 그린 Dijkstra'(md→html→docx, docx→html→md, hwp→html→md를 switch에 박아넣음)라는 점이며, 이 손그림을 삭제하고 엔진이 같은 경로를 '계산'하게 만드는 것이 모든 확장의 열쇠다. 그래프가 코어가 되면 FFmpeg(미디어), PDF 압축, HWP 양방향, AI 요약/번역/캡션이 전부 '엣지 추가'로 환원되고, OutputsForInput은 1-hop 직접 출력에서 도달 가능한 모든 포맷(transitive closure)으로 폭발한다. 동시에 사용자가 명시한 신규 가치(PDF 압축·HWP·영상·AI)를 인프라 완성을 기다리지 않고 빠르게 출시해 체감 차별화를 먼저 만든다. AI는 핵심 엔진이 아니라 '키 없으면 조용히 비활성되는 부가가치 엣지'로, 변환의 로컬 예측가능성이라는 신뢰를 절대 깨지 않는다.

    +
    + +
    01

    설계 원칙

    +

    아키텍처를 관통하는 불변식. 모든 코드 변경은 이 원칙을 위배하지 않아야 한다.

    +
    P01
    +

    변환은 엣지, 엔진은 라우터

    모든 Provider는 단일 홉 원자 변환(md→html, png→pdf)만 선언한다. 멀티홉(md→docx)은 절대 Provider 내부에 손으로 짜지 않고 엔진의 그래프 탐색이 자동 합성한다. DocumentProvider.RouteAsync의 switch 지옥이 재발하지 않도록 이를 불변식으로 강제한다.

    P02
    +

    기능이 그래프를 견인하되, 그래프가 기능을 받친다

    사용자 체감 가치(PDF 압축·HWP·영상·AI)를 빠르게 출시하되, 신규 기능은 반드시 '그래프 엣지'로만 추가한다. Phase 0에 심은 그래프 코어가 하드코딩 유혹을 구조적으로 차단한다.

    P03
    +

    손실은 가중치다

    품질 손실을 ConversionPair.LossClass(Lossless/Container/Recode/Rasterize)로 SSOT화하고 -log(보존율)+홉페널티로 환산한다. 멀티홉 경로 선택과 UI '손실 변환' 경고 배지가 모두 이 단일 출처를 소비한다.

    P04
    +

    AI는 끄면 사라지는 부가 엣지

    AI는 절대 기본 경로를 점유하지 않는다. 키가 없으면 모든 기존 변환은 100% 동작하고 AI 페어만 자동 비활성(NotReady)되며 ✨AI 배지로만 opt-in 노출된다. 변환의 로컬 예측가능성 신뢰를 깨지 않는다.

    P05
    +

    무거운 외부 도구는 분리 호출로만

    FFmpeg(GPL 정적링크 금지·LGPL 분리호출만), Ghostscript/MuPDF(AGPL·사용자 설치본 감지만), H2Orestart/Calibre(GPL·외부 프로세스 분리)를 본체에 절대 정적 링크하지 않는다. 라이선스 경계를 코드 리뷰 게이트로 강제해 상업 배포 오염을 원천 차단한다.

    P06
    +

    순수 .NET 우선, 외부 바이너리 차선

    단일 포터블 EXE 부담을 줄이기 위해 SharpCompress·Parquet.Net·PDFsharp·Svg.Skia 같은 순수 관리 코드를 EXE에 직접 포함하고, FFmpeg/Pandoc/Calibre 같은 무거운 바이너리는 '외부 설치 감지 + 미설치 시 안내/자동조달' 모델로만 통합한다.

    P07
    +

    점진 마이그레이션, 무중단

    IConverterProvider/ConvertResult/ConvertOptions 일반화는 기존 8개 Provider를 어댑터로 감싸 한 번에 깨지지 않게 한다. 모든 코어 변경은 회귀 테스트(현재 0개에서 출발)로 '동일 동작'을 객관 증명한다.

    +
    + +
    02

    타깃 아키텍처

    +

    4계층 변환 그래프 아키텍처. 핵심은 ProviderRegistry를 단일 홉 딕셔너리에서 ConversionGraph로 승격하는 것.

    +

    4계층 변환 그래프 아키텍처. (1) Abstractions 계층이 Provider 계약을 담고, (2) 그래프 코어가 모든 원자 변환을 방향 그래프로 합성해 Dijkstra로 멀티홉 경로를 푼다. (3) Provider 계층은 in-box 코드 Provider(이미지/문서/미디어/AI)와 manifest 기반 외부 도구 어댑터로 나뉘며, (4) 실행 계층(ExternalProcessRunner·ISettingsStore)이 외부 프로세스·설정·키를 횡단 관리한다. 핵심은 ProviderRegistry를 단일 홉 딕셔너리에서 ConversionGraph로 승격하는 것이다.

    +
    Abstractions 계층 (Everything2Everything.Abstractions)
    +
    Provider 계약을 별도 어셈블리로 분리해 타입 동일성을 보장하고 향후 플러그인의 안정적 참조점을 제공
    IConverterProviderConvertRequest/ConvertContextConvertResult(비파일 산출물 포함)ProviderCapabilityConversionPair+LossClassExternalDependency
    그래프 코어 계층 (Core.Graph)
    +
    모든 Provider Capability를 순회해 방향 그래프(노드=확장자, 엣지=Provider+LossClass 가중치)를 빌드하고, 자체 Dijkstra로 최저손실 멀티홉 경로를 탐색·실행
    ConversionGraphPathFinder(자체 Dijkstra)ChainExecutor(ExecuteChainAsync)ConversionEngine(라우터로 축소)ProviderRegistry(증분 등록 Register/Rebuild)
    Provider 계층
    +
    단일 홉 원자 변환 능력을 선언·실행. in-box 코드 Provider와 manifest 어댑터 Provider 공존
    MagickProvider/PdfProvider/HtmlProvider(기존)LlmProvider(AI)FfmpegProvider(미디어)PdfToolProvider(압축)ImageCombineProvider(N→1)ExternalToolProvider(manifest 어댑터 베이스)
    실행/인프라 계층
    +
    외부 프로세스 실행·설정 영속화·키 보안·미리보기를 횡단 제공
    ExternalProcessRunner(CliWrap, 타임아웃+stderr+Kill)ISettingsStore(DPAPI 암호화)ExternalToolDetector(번들 경로 폴백)IPreviewRenderer(프리뷰 캐시)ManifestLoader
    +
    데이터 흐름

    파일 입력 → ConversionEngine.ConvertOneAsync가 입력/출력 확장자 정규화 → ConversionGraph.FindBestPath(in,out,options)로 경로 탐색(직접 엣지 있으면 1홉, 없으면 손실가중치 기반 멀티홉) → ChainExecutor가 경로의 각 홉을 순차 실행하며 중간 산출물을 공용 workDir(Temp/e2e_{Guid})에 체이닝 → 각 홉은 Provider.ConvertAsync(ConvertRequest) 호출, 진행률은 홉 수로 분할 매핑 → 마지막 홉 산출물을 OutputPathHelper로 충돌 해결 후 최종 출력 → ConvertResult(출력 경로 + 비파일 산출물) 반환, 중간 산출물 정리. AI/외부도구 엣지는 CheckAvailabilityAsync 게이트를 먼저 통과해야 그래프에 활성 노드로 참여.

    +
    + +
    03

    핵심 아키텍처 결정 (ADR)

    +

    현재 코드의 구체적 한계를 직접 겨냥한 결정. 클릭하면 근거·대안·트레이드오프가 펼쳐진다.

    + +
    + ADR-1ProviderRegistry를 단일 홉 딕셔너리에서 ConversionGraph로 승격 + +
    +
    결정

    _byPair 단일 룩업(ProviderRegistry.cs:6,43)을 유지하되 그 위에 인접 리스트 그래프(Dictionary<string,List<Edge>>)를 빌드하고, 외부 의존성 없는 자체 Dijkstra(80~120줄, .NET 9 PriorityQueue 사용)로 멀티홉 경로를 탐색한다. DocumentProvider.RouteAsync의 손그림 멀티홉을 엔진 합성으로 대체.

    +
    근거

    현재 멀티홉이 Provider 내부 switch에 하드코딩되어 형식 N개에 O(N²)로 수동 증식한다. NCSA Polyglot 모델(노드=포맷, 엣지=Provider, 가중치=손실)은 학계 검증된 best practice이며, 그래프가 수십 노드·수백 엣지 규모라 성능 이슈가 없다.

    +
    +
    대안

    QuikGraph(MS-PL, 2022 이후 정체)·Pandoc식 단일 AST 허브(이질적 도메인에 부적합). 자체 구현이 단일 EXE/AOT/라이선스 검토 모두 무부담이라 1순위.

    +
    트레이드오프

    멀티홉은 중간 임시파일 I/O가 늘고 손실이 누적될 수 있다. 완화: 직접 엣지 우선, MaxHops=3 제한, 손실 블랙리스트, 손실 경로 UI 경고 배지.

    +
    +
    + ADR-2손실을 ConversionPair.LossClass 가중치로 SSOT화 + +
    +
    결정

    ConversionPair에 LossClass(Lossless=0/Container=0.05/Recode=0.4/Rasterize=0.8) 필드를 추가하고, 엣지 가중치를 -log(품질보존율)+홉페널티+실행비용 합산으로 계산한다. UI '손실 변환' 배지도 이 가중치를 소비.

    +
    근거

    손실은 본래 곱셈적(0.9×0.8)이므로 -log 변환으로 덧셈 최단경로(Dijkstra)가 곧 최대 품질보존 경로가 된다. 래스터화(텍스트/벡터→PNG)는 단방향 손실 절벽이므로 큰 페널티로 자연 회피.

    +
    +
    대안

    동적 손실 측정(Versus식 실측). 초기엔 정적 가중치 테이블로 시작하고 동적 측정은 처음부터 넣지 않는다(과도한 복잡도).

    +
    트레이드오프

    정적 가중치는 추정값이라 일부 쌍에서 비최적 경로 가능. 완화: 보수적으로 직접 엣지 우선, 멀티홉은 fallback으로만 운영.

    +
    +
    + ADR-3레지스트리 충돌을 조용한 first-wins에서 Priority 기반 명시 선택으로 교체 + +
    +
    결정

    _byPair.TryAdd(ProviderRegistry.cs:22)의 '조용한 첫 등록자 우선'을 ProviderCapability.Priority 필드 + 다중 Provider 공존 모델 + 충돌 시 진단 경고로 교체한다. 같은 (input,output)에 빠른변환/고품질/AI 등 복수 전략 등록 허용.

    +
    근거

    PDF압축 vs PDF렌더, AI변환 vs 일반변환처럼 한 쌍에 복수 전략이 필연적으로 생긴다. 현재는 부트스트랩 순서에 따라 비결정적으로 한쪽이 조용히 사라져 데이터 손실이다.

    +
    +
    대안

    현 first-wins 유지(확장 불가). 비용 기반 자동 선택만(사용자 전략 선택 불가). Priority+공존이 그래프 가중치와도 자연 연결.

    +
    트레이드오프

    같은 쌍에 복수 Provider가 등록되면 UI에서 전략 선택지를 노출해야 하는 추가 복잡도. 완화: 기본은 최저비용 자동 선택, 고급 모드에서만 명시 선택.

    +
    +
    + ADR-4IConverterProvider 시그니처를 ConvertRequest/ConvertContext로 일반화 + +
    +
    결정

    단일 sourcePath/단일 outputExtension/IProgress<double> 고정 시그니처(IConverterProvider.cs:9-15)를 ConvertRequest(다중 입력·옵션 백·미디어 메타) + ConvertContext로 일반화하고, ConvertResult(ConvertResult.cs)에 ExtractedText/AiResponse/Metadata/IntermediateArtifacts 필드를 추가한다. 기존 8개 Provider는 어댑터로 감싸 무중단 마이그레이션.

    +
    근거

    현 시그니처는 N→1 결합, AI 비파일 응답, 영상 메타데이터 프로빙, 멀티홉 중간 컨텍스트를 표현할 수 없다. 미디어/AI 엣지가 들어올 '그릇'을 코어에 먼저 판다.

    +
    +
    대안

    시그니처 유지하고 옵션에 모든 것 욱여넣기(갓 오브젝트 가속). 점진 어댑터 전략이 8개 Provider 동시 파괴를 방지.

    +
    트레이드오프

    어댑터 계층이 일시적 중복을 만든다. 완화: 회귀 테스트로 동일 동작 보장 후 어댑터를 점진 제거.

    +
    +
    + ADR-5AI는 IAiProvider 특수 인터페이스가 아니라 그래프의 부가 엣지로 편입 + +
    +
    결정

    LlmProvider를 일반 IConverterProvider로 구현하고 Microsoft.Extensions.AI(IChatClient) 추상화 위에 OpenAI/Anthropic 공식 SDK를 연결한다. AI는 로컬 변환이 없는 신규 페어(요약/번역/캡션/메타데이터)에만 노출되고, 키 부재 시 CheckAvailabilityAsync가 NotReady를 반환해 그래프에서 자동 비활성된다.

    +
    근거

    AI를 특수 카테고리로 두면 그래프·레지스트리 밖에 별도 배관이 생긴다. 엣지로 환원하면 OcrProvider가 Windows OCR을 흡수한 선례처럼 매트릭스에 자연 편입되고, AI 후처리 파이프(OCR→LLM 교정)도 멀티홉으로 자동 합성된다.

    +
    +
    대안

    별도 IAiConverterProvider 확장(추상화 분기 증가). 통합 IConverterProvider가 단순하고 그래프와 정합.

    +
    트레이드오프

    AI는 비결정적·유료·네트워크 의존이라 '재현 가능한 변환'과 충돌. 완화: ✨AI 배지·기본 경로 불점유·키 없으면 비활성 불변식.

    +
    +
    + ADR-6무거운 외부 도구는 분리 프로세스 호출 + 라이선스 게이트로만 통합 + +
    +
    결정

    FFmpeg는 BtbN lgpl-shared 빌드를 별도 프로세스로 호출(LGPL 준수), Ghostscript/MuPDF는 AGPL이라 사용자 설치본 감지만, H2Orestart/Calibre/Pandoc은 GPL이라 외부 프로세스 분리. 공통 ExternalProcessRunner(CliWrap, 타임아웃+stderr+Kill)로 통일하고, 라이선스 경계를 코드 리뷰 게이트로 강제한다.

    +
    근거

    단일 포터블 EXE 상업 배포에서 GPL/AGPL 바이너리 정적 링크는 즉시 라이선스 오염이다. 이미 LibreOffice를 외부 도구로 다루는 검증된 패턴을 그대로 확장.

    +
    +
    대안

    GPL 빌드 번들(라이선스 위반)·상업 라이선스 구매(비용). 분리 호출 + 사용자 설치 감지/LGPL 자동조달이 안전.

    +
    트레이드오프

    진정한 자족 EXE가 아니라 외부 의존 체인이 길어진다. 완화: 순수 .NET 라이브러리 우선, 외부 도구는 NotReady로 친절히 안내.

    +
    +
    + ADR-7CombineAsync를 ImageCombineProvider(N→1 엣지)로 분리 + +
    +
    결정

    ConversionEngine.CombineAsync의 ImageMagick 직접 의존(ConversionEngine.cs:2,164-257)과 정적 HashSet(CombinableInputs/Outputs:14-23)을 IMultiInputProvider 추상화로 분리한다. 엔진은 라이브러리 중립이 되고 결합 가능 형식은 Provider 능력 선언으로 통합.

    +
    근거

    현재 '결합'이 Provider 추상화 밖에 있어 엔진이 ImageMagick에 결합되고, PDF 병합·동영상 concat·오디오 믹스 같은 비이미지 결합으로 확장 불가하다. 정적 HashSet과 능력 선언의 이중 관리도 해소.

    +
    +
    대안

    현 구조 유지(이미지 결합만 영구 고착). N→1 추상화가 모든 결합을 동일 패턴으로 흡수.

    +
    트레이드오프

    결합 진행률 보고가 단일 출력 가정과 달라 재설계 필요. 완화: ConvertProgress를 N→1 케이스로 확장.

    +
    +
    + ADR-8ConvertOptions 갓 오브젝트를 그래프 옵션 + 형식별 옵션 백으로 분해 + +
    +
    결정

    11개 sub-record 갓 오브젝트(ConvertOptions.cs:35-55)를 그래프 옵션(AllowMultiHop/MaxHops/AvoidLossy) + 형식별 옵션 백(IReadOnlyDictionary 또는 Provider 선언형 스키마)으로 분해한다. Video/Audio/Ai/PdfCompress를 sub-record 증식 없이 수용하고 ISettingsStore(DPAPI 암호화)로 영속화.

    +
    근거

    형식 추가마다 sub-record가 비대해지고 모든 Provider가 무관한 옵션을 끌고 다닌다. 영상 코덱·AI 프롬프트·PDF 압축 레벨을 담을 자리가 코어 record 증식 없이 필요하다.

    +
    +
    대안

    sub-record 계속 추가(god object 가속). 옵션 백이 형식별 옵션만 주입해 확장성 확보.

    +
    트레이드오프

    강타입 안전성이 약화된다. 완화: Provider가 옵션 스키마(이름/타입/범위/기본값)를 선언하고 UI가 동적 생성·검증.

    +
    +
    + ADR-9manifest는 풀 DSL이 아니라 단순 CLI용 선언적 인자 템플릿으로 제한 채택 + +
    +
    결정

    manifest를 ExternalProcessRunner 위의 '선언적 인자 템플릿({input}/{output}/{outdir}/{format})'으로만 좁게 채택해 qpdf/Ghostscript 같은 단순 CLI 압축 도구를 코드 없이 추가한다. 복잡 로직(FFmpeg HW가속 폴백·AI)은 in-box 코드 Provider 원칙을 P1부터 못박는다.

    +
    근거

    Provider 8개·테스트 0개 단일 개발자 프로젝트에 풀 manifest DSL·동적 ALC 로더는 ROI가 낮다. FFmpeg의 nvenc→AV1 조건부 폴백은 manifest로 표현 불가하므로 하이브리드 경계가 필수.

    +
    +
    대안

    풀 플러그인 생태계(과잉 엔지니어링)·전부 코드(확장 비용). 좁은 manifest가 단순 도구 추가 비용만 제거.

    +
    트레이드오프

    manifest가 또 다른 갓 오브젝트가 될 위험. 완화: '90% 단순 CLI만 manifest, 복잡 로직은 in-box' 경계를 P1 불변식으로 명문화.

    +
    +
    +
    + +
    04

    변환 매트릭스 · 그래프 라우팅

    +

    단일 홉 → 멀티홉 자동 합성. transitive closure로 "이 파일로 만들 수 있는 모든 포맷"이 폭발한다.

    +
    +
    현재 상태

    8개 Provider가 PairsFromMatrix로 N×M 쌍을 선언하지만 ProviderRegistry는 (input,output) 단일 홉 딕셔너리(_byPair)만 매핑한다. 멀티홉(md→docx)은 DocumentProvider.RouteAsync(92-205)에 손코딩되어 형식 N개에 O(N²)로 수동 증식한다. 매트릭스는 '거의 모든 것→이미지/PDF/텍스트' 단방향으로만 풍부하고, 역방향(이미지/PDF→편집문서, HWP 출력, 미디어/아카이브)이 구조적으로 비어 있다. 동일포맷(pdf→pdf 압축)은 ConversionEngine.cs:88에서 무조건 Skip된다.

    +
    목표 상태

    ConversionGraph가 모든 Provider Capability를 순회해 방향 그래프를 빌드하고, Dijkstra가 임의의 A→Z를 원자 엣지 조합으로 자동 합성한다. OutputsForInput은 transitive closure로 확장되어 '이 파일로 만들 수 있는 모든 포맷'을 노출한다. PDF/HWP 양방향, 영상/오디오, 아카이브/데이터/벡터, AI 후처리가 모두 엣지로 편입되고, 동일포맷 압축(pdf→pdf)도 옵션으로 허용되는 엣지가 된다.

    +
    +

    구조적 공백

    +
    • PDF 압축(pdf→pdf): 어떤 Provider도 수행 못 함 — PdfToolProvider 신설 필요
    • PDF→DOCX/HTML 역변환: 편집가능 역변환 경로 전무 — LibreOffice 경유 추가
    • HWP/HWPX 출력: H2Orestart import 전용이라 →HWP 불가, →DOCX/HTML/TXT도 미노출
    • 영상/오디오 전 카테고리: mp4/mp3/flac 등 미디어 Provider 0개
    • 아카이브/폰트/벡터/데이터/전자책: 빈 카테고리(ComingSoon enum 미사용)
    • AI 변환(요약/번역/캡션): 추상화·옵션·Provider 어디에도 자리 없음
    • 동일포맷 최적화(이미지 리인코딩, PDF 압축): ConversionEngine.cs:88에서 Skip되어 표현 불가
    +
    그래프 라우팅 설계 — ProviderRegistry → ConversionGraph

    1) 그래프 빌드(앱 시작 1회): ProviderRegistry 생성자 루프(16-30)에서 각 Provider의 Capability.SupportedConversions를 순회해 인접 리스트 Dictionary<string,List<Edge>>를 구축한다. 노드=정규화된 확장자(.png/.pdf/.docx), 엣지=Edge{Provider, ConversionPair, Weight}. 노드 수십·엣지 수백 규모라 그래프는 매우 작다. 2) 가중치: 각 ConversionPair.LossClass(Lossless=0/Container=0.05/Recode=0.4/Rasterize=0.8)를 -log(품질보존율)로 환산하고 홉페널티(작은 상수)와 실행비용(외부 프로세스>in-process)을 가중합한다. 손실은 곱셈적이므로 -log 변환으로 덧셈 최단경로가 곧 최대 품질보존 경로가 된다. 3) 탐색: .NET 9 System.Collections.Generic.PriorityQueue로 Dijkstra(O(E log V), 80~120줄)를 자체 구현한다. ConversionEngine.ConvertOneAsync(91)에서 직접 엣지가 있으면 1홉(기존 동작 호환), 없으면 FindBestPath(inExt,outExt,options)로 멀티홉 경로를 구한다. AllowMultiHop(기본 true)/MaxHops(기본 3)/AvoidLossy 옵션으로 게이트. 4) 실행: ChainExecutor가 경로의 각 홉을 순차 실행하며 중간 산출물을 공용 workDir(Temp/e2e_{Guid})에 체이닝하고, 진행률을 홉 수로 분할해 IProgress에 매핑한다. 각 홉은 기존 provider.ConvertAsync를 그대로 호출(인터페이스 변경 불필요). 5) 안전장치: 멀티홉은 직접 엣지가 없을 때만 발동, 손실 블랙리스트(텍스트→래스터 같은 도메인 경계 전이 통제), 한 홉 실패 시 어느 홉에서 실패했는지 사용자에게 전달. 6) UI: OutputsForInput을 reachability(transitive closure)로 확장하고, 손실 경로로만 도달하는 출력에 '손실 변환' 경고 배지를 붙인다.

    +
    + +
    05

    AI 통합 — Codex OAuth + API

    +

    키가 없어도 모든 기존 변환은 100% 동작. AI는 ✨ 배지로만 opt-in 노출되는 부가가치 엣지.

    +
    +

    Codex non-interactive OAuth

    Codex CLI를 PATH에서 감지될 때만 활성화되는 구독자용 opt-in 보조 백엔드로 둔다. 핵심 제약: ChatGPT 구독 OAuth 토큰(auth.json의 access/refresh)은 Codex 백엔드 전용이라 api.openai.com에 직접 Bearer로 붙일 수 없다 — 구독 재사용은 오직 codex CLI 프로세스 호출로만 가능. 실행은 ExternalProcessRunner로 `codex exec --skip-git-repo-check --json --output-schema schema.json -o out.json --cd <tempdir> "<프롬프트 + 파일경로>"` 형태. --skip-git-repo-check는 변환 앱에 필수(git 저장소 아닌 폴더 허용), --output-schema로 응답을 JSON Schema로 강제해 메타데이터 추출, --json으로 JSONL 이벤트 스트림 파싱. CheckAvailabilityAsync에서 `codex --version` 프로브 + auth.json 존재 확인. auth.json refresh 토큰 race를 막기 위해 SemaphoreSlim(1) 직렬화 또는 --ephemeral 사용.

    +

    API 키 모드 (기본 경로)

    기본 경로는 API 키 + 공식 SDK다. Microsoft.Extensions.AI(IChatClient, MIT) 단일 추상화로 OpenAI(공식 OpenAI 패키지, MIT)와 Anthropic(공식 Anthropic 패키지, MIT)을 동일 인터페이스로 다룬다. 사용자는 설정에서 'OpenAI / Claude / Codex CLI / auto'를 고르고 API 키만 입력한다. 키는 ISettingsStore에서 System.Security.Cryptography.ProtectedData(DPAPI, CurrentUser)로 암호화해 %LOCALAPPDATA%에 저장하고, OPENAI_API_KEY/ANTHROPIC_API_KEY 환경변수도 폴백으로 읽어 CI/파워유저 친화. CheckAvailabilityAsync가 키 부재 시 NotReady(키 발급 URL을 ExternalDependency로 안내)를 반환해 그래프에서 자동 비활성.

    +
    +

    활용 사례 (AI 전용 신규 엣지)

    • 요약: pdf/docx/txt/md → txt/md (긴 문서를 LLM이 요약)
    • 번역: txt/docx/md → txt/docx (대상 언어는 옵션, 비파일 입력 LLM 왕복)
    • OCR 교정: OcrProvider 출력(.txt)을 받아 LLM이 오탈자/줄바꿈 정리 (그래프가 OCR→LLM 2단계 멀티홉으로 자동 합성)
    • 이미지 캡션/대체텍스트: png/jpg → txt (비전 모델)
    • 문서 언어 번역 + 포맷 정규화: csv→md(표), txt→md
    • 메타데이터 생성: 임의 입력 → json (제목/태그/요약, Structured Outputs로 구조화)
    +

    아키텍처 — AI는 끄면 사라지는 부가 엣지

    LlmProvider를 별도 IAiProvider가 아닌 일반 IConverterProvider로 구현해 그래프의 부가 엣지로 편입한다(ADR-5). AI는 로컬 변환이 없는 신규 페어(요약/번역/캡션/메타데이터)에만 노출되며, 등록 순서로 '로컬 변환이 이미 있는 페어는 로컬 Provider가 우선, AI는 신규 페어만'을 보장한다(Priority 충돌 모델). 불변식: 키가 없어도 모든 기존 변환은 100% 동작하고 AI 페어만 비활성, AI는 절대 기본 경로를 점유하지 않으며 UI에 ✨AI 배지(종량과금·네트워크 명시)로만 opt-in 노출된다. 텍스트 추출이 필요하면 DocumentProvider/PdfProvider/OcrProvider를 주입받아 '추출→LLM' 2단계로 구성(OcrProvider가 PdfProvider를 주입받는 선례). 프라이버시: 로컬 문서가 외부 서버로 전송되므로 명시적 동의 토글 필수(기본 OFF), 미래에 Ollama 로컬 모델 경로를 IChatClient로 열어둔다.

    +
    + +
    06

    미디어 레이어 — 영상·오디오·PDF 압축

    +

    FFmpeg(LGPL 분리 호출)·Ghostscript(AGPL 감지만)로 카테고리를 미디어 변환기로 점프.

    +

    영상 (Video)

    FfmpegProvider(FFMpegCore 5.4.0, MIT)로 mp4/mkv/webm/mov/avi/gif N×M 트랜스코딩. H.264/H.265는 HW 인코더(h264_nvenc/qsv/amf) 우선, LGPL 빌드엔 libx264/x265(GPL)가 없으므로 HW 미지원 시 AV1(libaom)/VP9(libvpx, 둘 다 BSD-like royalty-free)로 폴백. FFprobe로 duration 확보 후 NotifyOnProgress(Action<double>,TimeSpan)을 IProgress에 직결, CancellableThrough(ct)로 취소.

    오디오 (Audio)

    오디오는 mp3/aac/m4a/opus/ogg/flac/wav N×M. AAC는 FFmpeg 네이티브 aac 인코더(LGPL, libfdk-aac=nonfree 회피), Opus/FLAC/MP3는 LGPL 빌드로 직접 처리. 오디오 전용 출력(flac/mp3)은 영상 입력에서 오디오 트랙만 추출.

    PDF 압축

    PdfToolProvider 3단계: Light=PDFsharp(MIT, in-process) 또는 qpdf(Apache 2.0) 구조 최적화(object stream 압축·linearize), Strong=PDFium 렌더+ImageMagick 재인코딩(텍스트 선택성 잃지만 라이선스 안전), Max=Ghostscript(-dPDFSETTINGS /screen)는 AGPL이라 번들 금지·사용자 설치본 감지만. 병합/분할/암호화는 PDFsharp 또는 qpdf.

    이미지 최적화

    기존 MagickProvider의 ApplyEncoding(jpg/png/webp/avif/tiff 품질·알파평탄화·MaxLongEdge)을 공용 ImageEncoder 헬퍼로 추출해 PdfProvider/HtmlProvider/CombineAsync의 4중 복제를 제거. 동일포맷 이미지 리인코딩(품질 조절)도 엣지로 허용.

    +
    외부 바이너리 · 라이선스 게이트 전략

    단일 포터블 EXE 부담을 줄이기 위해 무거운 바이너리(FFmpeg ~100MB)는 절대 번들하지 않고 'RequiresExternal + 최초 사용 시 자동 다운로드' 모델. 라이선스 게이트(코드 리뷰 강제): FFmpeg는 BtbN lgpl-shared 빌드(--enable-gpl/nonfree 없음)를 별도 프로세스로 호출(동적 분리)해 LGPL 준수 — gyan.dev/BtbN gpl 빌드(GPLv3) 번들 절대 금지. ExternalToolDetector.TryFindFfmpeg가 (a)%LOCALAPPDATA%\Everything2Everything\ffmpeg, (b)시스템 PATH 순 탐지, 없으면 lgpl-shared zip을 SHA256 검증 후 다운로드. GlobalFFOptions.Configure로 경로 고정. NVENC는 LGPL 빌드에서 --enable-nonfree 없이 합법 사용 가능(NVIDIA 공식 확인). About 화면에 'uses FFmpeg under LGPLv2.1' 고지 + 소스 다운로드 링크(LGPL 의무). MSIX 변형에서는 샌드박스 정책상 lgpl-shared DLL을 패키지 동봉(여전히 LGPL 준수). Ghostscript/MuPDF(AGPL)는 사용자 설치본 감지만, codec 특허(H.264/AAC) 위험을 줄이려 AV1/VP9/Opus/FLAC(royalty-free)를 기본 권장 출력으로.

    +
    + +
    07

    실행 로드맵 · P1 → P8

    +

    각 단계가 독립적으로 가치를 전달하고 이전 단계에 의존한다. P1은 그래프 코어 + 즉시 체감(PDF 압축)을 함께 심는다.

    + +
    +
    P1
    +
    그래프 엔진 도입 + 즉시 체감 가치(PDF 압축)
    ProviderRegistry를 ConversionGraph로 승격하고 멀티홉 경로 탐색을 엔진에 내장한다. 동시에 PDF 압축이라는 즉시 체감 신기능을 출시해 '보이지 않는 리팩터링의 함정'을 회피한다.
    +
    예정LRISK medium
    +
    +
    +

    산출물

    • ConversionGraph + 자체 Dijkstra PathFinder(외부 의존성 0, .NET 9 PriorityQueue)
    • ConversionPair.LossClass 필드 + 정적 가중치 테이블
    • ConversionEngine.ConvertOneAsync 그래프 위임 + ChainExecutor(공용 workDir 헬퍼)
    • PdfToolProvider 신설: PDF 압축(Light=PDFsharp 구조최적화, Strong=PDFium 렌더+Magick 재인코딩, Max=Ghostscript 외부폴백) + 병합/분할
    • xUnit 테스트 프로젝트 신설(현재 0개) + 그래프 경로탐색 회귀 테스트
    +

    핵심 코드 변경

    ProviderRegistry.cs
    _byPair 위에 인접 리스트 그래프 빌드, 증분 등록 Register/Rebuild 추가
    ConversionEngine.cs:91
    TryGet 직접 매핑에서 그래프 FindBestPath→ExecuteChainAsync 위임으로 전환
    ConversionPair
    LossClass 필드 추가, 엣지 가중치 SSOT
    신규 PdfToolProvider
    동일포맷 pdf→pdf Skip(ConversionEngine.cs:88) 우회, 3단계 압축
    + +
    Exit Criteria
    기존 모든 변환이 그래프 경로로 동일 동작(회귀 테스트 통과)하고, PDF 파일을 3단계 레벨로 압축해 출력 용량 감소를 GUI에서 확인 가능.
    +
    +
    P2
    +
    손그림 멀티홉 제거 + HWP 한글 양방향
    DocumentProvider.RouteAsync의 손코딩 switch를 삭제하고 원자 엣지만 선언하게 해 그래프를 도그푸딩한다. HWP→DOCX/HTML/TXT 출력 매트릭스를 확장해 한글 사용자 핵심 요구를 충족.
    +
    예정MRISK mediumdepends · P1
    +
    +
    +

    산출물

    • DocumentProvider.RouteAsync(92-205) 삭제 → md→html, html→docx 등 원자 엣지만 선언, md→docx는 엔진 자동 합성
    • HWP/HWPX 출력 확장: HwpxProvider Outputs에 .docx/.html/.txt/.odt 추가(soffice --convert-to 파라미터화)
    • .hwp 입력 시 --infilter='Hwp2002_File' 조건부 지정 + 함초롬/맑은고딕 폰트 누락 감지 경고
    • DocumentProvider 입력에 .pdf 추가 → pdf→docx/html/txt 역변환(soffice) + pdf→txt 무외부 폴백(PdfPig)
    • RouteAsync 삭제가 손그림과 동일 동작함을 회귀 테스트로 증명
    +

    핵심 코드 변경

    DocumentProvider.cs:92-205
    멀티홉 switch 삭제, 단일 홉 원자 변환만 선언
    HwpxProvider
    Outputs 배열에 .docx/.html/.txt/.odt 추가, soffice 타깃 파라미터화
    DocumentProvider Inputs
    .pdf 추가로 PDF 역변환 엣지 개통
    + +
    Exit Criteria
    HWP/HWPX 파일을 DOCX/HTML/TXT/PDF로 변환 가능하고, md→docx 같은 멀티홉이 RouteAsync 없이 그래프 합성으로 동일하게 동작.
    +
    +
    P3
    +
    외부 프로세스 통합 + 인터페이스 일반화
    3중 복제된 LibreOffice 호출을 단일 ExternalProcessRunner로 통합하고(타임아웃·stderr·Kill), IConverterProvider/ConvertResult를 일반화해 미디어·AI 엣지가 들어올 그릇을 판다.
    +
    예정LRISK mediumdepends · P2
    +
    +
    +

    산출물

    • ExternalProcessRunner(CliWrap): 타임아웃+stderr수집+Kill 통합, LibreOffice 3중 복제 흡수
    • Abstractions 어셈블리 분리(IConverterProvider/ConvertResult 이전, 타입 동일성)
    • IConverterProvider→ConvertRequest/ConvertContext 일반화, 기존 8개 Provider 어댑터로 무중단 마이그레이션
    • ConvertResult에 ExtractedText/AiResponse/Metadata/IntermediateArtifacts 필드
    • ISettingsStore(DPAPI 암호화) 신설 — API 키·도구 경로 영속화 토대
    • Priority 기반 충돌 모델로 _byPair.TryAdd first-wins 교체
    +

    핵심 코드 변경

    3개 Provider
    ConvertWithLibreOfficeAsync 복붙을 ExternalProcessRunner로 통합
    IConverterProvider.cs:9-15
    ConvertRequest/ConvertContext로 일반화
    ConvertResult.cs
    비파일 산출물 필드 추가
    신규 ISettingsStore
    DPAPI ProtectedData 암호화 JSON 영속화
    + +
    Exit Criteria
    LibreOffice가 멈춰도 타임아웃으로 복구되고 stderr가 에러 메시지에 포함되며, 기존 변환이 일반화된 시그니처로 무중단 동작(회귀 테스트 통과).
    +
    +
    P4
    +
    미디어 레이어 — 영상/오디오 코덱·압축
    FFmpeg로 카테고리를 '미디어 변환기'로 점프시킨다. 영상/오디오 N×M 코덱·압축을 라이선스 안전하게 통합하고 배치 병렬화로 트랜스코딩 병목을 해소.
    +
    예정XLRISK highdepends · P3
    +
    +
    +

    산출물

    • FfmpegProvider: FFMpegCore(MIT) + 영상(mp4/mkv/webm/mov/avi/gif)·오디오(mp3/aac/m4a/opus/flac/wav) N×M
    • 바이너리 조달: ExternalToolDetector.TryFindFfmpeg + BtbN lgpl-shared 자동 다운로드(SHA256 검증), GlobalFFOptions 경로 고정
    • HW 인코더(nvenc/qsv/amf) 우선 + SW 폴백, NotifyOnProgress→IProgress 직결, CancellableThrough(ct)
    • 배치 병렬화: ConvertManyAsync 순차 for-loop(57-70)를 Parallel.ForEachAsync로 교체(MaxDegreeOfParallelism)
    • PreviewService→IPreviewRenderer 추상화 + FFmpeg 프레임 추출 + 프리뷰 캐시
    • ImageMagick ResourceLimits 전역 설정(decompression bomb 방어) + NU190x 취약점 경고 재활성화
    +

    핵심 코드 변경

    신규 FfmpegProvider
    FFMpegCore 래퍼, HW 가속 폴백, RequiresExternal
    ConversionEngine.cs:57-70
    순차 for-loop를 Parallel.ForEachAsync로 교체
    PreviewService.cs:23
    닫힌 switch를 IPreviewRenderer 레지스트리로, 영상 프레임 추출 추가
    +

    신규 Provider

    + FfmpegProvider
    +
    Exit Criteria
    mp4→webm, wav→mp3 등 영상/오디오 변환이 HW 가속으로 동작하고 진행률·취소가 정확하며, 100개 배치가 멀티코어를 활용.
    +
    +
    P5
    +
    AI 부가가치 레이어 — Codex OAuth + API
    변환에 'AI가 더 좋게 만든다'는 해자를 얹는다. 기본은 API 키 + 공식 SDK, Codex CLI는 구독자용 opt-in. 키 없으면 AI 페어만 비활성, 기존 변환 무영향.
    +
    예정LRISK highdepends · P3
    +
    +
    +

    산출물

    • LlmProvider: Microsoft.Extensions.AI(IChatClient)로 OpenAI/Anthropic 공식 SDK 연결 + Codex CLI opt-in(codex exec --json --output-schema)
    • AI 매트릭스: 요약(pdf/docx/txt→txt/md), 번역(→대상언어), OCR교정(OcrProvider 출력 2단계 파이프), 이미지 캡션(png/jpg→txt 비전), 메타데이터(→json Structured Output)
    • 키 관리: ISettingsStore DPAPI 암호화 + OPENAI_API_KEY/ANTHROPIC_API_KEY 환경변수 폴백, CheckAvailabilityAsync 게이트
    • UI: AI 출력 페어에 ✨AI 배지(종량과금·네트워크 명시) + 설정에서 백엔드/모델/키 입력
    • Codex 경로 SemaphoreSlim(1) 직렬화(auth.json refresh 토큰 race 방지) 또는 --ephemeral
    +

    핵심 코드 변경

    신규 LlmProvider
    IConverterProvider로 구현, AI는 로컬 변환 없는 신규 엣지로만
    CheckAvailabilityAsync
    키/codex --version 게이트로 키 부재 시 NotReady→그래프 자동 비활성
    UI
    AI 페어 ✨ 배지, 등록 순서로 기본 경로 불점유 보장
    +

    신규 Provider

    + LlmProvider
    +
    Exit Criteria
    API 키 입력 시 PDF 요약·이미지 캡션·번역이 동작하고, 키가 없으면 AI 페어만 사라지고 모든 기존 변환은 100% 동작.
    +
    +
    P6
    +
    매트릭스 자동 극대화 + 헤드리스 CLI
    앞 단계에서 쌓인 모든 엣지를 그래프가 자동 합성해 진짜 N×M·다방향을 완성하고(video→mp3→txt AI전사 등), 헤드리스 CLI로 자동화·스크립팅을 개방한다.
    +
    예정LRISK mediumdepends · P5
    +
    +
    +

    산출물

    • OutputsForInput을 transitive closure로 확장 — '이 파일로 만들 수 있는 모든 포맷' UI 노출 + 손실 경로 경고 배지
    • 멀티홉 도그푸딩 검증: hwp→pdf→png, video→mp3→txt(AI) 같은 신규 합성 경로 동작 확인
    • 헤드리스 CLI 분리: --json/--output-dir/--quality/--prompt/--codec/--recursive 플래그 + stdout JSON 결과 + exit code
    • 워치폴더 모드(FileSystemWatcher + 디바운스 + 파일잠금 재시도, 출력 디렉터리 분리로 무한루프 방지)
    • QuickProgressWindow 취소 토큰 전파 + 케이퍼빌리티 사전 점검
    +

    핵심 코드 변경

    ProviderRegistry.cs:52
    OutputsForInput을 그래프 reachability로 확장
    CliRouter.cs:21
    옵션 플래그 파싱 + stdout JSON + exit code
    App.xaml.cs:96
    Quick 경로에 취소 토큰 전파
    + +
    Exit Criteria
    HWP 파일에서 PNG까지(멀티홉) 변환 가능하고, CLI가 WPF 창 없이 JSON 결과를 stdout으로 반환해 스크립트가 파싱 가능.
    +
    +
    P7
    +
    순수 .NET 카테고리 보강 + manifest 어댑터
    EXE 번들 가능한 순수 관리 라이브러리로 빈 카테고리를 채우고, 단순 CLI 도구를 코드 없이 추가하는 좁은 manifest 어댑터를 도입한다.
    +
    예정LRISK lowdepends · P6
    +
    +
    +

    산출물

    • ArchiveProvider(SharpCompress, 순수관리) — zip/7z/tar/gz/bz2
    • DataProvider(Parquet.Net/ClosedXML/CsvHelper) — csv↔json↔xlsx↔parquet
    • VectorProvider(Svg.Skia) — svg→png/jpg/webp/pdf, EPS는 Magick+Ghostscript
    • PandocProvider(외부 CLI) — md/rst/latex/ipynb/epub 마크업 매트릭스, LibreOffice 겹침은 Priority 라우팅
    • EbookProvider(Calibre ebook-convert, 외부) — epub↔mobi↔azw3↔pdf
    • manifest 어댑터(ExternalProcessRunner 위 인자 템플릿): qpdf/gs 같은 단순 CLI 코드 없이 추가
    +

    핵심 코드 변경

    신규 4-5개 Provider
    순수 .NET은 EXE 직접 포함, 외부 CLI는 분리 호출
    ManifestLoader
    tools/*.manifest.json으로 단순 CLI 엣지 추가
    Bootstrap
    하드코딩 배열에 신규 Provider 등록 + manifest 동적 등록
    +

    신규 Provider

    + ArchiveProvider+ DataProvider+ VectorProvider+ PandocProvider+ EbookProvider
    +
    Exit Criteria
    zip 압축/해제, csv→xlsx, svg→png가 외부 도구 없이 동작하고, manifest 파일 하나로 새 CLI 변환 도구를 코어 재컴파일 없이 추가 가능.
    +
    +
    P8
    +
    확장성·신뢰성·배포 굳히기
    기능이 다 들어온 뒤 회귀 방지·UI 분해·배포를 다진다. 차별화는 끝났으니 여기서부터는 깨지지 않게 유지.
    +
    예정LRISK lowdepends · P7
    +
    +
    +

    산출물

    • UI MVVM 분해(MainWindow.xaml.cs 1171줄) + Provider 선언형 옵션 스키마 기반 동적 옵션 UI 생성
    • 히스토리 도메인 로직을 Core로 분리 + 데모 시드 제거 + 스트리밍 로드/회전 정책
    • CI 강화: NuGet 캐시 + self-contained portable EXE 산출 + 외부 바이너리 번들링 파이프라인(FFmpeg LGPL 고지) + dotnet test 게이트
    • 출력 형식 매트릭스 3중 중복(AllFormats/PopularOutputs/파일다이얼로그)을 단일 FormatCatalog로 통합
    • 테스트 확대: OutputPathHelper 충돌·결합 로직·JSONL round-trip 순수 함수 커버
    +

    핵심 코드 변경

    MainWindow.xaml.cs
    MVVM 분해, 동적 옵션 UI
    BuildMsix.ps1
    외부 바이너리 번들 + 라이선스 고지 단계
    build.yml/release.yml
    캐시+테스트 게이트+self-contained 산출물
    + +
    Exit Criteria
    PR마다 테스트가 게이트로 동작하고, self-contained portable EXE가 자동 산출되며, 새 형식 추가가 단일 FormatCatalog 한 곳 수정으로 끝남.
    +
    +
    + +
    08

    성공 지표

    +

    현재 → 목표. 각 지표가 로드맵 완료를 객관적으로 측정한다.

    +
    멀티홉 경로 자동 합성
    +
    DocumentProvider.RouteAsync에 손코딩된 3-4개 체인만 동작
    엔진이 임의 A→Z를 그래프 탐색으로 자동 합성, RouteAsync 0줄
    입력당 도달 가능 출력 포맷 수
    +
    1-hop 직접 출력만(OutputsForInput 직접 매핑)
    transitive closure로 확장된 도달 가능 전체 포맷 + 손실 배지
    지원 카테고리 수
    +
    이미지/PDF/문서/HEIC/OCR (약 5)
    +영상/오디오/아카이브/데이터/벡터/전자책/AI (약 12)
    PDF 압축 기능
    +
    어떤 Provider도 수행 불가
    3단계 레벨(Light/Strong/Max) 압축 + 병합/분할
    HWP 출력 매트릭스
    +
    →PDF/이미지만, →DOCX/HTML/TXT 미노출
    HWP→DOCX/HTML/TXT/PDF 완성
    코어 테스트 커버리지
    +
    테스트 프로젝트 0개
    그래프 탐색·OutputPathHelper·결합·JSONL round-trip 커버 + CI 게이트
    배치 처리 동시성
    +
    순차 for-loop(코어 1개만 사용)
    Parallel.ForEachAsync(MaxDegreeOfParallelism)로 멀티코어 활용
    CLI 자동화 가능성
    +
    WPF 창만 띄우고 stdout 무반환
    --json/--codec/--prompt 플래그 + stdout JSON + exit code
    +
    + +
    09

    리스크 레지스터

    +

    가장 큰 위협은 "보이지 않는 리팩터링의 함정"과 GPL/AGPL 라이선스 오염.

    + + + + + + + + + + + + + + + + + +
    리스크발생가능영향완화책
    '보이지 않는 리팩터링의 함정' — 그래프 코어 재설계가 사용자 체감 변화 0인 상태로 길어짐RISK mediumRISK highP1에서 그래프 도입과 PDF 압축(즉시 체감 신기능)을 묶고, transitive closure로 늘어나는 '만들 수 있는 포맷 목록'을 가시 성과로 노출. DocumentProvider.RouteAsync 삭제를 회귀 테스트로 동일 동작 증명.
    '최단 경로' 압박으로 LlmProvider/FfmpegProvider를 또 하드코딩 switch로 끼워넣어 RouteAsync 지옥 재생산RISK mediumRISK highP1에 그래프 코어를 먼저 심어 하드코딩을 구조적으로 차단. '신규 기능은 그래프 엣지로만 추가'를 불변식으로 명문화하고 코드 리뷰 게이트로 강제.
    GPL/AGPL 바이너리(FFmpeg gpl빌드·Ghostscript·H2Orestart) 정적 링크로 상업 배포 라이선스 오염RISK mediumRISK high모든 무거운 외부 도구를 별도 프로세스 분리 호출 + 사용자 설치 감지/LGPL 빌드 자동조달로만 통합. 라이선스 경계를 코드 리뷰 게이트로 강제(ADR-6).
    멀티홉 손실 누적·은폐 — HWP→PDF(래스터화)→DOCX가 '편집가능'을 약속하나 이미지 덩어리 반환RISK mediumRISK mediumLossClass 가중치로 래스터화에 큰 페널티, 멀티홉은 직접 엣지 없을 때만, MaxHops=3, 손실 블랙리스트, 손실 경로 UI 경고 배지 3겹 가드레일.
    인터페이스 일반화(ConvertRequest)가 8개 기존 Provider를 한 번에 깸RISK mediumRISK high기존 시그니처를 어댑터로 감싸 점진 마이그레이션, 무중단을 회귀 테스트로 보장. P3에 배치해 미디어/AI 동기가 코드에 들어온 뒤 일반화.
    AI 비결정성·종량과금·네트워크 의존이 '로컬 예측가능 변환' 신뢰를 깸RISK highRISK mediumAI는 기본 경로 불점유, ✨AI 배지 opt-in, 키 없으면 조용히 비활성을 설계 불변식으로 박음. 토큰/비용 표시, 사용자 확인 게이트, 재시도·백오프.
    테스트 0개 상태에서 대규모 코어 변경이 회귀를 탐지 못 함RISK highRISK highP1에서 xUnit 테스트 프로젝트를 최우선 신설하고 그래프 경로탐색·DocumentProvider 회귀를 첫 안전망으로. CI에 dotnet test 게이트 추가.
    manifest가 또 다른 갓 오브젝트화 — FFmpeg HW가속 폴백 같은 복잡 로직을 manifest로 표현 시도RISK lowRISK mediummanifest는 '90% 단순 CLI(qpdf/gs)만, 복잡 로직은 in-box 코드 Provider' 경계를 P1부터 불변식으로 명문화.
    +
    + +
    10

    다음 세션 인계 노트

    +

    이 문서가 SSOT다. 다음 세션은 아래 순서대로 시작한다.

    +
    ▶ Handoff — 어디서부터 시작하고 무엇을 먼저 검증할지
    +

    다음 세션은 P1(그래프 엔진 도입 + PDF 압축)부터 시작한다. 시작 순서와 검증 포인트:

    1) 가장 먼저 xUnit 테스트 프로젝트를 신설하라(현재 0개). 이게 모든 코어 변경의 안전망이며, 특히 DocumentProvider.RouteAsync 삭제가 '손그림과 동일 동작'임을 증명할 회귀 테스트의 전제다. 먼저 현재 RouteAsync의 모든 경로(md→docx, docx→md, hwp→html 등)에 대한 골든 테스트를 작성해 baseline을 고정하라.

    2) ConversionGraph + 자체 Dijkstra를 ProviderRegistry 옆에 얇게 얹어라. ProviderRegistry.cs:16-30 생성자 루프에 그래프 빌드 한 단계만 추가. _byPair는 유지(직접 엣지 1홉 호환). ConversionPair에 LossClass 필드 추가가 선결.

    3) 첫 검증: ConversionEngine.ConvertOneAsync(91)를 그래프 위임으로 바꾼 뒤, 기존 모든 변환이 동일 동작하는지 회귀 테스트로 확인. 그 다음에야 RouteAsync를 삭제하고 원자 엣지만 선언하게 바꿔 md→docx가 그래프 합성으로 동일하게 나오는지 검증.

    4) PDF 압축(PdfToolProvider)은 ConversionEngine.cs:88의 동일포맷 Skip을 우회해야 한다 — pdf→pdf를 엣지로 허용하는 메커니즘이 그래프 도입과 함께 필요. PDFsharp(MIT) in-process 압축부터 시작하면 외부 의존성 0으로 즉시 체감 가치.

    먼저 검증할 불변식: (a) 기존 8개 Provider 변환이 그래프 경로로 100% 동일 동작, (b) 멀티홉은 직접 엣지 없을 때만 발동, (c) 손실 경로에 가중치가 정확히 반영되는지. 라이선스 게이트(GPL/AGPL 분리 호출)는 P4(미디어)부터 본격 적용되지만, P1의 Ghostscript 폴백에서도 '사용자 설치본 감지만, 번들 금지' 원칙을 처음부터 지켜라.

    참고: 빌드 후에는 메모리의 project_build_pipeline(publish + 카스케이드 재등록 PowerShell 시퀀스)를 따르고, 사용자가 직접 push & GUI 검증하는 워크플로이므로 큰 결정은 빠른 승인 후 단일 commit으로 진행.

    +
    +
    + +
    11

    설계안 비교 · 심사

    +

    3개 독립 아키텍트가 서로 다른 각도에서 제안했고, 심사가 점수화·종합했다.

    +
    +
    +

    변환 그래프 코어 우선 — "모든 변환은 엣지(edge)이고, 엔진은 라우터(router)다." Provider를 손으로 체이닝하는 대신, 모든 변환을 그래프의 단방향 엣지로 등록하고 멀티홉 경로 탐색(Dijkstra)이 N×M·다방향을 자동으로 합성하게 만든다. UI/AI/미디어/PDF/HWP는 전부 이 그래프 위에 '엣지를 더하는 것'으로 환원된다.

    +
    84
    +

    Everything2Everything의 북극성은 "원자적 변환(atomic conversion)의 조합으로 임의의 A→Z를 자동 합성하는 변환 그래프 OS"다. + +현재 코드의 가장 강력한 증거는 DocumentProvider.RouteAsync(DocumentProvider.cs:92-205)다. 이건 사람이 손으로 그린 Dijkstra다 — `md→docx`를 `md→html→docx`로, `docx→md`를 `docx→html→md`로 중첩 switch에 박아넣었다. 형식이 N개로 늘면 이 손그림 그래프는 O(N²)로 폭발하고, 새 형식 하나가 모든 분기를 건드린다. 비전의 핵심…

    +

    핵심 변경

    +
    • ProviderRegistry를 그래프로 승격: _byPair 단일 룩업(ProviderRegistry.cs:6,43) 옆에 ConversionGraph(인접 리스트 Dictionary<string,List<Edge>>)를 빌드하고, 자체 Dijkstra FindBestPath(in,out,options)를 추가. 외부 라이브러리 없이 80~120줄, 단일 EXE/AOT 친화.
    • ConversionPair에 LossClass(Lossless/Container/Recode/Rasterize) 필드 추가 → 엣지 가중치의 단일 출처(SSOT). 충돌 시 _byPair.TryAdd의 '조용한 첫 등록자 우선'(ProviderRegistry.cs:22)을 비용/우선순위 기반 명시 선택 + 진단 경고로 교체.
    • ConversionEngine.ConvertOneAsync(ConversionEngine.cs:91)를 그래프 위임으로 전환: 직접 엣지가 없거나 더 싼 멀티홉이 있으면 ExecuteChainAsync가 경로의 각 홉을 순차 실행(중간 산출물은 DocumentProvider의 workDir 패턴을 공용 헬퍼로 승격해 재사용). 진행률은 홉 수로 분할 매핑.
    • DocumentProvider.RouteAsync의 손그림 멀티홉(:92-205)을 삭제 → DocumentProvider는 md→html, html→docx 같은 원자 엣지만 선언. md→docx는 엔진이 자동 합성. 이것이 그래프 코어의 첫 검증(dogfooding).
    • IConverterProvider 시그니처(IConverterProvider.cs:9-15)를 ConvertRequest/ConvertContext 객체로 일반화 — 다중 입력(N→1 결합), 비파일 결과(추출 텍스트·AI 응답·미디어 메타데이터), 멀티홉 중간 컨텍스트, 풍부한 진행률을 표현. ConvertResult(ConvertResult.cs:10)에 비파일 산출물 필드 추가.
    • CombineAsync의 ImageMagick 직접 의존(ConversionEngine.cs:2,164-257)을 IMultiInputProvider(N→1 엣지)로 분리 → 엔진의 라이브러리 결합 제거. 결합 가능 형식의 정적 HashSet(ConversionEngine.cs:14-23)을 Provider 능력 선언으로 통합.
    +

    최대 리스크 · 그래프 코어 재설계가 '엔진은 깔끔해졌는데 사용자에게 보이는 변화가 0'인 상태로 Phase 0~1을 길게 끄는 '보이지 않는 리팩터링의 함정'이 가장 큰 위험이다. 그래프 엔진은 그 자체로는 데모할 게 없다 — DocumentProvider.RouteAsync를 삭제

    +
    +
    +

    플러그인 생태계 우선 (Plugin-Ecosystem-First): 코어를 변환 호스트(host)로 축소하고, 모든 변환 능력을 선언적 manifest + 어댑터 Provider로 외부화한다. "코드를 늘려 포맷을 늘리는" 모델에서 "manifest를 늘려 포맷을 늘리는" 모델로 전환.

    +
    71
    +

    Everything2Everything을 단일 모놀리식 변환기가 아니라 "변환 능력의 OS"로 재정의한다. 코어는 더 이상 변환을 '아는' 주체가 아니라, 변환 능력을 선언받아 조합·라우팅·실행하는 얇은 호스트가 된다. 세상의 모든 변환 도구(FFmpeg/Ghostscript/Pandoc/LibreOffice/Calibre/qpdf/LLM)는 코드가 아닌 선언적 manifest(JSON)로 등록되며, 코어는 이 능력들을 방향 그래프로 합성해 manifest 작성자가 한 번도 명시하지 않은 멀티홉 경로(HWP→PDF→DOCX, PNG→PDF 압축→DOCX)까지 자동으로…

    +

    핵심 변경

    +
    • 코어 분리 + 어댑터 베이스 추출: IConverterProvider를 Everything2Everything.Abstractions 별도 어셈블리로 분리(타입 동일성 보장)하고, 7개 in-box Provider의 외부 프로세스 호출 로직(현재 DocumentProvider.SofficeConvertAsync:238-281이 3개 Provider에 복붙됨)을 단일 ExternalToolProvider 추상 베이스 + ProcessRunner(CliWrap 기반, 타임아웃·stderr 수집·Kill 통합)로 통합. 이 베이스가 manifest 어댑터의 실행 엔진이 된다.
    • 선언적 Manifest + 동적 등록 파이프라인: tools/*.manifest.json 스키마를 기존 ProviderCapability/ConversionPair/ExternalDependency 모양 그대로 직렬화한 형태로 정의. manifest는 (tool id, 실행파일 탐지 규칙, 입력×출력 매트릭스, argument 템플릿 {input}/{output}/{outdir}/{format}, 성공 판정, LossClass)를 선언. ManifestLoader가 런타임에 읽어 ExternalToolProvider 인스턴스로 합성. ProviderRegistry를 '닫힌 생성자'에서 Register/RegisterRange/Rebuild가 가능한 '증분 등록' 구조로 개조(현재 생성자 17-30행 인덱싱 로직을 private Index(provider)로 추출).
    • ProviderRegistry → ConversionGraph 승격 + 멀티홉 경로 탐색: 모든 Provider의 Capability.SupportedConversions를 순회해 방향 그래프(노드=확장자, 엣지=Provider+LossClass 가중치) 구축. 외부 의존성 없는 자체 Dijkstra(~100줄)로 최저손실 경로 탐색. ConversionEngine.ConvertOneAsync(91행)에서 직접 엣지가 없으면 그래프 탐색으로 폴백, 경로의 각 홉을 ExecuteChainAsync로 순차 실행(중간 산출물은 DocumentProvider의 workDir 패턴 재사용). DocumentProvider의 손으로 짠 RouteAsync switch는 '단일 홉 원자 변환'만 선언하도록 분해 → md→docx 같은 경로는 엔진이 자동 합성.
    • 레지스트리 충돌 모델 교체: _byPair.TryAdd(22행)의 '조용한 first-wins'를 ProviderCapability.Priority 필드 + 명시적 다중 Provider 공존 모델로 교체. 같은 (input,output)에 빠른변환/고품질/AI 등 복수 전략을 등록하고 우선순위·진단으로 선택. 이로써 manifest 어댑터가 in-box Provider를 덮어쓰는 사고를 방지하고 변환 전략 다중화 가능.
    • ConvertRequest/ConvertResult 일반화 + 옵션 백 분해: IConverterProvider 시그니처를 단일 sourcePath/outputExtension(IConverterProvider.cs:9-15)에서 ConvertRequest(다중 입력·미디어 메타·옵션 백)/ConvertContext로 일반화. ConvertResult(현재 OutputPaths만, ConvertResult.cs:10)에 추출 텍스트·AI 응답·메타데이터 필드 추가. ConvertOptions 갓 오브젝트(11개 sub-record)를 manifest별 IReadOnlyDictionary 옵션 백으로 분해해 Video/Audio/Ai 옵션을 sub-record 증식 없이 수용.
    • AI는 또 하나의 manifest 어댑터: LlmProvider를 Microsoft.Extensions.AI(IChatClient) 추상화 위에 구축. 기본 경로는 API 키(OpenAI/Anthropic 공식 SDK), opt-in 보조 경로는 Codex CLI(codex exec --json --output-schema, PATH 감지 시만 활성). 키는 DPAPI(ProtectedData)로 암호화 저장하는 ISettingsStore 신설. CheckAvailabilityAsync가 키/CLI 부재 시 NotReady→그래프에서 자동 비활성 노드 처리. AI는 로컬 변환이 없는 신규 페어(요약/번역/캡션/메타데이터)에만 노출, UI에 'AI' 배지.
    +

    최대 리스크 · manifest 추상화의 '표현력 천장'과 멀티홉 신뢰성이 동시에 무너지는 것. (1) 선언적 argument 템플릿({input}/{output}/{format})은 단순 CLI 도구에는 완벽하지만, FFmpeg의 코덱별 HW가속 폴백(nvenc 실패→AV1)이나 조건부 인자처럼 &#

    +
    +
    +

    AI·미디어 기능 우선 — 사용자가 명시한 신규 가치(Codex/API AI 통합, 영상/오디오/PDF 압축, HWP 한글 변환)를 최단 경로로 출시하고, 그래프/레지스트리 리팩터링은 '그 기능을 켜기 위한 최소 인프라'로만 취급한다. 인프라 완성도가 아니라 사용자 체감 차별화가 북극성이다.

    SELECTED BASE
    +
    89
    +

    Everything2Everything을 '무엇이든 → 무엇이든, 그리고 변환하면서 더 좋아지는' 도구로 만든다. 핵심 차별화는 두 가지다. (1) 변환이 단순 포맷 치환이 아니라 'AI 부가가치 레이어'를 거친다 — 영상을 mp4로 바꾸면서 자동으로 자막을 뽑고, PDF를 압축하면서 OCR 오탈자를 LLM이 교정하고, HWP를 DOCX로 풀면서 요약·번역을 곁들인다. AI는 변환의 핵심 엔진이 아니라 '후처리 부가가치 단계'로 배치해, API 키가 없어도 모든 기존 변환은 100% 동작하고 AI 페어에만 '✨ AI' 배지가 붙는다…

    +

    핵심 변경

    +
    • ConvertContext/ConvertRequest 도입으로 IConverterProvider 시그니처 일반화 — 단일 sourcePath/단일 outputExtension/IProgress<double> 고정(IConverterProvider.cs:9-15)을 다중 입력·비파일 결과(추출 텍스트·AI 응답·미디어 메타데이터)·다단계 진행률을 담는 컨텍스트 객체로 교체. AI·미디어·압축 Provider가 요구하는 모든 표현을 인터페이스 레벨에서 한 번에 연다.
    • ConvertOptions 갓-오브젝트(ConvertOptions.cs)를 형식별 옵션 백으로 분해하고 Ai/Media/PdfCompress 옵션 그룹 신설 — 영상 코덱·CRF·fps, 오디오 비트레이트, AI 모델·프롬프트·온도·백엔드(openai|anthropic|codex-cli|auto), PDF 압축 레벨(Light/Strong/Max)을 담을 자리를 만들고 DPAPI 암호화 설정 영속화 계층(ISettingsStore)을 신설해 API 키·도구 경로를 안전 저장.
    • ProviderRegistry를 얇은 ConversionGraph로 승격 — _byPair 단일홉(ProviderRegistry.cs:6,43) 위에 인접 리스트 그래프를 얹고 외부 의존성 없는 자체 Dijkstra(80~120줄, 비용=손실가중+홉페널티)로 멀티홉 경로를 자동 합성. DocumentProvider.RouteAsync(DocumentProvider.cs:92-205)의 손으로 짠 md→html→docx switch를 '원자 변환 선언 + 엔진 자동 합성'으로 대체해 형식 추가 시 O(N²) 수동 증식을 제거.
    • 공통 ExternalProcessRunner + 외부 도구 어댑터 추상화 신설 — 3곳에 복붙된 LibreOffice 호출(DocumentProvider/DocxProvider/HwpxProvider)을 타임아웃·stderr 수집·Kill 통합 단일 러너로 합치고, FFmpeg·Ghostscript·qpdf·codex CLI를 manifest 기반으로 동일 패턴 재사용. 신규 외부 도구 추가가 보일러플레이트 복붙 없이 끝나게.
    • 신규 1급 Provider 4종 추가 — LlmProvider(요약·번역·캡션·OCR교정·메타데이터, MEAI IChatClient 추상화 + Codex CLI opt-in), FfmpegProvider(영상/오디오 N×M 코덱·압축, RequiresExternal + LGPL 빌드 자동조달), PdfToolProvider(PDF→PDF 압축/병합/분할, PDFsharp in-process + gs 고급압축 폴백), 그리고 DocumentProvider 출력 매트릭스 확장으로 HWP→DOCX/HTML/TXT/PDF 완성.
    • 배치 병렬화 + 헤드리스 CLI — 순차 for-loop(ConversionEngine.cs:57-70)를 Parallel.ForEachAsync로 교체(AI 왕복·영상 트랜스코딩의 치명적 병목 해소)하고, stdout JSON + exit code + 옵션 플래그(--quality/--prompt/--codec/--json)를 받는 headless 모드를 분리해 AI 스크립팅·배치 자동화를 가능케.
    +

    최대 리스크 · AI·미디어 기능을 최단 경로로 밀다 보면 'Phase 0 인프라 일반화'를 건너뛰고 LlmProvider/FfmpegProvider를 또 하드코딩으로 끼워넣으려는 유혹이 가장 크다 — 그러면 DocumentProvider.RouteAsync처럼 새 switch 지옥이 카테고리마다 생겨

    +
    +
    심사 종합 권고

    승자는 idx 2(AI·미디어 기능 우선, 89점)를 '실행 골격'으로, idx 0(그래프 코어, 84점)을 '아키텍처 영혼'으로 삼아 종합한다. 단독 채택이 아니라 두 안의 합성이 정답이다.

    핵심 통찰: 세 안의 기술적 부품(자체 Dijkstra 멀티홉, ExternalProcessRunner 통합, ConvertRequest/ConvertResult 일반화, LossClass 가중치, Priority 충돌 모델, AI=엣지, DPAPI 키저장)은 사실상 동일하다. 진짜 차이는 '무엇을 북극성으로 삼아 순서를 짜느냐' 하나뿐이다. 이 프로젝트는 단일 개발자가 직접 push하고 GUI로 검증하며 큰 결정을 빠르게 승인하는 워크플로(프로젝트 메모리)이고, 사용자가 명시적으로 요구한 것은 AI/미디어/HWP/PDF압축이라는 '기능'이다. 따라서 '보이지 않는 리팩터링의 함정'(idx 0 본인이 인정한 최대 리스크)에 빠지는 그래프-우선 순서는 이 맥락에서 부적합하다.

    그러나 idx 2의 최대 리스크('최단 경로 압박으로 LlmProvider/FfmpegProvider를 또 하드코딩 switch로 끼워넣어 RouteAsync 지옥 재생산')는 실재하고, idx 0의 그래프 코어가 바로 이 리스크의 백신이다. 그래서 둘을 봉합하는 마스터플랜은 다음 순서다:

    Phase 0 (기반, 그러나 즉시 가치와 묶기 — idx 0의 자기 완화책 채택): ExternalProcessRunner 통합(3중 복제 제거) + Priority 충돌 모델 + ConvertRequest/ConvertResult 일반화(어댑터로 무중단). 동시에 DocumentProvider.RouteAsync를 원자 엣지로 분해하고 자체 Dijkstra를 넣어 '손그림=그래프 동일 동작'을 회귀 테스트로 증명(테스트 0개 탈출의 첫걸음). 이 단계의 가시 성과는 transitive closure로 늘어나는 '만들 수 있는 포맷 목록'.

    Phase 1 (즉시 체감 — idx 2 순서): HWP 출력 매트릭스 확장(이미 깔린 LibreOffice+H2Orestart 배관 재사용 → DOCX/HTML/TXT) + PDF 압축. 이때 신규 기능은 반드시 '그래프 엣지'로만 추가한다는 것을 불변식으로 박아 idx 2의 하드코딩 유혹을 Phase 0 그래프가 구조적으로 차단.

    Phase 2~3 (미디어 + AI 부가가치 레이어 — idx 2): FFmpeg/Ghostscript/qpdf를 idx 2의 라이선스 게이트(분리 프로세스·LGPL/AGPL 경계) 하에 엣지로 추가. AI는 idx 0의 '엣지' + idx 2의 '✨AI 배지·키 없으면 비활성·기본 경로 불점유' 이중 불변식으로 통합. 배치 병렬화는 이 시점에 필수.

    idx 1(플러그인 생태계, 71점)은 베이스로는 과잉 엔지니어링이라 탈락하지만, 두 아이디어는 흡수한다: (1) Priority 기반 충돌 모델(이미 Phase 0에 편입), (2) manifest를 '풀 DSL'이 아니라 'ExternalProcessRunner 위 선언적 인자 템플릿'으로 축소해 qpdf/gs 같은 단순 CLI를 코드 없이 추가하는 좁은 용도로만 채택. 복잡 로직(FFmpeg HW가속·AI)은 in-box 코드 원칙을 P1부터 못박아 manifest 갓오브젝트화를 방지한다(idx 1 본인의 하이브리드 경계 그대로).

    한 줄 요약: idx 2의 '기능이 견인하는 로드맵'에 idx 0의 '그래프가 받치는 코어'를 Phase 0에 심어, 사용자 체감 가치를 빠르게 내면서도 RouteAsync 지옥의 재발을 그래프로 원천 차단한다.

    +

    마스터플랜에 흡수한 최고의 아이디어

    +
    • [idx 0의 핵심] DocumentProvider.RouteAsync(92-205) 손그림 멀티홉을 '삭제'하고 엔진이 Dijkstra로 동일 경로를 계산하게 만드는 도그푸딩 — 이것을 회귀 테스트로 '그래프=손그림 동일 동작' 객관 증명. 테스트 0개인 현 상태에서 이 변환의 첫 안전망이 된다. 어떤 마스터플랜이 채택되든 이 검증 루프는 필수.
    • [idx 0의 핵심] 손실을 -log(보존율)+홉페널티 단일 가중치로 ConversionPair.LossClass(Lossless=0/Container=0.05/Recode=0.4/Rasterize=0.8) 필드에 SSOT화. 이것이 멀티홉 경로 선택과 UI '⚠손실' 배지의 단일 출처. idx 2의 '손실 변환 경고 배지'도 이 가중치를 그대로 소비.
    • [idx 0의 핵심] AI를 IAiProvider 특수 인터페이스가 아니라 '로컬 변환이 없는 신규 엣지(요약/번역/캡션)'로 그래프에 환원 — idx 2의 '후처리 부가가치 레이어'와 결합하면, AI는 그래프상 엣지이면서 동시에 키 없으면 자동 비활성 노드 + ✨AI 배지로 노출되는 이중 안전장치를 얻는다.
    • [idx 2의 핵심] AI 불변식: 키가 없어도 모든 기존 변환 100% 동작, AI는 절대 기본 경로를 점유하지 않고 ✨AI 배지 페어로만 opt-in, 키 부재 시 등록 순서·게이트로 조용히 비활성. '변환은 로컬에서 예측가능' 신뢰를 깨지 않는 설계 불변식.
    • [idx 2의 핵심] 라이선스 경계를 코드 리뷰 게이트로 강제: FFmpeg는 GPL 정적링크 금지·LGPL 분리호출만, Ghostscript/MuPDF는 AGPL이라 사용자 설치본 감지만, H2Orestart/Calibre는 GPL이라 외부 프로세스 분리. 모든 무거운 외부 도구 = '별도 프로세스 분리 호출 + 사용자 설치 감지 또는 LGPL 빌드 자동조달'. .NET 9 단일 EXE 상업 배포 오염 방지의 핵심.
    • [idx 2의 핵심] 단계 순서: PDF 압축 + HWP 출력 매트릭스 확장을 최우선 출시(이미 HwpxProvider의 LibreOffice+H2Orestart 배관 존재 → DOCX/HTML/TXT 출력만 추가하면 즉시 신규 가치). 초기 체감 가치를 그래프 리팩터링보다 먼저.
    • [idx 2의 핵심] 배치 병렬화: ConversionEngine 순차 for-loop(57-70)를 Parallel.ForEachAsync(동시성 제한 포함)로 교체. AI 네트워크 왕복·영상 트랜스코딩의 치명적 병목 해소. 더불어 ImageMagick ResourceLimits 전역 설정 + decompression bomb 방어로 미디어 공격면 차단.
    • [idx 1의 핵심] 레지스트리 충돌 모델 교체: _byPair.TryAdd(22)의 조용한 first-wins를 ProviderCapability.Priority + 다중 Provider 공존으로 교체 + 충돌 시 진단 경고. 같은 (input,output)에 빠른변환/고품질/AI 등 복수 전략 등록 가능. idx 0/2 모두 이 교체가 전제 조건.
    • [idx 1의 부분 채택] manifest는 '풀 생태계 비전'이 아니라 'ExternalProcessRunner 위의 선언적 인자 템플릿({input}/{output}/{outdir}/{format})'으로만 제한 채택 — qpdf/Ghostscript 같은 단순 CLI 압축 도구를 코드 없이 추가하는 용도. 단, 복잡 로직(FFmpeg HW가속 폴백/AI)은 in-box 코드 Provider 원칙을 P1부터 못박아 manifest 갓오브젝트화 방지.
    • [3안 공통] ExternalProcessRunner 단일 추상화로 LibreOffice 3중 복제(DocumentProvider:238-281 / DocxProvider:113-157 / HwpxProvider:107-151) 통합 — 타임아웃·stderr 수집·Kill 일원화. 이후 모든 외부 엣지(FFmpeg/Ghostscript/qpdf/Codex CLI)가 이 러너 하나 공유. 세 안이 만장일치로 지목한 가장 안전하고 즉시 실행가능한 첫 리팩터링.
    • [3안 공통] IConverterProvider 시그니처(9-15)를 ConvertRequest/ConvertContext로 일반화 + ConvertResult(10)에 비파일 산출물 필드(추출 텍스트·AI 응답·미디어 메타데이터) 추가. 단, 기존 8개 Provider는 어댑터로 감싸 점진 마이그레이션 + 회귀 테스트로 무중단 보장(idx 0의 마이그레이션 전략 채택).
    • [3안 공통] ConvertOptions 갓 오브젝트(14+ sub-record)를 그래프 옵션(AllowMultiHop/MaxHops/AvoidLossy) + 형식별 옵션 백(IReadOnlyDictionary 또는 Provider 선언형 스키마)으로 분해 → Video/Audio/Ai/PdfCompress를 sub-record 증식 없이 수용. DPAPI(ProtectedData) 기반 ISettingsStore 신설로 API 키·도구 경로 안전 저장.
    +
    + +
    12

    인터넷 리서치 (7개 토픽)

    +

    2026년 6월 기준 WebSearch로 조사한 라이브러리·방법론·라이선스. 클릭하면 출처까지 펼쳐진다.

    + +
    그래프 기반 멀티홉 변환 경로 탐색 아키텍처 (Everything2Everything 적용) +
    +

    핵심 발견

    • **Pandoc = 단일 AST 허브-앤-스포크**: 모든 포맷을 하나의 중립 AST(Pandoc AST)로 파싱(reader)하고 거기서 각 포맷으로 직렬화(writer)한다. M개 reader + N개 writer만 구현하면 M×N 변환을 자동 커버하고, 새 포맷은 reader/writer 1개 추가로 끝. 단 이 모델은 '한 도메인 안에서 의미가 보존되는 공통 표현'이 존재할 때(텍스트/마크업)만 성립한다. 이미지/오디오/문서를 하나의 AST로 묶는 건 불가능 — Everything2Everything처럼 도메인이 이질적이면 단일 허브가 아니라 '여러 허브를 가진 변환 그래프'가 정답이다.
    • **NCSA Polyglot / Conversion Software Registry(CSR)가 이 프로젝트의 정확한 청사진**: 노드=파일 포맷(확장자), 엣지=특정 소프트웨어를 통한 (입력→출력) 변환, 가중치=변환 시 '정보 보존량(information retained)'. 입력→출력 최단 경로를 탐색해 멀티홉 체인을 자동 생성한다. 손실 정량화는 별도 프레임워크 Versus(file-to-file 비교)로 측정해 엣지 가중치로 환산 → '정보 손실이 가장 적은 경로'를 고른다. 즉 노드=포맷 / 엣지=Provider / 가중치=손실 모델은 학계에서 이미 검증된 best practice다.
    • **가중치 모델링 best practice = 비용을 곱셈이 아니라 덧셈으로 만들기**: Dijkstra/A*는 경로비용이 엣지비용의 '합'일 때 동작한다. 손실은 본래 곱셈적(0.9 × 0.8...)이므로 `weight = -log(품질보존율)` 형태로 변환하면 합산 최단경로가 곧 '최대 품질보존 경로'가 된다. 여기에 lossy 엣지에 큰 페널티, lossless(컨테이너 재포장·무손실 코덱)에 0에 가까운 비용, 외부 도구 실행/렌더링 속도 비용을 가중합으로 섞는다. 홉 수 자체에도 작은 상수 페널티를 줘 '불필요하게 긴 체인'을 억제한다.
    • **손실 경로 회피 핵심 규칙들**: (1) 같은 lossy 인코딩을 두 번 거치지 않게 한다(JPEG→PNG→JPEG 같은 generation loss는 누적·비가역). (2) lossy→lossless 변환은 데이터를 복원하지 못하므로(이미 버려진 정보) 가중치에 반영. (3) 래스터화는 '단방향 손실 절벽' — 벡터/텍스트(PDF·SVG·DOCX)를 PNG로 한 번 떨구면 텍스트·벡터 정보가 영구 소실되므로, 래스터를 중간 허브로 쓰는 경로는 '꼭 필요할 때만(예: OCR, 썸네일)' 허용하고 가중치를 매우 높게 준다.
    • **중간 포맷(허브) 선택 전략**: 문서 도메인은 HTML/Markdown 또는 OOXML(DOCX)을 허브로(현재 DocumentProvider가 이미 HTML을 사실상 허브로 사용 중). 인쇄·레이아웃 보존이 중요하면 PDF가 허브(이미 PDFium 보유). 이미지 도메인은 무손실 중간 포맷(PNG/TIFF, 또는 ImageMagick 내부의 MIFF)을 허브로 써 generation loss를 막는다. 즉 '하나의 글로벌 허브'가 아니라 '도메인별 허브 + 도메인 간 경계는 의도적 손실 게이트(PDF, PNG)'로 설계하는 게 핵심.
    • **ImageMagick delegate = 이미 멀티홉 엔진**: decode/encode만 지정된 delegate를 자동으로 이어붙여 중간 포맷 체인을 만든다(예: BPG→PNG(중간)→내부표현→출력). %i(입력)/%o(출력)/%u(고유 임시파일) 토큰으로 중간 임시파일을 관리. 같은 변환에 delegate 여러 개면 선언 순서대로 시도하다 성공하는 것을 채택(우선순위=순서 + 가용성 fallback). Everything2Everything의 MagickProvider는 이 체인을 라이브러리 내부에서 이미 활용 중이므로, 이미지 노드 사이는 사실상 단일 '슈퍼노드'로 묶어도 된다.
    • **FFmpeg filtergraph = DAG 기반, format negotiation**: 노드=필터, pad=타입 있는 입출력 포트, 엣지=프레임 흐름. source/sink 개념, 사이클·다중 링크 허용. 핵심 시사점은 '인접 노드 간 포맷 협상(format negotiation)' — 변환 그래프에서도 각 Provider가 받을 수 있는/내보낼 수 있는 포맷 집합을 선언하고 엔진이 그 교집합으로 연결을 결정하는 구조가 견고하다.
    • **현재 코드 상태**: ProviderRegistry는 `(Input,Output)→Provider` 단일 홉 딕셔너리(`_byPair`)만 갖고, 멀티홉 경로 탐색이 전혀 없다. 멀티홉은 DocumentProvider.RouteAsync 안에 `md→html→docx`, `docx→html→md` 식으로 하드코딩되어 Provider 내부에 묻혀 있다. 이 하드코딩 분기들이 바로 '그래프로 끌어올려야 할' 멀티홉 로직이다.
    +

    권장 접근 (.NET 9 / 단일 EXE)

    단일 글로벌 AST 허브(Pandoc식)는 도메인이 이질적인 이 프로젝트에 부적합하다. 대신 **NCSA Polyglot 모델(노드=포맷, 엣지=Provider, 가중치=손실)을 ProviderRegistry 위에 얇은 그래프 레이어로 얹는 것**을 권장한다. + +**1) 그래프 빌드 (앱 시작 시 1회)**: 모든 Provider의 Capability.SupportedConversions를 순회해 방향 그래프를 만든다. 노드=정규화된 확장자, 엣지=해당 Provider+ConversionPair. 노드 수가 수십 개, 엣지 수가 수백 개 수준이므로 그래프는 매우 작다. + +**2) 경로 탐색**: Dijkstra(또는 A*) 1회로 충분. 비용은 엣지 가중합 = `α·(-log 품질보존율) + β·홉상수 + γ·실행비용`. lossy 게이트(래스터화, lossy 재인코딩)에 큰 가중치를 줘 손실 경로를 자연스럽게 회피한다. 직접 엣지(단일 홉)는 항상 비용이 낮아 기존 동작과 호환된다. + +**3) 라이브러리 선택**: 그래프가 작고 알고리즘이 표준적이므로 **외부 의존성 없이 자체 Dijkstra 약 80~120줄로 구현하는 것을 1순위로 권장**한다. 단일 포터블 EXE/MSIX 배포에 유리하고(트리밍·AOT 충돌 없음), MS-PL 같은 라이선스 검토도 불필요하다. 직접 구현이 부담되면 QuikGraph(MS-PL, net5~net10 호환)를 쓰되 2022년 이후 릴리스가 없는 점을 감안한다. + +**4) 손실 정량화(선택적 고도화)**: 초기에는 포맷 쌍별 정적 가중치 테이블(lossless=0.0, 컨테이너변환=0.05, lossy재인코딩=0.4, 래스터화=0.8 등)로 시작하고, 추후 Versus처럼 실제 결과물을 비교해 가중치를 보정하는 단계로 확장한다. 처음부터 동적 측정을 넣을 필요는 없다. + +**5) 안전장치**: 멀티홉은 '직접 엣지가 없을 때만' 발동하게 하고, 최대 홉 수(예: 3)와 '명시적으로 금지된 손실 전이' 블랙리스트를 둔다. 중간 산출물은 임시 폴더에 만들고 마지막에 정리(DocumentProvider의 workDir 패턴 그대로 재사용).

    +

    라이브러리 · 도구

    + + + + +
    이름용도라이선스성숙도
    자체 Dijkstra 구현 (직접 작성)ProviderRegistry 위에 포맷 그래프 + 가중치 최단경로 탐색을 직접 구현 (PriorityQueue<TElement,TPriority>는 .NET 9 BCL에 내장)N/A (프로젝트 코드)production (표준 알고리즘, 그래프 규모가 작아 검증 부담 낮음)
    QuikGraph방향 그래프 자료구조 + Dijkstra/A*/k-shortest path/BFS 등 알고리즘 제공MS-PL (Microsoft Public License, 상업적 사용 가능)active이나 정체 (최신 2.5.0이 2022-07 릴리스, 이후 신규 릴리스 없음 / 다운로드 1300만+)
    Kemsekov.GraphSharpDijkstra·그래프 컬러링·컴포넌트 등 알고리즘, QuikGraph 어댑터 제공확인 필요 (NuGet/리포 라이선스 확인 권장)active (3.1.x 최근 업데이트, QuikGraph보다 활발)
    Dijkstra.NET우선순위 큐 기반 Dijkstra(O(E log V)) 단일 목적 라이브러리MIT (리포 확인 권장)beta/소규모 (단순·경량, 업데이트 빈도 낮음)
    Pandoc (외부 CLI, 참조 아키텍처)문서 도메인 단일 AST 허브 변환 엔진. 라이브러리가 아니라 '허브-앤-스포크' 설계 참조 + 선택적 외부 도구GPL-2.0+ (CLI를 번들 없이 외부 호출하면 프로젝트 라이선스에 영향 없음)production (업계 표준, 활발)
    +

    통합 노트

    현재 ProviderRegistry는 `_byPair`(단일 홉)와 `_outputsByInput`만 갖고 있고, 멀티홉은 DocumentProvider.RouteAsync에 하드코딩돼 있다. 다음 단계로 그래프 레이어를 얇게 얹는 것을 권장한다. + +**1) ConversionGraph (신규, ProviderRegistry 내부 또는 옆에)**: 생성자에서 모든 Provider의 Capability.SupportedConversions를 순회해 `Dictionary<string, List<Edge>>`(노드=확장자, Edge={Provider, ConversionPair, Weight})로 인접 리스트를 만든다. ProviderRegistry 생성자 루프(현재 17~30행)에 그래프 빌드 한 단계만 추가하면 된다. + +**2) 가중치 부여**: ProviderCapability에 정적 손실 등급을 노출하는 게 깔끔하다. 예) `ConversionPair`에 선택적 `LossClass`(Lossless/Container/Recode/Rasterize) 필드를 추가하거나, Provider가 `double EstimateCost(ConversionPair)`를 구현(IConverterProvider 확장). 가중치는 `-log(보존율)+홉페널티` 합산. 기존 Provider는 기본값(직접 변환=저비용)으로 두면 무중단 마이그레이션 가능. + +**3) ConvertOptions 확장**: `bool AllowMultiHop`(기본 true), `int MaxHops`(기본 3), `bool AvoidLossy`(true면 래스터화·lossy 재인코딩 엣지를 큰 페널티/제외) 옵션을 추가. 현재 ConvertOptions 패턴(섹션별 옵션 객체)에 자연스럽게 들어간다. + +**4) ConversionEngine.ConvertOneAsync 수정**: 현재 91행 `_registry.TryGet`이 직접 매핑만 본다. 여기서 직접 엣지가 없으면(또는 더 저비용 멀티홉이 있으면) `ConversionGraph.FindBestPath(inExt, outExt, options)`로 경로를 구해, 경로의 각 홉을 순차 실행하는 `ExecuteChainAsync`로 위임한다. 중간 산출물은 DocumentProvider가 이미 쓰는 `workDir = Temp/e2e_..._{Guid}` 패턴을 공용 헬퍼로 올려 재사용하고, 각 홉은 기존 `provider.ConvertAsync`를 그대로 호출(인터페이스 변경 불필요). 진행률은 홉 수로 분할해 IProgress<double>에 매핑. + +**5) DocumentProvider 단순화(점진적)**: 그래프 레이어가 안정화되면 RouteAsync의 `md→html→docx` 같은 하드코딩 멀티홉 분기를 제거하고, DocumentProvider는 '단일 홉 원자 변환'(md→html, html→docx 등)만 선언하게 만든다. 그러면 md→docx는 엔진의 그래프 탐색이 자동으로 md→html→docx로 합성한다. 이게 Pandoc식 '작은 변환의 조합' 철학을 레지스트리 수준에서 실현하는 것. + +**6) UI/탐색 표시**: OutputsForInput가 지금은 직접 출력만 반환한다. 멀티홉을 켜면 도달 가능한 모든 출력(그래프 reachability)으로 확장할 수 있어, '이 파일로 만들 수 있는 모든 포맷' 목록이 훨씬 풍부해진다. 단, 손실 경로로만 도달하는 출력은 UI에서 경고 배지(예: '손실 변환')로 구분해 사용자에게 알리는 걸 권장(Versus 철학의 경량판).

    + +
    Codex non-interactive OAuth + API를 .NET 9/WPF 데스크톱 파일 변환기(Everything2Everything)에 통합 +
    +

    핵심 발견

    • [가장 중요] ChatGPT 구독 OAuth 토큰은 Codex 백엔드 전용이다. auth.json에 저장된 access/refresh 토큰은 codex CLI가 자체 백엔드를 호출할 때만 유효하며, 이 토큰을 꺼내 api.openai.com(공식 OpenAI API)에 직접 Bearer로 붙여도 동작하지 않는다. 따라서 '구독으로 API를 공짜로 쓰는' 경로는 오직 codex CLI 프로세스를 외부 실행하는 방법뿐이고, SDK를 직접 호출하려면 반드시 별도의 종량제 API 키(CODEX_API_KEY 또는 OPENAI_API_KEY)가 필요하다. 이 둘은 과금 모델이 완전히 분리된 별개 경로다.
    • Codex CLI 인증은 3가지: (1) 브라우저 ChatGPT OAuth(`codex login`) — 구독 사용, (2) 디바이스 코드 플로우(`codex login --device-auth`, 2026년 3월 추가, 헤드리스/원격용 베타) — URL+코드를 다른 기기에서 입력, (3) API 키(`CODEX_API_KEY`/`OPENAI_API_KEY` 환경변수) — CI/CD·프로그래매틱 권장. 토큰은 기본 `~/.codex/auth.json`(Windows는 `%USERPROFILE%\.codex\auth.json`)에 평문 JSON으로 저장되며, config의 `cli_auth_credentials_store`를 file/keyring/auto로 바꿀 수 있다.
    • 비대화형 실행은 `codex exec "<프롬프트>"`. 주요 플래그: `--json`(stdout이 JSONL 이벤트 스트림), `--output-schema <schema.json>`(응답을 JSON Schema로 강제 — 메타데이터/구조화 추출에 핵심), `-o/--output-last-message <path>`(최종 메시지를 파일로), `--model <name>`, `--cd <dir>`(작업 디렉터리), `--skip-git-repo-check`(git 저장소 아닌 폴더 허용 — 변환 앱에 필수), `--sandbox read-only|workspace-write|danger-full-access`, `--ephemeral`(세션 미저장). 프롬프트 안에 파일/이미지 경로를 직접 적으면 Codex가 읽어들인다.
    • 헤드리스 부트스트랩: 브라우저 있는 PC에서 `codex login` 후 생성된 auth.json을 헤드리스 머신으로 복사하거나, `printenv CODEX_ACCESS_TOKEN | codex login --with-access-token`로 토큰 주입 가능. CI에서는 auth.json을 매 실행 덮어쓰면 refresh된 토큰이 stale해지는 race가 있어 'if [ ! -f ] 가드'와 concurrency 직렬화가 권장된다 — 데스크톱 앱이 동시 변환 다건을 돌릴 때 동일 문제 발생 가능(아래 risks 참조).
    • 공식 OpenAI .NET SDK는 NuGet `OpenAI`(v2.10.0, 2026-04-04), MIT 라이선스, netstandard2.0 타깃이라 .NET 9에서 문제없이 동작. Chat Completions·비전(이미지 입력)·Structured Outputs(JSON Schema, gpt-4o 계열 이상) 모두 지원, 활발히 유지보수 중. Azure 전용 확장은 `Azure.AI.OpenAI`(공식 OpenAI 패키지 위에 얹힘). `OpenAI-DotNet`(8.x), `tryAGI.OpenAI`(4.x)는 커뮤니티 대안.
    • Anthropic Claude는 2026년부터 공식 .NET SDK가 NuGet `Anthropic` 패키지(v12.23.0, 2026-05-21, netstandard2.0)로 제공 — anthropics/anthropic-sdk-csharp. 이름이 비슷한 `Anthropic.SDK`(tghamm, 5.x)와 `tryAGI.Anthropic`은 비공식이다. 공식 패키지를 쓰는 것이 권장.
    • Microsoft.Extensions.AI(MEAI) 1.0이 2026-04-03 정식 출시(stable, MIT). `IChatClient` 단일 추상화로 OpenAI/Anthropic/Ollama/Bedrock/Gemini를 한 인터페이스로 다루며, 멀티모달(텍스트+이미지) 메시지를 지원. 프로바이더 교체가 한 줄 변경이라 이 프로젝트의 Provider/Registry 'OpenAI냐 Claude냐를 사용자가 선택' 요구에 정확히 들어맞는다.
    +

    권장 접근 (.NET 9 / 단일 EXE)

    단일 포터블 EXE / MSIX 배포라는 제약이 결정적이다. codex CLI는 별도 설치가 필요한 외부 Node 기반 바이너리이고, 포터블 EXE에 번들하기 어렵고(수백 MB), OAuth 토큰을 API로 재사용할 수 없으므로 '비용 절감' 명분도 사라진다. 따라서 기본 경로는 SDK 직접 호출로 가는 것이 맞다. + +권장 아키텍처(2층): +1) 추상화 층 — Microsoft.Extensions.AI의 `IChatClient`를 내부 LLM 게이트웨이로 채택. OpenAI는 공식 `OpenAI`(MIT) + MEAI OpenAI 커넥터, Claude는 공식 `Anthropic` 패키지(MEAI Anthropic 커넥터)로 연결. 사용자는 설정에서 'OpenAI / Claude / (옵션)Codex CLI'를 고르고 API 키만 입력하면 된다. 키는 Windows DPAPI(ProtectedData)로 암호화해 로컬 저장 — 포터블 EXE에서도 사용자별 암호화 가능. + +2) 선택적 Codex CLI 백엔드 — ChatGPT Pro/Plus 구독을 이미 보유한 파워유저를 위해, codex가 PATH에 감지될 때만 활성화되는 보조 백엔드로 둔다. `CheckAvailabilityAsync`에서 `codex --version` 프로브 → 실패 시 RequiresExternal로 다운로드 안내. 실행은 `codex exec --skip-git-repo-check --json --output-schema schema.json -o out.json --cd <tempdir> "<프롬프트 + 파일경로>"` 형태로 Process 호출, JSONL 마지막 메시지 파싱. + +즉 '기본은 API 키 + 공식 SDK(MEAI 추상화), Codex CLI는 구독자용 opt-in 보조 경로'의 하이브리드가 이 프로젝트에 최적이다. LLM 자체는 변환의 '핵심 엔진'이 아니라 '후처리/부가가치 단계'(요약·번역·포맷 정규화·OCR 교정·이미지 캡션·메타데이터)로 배치해, 키가 없어도 기존 변환은 100% 동작하고 LLM 기능만 비활성(ComingSoon/RequiresExternal 스타일)되게 한다.

    +

    라이브러리 · 도구

    + + + + + +
    이름용도라이선스성숙도
    OpenAI (공식 .NET SDK)OpenAI API 직접 호출 — Chat Completions, 비전(이미지 입력), Structured Outputs(JSON Schema). 문서 요약/번역/메타데이터 생성/이미지 캡션의 기본 엔진MITproduction (v2.10.0, 2026-04-04, 활발히 유지보수, netstandard2.0이라 .NET 9 호환)
    Anthropic (공식 Claude .NET SDK)Claude API 직접 호출 — OpenAI 대안. 긴 문서 요약/번역에 강점, 비전 지원MIT (anthropics/anthropic-sdk-csharp)production (v12.23.0, 2026-05-21, 공식). 주의: 비공식 'Anthropic.SDK'(tghamm)·'tryAGI.Anthropic'과 혼동 금지
    Microsoft.Extensions.AI / .AbstractionsIChatClient 단일 추상화로 OpenAI·Claude·Ollama를 동일 인터페이스로 — Provider/Registry의 LLM 백엔드 선택 계층MITproduction (1.0 정식, 2026-04-03, 멀티모달 지원). 본 프로젝트 추상화와 가장 정합
    OpenAI Codex CLI외부 프로세스(codex exec)로 ChatGPT 구독 OAuth를 재사용해 LLM 호출 — 구독 보유자 opt-in 보조 경로Apache-2.0 (openai/codex 리포)active (2026년 활발, GPT-5.5 에이전틱). 단 별도 설치 필요·OAuth 토큰 API 재사용 불가·포터블 번들 부적합
    Azure.AI.OpenAIAzure OpenAI Service를 쓸 경우의 확장(공식 OpenAI 패키지 위에 얹힘). 일반 OpenAI만 쓸 거면 불필요MITproduction (v2.1.0). 본 프로젝트엔 선택적
    tryAGI.OpenAI / OpenAI-DotNetOpenAI 비공식 커뮤니티 SDK 대안MITactive (각각 v4.2.0 / v8.8.x). 공식 OpenAI 패키지가 있으므로 우선순위 낮음
    +

    통합 노트

    현 추상화(IConverterProvider / ProviderCapability / ProviderRegistry / ConvertOptions)에 자연스럽게 끼워넣는 방법: + +1) 신규 `LlmProvider : IConverterProvider` 추가 (OcrProvider와 동일한 패턴). SupportedConversions를 LLM 후처리 매트릭스로 정의: + - 요약: .pdf/.docx/.txt/.md → .txt/.md (summary) + - 번역: .txt/.docx/.md → .txt/.docx (대상 언어는 옵션) + - 포맷 정규화: .txt → .md, .csv → .md(표) + - OCR 교정: OcrProvider 출력(.txt)을 받아 LLM이 오탈자/줄바꿈 정리 (파이프라인 2단계) + - 이미지 캡션/대체텍스트: .png/.jpg → .txt (비전) + - 메타데이터 생성: 임의 입력 → .json (제목/태그/요약, Structured Outputs) + OcrProvider가 PdfProvider를 생성자 주입으로 재사용하듯, LlmProvider도 텍스트 추출이 필요하면 DocumentProvider/PdfProvider/OcrProvider를 주입받아 '추출→LLM' 2단계로 구성한다. + +2) `ConvertOptions`에 `LlmOptions Llm { get; set; } = new();` 추가. 필드 예: Backend(\"openai\"|\"anthropic\"|\"codex-cli\"|\"auto\"), Model, ApiKey(또는 키 저장소 참조), Task(Summarize/Translate/Normalize/Caption/Metadata), TargetLanguage, MaxTokens, Temperature. 기존 OcrOptions와 동일한 스타일이라 직렬화·UI 바인딩 일관성 유지. + +3) `CheckAvailabilityAsync`가 게이트 역할: + - SDK 경로: API 키(설정 또는 OPENAI_API_KEY/ANTHROPIC_API_KEY 환경변수) 존재 확인 → 없으면 ProviderAvailability.NotReady(\"API 키 미설정\", MissingDependencies). ExternalDependency로 키 발급 URL 안내. + - Codex 경로: `codex --version` 프로세스 프로브 + auth.json 존재 확인 → 없으면 ProviderStatus.RequiresExternal로 다운로드/로그인 안내. + 기존 OcrProvider가 OCR 언어팩 유무로 NotReady를 반환하는 패턴을 그대로 따른다. + +4) `ConvertAsync` 구현: IProgress<double> / CancellationToken 시그니처 그대로 유지. SDK 경로는 IChatClient.GetResponseAsync(스트리밍 시 진행률 갱신), Codex 경로는 Process.Start로 `codex exec --json --output-schema ... -o out.json` 실행 후 표준출력 JSONL 파싱. 출력 파일은 기존 OutputPathHelper.ResolveOutputPath + OnCollision 규칙을 재사용. 실패 시 ConvertResult.Fail, 건너뛰기 ConvertResult.Skip로 통일. + +5) ProviderRegistry는 수정 불필요 — 생성자에서 providers 목록에 LlmProvider 인스턴스만 추가하면 _byPair 매트릭스에 자동 편입된다. 단 LLM은 비결정적·유료·네트워크 의존이므로, 동일 (input,output) 페어를 로컬 변환 Provider가 이미 점유한 경우 TryAdd가 먼저 등록된 쪽을 유지하는 현 동작 덕분에 'LLM은 로컬 변환이 없는 신규 페어(요약/번역 등)에만 노출'되도록 등록 순서를 조정하면 된다. UI에서는 LLM 출력 페어에 '✨ AI' 배지를 붙여 종량 과금/네트워크 사용을 사용자에게 명시할 것. + +6) 키 보안: 포터블 EXE에서도 System.Security.Cryptography.ProtectedData(DPAPI, CurrentUser)로 키를 암호화해 %APPDATA% 또는 앱 폴더에 저장. 환경변수 OPENAI_API_KEY/ANTHROPIC_API_KEY도 fallback으로 읽어 CI/파워유저 친화.

    + +
    FFmpeg .NET 통합 (영상/오디오/코덱 변환) — Everything2Everything 적용 리서치 +
    +

    핵심 발견

    • 라이브러리 선택은 명확하다: FFMpegCore가 정답이다. v5.4.0 (2025-10-27 릴리스, 누적 600만 다운로드, 일 3K, MIT 라이선스, .NET Standard 2.0+ → .NET 9 호환). 라이브러리 자체가 MIT이므로 상업적 사용에 제약이 전혀 없다.
    • Xabe.FFmpeg는 라이브러리 코드 자체가 CC BY-NC-SA 3.0 (비상업) 라이선스다. 상업적 사용은 별도 유료 상업 라이선스 구매가 필요하다. 인용: 'You may use Software under Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0) license for non commercial projects.' → 상업 사용 선호 방침상 탈락.
    • 핵심 라이선스 함정은 라이브러리가 아니라 FFmpeg 바이너리 자체다. FFmpeg는 기본 LGPL 2.1+이지만 --enable-gpl(libx264/libx265 H.264/H.265 인코더 포함) 또는 --enable-nonfree(libfdk-aac) 빌드는 GPL/비배포 라이선스로 바뀐다. 폐쇄소스 상업 EXE에 가장 흔한 gyan.dev 빌드(GPLv3)나 BtbN gpl 빌드를 번들하면 안 된다.
    • 결정적 발견: 폐쇄소스 상업 배포에는 BtbN의 'lgpl-shared' 빌드를 써야 한다. LGPL 준수 조건은 (1) --enable-gpl/--enable-nonfree 없이 빌드, (2) 동적 링크(여기선 별도 ffmpeg.exe 프로세스 호출이 가장 안전한 동적 분리), (3) FFmpeg 소스 코드 제공 의무(또는 다운로드 링크), (4) 다운로드 페이지/앱 내 FFmpeg 사용 고지(attribution)다. 이미 LibreOffice를 외부 도구로 호출하는 본 프로젝트 구조와 정확히 일치한다.
    • NVENC(h264_nvenc/hevc_nvenc)는 LGPL 빌드에서 --enable-nonfree 없이 사용 가능하다. NVIDIA의 FFmpeg/NVENC 인터페이스 작성자(Philip Lachsinger)가 직접 'turning it on did not stop your ffmpeg build from being lgpl compliant (it does not require the non-free flag)'라고 확인. 헤더는 MIT, 런타임은 GPU 드라이버의 system library exception 적용. → LGPL-shared 빌드 + NVENC HW 가속이 상업 배포에 합법적으로 양립한다.
    • 코덱 라이선스 매트릭스(상업 배포 관점): H.264/H.265 인코딩은 libx264/libx265=GPL(폐쇄소스 불가) 대신 하드웨어 인코더(h264_nvenc/hevc_nvenc, h264_qsv/hevc_qsv, h264_amf)를 쓰면 LGPL 유지. AV1=libaom/libsvtav1(BSD-like, royalty-free), VP9=libvpx(BSD-like), Opus=libopus(BSD), FLAC(Xiph BSD), AAC=FFmpeg 네이티브 aac 인코더(LGPL, libfdk-aac는 nonfree라 회피). 즉 LGPL-shared 빌드만으로 AV1/VP9/Opus/FLAC/AAC/MP3는 모두 커버되고, H.264/H.265는 HW 인코더로 우회.
    • FFMpegCore는 본 프로젝트의 IConverterProvider 패턴에 매끄럽게 들어간다. 진행률은 NotifyOnProgress(Action<double> onPercentageProgress, TimeSpan totalTimeSpan) → IProgress<double>로 직결, 취소는 CancellableThrough(CancellationToken token, int timeout=0) → 기존 ct 직결, HW 가속은 WithHardwareAcceleration(HardwareAccelerationDevice) (enum: Auto/D3D11VA/DXVA2/QSV/CUVID/CUDA/VDPAU/VAAPI/LibMFX)로 지원.
    • 바이너리 크기 현실: FFmpeg essentials 정적 빌드 ffmpeg.exe는 압축 32MB / 압축해제 ~103MB. 단일 포터블 EXE에 내장하면 100MB가 더해진다. .NET single-file publish는 ffmpeg.exe를 임베드해 자가추출(IncludeNativeLibrariesForSelfExtract)할 수 있으나 시작 시 temp 추출 비용이 크다. 더 나은 전략은 런타임 다운로드(FFMpegDownloader.DownloadFFMpegSuite())다.
    +

    권장 접근 (.NET 9 / 단일 EXE)

    FFMpegCore 5.4.0(MIT) + FFprobe를 채택하고, FFmpeg 바이너리는 EXE에 번들하지 말고 'RequiresExternal + 최초 사용 시 LGPL-shared 빌드 자동 다운로드' 방식을 권장한다. 근거: (1) 라이선스 — BtbN lgpl-shared 빌드(GPL/nonfree 없음)는 폐쇄소스 상업 EXE에 합법적이며, ffmpeg.exe를 별도 프로세스로 호출(=동적 분리)하면 본 앱 바이너리는 FFmpeg와 분리되어 LGPL 전염 없음. gyan.dev나 BtbN gpl 빌드(GPLv3, libx264/x265 포함)는 절대 번들 금지. (2) 배포 크기 — FFmpeg ~100MB를 단일 EXE에 박으면 다운로드/시작이 무거워지므로, 이미 LibreOffice를 외부 의존성으로 두는 본 프로젝트 철학대로 FFmpeg도 외부 도구로 취급. 구체적으로 FfmpegProvider를 ProviderStatus.RequiresExternal로 등록하고, CheckAvailabilityAsync에서 (a) 시스템 PATH의 ffmpeg.exe, (b) 앱 데이터 폴더(%LOCALAPPDATA%\\Everything2Everything\\ffmpeg)에 받아둔 바이너리, (c) FFMpegCore의 FFMpegDownloader로 최초 1회 자동 다운로드 순으로 탐지/조달. GlobalFFOptions.Configure(new FFOptions{ BinaryFolder = <앱데이터 ffmpeg 경로> })로 경로를 고정. (3) 코덱 전략 — H.264/H.265는 HW 인코더(h264_nvenc/qsv/amf, 미지원 시 mpeg4/우회) 우선, AV1/VP9/Opus/FLAC/AAC(네이티브)/MP3는 LGPL 빌드로 직접 처리. (4) 진행률은 FFprobe로 duration을 먼저 구해 NotifyOnProgress(Action<double>, TimeSpan)을 IProgress<double>에 연결, 취소는 CancellableThrough(CancellationToken)에 기존 ct 연결. MSIX 배포 시에는 자동 다운로드가 샌드박스/네트워크 정책에 걸릴 수 있으니, MSIX 변형에서는 lgpl-shared DLL/EXE를 앱 패키지에 동봉(여전히 LGPL 준수: 동적 호출 + 소스 제공 링크 + 고지)하는 분기 권장.

    +

    라이브러리 · 도구

    + + + + +
    이름용도라이선스성숙도
    FFMpegCoreFFmpeg/FFprobe CLI를 감싸는 .NET fluent wrapper. 트랜스코딩, 압축, 포맷 변환, 미디어 분석, 진행률/취소/HW가속 지원. 영상·오디오 Provider의 핵심 엔진.MIT (라이브러리 코드 — 상업 사용 자유. FFmpeg 바이너리 라이선스는 별개)production — v5.4.0(2025-10-27), 누적 600만 다운로드/일 3K, 활발한 유지보수. NuGet rosenbjergsoftworks
    Xabe.FFmpegFFmpeg .NET wrapper (대안 후보). 비슷한 트랜스코딩/변환 API.CC BY-NC-SA 3.0 (비상업 전용). 상업 사용은 유료 상업 라이선스 필수active — 유지되나 라이선스 모델이 상업 프로젝트에 부적합
    직접 Process 호출 (System.Diagnostics.Process로 ffmpeg.exe 실행)의존성 0, ffmpeg CLI를 직접 ProcessStartInfo로 실행하고 stderr를 파싱해 진행률 추출.N/A (본인 코드, ffmpeg 바이너리만 라이선스 대상)production — 가장 단순/투명하지만 인자 빌드·진행률 파싱(time=/duration 정규식)·에러 처리를 직접 구현해야 함
    FFmpeg 바이너리 (BtbN lgpl-shared 빌드)실제 트랜스코딩을 수행하는 네이티브 엔진. GPL/nonfree 미포함 LGPL 빌드.LGPL 2.1+ (--enable-gpl, --enable-nonfree 없음). 폐쇄소스 상업 배포 가능 — 단 동적 분리 호출 + 소스 제공 + 고지 필요production — BtbN/FFmpeg-Builds, 7.1.x 정기 릴리스(2025). winget: BtbN.FFmpeg.LGPL.Shared.7.1
    FFmpeg 바이너리 (gyan.dev essentials/full)가장 널리 쓰이는 Windows 정적 빌드. libx264/x265 H.264/H.265 SW 인코딩 포함.GPLv3 (essentials/full 모두 --enable-gpl). 폐쇄소스 상업 EXE에 번들 금지production — 사실상 표준, 정기 갱신
    +

    통합 노트

    새 FfmpegProvider : IConverterProvider를 src/Everything2Everything.Core/Converters/에 추가하고 ProviderRegistry에 등록한다(기존 7개 Provider와 동일). 구체 설계:\n\n1) Capability: Status=ProviderStatus.RequiresExternal, ExternalDependencies에 new ExternalDependency(Name:\"FFmpeg\", Description:\"영상/오디오 트랜스코딩 엔진(LGPL 빌드)\", DownloadUrl:\"https://github.com/BtbN/FFmpeg-Builds/releases\"). SupportedConversions는 ProviderCapability.PairsFromMatrix로 영상(mp4/mkv/webm/mov/avi/gif)·오디오(mp3/aac/m4a/opus/ogg/flac/wav) 입출력 N×M 구성. 단 코덱 호환 안 되는 쌍(예: → flac은 오디오 전용)은 매트릭스 후 필터링하거나 PairsFromMatrix 대신 명시적 ConversionPair 리스트로 정밀 제어 권장.\n\n2) 바이너리 탐지: ExternalToolDetector에 TryFindFfmpeg(out string ffmpegPath) 추가 — (a) %LOCALAPPDATA%\\Everything2Everything\\ffmpeg\\ffmpeg.exe, (b) 시스템 PATH(where ffmpeg), 순으로 탐지. CheckAvailabilityAsync에서 못 찾으면 NotReady 반환하되, 선택적으로 FFMpegDownloader.DownloadFFMpegSuite(new FFOptions{ BinaryFolder=<앱데이터경로> })로 자동 조달 후 GlobalFFOptions.Configure로 경로 고정. (FFMpegDownloader 기본 소스는 ffbinaries=gyan GPL 빌드일 수 있으니, 상업 배포에선 BtbN lgpl-shared zip을 직접 받는 커스텀 다운로더를 쓰거나 동봉 권장 — 라이선스 검증 필수.)\n\n3) ConvertAsync 본문(FFMpegCore 사용):\n var media = await FFProbe.AnalyseAsync(sourcePath, cancellationToken: ct); // duration 확보\n await FFMpegArguments\n .FromFileInput(sourcePath)\n .OutputToFile(outputPath, overwrite:true, opt => opt\n .WithHardwareAcceleration(HardwareAccelerationDevice.Auto) // NVENC/QSV 자동\n .WithVideoCodec(\"h264_nvenc\") // 또는 av1/libaom, vp9, opus 등 outExt별 분기\n .WithAudioCodec(\"aac\"))\n .NotifyOnProgress(p => progress?.Report(p/100.0), media.Duration) // IProgress<double> 직결\n .CancellableThrough(ct) // 기존 ct 직결\n .ProcessAsynchronously();\n 진행률 NotifyOnProgress(Action<double>, TimeSpan)의 0~100을 /100.0해 기존 IProgress<double> 계약(0~1)에 맞춘다. 예외는 기존 Provider처럼 try/catch로 ConvertResult.Fail, OperationCanceledException은 rethrow.\n\n4) HW 가속 폴백: CheckAvailabilityAsync 또는 첫 변환 시 ffmpeg -encoders로 h264_nvenc/h264_qsv/h264_amf 가용성을 탐지해 ConvertOptions에 저장하고, 없으면 SW 인코더로 폴백하되 H.264/H.265 SW는 GPL이라 LGPL 빌드엔 없음 → 폴백을 AV1/VP9(libaom/libvpx, LGPL) 또는 mpeg4로 잡거나 사용자에게 HW 미지원 안내. ConvertOptions에 코덱/품질(CRF) 옵션 필드 추가 고려.\n\n5) 라이선스 고지: 본 프로젝트 어딘가(About/설정)에 'This software uses libraries from the FFmpeg project under the LGPLv2.1' 문구 + FFmpeg 소스 다운로드 링크 추가(LGPL 의무). ExternalDependency.DownloadUrl을 통해 UI에서 안내 가능.\n\n6) csproj: Everything2Everything.Core.csproj ItemGroup에 <PackageReference Include=\"FFMpegCore\" Version=\"5.4.0\" /> 추가. 자동 다운로더를 쓸 경우 FFMpegCore에 내장된 FFMpegDownloader 사용(별도 패키지 불필요). 단일 EXE에 번들 시 ffmpeg.exe를 None/Content로 추가하고 single-file에서 SelfExtract 메타데이터로 제외/비압축 처리(시작 성능).

    + +
    PDF 압축 및 PDF↔문서 양방향 변환 (Everything2Everything .NET 9 / WPF 통합 관점) +
    +

    핵심 발견

    • 압축 엔진 라이선스가 핵심 갈림길이다. Ghostscript(-dPDFSETTINGS /screen·/ebook·/printer·/prepress)는 압축 품질이 가장 우수하지만 AGPL v3 듀얼 라이선스다. 포터블 EXE에 gs 바이너리를 동봉/배포하면 AGPL 전염 의무(소스 공개)가 발생하므로, 상업적 사용을 원하면 Artifex 상업 라이선스 구매가 필요하다. MuPDF/mutool clean도 동일하게 AGPL이라 같은 문제가 있다.
    • qpdf(Apache 2.0, 최신 12.4.0 / 2026-04, 매우 활발)가 라이선스 안전성 측면에서 1순위 무료 옵션이다. object stream 압축(--object-streams=generate), 스트림 재압축, linearize(웹 최적화), 암호화/복호화/비밀번호 변경, 페이지 분할·병합을 모두 CLI로 제공한다. 단, qpdf는 이미지 다운샘플링(리샘플링)은 하지 않는다 — 구조 최적화/무손실 위주라 압축률이 Ghostscript보다 낮다.
    • 최대 압축률(이미지 다운샘플링)은 라이선스 안전한 무료 도구만으로는 약하다. qpdf로 구조 최적화 + 프로젝트가 이미 보유한 ImageMagick/PDFium으로 이미지 페이지를 재인코딩하는 하이브리드가 AGPL을 피하면서 실용적 압축을 내는 최선의 무료 경로다.
    • PDF→DOCX 레이아웃 보존은 순수 .NET 라이브러리로는 사실상 불가능하다. PdfPig(Apache 2.0, v0.1.14 / 2026-03, 활발)는 텍스트·글자 위치(page.Letters)·단어(GetWords)·이미지 추출까지 가능하지만 DOCX 재구성(레이아웃 엔진)은 설계 범위 밖이다. Docnet.Core(MIT, PDFium 래퍼)도 렌더/텍스트 추출 전용이다.
    • PDF→DOCX/편집가능 변환의 현실적 최선은 이미 통합된 LibreOffice headless(soffice --convert-to docx)다. 무료(MPL/LGPL)이고 단락·표를 어느 정도 복원하지만, 스캔/복잡 레이아웃 PDF는 충실도가 낮다. 고충실도 상업 변환이 필요하면 Nutrient(PSPDFKit)/IronPDF 등 유료 SDK가 있으나 라이선스 비용이 든다.
    • PDF→텍스트/HTML은 순수 .NET로 충분하다. PdfPig(텍스트, 무료)와 PDFium(Docnet) 또는 mutool로 텍스트/구조 추출이 가능하다. PDF→HTML 레이아웃 보존은 LibreOffice 또는 pdf2htmlEX(외부)이 현실적이다.
    • PDF/A 변환은 Ghostscript(-dPDFA, AGPL) 또는 LibreOffice PDF export(SelectPdfVersion=1 → PDF/A-1)로 가능하며, 검증은 veraPDF(반사실적 표준 검증기, 무료)로 한다. 라이선스 안전을 원하면 LibreOffice 경로 + veraPDF 검증 조합이 적합하다.
    • 암호화/복호화/병합/분할은 qpdf(Apache 2.0) 단독으로 전부 커버 가능하며 가장 가벼운 단일 실행 파일이다. 순수 .NET 대안으로 PDFsharp 6.2.x(MIT, AES-128/256, 병합·분할·암호화 지원, .NET 9 호환)가 외부 의존성 없이 In-Process로 동작해 포터블 EXE에 가장 잘 맞는다.
    +

    권장 접근 (.NET 9 / 단일 EXE)

    이 프로젝트(단일 포터블 EXE / 상업적 사용 가능 라이선스 우선)에는 'AGPL 회피 + 가능한 한 in-process .NET' 원칙을 권장한다. + +1) PDF 압축: 1차로 PDFsharp(MIT) 또는 qpdf(Apache 2.0)로 구조 최적화(object stream 압축, linearize, 중복 객체 제거)를 in-process/경량 CLI로 수행한다. 이미지 다운샘플링이 필요한 '강한 압축' 모드는 이미 보유한 PDFium(Docnet/PDFtoImage)로 페이지를 렌더 후 ImageMagick로 JPEG/품질 조절 재인코딩하여 새 PDF를 만드는 하이브리드를 별도 옵션으로 제공한다(텍스트 선택성은 잃지만 라이선스 안전). Ghostscript는 '고급 압축' 옵션으로만 노출하되, 번들하지 말고 사용자가 별도 설치한 gs를 ExternalDependency로 감지해 쓰는 방식(LibreOffice/H2Orestart와 동일 패턴)으로 AGPL 배포 의무를 회피한다. + +2) PDF→DOCX/HTML(역변환): 기존 DocumentProvider의 LibreOffice 경로를 그대로 확장해 입력 매트릭스에 .pdf를 추가한다(soffice --convert-to docx/html/txt). 순수 .NET PdfPig는 'PDF→txt' 같은 빠른 무외부 경로와 텍스트 추출 폴백으로 사용한다. + +3) PDF/A·암호화·병합·분할: PDFsharp(MIT, in-process)를 기본 엔진으로, qpdf(Apache 2.0)를 무거운 작업(linearize/복잡 암호)의 외부 폴백으로 둔다. PDF/A 검증은 선택적 veraPDF 연동. + +전체적으로 '무료 기본(PDFsharp/qpdf/PDFium/LibreOffice) + 선택적 고급(Ghostscript/유료 SDK, 사용자 설치 감지)' 2계층 전략이 라이선스·포터블성·품질의 균형점이다.

    +

    라이브러리 · 도구

    + + + + + + + + + +
    이름용도라이선스성숙도
    qpdf (CLI + libqpdf)PDF 구조 최적화/object stream 압축, linearize(웹 최적화), 암호화·복호화·비밀번호 변경, 페이지 분할·병합. 이미지 다운샘플링은 안 함(무손실/구조 위주).Apache License 2.0 (v7+ 재라이선스)production / 매우 활발 (v12.4.0, 2026-04; v12.3.2, 2026-01). Windows MSVC 32/64bit 빌드 제공
    Ghostscript (gswin64c) + Ghostscript.NET 래퍼-dPDFSETTINGS(/screen·/ebook·/printer·/prepress) 프리셋 + -dDownsampleColorImages/-dColorImageResolution 등 이미지 다운샘플링으로 최고 압축률. -dPDFA로 PDF/A 변환.AGPL v3 (또는 Artifex 상업 라이선스). Ghostscript.NET 래퍼는 MIT지만 네이티브 gs는 AGPLproduction / 활발 (10.x). Ghostscript.NET 래퍼 NuGet은 1.3.3로 다소 정체
    MuPDF / mutool (clean)mutool clean으로 폰트/이미지 스트림 압축·garbage collect. 렌더링/멀티포맷(EPUB/XPS) 강점.AGPL v3 (또는 Artifex 상업 라이선스)production / 활발 (1.27.x)
    PdfPig (UglyToad.PdfPig)순수 .NET PDF 읽기/텍스트·글자 위치(Letters)·단어(GetWords/NearestNeighbour)·이미지(GetImages) 추출, 기본 PDF 생성·병합. PDF→txt/구조 분석에 적합.Apache License 2.0production / 활발 (v0.1.14, 2026-03; PDFBox 포팅). netstandard2.0
    Docnet.CorePDFium(Apache 2.0) .NET Standard 래퍼. 페이지 렌더(비트맵), 텍스트/메타데이터 추출.MIT (네이티브 PDFium은 Apache 2.0)active / 안정 (유지보수 보통)
    PDFsharp 6.x순수 .NET PDF 생성·수정·병합·분할, AES-128/256 암호화·복호화, PDF/A·PDF/UA 일부 지원.MIT (상업적 자유, 저작권 고지 유지 시)production / 활발 (6.2.4, .NET 9/10 호환)
    DocumentFormat.OpenXml + OpenXmlPowerTools(Clippit)DOCX in-process 생성/편집. PdfPig 추출 텍스트로 간단한 DOCX 조립 시 사용.MIT (둘 다)production / 활발 (OpenXml 3.5.1; PowerTools 4.5.x / Clippit 2026 유지)
    LibreOffice (soffice headless)PDF→DOCX/HTML/TXT 역변환, PDF/A export, 문서 상호변환. 이미 통합됨.MPL 2.0 / LGPL 3.0 (상업 사용 가능)production / 매우 활발
    veraPDFPDF/A·PDF/UA 적합성 검증(생성 아님). 변환 후 검증 단계.이중(GPLv3+ / MPLv2+), 둘 다 무료 사용 가능production / 활발 (PDF Association·OPF 유지, ISO 참조 검증기)
    IronPDF / Nutrient(PSPDFKit) .NET SDK고충실도 PDF→Word/Excel/PPT 변환, HTML↔PDF, 압축 등 올인원 상업 SDK.상업(유료, 무료 아님)production / 활발
    +

    통합 노트

    기존 추상화에 자연스럽게 끼워넣을 수 있다. 핵심은 IConverterProvider / ProviderCapability / ProviderRegistry(N×M 매트릭스)와 ExternalDependency 감지 패턴이 이미 LibreOffice·H2Orestart에서 검증되어 있다는 점이다. + +1) PDF 역변환(DOCX/HTML/TXT): 가장 저비용 통합. DocumentProvider.cs의 Inputs 배열에 \".pdf\"를 추가하고 RouteAsync에 PDF 분기를 넣으면 된다. .pdf→{docx,html,txt}는 그대로 SofficeConvertAsync 재사용(soffice는 PDF 입력을 Draw로 열어 변환). .pdf→txt 무외부 폴백은 PdfPig로 page.Text를 모아 쓰는 별도 경로를 추가하면 LibreOffice 없이도 동작. .pdf→md는 기존 패턴대로 pdf→html(soffice)→ReverseMarkdown 체인으로 처리. RoadmapNote/ExternalDependencies는 기존 LibreOffice 의존성 항목 그대로 재사용. + +2) PDF 압축/유틸리티(압축·암호화·병합·분할·PDF/A): 같은 \".pdf\"→\".pdf\" 변환은 현 ProviderRegistry가 (input==output)을 ConversionEngine 단계에서 걸러낼 가능성이 높으므로, '동일 확장자 변환'을 옵션 파라미터로 구분하는 새 PdfToolProvider(예: Id \"pdf-tools\")를 신설하는 편이 깔끔하다. ConvertOptions에 PdfCompress(레벨: Light=PDFsharp/qpdf 구조 최적화, Strong=PDFium 렌더+ImageMagick 재인코딩, Max=Ghostscript /screen) 같은 옵션 그룹을 추가한다(기존 Jpeg/Webp/Avif/Tiff/PdfRender 옵션 그룹과 동일한 record 스타일). 압축 강도에 따라 내부적으로 (a) PDFsharp in-process, (b) PDFium+ImageMagick 하이브리드(PdfProvider의 렌더 로직과 ApplyEncoding 재사용 가능), (c) gs CLI 폴백으로 디스패치. + +3) 라이선스/배포 경계: PDFsharp·PdfPig·qpdf는 번들 가능(MIT/Apache). Ghostscript·MuPDF·veraPDF·LibreOffice는 절대 EXE에 정적 링크/동봉하지 말고, CheckAvailabilityAsync에서 ExternalToolDetector로 사용자 설치본을 감지해 ProviderAvailability.NotReady(미설치 시) 또는 Ready로 분기한다(현 DocumentProvider.CheckAvailabilityAsync와 동일 패턴). 이렇게 하면 고급 압축/PDF-A는 '사용자가 직접 설치한 도구로만' 동작하여 AGPL 배포 전염을 구조적으로 회피한다. ExternalDependency.DownloadUrl에 gs/veraPDF 다운로드 링크를 넣어 안내. + +4) qpdf 통합 방식: SofficeConvertAsync와 동일한 ProcessStartInfo/ArgumentList 패턴으로 QpdfRunner 헬퍼를 하나 만들어 압축(--object-streams=generate --compress-streams=y --recompress-flate), 암호화(--encrypt), 복호화(--decrypt), 분할(--split-pages), 병합(--pages)을 공통 호출. libqpdf P/Invoke는 복잡도가 높아 CLI 호출이 통합 비용 대비 합리적.

    + +
    HWP/HWPX ↔ DOCX/PDF/HTML 양방향 변환 — Everything2Everything (.NET 9 / WPF) 통합 리서치 +
    +

    핵심 발견

    • **정방향(HWP/HWPX → PDF/이미지)은 이미 동작 중이며 최선의 경로다.** 프로젝트는 HwpxProvider.cs에서 LibreOffice headless(`soffice --convert-to pdf`) + H2Orestart 확장으로 PDF 변환 후 PdfProvider로 위임한다. H2Orestart는 2026년에도 활발히 유지보수 중(v0.7.12, 2026-05-10 릴리스). 이 구조는 옳다.
    • **H2Orestart는 GPLv3 + Java 의존이다.** 라이선스가 GPLv3(LGPL 아님)이므로 EXE에 정적/동적 번들하면 전염성(copyleft) 문제가 생긴다. 단, 현재처럼 별도 .oxt 확장으로 사용자가 LibreOffice에 설치 → 별도 프로세스(soffice.exe)를 외부 호출하는 방식이면 E2E 본체 코드와 라이선스가 분리되어 상업 배포에 안전하다. 추가로 H2Orestart는 내부적으로 JRE/JDK가 필요하다(LibreOffice의 Java 통합 활성화 필수). LibreOffice 빌드에 따라 별도 JRE 설치/설정이 필요할 수 있어 '단일 포터블 EXE' 자족성에 마이너스 요인.
    • **H2Orestart는 import(읽기) 전용이다 — 저장은 ODT로만 가능, HWP/HWPX 쓰기 불가.** 따라서 LibreOffice 경로로는 역방향(DOCX→HWP) 출력이 불가능하다. LibreOffice에 HWP export 필터가 없다.
    • **역방향 HWP/HWPX '쓰기'는 hwplib(HWP) / hwpxlib(HWPX)만이 현실적 오픈소스 해법이다.** 둘 다 neolord0 작성, Apache-2.0(상업 친화적), 2026년까지 활발(hwplib v1.1.5+ 2026-02-04, hwpxlib v1.0.8 2025-11-14). hwplib은 BlankFileMaker/HWPWriter로 빈 HWP 생성·텍스트·표·이미지 삽입까지 가능. **그러나 둘 다 Java 라이브러리이며 '포맷 변환' 기능이 없다** — DOCX를 파싱해서 hwplib 객체 모델로 매핑하는 변환 로직을 직접 구현해야 한다. 고품질 레이아웃 보존 역변환은 사실상 새 변환 엔진을 작성하는 수준이라 비용이 매우 크다.
    • **pyhwp/hwp5(hwp5html)는 AGPLv3 + 반쯤 정체 상태다.** 마지막 안정 릴리스 0.1b15(2020-05-30). HWP5→HTML/ODT/txt 추출은 되지만 HWPX 미지원이고, AGPLv3는 SaaS·배포에 전염성이 강해 상업 배포기에 부적합. 이미 LibreOffice 경로가 있으므로 채택 이점 없음.
    • **hwp.js는 사실상 abandoned다(Apache-2.0, v0.0.3 2020-10, 2020년 유지보수 중단 공지).** 브라우저 HWP 뷰어/파서로 WebView2와 결합 가능성은 있으나, 미완성·구버전이라 신뢰성 낮음. 동일 저자(hahnlee)의 Rust 계열(hwp-rs)과 신생 Rust 생태계(openhwp, HwpForge, hwpers, unhwp)가 더 활발하지만 .NET 직접 연동은 FFI 작업이 추가로 필요.
    • **한컴 공식 경로(HwpAutomation COM / 한글 SDK / Docs Converter)는 품질이 가장 높지만 상업 라이선스 유료다.** HwpCtrl ActiveX/COM은 개인·비상업은 무료이나 '판매되는 솔루션'에 쓰려면 한컴 승인 + 별도 라이선스 필요(contact_sdk@hancom.com). 한글 SDK는 한글 프로그램 설치 없이 HWP/HWPX↔HTML/PDF/ODF 변환 및 약 1,000개 기능 제공, .NET에서 호출 가능하나 유료. 역방향(→HWP)을 네이티브 품질로 보장하는 유일한 길이지만 비용·배포(런타임 동봉) 제약이 큼.
    • **LibreOffice headless 한글 변환 안정성/폰트 이슈가 존재한다.** Old Hangul(옛한글)·제주어 음절, Noto Sans 일부 글리프가 PDF에서 깨지는 보고가 있고, 시스템에 한글 폰트(맑은 고딕/함초롬바탕 등)가 없으면 폰트 치환으로 레이아웃이 틀어진다. 레거시 HWP는 `--infilter="Hwp2002_File"` 지정이 도움이 됨. 복잡한 표·다단·머리말은 레이아웃 손실 가능.
    +

    권장 접근 (.NET 9 / 단일 EXE)

    단일 포터블 EXE / 상업 라이선스 선호라는 제약을 고려하면 **계층적 전략**이 최적이다. + +1) 정방향(HWP·HWPX → PDF/PNG/JPG 등): **현재 HwpxProvider의 LibreOffice + H2Orestart 경로를 유지·강화**한다. 이미 구현되어 있고 H2Orestart가 2026년에도 활발하다. 강화 포인트: (a) `--infilter=\"Hwp2002_File\"`를 .hwp에 한해 추가 지정해 import 필터 명시, (b) 한글 폰트(함초롬·맑은 고딕) 번들 또는 폰트 누락 감지 경고, (c) JRE 미설치 시 명확한 안내. H2Orestart는 GPLv3이지만 '사용자가 LibreOffice에 설치한 확장을 외부 프로세스로 호출'하는 분리 모델이라 E2E 본체 라이선스에 전염되지 않는다. + +2) 정방향 HWPX → DOCX/HTML/TXT '편집 가능 포맷' 출력: PDF 외에 DOCX/ODT/HTML 출력 수요가 있으면, **LibreOffice의 `--convert-to docx`/`html`/`txt`** 를 동일 파이프라인에서 노출하면 된다(soffice가 ODT 경유로 DOCX/HTML export 지원). 이게 hwp5html/pyhwp(AGPL) 채택보다 라이선스·유지보수 면에서 우수하다. HwpxProvider의 HwpOutputs 배열에 .docx/.html/.txt/.odt를 추가하고 변환 분기만 PDF 대신 해당 포맷으로 바꾸면 즉시 매트릭스가 확장된다. + +3) 역방향(DOCX/PDF/HTML → HWP·HWPX): **현실적으로 고품질 무료 OSS 경로는 없다.** 단기적으로는 'Coming Soon' 또는 미지원으로 두고, 정말 필요하면 두 가지 옵션 — (a) **저품질 수용형**: HTML/DOCX 텍스트·표를 추출해 hwpxlib(Apache-2.0)로 최소 구조의 HWPX를 생성(레이아웃 보존 낮음, Java 브리지 필요), (b) **고품질 유료형**: 한컴 한글 SDK/Docs Converter 상업 라이선스로 별도 Provider 구성. 무료 단일 EXE 원칙을 우선한다면 역방향은 '로드맵'으로 남기고, HWPX 출력만 hwpxlib 기반 베스트에포트로 제공하는 것을 권한다. + +요약: 정방향은 LibreOffice 단일 엔진으로 PDF·이미지·DOCX·HTML까지 모두 커버(추가 의존성 0). 역방향은 무료로는 베스트에포트 HWPX만, 네이티브 품질은 유료 한컴 SDK로 분리.

    +

    라이브러리 · 도구

    + + + + + + +
    이름용도라이선스성숙도
    H2Orestart (ebandal)LibreOffice 확장. HWP/HWPX를 LibreOffice에서 import(읽기) → ODT/PDF/DOCX/HTML로 headless 변환. 정방향 변환의 핵심 엔진.GPLv3active (v0.7.12, 2026-05-10)
    hwplib (neolord0 / kr.dogfoot)HWP 5.0 바이너리 읽기 AND 쓰기(BlankFileMaker로 새 파일 생성, HWPWriter로 저장). 역방향(→HWP) 출력의 유일한 무료 경로.Apache-2.0active (v1.1.5+, 2026-02-04)
    hwpxlib (neolord0)HWPX(OWPML, ZIP+XML) 읽기/쓰기. HWPX 직접 생성·수정. 역방향 HWPX 출력 후보.Apache-2.0active (v1.0.8, 2025-11-14)
    pyhwp / hwp5 (hwp5html, mete0r)HWP5 파서. HWP5 → HTML/ODT/txt 추출.AGPLv3+semi-abandoned (마지막 안정 0.1b15, 2020-05-30)
    hwp.js (hahnlee)웹 기술 기반 HWP 뷰어/파서(브라우저 렌더링).Apache-2.0abandoned (v0.0.3, 2020-10, 2020년 유지보수 중단 공지)
    Rust 생태계 (hwp-rs, openhwp, HwpForge, hwpers, unhwp)HWP/HWPX 파싱·렌더·Markdown 추출. openhwp는 읽기/쓰기 지향.대부분 MIT/Apache-2.0 (크레이트별 상이, 확인 필요)active/beta (2025-2026 신생, 성숙도 편차 큼)
    한컴 한글 SDK / HwpAutomation(COM) / Docs Converter한컴 네이티브 변환. HWP/HWPX ↔ HTML/PDF/ODF/DOCX, 약 1,000개 한글 기능. 역방향(→HWP) 네이티브 품질 보장.상업(유료, 별도 라이선스). ActiveX/COM은 개인·비상업만 무료production (한컴 공식)
    +

    통합 노트

    **Provider/Registry 추상화에 끼우는 구체안 (D:\\workspace\\Everything2Everthing\\src\\Everything2Everything.Core)** + +현재 HwpxProvider.cs는 IConverterProvider를 구현하고, Capability.SupportedConversions = PairsFromMatrix(HwpInputs, HwpOutputs)로 (입력×출력) 쌍을 선언, ProviderRegistry가 (Input,Output)→Provider 딕셔너리로 라우팅한다. DocxProvider가 동일 패턴(LibreOffice/Word로 PDF→PdfProvider 위임)이라 이를 그대로 따른다. + +1) **정방향 출력 포맷 확장 (가장 비용 낮고 효과 큼)**: HwpxProvider의 `HwpOutputs` 배열에 `.docx`, `.html`, `.odt`, `.txt`를 추가한다. ConvertAsync의 분기에서 outExt가 이미지/PDF가 아니면 ConvertWithLibreOfficeAsync의 `--convert-to pdf`를 해당 필터(`docx:\"MS Word 2007 XML\"`, `html:HTML (StarWriter)`, `txt:Text` 등)로 파라미터화한다. 현재 메서드는 pdf 하드코딩이므로 target 포맷·확장자를 인자로 받도록 일반화하면 된다. .hwp 입력 시 `--infilter=\"Hwp2002_File\"`를 ArgumentList에 조건부 추가하면 import 안정성이 오른다. 이렇게 하면 HWP/HWPX → DOCX/HTML/TXT/ODT/PDF/이미지 매트릭스가 한 Provider·한 외부 의존(LibreOffice+H2Orestart)으로 완성된다. + +2) **availability 메시지 보강**: CheckAvailabilityAsync에 JRE 미설치 시 안내를 추가(H2Orestart는 LibreOffice의 Java 통합이 꺼져 있으면 동작 안 함). ExternalToolDetector에 한글 폰트(함초롬/맑은 고딕) 존재 여부 체크를 추가해 폰트 누락 시 ProviderAvailability에 경고 Reason을 실어주면 레이아웃 깨짐 사고를 예방. + +3) **역방향 Provider 신설(선택)**: 별도 `HwpReverseProvider`(또는 HwpxProvider에 입력 .docx/.html/.pdf → 출력 .hwpx 쌍 추가)를 만들되, 무료 경로는 hwpxlib 기반 베스트에포트로 한정. .NET↔Java 브리지가 필요하므로 (a) IKVM.NET으로 hwpxlib JAR을 .NET 어셈블리화, 또는 (b) 번들한 JRE로 hwpxlib 래퍼 JAR을 자식 프로세스 실행(현재 LibreOffice를 외부 프로세스로 부르는 패턴과 동일해 일관적). 단일 EXE 원칙상 (b)가 기존 외부-프로세스 모델과 잘 맞는다. Capability.Status는 ComingSoon 또는 RequiresExternal로 두고, RoadmapNote에 '레이아웃 보존 제한적, 고품질은 한컴 SDK 필요' 명시. + +4) **유료 고품질 역변환은 별도 옵셔널 Provider**: 한컴 SDK가 설치·라이선스된 환경에서만 활성화되는 `HancomSdkProvider`를 COM(dynamic)로 구현(DocxProvider의 ConvertWithWordCom이 Type.GetTypeFromProgID로 Word COM을 dynamic 호출하는 패턴과 동일). CheckAvailabilityAsync에서 ProgID/SDK DLL 존재로 가용성 판단, 없으면 NotReady로 빠지므로 무료 배포본에는 영향 없음. + +라이선스 격리 원칙: GPLv3(H2Orestart)·Java 라이브러리(hwplib/hwpxlib)·한컴 SDK 모두 '외부 프로세스 또는 사용자 설치 확장'으로 분리 호출하여 E2E 본체(상업 배포)와 라이선스 경계를 유지한다. 본체에 GPL/AGPL 코드를 링크하지 않는다.

    + +
    .NET 9 확장 가능 변환기 플러그인 아키텍처 — Everything2Everything용 best practice 리서치 +
    +

    핵심 발견

    • 기존 추상화는 이미 잘 설계된 정적 매트릭스다. IConverterProvider(Capability + CheckAvailabilityAsync + ConvertAsync), ProviderRegistry((input,output) 쌍 사전), ProviderCapability(ConversionPair 리스트)로 구성. 단, 현재 ProviderRegistry는 생성자에서 IEnumerable<IConverterProvider>를 받아 컴파일 타임에 고정된다 — 동적 등록/언로딩 진입점이 없다. 확장성의 첫 단계는 '런타임에 Provider를 추가하는 RegisterDynamic / Rebuild' 메서드 도입이다.
    • Microsoft 공식 .NET 플러그인 튜토리얼(2026-02 갱신)의 핵심 패턴: (1) 공유 계약 어셈블리(PluginBase)를 별도 프로젝트로 분리, (2) 플러그인 프로젝트는 계약을 <Private>false</Private> + <ExcludeAssets>runtime</ExcludeAssets>로 참조해 계약 DLL 중복 로드를 방지(이게 빠지면 같은 인터페이스가 서로 다른 타입으로 인식되어 캐스팅 실패), (3) 플러그인 csproj에 <EnableDynamicLoading>true</EnableDynamicLoading>를 넣어 의존성을 출력으로 복사, (4) 플러그인당 별도 AssemblyLoadContext(ALC) + AssemblyDependencyResolver로 의존성 충돌 격리. Load 오버라이드에서 계약 어셈블리는 null 반환해 default ALC로 fall back시켜야 타입 동일성이 유지된다.
    • 보안/신뢰 경계에 대한 Microsoft의 명시적 경고: '신뢰할 수 없는 코드는 신뢰된 .NET 프로세스에 안전하게 로드할 수 없다. 보안/안정성 경계가 필요하면 OS 또는 가상화 플랫폼이 제공하는 기술을 사용하라.' 즉 in-process ALC는 '버전 격리/핫리로드'용이지 '샌드박스'가 아니다. 진짜 격리(서드파티 untrusted 플러그인, 네이티브 크래시 차단)는 out-of-process 호스트(별도 .exe + IPC) 또는 Windows AppContainer/Job Object로만 달성된다.
    • 언로딩(hot-reload)은 협조적(cooperative)이며 footgun이 많다. collectible ALC를 써도 (a) 정적 캐시(직렬화기/DI 컨테이너가 플러그인 타입 캐시), (b) 해지 안 한 이벤트 핸들러, (c) 살아있는 Timer/Task/Thread, (d) 플러그인 타입이 host-scope 인프라로 누출되면 절대 언로드되지 않는다. 검증은 WeakReference<ALC> + 반복 GC로만 가능. 네이티브 라이브러리(ImageMagick/PDFium/LibreOffice)는 ALC 경계를 무시하므로 in-process 언로딩 대상에서 제외해야 한다.
    • MEF2(System.Composition)는 죽지 않았고 활발히 유지보수 중이다 — 최신 10.0.7(2025), .NET 9/10과 호환(.NET Core 2.0/Standard 2.0 타겟). 다만 이 프로젝트에는 권장하지 않는다: MEF은 런타임 리플렉션 기반 attribute 스캔이라 (a) 단일 포터블 EXE/AOT 친화성이 낮고, (b) 7개뿐인 1급 Provider에는 과도하다. 동적 서드파티 플러그인을 정말 열 때만 가치가 있다.
    • Source generator 방식이 이 프로젝트에 가장 적합한 '자동 등록' 수단이다. AutoRegisterInject(v1.4.1, MIT, netstandard2.0, .NET 9 호환)가 [RegisterScoped]/[RegisterSingleton] 등 attribute로 DI 등록 코드를 컴파일 타임에 생성 — 리플렉션 0, AOT 친화. 1급(in-box) Provider들은 source generator로 자동 등록하고, 동적 외부 플러그인만 ALC로 로드하는 하이브리드가 이상적.
    • 외부 도구 어댑터 패턴은 이미 코드에 존재한다(DocumentProvider.SofficeConvertAsync가 LibreOffice를 ProcessStartInfo로 감쌈). 이를 일반화한 'ExternalToolProvider' 베이스 클래스 + JSON manifest로 FFmpeg/Ghostscript/Pandoc을 균일 인터페이스로 흡수할 수 있다. CliWrap(v3.10.1, MIT, 2026-03 갱신)이 ProcessStartInfo 보일러플레이트(스트림 리다이렉트/취소/exit code/진행률)를 대체할 fluent 래퍼로 강력 추천.
    • 라이선스 함정 2가지가 단일 포터블 EXE 배포에 치명적이다. (1) Ghostscript: AGPL/상업 듀얼 라이선스 — 닫힌 소스 포터블 EXE에 번들하려면 Artifex 상업 라이선스 필요. PDF 처리는 이미 가진 PDFium으로 대체하는 게 안전. (2) FFmpeg: libx264/libx265 등 인기 코덱은 GPL이라 번들 시 앱 전체가 GPL 전염. 반드시 --enable-gpl 없이 빌드한 LGPL 빌드를 '동적 링크'(별도 exe 호출/별도 DLL)로 사용하고 소스 오퍼/저작권 고지를 포함해야 함. Pandoc도 GPL이라 같은 '동적 = 별도 프로세스 호출' 원칙 적용. LibreOffice(MPL-2.0)와 CliWrap/AutoRegisterInject(MIT)는 번들 안전.
    +

    권장 접근 (.NET 9 / 단일 EXE)

    단일 포터블 EXE/WPF/Windows 11 제약에서는 '3계층 하이브리드'가 최적이다. (계층 1 — In-box Provider, 지금처럼) 7개 1급 Provider는 컴파일 타임에 고정. 단 등록 보일러플레이트를 줄이려면 AutoRegisterInject source generator를 도입해 [RegisterE2EProvider] attribute만 붙이면 자동 등록되게 한다. 리플렉션 없고 AOT/트리밍 친화라 단일 EXE에 이상적. (계층 2 — 외부 도구 어댑터 Provider) 이미 있는 SofficeConvertAsync 패턴을 'ExternalToolProvider' 추상 베이스로 일반화한다. 각 외부 도구(LibreOffice/FFmpeg/Ghostscript/Pandoc)는 코드가 아니라 JSON manifest(tool id, 실행 파일 탐지 경로, 입력/출력 확장자 매트릭스, argument 템플릿, 성공 판정 규칙)로 선언하고, 런타임에 manifest를 읽어 ProviderRegistry에 합성한다. 프로세스 실행은 CliWrap으로 통일(취소/진행률/exit code 처리 일원화). 이렇게 하면 새 도구 추가가 '코드 빌드 없이 manifest + 탐지기 추가'로 끝난다. 외부 프로세스 호출 자체가 천연 격리 경계라서 도구가 크래시해도 앱은 살아있다(가장 가성비 좋은 안정성/보안 경계). (계층 3 — 진짜 동적 플러그인, 필요할 때만) 서드파티가 .NET DLL Provider를 끼우는 시나리오가 생기면 그때 collectible ALC + AssemblyDependencyResolver를 도입한다. 단 이건 untrusted 격리가 아님을 명심하고, 신뢰할 수 없는 플러그인은 out-of-process 워커(별도 exe)로 돌린다. ImageMagick/PDFium/WebView2 같은 네이티브 의존 Provider는 절대 언로드 대상으로 만들지 않는다(네이티브가 ALC 경계를 무시). 결론: 지금 당장 필요한 건 계층 1의 source generator 자동 등록과 계층 2의 manifest 기반 외부 도구 어댑터다. ALC 동적 로딩은 '서드파티 플러그인 마켓'을 실제로 열 때까지 미루는 게 복잡도 대비 합리적이다.

    +

    라이브러리 · 도구

    + + + + + + + + +
    이름용도라이선스성숙도
    System.Runtime.Loader.AssemblyLoadContext + AssemblyDependencyResolver (BCL 내장)플러그인 DLL을 격리된 컨텍스트에 동적 로드/언로드, 의존성 충돌 해결. .NET 공식 플러그인 메커니즘MIT (.NET 런타임)production (BCL 내장, .NET Core 3.0~.NET 9/10 안정)
    AutoRegisterInjectattribute 기반으로 IConverterProvider 구현체를 컴파일 타임에 DI 자동 등록 (리플렉션/스캔 제거)MITactive (v1.4.1, netstandard2.0, .NET 9 호환)
    System.Composition (MEF2)attribute 기반 런타임 플러그인 발견/합성 (Export/Import)MITactive (최신 10.0.7, 2025, .NET 9 호환)
    CliWrap외부 CLI 도구(LibreOffice/FFmpeg/Ghostscript/Pandoc)를 fluent하게 실행 — 인자/스트림/취소/진행률/exit code 일원화MITproduction (v3.10.1, 2026-03 갱신, 활발)
    LibreOffice (soffice)문서(DOCX/HTML/TXT/HWP) 변환 외부 엔진 (이미 사용 중)MPL-2.0production
    FFmpeg오디오/비디오 변환 어댑터 Provider 후보LGPL-2.1+ (코어) / 일부 코덱 GPL / nonfreeproduction
    GhostscriptPDF/PostScript 변환 어댑터 후보AGPL-3.0 / 상업 듀얼 (Artifex)production
    Pandoc마크다운/문서 포맷 광범위 변환 어댑터 후보GPL-2.0+production
    System.Reflection.Metadata어셈블리를 실제 로드하지 않고 PE/메타데이터만 읽어 플러그인 manifest/attribute를 스캔 (reflection-free discovery)MIT (.NET 런타임)production
    +

    통합 노트

    현재 추상화에 끼워넣는 구체 단계: (1) ProviderRegistry를 '닫힌 생성자'에서 '증분 등록 가능' 구조로 확장. 현재 생성자가 한 번에 _byPair 사전을 빌드하므로, 동일 인덱싱 로직을 private void Index(IConverterProvider) 로 빼고 public void Register(IConverterProvider)/RegisterRange/Rebuild를 추가한다. 이걸로 source generator 등록 + manifest 기반 어댑터 등록 둘 다 같은 진입점을 쓴다. (2) 계층1 자동등록: IConverterProvider 구현체(HeicProvider/PdfProvider 등)에 [RegisterE2EProvider] 같은 마커를 붙이고 AutoRegisterInject(또는 소형 자작 generator)로 'IEnumerable<IConverterProvider> GetBuiltInProviders()'를 컴파일 타임 생성. App 시작 시 registry.RegisterRange(GetBuiltInProviders()). (3) 계층2 어댑터: 새 추상 클래스 ExternalToolProvider : IConverterProvider 를 만든다. 이 클래스가 (a) ProviderCapability를 manifest의 입력×출력 매트릭스에서 ProviderCapability.PairsFromMatrix로 생성(기존 헬퍼 재사용), (b) CheckAvailabilityAsync는 manifest의 toolDetect 규칙(현 ExternalToolDetector 패턴 일반화)으로 실행 파일 탐지, (c) ConvertAsync는 manifest의 argument 템플릿({input}/{output}/{outdir}/{format} 토큰 치환)을 CliWrap으로 실행하고 결과 파일 존재로 성공 판정. 기존 ExternalDependency 레코드를 manifest의 dependency 섹션과 그대로 매핑. (4) Manifest 로더: tools/*.manifest.json 을 읽어 ExternalToolProvider 인스턴스들을 만들고 registry.RegisterRange. manifest 스키마는 기존 ProviderCapability/ConversionPair/ExternalDependency 모양을 그대로 직렬화한 형태로 잡으면 추상화 변경 최소. (5) 충돌 우선순위: _byPair.TryAdd는 first-wins라 in-box Provider가 동일 쌍을 가지면 manifest 어댑터보다 먼저 등록해 우선권을 준다(현재 동작 유지). 향후 우선순위 필드가 필요하면 ProviderCapability에 Priority(int) 추가 후 Index에서 비교. (6) 안정성: 외부 도구는 CliWrap의 ExecuteAsync에 CancellationToken과 타임아웃을 걸고, 크래시해도 ConvertResult.Fail로 흡수(이미 DocumentProvider가 try/catch로 처리하는 패턴 유지). 진짜 in-process .NET 플러그인(계층3)을 열 때만 collectible ALC를 도입하되, IConverterProvider 계약을 담은 별도 Everything2Everything.Abstractions 어셈블리를 만들고 플러그인은 그것을 <Private>false</Private>로 참조하게 해 타입 동일성을 보장한다.

    + +
    범용 문서/아카이브/폰트/CAD 변환 커버리지 확장 + 변환기 UX 패턴 (Everything2Everything, .NET 9/WPF/Windows 11) +
    +

    핵심 발견

    • Pandoc은 사실상의 'universal document 허브'다. pandoc 3.x 기준 43개 입력 / 57개 출력 포맷(마크다운 계열·HTML·LaTeX·DOCX·EPUB·RST·MediaWiki·Org·Textile·JATS 등)을 지원하며, 단일 도구로 N×M 마크업/문서 매트릭스를 한 번에 커버한다. .NET 통합은 SimonCropp의 PandocNet(MIT, v4.0.0 / 2026-04, CliWrap 기반 강타입 래퍼)이 가장 성숙하다. 단 pandoc.exe를 번들하지 않고 PATH 또는 명시 경로로 외부 설치를 요구한다 — 프로젝트가 LibreOffice를 외부 의존성으로 다루는 패턴과 정확히 동일.
    • 프로젝트는 이미 LibreOffice로 DOCX↔HTML↔TXT를 처리하므로, Pandoc은 LibreOffice가 약한 '마크업/경량 텍스트 포맷'(rst, org, latex, mediawiki, asciidoc, textile, ipynb, epub→md 등)을 채우는 보완재로 배치하는 것이 최적이다. 둘은 경쟁이 아니라 라우팅 분담(LibreOffice=오피스 바이너리, Pandoc=마크업/학술)이다.
    • CloudConvert가 '극한' 변환기의 레퍼런스 매트릭스: 212포맷 / 13카테고리(문서23·이미지42·비디오28·오디오21·스프레드시트8·슬라이드11·전자책22·아카이브39·벡터10·CAD3·폰트5·데이터·해시). Everything2Everything의 현재 커버리지(이미지·PDF·문서5종·HEIC·OCR)와 비교하면 아카이브·폰트·전자책·데이터·벡터·오디오/비디오가 미개척 영역.
    • '극한' UX를 만드는 4대 공통 패턴: (1) HandBrake/XnConvert식 배치 큐(queue) — 수백 파일을 한 번에 넣고 순차/병렬 처리 + per-row 진행률(프로젝트는 이미 큐 inline progress bar 보유), (2) 액션 체이닝(XnConvert: resize→watermark→convert를 한 파이프라인으로), (3) 워치/핫 폴더 자동화(폴더에 떨어뜨리면 자동 변환), (4) 명확한 드롭존 + 클릭 업로드 병행 + hover 시 시각 피드백.
    • 라이선스 함정 2건 확인: (a) SixLabors.Fonts는 3.0.0부터 'Split License'로 빌드 타임 라이선스 검증을 강제 — 연매출 1M USD 이상 + 클로즈드소스면 상업 라이선스 구매 필수. 폰트 변환에는 라이선스 비용 없는 LayoutFarm/Typography(MIT 계열) 또는 Aspose.Font(상용) 검토 권장. (b) Xabe.FFmpeg는 CC BY-NC-SA(비상업 전용)라 상업 배포 불가 — 오디오/비디오는 반드시 FFMpegCore(MIT) + FFmpeg 바이너리(LGPL 빌드)로 가야 한다.
    • CAD는 순수 .NET 솔루션이 아직 미성숙: ACadSharp(MIT, v3.6.x)는 DXF/DWG 읽기/쓰기가 가능하나 공식적으로 alpha(일부 엔티티 미구현). 실무 DWG↔DXF 변환은 ODA File Converter(무료, 비오픈소스, 재배포 제약)나 Aspose.CAD(상용)에 의존. LibreDWG는 GPLv3+라 클로즈드소스 EXE에 부적합. CAD는 우선순위 후순위 권장.
    • 아카이브는 SharpCompress(MS-PL/유사 permissive, v0.4x, 2026년에도 활발히 유지보수, 5일 전 커밋)가 단연 최적 — 순수 C#, 무의존, zip/tar/gzip/bzip2/lzip/zstd/7z 쓰기 + RAR/arj/arc 읽기, non-seekable 스트림 + async 지원. 단일 EXE에 그대로 포함 가능.
    +

    권장 접근 (.NET 9 / 단일 EXE)

    단계적 카테고리 확장을 권장한다. 핵심 원칙은 '프로젝트가 이미 검증한 외부-CLI 호출 패턴(DocumentProvider→soffice --headless --convert-to)을 그대로 복제'하는 것과 '단일 포터블 EXE에 부담을 주지 않도록 순수 관리 코드 라이브러리를 1순위로 채택'하는 것이다.\n\n[1순위 — 순수 .NET, EXE에 바로 포함, 라이선스 깨끗]\n- 아카이브: SharpCompress 추가 → ArchiveProvider 신설. zip/7z/tar/gz/bz2 ↔ (압축/해제). 무의존 순수 C#라 포터블 EXE에 이상적.\n- 데이터: Parquet.Net(MIT, v6.0.3, 순수관리, .NET8/10) + ClosedXML(MIT) + CsvHelper로 csv↔json↔xlsx↔parquet DataProvider. 전부 순수 관리 코드.\n- 벡터: 이미 SkiaSharp 생태계에 가까우므로 Svg.Skia(MIT, v5.0.0)로 svg→png/jpg/webp/pdf. EPS는 ImageMagick+Ghostscript 델리게이트 활용(이미 ImageMagick 보유).\n\n[2순위 — 외부 CLI 의존, LibreOffice와 동일한 ExternalToolDetector 패턴]\n- 마크업/학술 문서: PandocNet(MIT) + pandoc.exe 외부 의존 → PandocProvider. md/rst/org/latex/mediawiki/asciidoc/textile/ipynb/epub 등을 매트릭스로 노출.\n- 전자책: Calibre의 ebook-convert.exe(GPLv3, 별도 프로세스 호출이므로 GPL 전파 없음) → EbookProvider. epub↔mobi↔azw3↔pdf↔docx.\n- 오디오/비디오: FFMpegCore(MIT) + FFmpeg LGPL 바이너리 → MediaProvider. mp4/mkv/mp3/wav/flac/webm 등. (Xabe.FFmpeg는 비상업 라이선스이므로 배제)\n\n[3순위 — 보류/조건부]\n- 폰트: 라이선스 비용 없는 LayoutFarm/Typography로 ttf↔woff/woff2 읽기, woff2 쓰기는 Brotli 압축 필요. SixLabors.Fonts 3.x는 빌드타임 라이선스 강제로 회피. 수요 확인 후 진행.\n- CAD: ACadSharp가 alpha라 신중. ODA File Converter는 재배포 제약. 수요 검증 전까지 보류.\n\nUX는 워치폴더(FileSystemWatcher + 500ms 디바운스 + 파일 잠금 재시도) + CLI 자동화(이미 CliRouter 존재)를 더해 '배치/자동화 3종 세트(드래그앤드롭·핫폴더·CLI)'를 완성하는 것을 권장.

    +

    라이브러리 · 도구

    + + + + + + + + + +
    이름용도라이선스성숙도
    PandocNet (SimonCropp)Pandoc CLI 강타입 .NET 래퍼 — md/rst/org/latex/mediawiki/asciidoc/ipynb/epub 등 40+ 마크업·문서 상호변환의 허브MITactive (v4.0.0, 2026-04, CliWrap 기반). pandoc.exe 번들 안 함 — 외부 설치 필요
    SharpCompress아카이브 압축/해제 — zip/7z/tar/gzip/bzip2/lzip/zstd 쓰기 + rar/arj/arc 읽기MS-PL 계열 permissive (상업 사용 가능)production (v0.48.x, 2026년 5일 전 커밋, 활발). 순수 C#, 무의존, async 지원
    Parquet.Net (aloneguid)Apache Parquet 읽기/쓰기 (데이터 카테고리)MITproduction (v6.0.3, 2026-05, 27M 다운로드). 순수 관리 코드, 무의존, .NET8/10
    ClosedXML + CsvHelper + ExcelDataReaderxlsx/csv 읽기·쓰기, json 변환 (데이터 카테고리)MIT (ClosedXML, CsvHelper) / MS-PL (ExcelDataReader)production, 모두 순수 관리 코드
    Svg.Skia (wieslawsoltes)SVG → PNG/JPG/WebP/PDF/XPS 래스터화 (벡터 카테고리)MITactive (v5.0.0, 2026-05). SkiaSharp 백엔드 의존(네이티브 자산 필요)
    Calibre ebook-convert (CLI)전자책 변환 — epub↔mobi↔azw3↔pdf↔docx, --output-profile kindle 등GPLv3 (별도 프로세스 호출이므로 앱에 GPL 전파 안 됨)production (Calibre 9.x, 문서 2026-05 갱신). 외부 설치 필요
    FFMpegCore (rosenbjerg)오디오/비디오 변환 — mp4/mkv/webm/mp3/wav/flac 등 (FFmpeg/FFProbe 래퍼)MIT (래퍼) + FFmpeg는 LGPL 빌드 사용production, fluent 인자 빌더, sync/async
    Magick.NET (이미 보유)이미지 + 벡터(SVG/EPS/AI/PS) + PSD/HEIC. Ghostscript 델리게이트로 EPS/AI/PS 래스터화Apache-2.0production, 이미 프로젝트에서 사용 중
    LayoutFarm/Typography폰트 읽기/변환 — ttf/otf/ttc/woff/woff2 읽기, 글리프 레이아웃MIT/Apache 계열 (permissive, 라이선스 비용 없음)active이나 변환 API는 SixLabors/Aspose보다 저수준
    ACadSharp (DomCR)CAD — DXF/DWG 읽기/쓰기MITalpha (v3.6.x, 일부 엔티티 미구현·버그 가능)
    +

    통합 노트

    프로젝트의 추상화는 신규 카테고리 추가에 매우 친화적이다. 각 신규 Provider는 IConverterProvider 3개 멤버(Capability getter, CheckAvailabilityAsync, ConvertAsync)만 구현하면 되고, Everything2EverythingBootstrap.CreateDefault()의 providers 배열에 한 줄 추가하면 ProviderRegistry가 N×M 매트릭스를 자동 인덱싱한다(_byPair / _outputsByInput).\n\n구체적 통합 방법:\n\n1) 매트릭스 선언: ProviderCapability.PairsFromMatrix(Inputs, Outputs)를 그대로 재사용. 예) ArchiveProvider는 Inputs={zip,7z,tar,gz,...}, Outputs={zip,7z,tar,gz}로 선언. PandocProvider는 마크업 포맷 배열로 거대 매트릭스 자동 생성. (단 Pandoc/LibreOffice 매트릭스가 겹치는 pair는 ProviderRegistry가 _byPair.TryAdd로 '먼저 등록된 Provider 우선'이므로 Bootstrap 배열 순서로 라우팅 우선순위 제어 — LibreOffice를 오피스 바이너리에, Pandoc을 마크업에 우선시키려면 순서 조정).\n\n2) 외부 도구 탐지: DocumentProvider.CheckAvailabilityAsync가 ExternalToolDetector.TryFindLibreOfficeSoffice(out _)를 호출하고 NotReady(reason, ExternalDependencies)를 반환하는 패턴을 그대로 복제. ExternalToolDetector에 TryFindPandoc / TryFindCalibreEbookConvert / TryFindFfmpeg / TryFindGhostscript 메서드를 추가하면 됨. ExternalDependency 레코드(Name/Description/DownloadUrl/IsRequired)로 미설치 시 다운로드 안내 UI가 기존 DiagnoseWindow와 연동된다.\n\n3) 외부 CLI 변환: DocumentProvider.SofficeConvertAsync의 ProcessStartInfo(UseShellExecute=false, CreateNoWindow=true, ArgumentList, WaitForExitAsync(ct) + proc.Kill(true) on cancel) 패턴이 pandoc/ebook-convert/ffmpeg에 그대로 적용된다. 출력물 검증(File.Exists(produced)) + targetPath로 Move하는 흐름도 동일. progress?.Report()는 ffmpeg의 경우 stderr의 time= 파싱으로 실제 진행률 산출 가능(FFMpegCore가 OnProgress 콜백 제공).\n\n4) 순수 관리 코드 Provider(SharpCompress/Parquet.Net/ClosedXML/Svg.Skia)는 외부 프로세스 없이 ConvertAsync 내부에서 직접 호출 — CheckAvailabilityAsync는 항상 ProviderAvailability.Ready 반환(Status=Available). MagickProvider가 이미 이 형태이므로 동일 스타일.\n\n5) ConvertOptions 확장: 카테고리별 인코딩 옵션 클래스(JpegEncodingOptions 등)가 이미 있는 패턴을 따라 ArchiveOptions(압축레벨), EbookOptions(output-profile), MediaOptions(코덱/비트레이트), PandocOptions(standalone/toc) 등을 추가. ConvertOptions에 프로퍼티로 노출하면 UI가 자동 바인딩 가능.\n\n6) UX 확장: CliRouter(이미 존재)에 워치폴더 모드 추가 — FileSystemWatcher를 IHosted/백그라운드로 띄우고 Created 이벤트에 500ms 디바운스 타이머 + 파일 잠금 재시도(IOException 시 짧은 지연 후 재시도) + InternalBufferSize 64KB 상향. 변환은 기존 ConversionEngine + ProviderRegistry.TryGet으로 재사용. 큐 inline progress bar(이미 보유)와 결합하면 핫폴더→큐 자동 적재 UX 완성. XnConvert식 액션 체이닝은 ConvertOptions에 후처리 파이프라인(resize→convert) 추가로 확장 가능.

    +

    출처

    +
    +
    + +
    13

    코드 심층 분석 (5개 서브시스템)

    +

    현재 코드를 직접 읽고 file:line으로 인용한 약점·확장 차단 요소·개선 기회.

    +
    Core 추상화 & 변환 엔진 (Everything2Everything.Core) +
    +

    변환 능력을 IConverterProvider로 추상화하고, ProviderRegistry가 (입력확장자, 출력확장자) 쌍을 단일 홉 딕셔너리로 매핑하며, ConversionEngine이 단일/배치/결합 변환을 오케스트레이션하는 구조다. 양방향 N×M 매트릭스는 ProviderCapability.PairsFromMatrix로 각 Provider가 자기 입력·출력의 데카르트 곱을 선언해 표현하지만, 멀티홉 경로는 엔진이 아니라 각 Provider 내부에 하드코딩(DocxProvider/HwpxProvider→PdfProvider, HeicProvider→MagickProvider, DocumentProvider의 거대 switch)되어 있다. 이미지 중심으로 설계가 견고하게 동작하지만, 진정한 다방향 변환·AI·영상/오디오로 확장하려면 핵심 추상화 자체의 재설계가 필요하다.

    +

    설계 약점

    • 멀티홉 경로 탐색의 부재가 가장 큰 부채: ProviderRegistry는 단일 (input,output) 룩업만 하고(ProviderRegistry.cs:43,49) 그래프가 없어, A→B→C 같은 경로는 매번 Provider 내부에 손으로 짜야 한다. DocumentProvider.RouteAsync(DocumentProvider.cs:92-205)는 사실상 사람이 손으로 그린 경로 그래프이며, 형식이 늘어날수록 switch가 조합 폭발한다
    • Provider 간 체이닝이 생성자 주입으로 하드와이어됨: Bootstrap이 new HeicProvider(magick), new DocxProvider(pdf), new OcrProvider(pdf)처럼 의존성을 수동 결선(Everything2EverythingBootstrap.cs:9-21)한다. 새 중간 형식(예: DOCX→PDF→이미지 외에 DOCX→HTML→이미지)을 자동으로 발견할 방법이 없다
    • ProviderRegistry._byPair.TryAdd(ProviderRegistry.cs:22)는 같은 (input,output)을 여러 Provider가 선언하면 '먼저 등록된 것이 이긴다'를 조용히 적용한다. 우선순위/품질 기반 선택이 불가능하고, 충돌이 경고 없이 묻힌다(예: docx→png을 DocxProvider와 잠재적 다른 Provider가 동시 주장 시)
    • ConversionEngine이 ImageMagick에 직접 의존(ConversionEngine.cs:2)하고 CombineAsync/LoadImageForCombine/ApplyCombineEncoding(164-257)에서 MagickImageCollection을 직접 조작 — '결합'이 Provider 추상화 밖에 있어 엔진이 특정 라이브러리에 결합(coupling)되고, 이미지 외 결합(PDF 병합·동영상 concat)으로 확장 불가
    • ConvertOptions(ConvertOptions.cs:17-58)가 11개 sub-record를 가진 갓 오브젝트로, 형식 추가마다 sub-record가 늘어난다. 모든 Provider가 동일한 거대 옵션을 받지만 대부분 무시하고, 영상/오디오/코덱/AI 관련 옵션을 담을 자리가 없다
    • IConverterProvider(IConverterProvider.cs:9-15)가 '단일 파일 경로 in → outputDirectory에 파일 out, IProgress<double>'로 고정되어 있어 스트리밍, 다중 입력(N→1 결합), AI 프롬프트/모델 파라미터, 미디어 메타데이터(코덱·비트레이트·길이) 프로빙을 표현할 수 없다
    • Provider 목록이 컴파일타임 고정(Bootstrap의 배열). 플러그인 DLL 동적 로딩, ProviderStatus.ComingSoon을 실제 구현으로 교체할 확장 지점이 없다
    • 결합 가능 형식이 ConversionEngine의 정적 HashSet(CombinableInputs/Outputs, ConversionEngine.cs:14-23)에 박혀 있어 Provider의 능력 선언과 이중 관리되고 동기화가 깨지기 쉽다
    +

    확장성 차단 요소 (file:line)

    • ProviderRegistry.cs:6,43,49 — 매칭이 Dictionary<(Input,Output)> 단일 홉뿐. 멀티홉 경로 탐색(BFS/Dijkstra) API가 전혀 없어 양방향·다방향 극대화의 근본 한계
    • ProviderRegistry.cs:22 — `_byPair.TryAdd`가 동일 변환쌍 충돌을 조용히 첫 등록자 우선으로 삼킴. 비용/품질 가중치 기반 경로 선택 불가
    • ConversionEngine.cs:2 + 164-257 — 엔진이 ImageMagick(MagickImageCollection)에 직접 의존. 결합 로직이 Provider 밖에 있어 추상화 누수, 비이미지 결합 확장 차단
    • IConverterProvider.cs:9-15 — 시그니처가 (sourcePath, outputDirectory, outputExtension, ConvertOptions, IProgress<double>) 단일 파일·단일 출력 디렉터리·실수 진행률로 고정. 다중 입력·스트리밍·AI 파라미터·미디어 프로빙 표현 불가
    • Everything2EverythingBootstrap.cs:9-21 — Provider 인스턴스와 체이닝 의존성이 컴파일타임 하드코딩. 동적 플러그인 로딩/등록 지점 부재
    • ConvertOptions.cs:35-55 — 11개 이미지/문서 중심 sub-record. 영상 코덱/비트레이트/프레임레이트, 오디오, AI(모델·프롬프트·온도) 옵션을 담을 구조가 없고 형식마다 sub-record 증식
    • DocumentProvider.cs:92-205 — 멀티홉 라우팅이 손으로 짠 switch 그래프. 형식 N개에 대해 경로가 O(N^2)로 수동 증식, 새 형식 추가 시 모든 분기 갱신 필요
    • ConvertResult.cs:10 — 결과가 출력 파일 경로 리스트만 담음. 추출 텍스트·AI 응답·메타데이터·중간 산출물 같은 비파일 결과를 표현할 필드 없음
    +

    개선 기회

    • ProviderRegistry를 변환 그래프로 승격하고 엔진에 멀티홉 경로 탐색(BFS/Dijkstra) 추가 impact high effort high
    • CombineAsync를 IMultiInputProvider(또는 N→1 Provider 추상화)로 분리해 엔진의 ImageMagick 직접 의존 제거 impact high effort medium
    • IConverterProvider 시그니처를 ConvertRequest/ConvertContext 객체로 일반화하고 다중 입력·비파일 결과·미디어 메타데이터 지원 impact high effort high
    • AI/LLM 변환을 위한 IAiConverterProvider 확장과 ConvertOptions.Ai 옵션 도입 impact high effort medium
    • Provider 동적 로딩(플러그인) 도입 — Bootstrap 하드코딩 제거 impact medium effort high
    • ProviderRegistry 변환쌍 충돌을 명시적 우선순위/진단으로 전환 impact medium effort low
    • ConvertOptions를 형식별 옵션 백(IReadOnlyDictionary 또는 옵션 프로바이더)으로 분해 impact medium effort medium
    +
    Provider 매트릭스 전체 (8개 IConverterProvider + ProviderRegistry 디스패치) +
    +

    8개 Provider(Magick/Heic/Pdf/Docx/Html/Hwpx/Ocr/Document)가 `IConverterProvider`를 구현하고, 각자 `PairsFromMatrix(inputs, outputs)`로 N×M 변환 쌍을 카르테시안 곱으로 선언하면 `ProviderRegistry`가 `(input,output)→provider` 딕셔너리로 평탄화해 디스패치한다. 실제 변환은 대부분 "중간 포맷으로 정규화 후 위임"하는 파이프라인 — 이미지는 Magick로, 문서/한글은 LibreOffice→PDF→Magick로, HTML은 WebView2→PNG/PDF로 수렴한다. 결과적으로 매트릭스는 "거의 모든 것 → 이미지/PDF/텍스트" 방향으로만 풍부하고, 그 역방향(이미지/PDF → 편집가능 문서, HWP 출력, 미디어/아카이브 전 카테고리)이 구조적으로 비어 있다. 양방향·다방향·AI·미디어 코덱이라는 프로젝트 목표 대비 현재는 단방향 래스터라이저 집합에 가깝다.

    +

    설계 약점

    • 매트릭스가 사실상 단방향: 거의 모든 입력이 이미지/PDF/텍스트로 '나가기만' 한다. 편집가능 포맷으로 되돌아오는 경로가 OCR(이미지/PDF→txt/docx, 그것도 레이아웃 손실 평문)뿐이고, 진짜 구조 보존 역변환(PDF→DOCX, 이미지→벡터/문서)이 전무.
    • PDF가 입력으로만 풍부하고 출력이 빈약: PdfProvider는 PDF→이미지 렌더링만 한다(PdfProvider.cs:11-12). 이미지/HTML/DOCX→PDF는 각기 다른 Provider가 따로 만들지만, PDF→PDF(압축/병합/분할/회전)와 PDF→DOCX가 없어 프로젝트 핵심 목표인 'PDF 압축'을 어떤 Provider도 수행하지 못함.
    • HWP는 출력 불가가 구조적 한계로 고착: DocumentProvider가 HWP/HWPX를 입력으로만 받고 출력 Outputs 배열에 .hwp가 없다(DocumentProvider.cs:24, RoadmapNote:40). H2Orestart가 쓰기를 지원 안 해 'HWP↔DOCX/PDF/HTML' 목표의 절반(→HWP)이 막혀 있음.
    • DOC/DOCX 출력의 정의 충돌·왕복 불가능: DocxProvider는 .docx를 입력으로만(→PDF/이미지), DocumentProvider는 .docx를 입력이자 출력으로 선언하지만 OCR도 .docx를 출력한다. 'DOCX 편집본을 다시 받는' 일관된 단일 경로가 없고, DOCX→DOCX 같은 동일포맷은 ConversionEngine에서 Skip 처리됨.
    • 외부 프로세스 안정성 취약점: LibreOffice 호출이 3곳(DocxProvider/HwpxProvider/DocumentProvider)에 복붙되어 있고 타임아웃이 전혀 없다 — soffice가 멈추면 WaitForExitAsync가 무한 대기(취소 토큰에만 의존). 또한 soffice는 동일 사용자 프로필을 공유해 동시 인스턴스 충돌 위험이 있으나 직렬화/락이 없음.
    • AI/LLM 통합 지점 부재: ConvertOptions에 OCR 백엔드 문자열('auto')만 있을 뿐, Codex/LLM 호출을 위한 추상화(요약/번역/생성형 변환/캡셔닝)가 인터페이스·옵션·Provider 어디에도 없음. 현재 구조에 AI를 끼워넣을 확장점이 설계되지 않음.
    • 미디어·아카이브·폰트·CAD 카테고리 전무: 영상(mp4/mov/webm), 오디오(mp3/wav/flac), 아카이브(zip/7z/tar), 폰트(ttf/otf/woff), 벡터(svg/eps), CAD(dwg/dxf), 전자책(epub/mobi)을 다루는 Provider가 0개. `ProviderStatus.ComingSoon`은 enum에만 존재하고 이를 사용하는 Provider가 하나도 없어 로드맵 UI가 빈 상태.
    • 레지스트리의 조용한 우선순위 함정: `_byPair.TryAdd`(ProviderRegistry.cs:22)는 첫 등록 Provider가 승리하고 후속은 '말없이 무시'된다. 향후 같은 (input,output) 쌍을 두 Provider가 선언하면(예: PDF 압축 vs PDF 렌더) 부트스트랩 순서에 따라 비결정적으로 한쪽이 사라짐 — 경고도 없음.
    +

    확장성 차단 요소 (file:line)

    • IConverterProvider.cs:9-15 — ConvertAsync 시그니처가 단일 sourcePath/단일 outputExtension에 고정. 다방향(N입력→1출력 병합, 1입력→N출력 동시) 변환과 '입력=출력 동일포맷 최적화(PDF압축, 이미지 리인코딩)'가 인터페이스 레벨에서 표현 불가 (동일포맷은 ConversionEngine.cs:88-89에서 무조건 Skip됨).
    • DocumentProvider.cs:24 `Outputs = {.html,.docx,.md,.txt}` 및 :40 RoadmapNote — HWP/HWPX가 출력 배열에서 영구 제외. 양방향 HWP를 위해서는 H2Orestart 쓰기 대체재(또는 자체 HWP writer)가 필요하나 코드 구조상 출력 라우트(RouteAsync, :92-205)에 .hwp 분기 자체가 없음.
    • PdfProvider.cs:11-12 PdfRenderOutputs에 .pdf·.docx 부재 — PDF→PDF(압축/병합) 및 PDF→편집문서 경로가 Provider 차원에서 차단. PDFtoImage 라이브러리는 렌더 전용이라 PDF 쓰기/조작 백엔드(예: PdfPig/QuestPDF/iText) 도입 전까지 확장 불가.
    • ConvertOptions.cs 전체 — 옵션 클래스가 이미지·PDF렌더·HTML렌더·OCR로 한정. Video/Audio/Archive/Ai 옵션 그룹이 없어, 미디어 코덱(비트레이트/해상도/fps)이나 LLM 프롬프트/모델 선택을 전달할 통로가 없음. 새 카테고리는 옵션 모델 확장이 선행되어야 함.
    • MagickProvider.cs:8-19, HeicProvider.cs:11-12, PdfProvider.cs:11-12 등 — 각 Provider가 지원 확장자 배열을 하드코딩. ffmpeg 같은 '수백 포맷 양방향' 백엔드를 PairsFromMatrix로 표현하면 조합 폭발(예: 50입력×50출력=2500쌍)로 레지스트리 메모리·UI 매트릭스가 비현실적 — 능력 기반(capability predicate) 표현이 없음.
    • ProviderRegistry.cs:22 `_byPair.TryAdd` — 동일 (input,output)에 복수 Provider(예: 빠른 변환 vs 고품질 vs AI 변환)를 '선택지'로 공존시키는 모델 부재. 한 쌍당 정확히 하나의 Provider만 허용해 변환 전략 다중화(품질/속도/AI 토글)가 불가능.
    • Everything2EverythingBootstrap.cs:11-21 — Provider 목록이 컴파일타임 하드코딩 배열. 플러그인/동적 등록(외부 DLL, ffmpeg 유무에 따른 조건부 등록)이 없어 신규 카테고리 추가가 항상 코어 재컴파일을 요구.
    +

    개선 기회

    • LibreOffice 호출 로직을 단일 SofficeRunner 서비스로 통합 + 타임아웃·직렬화 추가 impact high effort low
    • 이미지 인코딩(ApplyEncoding/ApplyTransforms) 공용 ImageEncoder 헬퍼로 추출 impact medium effort low
    • FFmpeg 기반 미디어 Provider(영상·오디오) 신설 + 능력기반 매트릭스 표현 도입 impact high effort high
    • PDF 쓰기/조작 백엔드 도입으로 PDF→PDF(압축/병합/분할)·PDF→DOCX 경로 개통 impact high effort high
    • AI/LLM 변환 확장점 설계: IConverterProvider에 AI Provider 카테고리 + ConvertOptions.Ai 옵션 그룹 impact high effort high
    • 레지스트리 다중 Provider 공존 + 명시적 우선순위/충돌 경고 모델 impact medium effort medium
    • 아카이브(zip/7z) 및 폰트(ttf↔woff2) Provider 추가로 빈 카테고리 보강 impact medium effort medium
    +
    App UI / CLI / Shell (Everything2Everything WPF .NET 9) +
    +

    WPF FluentWindow 기반 데스크톱 UI로, MainWindow가 큐 관리·매트릭스 출력 필터링·진행 표시·프리뷰·이력을 모두 코드비하인드(MainWindow.xaml.cs 1171줄)에서 직접 처리한다. 출력 형식은 큐 내 모든 입력의 OutputsForFile 교집합으로 1-hop 직접 변환만 필터링하며(RefreshAvailableOutputFormats:836), 멀티홉/체이닝/AI/미디어 개념은 코드 어디에도 없다. CLI(CliRouter)는 5개 verb를 파싱하지만 register/diagnose를 제외하면 모두 GUI 창을 띄우는 런처에 불과해 stdout/exit-code 기반 자동화·파이프라인이 불가능하다. ContextMenuRegistrar는 12개 PopularOutputs를 별도 하드코딩하여 ProviderRegistry 매트릭스와 부분적으로만 동기화된다.

    +

    설계 약점

    • MVVM 전무: MainWindow.xaml.cs 1171줄에 View 로직·ViewModel(QueueItem/DateGroup/HistoryRow)·Service 호출(PreviewService/HistoryStorage/engine.ConvertManyAsync)·CSV/JSON 익스포트가 한 파일에 혼재. 명령은 일부 RelayCommand(25-38)지만 대부분 코드비하인드 이벤트 핸들러(OnProcessQueueClick 등)라 테스트·재사용 불가
    • 변환 옵션 UI가 형식별로 확장 불가능한 구조: 사이드바에는 Quality 슬라이더 하나만 있고 ConvertOptions의 10여 종 옵션(PngCompression, AvifSpeed, PdfRender.Dpi, Ocr.Language, HtmlRender.Viewport 등)이 전혀 노출되지 않음. BuildOptions(259-281)는 JPEG/WebP/AVIF Quality만 슬라이더에서 읽고 나머지는 모두 기본값 하드코딩
    • CLI가 진정한 자동화에 부적합: dialog/quick/showmain 모드가 모두 WPF 창을 띄우며(App.xaml.cs:39-57), stdout으로 결과(출력 경로/성공·실패 카운트)를 반환하지 않고 exit code도 Quick 실패 시 1만 반환. JSON 출력·배치 매니페스트·표준입력 파이프·진행 스트리밍이 없어 AI/스크립트가 결과를 파싱 불가
    • 출력 형식 매트릭스가 3곳에 중복 하드코딩: MainWindow.AllFormats(813-827, 12개), ContextMenuRegistrar.PopularOutputs(14-28, 12개), OpenFileDialog 입력 필터(96). 새 형식 추가 시 세 곳을 수동 동기화해야 하며 라벨·정렬·색상 리소스 키가 분산
    • 멀티홉 경로 표현 수단이 UI/엔진 양쪽에 전무: ComboBox는 1-hop 직접 변환만 나열하고, 멀티홉이 생기면 '직접 vs 경유' 구분, 경로 미리보기(예: HWP→PDF→PNG), 중간 형식 선택, 품질 누적 손실 경고를 표현할 자리가 없음
    • QuickProgressWindow는 취소 버튼이 없어(취소 토큰을 ConvertManyAsync에 전달조차 안 함, App.xaml.cs:96) CLI/우클릭 경로의 긴 변환을 중단할 방법이 없음 — 메인 창과 취소 UX가 비대칭
    • 케이퍼빌리티 상태가 시작 시 1회만 점검되고(RefreshCapabilityStatusAsync) 결과를 캐시하지 않아, 형식 선택 시점에 해당 출력이 외부 도구 부재로 실패할지 사전 차단하지 못함 — 변환 실행 후에야 실패 확인
    • 데모 시드 데이터(SeedDemoHistory:667-708)가 프로덕션 코드에 하드코딩되어 첫 실행 시 가짜 14.2MB PNG 등을 이력에 주입, 통계(EST. SPACE SAVED)를 오염시킴
    +

    확장성 차단 요소 (file:line)

    • MainWindow.xaml.cs:849-858 — RefreshAvailableOutputFormats가 OutputsForFile(1-hop 직접 변환)의 단순 교집합만 계산. 멀티홉 그래프 탐색(BFS/DFS over ConversionPair)이 없어 'HWP→DOCX 경유 PDF' 같은 간접 경로가 출력 목록에 절대 나타나지 않음
    • ProviderRegistry.cs:52-61 — OutputsForInput이 _outputsByInput 직접 매핑만 반환. 도달 가능 그래프(transitive closure)·경로 비용·중간 형식 개념이 없어 다방향 변환 확장의 근본 차단점
    • MainWindow.xaml.cs:963-976 — UpdateQualityPanelForFormat이 ext를 switch로 하드코딩(.jpg/.webp/.avif만 quality 패널 표시). 형식별 옵션 스키마가 Provider에서 선언적으로 오지 않아, 새 옵션(코덱·비트레이트·OCR 언어·LLM 프롬프트)을 추가하려면 XAML+코드비하인드를 직접 수정해야 함
    • MainWindow.xaml.cs:259-281 — BuildOptions가 ConvertOptions의 일부 필드만 수동 채움. 미디어/AI Provider가 요구할 옵션(영상 코덱, CRF, 프레임레이트, AI 모델명, API 키)을 받을 동적 옵션 바인딩 메커니즘 부재
    • IConverterProvider.cs:9-15 — ConvertAsync 시그니처가 단일 sourcePath→단일 outputExtension 동기 변환 전제. 스트리밍 입력, 다중 출력 산출물(예: 영상→썸네일+자막+트랜스코드), AI 비동기 잡, 진행 중 부분 결과를 표현할 수 없음
    • ContextMenuRegistrar.cs:14-28 — PopularOutputs 배열이 정적이라 AI/미디어 형식(.mp4/.webm 등) 추가 시 카스케이드에 자동 반영되지 않고, 멀티홉 출력도 우클릭 메뉴에 노출 불가
    • App.xaml.cs:39-57 & CliRouter.cs:21-49 — CLI 파서가 옵션 플래그(--quality, --output-dir, --json, --recursive)를 전혀 받지 않고 verb+files 구조만 지원. AI 통합(LLM 프롬프트 전달)·배치 스크립팅을 위한 인자 확장 여지가 구조적으로 막힘
    +

    개선 기회

    • 엔진에 변환 그래프 + 멀티홉 경로 탐색 도입 impact high effort high
    • Provider 선언형 옵션 스키마 + 동적 옵션 UI 생성 impact high effort high
    • 헤드리스 CLI 모드 분리 (stdout JSON + exit code + 옵션 플래그) impact high effort medium
    • 출력 형식 매트릭스 단일 SSOT로 통합 impact medium effort low
    • MainWindow를 MVVM으로 분해 impact medium effort high
    • QuickProgressWindow에 취소 + 케이퍼빌리티 사전 점검 impact medium effort medium
    +
    인프라 (히스토리 / 프리뷰 / 빌드 / 패키징 / CI-CD / 설정 영속화) +
    +

    히스토리는 `%LocalAppData%`에 append-only JSONL로 저장하는 정적 헬퍼(HistoryStorage)와 UI 전용 인메모리 컬렉션(HistoryStore)으로 단순 분리돼 있고, 프리뷰는 이미지/PDF/HEIC만 동기 렌더하며 문서·영상은 명시적으로 스텁 처리한다. 빌드/패키징은 framework-dependent MSIX 단일 산출물에 최적화돼 있어 .NET DLL과 C++ Shell DLL만 Layout에 복사하고 FFmpeg/Ghostscript 같은 대형 외부 바이너리 번들링 메커니즘이 전혀 없다. 가장 큰 구조적 공백은 설정 영속화 계층의 완전 부재로, 변환 옵션은 매번 UI에서 재구성되고(BuildOptions) AI API 키·도구 경로·사용자 기본값을 저장할 곳이 없으며, 테스트 인프라도 0이다.

    +

    설계 약점

    • 설정 영속화 계층이 전무함 — 저장되는 영구 상태는 history.jsonl 단 하나. ConvertOptions는 MainWindow.BuildOptions()(MainWindow.xaml.cs:259-281)에서 매 실행마다 UI 컨트롤로부터 새로 생성되고 _conflictRule·QualitySlider 값은 앱 재시작 시 소실. AI(Codex/LLM) API 키, LibreOffice/FFmpeg 경로, 사용자 기본 출력 형식을 저장할 메커니즘이 없어 AI 통합·도구 경로 캐싱의 토대가 없음
    • PreviewService가 문서·영상을 원천 미지원 — 문서는 '다음 업데이트에서 지원'(PreviewService.cs:28-29), 영상은 case 자체가 없어 RenderViaMagick(PreviewService.cs:30)로 폴백→MagickImage가 mp4/mov를 못 열어 예외. HWP↔DOCX, 영상 코덱 변환 목표 대비 프리뷰가 핵심 입력 타입을 커버 못함
    • 프리뷰 캐싱 부재 — 같은 파일을 다시 선택할 때마다 디코딩/리사이즈/temp PNG 왕복(RenderHeic의 PreviewService.cs:67-76)을 재수행. 대용량/배치 큐에서 동일 항목 반복 클릭 시 매번 풀 디코딩
    • 히스토리 전체 로드가 비확장적 — File.ReadAllLines(HistoryStorage.cs:27)로 전 파일을 메모리에 적재 후 OrderBy(MainWindow.xaml.cs:663)로 전량 정렬. 영상/배치 변환으로 엔트리가 수만 줄 쌓이면 시작 시 전부 파싱·정렬해야 함(페이징·tail read·만료 정책 없음)
    • 히스토리 도메인 로직이 UI에 결합 — 로드/그룹화/데모 시드(SeedDemoHistory MainWindow.xaml.cs:667-708)가 MainWindow.xaml.cs에 박혀 있고, 실측 데이터가 없으면 가짜 '842.4 MB 절약' 데모가 통계에 섞임(MainWindow.xaml.cs:685,704). 헤드리스/CLI 배치 경로에서 히스토리 기록 재사용 불가
    • 빌드/패키징이 대형 외부 바이너리 번들을 전혀 가정하지 않음 — BuildMsix.ps1:108-109가 publish 산출물 + Shell DLL만 Layout에 복사. FFmpeg(~80MB)/Ghostscript를 동봉하려면 Layout 복사 단계, MSIX 용량(현재 200MB+ 압축 한계 고려), 라이선스(FFmpeg LGPL/GPL) 처리 로직이 모두 없음
    • 테스트 인프라 0 — 솔루션에 테스트 프로젝트가 없음(Glob **/*Test* 무결과). OutputPathHelper 충돌 해결, ConversionEngine 결합 로직, JSONL round-trip 같은 순수 함수조차 자동 검증 없어 양방향 매트릭스 확장 시 회귀 탐지 불가
    • CI가 self-contained/portable EXE를 빌드·게시하지 않음 — README는 portable EXE를 'A) 가장 가벼움'으로 권장하나(README.md:140), 두 워크플로 모두 MSIX만 산출(build.yml:42, release.yml:75). .NET 9 Desktop Runtime 미설치 환경용 self-contained 배포 산출물이 자동화에 없음
    • CI에 NuGet/빌드 캐시가 없어(build.yml 전체) 매 실행마다 Magick.NET·WebView2 등 대형 패키지를 재복원 — 외부 바이너리까지 더해지면 빌드 시간 선형 증가
    +

    확장성 차단 요소 (file:line)

    • PreviewService.cs:23-31 — 확장자 switch가 닫힌 구조이고 default가 RenderViaMagick(MagickImage)로 폴백. 영상(.mp4/.mov/.mkv) 프리뷰를 추가하려면 FFmpeg 프레임 추출 case를 직접 삽입해야 하며, 현재는 영상 입력 시 MagickImage 생성자에서 예외 발생
    • PreviewService 전체가 static 클래스 + 하드코딩된 디코더 의존성(ImageMagick/PDFtoImage/Libheif) — 영상/AI 썸네일을 위한 디코더 플러그인 주입(DI) 지점이 없어 IPreviewRenderer 추상화 없이는 확장 불가
    • ConvertOptions(ConvertOptions.cs:17-58)에 영상(코덱/비트레이트/fps)·AI(모델/프롬프트/API키)·압축(PDF 압축 레벨) 옵션 sub-record가 없고, 영속화 대상이 아니라 [JsonSerializable] 직렬화 속성도 없음 — AI 기능 설정과 도구 경로를 담을 영구 설정 모델이 부재
    • BuildMsix.ps1:102-115 Layout 구성 단계가 publish 출력 + 단일 Shell DLL만 복사하도록 하드코딩 — FFmpeg/Ghostscript/Tesseract 같은 외부 바이너리를 동봉하는 Copy-Item 단계와 그 경로를 런타임에 해석하는 메커니즘이 없음
    • Package.appxmanifest:175-177이 runFullTrust 단일 capability만 선언 — AI 기능의 네트워크 호출(internetClient)이나 추가 파일 타입(.mp4/.mov/.webm/.mkv) ItemType 등록(현재 manifest:69-168에 영상 확장자 전무)이 없어 영상 우클릭 메뉴 노출 불가
    • HistoryEntry(HistoryStore.cs:5-15)가 영상/배치 변환 메타데이터(코덱, 해상도, duration, 다중 출력 통계)를 표현할 필드가 부족 — MetaLine 단일 문자열에 의존(HistoryStore.cs:13)해 구조화된 영상 변환 이력 질의 불가
    • 설정/키 저장소가 없어 AI API 키를 DPAPI(ProtectedData)로 암호화 저장할 진입점 자체가 부재 — HistoryStorage.cs의 LocalAppData 패턴은 있으나 평문 JSONL이라 비밀 저장에 부적합
    +

    개선 기회

    • ISettingsStore(설정 영속화 계층) 도입 — DPAPI 암호화 + JSON impact high effort medium
    • IPreviewRenderer 추상화 + 영상 프레임 추출(FFmpeg) + 프리뷰 캐시 impact high effort high
    • 외부 바이너리 번들링 파이프라인 (BuildMsix.ps1 확장 + ExternalToolDetector 폴백) impact high effort high
    • 히스토리 영속화의 스트리밍 로드 + 만료/회전 정책 impact medium effort medium
    • 히스토리 도메인 로직을 UI에서 Core로 분리 + 데모 시드 제거 impact medium effort low
    • 테스트 프로젝트 신설 (xUnit) — 순수 함수 우선 impact high effort medium
    • CI 강화 — NuGet 캐시 + self-contained portable 산출물 + 테스트 게이트 impact medium effort low
    • OutputPathHelper 충돌 처리 강건화 + 결합 출력 경로 일관화 impact low effort low
    +
    횡단 품질 (에러 처리 / 동시성 / 취소 / 보안 / 메모리 / 테스트) +
    +

    변환 파이프라인의 횡단 관심사는 비교적 일관된 패턴(IProgress 보고, OperationCanceledException 재던짐, try/finally 임시정리, ConvertResult.Fail 래핑)으로 손코딩되어 있고, 외부 프로세스 호출은 ProcessStartInfo.ArgumentList를 사용해 셸 인젝션을 구조적으로 회피한다. 그러나 배치 변환이 전적으로 순차 for-loop(ConversionEngine.cs:57)라 멀티코어/외부프로세스 대기 시간을 전혀 활용하지 못하고, 취소 토큰 전파가 라이브러리 경계(Magick.NET, Windows OCR, CDP)에서 끊기며, 메모리는 전 페이지/전 프레임을 한꺼번에 디코딩하는 비스트리밍 구조다. 테스트 프로젝트가 전무(0개)하고 NuGet 취약점 경고가 csproj에서 통째로 억제(NU1901-1904)되어, AI/영상/대용량 기능으로 확장하기 전에 품질 안전망이 먼저 필요하다.

    +

    설계 약점

    • 배치가 100% 순차 for-loop(ConversionEngine.cs:57-70)다. LibreOffice/OCR/WebView2처럼 대기 시간이 긴 변환에서 단일 파일씩만 처리해 멀티코어를 전혀 못 쓴다. 100장 이미지 변환도 코어 1개만 사용.
    • 취소 토큰이 라이브러리 경계에서 단절된다. Magick.NET의 image.Write/Resize/collection.Coalesce(MagickProvider.cs:84-94, PdfProvider.cs:84-87)는 ct를 받지 않아, 거대 이미지 1장 인코딩 중에는 ThrowIfCancellationRequested 체크 지점 사이에서 취소가 지연된다. Windows OCR engine.RecognizeAsync(OcrProvider.cs:164)도 ct 미전달.
    • CLI quick 경로가 아예 취소 불가능하다: App.RunQuickAsync가 ConvertManyAsync를 CancellationToken 없이 호출(App.xaml.cs:96)하고 QuickProgressWindow에 취소 UI도 없다. 컨텍스트 메뉴 대량 변환을 중단할 방법이 없음.
    • 에러 메시지 채널이 비일관적이다. 코어는 ConvertResult.Message(구조화)로 반환하지만, MainWindow는 catch에서 MessageBox.Show(ex.Message)로만 노출(MainWindow.xaml.cs:569)하고 OnProcessQueueClick은 개별 result.Status가 Failed/Skipped여도 그냥 히스토리에만 적고(MainWindow.xaml.cs:551-561) 사용자에게 실패를 표면화하지 않는다. 실패 파일도 done처럼 큐에서 제거됨(line 563).
    • 메모리가 비스트리밍이다. PDF는 페이지마다 PNG 전체를 MemoryStream에 디코딩(PdfProvider.cs:79-87), OCR은 파일을 MemoryStream→ToArray()→InMemoryRandomAccessStream으로 3중 복사(OcrProvider.cs:148-159), HTML 캡처는 base64 PNG 전체를 메모리에 들고 Magick으로 재디코딩(HtmlProvider.cs:76-98). 8000x8000 같은 대용량/멀티프레임 GIF는 collection.Coalesce()로 전 프레임을 동시에 메모리에 적재(MagickProvider.cs:84).
    • ImageMagick 리소스 한계(ResourceLimits.Memory/Width/Height)가 어디에도 설정돼 있지 않다. 악의적/손상 이미지(decompression bomb)나 거대 RAW가 프로세스 메모리를 무제한 점유 가능. MagickProvider/PdfProvider/HtmlProvider/CombineAsync 전부 무방비.
    • 테스트가 0개다(test 프로젝트/파일 없음 — Glob *Test* 결과 없음). 12종×N 양방향 매트릭스, 라우팅 분기(DocumentProvider.RouteAsync), 충돌 규칙, 취소 경로 모두 회귀 검증이 불가능.
    • csproj가 NuGet 취약점 경고를 통째로 억제한다: NoWarn에 NU1901;NU1902;NU1903;NU1904 (Everything2Everything.Core.csproj:11). 알려진 CVE가 있는 패키지가 들어와도 빌드가 침묵한다. WebView2/OpenXML/Magick은 외부 미디어를 파싱하는 공격면이 큰 라이브러리들이라 위험.
    • _cts 접근에 동시성 보호가 없다. OnProcessQueueClick은 _cts.Token을 await 호출 인자로 직접 읽고(MainWindow.xaml.cs:541) finally에서 _cts=null로 set(line 574)하는데, OnCancelProcessingClick(line 621-622)이 다른 시점에 _cts.Cancel()을 호출한다. UI 스레드 단일 진입으로 대체로 안전하나 _cts 수명/dispose가 명시적이지 않고 CancellationTokenSource.Dispose()가 한 번도 호출되지 않음(누수).
    +

    확장성 차단 요소 (file:line)

    • ConversionEngine.cs:57-70 — ConvertManyAsync의 순차 for-loop가 하드코딩됨. 영상 트랜스코딩(파일당 수십 초~분)이나 AI 호출(LLM 왕복 지연)을 추가하면 순차 처리가 치명적 병목이 된다. 병렬도(MaxDegreeOfParallelism) 옵션이 ConvertOptions에 없음(ConvertOptions.cs 전체).
    • IProgress<double> 단일 스칼라 진행 모델(IConverterProvider.ConvertAsync 시그니처)은 영상(프레임/시간코드), AI(토큰 스트리밍), 다단계 파이프라인의 진행을 표현 못 한다. ConvertProgress(Index,Total,CurrentPath,FileProgress) (ConversionEngine.cs:285)도 단일 파일=단일 출력 가정에 묶여 있어 1→N 페이지 분할의 부분 진행을 못 담는다.
    • ConvertResult가 동기 완료 모델(ConvertResult.cs:10-25)이라 스트리밍/증분 출력(영상 인코딩 중 부분 미리보기, LLM 토큰 스트림)을 표현할 타입이 없다. OutputPaths는 변환 끝난 뒤에야 채워짐.
    • 외부 프로세스 실행 로직(ConvertWithLibreOfficeAsync)이 DocxProvider/HwpxProvider/DocumentProvider에 거의 동일하게 3중 복제됨(DocxProvider.cs:113-157, HwpxProvider.cs:107-151, DocumentProvider.cs:238-281). FFmpeg/ghostscript(PDF압축)/codex CLI 같은 새 외부도구를 추가할 때마다 stderr 수집·타임아웃·종료처리·결과검증 보일러플레이트를 또 복붙해야 한다. 공통 ExternalProcessRunner 추상화 부재.
    • 외부 프로세스에 타임아웃이 없다(WaitForExitAsync(ct)만, DocumentProvider.cs:266 등). LibreOffice/Word COM(DocxProvider.cs:159-186)이 hang하면 취소하기 전까지 영원히 대기. 영상/대용량 작업에선 walltime 한계가 필수.
    • stderr를 RedirectStandardError=true로 켜두고도 한 번도 읽지 않는다(DocumentProvider.cs:252, HwpxProvider.cs:116, DocxProvider.cs:122). 파이프 버퍼가 가득 차면 자식 프로세스가 블록될 수 있고, 실패 시 LibreOffice의 실제 오류 사유를 버려 ConvertResult가 'exit N'만 남긴다(DocumentProvider.cs:270). AI/코덱 도구 디버깅이 불가능.
    +

    개선 기회

    • 배치 변환에 제한된 병렬 처리(Parallel.ForEachAsync) 도입 impact high effort medium
    • 공통 ExternalProcessRunner 추상화 (타임아웃 + stderr 수집 + Kill 통합) impact high effort medium
    • ImageMagick ResourceLimits 전역 설정 + decompression bomb 방어 impact high effort low
    • 코어 단위 테스트 프로젝트 신설 (xUnit) — 순수 로직 우선 impact high effort medium
    • 실패/건너뜀 결과를 UI에 일관되게 표면화 impact medium effort low
    • CLI quick 경로에 취소 토큰 전파 + NuGet 취약점 경고 재활성화 impact medium effort low
    • 대용량 입력 스트리밍/페이지 단위 메모리 관리 impact medium effort high
    • 외부 프로세스 입력 검증 강화 (LibreOffice 출력 파일명 충돌) impact low effort low
    +
    +
    + +
    + Everything2Everything SSOT · 생성 2026-06-01 · 17 agents · 1,426,291 tokens
    + 이 페이지는 docs/ssot/_data/*.json 에서 python build.py 로 재생성됩니다. · https://github.com/yunchan8804-blip/Everything2Everthing.git +
    +
    +
    + + \ No newline at end of file