전체 소스 심층 분석(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 미디어 레이어
576 lines
No EOL
70 KiB
JSON
576 lines
No EOL
70 KiB
JSON
[
|
||
{
|
||
"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<double> 진행률을 멀티홉 구간별로 분할 합성(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<double>'로 고정되어 있어 스트리밍, 다중 입력(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<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 응답·메타데이터·중간 산출물 같은 비파일 결과를 표현할 필드 없음"
|
||
],
|
||
"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<double>로 고정되어 영상/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<Provider>로 바꾸고 우선순위/품질태그를 부여하면 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 <ext> %1' CLI 호출로 GUI 없이 즉시 변환되는 깔끔한 셸 통합",
|
||
"진행 표시·취소 UX가 IProgress<ConvertProgress> + 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<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/코덱 도구 디버깅이 불가능."
|
||
],
|
||
"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 검토 지점."
|
||
}
|
||
]
|
||
}
|
||
] |