Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6335ed4
Initial plan
Copilot Mar 20, 2026
b5ba8d3
feat: replace react-chartjs-2/chart.js with custom @allcll/chart library
Copilot Mar 20, 2026
78ba46e
feat: add POC for uplot-react vs visx vs custom SVG chart bundle size…
Copilot Mar 21, 2026
392285b
Revert "feat: add POC for uplot-react vs visx vs custom SVG chart bun…
Copilot Mar 21, 2026
f2989ae
feat: replace @allcll/chart with visx in client for bundle size compa…
Copilot Mar 21, 2026
6836bf0
Revert "feat: replace @allcll/chart with visx in client for bundle si…
Copilot Mar 21, 2026
86f9211
refactor: optimize @allcll/chart for bundle size, performance, and re…
Copilot Mar 21, 2026
718e5ae
feat(@allcll/chart): entry animations, dark tooltip, legend toggle, l…
Copilot Mar 21, 2026
c5d346f
feat(@allcll/chart): value-based animations (sweep/grow/rise) and Rad…
Copilot Mar 22, 2026
78217b6
fix(DoughnutChart): handle full-circle arc when single slice has all …
Copilot Mar 23, 2026
e054b7c
refactor(chart): code quality and accessibility improvements
Copilot Mar 24, 2026
197b2c9
feat: visx 차트 라이브러리 + 코드 스플리팅 + Suspense fallback 구현
Copilot Mar 25, 2026
44b0a70
Merge remote-tracking branch 'origin/copilot/optimize-chart-library' …
Copilot Mar 25, 2026
fa40264
feat: chart.js 코드 스플리팅 + Suspense fallback + manualChunks 설정
Copilot Mar 25, 2026
bcb9978
Merge remote-tracking branch 'origin/copilot/optimize-chart-library' …
Copilot Mar 25, 2026
3feae00
feat: @allcll/charts 패키지 생성 및 클라이언트 lazy 임포트 업데이트
Copilot Mar 25, 2026
1f68876
chore: fix pnpm-lock
hyunwoo0081 Mar 25, 2026
df942fc
chore: delete img.png
hyunwoo0081 Mar 25, 2026
39f5ac7
fix: 메인 배너 이미지 width/height 속성 추가로 CLS 방지
Copilot Mar 25, 2026
87c1da7
chore: 폴더 정리 및 lazy 컴포넌트로 수정
hyunwoo0081 Apr 14, 2026
59b7c3f
fix: 빌드 오류 수정
hyunwoo0081 Apr 14, 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
Binary file removed img.png
Binary file not shown.
11 changes: 11 additions & 0 deletions packages/admin/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,16 @@ export default ({ mode }: ConfigEnv) => {
},
},
},
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('chart.js') || id.includes('react-chartjs-2')) {
return 'vendor-chartjs';
}
},
},
},
},
});
};
62 changes: 62 additions & 0 deletions packages/charts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 📊 @allcll/charts

`@allcll/charts`는 ALLCLL 서비스에서 사용되는 모든 차트 컴포넌트를 정의하는 공유 라이브러리입니다. `chart.js`와 `react-chartjs-2`를 기반으로 하며, 애플리케이션의 번들 사이즈 최적화를 위해 내부적으로 `React.lazy`와 `Suspense`를 활용합니다.

## 🚀 주요 특징

- **Lazy Loading 기본 탑재**: 모든 차트는 지연 로딩 처리되어 메인 번들 사이즈를 줄입니다.
- **자동 스켈레톤(Skeleton) 제공**: 차트 로딩 중에 발생할 수 있는 레이아웃 흔들림(CLS)을 방지합니다.
- **표준화된 API**: 일관된 인터페이스를 제공합니다.

## 📦 패키지 구조

```plaintext
charts/
├── src/
│ ├── components/ # 차트 로직 (Lazy, Suspense 로직 포함)
│ ├── skeletons/ # 로딩 상태 표시용 스켈레톤 UI
│ └── index.ts # 엔트리 포인트 (Public API)
└── package.json
```

## 🛠 사용 방법

```tsx
import { BarChart } from '@allcll/charts';

function MyPage() {
return <BarChart data={data} className="w-full" />;
}
```

## ⚙️ 애플리케이션 빌드 설정 (필수)

