Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 42 additions & 0 deletions src/components/progressbar/ProgressBar.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.track {
width: 100%;
max-width: 322px;
height: 20px;

border-radius: 9999px;
overflow: hidden;
position: relative;

background:
repeating-linear-gradient(
-45deg,
rgba(0, 0, 0, 0.04) 0px,
rgba(0, 0, 0, 0.04) 40px,
rgba(255, 255, 255, 0.7) 40px,
rgba(255, 255, 255, 0.7) 80px
),
Comment on lines +11 to +17

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

repeating-linear-gradientcolor-stop 순서가 올바르지 않아 그래디언트가 렌더링되지 않는 버그가 있습니다. color-stop의 위치 값은 오름차순으로 정렬되어야 합니다. 현재 코드에서는 40px 다음에 10px가 나와서 유효하지 않은 구문입니다.
의도하신 디자인이 줄무늬 패턴이라고 추측하여 아래와 같이 수정하는 것을 제안합니다. 이 코드는 40px 너비의 어두운 줄과 40px 너비의 밝은 줄이 반복되는 패턴을 만듭니다.

Suggested change
repeating-linear-gradient(
-45deg,
rgba(0, 0, 0, 0.04) 0px,
rgba(0, 0, 0, 0.04) 40px,
rgba(255, 255, 255, 0.7) 10px,
rgba(255, 255, 255, 0.7) 80px
),
repeating-linear-gradient(
-45deg,
rgba(0, 0, 0, 0.04) 0px,
rgba(0, 0, 0, 0.04) 40px,
rgba(255, 255, 255, 0.7) 40px,
rgba(255, 255, 255, 0.7) 80px
),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

수정 완료

var(--color-brand-secondary);
}

.fill {
height: 100%;
border-radius: inherit;
background: var(--color-brand-primary);

transition: width 800ms ease;
will-change: width;
}

@media (min-width: 744px) {
.track {
max-width: 566px;
height: 27px;
}
}

@media (min-width: 1024px) {
.track {
max-width: 1010px;
height: 27px;
}
}
100 changes: 100 additions & 0 deletions src/components/progressbar/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
'use client';

import { useEffect, useMemo, useRef } from 'react';
import styles from './ProgressBar.module.css';

export type ProgressBarProps = {
/** 0~1 (0.3 = 30%) */
value?: number;
/** done/total로도 계산 가능 */
done?: number;
total?: number;

/** 애니메이션 (default: true) */
animate?: boolean;

replayOnMount?: boolean;

durationMs?: number;

ariaLabel?: string;
className?: string;
};

function clamp01(n: number): number {
if (Number.isNaN(n)) return 0;
return Math.max(0, Math.min(1, n));
}

export default function ProgressBar({
value,
done,
total,
animate = true,
replayOnMount = true,
durationMs = 800,
ariaLabel = 'progress',
className,
}: ProgressBarProps) {
const ratio = useMemo(() => {
if (typeof value === 'number') return clamp01(value);
if (typeof done === 'number' && typeof total === 'number' && total > 0) {
return clamp01(done / total);
}
return 0;
}, [value, done, total]);

const targetPercent = useMemo(() => ratio * 100, [ratio]);

const fillRef = useRef<HTMLDivElement | null>(null);
const didMountRef = useRef<boolean>(false);
const rafRef = useRef<number | null>(null);

useEffect(() => {
const el = fillRef.current;
if (!el) return;

el.style.transition = animate ? `width ${durationMs}ms ease` : 'none';

if (!animate) {
el.style.width = `${targetPercent}%`;
didMountRef.current = true;
return;
}

const isFirstMount = !didMountRef.current;
didMountRef.current = true;

if (replayOnMount && isFirstMount) {
el.style.width = '0%';

if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
rafRef.current = requestAnimationFrame(() => {
el.style.width = `${targetPercent}%`;
});

return () => {
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
};
}

el.style.width = `${targetPercent}%`;
}, [animate, durationMs, replayOnMount, targetPercent]);
Comment on lines +53 to +82

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

현재 useEffect 훅은 애니메이션 지속 시간 설정, 애니메이션 비활성화 처리, 첫 마운트 시 애니메이션 재생 등 여러 책임을 가지고 있어 복잡성이 높습니다.
코드를 더 읽기 쉽고 유지보수하기 좋게 만들기 위해 이 useEffect를 여러 개의 작은 훅으로 분리하거나, 관련 로직을 useProgressAnimation과 같은 커스텀 훅으로 추출하는 것을 고려해 보세요.

예를 들어, --pb-duration CSS 변수를 설정하는 로직과 너비(width)를 업데이트하는 로직을 별도의 useEffect로 분리할 수 있습니다.

// 예시
useEffect(() => {
  const el = fillRef.current;
  if (!el) return;
  el.style.setProperty('--pb-duration', animate ? `${durationMs}ms` : '0ms');
}, [animate, durationMs]);

useEffect(() => {
  const el = fillRef.current;
  if (!el) return;

  // 너비 업데이트 및 애니메이션 로직
  // ...
}, [animate, replayOnMount, targetPercent]);


return (
<div
className={`${styles.track} ${className ?? ''}`}
role="progressbar"
aria-label={ariaLabel}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(targetPercent)}
>
<div
ref={fillRef}
className={styles.fill}
style={{ width: animate && replayOnMount ? '0%' : `${targetPercent}%` }}
/>
</div>
);
}
2 changes: 2 additions & 0 deletions src/components/progressbar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './ProgressBar';
export type { ProgressBarProps } from './ProgressBar';