sophia22001/gdg-4th-frontend-shoppingmall-answer
GitHub - sophia22001/gdg-4th-frontend-shoppingmall-answer
Contribute to sophia22001/gdg-4th-frontend-shoppingmall-answer development by creating an account on GitHub.
github.com
프로젝트 트랙 4기의 4차 미션 코스는 오프라인으로 만나 API 연결을 직접 해보는 시간을 가지는 것이었다.
프로젝트 트랙은 개발 초심자분들이 많아서 당일에 바로 연결을 해보기에 처음부터 모든 것을 하기에는 무리가 있다.
프론트엔드 공부를 쭉 했던 GDG 개발 학회 운영진으로서, 프론트엔드는 내가 디자인부터 모든 구현을 하고, 사람들에게 틀을 제공할 때는 api 코드 짜는 부분만 몇 개 비워두기로 했다.
프론트엔드 API 연결 세션 가이드를 사람들에게 안내하고 실습 중 모르는 부분이 있으면 질문을 받기로 하였다.
당일에 내가 짠 코드와 프로젝트 구조를 이해하고 api 연결까지 하는데는 많은 어려움이 있을 거라 생각했는데 그래도 8팀 중 1팀이 성공했다! (뿌듯.. )
벡엔드에서 했던 미션코스 내용과 맞추어야하기도 하고
로그인 기능도 구현해야 더 잘 동작하는 코드가 만들어지는 그런 조그마한 문제들이 남아있어서 완벽한 코드라고는 못하지만.. 4일정도에 다하려니까 좀 촉박했는데 그래도 나쁘지 않게 기본 기능은 다 구현했다.
그리고 이제는 API 관련 코드를 하나의 폴더와 파일에서 관리하게 만들어보는 걸 해볼 것이다.
그게 완벽하게는 안되는 줄 알았는데 어떻게 하면 되긴 하는 것 같아서 공부해봐야겠다.
연결 방식
벡엔드 미션 코스를 하시는 분들이 그동안 하셨던 미션을 배포해서 연결하는 것은 조금 무리여서, 통신을 하려면 같은 컴퓨터에서 동시에 실행하거나 같은 와이파이를 연결해서 통신을 시도해야한다.
나도 이번에 이런 연결방식은 처음 알았는데 정말 되니까 신기했다 ..
1. 연결하려는 벡엔드와 프론트엔드 노트북은 같은 학교 와이파이로 연결한다.
2. 와이파이 정보에서 IP 주소를 확인한다.
3. 프론트엔드는 .env 파일을 아래와 같이 수정한다.
VITE_API_URL="http://{IPADDRESS}:8080/api"
그리고 api > baseApi.js 파일에 Axios Client를 만들어서 이걸로 다른 곳에서 사용한다.
프로젝트 구조
전체 프로젝트 구조는 다음과 같다.
.
├── public
├── src
│ ├── App.css
│ ├── App.jsx
│ ├── api # 기본 Axios 클라이언트 세팅
│ ├── assets
│ ├── components
│ │ ├── BlueButton.jsx
│ │ ├── Contents
│ │ │ ├── ApiDefaultContents.jsx # 소비자 API 관리
│ │ │ ├── ...
│ │ ├── ...
│ │ ├── StockBox.jsx # 어드민 API 관리
│ │ └── navbar # 상단 헤더
│ ├── contexts
│ ├── index.css
│ ├── main.jsx
│ └── pages
│ ├── Admin.jsx # StockBox
│ ├── Home.jsx # Contents
│ └── Purchased.jsx
├── .env # 백엔드 IP 주소
└── vite.config.js
pages 와 components 폴더
유저 기능
1) pages > Home
1. 전체 상품 불러오기
1. 전체 상품 불러오기 : /items
<response>
[ { "id": 0, "itemName": "string", "quantity": 0, "price": 0 }, ... ]
- a. 등록된 상품이 한개 이상인 경우 전체 판매 목록을 보여준다.

- b. 등록된 상품이 한개도 없는 경우 GDG 로고가 뜨는 빈 화면을 보여준다.