새로운 애플리케이션(Vite 기반)을 추가할 때, `@allcll/charts` 패키지의 라이브러리 코드를 단일 청크(`vendor-chartjs`)로 묶어 성능을 최적화하려면 각 애플리케이션의 `vite.config.ts`에 아래 설정을 추가하세요.

```typescript
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
// chart.js 라이브러리를 별도 청크로 분리
if (id.includes('chart.js') || id.includes('react-chartjs-2')) {
return 'vendor-chartjs';
}
},
},
},
},
});
```

## 💡 개발 가이드

- **새로운 차트 추가**: `src/components/`에 차트 구현체를, `src/skeletons/`에 대응되는 스켈레톤을 작성합니다.
- **엔트리 등록**: `src/components/LazyCharts.tsx`에서 `createLazyChart` 팩토리 함수를 사용하여 등록합니다.
- **코드 스플리팅**: 위와 같이 `vite.config.ts`에 `manualChunks` 설정을 추가하면, `@allcll/charts`의 Lazy 컴포넌트 호출 시 라이브러리 파일이 `vendor-chartjs`로 자동 분류됩니다.

## 🤝 기여하기

자세한 내용은 모노레포 루트의 [기여 가이드라인](../../CONTRIBUTING.md)을 참조하세요.
21 changes: 21 additions & 0 deletions packages/charts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@allcll/charts",
"private": true,
"version": "0.0.1",
"type": "module",
"sideEffects": false,
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"chart.js": "^4.5.0",
"react-chartjs-2": "^5.3.0"
},
"peerDependencies": {
"react": "^18.3.1"
},
"devDependencies": {
"@types/react": "^19.1.8"
}
}
25 changes: 25 additions & 0 deletions packages/charts/src/components/BarChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Bar } from 'react-chartjs-2';
import {
Chart as ChartJS,
BarElement,
CategoryScale,
LinearScale,
Tooltip,
Legend,
type ChartData,
type ChartOptions,
} from 'chart.js/auto';

ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend);

export interface BarChartProps {
data: ChartData<'bar'>;
options?: ChartOptions<'bar'>;
className?: string;
}

function BarChart({ data, options, className }: BarChartProps) {
return <Bar data={data} options={options} className={className} />;
}

export default BarChart;
16 changes: 16 additions & 0 deletions packages/charts/src/components/DoughnutChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Doughnut } from 'react-chartjs-2';
import { Chart as ChartJS, ArcElement, Tooltip, Legend, type ChartData, type ChartOptions } from 'chart.js/auto';

ChartJS.register(ArcElement, Tooltip, Legend);

export interface DoughnutChartProps {
data: ChartData<'doughnut'>;
options?: ChartOptions<'doughnut'>;
className?: string;
}

function DoughnutChart({ data, options, className }: DoughnutChartProps) {
return <Doughnut data={data} options={options} className={className} />;
}

export default DoughnutChart;
27 changes: 27 additions & 0 deletions packages/charts/src/components/LazyCharts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { lazy, Suspense, ComponentType } from 'react';
import { DoughnutChartSkeleton } from '../skeletons/DoughnutChartSkeleton';
import { BarChartSkeleton } from '../skeletons/BarChartSkeleton';
import { RadarChartSkeleton } from '../skeletons/RadarChartSkeleton';
import { MixedChartSkeleton } from '../skeletons/MixedChartSkeleton';

interface SkeletonProps {
className?: string;
height?: number;
}

const createLazyChart = <T extends object>(
importFn: () => Promise<{ default: ComponentType<T> }>,
Skeleton: ComponentType<SkeletonProps>,
) => {
const LazyComponent = lazy(importFn);
return (props: T & SkeletonProps) => (
<Suspense fallback={<Skeleton className={props.className} height={props.height} />}>
<LazyComponent {...props} />
</Suspense>
);
};

export const DoughnutChart = createLazyChart(() => import('./DoughnutChart'), DoughnutChartSkeleton);
export const BarChart = createLazyChart(() => import('./BarChart'), BarChartSkeleton);
export const RadarChart = createLazyChart(() => import('./RadarChart'), RadarChartSkeleton);
export const MixedChart = createLazyChart(() => import('./MixedChart'), MixedChartSkeleton);
31 changes: 31 additions & 0 deletions packages/charts/src/components/MixedChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Chart } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
type ChartData,
type ChartOptions,
type TooltipItem,
} from 'chart.js/auto';

