diff --git a/apps/frontend/app/(client)/(code-editor)/_components/TestcasePanel/TestcasePanel.tsx b/apps/frontend/app/(client)/(code-editor)/_components/TestcasePanel/TestcasePanel.tsx index 1bf0a8f581..a071d98211 100644 --- a/apps/frontend/app/(client)/(code-editor)/_components/TestcasePanel/TestcasePanel.tsx +++ b/apps/frontend/app/(client)/(code-editor)/_components/TestcasePanel/TestcasePanel.tsx @@ -3,13 +3,23 @@ import { ScrollArea, ScrollBar } from '@/components/shadcn/scroll-area' import { cn, getResultColor } from '@/libs/utils' import type { TestResultDetail } from '@/types/type' +import { DiffMatchPatch } from 'diff-match-patch-typescript' import { useState, type ReactNode } from 'react' import { IoMdClose } from 'react-icons/io' -import { WhitespaceVisualizer } from '../WhitespaceVisualizer' import { AddUserTestcaseDialog } from './AddUserTestcaseDialog' import { TestcaseTable } from './TestcaseTable' import { useTestResults } from './useTestResults' +function getWidthClass(length: number) { + if (length < 5) { + return 'w-40' + } else if (length < 7) { + return 'w-28' + } else { + return 'w-24' + } +} + export function TestcasePanel() { const [testcaseTabList, setTestcaseTabList] = useState([]) const [currentTab, setCurrentTab] = useState(0) @@ -52,23 +62,36 @@ export function TestcasePanel() { return ( <> -
+
setCurrentTab(0)} nextTab={testcaseTabList[0]?.originalId} - className="flex-shrink-0" + className={cn( + 'h-full flex-shrink-0 overflow-hidden text-ellipsis whitespace-nowrap', + getWidthClass(testcaseTabList.length) + )} > - {testcaseTabList.length < 7 ? 'Testcase Result' : 'TC Res'} +
+ + {(() => { + if (testcaseTabList.length < 7) { + return 'Testcase Result' + } else { + return 'TC Res' + } + })()} + +
-
+
{testcaseTabList.map((testcase, index) => ( removeTab(testcase.originalId)} testcaseId={testcase.originalId} key={testcase.originalId} + className={cn( + 'h-12 flex-shrink-0 overflow-hidden text-ellipsis whitespace-nowrap', // 높이, 말줄임 처리 + getWidthClass(testcaseTabList.length) + )} > - { - (testcaseTabList.length < 7 - ? TAB_CONTENT - : SHORTHAND_TAB_CONTENT)[ - testcase.isUserTestcase ? 'user' : 'sample' - ] - }{' '} - #{testcase.id} +
+ { + (testcaseTabList.length < 7 + ? TAB_CONTENT + : SHORTHAND_TAB_CONTENT)[ + testcase.isUserTestcase ? 'user' : 'sample' + ] + }{' '} + #{testcase.id} +
))}
- - + +
) } -function LabeledField({ label, text }: { label: string; text: string }) { +interface LabeledFieldProps { + label: string + text: string + compareText?: string +} +function LabeledField({ label, text, compareText }: LabeledFieldProps) { + const getColoredText = ( + text: string, + compareText: string, + isExpectedOutput: boolean + ) => { + const isNumeric = (str: string) => /^[+-]?\d+(\.\d+)?$/.test(str.trim()) + + const isBothNumeric = isNumeric(text) && isNumeric(compareText) + + if (isBothNumeric) { + const num1 = parseFloat(text) + const num2 = parseFloat(compareText) + + if (num1 !== num2) { + return isExpectedOutput ? ( + {compareText} + ) : ( + {text} + ) + } else { + return {text} + } + } + const dmp = new DiffMatchPatch() + const diffs = dmp.diff_main(compareText, text) + dmp.diff_cleanupSemantic(diffs) + + return diffs.map(([op, data], idx) => { + if (op === 0) { + // 동일한 텍스트는 흰색으로 표시 + return ( + + {data} + + ) + } else if (op === -1 && isExpectedOutput) { + // Expected Output에만 있는 텍스트는 초록색으로 표시 + return ( + + {data} + + ) + } else if (op === 1 && !isExpectedOutput) { + // Output에만 있는 텍스트는 빨간색으로 표시 + return ( + + {data} + + ) + } + return null + }) + } + + const renderText = (label: string, text: string, compareText?: string) => { + // Input 값은 항상 흰색으로 출력 + if (label === 'Input') { + return {text} + } + + // Expected Output 처리 + if (label === 'Expected Output') { + return getColoredText(compareText || '', text, true) + } + + // "Output" 처리 + return getColoredText(text, compareText || '', false) + } + return (

{label}


- +
+ {renderText(label, text, compareText)} +
) diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 1371b48e52..8c287eeb32 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -70,6 +70,7 @@ "cmdk": "^1.0.4", "date-fns": "^4.1.0", "dayjs": "^1.11.13", + "diff-match-patch-typescript": "^1.1.0", "dotenv": "^16.4.7", "embla-carousel-react": "8.5.1", "framer-motion": "^11.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef0f49bced..b07152ed2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -522,6 +522,9 @@ importers: dayjs: specifier: ^1.11.13 version: 1.11.13 + diff-match-patch-typescript: + specifier: ^1.1.0 + version: 1.1.0 dotenv: specifier: ^16.4.7 version: 16.4.7 @@ -6279,6 +6282,9 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff-match-patch-typescript@1.1.0: + resolution: {integrity: sha512-7WFVb3bRj5o+xRJtd1mLpbB9o19GE1FpY/v7z4GgMurmyaxZnuYdsEwn/K93ugn3nB+ce7KMn9hYjfAtXmUkVQ==} + diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -18149,6 +18155,8 @@ snapshots: didyoumean@1.2.2: {} + diff-match-patch-typescript@1.1.0: {} + diff@4.0.2: {} diff@5.2.0: {}