diff --git a/apps/landing/package.json b/apps/landing/package.json
index 74454ba..9dd1d0b 100644
--- a/apps/landing/package.json
+++ b/apps/landing/package.json
@@ -18,6 +18,7 @@
"next": "15.3.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
+ "react-latex-next": "^3.0.0",
"react-syntax-highlighter": "15.6.1"
},
"devDependencies": {
@@ -25,7 +26,7 @@
"@types/node": "^24",
"@types/react": "^19",
"@types/react-dom": "^19",
- "typescript": "^5",
- "@types/react-syntax-highlighter": "^15.5.13"
+ "@types/react-syntax-highlighter": "^15.5.13",
+ "typescript": "^5"
}
}
\ No newline at end of file
diff --git a/apps/landing/src/app/test-case/page.tsx b/apps/landing/src/app/test-case/page.tsx
index a6f1d87..5003d04 100644
--- a/apps/landing/src/app/test-case/page.tsx
+++ b/apps/landing/src/app/test-case/page.tsx
@@ -1,6 +1,9 @@
+import 'katex/dist/katex.min.css'
+
import { Box, Grid, Text, VStack } from '@devup-ui/react'
import { readFile } from 'fs/promises'
import { Metadata } from 'next'
+import Latex from 'react-latex-next'
import TestCaseCircle from '@/components/test-case/TestCaseCircle'
@@ -81,24 +84,34 @@ export default async function TestCasePage() {
gridTemplateColumns="repeat(auto-fill, minmax(16px, 1fr))"
>
{testStatus[key][2].map(
- ([text, expected, actual, isSuccess], idx) => (
-
-
- {text}
-
- 정답 : {expected}
-
- 결과 : {actual}
-
- {isSuccess ? '✅ 테스트 성공' : '❌ 테스트 실패'}
-
-
- ),
+ ([text, expected, actual, isSuccess], idx) => {
+ const textParts = parseTextWithLaTeX(text)
+
+ return (
+
+
+ {textParts.map((part, partIdx) =>
+ part.type === 'latex' ? (
+ ${part.content}$
+ ) : (
+ {part.content}
+ ),
+ )}
+
+ 정답 : {expected}
+
+ 결과 : {actual}
+
+ {isSuccess ? '✅ 테스트 성공' : '❌ 테스트 실패'}
+
+
+ )
+ },
)}
@@ -107,3 +120,50 @@ export default async function TestCasePage() {
)
}
+
+/**
+ * This function parses text with LaTeX expressions and returns an array of parts.
+ * It assumes that LaTeX is wrapped in double dollar delimiters ($$...$$).
+ * Note that single dollar delimiters ($...$) are not rendered.
+ * @param input - The input text to parse.
+ * @returns An array of parts, where each part is either a text or a LaTeX expression.
+ */
+const parseTextWithLaTeX = (input: string) => {
+ const parts: Array<{
+ type: 'text' | 'latex'
+ content: string
+ }> = []
+ const latexRegex = /\$\$([^$]+(?:\$(?!\$)[^$]*)*)\$\$/g
+ let lastIndex = 0
+ let match
+
+ while ((match = latexRegex.exec(input)) !== null) {
+ // if there is text before the LaTeX expression, add it as a text part:
+ if (match.index > lastIndex) {
+ const textContent = input.slice(lastIndex, match.index)
+ if (textContent) {
+ parts.push({ type: 'text', content: textContent })
+ }
+ }
+
+ // add the LaTeX expression from double dollars:
+ const latexContent = match[1]
+ parts.push({ type: 'latex', content: latexContent })
+ lastIndex = match.index + match[0].length
+ }
+
+ // add remaining text after the last LaTeX expression:
+ if (lastIndex < input.length) {
+ const remainingText = input.slice(lastIndex)
+ if (remainingText) {
+ parts.push({ type: 'text', content: remainingText })
+ }
+ }
+
+ // if no LaTeX found, return the original text as a single text part:
+ if (!parts.length) {
+ parts.push({ type: 'text', content: input })
+ }
+
+ return parts
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 401c50a..5cd8aa3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -65,6 +65,9 @@ importers:
react-dom:
specifier: ^19.1.0
version: 19.1.0(react@19.1.0)
+ react-latex-next:
+ specifier: ^3.0.0
+ version: 3.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-syntax-highlighter:
specifier: 15.6.1
version: 15.6.1(react@19.1.0)
@@ -1130,6 +1133,10 @@ packages:
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
+ commander@8.3.0:
+ resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
+ engines: {node: '>= 12'}
+
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -1820,6 +1827,10 @@ packages:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
+ katex@0.16.22:
+ resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==}
+ hasBin: true
+
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -2234,6 +2245,13 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+ react-latex-next@3.0.0:
+ resolution: {integrity: sha512-x70f1b1G7TronVigsRgKHKYYVUNfZk/3bciFyYX1lYLQH2y3/TXku3+5Vap8MDbJhtopePSYBsYWS6jhzIdz+g==}
+ engines: {node: '>=12', npm: '>=5'}
+ peerDependencies:
+ react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
+
react-syntax-highlighter@15.6.1:
resolution: {integrity: sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==}
peerDependencies:
@@ -3794,6 +3812,8 @@ snapshots:
comma-separated-tokens@2.0.3: {}
+ commander@8.3.0: {}
+
concat-map@0.0.1: {}
cross-spawn@7.0.6:
@@ -4686,6 +4706,10 @@ snapshots:
object.assign: 4.1.7
object.values: 1.2.1
+ katex@0.16.22:
+ dependencies:
+ commander: 8.3.0
+
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -5281,6 +5305,12 @@ snapshots:
react-is@16.13.1: {}
+ react-latex-next@3.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+ dependencies:
+ katex: 0.16.22
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+
react-syntax-highlighter@15.6.1(react@19.1.0):
dependencies:
'@babel/runtime': 7.27.6