From a953fe96823cd69523ba7c3fdfc74a6554befbea Mon Sep 17 00:00:00 2001 From: harry Date: Fri, 16 Jan 2026 15:01:41 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20UI=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EB=A6=AC=EB=89=B4=EC=96=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wargame Stats: 새로운 깔끔한 디자인 적용 (JetBrains Mono 폰트) - Most Solved Categories: 동일한 스타일로 통일 - 테마 시스템 제거 (단일 디자인으로 통합) - 카테고리 이름 첫 글자 대문자 처리 - 홈페이지 테마 선택 UI 제거 - 테스트 코드 업데이트 --- .../utils/generateCategorySvg.test.ts | 67 +++++++---- src/pages/api/stats.ts | 10 +- src/pages/index.tsx | 16 +-- src/styles/Home.module.css | 56 +++++++++ src/utils/generateCategorySvg.ts | 113 +++++++++--------- src/utils/generateStatsSvg.ts | 90 +++++++------- 6 files changed, 210 insertions(+), 142 deletions(-) diff --git a/src/__tests__/utils/generateCategorySvg.test.ts b/src/__tests__/utils/generateCategorySvg.test.ts index cfa9715..81efe9d 100644 --- a/src/__tests__/utils/generateCategorySvg.test.ts +++ b/src/__tests__/utils/generateCategorySvg.test.ts @@ -20,21 +20,27 @@ describe('generateCategorySvg 유틸리티 함수 테스트', () => { expect(result).toContain(''); - // 카테고리 데이터가 SVG에 포함되어 있는지 확인 - expect(result).toContain('web: 2000'); - expect(result).toContain('pwnable: 1500'); - expect(result).toContain('reversing: 1000'); - expect(result).toContain('crypto: 500'); - + // 카테고리 이름이 SVG에 포함되어 있는지 확인 (첫 글자 대문자) + expect(result).toContain('Web'); + expect(result).toContain('Pwnable'); + expect(result).toContain('Reversing'); + expect(result).toContain('Crypto'); + + // 퍼센트가 표시되는지 확인 + expect(result).toContain('40%'); + expect(result).toContain('30%'); + expect(result).toContain('20%'); + expect(result).toContain('10%'); + // 색상이 적용되었는지 확인 expect(result).toContain('fill="#ff6b6b"'); expect(result).toContain('fill="#339af0"'); - + // 총점이 표시되는지 확인 expect(result).toContain('5000'); - + // 제목이 포함되어 있는지 확인 - expect(result).toContain('Most solved categories'); + expect(result).toContain('Most Solved Categories'); }); it('카테고리가 없는 경우 "No data"가 표시되어야 함', () => { @@ -67,34 +73,43 @@ describe('generateCategorySvg 유틸리티 함수 테스트', () => { const result = generateCategorySvg(mockStats); - // 상위 5개 카테고리는 범례에 표시되어야 함 - expect(result).toContain('web: 3000'); - expect(result).toContain('pwnable: 2500'); - expect(result).toContain('reversing: 2000'); - expect(result).toContain('crypto: 1500'); - expect(result).toContain('forensic: 1000'); - - // 6번째 카테고리인 misc는 범례에 없어야 함 - expect(result).not.toContain('misc: 500'); + // 상위 5개 카테고리는 범례에 표시되어야 함 (첫 글자 대문자) + expect(result).toContain('Web'); + expect(result).toContain('Pwnable'); + expect(result).toContain('Reversing'); + expect(result).toContain('Crypto'); + expect(result).toContain('Forensic'); + + // 상위 5개의 퍼센트가 표시되어야 함 + expect(result).toContain('29%'); + expect(result).toContain('24%'); + expect(result).toContain('19%'); + expect(result).toContain('14%'); + expect(result).toContain('10%'); + + // 6번째 카테고리인 misc는 범례에 표시되지만 차트에는 있음 + // (범례는 최대 5개만 표시) + const legendMatches = result.match(/class="legend-text">/g) || []; + expect(legendMatches.length).toBe(5); }); - it('매우 작은 카테고리(<5%)는 퍼센트 라벨이 표시되지 않아야 함', () => { + it('매우 작은 카테고리(<8%)는 퍼센트 라벨이 표시되지 않아야 함', () => { const mockStats: TCategoryStats = { nickname: 'weakness', total_score: 10000, categories: [ - { name: 'web', score: 9600, rank: 50, color: '#ff6b6b' }, // 96% - { name: 'misc', score: 400, rank: 300, color: '#20c997' } // 4% (5% 미만이므로 표시되지 않아야 함) + { name: 'web', score: 9300, rank: 50, color: '#ff6b6b' }, // 93% + { name: 'misc', score: 700, rank: 300, color: '#20c997' } // 7% (8% 미만이므로 표시되지 않아야 함) ] }; const result = generateCategorySvg(mockStats); - - // 퍼센트 라벨 확인 (4%는 표시되지 않아야 함) + + // 퍼센트 라벨 확인 (7%는 표시되지 않아야 함) const percentLabels = result.match(/class="percentage-label">(.*?)%

예시:

- - Dreamhack stats example diff --git a/src/styles/Home.module.css b/src/styles/Home.module.css index 977dd08..3c0ea26 100644 --- a/src/styles/Home.module.css +++ b/src/styles/Home.module.css @@ -149,4 +149,60 @@ .socialLink svg { width: 24px; height: 24px; +} + +.themeDescription { + margin: 0.5rem 0 1.5rem 0; + color: #666; + font-size: 0.95rem; +} + +.themeDescription code { + background: #f0f0f0; + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-size: 0.85rem; +} + +.themeGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 1.5rem; + margin-top: 1rem; +} + +.themeItem { + text-align: center; + padding: 1rem; + border: 1px solid #eaeaea; + border-radius: 8px; + background: #fafafa; +} + +.themeItem h4 { + margin: 0 0 0.3rem 0; + font-size: 1.1rem; + color: #333; +} + +.themeDesc { + margin: 0 0 1rem 0; + font-size: 0.85rem; + color: #666; +} + +.themeItem .codeSection { + margin-top: 1rem; + text-align: left; +} + +.themeItem .codeSection .code { + font-size: 0.75rem; + background: #fff; +} + +@media (max-width: 800px) { + .themeGrid { + grid-template-columns: 1fr; + } } \ No newline at end of file diff --git a/src/utils/generateCategorySvg.ts b/src/utils/generateCategorySvg.ts index e2d02eb..3bf34d3 100644 --- a/src/utils/generateCategorySvg.ts +++ b/src/utils/generateCategorySvg.ts @@ -4,65 +4,65 @@ export function generateCategorySvg(stats: TCategoryStats): string { // SVG 크기 및 차트 설정 const width = 390; const height = 190; - const pieCenterX = 290; // 원형 차트 중심 X (왼쪽) - const pieCenterY = 96; // 원형 차트 중심 Y - const radius = 88; // 원형 차트 반지름 - + const pieCenterX = 295; + const pieCenterY = 105; + const radius = 70; + // 전체 스코어 const totalScore = stats.total_score; - + // 카테고리 데이터 const categories = stats.categories; const totalCategoryScore = categories.reduce((sum, category) => sum + category.score, 0); - + // 원 차트 및 범례 생성 let pieChart = ''; let legends = ''; let currentAngle = 0; - const legendStartX = 25; // 범례 시작 X 위치 (오른쪽) - const legendStartY = 80; // 범례 시작 Y 위치 (제목 아래) - const legendItemHeight = 22; // 범례 항목 간 간격 - const maxLegendItems = 5; // 표시할 최대 범례 항목 수 - + const legendStartX = 25; + const legendStartY = 75; + const legendItemHeight = 22; + const maxLegendItems = 5; + // 카테고리가 없는 경우 if (categories.length === 0 || totalCategoryScore === 0) { - pieChart = ` + pieChart = ` No data`; legends = `No category data`; } else { // 상위 카테고리만 범례에 표시 (최대 maxLegendItems개) const topCategories = categories.slice(0, maxLegendItems); - + categories.forEach((category) => { const percentage = category.score / totalCategoryScore; const angleSize = percentage * 360; const endAngle = currentAngle + angleSize; - + // 원형 조각 경로 const startX = pieCenterX + radius * Math.cos((currentAngle - 90) * Math.PI / 180); const startY = pieCenterY + radius * Math.sin((currentAngle - 90) * Math.PI / 180); const endX = pieCenterX + radius * Math.cos((endAngle - 90) * Math.PI / 180); const endY = pieCenterY + radius * Math.sin((endAngle - 90) * Math.PI / 180); const largeArcFlag = angleSize > 180 ? 1 : 0; - + const path = ` - `; // 퍼센트 라벨 const midAngle = currentAngle + angleSize / 2; - const labelRadius = radius * 0.70; - const labelX = pieCenterX + labelRadius * Math.cos((midAngle - 90) * Math.PI / 180) - 3; + const labelRadius = radius * 0.65; + const labelX = pieCenterX + labelRadius * Math.cos((midAngle - 90) * Math.PI / 180); const labelY = pieCenterY + labelRadius * Math.sin((midAngle - 90) * Math.PI / 180); - const percentLabel = percentage > 0.05 ? // 5% 이상만 표시 - `${Math.round(percentage * 100)}%` : ''; - + const percentLabel = percentage > 0.08 ? + `${Math.round(percentage * 100)}%` : ''; + pieChart += path + percentLabel; currentAngle = endAngle; }); @@ -70,56 +70,59 @@ export function generateCategorySvg(stats: TCategoryStats): string { // 범례 생성 (상위 카테고리만) topCategories.forEach((category, index) => { const legendY = legendStartY + (index * legendItemHeight); + const percentage = Math.round((category.score / totalCategoryScore) * 100); + const capitalizedName = category.name.charAt(0).toUpperCase() + category.name.slice(1); legends += ` - - ${category.name}: ${category.score} + + ${capitalizedName} + ${percentage}% `; }); } - + // 중앙 총점 표시 (원형 차트 내부에) const centerCircle = ` - - Score - ${totalScore} - `; - - // 제목 (오른쪽 상단) - const header = ` - Dreamhack - Most solved categories + + Total + ${totalScore} `; // SVG 구조 반환 return ` - - - - - ${header} - + + + + + Most Solved Categories + + + + + - ${pieChart} - ${centerCircle} + ${legends} - + - ${legends} + ${pieChart} + ${centerCircle} + + `; } \ No newline at end of file diff --git a/src/utils/generateStatsSvg.ts b/src/utils/generateStatsSvg.ts index 1f856af..0c539fb 100644 --- a/src/utils/generateStatsSvg.ts +++ b/src/utils/generateStatsSvg.ts @@ -4,57 +4,51 @@ export function generateStatsSvg(stats: Tstats): string { return ` - - - - - - - - - - - - - - - - - - - - - - - TOP - ${stats.wargameRankPercentage}% - - Dreamhack wargame stats - ${stats.nickname} - - - - - Solved - ${stats.wargame_solved} - - Rank - ${stats.wargame_rank} - - Score - ${stats.wargame_score} - + + + + Dreamhack Wargame Stats + + + ${stats.nickname} + + + + TOP + ${stats.wargameRankPercentage}% + + + + + + Solved Challenges + ${stats.wargame_solved} + + + + + Rank + ${stats.wargame_rank} + + + + + Score + ${stats.wargame_score} + + - + `; -} \ No newline at end of file +}