useEffect(() => {
async function fetchAllItems() {
try {
const response = await baseApi.get("/items");
setAllItems(response.data);
console.log("전체 데이터 가져오기 성공");
} catch (error) {
console.error("전체 데이터 가져오기 실패:", error);
}
}
fetchAllItems();
}, []);
2. 특정 상품 검색하기
2. 특정 상품 검색하기 : /items/search
특정 상품명을 검색하면 그 하나의 상품이 검색된다.
검색어에 포함된 글자를 적으면 해당하는 상품들이 보여야하지만 벡엔드 미션코스에서 하나만 찾도록 구현되어서 지금은 하나만 나올 수 있게 했다.
<request>
{ "userName": "string", "position": "string", "itemName": "string" }
<response>
{ "id": 0, "itemName": "string", "quantity": 0, "price": 0 }

async function handleSearch(queryItemName) {
// ...
// 검색한 name을 서버에 전송
// 검색이 안되면 빈 배열을 반환
try {
const response = await baseApi.post("/items/search", {
userName,
position,
itemName,
});
const data = response.data;
if (isEmptyObject(data)) {
setSearchResult([]); // 검색 결과 없음
} else {
setSearchResult([data]); // 검색 결과 있음 (배열로 감싸기)
}
console.log("검색 반환 데이터: ", response.data);
console.log("검색어 보내기 성공");
} catch (error) {
console.log("Error POST data: ", error);
setSearchResult([]);
}
}
여기서 검색 데이터는 벡엔드 구현상 객체 데이터 딱 한 개만 나와서 프론트에서 배열로 감싸주고, 그걸 map()으로 활용하는 방식으로 했다.
3. 구매하기
3. 구매하기: /items/buy
장바구니 버튼을 누르고 장바구니 구매하기 버튼을 누르면 구매가 완료된다.
구매가 성공하면, 내 구매 내역 페이지로 이동한다.
<request>
{ "userName": "string", "position": "string", "items": [ { "itemName": "string" "count": 0 }, ... }
<response>
{ "totalPrice": 0, "items": [ { "itemName": "string", "price": 0, "count": 0 }, ... }
async function handleBuy() {
console.log("구매하기 버튼 클릭됨");
// ...
try {
const response = await baseApi.post("items/buy", buyItemData);
const data = response.data;
// 받는 데이터: { totalPrice, items: [ {itemName, price, count} ] }
alert("구매 완료!");
// 구매 내역을 localStorage 에 저장하기 - '내 구매 내역' 버튼 기능을 위한.
localStorage.setItem("lastBuyResult", JSON.stringify(data));
// 성공하면 구매완료 페이지로, 응답 데이터를 Cart로 넘기기
// useLocation.state 이용하기
nav("/purchased", { state: { buyResult: data } });
} catch (error) {
console.error("구매하기 실패:", error);
alert("구매 실패!");
}
}
localStorage 가 구매하기 기능 자체에서 필요한 건 아니다.
구매하기 기능을 끝내고 시간이 쪼금 남아서 내 구매 내역으로 바로 갈 수 있게 하는 버튼을 추가했다.
그런데 생각해보니 구매하기 api 요청을 성공해야 구매 내역 페이지로 갈 수 있게 해놨었다.
그러면 api 요청을 보내지 않고도 바로 구매내역 페이지로 이동하게 하려면 어떻게 해야할까?
-> api 요청을 보냈을 때 그 데이터를 localStorage 에 저장해놓는 것이다. 그렇게 하면 바로 내 구매내역 버튼을 클릭해도 저장된 데이터를 그대로 가져와 보여줄 수 있다.
여기서는 setItem으로 설정하고, 이걸 사용하는 곳에서는 아래와 같이 쓸 수 있다.
// location.state가 있는 경우
const stateBuyResult = location.state?.buyResult;
// location.state가 없는 경우 - localStorage에서 불러오기
const localBuyResult = localStorage.getItem("lastBuyResult");
하지만 어떠한 구매도 하지 않은 상태이면? (어떠한 검색 api 요청도 보내지 않은 상태)
예상대로 로컬 스토리지로 저장한 정보가 없으니까 아무것도 보여주지 못한다. -> 따로 예외처리를 해줘야 한다.
2) pages > Purchased
내 구매 내역 페이지에는 총 결제된 금액과 내가 구매한 상품의 목록이 뜬다.
장바구니 페이지도 있으면 좋겠지만 이것도 벡엔드 미션코스에서 구현하라고 하지 않았기 때문에 맞춰서 장바구니 페이지는 만들지 않고, 장바구니를 담고 그것을 리스트에 담아 구매 api를 호출하는 방식으로 구현했다.
예시로 가져온 페이지에서 실수로 0원으로 등록해서 보이지 않지만.. 실행해보면 만약 상품 가격을 10원으로 등록했으면 총 결제 금액이 99810원이라고 나올 것이다.
장바구니 Hook
장바구니는 커스텀 훅으로 구현했다.
훅에는 cartItems와 handleAddItem 을 전역으로 사용할 수 있게 넘겨주었다. '장바구니' 버튼을 누르면 handleAddItem이 클릭되어 cartItems의 목록에 추가된다.
import { useContext } from "react";
import { createContext } from "react";
const CartContext = createContext();
export const CartPrivider = ({ children }) => {
// 장바구니로 추가된 아이템들 리스트
const [cartItems, setCartItems] = useState([]);
// 장바구니 리스트로 추가하는 함수
const handleAddItem = item => {
setCartItems(prev => [...prev, item]);
};
return (
<CartContext.Provider value={{ cartItems, handleAddItem }}>
{children}
</CartContext.Provider>
);
};
// 커스텀 훅
export const useCart = () => useContext(CartContext);
useCart() 사용하기
Home 페이지에서 반영된 cartItems를 가져올 수 있게되고, 이 데이터를 POST로 벡엔드에 보내면 벡엔드에서 구매하는 기능을 처리해 '내 구매 내역' 페이지에 필요한 응답 데이터를 보내준다.
const { cartItems } = useCart();
const [items, setItems] = useState([{ itemName: "", count: 0 }]); // 여러 개의 상품처리 관리 상태
const buyItemData = { userName, position, items: cartItems }; // 구매 POST 보낼 데이터
// ----장바구니 구매하기 api ------------------------------
async function handleBuy() {
...
try {
const response = await baseApi.post("items/buy", buyItemData);
const data = response.data;
// 받는 데이터: { totalPrice, items: [ {itemName, price, count} ] }
alert("구매 완료!");
// 성공하면 구매완료 페이지로, 응답 데이터를 Cart로 넘기기
// useLocation.state 이용하기
nav("/purchased", { state: { buyResult: data } });
} catch (error) {
console.error("구매하기 실패:", error);
alert("구매 실패!");
}
}
관리자 기능
pages > Admin
헤더에 '관리자' 버튼을 클릭하면 관리자 페이지로 넘어간다.
관리자 페이지에는 상품을 새로 등록하는 기능, 어떤 등록된 상품의 재고를 추가하는 기능, 어떤 등록된 상품을 삭제하는 기능이 있다.
1. 상품 등록하기
1. 상품 등록하기 : /items/register
request
{ "userName": "string", "position": "ADMIN", "itemName": "string", "price": 0, "stock": 0 }
async function handleRegister() {
// ...
try {
const response = await baseApi.post("/items/register", {
userName,
position,
itemName,
price,
stock,
});
alert("상품이 등록되었습니다.");
} catch (error) {
console.log("Error POST data: ", error);
}
}
2. 상품 재고 추가하기
2. 상품 재고 추가하기 : /items/increase
request
{ "userName": "string", "position": "ADMIN", "itemName": "string", "count": 0 }
response{ "itemName": "string", "count": 0 }
async function handleIncrease() {
// ...
try {
const response = await baseApi.post("/items/increase", {
userName,
position,
itemName,
count,
});
alert("재고가 추가되었습니다.");
} catch (error) {
console.log("Error POST data: ", error);
}
}
3. 상품 삭제하기
3. 상품 삭제하기 : /items/delete
request
{ "userName": "string", "position": "ADMIN", "items": [ { "itemName": "string" }, ... }
response
{ "items": [ { "itemName": "string", "stock": 0 }, ... }
async function handleDelete() {
//...
try {
const response = await baseApi.post("/items/delete", {
userName,
position,
items: [{ itemName }],
});
alert("상품이 삭제되었습니다.");
} catch (error) {
console.log("Error POST data: ", error);
}
}
끝 !! 😚
'[Web-Front] React' 카테고리의 다른 글
SQLite로 내부 DB 연동하기 (3) | 2025.06.05 |
---|