diff --git a/README.md b/README.md index 7345cd29..6044e1d4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # Team6-AwesomeOrange +> 백엔드 레포는 여기로! => [백엔드 레포](https://github.com/softeerbootcamp4th/Team6-AwesomeOrange-BE) + ## Contributors -| [@lybell-art](https://github.com/lybell-art) | [@darkdulgi](https://github.com/darkdulgi) | [@ahra1221](https://github.com/blaxsior) | [@win-luck](https://github.com/win-luck) | +| [@lybell-art](https://github.com/lybell-art) | [@darkdulgi](https://github.com/darkdulgi) | [@blaxsior](https://github.com/blaxsior) | [@win-luck](https://github.com/win-luck) | |:---------------------------------------------------------:|:-------------------------------------------------------:|:-------------------------------------------------------:|:-----------------------------------------------------------------:| | | | | | | **Front-End** | **Front-End** | **Back-End** | **Back-End** | @@ -14,21 +16,45 @@ [Convention](https://github.com/softeerbootcamp4th/Team6-AwesomeOrange-BE/wiki/%08%5BTeam-Convention%5D) ## Plan & Design Link +[Plan & Design Link(Figma)](https://www.figma.com/design/XieJv765qFmU9dFuQAG7tQ/%EC%96%B4%EC%8D%B8%EC%98%A4%EB%A0%8C%EC%A7%80_Hand-off_%EC%B5%9C%EC%A2%85(07%2F24)?node-id=33-1157) + +## Schedule +**Front-End** + +| 1주차 | 공통 커스텀 훅 및 인터랙션 인터페이스 추가, 메인 페이지의 인트로, 헤더, 차량 기본정보, QnA, 푸터 구현 | +| --- | --- | +| 2주차 | 인터랙션 페이지, 인터랙션 모달, 각각의 인터랙션 구현 | +| 3주차 | 각각의 인터랙션 구현, 기대평 구현 | +| 4주차 | 선착순 이벤트 구현, 시간 남을 시 어드민 페이지(로그인, 이벤트목록) 구현 | +| 5주차 | 어드민 페이지(이벤트 등록수정, 이벤트 관리, 기대평 관리) 구현 및 리팩토링, 발표자료 제작 | + +**Back-End** + +| 1주차 | JPA Entity 구축, 배포 등 인프라 설정, 유저 로그인, 선착순 이벤트 프로토타입 구현 | +| --- | --- | +| 2주차 | 기대평, 어드민 시스템, 가중치 반영 추첨 구현 (+단위 테스트) | +| 3주차 | 선착순 이벤트 최적화, 서비스 확장성 개선, 테스트코드 작성 | +| 4주차 | 버그 수정, 부하 테스트 기반 서비스 최적화 | + +## Backlog +### Front-End +![image](https://github.com/user-attachments/assets/3fec291d-4aed-4f04-895b-7b2686aecc59) + +### Back-End +![image](https://github.com/user-attachments/assets/d7444775-cbad-48a2-a278-fd73368a1b6e) + +## ERD +image ## Tech Stack ### Front-End -- Javascript ES2020+ -- React -- Tailwindcss -- Vite -- Zustand + ### Back-End -- Spring Boot 3.2.2 -- Java 17 -- MySQL 8.0 -- Redis -- AWS EC2 -- AssertJ -- Docker + + +## Issue & TroubleShooting +[Issue & TroubleShooting](https://github.com/softeerbootcamp4th/Team6-AwesomeOrange-BE/wiki/%5BIssue-&-TroubleShooting%5D) + +## Project Archeitecture diff --git a/package.json b/package.json index 1952382b..3f08221e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --host", "build": "vite build && npm run build:server && node prerender.js", "build:client": "vite build", "build:server": "vite build --ssr src/main-server.jsx --outDir dist-ssg", diff --git a/public/font/HyundaiSansTextKROTFBold.otf b/public/font/HyundaiSansTextKROTFBold.otf new file mode 100644 index 00000000..ce21c761 Binary files /dev/null and b/public/font/HyundaiSansTextKROTFBold.otf differ diff --git a/public/font/HyundaiSansTextKROTFMedium.otf b/public/font/HyundaiSansTextKROTFMedium.otf new file mode 100644 index 00000000..17789b14 Binary files /dev/null and b/public/font/HyundaiSansTextKROTFMedium.otf differ diff --git a/public/font/HyundaiSansTextOffice-Medium.ttf b/public/font/HyundaiSansTextOffice-Medium.ttf deleted file mode 100644 index e7af0429..00000000 Binary files a/public/font/HyundaiSansTextOffice-Medium.ttf and /dev/null differ diff --git a/public/font/HyundaiSansTextOffice-Medium.woff b/public/font/HyundaiSansTextOffice-Medium.woff deleted file mode 100644 index 52bd3c4d..00000000 Binary files a/public/font/HyundaiSansTextOffice-Medium.woff and /dev/null differ diff --git a/src/assets/property1.svg b/src/assets/property1.svg new file mode 100644 index 00000000..affa9626 --- /dev/null +++ b/src/assets/property1.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/property2.svg b/src/assets/property2.svg new file mode 100644 index 00000000..06bb286c --- /dev/null +++ b/src/assets/property2.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/property3.svg b/src/assets/property3.svg new file mode 100644 index 00000000..431b4062 --- /dev/null +++ b/src/assets/property3.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/property4.svg b/src/assets/property4.svg new file mode 100644 index 00000000..b885cdea --- /dev/null +++ b/src/assets/property4.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/property5.svg b/src/assets/property5.svg new file mode 100644 index 00000000..d789efc4 --- /dev/null +++ b/src/assets/property5.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/common/useMountDragEvent.js b/src/common/useMountDragEvent.js new file mode 100644 index 00000000..61edb4b0 --- /dev/null +++ b/src/common/useMountDragEvent.js @@ -0,0 +1,32 @@ +import { useEffect } from "react"; +import throttleRaf from "@/common/throttleRaf.js"; + +function useMountDragEvent(dragging, dragEnd) { + useEffect(() => { + const onPointerMove = throttleRaf((e) => { + const { clientX, clientY } = e; + dragging({ x: clientX, y: clientY }); + }); + const onTouchMove = throttleRaf((e) => { + const { clientX, clientY } = e.touches[0]; + dragging({ x: clientX, y: clientY }); + }); + + window.addEventListener("pointermove", onPointerMove); + window.addEventListener("pointerup", dragEnd); + window.addEventListener("pointercancel", dragEnd); + window.addEventListener("touchmove", onTouchMove); + window.addEventListener("touchend", dragEnd); + window.addEventListener("touchcancel", dragEnd); + return () => { + window.removeEventListener("pointermove", onPointerMove); + window.removeEventListener("pointerup", dragEnd); + window.removeEventListener("pointercancel", dragEnd); + window.removeEventListener("touchmove", onTouchMove); + window.removeEventListener("touchend", dragEnd); + window.removeEventListener("touchcancel", dragEnd); + }; + }, [dragging, dragEnd]); +} + +export default useMountDragEvent; diff --git a/src/index.css b/src/index.css index cac3c975..a118db94 100644 --- a/src/index.css +++ b/src/index.css @@ -5,13 +5,21 @@ @font-face { font-family: "ds-digital"; src: url("/font/DS-DIGI.TTF") format("truetype"); + font-display: swap; } @font-face { font-family: "hdsans"; - src: - url("/font/HyundaiSansTextOffice-Medium.woff") format("woff"), - url("/font/HyundaiSansTextOffice-Medium.ttf") format("truetype"); + src: url("/font/HyundaiSansTextKROTFBold.otf") format("opentype"); + font-weight: bold; + font-display: swap; +} + +@font-face { + font-family: "hdsans"; + src: url("/font/HyundaiSansTextKROTFMedium.otf") format("opentype"); + font-weight: medium; + font-display: swap; } @layer base { @@ -24,4 +32,4 @@ .graphic-gradient { @apply bg-gradient-to-r from-[#3ED7BE] to-[#069AF8]; } -} \ No newline at end of file +} diff --git a/src/interactions/fastCharge/BatteryProgressBar.jsx b/src/interactions/fastCharge/BatteryProgressBar.jsx new file mode 100644 index 00000000..148afa9b --- /dev/null +++ b/src/interactions/fastCharge/BatteryProgressBar.jsx @@ -0,0 +1,30 @@ +import style from "./batteryStyle.module.css"; + +function getBatteryColor(progress) { + if (progress <= 50 / 330) return "bg-red-500"; + if (progress <= 190 / 330) return "bg-yellow-400"; + return "bg-blue-400"; +} + +function BatteryProgressBar({ progress }) { + const batteryColor = getBatteryColor(progress); + const batteryDynamicStyle = { + "--progress": progress, + }; + + return ( +
+
+
+
+
+ ); +} + +export default BatteryProgressBar; diff --git a/src/interactions/fastCharge/FastChargeInteraction.jsx b/src/interactions/fastCharge/FastChargeInteraction.jsx new file mode 100644 index 00000000..d125b6fe --- /dev/null +++ b/src/interactions/fastCharge/FastChargeInteraction.jsx @@ -0,0 +1,80 @@ +import { useImperativeHandle } from "react"; +import BatteryProgressBar from "./BatteryProgressBar.jsx"; +import orderIcon from "@/assets/property2.svg"; +import dialSvg from "./timer.svg"; +import useDialDrag from "./useDialDrag.js"; + +const MAX_MINUTE = 30; + +function getProgress(angle) { + const rawProgress = -angle / (Math.PI * 2); + if (rawProgress < 0) return 0; + if (rawProgress > 1) return 1; + return rawProgress; +} + +function FastChargeInteraction({ interactCallback, $ref }) { + const { + angle, + style: dialStyle, + ref: dialRef, + onPointerStart, + resetAngle, + } = useDialDrag(0); + + useImperativeHandle( + $ref, + () => ({ + reset() { + resetAngle(); + }, + }), + [resetAngle], + ); + const progress = getProgress(angle); + + return ( +
+
+ 2 +
+

+ 불편함 없이, 더 빠르게 +

+

+ The new IONIQ 5의 배터리를 충전하는 데 얼마만큼의 시간이 걸릴까요? +

+

+ 다이얼을 돌려 충전에 필요한 시간을 확인해보세요! +

+
+
+
+
+ +
+
+ 다이얼 { + onPointerStart(e); + interactCallback?.(); + }} + draggable="false" + /> +

+ + {Math.round(progress * MAX_MINUTE)} + + 분 +

+
+
+ ); +} + +export default FastChargeInteraction; diff --git a/src/interactions/fastCharge/batteryStyle.module.css b/src/interactions/fastCharge/batteryStyle.module.css new file mode 100644 index 00000000..2417d3e9 --- /dev/null +++ b/src/interactions/fastCharge/batteryStyle.module.css @@ -0,0 +1,47 @@ +.hull { + --bar-scale: var(--progress, 1); +} + +/* +768px 미만 : 48px ~ 256px +768px 이상 : 66px ~ 352px (명세에 나온 80px ~ 410px과 실제 산출된 디자인의 width가 다름) + +8px ~ 216px + +*/ + +.left { + width: 1.5rem; + transition: background-color 0.3s; +} + +.bar { + width: calc(100% - 2.5rem); + transform-origin: left center; + transform: scaleX( + calc((var(--progress, 1) * 208 + 8) / 216) + ); /* 8px ~ 216px */ + transition: background-color 0.3s; +} + +.right { + width: 1.5rem; + transform-origin: left center; + transform: translateX( + calc((1 - var(--progress, 1)) * -13rem) + ); /* -208px ~ 0px */ + transition: background-color 0.3s; +} + +@media (min-width: 768px) { + .bar { + transform: scaleX( + calc((var(--progress, 1) * 286 + 26) / 312) + ); /* 26px ~ 312px */ + } + .right { + transform: translateX( + calc((1 - var(--progress, 1)) * -17.875rem) + ); /* -286px ~ 0px */ + } +} diff --git a/src/interactions/fastCharge/timer.svg b/src/interactions/fastCharge/timer.svg new file mode 100644 index 00000000..ef8de03b --- /dev/null +++ b/src/interactions/fastCharge/timer.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/interactions/fastCharge/useDialDrag.js b/src/interactions/fastCharge/useDialDrag.js new file mode 100644 index 00000000..388cfac8 --- /dev/null +++ b/src/interactions/fastCharge/useDialDrag.js @@ -0,0 +1,75 @@ +import { useState, useRef, useCallback } from "react"; +import { clamp } from "@/common/utils.js"; +import useMountDragEvent from "@/common/useMountDragEvent.js"; + +function getAngle(pointer, center) { + const vx = pointer.x - center.x; + const vy = pointer.y - center.y; + return Math.atan2(vx, -vy); +} + +function getAngleDelta(prev, current) { + if (prev > Math.PI * 0.5 && current < -Math.PI * 0.5) + return current + Math.PI * 2 - prev; + if (prev < -Math.PI * 0.5 && current > Math.PI * 0.5) + return current - Math.PI * 2 - prev; + return current - prev; +} + +function useDialDrag() { + const [isDrag, setIsDrag] = useState(false); + const [angle, setAngle] = useState(0); + const dialRef = useRef(null); + const dialCenter = useRef({ x: 0, y: 0 }); + const prevAngle = useRef(0); + const angleCache = useRef(0); + + const applyPointerMove = useCallback( + (cursor) => { + if (!isDrag) return; + const currentAngle = getAngle(cursor, dialCenter.current); + angleCache.current += getAngleDelta(prevAngle.current, currentAngle); + setAngle(angleCache.current); + prevAngle.current = currentAngle; + }, + [isDrag], + ); + const onPointerEnd = useCallback(() => { + setIsDrag(false); + angleCache.current = clamp(angleCache.current, -Math.PI * 2, 0); + setAngle(angleCache.current); + }, []); + useMountDragEvent(applyPointerMove, onPointerEnd); + + function onPointerStart(e) { + if (dialRef.current === null) return; + + const { clientX, clientY } = e; + const boundRect = dialRef.current.getBoundingClientRect(); + dialCenter.current.x = boundRect.x + boundRect.width / 2; + dialCenter.current.y = boundRect.y + boundRect.height / 2; + prevAngle.current = getAngle( + { x: clientX, y: clientY }, + dialCenter.current, + ); + + setIsDrag(true); + } + + const resetAngle = useCallback(() => setAngle(0), []); + + const style = { + transform: `rotate(${angle}rad)`, + transition: isDrag ? "none" : "transform 0.5s", + }; + + return { + angle, + style, + ref: dialRef, + onPointerStart, + resetAngle, + }; +} + +export default useDialDrag; diff --git a/src/interactions/univasalIsland/Phone.jsx b/src/interactions/univasalIsland/Phone.jsx new file mode 100644 index 00000000..39e13572 --- /dev/null +++ b/src/interactions/univasalIsland/Phone.jsx @@ -0,0 +1,58 @@ +function Phone({ dynamicStyle, onPointerDown, isSnapped }) { + const staticStyle = `absolute flex justify-center items-center + left-[541px] top-[293px] w-[54px] h-[97px] + lg:left-[528px] lg:top-[185px] lg:w-[66px] lg:h-[118px] + xl:left-[516px] xl:top-[75px] xl:w-[77px] xl:h-[140px] + touch-none + `; + const phoneScreenFill = isSnapped ? "fill-green-700" : "fill-neutral-900"; + const lightningOpacity = isSnapped ? "opacity-100" : "opacity-0"; + + return ( +
+ + + + + + + +
+ ); +} + +export default Phone; diff --git a/src/interactions/univasalIsland/UnivasalIslandInteraction.jsx b/src/interactions/univasalIsland/UnivasalIslandInteraction.jsx new file mode 100644 index 00000000..1e68d81c --- /dev/null +++ b/src/interactions/univasalIsland/UnivasalIslandInteraction.jsx @@ -0,0 +1,108 @@ +import { useImperativeHandle } from "react"; +import Phone from "./Phone.jsx"; +import useIslandDrag from "./useIslandDrag.js"; + +import orderIcon from "@/assets/property3.svg"; +import seat from "./assets/seat.png"; +import univasalIsland1x from "./assets/univasalIsland@1x.png"; +import univasalIsland2x from "./assets/univasalIsland@2x.png"; +import univasalIslandLeg from "./assets/univasalIsland2.png"; + +function UnivasalIslandInteraction({ interactCallback, $ref }) { + const { + islandEventListener, + phoneEventListener, + islandStyle, + phoneStyle, + reset, + phoneSnapArea, + phoneIsSnapping, + } = useIslandDrag(); + + useImperativeHandle($ref, () => ({ reset }), [reset]); + + const seatHullStyle = `absolute w-[1200px] h-[800px] + bottom-[min(calc(100%-800px),-140px)] + lg:bottom-[min(calc(100%-900px),-170px)] + xl:bottom-[min(calc(100%-1000px),-200px)] + flex justify-center items-end select-none`; + + const seatStyle = `w-[317.44px] h-[501.88px] + lg:w-[385.46px] lg:h-[610.64px] + xl:w-[453.48px] xl:h-[718.4px]`; + + const univasalIslandStaticStyle = `w-[158.2px] h-[546px] + lg:w-[192.1px] lg:h-[663px] + xl:w-[226px] xl:h-[780px] + flex flex-col gap-2 cursor-pointer touch-none`; + + const snapAreaStyle = `absolute scale-50 + left-[21px] top-[40px] w-[54px] h-[97px] + lg:left-[25px] lg:top-[49px] lg:w-[66px] lg:h-[118px] + xl:left-[30px] xl:top-[56px] xl:w-[77px] xl:h-[140px] + `; + + return ( +
+
+ 3 +
+

+ 나에게 맞게, 자유자재로 +

+

+ 새로워진 The new IONIQ 5의 유니버설 아일랜드는 어떤 모습일까요? +

+

+ 유니버설 아일랜드를 드래그하여 이동시키고 스마트폰을 충전해보세요! +

+
+
+
+ left seat +
{ + islandEventListener.onPointerDown(e); + interactCallback?.(); + }} + > + univasal island + univasal island +
+
+ right seat + { + phoneEventListener.onPointerDown(e); + interactCallback?.(); + }} + /> +
+
+ ); +} + +export default UnivasalIslandInteraction; diff --git a/src/interactions/univasalIsland/assets/chargeMark.svg b/src/interactions/univasalIsland/assets/chargeMark.svg new file mode 100644 index 00000000..a10e8f9c --- /dev/null +++ b/src/interactions/univasalIsland/assets/chargeMark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/interactions/univasalIsland/assets/iphone.svg b/src/interactions/univasalIsland/assets/iphone.svg new file mode 100644 index 00000000..f03e7e57 --- /dev/null +++ b/src/interactions/univasalIsland/assets/iphone.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/interactions/univasalIsland/assets/seat.png b/src/interactions/univasalIsland/assets/seat.png new file mode 100644 index 00000000..1e9fd1eb Binary files /dev/null and b/src/interactions/univasalIsland/assets/seat.png differ diff --git a/src/interactions/univasalIsland/assets/univasalIsland2.png b/src/interactions/univasalIsland/assets/univasalIsland2.png new file mode 100644 index 00000000..5e5135c9 Binary files /dev/null and b/src/interactions/univasalIsland/assets/univasalIsland2.png differ diff --git a/src/interactions/univasalIsland/assets/univasalIsland@1x.png b/src/interactions/univasalIsland/assets/univasalIsland@1x.png new file mode 100644 index 00000000..3ce19393 Binary files /dev/null and b/src/interactions/univasalIsland/assets/univasalIsland@1x.png differ diff --git a/src/interactions/univasalIsland/assets/univasalIsland@2x.png b/src/interactions/univasalIsland/assets/univasalIsland@2x.png new file mode 100644 index 00000000..7a82758f Binary files /dev/null and b/src/interactions/univasalIsland/assets/univasalIsland@2x.png differ diff --git a/src/interactions/univasalIsland/useIslandDrag.js b/src/interactions/univasalIsland/useIslandDrag.js new file mode 100644 index 00000000..26eccc58 --- /dev/null +++ b/src/interactions/univasalIsland/useIslandDrag.js @@ -0,0 +1,144 @@ +import { useState, useRef, useMemo, useCallback } from "react"; +import { clamp } from "@/common/utils.js"; +import useMountDragEvent from "@/common/useMountDragEvent.js"; + +const PHONE_INITIAL_X = 150; +const PHONE_INITIAL_Y = 100; + +function aabbCheck(bound1, bound2) { + if (bound1.right < bound2.left) return false; + if (bound1.left > bound2.right) return false; + if (bound1.top > bound2.bottom) return false; + if (bound1.bottom < bound2.top) return false; + return true; +} + +function useIslandDrag() { + // island state + const islandIsDrag = useRef(false); + const islandStartMouseYPosition = useRef(0); + const islandStartPosition = useRef(0); + const [islandY, setIslandY] = useState(0); + + // phone state + const phoneIsDrag = useRef(false); + const phoneStartMousePosition = useRef({ x: 0, y: 0 }); + const phoneStartPosition = useRef({ x: PHONE_INITIAL_X, y: PHONE_INITIAL_Y }); + const [phoneIsSnapping, setPhoneIsSnapping] = useState(false); + const [phoneShouldSnapped, setPhoneShouldSnapped] = useState(false); + const [phoneX, setPhoneX] = useState(PHONE_INITIAL_X); + const [phoneY, setPhoneY] = useState(PHONE_INITIAL_Y); + + // phone snap area + const phoneSnapArea = useRef(null); + + // mount island drag event + const islandOnDragStart = function (e) { + islandIsDrag.current = true; + setPhoneShouldSnapped(false); + islandStartMouseYPosition.current = e.clientY; + islandStartPosition.current = islandY; + }; + const islandOnDragging = useCallback( + function ({ y: mouseY }) { + if (!islandIsDrag.current) return; + const rawY = + mouseY - + islandStartMouseYPosition.current + + islandStartPosition.current; + const y = clamp(rawY, -50, 50); + + setIslandY(y); + + if (phoneIsSnapping) { + setPhoneX(0); + setPhoneY(y); + } + }, + [phoneIsSnapping], + ); + const islandOnDragEnd = useCallback(() => { + if (!islandIsDrag.current) return; + islandIsDrag.current = false; + }, []); + useMountDragEvent(islandOnDragging, islandOnDragEnd); + + // mount phone drag event + const phoneOnDragStart = function (e) { + phoneIsDrag.current = true; + setPhoneShouldSnapped(false); + phoneStartMousePosition.current = { x: e.clientX, y: e.clientY }; + phoneStartPosition.current = { x: phoneX, y: phoneY }; + }; + const phoneOnDragging = useCallback(function ({ x: mouseX, y: mouseY }) { + if (!phoneIsDrag.current) return; + + const x = + mouseX - phoneStartMousePosition.current.x + phoneStartPosition.current.x; + const y = + mouseY - phoneStartMousePosition.current.y + phoneStartPosition.current.y; + + setPhoneX(x); + setPhoneY(y); + }, []); + const phoneOnDragEnd = useCallback( + (e) => { + if (!phoneIsDrag.current) return; + + phoneIsDrag.current = false; + const isSnapped = aabbCheck( + e.target.getBoundingClientRect(), + phoneSnapArea.current.getBoundingClientRect(), + ); + setPhoneIsSnapping(isSnapped); + if (isSnapped) { + setPhoneX(0); + setPhoneY(islandY); + setPhoneShouldSnapped(true); + } + }, + [islandY, phoneIsDrag], + ); + useMountDragEvent(phoneOnDragging, phoneOnDragEnd); + + // reset function interface + const reset = useCallback(() => { + islandStartMouseYPosition.current = 0; + phoneStartMousePosition.current = { x: 0, y: 0 }; + islandStartPosition.current = 0; + phoneStartPosition.current = { x: PHONE_INITIAL_X, y: PHONE_INITIAL_Y }; + islandIsDrag.current = false; + phoneIsDrag.current = false; + setIslandY(0); + setPhoneIsSnapping(false); + setPhoneShouldSnapped(false); + setPhoneX(PHONE_INITIAL_X); + setPhoneY(PHONE_INITIAL_Y); + }, []); + + // island style + const islandStyle = useMemo( + () => ({ + transform: `translateY(${islandY}px)`, + }), + [islandY], + ); + + // phone style은 상당히 많은 state 종속성을 가지고 있으므로 useMemo가 의미가 없음 + const phoneStyle = { + transform: `translate(${phoneX}px, ${phoneY}px)`, + transition: phoneShouldSnapped ? "transform 0.5s" : "none", + }; + + return { + reset, + islandStyle, + phoneStyle, + phoneIsSnapping, + islandEventListener: { onPointerDown: islandOnDragStart }, + phoneEventListener: { onPointerDown: phoneOnDragStart }, + phoneSnapArea, + }; +} + +export default useIslandDrag; diff --git a/tailwind.redefine.js b/tailwind.redefine.js index 6e575d04..b86db9b6 100644 --- a/tailwind.redefine.js +++ b/tailwind.redefine.js @@ -57,4 +57,18 @@ export default { }, black: "#0D0D0D", }, + fontSize: { + "detail-s": ["10px", "13px"], + "detail-m": ["11px", "14px"], + "detail-l": ["12px", "16px"], + "body-s": ["14px", "20px"], + "body-m": ["16px", "24px"], + "body-l": ["22px", "32px"], + "title-s": ["24px", "36px"], + "title-m": ["28px", "40px"], + "title-l": ["32px", "44px"], + "head-s": ["36px", "52px"], + "head-m": ["45px", "64px"], + "head-l": ["57px", "80px"] + } };