Skip to content

Commit

Permalink
Merge pull request #30 from endless-horses/feature#27-result-page
Browse files Browse the repository at this point in the history
�타이어 결과물 페이지 구현 #27
  • Loading branch information
jinlee1703 authored Mar 5, 2024
2 parents c08fd79 + 02f14af commit b594bc2
Show file tree
Hide file tree
Showing 9 changed files with 329 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Main from '@pages/main';
import Production from '@pages/production';
import Result from '@pages/result';

function App() {
return (
Expand All @@ -10,6 +11,7 @@ function App() {
<Routes>
<Route path="/" element={<Main />} />
<Route path="/production" element={<Production />} />
<Route path="/results/:id" element={<Result />} />
</Routes>
</BrowserRouter>
</div>
Expand Down
21 changes: 21 additions & 0 deletions src/api/result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import axios, { AxiosResponse } from 'axios';
import { PostResultBody, ResultData, ResultInfo } from './result.types';

const instance = axios.create({
baseURL: `${process.env.REACT_APP_API_URL}/api/results`,
timeout: 15000,
withCredentials: true,
});

const responseBody = (response: AxiosResponse) => response.data;
// const responseStatus = (response: AxiosResponse) => response.status;

const resultRequests = {
get: <T>(url: string) => instance.get<T>(url).then(responseBody),
post: <T, B>(url: string, body: B) => instance.post<T>(url, body).then(responseBody),
};

export const Result = {
read: (id: string): Promise<ResultData> => resultRequests.get<ResultData>(id),
create: (body: PostResultBody): Promise<ResultInfo> => resultRequests.post<ResultInfo, PostResultBody>('', body),
};
26 changes: 26 additions & 0 deletions src/api/result.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { AccessoryData } from './accessory.types';
import { ColorData } from './color.types';
import { FontData } from './font.types';
import { PatternData } from './pattern.types';
import { WheelData } from './wheel.types';

export interface PostResultBody {
patternId: number;
wheelId: number;
fontId: number;
colorId: number;
accessoryId: number;
}

export interface ResultInfo {
id: string;
}

export interface ResultData {
id: string;
pattern: PatternData;
wheel: WheelData;
font: FontData;
color: ColorData;
accessory: AccessoryData;
}
27 changes: 26 additions & 1 deletion src/pages/production/button-group/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import Button from '@components/button';
import { Container } from './index.style';
import { useNavigate } from 'react-router';
import { Result } from '@api/result';
import { useRecoilState } from 'recoil';
import { selectedPatternState } from '@context/pattern';
import { selectedWheelState } from '@context/wheel';
import { selectedFontState } from '@context/font';
import { selectedColorState } from '@context/color';
import { selectedAccessoryState } from '@context/accessory';
import { ResultInfo } from '@api/result.types';

interface ButtonGroupProps {
step: number;
Expand All @@ -9,6 +17,11 @@ interface ButtonGroupProps {

function ButtonGroup(props: ButtonGroupProps) {
const navigate = useNavigate();
const [selectedPattern] = useRecoilState(selectedPatternState);
const [selectedWheel] = useRecoilState(selectedWheelState);
const [selectedFont] = useRecoilState(selectedFontState);
const [selectedColor] = useRecoilState(selectedColorState);
const [selectedAccessory] = useRecoilState(selectedAccessoryState);

return (
<Container className="button_group" position={props.position}>
Expand All @@ -20,7 +33,19 @@ function ButtonGroup(props: ButtonGroupProps) {
<Button
text={props.step < 3 ? '다음 단계로' : '결과 확인하기'}
onClick={() => {
if (props.step < 3) navigate(`/production?step=${props.step + 1}`);
if (props.step < 3) {
navigate(`/production?step=${props.step + 1}`);
} else {
Result.create({
patternId: selectedPattern + 1,
wheelId: selectedWheel + 1,
fontId: selectedFont + 1,
colorId: selectedColor + 1,
accessoryId: selectedAccessory + 1,
}).then((resultInfo: ResultInfo) => {
navigate(`/results/${resultInfo.id}`);
});
}
}}
/>
</Container>
Expand Down
50 changes: 50 additions & 0 deletions src/pages/result/index.style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import styled from 'styled-components';

export const Container = styled.div`
width: 100%;
height: 80vh;
`;

export const Content = styled.div`
height: 100%;
display: flex;
flex-direction: row;
.sumary {
padding: 0 100px;
width: 100%;
border-left: thin solid #969696;
.t1 {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
font-size: 30px;
font-weight: bold;
.small {
font-size: 20px;
text-align: right;
}
}
.t2 {
display: flex;
flex-direction: row;
justify-content: space-between;
font-size: 25px;
font-weight: bold;
}
.t3 {
display: flex;
flex-direction: row;
justify-content: space-between;
color: gray;
font-size: 20px;
font-weight: bold;
}
}
`;

export const ButtonGroup = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
`;
37 changes: 37 additions & 0 deletions src/pages/result/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Header from '@components/header';
import { ButtonGroup, Container, Content } from './index.style';
import Preview from './preview';
import Summary from './summary';
import Button from '@components/button';
import { useNavigate } from 'react-router';
import { useLocation } from 'react-router-dom';

function Result() {
const navigate = useNavigate();
const location = useLocation();

return (
<>
<Container>
<Header />
<Content>
<Preview />
<Summary />
</Content>
<hr className="sep" />
<ButtonGroup>
<Button inversion onClick={() => navigate('/')} text="처음으로" />
<Button
onClick={() => {
navigator.clipboard.writeText(`http://www.outfitoftire.co.kr${location.pathname}`);
alert('주소가 복사되었습니다.');
}}
text="공유하기"
/>
</ButtonGroup>
</Container>
</>
);
}

export default Result;
5 changes: 5 additions & 0 deletions src/pages/result/preview/index.style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import styled from 'styled-components';

export const Wrapper = styled.div`
background-color: white;
`;
73 changes: 73 additions & 0 deletions src/pages/result/preview/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useEffect, useRef, useState } from 'react';
import { Wrapper } from './index.style';
import * as THREE from 'three';
import liv from '@assets/pattern/liv.png';
import lug from '@assets/pattern/lug.png';
import livlug from '@assets/pattern/livlug.png';
import block from '@assets/pattern/block.png';
import v from '@assets/pattern/v.png';
import bedeching from '@assets/pattern/bedeching.png';
import { useParams } from 'react-router';
import { ResultData } from '@api/result.types';
import { Result } from '@api/result';

function Preview() {
const { id } = useParams();
const [result, setResult] = useState<ResultData>();
const mountRef = useRef<HTMLDivElement>(null);
const image = [liv, lug, livlug, block, v, bedeching];

useEffect(() => {
const getData = async () => {
const resultData = await Result.read(id ?? '');
setResult(resultData);
};
getData();
}, [id]);

useEffect(() => {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setClearColor(0xffffff);
renderer.setSize(700, 500);

if (mountRef.current) {
mountRef.current.appendChild(renderer.domElement);
}

const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load(image[result?.pattern.id ?? 0 - 1]);
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.center.set(0.5, 0.5);
texture.rotation = Math.PI / 2;
texture.offset.set(0, 0);
texture.repeat.set(2, 8);

const tireGeometry = new THREE.TorusGeometry(6, 2, 16, 150);
const tireMaterial = new THREE.MeshBasicMaterial({ map: texture });
const tire = new THREE.Mesh(tireGeometry, tireMaterial);
tire.rotation.y = Math.PI / 2;
scene.add(tire);

camera.position.z = 15;

const animate = function () {
requestAnimationFrame(animate);
tire.rotation.y += 0.01;
renderer.render(scene, camera);
};
animate();

// 컴포넌트 언마운트 시 클린업 로직
return () => {
if (mountRef.current) {
mountRef.current.removeChild(renderer.domElement);
}
};
}, [result]);

return <Wrapper ref={mountRef} className="preview" />;
}

export default Preview;
89 changes: 89 additions & 0 deletions src/pages/result/summary/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { ResultData } from '@api/result.types';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router';
import { Result } from '@api/result';

function formatCurrency(price: number) {
return '+ ' + price.toLocaleString('ko-KR') + '원';
}

function Summary() {
const { id } = useParams();
const [result, setResult] = useState<ResultData>();
const [sum, setSum] = useState(0);

useEffect(() => {
const getData = async () => {
if (id) {
const resultData = await Result.read(id);
setResult(resultData);
}
};
getData();
}, [id]);

useEffect(() => {
if (result) {
const total = result.pattern.price + result.wheel.price + result.font.price + result.accessory.price;
setSum(total);
}
}, [result]);

if (!result) {
return <div>Loading...</div>;
}

return (
<div className="sumary">
<div className="t1">
<p>견적 요약</p>
<div>
<p className="small">예상 견적 금액</p>
<p>{formatCurrency(sum)}</p>
</div>
</div>
<div>
<div className="t2">
<p>패턴 디자인</p>
<p>{formatCurrency(result?.pattern.price)}</p>
</div>
<div className="t3">
<p>{result?.pattern.name}</p>
<p>{result?.pattern.price}</p>
</div>
</div>
<div>
<div className="t2">
<p>휠 모양</p>
<p>{formatCurrency(result?.wheel.price)}</p>
</div>
<div className="t3">
<p>{result?.wheel.name}</p>
<p>{formatCurrency(result?.wheel.price)}</p>
</div>
</div>
<div>
<div className="t2">
<p>측면 디자인</p>
<p>{result?.font.price}</p>
</div>
<div className="t3">
<p>{result?.font.name}</p>
<p>{formatCurrency(result?.font.price)}</p>
</div>
</div>
<div>
<div className="t2">
<p>액세서리</p>
<p>{formatCurrency(result?.accessory.price)}</p>
</div>
<div className="t3">
<p>{result?.accessory.name}</p>
<p>{formatCurrency(result?.accessory.price)}</p>
</div>
</div>
</div>
);
}

export default Summary;

0 comments on commit b594bc2

Please sign in to comment.