diff --git a/apps/landing/package.json b/apps/landing/package.json
index e0c1ffe9..71f959ff 100644
--- a/apps/landing/package.json
+++ b/apps/landing/package.json
@@ -17,6 +17,7 @@
"braillify": "workspace:*",
"clsx": "^2.1.1",
"katex": "^0.17.0",
+ "mathlive": "^0.110.0",
"next": "16.2.10",
"react": "^19.2.7",
"react-dom": "^19.2.7",
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_AMS-Regular.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_AMS-Regular.woff2
new file mode 100644
index 00000000..0acaaff0
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_AMS-Regular.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_Caligraphic-Bold.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_Caligraphic-Bold.woff2
new file mode 100644
index 00000000..f390922e
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_Caligraphic-Bold.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_Caligraphic-Regular.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_Caligraphic-Regular.woff2
new file mode 100644
index 00000000..75344a1f
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_Caligraphic-Regular.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_Fraktur-Bold.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_Fraktur-Bold.woff2
new file mode 100644
index 00000000..395f28be
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_Fraktur-Bold.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_Fraktur-Regular.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_Fraktur-Regular.woff2
new file mode 100644
index 00000000..735f6948
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_Fraktur-Regular.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_Main-Bold.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_Main-Bold.woff2
new file mode 100644
index 00000000..ab2ad21d
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_Main-Bold.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_Main-BoldItalic.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_Main-BoldItalic.woff2
new file mode 100644
index 00000000..5931794d
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_Main-BoldItalic.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_Main-Italic.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_Main-Italic.woff2
new file mode 100644
index 00000000..b50920e1
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_Main-Italic.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_Main-Regular.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_Main-Regular.woff2
new file mode 100644
index 00000000..eb24a7ba
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_Main-Regular.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_Math-BoldItalic.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_Math-BoldItalic.woff2
new file mode 100644
index 00000000..29657023
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_Math-BoldItalic.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_Math-Italic.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_Math-Italic.woff2
new file mode 100644
index 00000000..215c143f
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_Math-Italic.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_SansSerif-Bold.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_SansSerif-Bold.woff2
new file mode 100644
index 00000000..cfaa3bda
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_SansSerif-Bold.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_SansSerif-Italic.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_SansSerif-Italic.woff2
new file mode 100644
index 00000000..349c06dc
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_SansSerif-Italic.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_SansSerif-Regular.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_SansSerif-Regular.woff2
new file mode 100644
index 00000000..a90eea85
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_SansSerif-Regular.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_Script-Regular.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_Script-Regular.woff2
new file mode 100644
index 00000000..b3048fc1
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_Script-Regular.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_Size1-Regular.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_Size1-Regular.woff2
new file mode 100644
index 00000000..c5a8462f
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_Size1-Regular.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_Size2-Regular.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_Size2-Regular.woff2
new file mode 100644
index 00000000..e1bccfe2
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_Size2-Regular.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_Size3-Regular.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_Size3-Regular.woff2
new file mode 100644
index 00000000..249a2866
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_Size3-Regular.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_Size4-Regular.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_Size4-Regular.woff2
new file mode 100644
index 00000000..680c1308
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_Size4-Regular.woff2 differ
diff --git a/apps/landing/public/mathlive/fonts/KaTeX_Typewriter-Regular.woff2 b/apps/landing/public/mathlive/fonts/KaTeX_Typewriter-Regular.woff2
new file mode 100644
index 00000000..771f1af7
Binary files /dev/null and b/apps/landing/public/mathlive/fonts/KaTeX_Typewriter-Regular.woff2 differ
diff --git a/apps/landing/src/app/DemoChrome.tsx b/apps/landing/src/app/DemoChrome.tsx
new file mode 100644
index 00000000..66bb2678
--- /dev/null
+++ b/apps/landing/src/app/DemoChrome.tsx
@@ -0,0 +1,50 @@
+import { Box, Flex, Image, Text } from '@devup-ui/react'
+
+/** 데모 섹션 상단의 손가락 아이콘 + 안내 문구. 한글·수학 데모가 공유한다. */
+export function DemoHeading({ children }: { children: string }) {
+ return (
+
+
+
+ {children}
+
+
+ )
+}
+
+/** 입력 박스와 출력 박스 사이의 방향 화살표. 한글·수학 데모가 공유한다. */
+export function DemoArrow() {
+ return (
+
+
+
+
+ )
+}
diff --git a/apps/landing/src/app/DemoTabs.tsx b/apps/landing/src/app/DemoTabs.tsx
new file mode 100644
index 00000000..2d53a04f
--- /dev/null
+++ b/apps/landing/src/app/DemoTabs.tsx
@@ -0,0 +1,80 @@
+'use client'
+import { Box, Flex, Text, VStack } from '@devup-ui/react'
+import { useRef, useState } from 'react'
+
+import { MathTrans } from './MathTrans'
+import { Trans } from './Trans'
+
+const TABS = [
+ { key: 'korean', label: '한글' },
+ { key: 'math', label: '수학' },
+] as const
+
+type DemoMode = (typeof TABS)[number]['key']
+
+const PANEL_ID = 'demo-panel'
+const tabId = (key: DemoMode) => `demo-tab-${key}`
+
+export function DemoTabs() {
+ const [mode, setMode] = useState('korean')
+ const tabRefs = useRef<(HTMLElement | null)[]>([])
+
+ const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
+ if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return
+ e.preventDefault()
+ const dir = e.key === 'ArrowRight' ? 1 : -1
+ const next = (index + dir + TABS.length) % TABS.length
+ setMode(TABS[next].key)
+ tabRefs.current[next]?.focus()
+ }
+
+ return (
+
+
+ {TABS.map(({ key, label }, index) => (
+ {
+ tabRefs.current[index] = el
+ }}
+ aria-controls={PANEL_ID}
+ aria-selected={mode === key}
+ as="button"
+ bg={mode === key ? '$text' : 'transparent'}
+ border="1px solid $text"
+ borderRadius="9999px"
+ color={mode === key ? '$background' : '$text'}
+ cursor="pointer"
+ id={tabId(key)}
+ onClick={() => setMode(key)}
+ onKeyDown={(e) => handleKeyDown(e, index)}
+ px={['20px', null, null, '28px']}
+ py={['8px', null, null, '10px']}
+ role="tab"
+ tabIndex={mode === key ? 0 : -1}
+ type="button"
+ typography="button"
+ >
+ {label}
+
+ ))}
+
+
+ {mode === 'korean' ? : }
+
+
+ )
+}
diff --git a/apps/landing/src/app/MathTrans.tsx b/apps/landing/src/app/MathTrans.tsx
new file mode 100644
index 00000000..df572599
--- /dev/null
+++ b/apps/landing/src/app/MathTrans.tsx
@@ -0,0 +1,56 @@
+'use client'
+import { Flex, VStack } from '@devup-ui/react'
+import { useEffect, useState } from 'react'
+
+import { DemoArrow, DemoHeading } from './DemoChrome'
+import { MathTransInput } from './MathTransInput'
+import { TransInput } from './TransInput'
+
+export function MathTrans() {
+ const [latex, setLatex] = useState('')
+ const [translateToUnicode, setTranslateToUnicode] = useState<
+ (input: string) => string
+ >(() => () => '')
+ useEffect(() => {
+ import('braillify').then((mod) => {
+ setTranslateToUnicode(() => (input: string) => {
+ try {
+ return mod.translateToUnicode(input)
+ } catch (e) {
+ console.error(e)
+ return '점역할 수 없는 수식이 포함되어 있습니다.'
+ }
+ })
+ })
+ }, [])
+
+ const braille = latex.trim() ? translateToUnicode(`$${latex}$`) : ''
+
+ return (
+
+
+ 수식도 점자가 됩니다. 수식 키보드로 입력해 수학 점역을 체험해보세요!
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/landing/src/app/MathTransInput.tsx b/apps/landing/src/app/MathTransInput.tsx
new file mode 100644
index 00000000..f9def1ed
--- /dev/null
+++ b/apps/landing/src/app/MathTransInput.tsx
@@ -0,0 +1,169 @@
+'use client'
+
+import { Flex, Text } from '@devup-ui/react'
+import type { MathfieldElement } from 'mathlive'
+import { useEffect, useRef, useState } from 'react'
+
+declare global {
+ namespace React.JSX {
+ interface IntrinsicElements {
+ 'math-field': React.DetailedHTMLProps<
+ React.HTMLAttributes,
+ MathfieldElement
+ > & { 'math-virtual-keyboard-policy'?: 'auto' | 'manual' }
+ }
+ }
+}
+
+/**
+ * MathLive는 한 글자 인자를 중괄호 없이 직렬화한다 (예: \frac34, \frac{3}4).
+ * braillify의 LaTeX 파서는 \frac{분자}{분모} 형태만 분수로 인식하므로
+ * \frac의 두 인자를 항상 중괄호로 감싼 정규형으로 변환한다.
+ */
+export function normalizeFracBraces(latex: string): string {
+ let out = ''
+ let i = 0
+ while (i < latex.length) {
+ if (latex.startsWith('\\frac', i) && !/[a-zA-Z]/.test(latex[i + 5] ?? '')) {
+ const [num, j] = readArg(latex, i + 5)
+ const [den, k] = readArg(latex, j)
+ out += `\\frac${num}${den}`
+ i = k
+ continue
+ }
+ out += latex[i]
+ i += 1
+ }
+ return out
+}
+
+/** i 위치부터 LaTeX 인자 하나를 읽어 중괄호로 감싼 형태와 다음 인덱스를 돌려준다. */
+function readArg(latex: string, i: number): [string, number] {
+ while (latex[i] === ' ') i += 1
+ if (latex[i] === '{') {
+ let depth = 0
+ let j = i
+ do {
+ if (latex[j] === '{') depth += 1
+ else if (latex[j] === '}') depth -= 1
+ j += 1
+ } while (j < latex.length && depth > 0)
+ // depth === 0 이면 짝이 맞는 '}' 를 j-1 에서 소비한 것이고,
+ // depth > 0 이면 닫는 중괄호 없이 끝난 것이라 내용을 j 까지 살린다.
+ const end = depth === 0 ? j - 1 : j
+ return [`{${normalizeFracBraces(latex.slice(i + 1, end))}}`, j]
+ }
+ if (latex[i] === '\\') {
+ let j = i + 1
+ while (j < latex.length && /[a-zA-Z]/.test(latex[j] ?? '')) j += 1
+ // 제어기호(\, \! 등)는 백슬래시 뒤에 글자가 없으므로 기호 한 글자를 포함시킨다.
+ if (j === i + 1 && j < latex.length) j += 1
+ return [`{${latex.slice(i, j)}}`, j]
+ }
+ return [`{${latex[i] ?? ''}}`, i + 1]
+}
+
+export function MathTransInput({
+ latex,
+ onLatexChange,
+ placeholder,
+}: {
+ latex: string
+ onLatexChange: (latex: string) => void
+ placeholder: string
+}) {
+ const [ready, setReady] = useState(false)
+ const fieldRef = useRef(null)
+
+ useEffect(() => {
+ let cancelled = false
+ import('mathlive').then(({ MathfieldElement }) => {
+ if (cancelled) return
+ MathfieldElement.fontsDirectory = '/mathlive/fonts'
+ MathfieldElement.soundsDirectory = null
+ setReady(true)
+ })
+ return () => {
+ cancelled = true
+ }
+ }, [])
+
+ useEffect(() => {
+ const field = fieldRef.current
+ if (!ready || !field) return
+ const show = () => window.mathVirtualKeyboard.show()
+ const hide = () => window.mathVirtualKeyboard.hide()
+ field.addEventListener('focusin', show)
+ field.addEventListener('focusout', hide)
+ return () => {
+ field.removeEventListener('focusin', show)
+ field.removeEventListener('focusout', hide)
+ window.mathVirtualKeyboard.hide()
+ }
+ }, [ready])
+
+ return (
+ // 바깥 Flex 는 padding 없는 flex 아이템으로, 출력측 TransInput 의 외곽
+ // Flex(flex=1 h=100% w=100%)와 flex-basis 를 동일하게 맞춰 좌우 박스 너비를
+ // 같게 한다. 실제 배경/여백은 안쪽 박스가 담당한다.
+
+ fieldRef.current?.focus()}
+ p={['16px', null, null, '40px']}
+ w="100%"
+ >
+
+ {ready && (
+
+ onLatexChange(
+ normalizeFracBraces(
+ (e.target as MathfieldElement).getValue(
+ 'latex-without-placeholders',
+ ),
+ ),
+ )
+ }
+ style={{
+ background: 'transparent',
+ border: 'none',
+ display: 'block',
+ fontSize: '28px',
+ width: '100%',
+ }}
+ />
+ )}
+ {!latex && (
+
+ {placeholder}
+
+ )}
+
+
+ {latex ? `LaTeX: $${latex}$` : 'LaTeX가 자동으로 생성됩니다'}
+
+
+
+ )
+}
diff --git a/apps/landing/src/app/Trans.tsx b/apps/landing/src/app/Trans.tsx
index 3d6c57d8..95300f5b 100644
--- a/apps/landing/src/app/Trans.tsx
+++ b/apps/landing/src/app/Trans.tsx
@@ -1,7 +1,8 @@
'use client'
-import { Box, Flex, Image, Text, VStack } from '@devup-ui/react'
+import { Flex, VStack } from '@devup-ui/react'
import { useEffect, useState } from 'react'
+import { DemoArrow, DemoHeading } from './DemoChrome'
import { TransInput } from './TransInput'
export function Trans() {
@@ -27,26 +28,7 @@ export function Trans() {
return (
-
-
-
- 직접 입력해 실시간 점자 번역을 체험해보세요!
-
-
+ 직접 입력해 실시간 점자 번역을 체험해보세요!
-
-
-
-
+
-
+
diff --git a/bun.lock b/bun.lock
index 93fe691e..fb78c793 100644
--- a/bun.lock
+++ b/bun.lock
@@ -25,6 +25,7 @@
"braillify": "workspace:*",
"clsx": "^2.1.1",
"katex": "^0.17.0",
+ "mathlive": "^0.110.0",
"next": "16.2.10",
"react": "^19.2.7",
"react-dom": "^19.2.7",
@@ -42,13 +43,15 @@
},
"packages/node": {
"name": "braillify",
- "version": "2.0.1",
+ "version": "2.1.0",
"devDependencies": {
"binaryen": "^130.0.0",
},
},
},
"packages": {
+ "@arnog/colors": ["@arnog/colors@0.5.0", "", {}, "sha512-NB7yqrO7qvInsqJdqz6C6mDU+2oiKmbBbk3czMS0E7KkGxLRy538tEHCsB5TKqmaxxXpi6/U120TnGl8NsDePg=="],
+
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
@@ -83,6 +86,8 @@
"@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
+ "@cortex-js/compute-engine": ["@cortex-js/compute-engine@0.58.0", "", { "dependencies": { "@arnog/colors": "^0.5.0", "complex-esm": "^2.1.1-esm1", "decimal.js": "^10.6.0" } }, "sha512-N0+vXjVZfKwJS7ZIGvq41mojI55m5TrdDXveAXzNjzjZ9iNiNrdEzXL2EmrcP+9GCWCwTonrEeZWAIG78f3INg=="],
+
"@devup-ui/components": ["@devup-ui/components@0.1.47", "", { "dependencies": { "@devup-ui/react": "^1.0.37", "clsx": "^2.1", "react": "^19.2.7" } }, "sha512-B/V2fTbSUIFObF/Zz4gyGhDmuY3vmbej9678494VrmrAM/5JeaN/X+0quWGIpjwPy/rhvAW6nNLEf0XJPZQ7ew=="],
"@devup-ui/eslint-plugin": ["@devup-ui/eslint-plugin@1.0.16", "", { "dependencies": { "@typescript-eslint/utils": "^8.60", "typescript-eslint": "^8.60" } }, "sha512-gXhEVO9c4qGfR6HcCXsnRZHZlepDBZ1BnA0M2pB1/9asXSqWoJmt75xE0beXtt7wgrBHO/Z5gh+iX8Xu3e2ewQ=="],
@@ -417,6 +422,8 @@
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
+ "complex-esm": ["complex-esm@2.1.1-esm1", "", {}, "sha512-IShBEWHILB9s7MnfyevqNGxV0A1cfcSnewL/4uPFiSxkcQL4Mm3FxJ0pXMtCXuWLjYz3lRRyk6OfkeDZcjD6nw=="],
+
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
@@ -437,6 +444,8 @@
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+ "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
+
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
@@ -753,6 +762,8 @@
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+ "mathlive": ["mathlive@0.110.0", "", { "dependencies": { "@cortex-js/compute-engine": "0.58.0" } }, "sha512-UOpJsQ6h1eeN0xZULGTl1MwUB/lZLCDuzKeHvKEh7Zra8U/rtgDNrssj5PZdJtBTIV48AfEhtv6rv47AeMOxJA=="],
+
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "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" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
"mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="],
diff --git a/docs/superpowers/plans/2026-07-22-demo-tabs-unify.md b/docs/superpowers/plans/2026-07-22-demo-tabs-unify.md
new file mode 100644
index 00000000..9a770171
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-22-demo-tabs-unify.md
@@ -0,0 +1,346 @@
+# 랜딩 데모 한글/수학 탭 통합 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 홈페이지의 한글 점역 데모와 수학 점역 데모(두 개의 세로 섹션)를 '한글'/'수학' 탭 토글 하나로 통합하고, 수학 안내문을 왼쪽(입력창 placeholder)으로 옮기며, 입력·출력 박스 크기를 동일하게 맞춘다.
+
+**Architecture:** 새 클라이언트 컴포넌트 `DemoTabs`가 탭 상태(`'korean' | 'math'`)를 갖고 기존 `Trans` 또는 `MathTrans`를 조건부 렌더한다. `Trans`는 수정하지 않는다. `MathTransInput`은 TransInput과 동일한 박스 규격(minH 25dvh, 동일 padding/borderRadius)으로 바뀌고, 빈 상태 placeholder 오버레이와 LaTeX 미리보기 줄을 내장한다. `MathTrans`는 미리보기 렌더를 제거하고 데스크톱 500px 고정 행 높이로 좌우 박스를 대칭화한다.
+
+**Tech Stack:** Next.js 16 (App Router), React 19, devup-ui, MathLive 0.110.x (이미 설치됨), braillify WASM
+
+## Global Constraints
+
+- 브랜치: `demo-math-landing` (이미 체크아웃됨), base `fa0c527`
+- 코어 엔진(`libs/braillify`) 및 WASM 바인딩(`packages/node`) 수정 금지
+- `apps/landing/src/app/Trans.tsx`, `TransInput.tsx`는 수정 금지 (한글 데모는 요청 범위 밖)
+- 검증 게이트: `cd apps/landing && bun run build` → `✓ Compiled successfully` (테스트 인프라 없음)
+- UI 문구 (verbatim, 사용자 지정):
+ - 탭 라벨: `한글`, `수학`
+ - 수학 입력창(왼쪽) placeholder: `수식을 입력하면 이곳에 수학 점자가 표시됩니다.`
+ - 수학 출력창(오른쪽) placeholder(예시 유지): `예: 사분의 삼 → ⠼⠙⠌⠼⠉`
+ - LaTeX 미리보기: 값 있을 때 `` LaTeX: $${latex}$ ``, 없을 때 `LaTeX가 자동으로 생성됩니다`
+- 기본 선택 탭은 `한글`
+- 커밋 메시지는 기존 컨벤션(`feat:`/`fix:` + 한국어) 준수
+
+---
+
+### Task 1: 수학 데모 좌측 안내문 이동 + 입출력 박스 동일 크기
+
+**Files:**
+- Modify: `apps/landing/src/app/MathTransInput.tsx` (컴포넌트 부분 전면 교체 — normalizeFracBraces/readArg와 declare global 블록은 그대로 둔다)
+- Modify: `apps/landing/src/app/MathTrans.tsx`
+
+**Interfaces:**
+- Consumes: 기존 `TransInput`(수정 금지), `normalizeFracBraces` (같은 파일에 이미 존재)
+- Produces: `MathTransInput({ onLatexChange: (latex: string) => void, placeholder: string })` — LaTeX 미리보기 줄을 자체 렌더하므로 `MathTrans`는 미리보기를 렌더하지 않는다. `MathTrans()`는 props 없음 (변경 없음, Task 2가 소비).
+
+- [ ] **Step 1: MathTransInput.tsx의 컴포넌트를 교체**
+
+파일 상단의 `'use client'`, import, `declare global`, `normalizeFracBraces`, `readArg`는 **그대로 두고**, `export function MathTransInput` 전체를 아래로 교체한다. import 줄은 다음으로 갱신한다:
+
+```tsx
+import { Box, Flex, Text } from '@devup-ui/react'
+import type { MathfieldElement } from 'mathlive'
+import { useEffect, useRef, useState } from 'react'
+```
+
+```tsx
+export function MathTransInput({
+ onLatexChange,
+ placeholder,
+}: {
+ onLatexChange: (latex: string) => void
+ placeholder: string
+}) {
+ const [ready, setReady] = useState(false)
+ const [latex, setLatex] = useState('')
+ const fieldRef = useRef(null)
+
+ useEffect(() => {
+ let cancelled = false
+ import('mathlive').then(({ MathfieldElement }) => {
+ if (cancelled) return
+ MathfieldElement.fontsDirectory = '/mathlive/fonts'
+ MathfieldElement.soundsDirectory = null
+ setReady(true)
+ })
+ return () => {
+ cancelled = true
+ }
+ }, [])
+
+ useEffect(() => {
+ const field = fieldRef.current
+ if (!ready || !field) return
+ const show = () => window.mathVirtualKeyboard.show()
+ const hide = () => window.mathVirtualKeyboard.hide()
+ field.addEventListener('focusin', show)
+ field.addEventListener('focusout', hide)
+ return () => {
+ field.removeEventListener('focusin', show)
+ field.removeEventListener('focusout', hide)
+ window.mathVirtualKeyboard.hide()
+ }
+ }, [ready])
+
+ return (
+
+
+ {ready && (
+ {
+ const value = normalizeFracBraces(
+ (e.target as MathfieldElement).getValue(
+ 'latex-without-placeholders',
+ ),
+ )
+ setLatex(value)
+ onLatexChange(value)
+ }}
+ style={{
+ background: 'transparent',
+ border: 'none',
+ display: 'block',
+ fontSize: '28px',
+ width: '100%',
+ }}
+ />
+ )}
+ {!latex && (
+
+ {placeholder}
+
+ )}
+
+
+ {latex ? `LaTeX: $${latex}$` : 'LaTeX가 자동으로 생성됩니다'}
+
+
+ )
+}
+```
+
+설계 근거:
+- 박스 규격(`bg`, `borderRadius`, `minH="25dvh"`, `p`)은 `TransInput.tsx`의 Box와 동일 — "입력·출력 박스 크기 동일" 요구
+- placeholder는 math-field가 클릭을 받아야 하므로 `pointerEvents="none"` 오버레이. math-field 로드 후에는 필드 아래(top 48px)에 표시해 커서와 겹치지 않게 한다
+- LaTeX 미리보기가 박스 안으로 들어와 좌우 박스 외곽 크기가 정확히 같아진다
+
+- [ ] **Step 2: MathTrans.tsx 수정**
+
+`apps/landing/src/app/MathTrans.tsx`에서 (a) 입력측 `VStack`+미리보기 `Text` 래퍼를 제거하고 `MathTransInput`을 직접 배치하며 placeholder를 전달, (b) 출력 placeholder를 예시만 남기고, (c) 행 레이아웃을 Trans와 동일한 데스크톱 500px 높이로 바꾼다. return문의 두 번째 ``(행 레이아웃)를 아래로 교체:
+
+```tsx
+
+
+
+
+
+
+
+
+```
+
+주의: `MathTransInput`은 이제 자체적으로 `flex-direction: column` Flex를 반환하지만 부모 행에서 `TransInput`과 동일하게 flex 아이템으로 늘어나야 한다. `TransInput`의 최외곽은 ``이므로, `MathTransInput` 최외곽 Flex에도 `flex="1"`을 추가해야 좌우 너비가 같아진다 — Step 1 코드의 최외곽 ``에 `flex="1"` prop을 포함할 것 (누락 시 이 Step에서 추가).
+
+- [ ] **Step 3: 빌드 확인**
+
+Run: `cd /Users/Yeon/braillify/apps/landing && bun run build`
+Expected: `✓ Compiled successfully`
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd /Users/Yeon/braillify
+git add apps/landing/src/app/MathTransInput.tsx apps/landing/src/app/MathTrans.tsx
+git commit -m "feat: 수학 데모 안내문을 입력창으로 이동하고 입출력 박스 크기 통일"
+```
+
+---
+
+### Task 2: DemoTabs 탭 토글 + 홈페이지 통합
+
+**Files:**
+- Create: `apps/landing/src/app/DemoTabs.tsx`
+- Modify: `apps/landing/src/app/page.tsx` (Trans/MathTrans 두 섹션 → DemoTabs 하나)
+
+**Interfaces:**
+- Consumes: `Trans()` (props 없음, `./Trans`), `MathTrans()` (props 없음, `./MathTrans`)
+- Produces: `DemoTabs()` — props 없음, page.tsx가 렌더
+
+- [ ] **Step 1: DemoTabs.tsx 생성**
+
+```tsx
+'use client'
+import { Flex, Text, VStack } from '@devup-ui/react'
+import { useState } from 'react'
+
+import { MathTrans } from './MathTrans'
+import { Trans } from './Trans'
+
+const TABS = [
+ { key: 'korean', label: '한글' },
+ { key: 'math', label: '수학' },
+] as const
+
+type DemoMode = (typeof TABS)[number]['key']
+
+export function DemoTabs() {
+ const [mode, setMode] = useState('korean')
+ return (
+
+
+ {TABS.map(({ key, label }) => (
+ setMode(key)}
+ px={['20px', null, null, '28px']}
+ py={['8px', null, null, '10px']}
+ role="tab"
+ typography="button"
+ >
+ {label}
+
+ ))}
+
+ {mode === 'korean' ? : }
+
+ )
+}
+```
+
+설계 근거:
+- 탭 전환 시 `MathTransInput`이 언마운트되며 effect cleanup의 `window.mathVirtualKeyboard.hide()`가 열린 수식 키보드를 닫는다 (이미 구현됨)
+- `Trans`/`MathTrans`는 각자의 안내 헤딩(손가락 아이콘 줄)을 유지하므로 탭 아래에서 문맥이 자연스럽다
+- 색 토큰은 사이트 기존 `$text`/`$background` 반전 스타일
+
+- [ ] **Step 2: page.tsx 수정**
+
+`apps/landing/src/app/page.tsx`에서:
+
+import 교체 — `import { Trans } from './Trans'`와 `import { MathTrans } from './MathTrans'`를 제거하고 추가:
+
+```tsx
+import { DemoTabs } from './DemoTabs'
+```
+
+본문에서 아래 두 섹션을:
+
+```tsx
+
+
+
+
+
+
+```
+
+다음 하나로 교체:
+
+```tsx
+
+
+
+```
+
+- [ ] **Step 3: 빌드 확인**
+
+Run: `cd /Users/Yeon/braillify/apps/landing && bun run build`
+Expected: `✓ Compiled successfully`
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd /Users/Yeon/braillify
+git add apps/landing/src/app/DemoTabs.tsx apps/landing/src/app/page.tsx
+git commit -m "feat: 한글·수학 점역 데모를 탭 토글로 통합"
+```
+
+---
+
+### Task 3: 브라우저 검증 및 마무리
+
+**Files:**
+- 수정 없음 (검증 결과에 따른 보정만)
+
+- [ ] **Step 1: dev 서버 실행** (`bun run --cwd apps/landing dev`, 포트 3000)
+
+- [ ] **Step 2: 체크리스트 검증**
+
+1. 홈에 데모 섹션이 **하나만** 있고 상단에 `한글`/`수학` 탭이 보인다 (기본 `한글` 선택)
+2. `한글` 탭: 기존 한글 데모가 그대로 동작 (입력 → 점자)
+3. `수학` 탭 클릭: 수학 데모로 전환, 왼쪽 입력창에 안내문 "수식을 입력하면 이곳에 수학 점자가 표시됩니다."가 보인다
+4. 오른쪽 출력창 placeholder가 "예: 사분의 삼 → ⠼⠙⠌⠼⠉"이다
+5. 데스크톱 레이아웃에서 좌우 박스의 외곽 크기(너비·높이)가 동일하다 (getBoundingClientRect 비교, ±2px)
+6. 수식 입력(VK 3÷4) → LaTeX 줄 `LaTeX: $\frac{3}{4}$` + 점자 `⠼⠙⠌⠼⠉`, placeholder 사라짐
+7. 수학 탭에서 필드 포커스(키보드 열림) 상태로 `한글` 탭 전환 → 수식 키보드가 닫힌다
+8. 콘솔 에러 없음
+
+- [ ] **Step 3: 최종 빌드 재확인** (`bun run build` → `✓ Compiled successfully`)
+
+- [ ] **Step 4: 보정이 있었다면 커밋** (`fix: 데모 탭 통합 브라우저 검증 후 보정`)
diff --git a/docs/superpowers/plans/2026-07-22-math-braille-landing-demo.md b/docs/superpowers/plans/2026-07-22-math-braille-landing-demo.md
new file mode 100644
index 00000000..9ffbf420
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-22-math-braille-landing-demo.md
@@ -0,0 +1,390 @@
+# 수학 점역 랜딩 데모 (MathLive) Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 랜딩 페이지에 MathLive 기반 WYSIWYG 수식 입력기를 추가해, 사용자가 화면 수식 키보드로 수식을 편집하면 자동 생성된 LaTeX가 실시간으로 한국 수학 점자로 점역되는 데모 섹션을 만든다.
+
+**Architecture:** 기존 `Trans`(텍스트 점역 데모) 섹션과 동일한 시각 언어를 따르는 새 `MathTrans` 섹션을 홈페이지에 추가한다. MathLive `` 웹 컴포넌트가 LaTeX를 생성하면 `$...$`로 감싸 기존 braillify WASM API(`translateToUnicode`)에 그대로 넘긴다. **코어 엔진 변경은 없다** — `libs/braillify`의 `latex_math.rs`가 이미 `$\frac{3}{4}$` → `⠼⠙⠌⠼⠉`, `x^2`(중괄호 없는 위첨자), `\le`/`\leq`, `\times`, `\cdot`, `\pi` 등을 처리함을 CLI로 검증 완료.
+
+**Tech Stack:** Next.js 16 (App Router), React 19, devup-ui, MathLive 0.110.x (웹 컴포넌트 + 가상 수식 키보드), braillify WASM (workspace 패키지)
+
+## Global Constraints
+
+- 브랜치: `demo-math-landing` (origin/main 기반, 이미 생성됨 — 기존 `demo-ios-app` 네이밍 패턴 `demo-<대상>` 준수)
+- mathlive 버전: `^0.110.0`
+- **외부 CDN 금지** — braillify는 "외부 연결 없이 실행"이 셀링 포인트이므로 MathLive 폰트를 `apps/landing/public/mathlive/fonts/`에 self-host 한다
+- **코어 엔진(`libs/braillify`) 및 WASM 바인딩(`packages/node`) 수정 금지** — 랜딩 앱 안에서만 작업
+- 분수는 LaTeX `\frac{}{}` 표기만 유효 (AGENTS.md NON-NEGOTIABLE) — MathLive는 `/` 입력 시 자동으로 `\frac{}{}`를 생성하므로 자연 충족
+- `apps/landing`에는 테스트 인프라가 없다. 각 태스크의 검증 게이트는 `bun run build`(Next 빌드 + 타입체크)와 브라우저 확인이다
+- UI 문구는 한국어, 기존 홈페이지 문체(“~해보세요!”) 유지
+- 커밋 메시지는 기존 컨벤션(`feat:`, `fix:` 등) 준수
+
+## 사전 완료 사항 (이 플랜 실행 전 이미 됨)
+
+- [x] `demo-math-landing` 브랜치 생성 (origin/main 기반)
+- [x] iOS 데모 WIP는 stash에 보존됨 (`git stash list` → "demo-ios-app WIP")
+
+---
+
+### Task 1: mathlive 의존성 추가 + 폰트 self-hosting
+
+**Files:**
+- Modify: `apps/landing/package.json` (dependencies에 mathlive 추가)
+- Create: `apps/landing/public/mathlive/fonts/*` (node_modules에서 복사)
+
+**Interfaces:**
+- Produces: `import('mathlive')` 가능한 상태, `/mathlive/fonts/` 경로로 폰트 정적 서빙
+
+- [ ] **Step 1: package.json에 mathlive 추가**
+
+`apps/landing/package.json`의 `dependencies`에서 `"katex": "^0.17.0",` 바로 다음 줄에 추가:
+
+```json
+ "mathlive": "^0.110.0",
+```
+
+- [ ] **Step 2: 설치**
+
+Run: `cd /Users/Yeon/braillify && bun install`
+Expected: 에러 없이 완료, `bun.lock` 갱신됨
+
+- [ ] **Step 3: MathLive 폰트를 public으로 복사**
+
+bun 워크스페이스는 루트 `node_modules`에 호이스팅하므로 실제 위치를 확인 후 복사:
+
+```bash
+cd /Users/Yeon/braillify
+FONTS_DIR=$(dirname "$(find node_modules apps/landing/node_modules -path '*mathlive/fonts/KaTeX_Main-Regular.woff2' 2>/dev/null | head -1)")
+mkdir -p apps/landing/public/mathlive
+cp -R "$FONTS_DIR" apps/landing/public/mathlive/fonts
+ls apps/landing/public/mathlive/fonts | head
+```
+
+Expected: `KaTeX_AMS-Regular.woff2` 등 woff2 파일 목록 출력
+
+- [ ] **Step 4: 빌드로 회귀 없음 확인**
+
+Run: `cd /Users/Yeon/braillify/apps/landing && bun run build`
+Expected: `✓ Compiled successfully` — 아직 코드 변경이 없으므로 기존과 동일하게 통과
+
+- [ ] **Step 5: Commit**
+
+```bash
+cd /Users/Yeon/braillify
+git add apps/landing/package.json bun.lock apps/landing/public/mathlive
+git commit -m "feat: 랜딩에 mathlive 의존성 추가 및 폰트 self-hosting"
+```
+
+---
+
+### Task 2: MathTransInput — MathLive 필드 래퍼 컴포넌트
+
+**Files:**
+- Create: `apps/landing/src/app/MathTransInput.tsx`
+
+**Interfaces:**
+- Consumes: Task 1의 mathlive 패키지, `/mathlive/fonts` 정적 경로
+- Produces: `MathTransInput({ onLatexChange: (latex: string) => void })` — 사용자가 수식을 편집할 때마다 placeholder 토큰이 제거된 LaTeX 문자열을 콜백으로 전달. 포커스 시 화면 수식 키보드 표시.
+
+**설계 근거 (구현자가 알아야 할 것):**
+- ``는 웹 컴포넌트라 SSR 불가 → `'use client'` + `useEffect` 안에서 `import('mathlive')` 동적 로드, 로드 완료 전에는 자리 placeholder Box 렌더
+- 가상 키보드가 데모의 핵심 → `math-virtual-keyboard-policy="manual"` + `focusin`/`focusout`에서 `window.mathVirtualKeyboard.show()/hide()` (기본 `auto` 정책은 터치 기기에서만 키보드를 띄우므로 데스크톱 데모에 부적합)
+- LaTeX 추출은 `field.getValue('latex-without-placeholders')` — 기본 `.value`는 빈 인자를 `\placeholder{}`로 직렬화해 braillify가 점역할 수 없음
+- `MathfieldElement.soundsDirectory = null`로 키보드 효과음 비활성 (사운드 파일 미제공)
+
+- [ ] **Step 1: 컴포넌트 작성**
+
+`apps/landing/src/app/MathTransInput.tsx` 생성:
+
+```tsx
+'use client'
+
+import { Box } from '@devup-ui/react'
+import type { MathfieldElement } from 'mathlive'
+import { useEffect, useRef, useState } from 'react'
+
+declare global {
+ namespace React.JSX {
+ interface IntrinsicElements {
+ 'math-field': React.DetailedHTMLProps<
+ React.HTMLAttributes,
+ MathfieldElement
+ > & { 'math-virtual-keyboard-policy'?: 'auto' | 'manual' }
+ }
+ }
+}
+
+export function MathTransInput({
+ onLatexChange,
+}: {
+ onLatexChange: (latex: string) => void
+}) {
+ const [ready, setReady] = useState(false)
+ const fieldRef = useRef(null)
+
+ useEffect(() => {
+ let cancelled = false
+ import('mathlive').then(({ MathfieldElement }) => {
+ if (cancelled) return
+ MathfieldElement.fontsDirectory = '/mathlive/fonts'
+ MathfieldElement.soundsDirectory = null
+ setReady(true)
+ })
+ return () => {
+ cancelled = true
+ }
+ }, [])
+
+ useEffect(() => {
+ const field = fieldRef.current
+ if (!ready || !field) return
+ const show = () => window.mathVirtualKeyboard.show()
+ const hide = () => window.mathVirtualKeyboard.hide()
+ field.addEventListener('focusin', show)
+ field.addEventListener('focusout', hide)
+ return () => {
+ field.removeEventListener('focusin', show)
+ field.removeEventListener('focusout', hide)
+ }
+ }, [ready])
+
+ return (
+
+ {ready ? (
+
+ onLatexChange(
+ (e.target as MathfieldElement).getValue(
+ 'latex-without-placeholders',
+ ),
+ )
+ }
+ style={{
+ background: 'transparent',
+ border: 'none',
+ display: 'block',
+ fontSize: '28px',
+ width: '100%',
+ }}
+ />
+ ) : (
+
+ )}
+
+ )
+}
+```
+
+- [ ] **Step 2: 타입체크/빌드 통과 확인**
+
+Run: `cd /Users/Yeon/braillify/apps/landing && bun run build`
+Expected: `✓ Compiled successfully` — 아직 어디서도 import하지 않지만 타입 선언(`React.JSX.IntrinsicElements` 증강)이 유효한지 이 단계에서 확인된다. `math-field` JSX 태그 관련 타입 에러가 나면 declaration 블록을 점검할 것 (React 19는 `React.JSX` 네임스페이스를 사용한다)
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd /Users/Yeon/braillify
+git add apps/landing/src/app/MathTransInput.tsx
+git commit -m "feat: MathLive 수식 입력 래퍼 컴포넌트 MathTransInput 추가"
+```
+
+---
+
+### Task 3: MathTrans 섹션 — LaTeX → 점자 변환 + 홈페이지 연결
+
+**Files:**
+- Create: `apps/landing/src/app/MathTrans.tsx`
+- Modify: `apps/landing/src/app/page.tsx:79-81` (`` 래퍼 Flex 바로 아래에 동일 래퍼로 추가)
+
+**Interfaces:**
+- Consumes: Task 2의 `MathTransInput({ onLatexChange })`, 기존 `TransInput`(`apps/landing/src/app/TransInput.tsx` — `blurPlaceholder`/`focusPlaceholder`/`readOnly`/`value` props), braillify WASM의 `translateToUnicode(text: string): string`
+- Produces: 홈페이지에 렌더되는 `MathTrans()` 섹션 (props 없음)
+
+**설계 근거:**
+- 변환 파이프라인: MathLive LaTeX → `` `$${latex}$` `` 로 감싸기 → `translateToUnicode` — 기존 `Trans.tsx`의 WASM 동적 로드 패턴을 따르되, 로드는 마운트 시 1회만 한다 (`Trans.tsx`의 `[input]` 의존성은 매 입력마다 재로드하는 기존 버그이므로 답습하지 않는다)
+- 입력 중간 상태(`\frac{}{}` 등)는 점역 불가 → catch에서 안내 문구 반환, 데모상 허용
+- 생성된 LaTeX를 코드 폰트로 노출해 "LaTeX 자동 생성"을 보여준다
+
+- [ ] **Step 1: MathTrans 컴포넌트 작성**
+
+`apps/landing/src/app/MathTrans.tsx` 생성:
+
+```tsx
+'use client'
+import { Box, Flex, Image, Text, VStack } from '@devup-ui/react'
+import { useEffect, useState } from 'react'
+
+import { MathTransInput } from './MathTransInput'
+import { TransInput } from './TransInput'
+
+export function MathTrans() {
+ const [latex, setLatex] = useState('')
+ const [translateToUnicode, setTranslateToUnicode] = useState<
+ (input: string) => string
+ >(() => () => '')
+ useEffect(() => {
+ import('braillify').then((mod) => {
+ setTranslateToUnicode(() => (input: string) => {
+ try {
+ return mod.translateToUnicode(input)
+ } catch (e) {
+ console.error(e)
+ return '점역할 수 없는 수식이 포함되어 있습니다.'
+ }
+ })
+ })
+ }, [])
+
+ const braille = latex.trim() ? translateToUnicode(`$${latex}$`) : ''
+
+ return (
+
+
+
+
+ 수식도 점자가 됩니다. 수식 키보드로 입력해 수학 점역을 체험해보세요!
+
+
+
+
+
+
+ {latex ? `LaTeX: $${latex}$` : 'LaTeX가 자동으로 생성됩니다'}
+
+
+
+
+
+
+
+
+
+ )
+}
+```
+
+- [ ] **Step 2: page.tsx에 섹션 추가**
+
+`apps/landing/src/app/page.tsx` 수정. import에 추가:
+
+```tsx
+import { MathTrans } from './MathTrans'
+```
+
+`` 바로 다음 줄에 추가:
+
+```tsx
+
+
+
+```
+
+- [ ] **Step 3: 빌드 통과 확인**
+
+Run: `cd /Users/Yeon/braillify/apps/landing && bun run build`
+Expected: `✓ Compiled successfully`
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd /Users/Yeon/braillify
+git add apps/landing/src/app/MathTrans.tsx apps/landing/src/app/page.tsx
+git commit -m "feat: 홈페이지에 MathLive 기반 수학 점역 데모 섹션 추가"
+```
+
+---
+
+### Task 4: 브라우저 검증 및 마무리
+
+**Files:**
+- 수정 없음 (검증 결과에 따른 스타일/버그 수정만)
+
+**Interfaces:**
+- Consumes: Task 3까지의 전체 결과물
+
+- [ ] **Step 1: dev 서버 실행**
+
+Run: `cd /Users/Yeon/braillify/apps/landing && bun run dev`
+Expected: `http://localhost:3000`에서 서비스 시작
+
+- [ ] **Step 2: 브라우저에서 체크리스트 검증**
+
+`http://localhost:3000` 접속 후 다음을 모두 확인 (브라우저 자동화 도구 사용 가능):
+
+1. 기존 텍스트 점역 데모 아래에 수학 데모 섹션이 렌더된다
+2. 수식 필드 클릭 시 **화면 수식 키보드**가 하단에 나타난다
+3. 키보드로 `3`, `/`, `4` 입력 → 필드에 분수 WYSIWYG 렌더, LaTeX 표시줄에 `$\frac{3}{4}$`, 점자 출력에 `⠼⠙⠌⠼⠉`
+4. `x`, `^`, `2` 입력 → 점자 출력에 `⠭⠘⠼⠃` 포함
+5. 필드 밖 클릭 시 가상 키보드가 사라진다
+6. 수식을 전부 지우면 점자 출력이 비고 placeholder가 다시 보인다
+7. 콘솔에 폰트 404 에러가 없다 (`/mathlive/fonts/` 로드 확인)
+8. 다크 테마 토글 시 수식 필드 글자가 읽을 수 있는 상태다 (수식 글자색이 배경에 묻히면 `MathTransInput`의 `math-field` style에 `color: 'var(--text)'` 추가)
+
+Expected: 8개 항목 전부 통과. 실패 항목은 superpowers:systematic-debugging으로 원인을 찾아 수정 후 재검증
+
+- [ ] **Step 3: 최종 빌드 재확인**
+
+Run: `cd /Users/Yeon/braillify/apps/landing && bun run build`
+Expected: `✓ Compiled successfully`
+
+- [ ] **Step 4: 수정 사항이 있었다면 커밋**
+
+```bash
+cd /Users/Yeon/braillify
+git add apps/landing/src
+git commit -m "fix: 수학 점역 데모 브라우저 검증 후 스타일 보정"
+```
+
+(수정이 없었다면 이 커밋은 생략)