ChartJS.register(CategoryScale, LinearScale, BarElement, PointElement, LineElement, Title, Tooltip, Legend);

export type MixedChartType = 'bar' | 'line';
export type MixedChartTooltipItem = TooltipItem<MixedChartType>;

export interface MixedChartProps {
data: ChartData<MixedChartType>;
options?: ChartOptions<MixedChartType>;
}

function MixedChart({ data, options }: MixedChartProps) {
return <Chart type="bar" data={data} options={options} />;
Comment on lines +27 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Forward sizing props to loaded MixedChart

StatisticsChart now passes height={288}, but this wrapper drops that prop when the lazy chunk resolves because MixedChart only accepts { data, options } and renders <Chart> without forwarding sizing props. This means the fallback skeleton honors the height while the real chart does not, causing avoidable layout shift and inconsistent chart sizing after load.

Useful? React with 👍 / 👎.

}

export default MixedChart;
26 changes: 26 additions & 0 deletions packages/charts/src/components/RadarChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Radar } from 'react-chartjs-2';
import {
Chart as ChartJS,
RadialLinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
Legend,
type ChartData,
type ChartOptions,
} from 'chart.js/auto';

ChartJS.register(RadialLinearScale, PointElement, LineElement, Filler, Tooltip, Legend);

export interface RadarChartProps {
data: ChartData<'radar'>;
options?: ChartOptions<'radar'>;
className?: string;
}

function RadarChart({ data, options, className }: RadarChartProps) {
return <Radar data={data} options={options} className={className} />;
}

export default RadarChart;
14 changes: 14 additions & 0 deletions packages/charts/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export { DoughnutChart, BarChart, RadarChart, MixedChart } from './components/LazyCharts';

export type { DoughnutChartProps } from './components/DoughnutChart';
export type { BarChartProps } from './components/BarChart';
export type { RadarChartProps } from './components/RadarChart';
export type { MixedChartProps, MixedChartType, MixedChartTooltipItem } from './components/MixedChart';

export {
DoughnutChartSkeleton,
BarChartSkeleton,
RadarChartSkeleton,
MixedChartSkeleton,
} from './skeletons/ChartSkeleton';
export type { MixedChartSkeletonProps } from './skeletons/MixedChartSkeleton';
16 changes: 16 additions & 0 deletions packages/charts/src/skeletons/BarChartSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function BarChartSkeleton({ className }: { className?: string }) {

Check warning on line 1 in packages/charts/src/skeletons/BarChartSkeleton.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=allcll_allcll-frontend&issues=AZ2KW2w2Wym3B0O6-RDV&open=AZ2KW2w2Wym3B0O6-RDV&pullRequest=341
return (
<div
className={`w-full bg-gray-100 animate-pulse rounded ${className ?? ''}`}
style={{ aspectRatio: '16 / 9', minHeight: 120 }}
aria-busy="true"
aria-label="차트 로딩 중"
>
<div className="flex items-end gap-2 h-full px-6 pb-6 pt-4">
{[55, 80, 40, 65].map(h => (
<div key={h} className="flex-1 bg-gray-300 rounded-t" style={{ height: `${h}%` }} />
))}
</div>
</div>
);
}
4 changes: 4 additions & 0 deletions packages/charts/src/skeletons/ChartSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { DoughnutChartSkeleton } from './DoughnutChartSkeleton';
export { BarChartSkeleton } from './BarChartSkeleton';
export { RadarChartSkeleton } from './RadarChartSkeleton';
export { MixedChartSkeleton } from './MixedChartSkeleton';
7 changes: 7 additions & 0 deletions packages/charts/src/skeletons/DoughnutChartSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function DoughnutChartSkeleton({ className }: { className?: string }) {

Check warning on line 1 in packages/charts/src/skeletons/DoughnutChartSkeleton.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=allcll_allcll-frontend&issues=AZ2KW2tzWym3B0O6-RDT&open=AZ2KW2tzWym3B0O6-RDT&pullRequest=341
return (
<div className={`flex items-center justify-center ${className ?? ''}`} aria-busy="true" aria-label="차트 로딩 중">
<div className="rounded-full bg-gray-200 animate-pulse" style={{ width: '100%', aspectRatio: '1' }} />
</div>
);
}
21 changes: 21 additions & 0 deletions packages/charts/src/skeletons/MixedChartSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export interface MixedChartSkeletonProps {
className?: string;
height?: number;
}

