From 514119f440cf69e4af798c99fd5fffe90e2ae4e7 Mon Sep 17 00:00:00 2001 From: HWAN0218 Date: Tue, 3 Feb 2026 18:38:42 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20progress=20bar=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../progressbar/ProgressBar.module.css | 43 +++++++ src/components/progressbar/ProgressBar.tsx | 107 ++++++++++++++++++ src/components/progressbar/index.ts | 2 + 3 files changed, 152 insertions(+) create mode 100644 src/components/progressbar/ProgressBar.module.css create mode 100644 src/components/progressbar/ProgressBar.tsx create mode 100644 src/components/progressbar/index.ts diff --git a/src/components/progressbar/ProgressBar.module.css b/src/components/progressbar/ProgressBar.module.css new file mode 100644 index 0000000..d3b7108 --- /dev/null +++ b/src/components/progressbar/ProgressBar.module.css @@ -0,0 +1,43 @@ +.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) 10px, + rgba(255, 255, 255, 0.7) 80px + ), + #eef2f7; +} + +.fill { + height: 100%; + border-radius: inherit; + background: #5189fa; + + + transition: width var(--pb-duration, 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; + } +} diff --git a/src/components/progressbar/ProgressBar.tsx b/src/components/progressbar/ProgressBar.tsx new file mode 100644 index 0000000..6c42e4b --- /dev/null +++ b/src/components/progressbar/ProgressBar.tsx @@ -0,0 +1,107 @@ +'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; + + + 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(null); + const didMountRef = useRef(false); + const rafRef = useRef(null); + + useEffect(() => { + const el = fillRef.current; + if (!el) return; + + + el.style.setProperty('--pb-duration', animate ? `${durationMs}ms` : '0ms'); + + + 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]); + + return ( +
+
+
+ ); +} diff --git a/src/components/progressbar/index.ts b/src/components/progressbar/index.ts new file mode 100644 index 0000000..26b78a6 --- /dev/null +++ b/src/components/progressbar/index.ts @@ -0,0 +1,2 @@ +export { default } from './ProgressBar'; +export type { ProgressBarProps } from './ProgressBar'; From 7f32d58131393b3418fd4c24aa0b43abda60fa0d Mon Sep 17 00:00:00 2001 From: HWAN0218 Date: Tue, 3 Feb 2026 19:11:04 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=EC=83=81=ED=83=9C=EB=B0=94=20?= =?UTF-8?q?=EB=B0=B0=EA=B2=BD=20=ED=8C=A8=ED=84=B4=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EB=B0=8F=20=EC=B4=88=EA=B8=B0=20=EB=A0=8C=EB=8D=94=20=EA=B9=9C?= =?UTF-8?q?=EB=B9=A1=EC=9E=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/progressbar/ProgressBar.module.css | 5 ++--- src/components/progressbar/ProgressBar.tsx | 13 +++---------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/components/progressbar/ProgressBar.module.css b/src/components/progressbar/ProgressBar.module.css index d3b7108..d4d0aa6 100644 --- a/src/components/progressbar/ProgressBar.module.css +++ b/src/components/progressbar/ProgressBar.module.css @@ -12,7 +12,7 @@ -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) 40px, rgba(255, 255, 255, 0.7) 80px ), #eef2f7; @@ -23,8 +23,7 @@ border-radius: inherit; background: #5189fa; - - transition: width var(--pb-duration, 800ms) ease; + transition: width 800ms ease; will-change: width; } diff --git a/src/components/progressbar/ProgressBar.tsx b/src/components/progressbar/ProgressBar.tsx index 6c42e4b..684ed5d 100644 --- a/src/components/progressbar/ProgressBar.tsx +++ b/src/components/progressbar/ProgressBar.tsx @@ -6,18 +6,15 @@ 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; @@ -57,9 +54,7 @@ export default function ProgressBar({ const el = fillRef.current; if (!el) return; - - el.style.setProperty('--pb-duration', animate ? `${durationMs}ms` : '0ms'); - + el.style.transition = animate ? `width ${durationMs}ms ease` : 'none'; if (!animate) { el.style.width = `${targetPercent}%`; @@ -70,7 +65,6 @@ export default function ProgressBar({ const isFirstMount = !didMountRef.current; didMountRef.current = true; - if (replayOnMount && isFirstMount) { el.style.width = '0%'; @@ -84,7 +78,6 @@ export default function ProgressBar({ }; } - // 값 변경 시 자연스럽게 이동 el.style.width = `${targetPercent}%`; }, [animate, durationMs, replayOnMount, targetPercent]); @@ -100,7 +93,7 @@ export default function ProgressBar({
); From d1cc7311d8e7828a04759bc043d6298c9a6fbc35 Mon Sep 17 00:00:00 2001 From: HWAN0218 Date: Tue, 3 Feb 2026 23:43:00 +0900 Subject: [PATCH 3/3] =?UTF-8?q?style:=20=EC=A7=84=ED=96=89=EB=B0=94=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=AC=20var=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/progressbar/ProgressBar.module.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/progressbar/ProgressBar.module.css b/src/components/progressbar/ProgressBar.module.css index d4d0aa6..f17d3a5 100644 --- a/src/components/progressbar/ProgressBar.module.css +++ b/src/components/progressbar/ProgressBar.module.css @@ -15,13 +15,13 @@ rgba(255, 255, 255, 0.7) 40px, rgba(255, 255, 255, 0.7) 80px ), - #eef2f7; + var(--color-brand-secondary); } .fill { height: 100%; border-radius: inherit; - background: #5189fa; + background: var(--color-brand-primary); transition: width 800ms ease; will-change: width;