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
+
## 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 (
+
+
+
+
+
+ 불편함 없이, 더 빠르게
+
+
+ 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 (
+
+
+
+
+
+ 나에게 맞게, 자유자재로
+
+
+ 새로워진 The new IONIQ 5의 유니버설 아일랜드는 어떤 모습일까요?
+
+
+ 유니버설 아일랜드를 드래그하여 이동시키고 스마트폰을 충전해보세요!
+
+
+
+
+
+
{
+ islandEventListener.onPointerDown(e);
+ interactCallback?.();
+ }}
+ >
+
+
+
+
+
+
{
+ 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"]
+ }
};