diff --git a/package-lock.json b/package-lock.json index e13d8d1..f6538b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "canvas-confetti": "^1.9.3", "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "styled-components": "^6.1.19" }, "devDependencies": { "@eslint/js": "^9.30.1", @@ -320,6 +321,27 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", @@ -1387,6 +1409,12 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1532,6 +1560,15 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001731", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", @@ -1629,11 +1666,30 @@ "node": ">= 8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -2274,7 +2330,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -2390,7 +2445,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -2435,6 +2489,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2552,6 +2612,12 @@ "semver": "bin/semver.js" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2579,7 +2645,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -2598,6 +2663,68 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-components": { + "version": "6.1.19", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz", + "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.49", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2628,6 +2755,12 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index aa3af7a..8f680c6 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "dependencies": { "canvas-confetti": "^1.9.3", "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "styled-components": "^6.1.19" }, "devDependencies": { "@eslint/js": "^9.30.1", diff --git a/src/components/CountdownExample.jsx b/src/components/CountdownExample.jsx index e057f19..469f794 100644 --- a/src/components/CountdownExample.jsx +++ b/src/components/CountdownExample.jsx @@ -1,31 +1,8 @@ -import { useState, useEffect } from "react"; +import { useCountdown } from "../hooks/useCountdown"; export const CountdownExample = () => { - const targetDate = new Date("2025-08-25T00:00:00"); - - const calculateTimeLeft = (targetDate) => { - const difference = targetDate.getTime() - new Date().getTime(); - - if (difference <= 0) { - return { days: 0, hours: 0, minutes: 0, seconds: 0 }; - } - - const days = Math.floor(difference / (1000 * 60 * 60 * 24)); - const hours = Math.floor((difference / (1000 * 60 * 60)) % 24); - const minutes = Math.floor((difference / (1000 * 60)) % 60); - const seconds = Math.floor((difference / 1000) % 60); - - return { days, hours, minutes, seconds }; - }; - const [timeLeft, setTimeLeft] = useState(() => calculateTimeLeft(targetDate)); - - useEffect(() => { - const timer = setInterval(() => { - setTimeLeft(calculateTimeLeft(targetDate)); - }, 1000); - - return () => clearInterval(timer); - }, [targetDate]); + const targetDate = new Date("2025-08-25T00:00:00"); + const timeLeft = useCountdown(targetDate); return (
diff --git a/src/components/WindowSizeExample.jsx b/src/components/WindowSizeExample.jsx index 8a4c768..637f074 100644 --- a/src/components/WindowSizeExample.jsx +++ b/src/components/WindowSizeExample.jsx @@ -1,32 +1,15 @@ -import { useEffect, useState } from "react"; +import { useWindowSize } from "../hooks/useWindowSize"; export const WindowSizeExample = () => { // 실습 1. 하단 코드를 useWindowSize (커스텀 훅으로 바꿔주세요!) - const [windowSize, setWindowSize] = useState({ - width: window.innerWidth, - height: window.innerHeight, - }); - - useEffect(() => { - const handleResize = () => { - setWindowSize({ - width: window.innerWidth, - height: window.innerHeight, - }); - }; - - window.addEventListener("resize", handleResize); - - return () => { - window.removeEventListener("resize", handleResize); - }; - }, []); + const {width, height} = useWindowSize(); + console.log(width); return (

useWindowSize 실습

-

화면 너비: {windowSize.width}px

-

화면 높이: {windowSize.height}px

+

화면 너비: {width}px

+

화면 높이: {height}px

); -}; +}; \ No newline at end of file diff --git a/src/components/YourOwnHookPage.jsx b/src/components/YourOwnHookPage.jsx index ee5502f..44784e7 100644 --- a/src/components/YourOwnHookPage.jsx +++ b/src/components/YourOwnHookPage.jsx @@ -1,11 +1,30 @@ -import { useSomething } from "../hooks/useSomething"; +import { useDice } from "../hooks/useDiceValue"; +import { styled } from "styled-components"; -export const YourOwnHookPage = () => { - // const { something... } = useSomething(); +const Dice = styled.div` + display: flex; + font-size: 100px; + width: 150px; + height: 150px; + border-radius: 20px; + border: 1px solid; + margin: 0 auto 30px auto; + justify-content: center; + background-color: #e7e7e7; +` + +export const RollDice = () => { + const { diceValue, rollDice, rolling } = useDice(); + // const { something... } = useSomething(); <- 이런 형태로 활용! // 하단 UI에 자유롭게 위에서 받아온 값들을 바인딩 해보세요~ return (
-

useSomething 실습

+ {diceValue} +
); }; diff --git a/src/hooks/useCountdown.js b/src/hooks/useCountdown.js index 19c04c3..aa2eceb 100644 --- a/src/hooks/useCountdown.js +++ b/src/hooks/useCountdown.js @@ -1,3 +1,33 @@ +import { useState, useEffect } from "react"; + export const useCountdown = (targetDate) => { + + + + const calculateTimeLeft = (targetDate) => { + const difference = targetDate.getTime() - new Date().getTime(); + + if (difference <= 0) { + return { days: 0, hours: 0, minutes: 0, seconds: 0 }; + } + + const days = Math.floor(difference / (1000 * 60 * 60 * 24)); + const hours = Math.floor((difference / (1000 * 60 * 60)) % 24); + const minutes = Math.floor((difference / (1000 * 60)) % 60); + const seconds = Math.floor((difference / 1000) % 60); + + return { days, hours, minutes, seconds }; + }; + const [timeLeft, setTimeLeft] = useState(() => calculateTimeLeft(targetDate)); + + useEffect(() => { + const timer = setInterval(() => { + setTimeLeft(calculateTimeLeft(targetDate)); + }, 1000); + + return () => clearInterval(timer); + }, [targetDate]); + return timeLeft; + }; diff --git a/src/hooks/useDiceValue.js b/src/hooks/useDiceValue.js new file mode 100644 index 0000000..eaccf52 --- /dev/null +++ b/src/hooks/useDiceValue.js @@ -0,0 +1,62 @@ +import { useState, useEffect } from "react"; //리액트에서 useState, useEffect 훅을 불러옴 + +//useDice라는 커스텀 훅 정의 시작 +export const useDice = () => { //컴포넌트에서 const { diceValue, rollDice } = useDice();처럼 쓸 수 있게 해주는 함수임 + + const [diceValue, setDiceValue] = useState(1); + //주사위 숫자를 저장할 상태(diceValue)를 만듦 + //diceValue: 현재 주사위 숫자, setDiceValue: 주사위 숫자를 변경하는 함수 + //초기 상태는 1로 설정함 + + const [rolling, setRolling] = useState(false); + //주사위가 굴러가고 있는지 여부를 나타내는 상태(rolling)를 만듦 + //초기 상태는 false, 클릭 시 true로 바뀌었다가 다 굴러가면 다시 false가 됨 + + + //사용자가 클릭시에 호출될 함수 + const rollDice = () => { + if (!rolling) setRolling(true); + //주사위가 이미 굴러가고 있으면(rolling이라면) 무시하고, + //아니라면 rolling을 true로 바꿔서 주사위 굴릴 때의 애니메이션을 시작함 + //rolling 도중 중복 클릭을 막기 위함 + }; + + useEffect(() => { + //useEffect는 rolling이 true로 바뀌었을 때만 실행됨 + //주사위를 굴리기 시작하면(=주사위가 굴러가고 있는 rolling 상태이면) 애니메이션 효과가 여기에서 실행됨 + + if (!rolling) return; //rolling이 false이면 아무것도 안함 + + //<<주사위를 굴릴 때 숫자가 반복적으로 바뀌는 애니메이션 지정>> + //0.1초마다 실행되는 반복 타이머 설정(숫자가 계속 바뀌게 하는 부분) + const interval = setInterval(() => { + const newValue = Math.floor(Math.random() * 6) +1; //1.newValue가 가질 수 있는 숫자들을 만듦 + setDiceValue(newValue) //2. 굴려서 나온 새로운 숫자(newValue)로 주사위 상태를 변경해줌 + }, 100); //3. 100ms(0.1초)마다 위 로직 반복 + + //<<위 애니메이션 멈추기>> + //일정 시간이 지나면 interval을 멈추기 위한 타이머 설정 + const timeout = setTimeout(() => { + clearInterval(interval);// 숫자 변경(위에서 정의한 interval) 중지 + setRolling(false); //rolling 상태 false로 되돌림 + }, 800); //1000ms(1초) 후 종료 + + //useEffect에서 컴포넌트의 상태가 바뀌었을 경우, 정리해주는 함수를 return 해야 함 + return () => { + clearInterval(interval); + clearTimeout(timeout); + }; + + }, [rolling]); + //rolling 상태가 바뀔 때마다 실행됨 + + //!!정리!! + // rolling이 true로 바뀌는 순간 useEffect가 작동하고, + //애니메이션 시작 -> 멈춤 -> 상태 복귀(false)가 일어나는 흐름 + + return { diceValue, rollDice, rolling }; + //diceValue(현재 숫자), rollDice(주사위 굴리는 함수), rolling(주사위 굴러가고 있는지에 대한 상태)를 외부에 전달하는 과정 + //컴포넌트에서 사용할 수 있도록 현재 상태와 함수들을 외부에 전달하는 것임 + //이제 jsx파일의 컴포넌트에서 useDice()로 불러와서 쓸 수 있게 됨 + +}; //훅 정의 종료 \ No newline at end of file diff --git a/src/hooks/useSomething.js b/src/hooks/useSomething.js deleted file mode 100644 index 48815a1..0000000 --- a/src/hooks/useSomething.js +++ /dev/null @@ -1,5 +0,0 @@ -export const useSomething = () => { - // 여러분의 use{Something}을 만들어주세요! - // 정답은 없습니다. 커스텀훅의 필요성을 스스로 느껴보세요. - // 아이디어를 생각하고, 스스로 구현하다가 어려우면 손 들어주세요! -}; diff --git a/src/hooks/useWindowSize.js b/src/hooks/useWindowSize.js index 28d9f7c..5d3a0fb 100644 --- a/src/hooks/useWindowSize.js +++ b/src/hooks/useWindowSize.js @@ -1 +1,27 @@ // 커스텀훅 코드를 작성해보세요! +import { useState, useEffect } from "react"; + +export const useWindowSize = () => { + + const [windowSize, setWindowSize] = useState({ + width: window.innerWidth, + height: window.innerHeight, + }); + + useEffect(() => { + const handleResize = () => { + setWindowSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + }; + + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + + return windowSize ; +}; diff --git a/src/pages/MainPage.jsx b/src/pages/MainPage.jsx index b362d51..90f57c4 100644 --- a/src/pages/MainPage.jsx +++ b/src/pages/MainPage.jsx @@ -3,7 +3,7 @@ import { ConfettiExample } from "../components/ConfettiExample"; import { CountdownExample } from "../components/CountdownExample"; import { WindowSizeExample } from "../components/WindowSizeExample"; import { FetchExample } from "../components/FetchExample"; -import { YourOwnHookPage } from "../components/YourOwnHookPage"; +import { RollDice } from "../components/YourOwnHookPage"; import "../styles/Main.styled.css"; export const MainPage = () => { @@ -20,7 +20,7 @@ export const MainPage = () => { case 4: return ; case 5: - return ; + return ; default: return null; } @@ -46,7 +46,7 @@ export const MainPage = () => {