From cdfbb5909568b4346c63b32b734f038311c8de9a Mon Sep 17 00:00:00 2001
From: Jaeho
Date: Sun, 23 Aug 2026 04:02:40 +0900
Subject: [PATCH 1/4] =?UTF-8?q?feat(frontend):=20=EB=A7=88=ED=81=AC?=
=?UTF-8?q?=EB=8B=A4=EC=9A=B4=20=EA=B3=84=EC=95=BD=20=ED=95=84=EB=93=9C=20?=
=?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=9F=AC=20=EB=8F=84=EC=9E=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
shared/ui/Markdown — react-markdown+remark-gfm+rehype-sanitize(security.md §4.4
지정 조합, exact pin). 렌더러 본체는 React.lazy 청크로 분리해 메인 번들
+4.9KB 에 그친다(로딩 사이 fallback 은 종전과 동일한 plain text). GFM 계약
필드(코칭 모범답안·리라이트, 분석 산출물)에만 적용 — plain 계약 필드
(하이라이트 매칭·델타 스트리밍 표면)와 사용자 입력은 렌더하지 않는다.
분석 상세 모달에 '분석 원문 보기' 신설 — presigned 직접 fetch 는 내부
호스트라 불가하므로 Core 프록시(GET /api/documents/{id}/content) 경유.
오답노트 목록/드릴의 중복 패널은 ReviewBlock 으로 공용화. 테스트 +4.
---
frontend/CLAUDE.md | 1 +
frontend/package-lock.json | 1606 ++++++++++++++++-
frontend/package.json | 3 +
.../analysis/model/useDocumentMarkdown.ts | 22 +
.../src/features/analysis/ui/DocumentList.tsx | 32 +-
.../features/interview/ui/BookmarkDrill.tsx | 25 +-
.../features/interview/ui/BookmarkList.tsx | 25 +-
.../src/features/interview/ui/ReviewBlock.tsx | 38 +
.../ui/live/AnswerCoachingAccordion.tsx | 11 +-
frontend/src/shared/api/generated.ts | 69 +
.../src/shared/ui/Markdown/Markdown.test.tsx | 49 +
frontend/src/shared/ui/Markdown/Markdown.tsx | 20 +
.../src/shared/ui/Markdown/Markdown.types.ts | 5 +
.../shared/ui/Markdown/MarkdownContent.tsx | 94 +
frontend/src/shared/ui/Markdown/index.ts | 2 +
frontend/src/shared/ui/index.ts | 2 +
16 files changed, 1913 insertions(+), 91 deletions(-)
create mode 100644 frontend/src/features/analysis/model/useDocumentMarkdown.ts
create mode 100644 frontend/src/features/interview/ui/ReviewBlock.tsx
create mode 100644 frontend/src/shared/ui/Markdown/Markdown.test.tsx
create mode 100644 frontend/src/shared/ui/Markdown/Markdown.tsx
create mode 100644 frontend/src/shared/ui/Markdown/Markdown.types.ts
create mode 100644 frontend/src/shared/ui/Markdown/MarkdownContent.tsx
create mode 100644 frontend/src/shared/ui/Markdown/index.ts
diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md
index 5b372481..b259e563 100644
--- a/frontend/CLAUDE.md
+++ b/frontend/CLAUDE.md
@@ -20,6 +20,7 @@
| Styling | Tailwind CSS v4 (`@theme` 토큰) | 4.x (도입) |
| API 타입 | openapi-typescript → `shared/api/generated.ts` | (도입, §7.1) |
| 테스트 | Vitest + Testing Library (jsdom) | (도입) |
+| 마크다운 렌더 | react-markdown + remark-gfm + rehype-sanitize (`shared/ui/Markdown`, lazy) | (도입, A5 — 계약: docs/frontend-types.md §6.5) |
### 미정 (도입 시점에 결정)
- Form: React Hook Form + Zod (현재 controlled `useState` 검증으로 충분 — 폼 복잡도 증가 시 도입)
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index f8c399b5..8b699bc8 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -17,7 +17,10 @@
"jspdf": "^4.2.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
+ "react-markdown": "10.1.0",
"react-router-dom": "^7.15.0",
+ "rehype-sanitize": "6.0.0",
+ "remark-gfm": "4.0.1",
"tailwindcss": "^4.3.0"
},
"devDependencies": {
@@ -1540,6 +1543,15 @@
"assertion-error": "^2.0.1"
}
},
+ "node_modules/@types/debug": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
+ "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
@@ -1551,9 +1563,26 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
+ "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1561,6 +1590,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "24.12.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
@@ -1588,7 +1632,6 @@
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
- "dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -1611,6 +1654,12 @@
"license": "MIT",
"optional": true
},
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.57.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz",
@@ -1906,6 +1955,12 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
+ "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==",
+ "license": "ISC"
+ },
"node_modules/@vitejs/plugin-react": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
@@ -2179,6 +2234,16 @@
"proxy-from-env": "^2.1.0"
}
},
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2327,6 +2392,16 @@
"node": ">=10.0.0"
}
},
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@@ -2361,6 +2436,46 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2400,6 +2515,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2488,7 +2613,6 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/data-urls": {
@@ -2529,6 +2653,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/decode-named-character-reference": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
+ "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -2549,7 +2686,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -2564,6 +2700,19 @@
"node": ">=8"
}
},
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/dom-accessibility-api": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
@@ -2894,6 +3043,16 @@
"node": ">=4.0"
}
},
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -2924,6 +3083,12 @@
"node": ">=12.0.0"
}
},
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -3229,6 +3394,61 @@
"node": ">= 0.4"
}
},
+ "node_modules/hast-util-sanitize": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz",
+ "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "unist-util-position": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/hermes-estree": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
@@ -3259,6 +3479,16 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
@@ -3359,12 +3589,52 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/inline-style-parser": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
+ "license": "MIT"
+ },
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
"license": "MIT"
},
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3388,6 +3658,28 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -3862,6 +4154,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -3892,6 +4194,16 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/markdown-table": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -3901,83 +4213,928 @@
"node": ">= 0.4"
}
},
- "node_modules/mdn-data": {
- "version": "2.27.1",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
- "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
- "dev": true,
- "license": "CC0-1.0"
+ "node_modules/mdast-util-find-and-replace": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "escape-string-regexp": "^5.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
},
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"license": "MIT",
"engines": {
- "node": ">= 0.6"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
+ "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
"license": "MIT",
"dependencies": {
- "mime-db": "1.52.0"
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-gfm-autolink-literal": "^2.0.0",
+ "mdast-util-gfm-footnote": "^2.0.0",
+ "mdast-util-gfm-strikethrough": "^2.0.0",
+ "mdast-util-gfm-table": "^2.0.0",
+ "mdast-util-gfm-task-list-item": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
},
- "engines": {
- "node": ">= 0.6"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/min-indent": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
- "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
- "dev": true,
+ "node_modules/mdast-util-gfm-autolink-literal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
"license": "MIT",
- "engines": {
- "node": ">=4"
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-find-and-replace": "^3.0.0",
+ "micromark-util-character": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/minimatch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
- "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "dev": true,
- "license": "ISC",
+ "node_modules/mdast-util-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
+ "license": "MIT",
"dependencies": {
- "brace-expansion": "^1.1.7"
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0"
},
- "engines": {
- "node": "*"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
- "node_modules/nanoid": {
- "version": "3.3.18",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
- "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
+ "node_modules/mdast-util-gfm-strikethrough": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
"license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
},
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/natural-compare": {
- "version": "1.4.0",
+ "node_modules/mdast-util-gfm-table": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "markdown-table": "^3.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-task-list-item": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-extension-gfm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
+ "micromark-extension-gfm-footnote": "^2.0.0",
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
+ "micromark-extension-gfm-table": "^2.0.0",
+ "micromark-extension-gfm-tagfilter": "^2.0.0",
+ "micromark-extension-gfm-task-list-item": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-autolink-literal": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-strikethrough": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-table": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
+ "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-tagfilter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-task-list-item": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true,
@@ -4107,6 +5264,31 @@
"node": ">=6"
}
},
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
"node_modules/parse-json": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
@@ -4284,6 +5466,16 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/property-information": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
+ "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
@@ -4342,6 +5534,33 @@
"license": "MIT",
"peer": true
},
+ "node_modules/react-markdown": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
+ "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
+ },
"node_modules/react-router": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz",
@@ -4401,6 +5620,86 @@
"license": "MIT",
"optional": true
},
+ "node_modules/rehype-sanitize": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz",
+ "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-sanitize": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-gfm": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-gfm": "^3.0.0",
+ "micromark-extension-gfm": "^3.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-stringify": "^11.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-stringify": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -4543,6 +5842,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -4567,6 +5876,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@@ -4593,6 +5916,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/style-to-js": {
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.14"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.7"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -4740,6 +6081,26 @@
"node": ">=20"
}
},
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
@@ -4841,6 +6202,93 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz",
@@ -4898,6 +6346,34 @@
"base64-arraybuffer": "^1.0.2"
}
},
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/vite": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz",
@@ -5481,6 +6957,16 @@
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
}
}
}
diff --git a/frontend/package.json b/frontend/package.json
index a121ff8c..8c3ecd1e 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -23,7 +23,10 @@
"jspdf": "^4.2.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
+ "react-markdown": "10.1.0",
"react-router-dom": "^7.15.0",
+ "rehype-sanitize": "6.0.0",
+ "remark-gfm": "4.0.1",
"tailwindcss": "^4.3.0"
},
"devDependencies": {
diff --git a/frontend/src/features/analysis/model/useDocumentMarkdown.ts b/frontend/src/features/analysis/model/useDocumentMarkdown.ts
new file mode 100644
index 00000000..5ccbf3a4
--- /dev/null
+++ b/frontend/src/features/analysis/model/useDocumentMarkdown.ts
@@ -0,0 +1,22 @@
+import { useQuery } from '@tanstack/react-query'
+import { apiClient } from '@/shared/api'
+
+// 분석 원문(마크다운) 로드 — 계약상 유일한 마크다운 문서(docs/messaging.md §5.4)인데
+// 지금까지 프론트 어디에도 렌더되지 않던 gap 을 메운다.
+// presigned URL 직접 fetch 가 아니라 Core 프록시(`GET /api/documents/{id}/content`) 경유 —
+// presigned 는 내부(MinIO) 호스트라 브라우저가 접근할 수 없다(TTS 오디오 프록시와 동일 이유).
+export function useDocumentMarkdown(documentId: number, enabled: boolean) {
+ return useQuery({
+ queryKey: ['analysis', 'document-markdown', documentId],
+ enabled,
+ staleTime: 5 * 60_000,
+ queryFn: async ({ signal }) => {
+ // 모달을 닫으면 react-query 가 abort — 원문 다운로드를 붙들고 있지 않는다.
+ const res = await apiClient.get(`/api/documents/${documentId}/content`, {
+ signal,
+ responseType: 'text',
+ })
+ return res.data
+ },
+ })
+}
diff --git a/frontend/src/features/analysis/ui/DocumentList.tsx b/frontend/src/features/analysis/ui/DocumentList.tsx
index 23787377..8a32d6ac 100644
--- a/frontend/src/features/analysis/ui/DocumentList.tsx
+++ b/frontend/src/features/analysis/ui/DocumentList.tsx
@@ -1,8 +1,9 @@
import { useState } from 'react'
-import { EmptyState, ListSkeleton, Modal, StatusBadge, type StatusTone, QueryError } from '@/shared/ui'
+import { EmptyState, ListSkeleton, Markdown, Modal, StatusBadge, type StatusTone, QueryError } from '@/shared/ui'
import { DOCUMENT_SOURCE_LABEL as SOURCE_LABEL } from '@/domain/rag'
import type { DocumentFilter } from '../api/analysis'
import { useDocuments } from '../model/useDocuments'
+import { useDocumentMarkdown } from '../model/useDocumentMarkdown'
import type {
AnalysisSourceType,
AnalysisStatus,
@@ -170,6 +171,9 @@ function DocumentCard({
}
function DocumentDetail({ doc }: { doc: AnalyzedDocument }) {
+ // 분석 원문(마크다운)은 열람 요청 시에만 로드 — 모달 오픈만으로 presigned 발급을 낭비하지 않는다.
+ const [showRaw, setShowRaw] = useState(false)
+ const markdown = useDocumentMarkdown(doc.id, showRaw)
return (
{doc.summary ? (
@@ -200,6 +204,32 @@ function DocumentDetail({ doc }: { doc: AnalyzedDocument }) {
) : null}
+
+ {doc.analysisStatus === 'ANALYZED' && doc.documentPath ? (
+
+
+ {showRaw &&
+ (markdown.isPending ? (
+ 원문을 불러오는 중…
+ ) : markdown.isError ? (
+ markdown.refetch()}
+ />
+ ) : (
+
+ {markdown.data ?? ''}
+
+ ))}
+
+ ) : null}
)
}
diff --git a/frontend/src/features/interview/ui/BookmarkDrill.tsx b/frontend/src/features/interview/ui/BookmarkDrill.tsx
index 4563cd85..9e277459 100644
--- a/frontend/src/features/interview/ui/BookmarkDrill.tsx
+++ b/frontend/src/features/interview/ui/BookmarkDrill.tsx
@@ -4,6 +4,7 @@ import { TextArea } from '@/shared/ui/TextArea'
import { Eyebrow, StatusBadge } from '@/shared/ui'
import { useQuestionRunner } from '@/shared/hooks'
import { categoryLabel } from '../lib/categoryLabel'
+import { ReviewBlock } from './ReviewBlock'
import type { BookmarkedQuestion } from '../api/bookmarkApi'
const STORAGE_KEY = 'stackup:bookmark-drill-answers'
@@ -95,7 +96,10 @@ export function BookmarkDrill({
{current.myAnswer && (
)}
- {current.modelAnswer && }
+ {/* 모범 답안만 GFM 마크다운 계약 — 내 답변·코칭은 plain 유지. */}
+ {current.modelAnswer && (
+
+ )}
{current.coachingComment && (
)}
@@ -124,22 +128,19 @@ function Panel({
title,
body,
tone = 'strong',
+ markdown = false,
}: {
title: string
body: string
tone?: 'strong' | 'muted'
+ markdown?: boolean
}) {
return (
-
+ {title}}
+ body={body}
+ tone={tone}
+ markdown={markdown}
+ />
)
}
diff --git a/frontend/src/features/interview/ui/BookmarkList.tsx b/frontend/src/features/interview/ui/BookmarkList.tsx
index e4b12ebc..f151b165 100644
--- a/frontend/src/features/interview/ui/BookmarkList.tsx
+++ b/frontend/src/features/interview/ui/BookmarkList.tsx
@@ -6,6 +6,7 @@ import { categoryLabel } from '../lib/categoryLabel'
import { useBookmarks, useSetQuestionBookmark } from '../model/useBookmarks'
import type { BookmarkedQuestion } from '../api/bookmarkApi'
import { BookmarkDrill } from './BookmarkDrill'
+import { ReviewBlock } from './ReviewBlock'
export function BookmarkList() {
const { data = [], isPending, isError, refetch } = useBookmarks()
@@ -112,7 +113,10 @@ function BookmarkCard({ item }: { item: BookmarkedQuestion }) {
{item.myAnswer && (
)}
- {item.modelAnswer && }
+ {/* 모범 답안만 GFM 마크다운 계약 — 내 답변(사용자 입력)·코칭(plain 계약)은 렌더 금지. */}
+ {item.modelAnswer && (
+
+ )}
{item.coachingComment && (
)}
@@ -133,23 +137,20 @@ function Block({
title,
body,
tone = 'strong',
+ markdown = false,
}: {
title: string
body: string
tone?: 'strong' | 'muted'
+ markdown?: boolean
}) {
return (
-
+ {title}
}
+ body={body}
+ tone={tone}
+ markdown={markdown}
+ />
)
}
diff --git a/frontend/src/features/interview/ui/ReviewBlock.tsx b/frontend/src/features/interview/ui/ReviewBlock.tsx
new file mode 100644
index 00000000..8183dfdd
--- /dev/null
+++ b/frontend/src/features/interview/ui/ReviewBlock.tsx
@@ -0,0 +1,38 @@
+import type { ReactNode } from 'react'
+import { Markdown } from '@/shared/ui'
+
+// 오답노트 목록/드릴이 공유하는 복습 패널 — 제목 스타일만 호출부가 넘긴다.
+// 모범 답안(GFM 마크다운 계약)만 markdown 으로 렌더하고, 사용자 입력(내 답변)·plain 계약
+// 필드(코칭)는 절대 마크다운으로 렌더하지 않는다 (docs/frontend-types.md §6.5).
+export function ReviewBlock({
+ label,
+ body,
+ tone = 'strong',
+ markdown = false,
+}: {
+ label: ReactNode
+ body: string
+ tone?: 'strong' | 'muted'
+ markdown?: boolean
+}) {
+ const toneClass = tone === 'strong' ? 'text-fg-strong' : 'text-fg-muted'
+ return (
+
+ {label}
+ {markdown ? (
+
+ {body}
+
+ ) : (
+
+ {body}
+
+ )}
+
+ )
+}
diff --git a/frontend/src/features/interview/ui/live/AnswerCoachingAccordion.tsx b/frontend/src/features/interview/ui/live/AnswerCoachingAccordion.tsx
index c23cfc7f..c593c26d 100644
--- a/frontend/src/features/interview/ui/live/AnswerCoachingAccordion.tsx
+++ b/frontend/src/features/interview/ui/live/AnswerCoachingAccordion.tsx
@@ -1,5 +1,6 @@
import { useState } from 'react'
import type { Message } from '@/domain/session'
+import { Markdown } from '@/shared/ui'
// 답변 구조 채점값(FULL_STAR/PARTIAL_STAR/NONE) → 한국어 라벨.
const STRUCTURE_LABEL: Record = {
@@ -129,17 +130,15 @@ export function AnswerCoachingAccordion({ message }: { message: Message }) {
{message.modelAnswer && (
모범 답안
-
- {message.modelAnswer}
-
+ {/* 모범 답안·리라이트는 GFM 마크다운 계약 필드 — 코드·리스트가 실제로 온다
+ (docs/messaging.md §5.11). 나머지 코칭 텍스트는 plain text 계약. */}
+
{message.modelAnswer}
)}
{message.answerRewrite && (
내 답변, 이렇게 고치면
-
- {message.answerRewrite}
-
+
{message.answerRewrite}
)}
diff --git a/frontend/src/shared/api/generated.ts b/frontend/src/shared/api/generated.ts
index c9f000d1..7153e2e0 100644
--- a/frontend/src/shared/api/generated.ts
+++ b/frontend/src/shared/api/generated.ts
@@ -881,6 +881,26 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/api/documents/{documentId}/content": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * 분석 원문(마크다운) 프록시
+ * @description presigned URL 은 내부(MinIO) 호스트라 브라우저가 직접 접근할 수 없어 Core 가 원문 바이트를 중계한다 (TTS 오디오 프록시와 동일 패턴).
+ */
+ get: operations["getAnalyzedDocumentContent"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/auth/google/callback": {
parameters: {
query?: never;
@@ -3903,6 +3923,55 @@ export interface operations {
};
};
};
+ getAnalyzedDocumentContent: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ documentId: number;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description 분석 원문 (text/markdown) */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "*/*": string;
+ };
+ };
+ /** @description 인증 실패 */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "*/*": string;
+ };
+ };
+ /** @description 분석 문서 없음 */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "*/*": string;
+ };
+ };
+ /** @description 아직 분석 산출물이 없음 */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "*/*": string;
+ };
+ };
+ };
+ };
completeGoogleLogin: {
parameters: {
query?: {
diff --git a/frontend/src/shared/ui/Markdown/Markdown.test.tsx b/frontend/src/shared/ui/Markdown/Markdown.test.tsx
new file mode 100644
index 00000000..8eab72ef
--- /dev/null
+++ b/frontend/src/shared/ui/Markdown/Markdown.test.tsx
@@ -0,0 +1,49 @@
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+import { Markdown } from './Markdown'
+
+describe('Markdown', () => {
+ it('lazy 로딩 사이에는 plain text fallback 을 보여준다 — 내용 공백이 없다', () => {
+ // 반드시 파일의 첫 테스트여야 한다 — lazy 모듈이 한 번 로드되면 이후엔 동기 렌더된다.
+ render({'**핵심** 문장'})
+ expect(screen.getByText('**핵심** 문장')).toBeInTheDocument()
+ })
+
+ it('GFM 마크다운을 렌더한다 — 굵게·리스트·코드가 원문 기호로 노출되지 않는다', async () => {
+ render(
+ {'**핵심**을 지키세요.\n\n- 첫째\n- 둘째\n\n`useMemo` 사용'},
+ )
+
+ // lazy 로더가 풀린 뒤 실제 엘리먼트로 렌더된다.
+ const strong = await screen.findByText('핵심')
+ expect(strong.tagName).toBe('STRONG')
+ expect(screen.getByRole('list')).toBeInTheDocument()
+ expect(screen.getAllByRole('listitem')).toHaveLength(2)
+ const code = screen.getByText('useMemo')
+ expect(code.tagName).toBe('CODE')
+ // 원문 기호가 텍스트로 새지 않는다.
+ expect(screen.queryByText(/\*\*/)).toBeNull()
+ })
+
+ it('raw HTML(script 포함)은 실행 가능한 요소로 렌더되지 않는다', async () => {
+ const { container } = render(
+ {'안전 확인
끝'},
+ )
+
+ await screen.findByText(/안전 확인/)
+ expect(container.querySelector('script')).toBeNull()
+ expect(container.querySelector('img')).toBeNull()
+ expect((window as unknown as { __pwned?: boolean }).__pwned).toBeUndefined()
+ })
+
+ it('코드 블록은 pre 로 감싸 가로 스크롤 컨테이너에 렌더된다', async () => {
+ const { container } = render(
+ {'```java\n@Transactional\npublic void end() {}\n```'},
+ )
+
+ await screen.findByText(/Transactional/)
+ const pre = container.querySelector('pre')
+ expect(pre).not.toBeNull()
+ expect(pre?.className).toContain('overflow-x-auto')
+ })
+})
diff --git a/frontend/src/shared/ui/Markdown/Markdown.tsx b/frontend/src/shared/ui/Markdown/Markdown.tsx
new file mode 100644
index 00000000..29f79bc9
--- /dev/null
+++ b/frontend/src/shared/ui/Markdown/Markdown.tsx
@@ -0,0 +1,20 @@
+import { Suspense, lazy } from 'react'
+import type { MarkdownProps } from './Markdown.types'
+
+// 렌더러(react-markdown 계열)는 무거우므로 사용 시점에 lazy 로드 — 메인 청크에 얹지 않는다
+// (downloadPdf 의 jspdf 선례). 로딩 사이에는 종전과 동일한 plain text 로 보여 깜빡임이 없다.
+const MarkdownContent = lazy(() => import('./MarkdownContent'))
+
+export function Markdown({ children, className }: MarkdownProps) {
+ return (
+
+ {children}
+
+ }
+ >
+ {children}
+
+ )
+}
diff --git a/frontend/src/shared/ui/Markdown/Markdown.types.ts b/frontend/src/shared/ui/Markdown/Markdown.types.ts
new file mode 100644
index 00000000..1065d7c7
--- /dev/null
+++ b/frontend/src/shared/ui/Markdown/Markdown.types.ts
@@ -0,0 +1,5 @@
+export type MarkdownProps = {
+ /** GFM 마크다운 원문. 마크다운 계약 필드(docs/messaging.md §5.4·§5.11)에만 사용한다. */
+ children: string
+ className?: string
+}
diff --git a/frontend/src/shared/ui/Markdown/MarkdownContent.tsx b/frontend/src/shared/ui/Markdown/MarkdownContent.tsx
new file mode 100644
index 00000000..6ceec17c
--- /dev/null
+++ b/frontend/src/shared/ui/Markdown/MarkdownContent.tsx
@@ -0,0 +1,94 @@
+import type { ComponentProps } from 'react'
+import ReactMarkdown from 'react-markdown'
+import rehypeSanitize from 'rehype-sanitize'
+import remarkGfm from 'remark-gfm'
+import type { MarkdownProps } from './Markdown.types'
+
+// 렌더러 본체 — Markdown.tsx 가 lazy import 한다(메인 청크 비대화 방지, downloadPdf 의 jspdf 선례).
+// sanitize: react-markdown 은 raw HTML 을 렌더하지 않지만(escape), rehype-sanitize 를 겹쳐
+// 플러그인 추가 등 미래 변화에도 안전 기본값을 유지한다 (docs/security.md §4.4).
+
+// react-markdown 은 components 에 hast `node` 를 넘긴다 — DOM 속성이 아니므로 걸러낸다.
+function omitNode(props: T): Omit {
+ const rest = { ...props }
+ delete (rest as { node?: unknown }).node
+ return rest
+}
+
+// global.css 가 p/heading margin 을 0 으로 리셋하므로 수직 리듬은 여기서 직접 정의한다.
+// 헤딩은 디스플레이 스케일(38px+)이 아니라 본문 카드 관례(text-[18px]/text-body bold 우회)를 따른다.
+const components: ComponentProps['components'] = {
+ h1: (p) => (
+
+ ),
+ h2: (p) => (
+
+ ),
+ h3: (p) => ,
+ h4: (p) => ,
+ h5: (p) => ,
+ h6: (p) => ,
+ p: (p) => ,
+ ul: (p) => ,
+ ol: (p) =>
,
+ a: (p) => (
+
+ ),
+ strong: (p) => ,
+ blockquote: (p) => (
+
+ ),
+ hr: (p) =>
,
+ code: (p) => (
+
+ ),
+ pre: (p) => (
+
+ ),
+ table: (p) => (
+
+ ),
+ th: (p) => {
+ // text-left 를 항상 박으면 author CSS 가 GFM 의 align 속성(`:---:`)을 이겨버린다 —
+ // 정렬 지정이 없는 컬럼에만 좌측 정렬 기본값을 준다.
+ const { align, ...rest } = omitNode(p) as ComponentProps<'th'>
+ return (
+ |
+ )
+ },
+ td: (p) => | ,
+}
+
+export default function MarkdownContent({ children, className }: MarkdownProps) {
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/frontend/src/shared/ui/Markdown/index.ts b/frontend/src/shared/ui/Markdown/index.ts
new file mode 100644
index 00000000..39f5c63b
--- /dev/null
+++ b/frontend/src/shared/ui/Markdown/index.ts
@@ -0,0 +1,2 @@
+export { Markdown } from './Markdown'
+export type { MarkdownProps } from './Markdown.types'
diff --git a/frontend/src/shared/ui/index.ts b/frontend/src/shared/ui/index.ts
index abf602a3..92f496e7 100644
--- a/frontend/src/shared/ui/index.ts
+++ b/frontend/src/shared/ui/index.ts
@@ -23,3 +23,5 @@ export type { RevealProps } from './Reveal'
export { QueryError } from './QueryError'
export type { QueryErrorProps } from './QueryError'
export { ConnectionBanner } from './ConnectionBanner'
+export { Markdown } from './Markdown'
+export type { MarkdownProps } from './Markdown'
From 35db8ab77f0288073ba6742790604e32eb13044d Mon Sep 17 00:00:00 2001
From: Jaeho
Date: Sun, 23 Aug 2026 04:02:40 +0900
Subject: [PATCH 2/4] =?UTF-8?q?feat(backend):=20=EB=B6=84=EC=84=9D=20?=
=?UTF-8?q?=EC=9B=90=EB=AC=B8=20=EB=A7=88=ED=81=AC=EB=8B=A4=EC=9A=B4=20?=
=?UTF-8?q?=ED=94=84=EB=A1=9D=EC=8B=9C=20=EC=97=94=EB=93=9C=ED=8F=AC?=
=?UTF-8?q?=EC=9D=B8=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
GET /api/documents/{id}/content — presigned URL 은 내부(MinIO) 호스트라
브라우저가 직접 접근할 수 없어 Core 가 소유권 검증 후 원문 바이트를
중계한다(TTS 오디오 프록시와 동일 패턴). 테스트 3건.
---
backend/openapi.json | 63 +++++++++++++++++++
.../AnalyzedDocumentQueryService.java | 11 ++++
.../AnalyzedDocumentController.java | 29 +++++++++
.../AnalyzedDocumentQueryServiceTest.java | 60 ++++++++++++++++++
4 files changed, 163 insertions(+)
create mode 100644 backend/src/test/java/com/stackup/stackup/document/application/AnalyzedDocumentQueryServiceTest.java
diff --git a/backend/openapi.json b/backend/openapi.json
index 2289ce4f..ce3b3587 100644
--- a/backend/openapi.json
+++ b/backend/openapi.json
@@ -2842,6 +2842,69 @@
}
}
},
+ "/api/documents/{documentId}/content" : {
+ "get" : {
+ "tags" : [ "Documents" ],
+ "summary" : "분석 원문(마크다운) 프록시",
+ "description" : "presigned URL 은 내부(MinIO) 호스트라 브라우저가 직접 접근할 수 없어 Core 가 원문 바이트를 중계한다 (TTS 오디오 프록시와 동일 패턴).",
+ "operationId" : "getAnalyzedDocumentContent",
+ "parameters" : [ {
+ "name" : "documentId",
+ "in" : "path",
+ "required" : true,
+ "schema" : {
+ "type" : "integer",
+ "format" : "int64"
+ }
+ } ],
+ "responses" : {
+ "200" : {
+ "description" : "분석 원문 (text/markdown)",
+ "content" : {
+ "*/*" : {
+ "schema" : {
+ "type" : "string",
+ "format" : "binary"
+ }
+ }
+ }
+ },
+ "401" : {
+ "description" : "인증 실패",
+ "content" : {
+ "*/*" : {
+ "schema" : {
+ "type" : "string",
+ "format" : "binary"
+ }
+ }
+ }
+ },
+ "404" : {
+ "description" : "분석 문서 없음",
+ "content" : {
+ "*/*" : {
+ "schema" : {
+ "type" : "string",
+ "format" : "binary"
+ }
+ }
+ }
+ },
+ "422" : {
+ "description" : "아직 분석 산출물이 없음",
+ "content" : {
+ "*/*" : {
+ "schema" : {
+ "type" : "string",
+ "format" : "binary"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/api/auth/google/callback" : {
"get" : {
"tags" : [ "Auth" ],
diff --git a/backend/src/main/java/com/stackup/stackup/document/application/AnalyzedDocumentQueryService.java b/backend/src/main/java/com/stackup/stackup/document/application/AnalyzedDocumentQueryService.java
index 4ae784c1..e6537b1d 100644
--- a/backend/src/main/java/com/stackup/stackup/document/application/AnalyzedDocumentQueryService.java
+++ b/backend/src/main/java/com/stackup/stackup/document/application/AnalyzedDocumentQueryService.java
@@ -53,6 +53,17 @@ public AnalyzedDocumentResult getForUser(Long userId, Long documentId) {
return AnalyzedDocumentResult.of(doc, parseTechStack(doc.getTechStack()), downloadUrl);
}
+ // 분석 원문(마크다운) 프록시 — presigned URL 은 내부(MinIO) 호스트라 브라우저가 직접 접근할
+ // 수 없다(TTS 오디오 프록시와 동일 이유). 소유권 검증 후 Core 가 바이트를 중계한다.
+ public java.io.InputStream getContentForUser(Long userId, Long documentId) {
+ AnalyzedDocument doc = documentRepository.findActiveByIdAndOwner(documentId, userId)
+ .orElseThrow(() -> new DomainException(ApiErrorCode.DOC_NOT_FOUND));
+ if (doc.getDocumentPath() == null || doc.getDocumentPath().isBlank()) {
+ throw new DomainException(ApiErrorCode.DOC_NOT_ANALYZED);
+ }
+ return storage.get(doc.getDocumentPath());
+ }
+
private List parseTechStack(String json) {
if (json == null || json.isBlank()) {
return List.of();
diff --git a/backend/src/main/java/com/stackup/stackup/document/presentation/AnalyzedDocumentController.java b/backend/src/main/java/com/stackup/stackup/document/presentation/AnalyzedDocumentController.java
index b99191b0..9cb89886 100644
--- a/backend/src/main/java/com/stackup/stackup/document/presentation/AnalyzedDocumentController.java
+++ b/backend/src/main/java/com/stackup/stackup/document/presentation/AnalyzedDocumentController.java
@@ -7,8 +7,14 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
+import java.time.Duration;
import java.util.List;
import lombok.RequiredArgsConstructor;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.core.io.Resource;
+import org.springframework.http.CacheControl;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -61,4 +67,27 @@ public AnalyzedDocumentResponse get(
) {
return AnalyzedDocumentResponse.from(queryService.getForUser(principal.userId(), documentId));
}
+
+ @Operation(
+ operationId = "getAnalyzedDocumentContent",
+ summary = "분석 원문(마크다운) 프록시",
+ description = "presigned URL 은 내부(MinIO) 호스트라 브라우저가 직접 접근할 수 없어 "
+ + "Core 가 원문 바이트를 중계한다 (TTS 오디오 프록시와 동일 패턴)."
+ )
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "분석 원문 (text/markdown)"),
+ @ApiResponse(responseCode = "401", description = "인증 실패"),
+ @ApiResponse(responseCode = "404", description = "분석 문서 없음"),
+ @ApiResponse(responseCode = "422", description = "아직 분석 산출물이 없음")
+ })
+ @GetMapping("/{documentId}/content")
+ public ResponseEntity content(
+ @AuthenticationPrincipal UserPrincipal principal,
+ @PathVariable Long documentId
+ ) {
+ return ResponseEntity.ok()
+ .contentType(MediaType.parseMediaType("text/markdown; charset=utf-8"))
+ .cacheControl(CacheControl.maxAge(Duration.ofMinutes(10)).cachePrivate())
+ .body(new InputStreamResource(queryService.getContentForUser(principal.userId(), documentId)));
+ }
}
diff --git a/backend/src/test/java/com/stackup/stackup/document/application/AnalyzedDocumentQueryServiceTest.java b/backend/src/test/java/com/stackup/stackup/document/application/AnalyzedDocumentQueryServiceTest.java
new file mode 100644
index 00000000..790edc2d
--- /dev/null
+++ b/backend/src/test/java/com/stackup/stackup/document/application/AnalyzedDocumentQueryServiceTest.java
@@ -0,0 +1,60 @@
+package com.stackup.stackup.document.application;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.stackup.stackup.common.exception.ApiErrorCode;
+import com.stackup.stackup.common.exception.DomainException;
+import com.stackup.stackup.common.storage.ObjectStorageClient;
+import com.stackup.stackup.document.domain.AnalyzedDocument;
+import com.stackup.stackup.document.domain.AnalyzedDocumentRepository;
+import java.io.ByteArrayInputStream;
+import java.util.Optional;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class AnalyzedDocumentQueryServiceTest {
+
+ @Mock AnalyzedDocumentRepository documentRepository;
+ @Mock ObjectStorageClient storage;
+ @InjectMocks AnalyzedDocumentQueryService service;
+
+ // 분석 원문 프록시(A5) — presigned URL 은 내부 호스트라 브라우저 직접 접근 불가,
+ // Core 가 소유권 검증 후 바이트를 중계한다.
+ @Test
+ void getContentForUser_streamsMarkdownFromStorage() {
+ AnalyzedDocument doc = mock(AnalyzedDocument.class);
+ when(doc.getDocumentPath()).thenReturn("analyzed/resume/42/summary.md");
+ when(documentRepository.findActiveByIdAndOwner(42L, 1L)).thenReturn(Optional.of(doc));
+ when(storage.get("analyzed/resume/42/summary.md"))
+ .thenReturn(new ByteArrayInputStream("## 개요".getBytes()));
+
+ assertThat(service.getContentForUser(1L, 42L)).isNotNull();
+ }
+
+ @Test
+ void getContentForUser_throwsNotFoundForOthersDocument() {
+ when(documentRepository.findActiveByIdAndOwner(42L, 1L)).thenReturn(Optional.empty());
+
+ assertThatThrownBy(() -> service.getContentForUser(1L, 42L))
+ .isInstanceOfSatisfying(DomainException.class,
+ e -> assertThat(e.getErrorCode()).isEqualTo(ApiErrorCode.DOC_NOT_FOUND));
+ }
+
+ @Test
+ void getContentForUser_throwsWhenNoDocumentPathYet() {
+ AnalyzedDocument doc = mock(AnalyzedDocument.class);
+ when(doc.getDocumentPath()).thenReturn(null);
+ when(documentRepository.findActiveByIdAndOwner(42L, 1L)).thenReturn(Optional.of(doc));
+
+ assertThatThrownBy(() -> service.getContentForUser(1L, 42L))
+ .isInstanceOfSatisfying(DomainException.class,
+ e -> assertThat(e.getErrorCode()).isEqualTo(ApiErrorCode.DOC_NOT_ANALYZED));
+ }
+}
From 4e72a8da634dcc9c15077bd17f737b0b0e79a76d Mon Sep 17 00:00:00 2001
From: Jaeho
Date: Sun, 23 Aug 2026 04:02:40 +0900
Subject: [PATCH 3/4] =?UTF-8?q?feat(ai):=20AI=20=ED=85=8D=EC=8A=A4?=
=?UTF-8?q?=ED=8A=B8=20=EC=B6=9C=EB=A0=A5=20=ED=8F=AC=EB=A7=B7=20=EA=B3=84?=
=?UTF-8?q?=EC=95=BD=EC=9D=84=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8?=
=?UTF-8?q?=EC=97=90=20=EB=AA=85=EC=8B=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
plain text 계약 필드(피드백 요약·highlights·studyPlan·패널 detail·질문)는
마크다운 서식 기호 금지 — 하이라이트 부분 문자열 매칭·스트리밍 표시와의
충돌을 원천 차단. answer_coaching 의 model_answer/answer_rewrite 만 GFM
마크다운 허용(프론트가 sanitize 렌더).
---
ai/src/ai_server/chain/prompts/answer_coaching.py | 3 +++
ai/src/ai_server/chain/prompts/feedback_generation.py | 1 +
ai/src/ai_server/chain/prompts/feedback_panel.py | 1 +
ai/src/ai_server/chain/prompts/feedback_synthesis.py | 2 ++
ai/src/ai_server/chain/prompts/question_generation.py | 1 +
5 files changed, 8 insertions(+)
diff --git a/ai/src/ai_server/chain/prompts/answer_coaching.py b/ai/src/ai_server/chain/prompts/answer_coaching.py
index 08f61775..497b96f5 100644
--- a/ai/src/ai_server/chain/prompts/answer_coaching.py
+++ b/ai/src/ai_server/chain/prompts/answer_coaching.py
@@ -13,6 +13,9 @@
"없는 경험을 지어내지 말고, 있는 내용을 구조(두괄식·근거·결과)와 구체성으로 보강. 답변이 비었거나 "
"'모르겠다'면 '이렇게 접근했다면' 식으로 짧게.\n"
"- **coaching_comment (한 줄 코칭)**: 이 답변에서 가장 중요한 보완점 하나를 한 문장으로.\n"
+ "포맷: model_answer 와 answer_rewrite 는 필요하면 **GFM 마크다운**(굵게·리스트·코드 블록)을 "
+ "사용해도 됩니다 — 화면이 마크다운으로 렌더합니다. coaching_comment 는 서식 기호 없는 "
+ "일반 텍스트 한 문장으로.\n"
"- **질문이 경험·행동·자소서 기반**('그때 무엇을 어떻게 했는가', 지원동기·성장·갈등 등)이면 "
"model_answer 와 answer_rewrite 를 **STAR(상황·과제·행동·결과) 골격**으로 구성하고, 본인의 구체적 "
"행동·정량적 결과·배운 점이 드러나게 코칭하세요. (기술 개념 질문이면 STAR 대신 정확성·깊이 우선.)\n"
diff --git a/ai/src/ai_server/chain/prompts/feedback_generation.py b/ai/src/ai_server/chain/prompts/feedback_generation.py
index e23301cd..89cea40e 100644
--- a/ai/src/ai_server/chain/prompts/feedback_generation.py
+++ b/ai/src/ai_server/chain/prompts/feedback_generation.py
@@ -10,6 +10,7 @@
" - logic_score: 논리·인과관계 명확성\n"
" - communication_score: 답변의 명료성·구조화\n"
"- 요약:\n"
+ " - 모든 텍스트 필드는 마크다운 서식 기호 없이 일반 텍스트로 작성 (화면이 plain text 렌더).\n"
" - strengths_summary: 가장 잘한 점 3가지 이내 (각 1~2문장).\n"
" - weaknesses_summary: 가장 부족한 점 3가지 이내 (각 1~2문장).\n"
" - improvement_keywords: 다음 면접에서 채울 키워드 5~10개 (짧은 명사구).\n"
diff --git a/ai/src/ai_server/chain/prompts/feedback_panel.py b/ai/src/ai_server/chain/prompts/feedback_panel.py
index 389f1718..490db46e 100644
--- a/ai/src/ai_server/chain/prompts/feedback_panel.py
+++ b/ai/src/ai_server/chain/prompts/feedback_panel.py
@@ -14,6 +14,7 @@
"- 점수를 매기기 전에 강점/약점 근거를 먼저 정리한 뒤 산정한다(즉흥 점수 금지).\n"
"- strength/weakness 는 각각 한 줄(한국어, 구체적으로). keywords 는 이 축에서 보완할 "
"개선 키워드 0~4개(짧은 명사구).\n"
+ "- 모든 텍스트 필드는 마크다운 서식 기호 없이 일반 텍스트로 작성하세요 (화면이 plain text 렌더).\n"
"- detail: 이 축에 대한 2~4문장 상세 평가. **답변의 구체적 부분을 인용/지목**하고 무엇이 "
"왜 좋았는지/아쉬웠는지 근거를 들어 서술한다(추상적 총평 금지).\n"
"- score_rationale: 그 점수를 준 핵심 근거(가점/감점 요인)를 한두 문장으로.\n"
diff --git a/ai/src/ai_server/chain/prompts/feedback_synthesis.py b/ai/src/ai_server/chain/prompts/feedback_synthesis.py
index a336c610..b9f8090e 100644
--- a/ai/src/ai_server/chain/prompts/feedback_synthesis.py
+++ b/ai/src/ai_server/chain/prompts/feedback_synthesis.py
@@ -11,6 +11,8 @@
"- improvement_keywords: 다음 면접에서 보완할 키워드 5~10개(짧은 명사구).\n"
"- study_plan: 구체적 학습 방향/다음 단계 액션 아이템 3~6개. 각 항목은 '무엇을 어떻게'가 "
"드러나는 실행 문장(예: 'Redis 분산 락의 SETNX·TTL 옵션을 직접 구현해보며 원자성 보장 원리 정리').\n"
+ "모든 텍스트 필드는 **마크다운 서식 기호 없이 일반 텍스트**로 작성하세요(굵게 **, 리스트 -, "
+ "코드 ` 금지) — 화면이 plain text 로 렌더하고, highlights 는 원문 부분 문자열 매칭에 쓰입니다.\n"
"- highlights: 지원자가 꼭 기억해야 할 **가장 중요한 핵심 구절 3~6개**. 화면에서 강조 표시할 "
"용도라, 반드시 위 strengths_summary·weaknesses_summary 본문에 **그대로 등장한 짧은 구절"
"(각 2~12어절)을 글자 그대로 발췌**하세요(새로 짓거나 바꿔쓰지 말 것 — 부분 문자열 매칭에 사용). "
diff --git a/ai/src/ai_server/chain/prompts/question_generation.py b/ai/src/ai_server/chain/prompts/question_generation.py
index 2370b159..ded02d16 100644
--- a/ai/src/ai_server/chain/prompts/question_generation.py
+++ b/ai/src/ai_server/chain/prompts/question_generation.py
@@ -16,6 +16,7 @@
"- 카테고리는 다음 중에서 선택: CS_FUNDAMENTAL, PROJECT_DEEP_DIVE, TECH_CHOICE, "
"BEHAVIORAL.\n"
"- 한국어로 작성하되 기술 용어는 영문 원어를 그대로 둡니다.\n"
+ "- 질문·근거 텍스트에는 마크다운 서식 기호를 쓰지 마세요 (화면이 plain text 렌더, 스트리밍 표시).\n"
"- 질문 문장은 **간결하게**: 한두 문장(대략 80자 이내)으로, 장황한 배경 설명이나 "
"중복 수식 없이 핵심만 묻습니다. 면접관이 입으로 자연스럽게 말할 길이여야 합니다.\n"
"- **직무 맞춤(타깃 회사/JD 제공 시)**: 먼저 채용공고에서 **핵심 요구 역량 5~8개를 뽑아** "
From d92972e3ad54bc79e588cc8be9f8f4b52e4e25d1 Mon Sep 17 00:00:00 2001
From: Jaeho
Date: Sun, 23 Aug 2026 04:02:40 +0900
Subject: [PATCH 4/4] =?UTF-8?q?docs:=20AI=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20?=
=?UTF-8?q?=ED=95=84=EB=93=9C=20=ED=8F=AC=EB=A7=B7=20=EA=B3=84=EC=95=BD=20?=
=?UTF-8?q?=EB=AA=85=EB=AC=B8=ED=99=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
messaging.md §5.4/§5.11 포맷 계약, frontend-types.md §6.5 신설(필드-렌더
매핑 표), design-system.md §2.14 프로즈 렌더링 규정 신설(+아이콘 §2.15
재번호·§3 인벤토리 등재), api-conventions.md 문서 원문 프록시 행.
---
docs/api-conventions.md | 1 +
docs/design-system.md | 16 +++++++++++++++-
docs/frontend-types.md | 11 +++++++++++
docs/messaging.md | 9 +++++++++
4 files changed, 36 insertions(+), 1 deletion(-)
diff --git a/docs/api-conventions.md b/docs/api-conventions.md
index 3245da57..3edb827e 100644
--- a/docs/api-conventions.md
+++ b/docs/api-conventions.md
@@ -67,6 +67,7 @@ POST /api/resumes/{id}/reanalyze 재분석
GET /api/documents 분석 문서 목록
GET /api/documents/{id} 분석 문서 상세 (S3 URL 포함)
+GET /api/documents/{id}/content 분석 원문(마크다운) 프록시 — presigned 는 내부 호스트라 Core 중계
```
### 2.4 면접 세션
diff --git a/docs/design-system.md b/docs/design-system.md
index 76c46328..59a33452 100644
--- a/docs/design-system.md
+++ b/docs/design-system.md
@@ -230,7 +230,20 @@ Tailwind v4 기본 `--spacing: 0.25rem` (= 4px) 사용. `p-4` = `16px`.
**Breakpoints** — Tailwind v4 default 사용:
- `sm: 640px`, `md: 768px`, `lg: 1024px`, `xl: 1280px`, `2xl: 1536px`.
-### 2.14 아이콘
+### 2.14 프로즈(마크다운) 렌더링 (A5)
+
+GFM 마크다운 계약 필드([`frontend-types.md §6.5`](./frontend-types.md))는 `shared/ui/Markdown` 으로만
+렌더한다 — react-markdown + remark-gfm + rehype-sanitize ([`security.md §4.4`](./security.md)),
+렌더러 본체는 lazy 청크(메인 번들 미가산).
+
+- 본문: `text-body` + `leading-relaxed` (기존 AI 텍스트 표면 관례 승계), 수직 리듬 `space-y-2`
+ (global.css 가 p/heading margin 을 0 으로 리셋하므로 컴포넌트가 자체 정의)
+- 헤딩: 디스플레이 스케일이 아니라 카드 관례 — h1/h2 → 18px bold, h3 이하 → `text-body` bold
+- 코드: `--font-mono`, 인라인은 `bg-surface-raised`, 블록은 `bg-surface` + `overflow-x-auto`
+- 링크: `text-primary-fg` underline, `target=_blank rel=noreferrer`. 표는 가로 스크롤 컨테이너
+- 컨테이너 폭은 사용처가 `max-w-readable`(65ch) 로 제한
+
+### 2.15 아이콘
- 라이브러리 — **Lucide Icons** (`lucide-react` 도입 예정, 트리쉐이킹 지원).
- 크기 — `16 / 20 / 24 px` (line-height 와 정렬).
@@ -289,6 +302,7 @@ Tailwind v4 기본 `--spacing: 0.25rem` (= 4px) 사용. `p-4` = `16px`.
- `Skeleton` — 로딩 placeholder (4-state §5).
- `EmptyState` — 빈 목록 안내, 아이콘 + 설명 + CTA (4-state §5).
- `Card` — `header / body / footer` slot.
+- `Markdown` — GFM 마크다운 계약 필드 전용 프로즈 렌더러 (스타일 §2.14, sanitize 포함, lazy 청크).
### Feedback
- `Toast` — 4종 (success / info / warning / error), 우상단 stack, 4초 자동 dismiss, `z-toast`.
diff --git a/docs/frontend-types.md b/docs/frontend-types.md
index 8909b664..d427dce0 100644
--- a/docs/frontend-types.md
+++ b/docs/frontend-types.md
@@ -113,6 +113,17 @@ class ErrorBoundary extends Component {
---
+## 6.5 AI 텍스트 필드 포맷 계약 (A5)
+
+서버가 주는 AI 생성 텍스트는 필드별로 포맷이 계약돼 있다 (정본: [`messaging.md §5.4·§5.11`](./messaging.md)):
+
+| 포맷 | 필드 | 렌더 |
+|---|---|---|
+| **GFM 마크다운** | 분석 산출물(`documentPath` 문서), `answerCoaching[].modelAnswer`/`answerRewrite` | `shared/ui/Markdown` (lazy + sanitize) |
+| **일반 텍스트** | 피드백 요약·`highlights[]`·`studyPlan[]`·패널 `detail`·질문 텍스트·`coachingComment` 등 나머지 전부 | plain (`whitespace-pre-wrap`) — `HighlightedText` 부분 문자열 매칭·델타 스트리밍과의 충돌을 막기 위한 계약 |
+
+사용자 입력(내 답변 등)은 어떤 경우에도 마크다운으로 렌더하지 않는다.
+
## 7. 안티패턴
- ❌ `XxxResponse` 를 함수 반환 타입으로 사용 — `XxxResult` 로.
diff --git a/docs/messaging.md b/docs/messaging.md
index 0fa3e954..b4ea8d9f 100644
--- a/docs/messaging.md
+++ b/docs/messaging.md
@@ -205,6 +205,10 @@
}
```
+- **포맷 계약 (A5)**: `documentPath` 가 가리키는 분석 산출물은 **GFM 마크다운**(프롬프트가 `##`
+ 헤딩 구조를 지시 — 프론트는 상세 모달 "분석 원문 보기"에서 `shared/ui/Markdown` 으로 렌더).
+ `summary` 는 마크다운 서식 없는 일반 텍스트(2~4문장).
+
### 5.5 `callback.analysis` (실패)
```json
{
@@ -524,6 +528,11 @@
}
```
+- **텍스트 필드 포맷 계약 (A5)**: `strengthsSummary`·`weaknessesSummary`·`studyPlan[]`·`highlights[]`·
+ `panelBreakdown[].detail`·`answerCoaching[].coachingComment` 는 **마크다운 서식 기호 없는 일반
+ 텍스트**다(프롬프트로 강제 — highlights 는 원문 부분 문자열 매칭에 쓰이므로 특히). 예외:
+ `answerCoaching[].modelAnswer`/`answerRewrite` 는 **GFM 마크다운 허용** — 프론트가
+ `shared/ui/Markdown`(sanitize 포함)으로 렌더한다.
- 피드백 생성의 부분 실패(패널·부가 평가위원)는 AI 서버 내부에서 폴백(빈 결과/생략)으로 흡수되어
성공 콜백으로 나간다 — FAILED 는 그 방어망 밖의 **예상 못 한 예외** 전용이다. `errorCode` 는
questions/followup 과 동일 분류: `TypeError`(LLM 출력 스키마 불일치)면 `GENERATION_SCHEMA_INVALID`