Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ryong9rrr 용상윤 #4

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
4 changes: 3 additions & 1 deletion jsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"compilerOptions": {
"target": "es6"
}
},
"exclude": ["node_modules"],
"include": ["src"]
}
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"axios": "^1.1.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.6.0",
"react-router-dom": "^6.4.2",
"react-scripts": "5.0.1",
"styled-components": "^5.3.6",
Expand Down
12 changes: 11 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import Layout from "./components/Layout";
import { CarContextProvider } from "./contexts/carContext";
import Router from "./routes";

function App() {
return <h1>Hello React!</h1>;
return (
<Layout>
<CarContextProvider>
<Router />
</CarContextProvider>
</Layout>
);
}

export default App;
12 changes: 12 additions & 0 deletions src/apis/car.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Api from "./core";

class CarApi extends Api {
async getCars(query = {}) {
const response = await this.baseInstance("/cars", { params: query });
return response.data.payload;
}
}

const carApi = new CarApi();

export default carApi;
22 changes: 22 additions & 0 deletions src/apis/core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import axios from "axios";

const { REACT_APP_ALTIMOBILITY_API_END_POINT } = process.env;

const createInstance = (url, config = {}) => {
return axios.create({
baseURL: url,
headers: {
"Content-Type": "application/json",
},
...config,
timeout: 2000,
});
};

export default class Api {
API_END_POINT = REACT_APP_ALTIMOBILITY_API_END_POINT;
constructor() {
const instance = createInstance(this.API_END_POINT);
this.baseInstance = instance;
}
}
51 changes: 51 additions & 0 deletions src/components/CarItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from "react";
import { COLOR_PALETTE, FONT_SIZE } from "@/styles/constants";
import { formattedFuelType, formatNumber, formattedSegment } from "@/utils";
import styled from "styled-components";

const CarItem = ({ car, onClick }) => {
return (
<Container onClick={onClick}>
<CarInfo>
<CarInfoTitle>
<h3>{car.attribute.brand}</h3>
<h4>{car.attribute.name}</h4>
</CarInfoTitle>
<Text>
{formattedSegment[car.attribute.segment]} / {formattedFuelType[car.attribute.fuelType]}
</Text>
<Text>월 {formatNumber(car.amount)} 원 부터</Text>
</CarInfo>
<img width="30%" src={car.attribute.imageUrl} alt={car.attribute.name} />
</Container>
);
};

export default CarItem;

const Container = styled.li`
padding: 16px 32px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
&:hover {
background-color: ${COLOR_PALETTE.BACKGROUND_HOVER};
}
`;

const CarInfo = styled.div`
display: flex;
flex-direction: column;
`;

const CarInfoTitle = styled.div`
display: flex;
flex-direction: column;
margin-bottom: 8px;
font-weight: 600;
`;

const Text = styled.span`
font-size: ${FONT_SIZE.SMALL};
`;
57 changes: 57 additions & 0 deletions src/components/CarList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import styled from "styled-components";
import { useNavigate, useSearchParams } from "react-router-dom";
import CarItem from "./CarItem";
import carApi from "../apis/car";
import usePromise from "../hooks/usePromise";
import { useMemo } from "react";
import { useCar } from "@/contexts/carContext";

const CarList = () => {
const { setCar } = useCar();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const fuelType = useMemo(() => searchParams.get("fuelType"), [searchParams]);
const segment = useMemo(() => searchParams.get("segment"), [searchParams]);
const query = {
fuelType,
segment,
};

const handleClickCarList = (car) => {
setCar(car);
navigate("detail");
};

const [loading, cars, error] = usePromise(() => {
return carApi.getCars(query);
}, [fuelType, segment]);

if (loading) {
return <TextBox>불러오는 중</TextBox>;
}

if (error) {
return <TextBox>차량을 불러오지 못했습니다. 다시 시도해주세요.</TextBox>;
}

if (!cars || cars.length === 0) {
return <TextBox>차량이 없습니다.</TextBox>;
}

return (
<>
{cars.map((car) => (
<CarItem key={car.id} car={car} onClick={handleClickCarList.bind(this, car)} />
))}
</>
);
};

export default CarList;

const TextBox = styled.div`
flex: 1;
display: flex;
justify-content: center;
align-items: center;
`;
49 changes: 49 additions & 0 deletions src/components/Header.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from "react";
import { BiArrowBack } from "react-icons/bi";
import { COLOR_PALETTE, FONT_SIZE } from "@/styles/constants";
import styled from "styled-components";
import { useNavigate } from "react-router-dom";