export function MixedChartSkeleton({ className, height = 384 }: MixedChartSkeletonProps) {

Check warning on line 6 in packages/charts/src/skeletons/MixedChartSkeleton.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=allcll_allcll-frontend&issues=AZ2KW2xDWym3B0O6-RDW&open=AZ2KW2xDWym3B0O6-RDW&pullRequest=341
return (
<div
className={`w-full bg-gray-100 animate-pulse rounded ${className ?? ''}`}
style={{ height }}
aria-busy="true"
aria-label="차트 로딩 중"
>
<div className="flex items-end gap-1 h-full px-12 pb-12 pt-4">
{Array.from({ length: 10 }, (_, i) => (
<div key={i} className="flex-1 bg-gray-300 rounded-t" style={{ height: `${25 + (i % 4) * 18}%` }} />
))}
</div>
</div>
);
}
10 changes: 10 additions & 0 deletions packages/charts/src/skeletons/RadarChartSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function RadarChartSkeleton({ className }: { className?: string }) {

Check warning on line 1 in packages/charts/src/skeletons/RadarChartSkeleton.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=allcll_allcll-frontend&issues=AZ2KW2woWym3B0O6-RDU&open=AZ2KW2woWym3B0O6-RDU&pullRequest=341
return (
<div className={`flex items-center justify-center ${className ?? ''}`} aria-busy="true" aria-label="차트 로딩 중">
<div
className="bg-gray-200 animate-pulse"
style={{ width: '100%', aspectRatio: '1', clipPath: 'polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%)' }}
/>
</div>
);
}
26 changes: 26 additions & 0 deletions packages/charts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
56 changes: 29 additions & 27 deletions packages/client/index.html
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />

<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/ci.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<head>
<meta charset="UTF-8" />

<title>올클(ALLCLL) | 세종대 수강신청 도우미</title>
<meta name="description" content="세종대 학생을 위한 수강신청 도우미 올클(ALLCLL). 시간표 관리, 과목 분석, 수강신청 연습, 실시간 여석 알림, 졸업요건 검사까지 한 번에 지원합니다." />
<meta name="keywords" content="세종대 수강 신청 여석 관심과목 연습" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/ci.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="preload" href="/font/woff2/Pretendard-Regular.subset.woff2" as="font" type="font/woff2" crossorigin />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<meta property="og:url" content="https://allcll.kr" />
<meta property="og:title" content="올클(ALLCLL) | 수강신청 도우미" />
<meta property="og:type" content="website" />
<meta property="og:image" content="/ogImg.png" />
<meta property="og:description" content="세종대 수강신청의 어려움 ALLCLL이 도와드릴게요" />
<meta name="google-adsense-account" content="ca-pub-3971325514117679" />
<script
async
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3971325514117679"
crossorigin="anonymous"
></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/app/main.tsx"></script>
</body>
</html>
<title>올클(ALLCLL) | 세종대 수강신청 도우미</title>
<meta name="description"
content="세종대 학생을 위한 수강신청 도우미 올클(ALLCLL). 시간표 관리, 과목 분석, 수강신청 연습, 실시간 여석 알림, 졸업요건 검사까지 한 번에 지원합니다." />
<meta name="keywords" content="세종대 수강 신청 여석 관심과목 연습" />

<meta property="og:url" content="https://allcll.kr" />
<meta property="og:title" content="올클(ALLCLL) | 수강신청 도우미" />
<meta property="og:type" content="website" />
<meta property="og:image" content="/ogImg.png" />
<meta property="og:description" content="세종대 수강신청의 어려움 ALLCLL이 도와드릴게요" />
<meta name="google-adsense-account" content="ca-pub-3971325514117679" />
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3971325514117679"
crossorigin="anonymous"></script>
</head>

<body>
<div id="root"></div>
<script type="module" src="/src/app/main.tsx"></script>
</body>

</html>
Loading
Loading