Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
82b5d63
feat: 랜딩에 mathlive 의존성 추가 및 폰트 self-hosting
jessieyeon Jul 22, 2026
990c939
feat: MathLive 수식 입력 래퍼 컴포넌트 MathTransInput 추가
jessieyeon Jul 22, 2026
3af34fd
feat: 홈페이지에 MathLive 기반 수학 점역 데모 섹션 추가
jessieyeon Jul 22, 2026
c4c32c5
fix: MathLive 중괄호 생략 \frac 정규화 및 수학 데모 접근성 속성 보정
jessieyeon Jul 22, 2026
f1ce9b9
docs: 수학 점역 랜딩 데모 구현 플랜 추가
jessieyeon Jul 22, 2026
fa0c527
fix: 수학 데모 마무리 보정 — 언마운트 시 가상 키보드 숨김, placeholder 정리
jessieyeon Jul 22, 2026
1d8bb3c
feat: 수학 데모 안내문을 입력창으로 이동하고 입출력 박스 크기 통일
jessieyeon Jul 22, 2026
8ffa6cf
fix: 수학 입력 박스 높이를 출력 박스와 동일한 동적 규격으로 보정
jessieyeon Jul 22, 2026
3bd1df8
feat: 한글·수학 점역 데모를 탭 토글로 통합
jessieyeon Jul 22, 2026
9e9f713
docs: 랜딩 데모 탭 통합 구현 플랜 추가
jessieyeon Jul 22, 2026
8140e02
fix: 코드리뷰 지적 반영 — 데모 입력 박스 클릭 포커스·파서 보강·레이아웃 중복 제거·탭 접근성·박스 크기 통일
jessieyeon Jul 22, 2026
0f0d99e
refactor: 코드리뷰 반영 — 세로 컨테이너를 VStack으로 정리하고 불필요한 속성 제거
jessieyeon Jul 28, 2026
143aaa8
fix: 코드리뷰 추가 반영 — style을 css로 전환, VStack flexDirection 정리, 플랜 문서 삭제
jessieyeon Jul 30, 2026
0d2ff62
fix: 수학 입력 박스의 w=100%를 flex=1로 대체
jessieyeon Jul 30, 2026
606980f
refactor(landing): LaTeX \frac 정규화 순수 함수를 ts 파일로 분리하고 테스트 추가 (리뷰 #8)
jessieyeon Aug 7, 2026
85a6caa
fix(landing): 수학 입력 박스 이중 래퍼를 VStack 하나로 병합 (리뷰 #9 #10 #11)
jessieyeon Aug 7, 2026
747296d
refactor(landing): 컨테이너 루트의 동작하지 않는 flex=1 제거 (리뷰 #4 #7)
jessieyeon Aug 7, 2026
bb6c146
fix(landing): 데모 아이콘 폭을 짝수로, 화살표에 대체 텍스트 부여 (리뷰 #2 #3)
jessieyeon Aug 7, 2026
95ac3d1
docs(landing): 재배포하는 MathLive/KaTeX 폰트의 MIT 라이선스 고지 추가 (리뷰 #1)
jessieyeon Aug 7, 2026
64330ed
fix(landing): 수학 입력 박스 폭 어긋남 복구 — padding 없는 바깥 래퍼 유지
jessieyeon Aug 7, 2026
51ab161
fix(landing): 리뷰 코멘트대로 재반영 (리뷰 #5 #6 #9 #10)
jessieyeon Aug 7, 2026
3a93d45
fix(landing): 탭 radius 를 알약(1000px)으로 되돌리고 행의 alignItems=center 제거 (리…
jessieyeon Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,9 @@ tarpaulin-report.html
tarpaulin-report.json
**/tarpaulin-report.html
**/tarpaulin-report.json

# superpowers 작업 산출물(플랜 문서 등) — 저장소에 커밋하지 않는다.
docs/superpowers/

# superpowers 로컬 작업 디렉터리.
.superpowers/
1 change: 1 addition & 0 deletions apps/landing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 22 additions & 0 deletions apps/landing/public/mathlive/LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
The KaTeX_*.woff2 font files in ./fonts are redistributed from the MathLive
package (https://github.com/arnog/mathlive) under the MIT License below.

Copyright (c) 2017 - present Arno Gourdol. All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
24 changes: 24 additions & 0 deletions apps/landing/src/app/DemoArrow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Flex, Image } from '@devup-ui/react'

/** 입력 박스와 출력 박스 사이의 방향 화살표. 한글·수학 데모가 공유한다. */
export function DemoArrow() {
return (
<Flex alignSelf="center">
{/* 데스크톱에서만 보이는 원형 장식. 화살표와 중복 안내되지 않도록 장식으로 둔다. */}
<Image
alt=""
display={['none', null, null, 'block']}
mr="10px"
role="presentation"
src="/images/home/translate-arrow-circle.svg"
w="16px"
/>
<Image
alt="점역 결과 방향"
src="/images/home/translate-arrow.svg"
transform={[null, null, null, 'rotate(-90deg)']}
w={['16px', null, null, '24px']}
/>
</Flex>
)
}
29 changes: 29 additions & 0 deletions apps/landing/src/app/DemoHeading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Box, Flex, Text } from '@devup-ui/react'

/** 데모 섹션 상단의 손가락 아이콘 + 안내 문구. 한글·수학 데모가 공유한다. */
export function DemoHeading({ children }: { children: string }) {
return (
<Flex
alignItems="flex-start"
gap={['10px', null, null, '20px']}
justifyContent={['center', null, null, 'flex-start']}
>
<Box
aria-hidden="true"
bg="$text"
flexShrink={0}
h={['20px', null, null, '32px']}
maskImage="url(/images/home/finger-point.svg)"
maskPosition="center"
maskRepeat="no-repeat"
maskSize="contain"
// 아이콘 원본 비율(28x32)상 h=20px 의 이론 폭은 17.5px 이다.
// maskSize=contain 이라 박스를 18px 로 잡아도 아이콘은 늘어나지 않는다.
w={['18px', null, null, '28px']}
/>
<Text color="$text" pos="relative" top="-2px" typography="mainTextSm">
{children}
</Text>
</Flex>
)
}
79 changes: 79 additions & 0 deletions apps/landing/src/app/DemoTabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'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<DemoMode>('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 (
<VStack gap={['16px', null, null, '30px']} w="100%">
<Flex
aria-label="점역 데모 종류"
gap="10px"
justifyContent={['center', null, null, 'flex-start']}
role="tablist"
>
{TABS.map(({ key, label }, index) => (
<Text
key={key}
ref={(el: HTMLElement | null) => {
tabRefs.current[index] = el
}}
aria-controls={PANEL_ID}
aria-selected={mode === key}
as="button"
bg={mode === key ? '$text' : 'transparent'}
border="1px solid $text"
borderRadius="1000px"
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}
</Text>
))}
</Flex>
<Box
aria-labelledby={tabId(mode)}
display="flex"
flexDirection="column"
id={PANEL_ID}
role="tabpanel"
w="100%"
>
{mode === 'korean' ? <Trans /> : <MathTrans />}
</Box>
</VStack>
)
}
15 changes: 15 additions & 0 deletions apps/landing/src/app/MathTrans.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { VStack } from '@devup-ui/react'

import { DemoHeading } from './DemoHeading'
import { MathTransPanel } from './MathTransPanel'

export function MathTrans() {
return (
<VStack gap={['16px', null, null, '30px']}>
<DemoHeading>
수식도 점자가 됩니다. 수식 키보드로 입력해 수학 점역을 체험해보세요!
</DemoHeading>
<MathTransPanel />
</VStack>
)
}
123 changes: 123 additions & 0 deletions apps/landing/src/app/MathTransInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
'use client'

import { css, Flex, Text, VStack } from '@devup-ui/react'
import type { MathfieldElement } from 'mathlive'
import { useEffect, useRef, useState } from 'react'

import { normalizeFracBraces } from './normalizeFracBraces'

declare global {
// 커스텀 엘리먼트를 JSX 에 등록하려면 React.JSX 네임스페이스 병합이 유일한 방법이다.
// oxlint-disable-next-line no-namespace
namespace React.JSX {
interface IntrinsicElements {
'math-field': React.DetailedHTMLProps<
React.HTMLAttributes<MathfieldElement>,
MathfieldElement
> & { 'math-virtual-keyboard-policy'?: 'auto' | 'manual' }
}
}
}

export function MathTransInput({
latex,
onLatexChange,
placeholder,
}: {
latex: string
onLatexChange: (latex: string) => void
placeholder: string
}) {
const [ready, setReady] = useState(false)
const fieldRef = useRef<MathfieldElement>(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)는 호출부가 맡는다. 바깥 Flex 에 padding 을 두지 않는 이유는
// flex-basis:0 이라도 flex 아이템의 border-box 크기가 padding 합보다 작아질 수
// 없어서, padding 이 있으면 그만큼 출력측 TransInput 보다 넓어지기 때문이다.
<Flex>
<VStack
bg="$containerBackground"
borderRadius={['16px', null, null, '30px']}
cursor="text"
gap="12px"
minH="25dvh"
onClick={() => fieldRef.current?.focus()}
p={['16px', null, null, '40px']}
w="100%"
>
<VStack flex="1" gap="8px">
{ready && (
<math-field
ref={fieldRef}
className={css({
background: 'transparent',
border: 'none',
display: 'block',
fontSize: '28px',
width: '100%',
})}
math-virtual-keyboard-policy="manual"
onInput={(e) =>
onLatexChange(
normalizeFracBraces(
(e.target as MathfieldElement).getValue(
'latex-without-placeholders',
),
),
)
}
/>
)}
{!latex && (
<Text
color="$text"
opacity={0.5}
pointerEvents="none"
typography="braille"
whiteSpace="pre-line"
>
{placeholder}
</Text>
)}
</VStack>
<Text
color="$text"
fontFamily="monospace"
minH="1.5em"
opacity={0.7}
wordBreak="break-all"
>
{latex ? `LaTeX: $${latex}$` : 'LaTeX가 자동으로 생성됩니다'}
</Text>
</VStack>
</Flex>
)
}
54 changes: 54 additions & 0 deletions apps/landing/src/app/MathTransPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use client'
import { VStack } from '@devup-ui/react'
import { useEffect, useState } from 'react'

import { DemoArrow } from './DemoArrow'
import { MathTransInput } from './MathTransInput'
import { TransInput } from './TransInput'

/** 수식 입력 → 수학 점자 출력. 상태를 쥔 부분만 클라이언트로 분리한다. */
export function MathTransPanel() {
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 (
<VStack
flexDirection={[null, null, null, 'row']}
gap={['12px', null, null, '30px']}
h={['auto', null, null, '500px']}
// 좌우 박스가 폭을 반씩 나누도록 호출부에서 분배한다. 화살표(가운데)는 제외.
selectors={{
'&>*:first-child, &>*:last-child': { flex: 1, minWidth: 0 },
}}
>
<MathTransInput
latex={latex}
onLatexChange={setLatex}
placeholder="수식을 입력하면 이곳에 수학 점자가 표시됩니다."
/>
<DemoArrow />
<TransInput
blurPlaceholder="예: 사분의 삼 → ⠼⠙⠌⠼⠉"
focusPlaceholder="예: 사분의 삼 → ⠼⠙⠌⠼⠉"
readOnly
value={braille}
/>
</VStack>
)
}
Loading
Loading