diff --git a/apps/landing/devup.json b/apps/landing/devup.json index 5820886d..90f9c6fd 100644 --- a/apps/landing/devup.json +++ b/apps/landing/devup.json @@ -7,44 +7,43 @@ "link": "#006BFF", "text": "#2B2B2B", "background": "#EFEEEB", - "containerBackground": "#FFFFFF", + "containerBackground": "#FFF", "border": "#E0E0E0", "success": "#16887F", "warning": "#FF9800", "error": "#EA1C5D", "info": "#2196F3", - "base": "#FFFFFF", - "negativeBase": "#000000", + "base": "#FFF", + "negativeBase": "#000", "title": "#1A1A1A", "caption": "#787878", "menuHover": "#D8D8D8", - "menuActive": "#CCCCCC" + "menuActive": "#CCC" }, "dark": { "primary": "#EBEBEB", "secondary": "#EAD5FF", "link": "#006BFF", "text": "#EBEBEB", - "background": "#000000", + "background": "#000", "containerBackground": "#202020", - "border": "#333333", + "border": "#333", "success": "#3F8580", "warning": "#FF9800", "error": "#EA1C5D", "info": "#2196F3", - "base": "#000000", - "negativeBase": "#FFFFFF", + "base": "#000", + "negativeBase": "#FFF", "title": "#FAFAFA", "caption": "#787878", - "menuHover": "#191919", - "menuActive": "#292929" + "menuHover": "#272727", + "menuActive": "#434343" } }, "typography": { "mainText": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "22px", "lineHeight": "normal", @@ -55,17 +54,16 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "32px", "lineHeight": "normal", "letterSpacing": "-0.06em" - } + }, + null ], "buttonLg": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "17px", "lineHeight": "normal", @@ -76,17 +74,16 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "24px", "lineHeight": "normal", "letterSpacing": "-0.06em" - } + }, + null ], "mainTextSm": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "16px", "lineHeight": "normal", @@ -97,17 +94,16 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "24px", "lineHeight": "normal", "letterSpacing": "-0.06em" - } + }, + null ], "braille": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "16px", "lineHeight": 1.8, @@ -118,17 +114,16 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "22px", "lineHeight": 1.8, "letterSpacing": "-0.06em" - } + }, + null ], "title": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 700, "fontSize": "24px", "lineHeight": "normal", @@ -139,17 +134,16 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 700, "fontSize": "36px", "lineHeight": "normal", "letterSpacing": "-0.06em" - } + }, + null ], "bodyLg": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "15px", "lineHeight": 1.8, @@ -160,17 +154,16 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "18px", "lineHeight": 1.8, "letterSpacing": "-0.06em" - } + }, + null ], "featureCount": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 700, "fontSize": "14px", "lineHeight": "normal", @@ -181,17 +174,16 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 700, "fontSize": "18px", "lineHeight": "normal", "letterSpacing": "-0.06em" - } + }, + null ], "featureTitle": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 700, "fontSize": "18px", "lineHeight": "normal", @@ -202,17 +194,16 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 700, "fontSize": "24px", "lineHeight": "normal", "letterSpacing": "-0.06em" - } + }, + null ], "body": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "14px", "lineHeight": 1.8, @@ -223,17 +214,16 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "16px", "lineHeight": 1.8, "letterSpacing": "-0.06em" - } + }, + null ], "button": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "15px", "lineHeight": "normal", @@ -244,17 +234,16 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "20px", "lineHeight": "normal", "letterSpacing": "-0.06em" - } + }, + null ], "footer": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 400, "fontSize": "12px", "lineHeight": 1.8, @@ -265,17 +254,16 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 400, "fontSize": "14px", "lineHeight": 1.8, "letterSpacing": "-0.06em" - } + }, + null ], "gnbMenu": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "15px", "lineHeight": "normal", @@ -286,17 +274,16 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "18px", "lineHeight": "normal", "letterSpacing": "-0.06em" - } + }, + null ], "docsMenu": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "14px", "lineHeight": 1.8, @@ -307,17 +294,16 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "15px", "lineHeight": 1.8, "letterSpacing": "-0.06em" - } + }, + null ], "docsTitle": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 700, "fontSize": "24px", "lineHeight": "normal", @@ -328,17 +314,16 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 700, "fontSize": "32px", "lineHeight": "normal", "letterSpacing": "-0.06em" - } + }, + null ], "gnbMenuBold": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 700, "fontSize": "15px", "lineHeight": "normal", @@ -349,17 +334,16 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 700, "fontSize": "18px", "lineHeight": "normal", "letterSpacing": "-0.06em" - } + }, + null ], "docsCaption": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "13px", "lineHeight": 1.8, @@ -370,17 +354,16 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "14px", "lineHeight": 1.8, "letterSpacing": "-0.06em" - } + }, + null ], "docsMenuBold": [ { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 700, "fontSize": "14px", "lineHeight": 1.8, @@ -391,16 +374,15 @@ null, { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 700, "fontSize": "15px", "lineHeight": 1.8, "letterSpacing": "-0.06em" - } + }, + null ], "docsCaptionBold": { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 700, "fontSize": "14px", "lineHeight": 1.8, @@ -408,12 +390,51 @@ }, "docsSmall": { "fontFamily": "Spoqa Han Sans Neo", - "fontStyle": "normal", "fontWeight": 500, "fontSize": "12px", "lineHeight": 1.8, "letterSpacing": "-0.06em" - } + }, + "progress": [ + { + "fontFamily": "Spoqa Han Sans Neo", + "fontWeight": 700, + "fontSize": "14px", + "lineHeight": "normal", + "letterSpacing": "-0.06em" + }, + null, + null, + null, + { + "fontFamily": "Spoqa Han Sans Neo", + "fontWeight": 700, + "fontSize": "17px", + "lineHeight": "normal", + "letterSpacing": "-0.06em" + }, + null + ], + "bodyBold": [ + { + "fontFamily": "Spoqa Han Sans Neo", + "fontWeight": 700, + "fontSize": "14px", + "lineHeight": 1.8, + "letterSpacing": "-0.06em" + }, + null, + null, + null, + { + "fontFamily": "Spoqa Han Sans Neo", + "fontWeight": 700, + "fontSize": "16px", + "lineHeight": 1.8, + "letterSpacing": "-0.06em" + }, + null + ] } } } \ No newline at end of file diff --git a/apps/landing/package.json b/apps/landing/package.json index 3adcb291..52fa9abf 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -9,7 +9,8 @@ "lint": "next lint" }, "dependencies": { - "@devup-ui/react": "^1.0.15", + "@devup-ui/components": "^0.1.27", + "@devup-ui/react": "^1.0.21", "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", "@next/mdx": "^15.5.0", diff --git a/apps/landing/public/images/test-case/error.svg b/apps/landing/public/images/test-case/error.svg new file mode 100644 index 00000000..fdd971fb --- /dev/null +++ b/apps/landing/public/images/test-case/error.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/landing/public/images/test-case/success.svg b/apps/landing/public/images/test-case/success.svg new file mode 100644 index 00000000..3564f255 --- /dev/null +++ b/apps/landing/public/images/test-case/success.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/landing/src/app/globals.css b/apps/landing/src/app/globals.css deleted file mode 100644 index 8fdd34d8..00000000 --- a/apps/landing/src/app/globals.css +++ /dev/null @@ -1,19 +0,0 @@ -@import url(//spoqa.github.io/spoqa-han-sans/css/SpoqaHanSansNeo.css); - - -html, -body { - max-width: 100vw; - overflow-x: hidden; -} - - -* { - box-sizing: border-box; - padding: 0; - margin: 0; -} - -a { - color: var(--link); -} \ No newline at end of file diff --git a/apps/landing/src/app/layout.tsx b/apps/landing/src/app/layout.tsx index c068f239..a3084293 100644 --- a/apps/landing/src/app/layout.tsx +++ b/apps/landing/src/app/layout.tsx @@ -1,6 +1,4 @@ -import './globals.css' - -import { Box, css, ThemeScript } from '@devup-ui/react' +import { Box, css, globalCss, ThemeScript } from '@devup-ui/react' import type { Metadata } from 'next' import Footer from '@/components/Footer' @@ -48,6 +46,25 @@ export const metadata: Metadata = { }, } +globalCss({ + imports: ['https://spoqa.github.io/spoqa-han-sans/css/SpoqaHanSansNeo.css'], + 'html, body': { + maxWidth: '100vw', + overflowX: 'hidden', + }, + '*': { + boxSizing: 'border-box', + padding: 0, + margin: 0, + }, + a: { + color: 'var(--link)', + }, + '::placeholder': { + fontFamily: 'Spoqa Han Sans Neo, Arial, Helvetica, sans-serif', + }, +}) + export default function RootLayout({ children, }: Readonly<{ diff --git a/apps/landing/src/app/test-case/page.tsx b/apps/landing/src/app/test-case/page.tsx index 93db7129..a63e08ec 100644 --- a/apps/landing/src/app/test-case/page.tsx +++ b/apps/landing/src/app/test-case/page.tsx @@ -1,11 +1,20 @@ import 'katex/dist/katex.min.css' -import { Box, Grid, Text, VStack } from '@devup-ui/react' +import { Box, css, Flex, 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' +import { FailedOnlyInput } from '@/components/test-case/FailedOnlyInput' +import { TestCaseList } from '@/components/test-case/list/TestCaseList' +import { TestCaseTable } from '@/components/test-case/table/TestCaseTable' +import { TestCaseDisplayBoundary } from '@/components/test-case/TestCaseDisplayBoundary' +import { TestCaseFilterContainer } from '@/components/test-case/TestCaseFilterContainer' +import { TestCaseProvider } from '@/components/test-case/TestCaseProvider' +import { TestCaseRuleContainer } from '@/components/test-case/TestCaseRuleContainer' +import { TestCaseStat } from '@/components/test-case/TestCaseStat' +import { TestCaseTypeBoundary } from '@/components/test-case/TestCaseTypeBoundary' +import { TestCaseTypeToggle } from '@/components/test-case/TestCaseTypeToggle' +import { TestStatusMap } from '@/types' export const metadata: Metadata = { alternates: { @@ -17,18 +26,7 @@ export default async function TestCasePage() { const [testStatus, ruleMap] = await Promise.all([ readFile('../../test_status.json', 'utf-8').then((data) => JSON.parse(data), - ) as Promise< - Record< - string, - [ - success: number, - fail: number, - Array< - [text: string, expected: string, actual: string, isSuccess: boolean] - >, - ] - > - >, + ) as Promise, readFile('../../rule_map.json', 'utf-8').then((data) => JSON.parse(data), ) as Promise>, @@ -38,138 +36,127 @@ export default async function TestCasePage() { const cases = Object.entries(ruleMap).map(([key, value]) => { totalTest += testStatus[key][0] totalFail += testStatus[key][1] + + const isBut = value.title.includes('다만') + return ( - 0} + option="failedOnly" > - - - {value.title} ({testStatus[key][0] - testStatus[key][1]}/ - {testStatus[key][0]}) - - - {value.description} - - - - - {testStatus[key][2].map( - ([text, expected, actual, isSuccess], idx) => { - const textParts = parseTextWithLaTeX(text) - - return ( - - - {textParts.map((part, partIdx) => - part.type === 'latex' ? ( - ${part.content}$ - ) : ( - {part.content} - ), - )} - - 정답 : {expected} - - 결과 : {actual} - - {isSuccess ? '✅ 테스트 성공' : '❌ 테스트 실패'} - - - ) - }, - )} - - + + + + + + {value.title} + + + + + {value.description} + + + + + + + + + + ) }) return ( - - - - 테스트 케이스 ({(totalTest - totalFail).toLocaleString()}/ - {totalTest.toLocaleString()}) - - - 모든 테스트 케이스는{' '} - + + + - 2024 개정 한국 점자 규정 + + 테스트 케이스 + + + + + 모든 테스트 케이스는{' '} + + 2024 개정 한국 점자 규정 + + 을 기반으로 작성되었습니다. - 을 기반으로 작성되었습니다. - - - {cases} - + + + + 목록 형식 + + 표 형식 + + + + + 실패한 케이스만 표시하기 + + + + {cases} + + + ) } - -/** - * 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/apps/landing/src/components/test-case/FailedOnlyInput.tsx b/apps/landing/src/components/test-case/FailedOnlyInput.tsx new file mode 100644 index 00000000..10b509c8 --- /dev/null +++ b/apps/landing/src/components/test-case/FailedOnlyInput.tsx @@ -0,0 +1,24 @@ +'use client' + +import { Input } from '@devup-ui/react' +import { ComponentProps } from 'react' + +import { useTestCase } from './TestCaseProvider' + +export function FailedOnlyInput( + props: Omit< + ComponentProps>, + 'checked' | 'onChange' | 'defaultChecked' + >, +) { + const { options, onChangeOptions } = useTestCase() + return ( + + onChangeOptions({ ...options, failedOnly: e.target.checked }) + } + {...props} + /> + ) +} diff --git a/apps/landing/src/components/test-case/TestCaseDisplayBoundary.tsx b/apps/landing/src/components/test-case/TestCaseDisplayBoundary.tsx new file mode 100644 index 00000000..a9cd5c71 --- /dev/null +++ b/apps/landing/src/components/test-case/TestCaseDisplayBoundary.tsx @@ -0,0 +1,27 @@ +'use client' + +import { TestCaseOptions, useTestCase } from './TestCaseProvider' + +/** + * `TestCaseContext` 값을 참조하여 렌더링 여부를 결정합니다. + * `option`이 `undefined`이면 `display` 값에 따라 렌더링 여부를 결정합니다. + * `options[option]`가 `false`면 무조건 렌더링 합니다. + * `options[option]`가 `true`면 `display` 값에 따라 렌더링 여부를 결정합니다. + * @param display - 표시 여부 + * @param option - 옵션 키 + * @param children - 자식 요소 + * @returns 렌더링 여부를 결정한 자식 요소 + */ +export function TestCaseDisplayBoundary({ + display, + option, + children, +}: { + display: boolean + option?: keyof TestCaseOptions + children: React.ReactNode +}) { + const { options } = useTestCase() + if ((option ? options[option] : true) && !display) return null + return children +} diff --git a/apps/landing/src/components/test-case/TestCaseFilterContainer.tsx b/apps/landing/src/components/test-case/TestCaseFilterContainer.tsx new file mode 100644 index 00000000..e00a7781 --- /dev/null +++ b/apps/landing/src/components/test-case/TestCaseFilterContainer.tsx @@ -0,0 +1,23 @@ +'use client' + +import { VStack } from '@devup-ui/react' + +import { useTestCase } from './TestCaseProvider' + +export function TestCaseFilterContainer({ + children, +}: { + children: React.ReactNode +}) { + const { options } = useTestCase() + const isList = options.type === 'list' + return ( + + {children} + + ) +} diff --git a/apps/landing/src/components/test-case/TestCaseProvider.tsx b/apps/landing/src/components/test-case/TestCaseProvider.tsx new file mode 100644 index 00000000..ddf449a1 --- /dev/null +++ b/apps/landing/src/components/test-case/TestCaseProvider.tsx @@ -0,0 +1,48 @@ +'use client' + +import { createContext, useContext, useState } from 'react' + +import { TestStatusMap } from '@/types' + +export type TestCaseOptions = { + failedOnly: boolean + type: 'list' | 'table' +} + +const TestCaseContext = createContext<{ + testStatusMap: TestStatusMap + options: TestCaseOptions + onChangeOptions: (options: TestCaseOptions) => void +} | null>(null) + +export function useTestCase() { + const context = useContext(TestCaseContext) + if (!context) { + throw new Error('useTestCase must be used within a TestCaseProvider') + } + return context +} + +export function TestCaseProvider({ + testStatusMap, + children, +}: { + testStatusMap: TestStatusMap + children: React.ReactNode +}) { + const [options, setOptions] = useState({ + failedOnly: false, + type: 'list', + }) + const handleChangeOptions = (options: TestCaseOptions) => { + setOptions((prev) => ({ ...prev, ...options })) + } + + return ( + + {children} + + ) +} diff --git a/apps/landing/src/components/test-case/TestCaseRuleContainer.tsx b/apps/landing/src/components/test-case/TestCaseRuleContainer.tsx new file mode 100644 index 00000000..3478f51b --- /dev/null +++ b/apps/landing/src/components/test-case/TestCaseRuleContainer.tsx @@ -0,0 +1,27 @@ +'use client' + +import { VStack } from '@devup-ui/react' + +import { useTestCase } from './TestCaseProvider' + +export function TestCaseRuleContainer({ + exception, + children, +}: { + exception: boolean + children: React.ReactNode +}) { + const { options } = useTestCase() + const isList = options.type === 'list' + return ( + + {children} + + ) +} diff --git a/apps/landing/src/components/test-case/TestCaseStat.tsx b/apps/landing/src/components/test-case/TestCaseStat.tsx new file mode 100644 index 00000000..ae8881f0 --- /dev/null +++ b/apps/landing/src/components/test-case/TestCaseStat.tsx @@ -0,0 +1,49 @@ +import { Center, Text } from '@devup-ui/react' +import { ComponentProps } from 'react' + +interface TestCaseStatProps extends ComponentProps> { + showTotal?: boolean + total: number + success: number + fail: number +} + +export function TestCaseStat({ + showTotal = false, + total, + success, + fail, + ...props +}: TestCaseStatProps) { + const hasFail = fail > 0 + + return ( + + {showTotal && ( + + 전체 {total.toLocaleString()} + + )} + + 성공 {success.toLocaleString()} + + + 실패 {fail.toLocaleString()} + + + ({Math.round((success / total) * 100)}%) + + + ) +} diff --git a/apps/landing/src/components/test-case/TestCaseTypeBoundary.tsx b/apps/landing/src/components/test-case/TestCaseTypeBoundary.tsx new file mode 100644 index 00000000..f551893c --- /dev/null +++ b/apps/landing/src/components/test-case/TestCaseTypeBoundary.tsx @@ -0,0 +1,19 @@ +'use client' + +import { TestCaseDisplayBoundary } from './TestCaseDisplayBoundary' +import { useTestCase } from './TestCaseProvider' + +export function TestCaseTypeBoundary({ + type, + children, +}: { + type: 'list' | 'table' + children?: React.ReactNode +}) { + const { options } = useTestCase() + return ( + + {children} + + ) +} diff --git a/apps/landing/src/components/test-case/TestCaseTypeToggle.tsx b/apps/landing/src/components/test-case/TestCaseTypeToggle.tsx new file mode 100644 index 00000000..d9144b4b --- /dev/null +++ b/apps/landing/src/components/test-case/TestCaseTypeToggle.tsx @@ -0,0 +1,19 @@ +'use client' + +import { Toggle } from '@devup-ui/components' +import { ComponentProps } from 'react' + +import { useTestCase } from './TestCaseProvider' + +export function TestCaseTypeToggle(props: ComponentProps) { + const { options, onChangeOptions } = useTestCase() + return ( + + onChangeOptions({ ...options, type: value ? 'table' : 'list' }) + } + value={options.type === 'table'} + {...props} + /> + ) +} diff --git a/apps/landing/src/components/test-case/list/TestCaseList.tsx b/apps/landing/src/components/test-case/list/TestCaseList.tsx new file mode 100644 index 00000000..3445a95f --- /dev/null +++ b/apps/landing/src/components/test-case/list/TestCaseList.tsx @@ -0,0 +1,95 @@ +import { Grid, Text } from '@devup-ui/react' +import Latex from 'react-syntax-highlighter/dist/cjs/languages/hljs/latex' + +import { TestStatus } from '@/types' + +import TestCaseCircle from '../TestCaseCircle' +import { TestCaseDisplayBoundary } from '../TestCaseDisplayBoundary' + +export function TestCaseList({ results }: { results: TestStatus[2] }) { + return ( + + {results.map(([text, expected, actual, isSuccess], idx) => { + const textParts = parseTextWithLaTeX(text) + + return ( + + + + {textParts.map((part, partIdx) => + part.type === 'latex' ? ( + ${part.content}$ + ) : ( + {part.content} + ), + )} + + 정답 : {expected} + + 결과 : {actual} + + {isSuccess ? '✅ 테스트 성공' : '❌ 테스트 실패'} + + + + ) + })} + + ) +} + +/** + * 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/apps/landing/src/components/test-case/table/TestCaseTable.tsx b/apps/landing/src/components/test-case/table/TestCaseTable.tsx new file mode 100644 index 00000000..3beb8b3e --- /dev/null +++ b/apps/landing/src/components/test-case/table/TestCaseTable.tsx @@ -0,0 +1,109 @@ +import { css, Flex, Image, VStack } from '@devup-ui/react' +import { Text } from '@devup-ui/react' + +import { Table, Tbody, Td, Th, Thead, Tr } from '@/components/test-case/table' +import { TestStatus } from '@/types' + +import { TestCaseDisplayBoundary } from '../TestCaseDisplayBoundary' + +export function TestCaseTable({ results }: { results: TestStatus[2] }) { + return ( + + + + 번호 + 예문 + 정답 + 결과 + 성공 여부 + + + + {results.map(([text, expected, actual, isSuccess], index) => ( + + + {index + 1} + {text} + {expected} + {actual} + + + {isSuccess ? '성공' : '실패'} + + + + + + + + + {index + 1} + + + + 예문 + {text} + + + 정답 + {expected} + + + 결과 + {actual} + + + + + + ))} + + + ) +} diff --git a/apps/landing/src/components/test-case/table/index.tsx b/apps/landing/src/components/test-case/table/index.tsx new file mode 100644 index 00000000..3bb304cb --- /dev/null +++ b/apps/landing/src/components/test-case/table/index.tsx @@ -0,0 +1,96 @@ +import { Box } from '@devup-ui/react' +import { ComponentProps } from 'react' + +export function Table(props: ComponentProps>) { + return ( + + ) +} + +export function Thead(props: ComponentProps>) { + return ( + + ) +} + +export function Tbody(props: ComponentProps>) { + return +} + +export function Tr(props: ComponentProps>) { + return +} + +export function Th(props: ComponentProps>) { + return ( + + ) +} + +export function Td({ + typography = 'body', + ...props +}: ComponentProps>) { + return ( + + ) +} diff --git a/apps/landing/src/types/index.ts b/apps/landing/src/types/index.ts index 892545b6..633282a9 100644 --- a/apps/landing/src/types/index.ts +++ b/apps/landing/src/types/index.ts @@ -1 +1,9 @@ export type Merge = Omit> & U + +export type TestStatus = [ + success: number, + fail: number, + Array<[text: string, expected: string, actual: string, isSuccess: boolean]>, +] + +export type TestStatusMap = Record diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f636a759..74ac7cda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,9 +41,12 @@ importers: apps/landing: dependencies: + '@devup-ui/components': + specifier: ^0.1.27 + version: 0.1.27(@devup-ui/react@1.0.21(csstype@3.1.3)(react@19.1.1))(csstype@3.1.3)(react@19.1.1) '@devup-ui/react': - specifier: ^1.0.15 - version: 1.0.19(csstype@3.1.3)(react@19.1.1) + specifier: ^1.0.21 + version: 1.0.21(csstype@3.1.3)(react@19.1.1) '@mdx-js/loader': specifier: ^3.1.0 version: 3.1.1 @@ -188,14 +191,21 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@devup-ui/components@0.1.27': + resolution: {integrity: sha512-ZgSVgiR8UGnsiEV1UFHE6Bkz8UbGfa/689TejTUZp7y9sQk277WurzWBRBNjxtHF5A/COcBJ5H6dFu7eCrAVXA==} + peerDependencies: + '@devup-ui/react': 1.0.21 + csstype: '*' + react: '*' + '@devup-ui/next-plugin@1.0.42': resolution: {integrity: sha512-g4ISSOoUlkHW7kzxIuHff8LBktxUJyZsuDi3X/6d8hy7qoVJAFpVBlw9NOc/TqbBLsX/pH81v3E1EVS0l5PGuQ==} peerDependencies: '@devup-ui/webpack-plugin': '*' next: '*' - '@devup-ui/react@1.0.19': - resolution: {integrity: sha512-o9ccE9zmQCGYtN6k+xPC4Gu83sX5KoHebyzj9RU1Fd0glNLl+o+j5QVzetAXCu4l/m02kBe3DEJ6w3wPeqEL3w==} + '@devup-ui/react@1.0.21': + resolution: {integrity: sha512-zX7wrcNiieE1hH714omghdYY94bN6gdS8HaVhiVThTvMxbPPHR01v1xP3dMWmufEw8je5YsZkxFfYGn10xYyXw==} peerDependencies: csstype: '*' react: '*' @@ -1056,8 +1066,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.8.9: - resolution: {integrity: sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==} + baseline-browser-mapping@2.8.20: + resolution: {integrity: sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==} hasBin: true better-path-resolve@1.0.0: @@ -1074,8 +1084,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.26.2: - resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==} + browserslist@4.27.0: + resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1102,6 +1112,9 @@ packages: caniuse-lite@1.0.30001745: resolution: {integrity: sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==} + caniuse-lite@1.0.30001751: + resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1148,6 +1161,10 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -1254,8 +1271,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.227: - resolution: {integrity: sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==} + electron-to-chromium@1.5.240: + resolution: {integrity: sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2104,8 +2121,8 @@ packages: encoding: optional: true - node-releases@2.0.21: - resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} + node-releases@2.0.26: + resolution: {integrity: sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==} object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -2681,8 +2698,8 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + update-browserslist-db@1.1.4: + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -3012,13 +3029,20 @@ snapshots: human-id: 4.1.1 prettier: 2.8.8 + '@devup-ui/components@0.1.27(@devup-ui/react@1.0.21(csstype@3.1.3)(react@19.1.1))(csstype@3.1.3)(react@19.1.1)': + dependencies: + '@devup-ui/react': 1.0.21(csstype@3.1.3)(react@19.1.1) + clsx: 2.1.1 + csstype: 3.1.3 + react: 19.1.1 + '@devup-ui/next-plugin@1.0.42(@devup-ui/webpack-plugin@1.0.41(@devup-ui/wasm@1.0.40))(next@15.5.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1))': dependencies: '@devup-ui/webpack-plugin': 1.0.41(@devup-ui/wasm@1.0.40) - browserslist: 4.26.2 + browserslist: 4.27.0 next: 15.5.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@devup-ui/react@1.0.19(csstype@3.1.3)(react@19.1.1)': + '@devup-ui/react@1.0.21(csstype@3.1.3)(react@19.1.1)': dependencies: csstype: 3.1.3 react: 19.1.1 @@ -3799,7 +3823,7 @@ snapshots: balanced-match@1.0.2: {} - baseline-browser-mapping@2.8.9: {} + baseline-browser-mapping@2.8.20: {} better-path-resolve@1.0.0: dependencies: @@ -3818,13 +3842,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.26.2: + browserslist@4.27.0: dependencies: - baseline-browser-mapping: 2.8.9 - caniuse-lite: 1.0.30001745 - electron-to-chromium: 1.5.227 - node-releases: 2.0.21 - update-browserslist-db: 1.1.3(browserslist@4.26.2) + baseline-browser-mapping: 2.8.20 + caniuse-lite: 1.0.30001751 + electron-to-chromium: 1.5.240 + node-releases: 2.0.26 + update-browserslist-db: 1.1.4(browserslist@4.27.0) cac@6.7.14: {} @@ -3849,6 +3873,8 @@ snapshots: caniuse-lite@1.0.30001745: {} + caniuse-lite@1.0.30001751: {} + ccount@2.0.1: {} chai@5.3.3: @@ -3886,6 +3912,8 @@ snapshots: client-only@0.0.1: {} + clsx@2.1.1: {} + collapse-white-space@2.1.0: {} color-convert@2.0.1: @@ -3983,7 +4011,7 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.227: {} + electron-to-chromium@1.5.240: {} emoji-regex@8.0.0: {} @@ -5212,7 +5240,7 @@ snapshots: dependencies: whatwg-url: 5.0.0 - node-releases@2.0.21: {} + node-releases@2.0.26: {} object-assign@4.1.1: {} @@ -5934,9 +5962,9 @@ snapshots: universalify@0.1.2: {} - update-browserslist-db@1.1.3(browserslist@4.26.2): + update-browserslist-db@1.1.4(browserslist@4.27.0): dependencies: - browserslist: 4.26.2 + browserslist: 4.27.0 escalade: 3.2.0 picocolors: 1.1.1