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/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..46994a7b --- /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..9b311aa4 --- /dev/null +++ b/src/interactions/fastCharge/useDialDrag.js @@ -0,0 +1,99 @@ +import { useState, useRef, useCallback, useEffect } from "react"; +import { clamp } from "@/common/utils.js"; +import throttleRaf from "@/common/throttleRaf.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); + + useEffect(() => { + function applyPointerMove(cursor) { + const currentAngle = getAngle(cursor, dialCenter.current); + + angleCache.current += getAngleDelta(prevAngle.current, currentAngle); + setAngle(angleCache.current); + prevAngle.current = currentAngle; + } + + const onPointerMove = throttleRaf((e) => { + if (!isDrag) return; + const { clientX, clientY } = e; + applyPointerMove({ x: clientX, y: clientY }); + }); + const onTouchMove = throttleRaf((e) => { + if (!isDrag) return; + const { clientX, clientY } = e.touches[0]; + applyPointerMove({ x: clientX, y: clientY }); + }); + function onPointerEnd() { + setIsDrag(false); + angleCache.current = clamp(angleCache.current, -Math.PI * 2, 0); + setAngle(angleCache.current); + } + + window.addEventListener("pointermove", onPointerMove); + window.addEventListener("pointerup", onPointerEnd); + window.addEventListener("pointercancel", onPointerEnd); + window.addEventListener("touchmove", onTouchMove); + window.addEventListener("touchend", onPointerEnd); + window.addEventListener("touchcancel", onPointerEnd); + return () => { + window.removeEventListener("pointermove", onPointerMove); + window.removeEventListener("pointerup", onPointerEnd); + window.removeEventListener("pointercancel", onPointerEnd); + window.removeEventListener("touchmove", onTouchMove); + window.removeEventListener("touchend", onPointerEnd); + window.removeEventListener("touchcancel", onPointerEnd); + }; + }, [isDrag]); + + 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/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"] + } };