const Header = ({ title, hasBack = false }) => {
const navigate = useNavigate();
const handleClickBack = () => {
navigate(-1);
};

return (
<Container>
{hasBack && (
<BackButton onClick={handleClickBack}>
<BiArrowBack />
</BackButton>
)}
{title}
</Container>
);
};

export default Header;

const Container = styled.header`
position: relative;
display: flex;
justify-content: center;
align-items: center;
border-bottom: 2px solid ${COLOR_PALETTE.BLACK};
padding: 16px;
font-size: ${FONT_SIZE.LARGE};
font-weight: 600;
text-align: center;
`;

const BackButton = styled.div`
position: absolute;
top: 0;
left: 0;
width: 54px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
`;
19 changes: 19 additions & 0 deletions src/components/Layout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";
import styled from "styled-components";

const Layout = ({ children }) => {
return <Container>{children}</Container>;
};

export default Layout;

const Container = styled.div`
max-width: 450px;
min-width: 360px;
height: 100vh;
box-sizing: border-box;
margin: 0 auto;
background-color: #fafafa;
display: flex;
flex-direction: column;
`;
23 changes: 23 additions & 0 deletions src/components/TagItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react";
import { COLOR_PALETTE } from "@/styles/constants";
import styled from "styled-components";
import { formattedAll } from "../utils/index";

const TagItem = ({ tag, isClicked, onClick }) => {
return (
<TagItemContainer isClicked={isClicked} onClick={onClick}>
{formattedAll[tag.segment || tag.fuelType]}
</TagItemContainer>
);
};

export default TagItem;

export const TagItemContainer = styled.li`
padding: 4px 16px;
border-radius: 16px;
background-color: ${({ isClicked }) => (isClicked ? COLOR_PALETTE.BLACK : COLOR_PALETTE.GRAY)};
color: ${({ isClicked }) => (isClicked ? "white" : COLOR_PALETTE.BLACK)};
font-weight: 600;
cursor: pointer;
`;
63 changes: 63 additions & 0 deletions src/components/TagList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from "react";
import { COLOR_PALETTE } from "@/styles/constants";
import TagItem, { TagItemContainer } from "./TagItem";
import styled from "styled-components";
import { useSearchParams } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { makeQueryString } from "../utils/index";

const SEGMENT = "SEGMENT";
const FUEL = "FUEL";

const tags = [
{ type: SEGMENT, segment: "E" },
{ type: SEGMENT, segment: "D" },
{ type: SEGMENT, segment: "C" },
{ type: FUEL, fuelType: "ev" },
];

const TagList = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const fuelType = searchParams.get("fuelType");
const segment = searchParams.get("segment");

const handleClickTag = (tag) => {
const currentQuery = {};
if (fuelType) currentQuery.fuelType = fuelType;
if (segment) currentQuery.segment = segment;
if (tag.type === SEGMENT && segment !== tag.segment) {
currentQuery.segment = tag.segment;
}
if (tag.type === FUEL && fuelType !== tag.fuelType) {
currentQuery.fuelType = tag.fuelType;
}

navigate(`?${makeQueryString(currentQuery)}`);
};

return (
<Container>
<TagItemContainer isClicked={!fuelType && !segment} onClick={() => navigate("/")}>
전체
</TagItemContainer>
{tags.map((tag) => (
<TagItem
key={tag.segment || tag.fuelType}
isClicked={fuelType === tag.fuelType || segment === tag.segment}
tag={tag}
onClick={handleClickTag.bind(this, tag)}
/>
))}
</Container>
);
};

export default TagList;

const Container = styled.ul`
padding: 8px;
display: flex;
gap: 4px;
border-bottom: 2px solid ${COLOR_PALETTE.BLACK};
`;
23 changes: 23 additions & 0 deletions src/contexts/carContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, { useState } from "react";
import { useContext } from "react";
import { useMemo } from "react";

const CarContext = React.createContext({
car: null,
});

export const useCar = () => useContext(CarContext);

export const CarContextProvider = ({ children }) => {
const [car, setCar] = useState(null);

const value = useMemo(
() => ({
car,
setCar,
}),
[car],
);

return <CarContext.Provider value={value}>{children}</CarContext.Provider>;
};
1 change: 1 addition & 0 deletions src/hooks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as usePromise } from "./usePromise";
Loading