From cf8135882db98faaec5aae4e923a907ef389b091 Mon Sep 17 00:00:00 2001 From: byeong Date: Fri, 5 Jun 2026 17:11:30 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=2004-Tabs=20=EA=B3=BC=EC=A0=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(=EC=B5=9C=EB=B3=91=EC=B0=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 04-Tabs/chan-byeong/tab-test.tsx | 16 ++++++ 04-Tabs/chan-byeong/tabs.tsx | 84 ++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 04-Tabs/chan-byeong/tab-test.tsx create mode 100644 04-Tabs/chan-byeong/tabs.tsx diff --git a/04-Tabs/chan-byeong/tab-test.tsx b/04-Tabs/chan-byeong/tab-test.tsx new file mode 100644 index 0000000..478c83f --- /dev/null +++ b/04-Tabs/chan-byeong/tab-test.tsx @@ -0,0 +1,16 @@ +import { Tabs } from "./tabs"; + +export function TabTest() { + return ( + + + + + + + 첫번째 + 두번째 + 세번째 + + ); +} diff --git a/04-Tabs/chan-byeong/tabs.tsx b/04-Tabs/chan-byeong/tabs.tsx new file mode 100644 index 0000000..e45a45d --- /dev/null +++ b/04-Tabs/chan-byeong/tabs.tsx @@ -0,0 +1,84 @@ +import { + createContext, + useContext, + useState, + type ReactNode, + type Dispatch, + type SetStateAction, +} from "react"; + +type TabContextType = { + value: string; + setValue: Dispatch>; +}; + +const TabContext = createContext(null); + +interface TabsProps { + children: ReactNode; + defaultValue: string; +} + +export function Tabs({ children, defaultValue }: TabsProps) { + const [activeTab, setActivetab] = useState(defaultValue); + + const tabCtx: TabContextType = { + value: activeTab, + setValue: setActivetab, + }; + + return {children}; +} + +function TabsList({ children, ...props }: { children: ReactNode }) { + return ( +
    + {children} +
+ ); +} + +function TabTrigger({ value, ...props }: { value: string }) { + const tabCtx = useContext(TabContext); + + if (!tabCtx) { + throw new Error("tab context must be called in TabsProvier"); + } + + const handleClick = () => { + tabCtx.setValue(value); + }; + + return ( + + ); +} + +function TabContent({ + value, + children, + ...props +}: { + value: string; + children: ReactNode; +}) { + const tabCtx = useContext(TabContext); + + if (!tabCtx) { + throw new Error("tab context must be called in TabsProvier"); + } + + if (tabCtx.value !== value) return null; + + return
{children}
; +} + +Tabs.List = TabsList; +Tabs.Trigger = TabTrigger; +Tabs.Content = TabContent; From 38314ce456c92e850f8552aa4bf3092e38f3d034 Mon Sep 17 00:00:00 2001 From: byeong Date: Fri, 26 Jun 2026 18:19:27 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=2007-stopwatch=20=EA=B3=BC=EC=A0=9C?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 07-Stopwatch/chan-byeong/index.tsx | 63 ++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 07-Stopwatch/chan-byeong/index.tsx diff --git a/07-Stopwatch/chan-byeong/index.tsx b/07-Stopwatch/chan-byeong/index.tsx new file mode 100644 index 0000000..6792bb2 --- /dev/null +++ b/07-Stopwatch/chan-byeong/index.tsx @@ -0,0 +1,63 @@ +import { useState, useRef, useEffect } from "react"; + +export default function Stopwatch() { + const [tick, setTick] = useState(0); + const [isRunning, setIsRunning] = useState(false); + const rAfRef = useRef(null); + + useEffect(() => { + if (!isRunning) return; + + let startTime = performance.now(); + const update = (now: number) => { + const duration = now - startTime; + startTime = now; + + setTick((prev) => prev + duration); + rAfRef.current = requestAnimationFrame(update); + }; + + rAfRef.current = requestAnimationFrame(update); + + return () => { + if (rAfRef.current !== null) { + globalThis.cancelAnimationFrame(rAfRef.current); + } + }; + }, [isRunning]); + + const handleStartButton = () => { + setIsRunning((prev) => !prev); + }; + + const handleResetButton = () => { + setTick(0); + setIsRunning(false); + }; + + return ( +
+

{formatTick(tick)}

+
+ + +
+
+ ); +} + +function formatTick(tick: number) { + const totalMs = Math.floor(tick); + const ms = totalMs % 1000; + const totalSeconds = Math.floor(totalMs / 1000); + const s = totalSeconds % 60; + const totalMinutes = Math.floor(totalSeconds / 60); + const m = totalMinutes % 60; + const h = Math.floor(totalMinutes / 60); + + return `${h > 0 ? h + "h : " : ""}${m > 0 ? m + "m : " : ""}${s}s : ${ms}ms`; +}