Skip to content
5,006 changes: 5,006 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions src/apis/query/useGetStockList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useEffect, useState } from "react";
import { api } from "../../lib/api";

interface StockPriceDto {
stockCode: string;
stockName: string;
price: number;
change: number;
changeRate: number;
dataStatus: "NORMAL" | "ERROR";
}

interface StockListResponse {
stockPriceDtoList: StockPriceDto[];
dataStatus: "NORMAL" | "ERROR";
}

export const useGetStockList = () => {
const [stockList, setStockList] = useState<StockPriceDto[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);

const getStockList = async () => {
try {
setIsLoading(true);
const { data } = await api.get<StockListResponse>("/stocks");

console.log("주식 응답:", data);

if (data.dataStatus !== "NORMAL") {
throw new Error("데이터 상태 비정상");
}

setStockList(data.stockPriceDtoList);
setIsError(false);
} catch (error) {
console.error("주식 목록 조회 실패:", error);
setIsError(true);
} finally {
setIsLoading(false);
}
};

useEffect(() => {
getStockList();
}, []);

return {
stockList,
isLoading,
isError,
getStockList,
};
};
43 changes: 43 additions & 0 deletions src/apis/query/usePostOrder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useMutation } from "@tanstack/react-query";
import { api } from "../../lib/api";

type OrderType = "BUY" | "SELL";

interface PostOrderRequest {
stockCode: string;
orderType: OrderType;
price: number;
quantity: number;
}

interface PostOrderResponse {
orderId: number;
stockCode: string;
orderType: OrderType;
price: number;
quantity: number;
createdAt: string;
}

export const usePostOrder = () => {
return useMutation({
mutationFn: async (body: PostOrderRequest) => {
const accessToken = localStorage.getItem("accessToken");
if (!accessToken) {
throw new Error("토큰 없음");
}

const { data } = await api.post<PostOrderResponse>(
"/order/create",
body,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);

return data;
},
});
};
54 changes: 54 additions & 0 deletions src/apis/query/useStockInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useEffect, useState } from "react";
import { api } from "../../lib/api";

interface StockDto {
stockCode: string;
stockName: string;
price: number;
change: number;
changeRate: number;
dataStatus: "NORMAL" | "ERROR";
userBalance: number;
userStockQuantity: number;
}



export const useStockInfo = (code: string) => {
const [stock, setStock] = useState<StockDto | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
const accessToken = localStorage.getItem("accessToken");

const getStockInfo = async () => {
try {
setIsLoading(true);

const { data } = await api.get<StockDto>(
`/stocks/${code}/tradeInfo`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);


setStock(data);
setIsError(false);
} catch (error) {
console.error("주식 조회 실패:", error);
setIsError(true);
} finally {
setIsLoading(false);
}
};

useEffect(() => {
if (code) getStockInfo();
}, [code]);

return {
stock, isLoading, isError
};
};
91 changes: 64 additions & 27 deletions src/pages/list/List.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,51 @@
import ListStock from "./components/ListStock";
import { useState } from "react";
import { useState, useMemo } from "react";
import SellModal from "../order/components/SellModal";
import BuyModal from "../order/components/BuyModal";
import { useGetStockList } from "../../apis/query/useGetStockList";
const List =() =>{
const [keyword, setKeyword] = useState("");
const [isSellOpen, setIsSellOpen] = useState(false);
const [isBuyOpen, setIsBuyOpen] = useState(false);
const [isFixed, setIsFixed] = useState(false);
const mockStocks = [
{ id: 1, name: "삼성전자", price: 75000, dir: 1, change: 500 },
{ id: 2, name: "카카오", price: 48200, dir: -1, change: -1200 },
{ id: 3, name: "네이버", price: 214000, dir: 1, change: 3500 },
{ id: 4, name: "삼성전기", price: 412000, dir: -1, change: -8000 },
{ id: 5, name: "SK하이닉스", price: 120000, dir: 1, change: 2500 },
];

const filteredStocks = mockStocks.filter((stock) =>
stock.name.toLowerCase().includes(keyword.toLowerCase())
const [stockCode, setStockCode] = useState('0');
const { stockList, isLoading, isError } = useGetStockList();
const [page, setPage] = useState(1);

const PAGE_SIZE = 5;

const filteredStocks = useMemo(() => {
return stockList.filter((stock) =>
stock.stockName.toLowerCase().includes(keyword.toLowerCase())
);
}, [stockList, keyword]);

const paginatedStocks = useMemo(() => {
const start = (page - 1) * PAGE_SIZE;
const end = start + PAGE_SIZE;
return filteredStocks.slice(start, end);
}, [filteredStocks, page]);
const totalPages = Math.ceil(filteredStocks.length / PAGE_SIZE);

const OpenModalType = (type: 'buy' | 'sell') => {
if (isLoading) return <div>로딩중...</div>;
if (isError) return <div>에러 발생</div>;
console.log(stockList);

const OpenModalType = (type: 'buy' | 'sell', stockNumCode: string) => {
setIsFixed(true);
setStockCode(stockNumCode);
if (type === 'buy') {
setIsBuyOpen(true);
} else {
setIsSellOpen(true);
}
};


return (
<div className="pt-[8.2rem] pb-[8rem] bg-[#F9FAFB]">
{isBuyOpen && <BuyModal stockName="삼성전자" stockCode="000660" stockPrice={128000} myAsset={1000000} onClose={() => {setIsBuyOpen(false); setIsFixed(false);}}/>}
{isSellOpen && <SellModal stockName="삼성전자" stockCode="000660" stockPrice={128000} myStockCount={3} onClose={() => {setIsSellOpen(false); setIsFixed(false);}}/>}
{isBuyOpen && <BuyModal stockCode={stockCode} onClose={() => {setIsBuyOpen(false); setIsFixed(false);}}/>}
{isSellOpen && <SellModal stockCode={stockCode} onClose={() => {setIsSellOpen(false); setIsFixed(false);}}/>}
<div className={`w-full h-full flex flex-col gap-[2.4rem] justify-between ${isFixed ? 'fixed' : ''} `}>


Expand All @@ -46,29 +59,53 @@ const List =() =>{
type="text"
placeholder="종목명을 입력하세요"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onChange={(e) => {
setKeyword(e.target.value);
setPage(1);
}}
className="w-full p-[1.2rem] rounded-[12px] border bg-white border-gray-200 text-[1.4rem] focus:outline-none focus:ring-2 focus:ring-[#155DFC]"
/>
</div>

<div className="max-w-[500px] w-full flex flex-col px-[2rem] pt-[15rem] gap-[0.8rem]">
<div className="max-w-[500px] w-full flex flex-col px-[2rem] pt-[13.7rem] gap-[0.8rem]">

<div className="flex justify-center gap-[1rem] mb-[1rem] items-center">
<button
disabled={page === 1}
onClick={() => setPage(prev => prev - 1)}
className="text-center text-[2rem] text-gray-500"
>
◀︎
</button>

<span className="text-[1.2rem] font-bold text-gray-500">{page} / {totalPages}</span>

<button
disabled={page === totalPages}
onClick={() => setPage(prev => prev + 1)}
className="text-center text-[2rem] text-gray-500"
>
</button>
</div>

{filteredStocks.length > 0 ? (
filteredStocks.map((stock) => (
<ListStock
key={stock.id}
name={stock.name}
price={stock.price}
dir={stock.dir}
change={stock.change}
onType={(type) => OpenModalType(type)}
/>
))
paginatedStocks.map((stock) => (
<ListStock
key={stock.stockCode}
name={stock.stockName}
price={stock.price}
dir={stock.changeRate}
change={stock.change}
onType={(type) => OpenModalType(type, stock.stockCode)}
/>
))
) : (
<div className="py-[3rem] text-center text-[1.4rem] text-gray-400">
🔎 검색 결과가 없습니다.
</div>
)}

</div>
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/pages/list/components/ListStock.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from 'react';
import upRatio from '/icons/home/upRatio.svg';
import downRatio from '/icons/home/downRatio.svg';
interface ListStockProps {
Expand Down Expand Up @@ -30,4 +31,4 @@ const ListStock = ({name, price, dir, change, onType}: ListStockProps) => {
</div>
)
}
export default ListStock;
export default React.memo(ListStock);
4 changes: 2 additions & 2 deletions src/pages/order/Order.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ const Order = () =>{
return (
<div className="relative w-full h-full flex flex-col gap-[2.4rem] justify-between bg-[#fafafa]">
<OrderHeader stockName={stockName}/>
{isSellOpen && <SellModal stockName={stockName} stockCode="000660" stockPrice={128000} myStockCount={3} onClose={() => setIsSellOpen(false)}/>}
{isBuyOpen && <BuyModal stockName={stockName} stockCode="000660" stockPrice={128000} myAsset={1000000} onClose={() => setIsBuyOpen(false)}/>}
{isSellOpen && <SellModal stockCode="000660" onClose={() => setIsSellOpen(false)}/>}
{isBuyOpen && <BuyModal stockCode="0000000" onClose={() => setIsBuyOpen(false)}/>}
<div className="relative pt-[8rem] px-[2rem] pb-[11rem] flex flex-col gap-[1.6rem] bg-[#fafafa]">
<div className="rounded-3xl bg-white shadow-sm p-[2.4rem]">
<h1 className="text-[#6A7282] text-[1.4rem] font-normal">현재가</h1>
Expand Down
54 changes: 39 additions & 15 deletions src/pages/order/components/BuyModal.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,61 @@
import closeIcon from '/icons/order/close.svg'
import plusIcon from '/icons/order/plus.svg'
import minusIcon from '/icons/order/minus.svg'
import { useState } from 'react'
import { useState } from 'react';
import { useStockInfo } from '../../../apis/query/useStockInfo';
import { usePostOrder } from '../../../apis/query/usePostOrder';
interface OrderModalProps {
stockName: string;
stockCode: string;
stockPrice: number;
myAsset: number;
onClose: () => void
}
const BuyModal = ({stockName, stockCode, stockPrice, myAsset, onClose}: OrderModalProps) => {
const [quantity, setQuantity] = useState(1)
const price = stockPrice || 128000
const maxQuantity = Math.floor((myAsset || 0) / price)

const BuyModal = ({stockCode,onClose}: OrderModalProps) => {
const { mutate: createOrder } = usePostOrder();
const [quantity, setQuantity] = useState(0);
const { stock } = useStockInfo(stockCode);
const price = stock?.price ?? 0;
const maxQuantity = price > 0
? Math.floor((stock?.userBalance || 0) / price)
: 0;

const handleMinus = () => {
setQuantity(prev => Math.max(1, prev - 1))
setQuantity(prev => Math.max(0, prev - 1))
}

console.log(stock);
const handlePlus = () => {
setQuantity(prev => Math.min(maxQuantity, prev + 1))
}

const handleOrder = () => {
if (quantity <= 0) return;
createOrder(
{
stockCode: stock?.stockCode || '0',
orderType: "BUY",
price: stock?.price || 0,
quantity: quantity,
},
{
onSuccess: (data) => {
console.log("주문 성공", data);
},
onError: (error) => {
console.error("주문 실패", error);
},
}
);
};

return (
<div className="max-w-[500px] fixed top-0 z-102 w-full h-full bg-[#00000080]">
<div className="relative w-full h-full">
<div className="absolute bottom-0 rounded-t-[32px] bg-white w-full py-[2rem]">
<div className="flex flex-row justify-between items-center border-b border-b-[#F3F4F6] px-[2.4rem] pb-[2rem] pt-[1rem]">
<span className='text-[1.7rem] font-bold'>매도하기</span>
<span className='text-[1.7rem] font-bold'>매수하기</span>
<img src={closeIcon} className="w-[2.4rem] h-[2.4rem] cursor-pointer" onClick={onClose}/>
</div>
<div className='relative w-full px-[2.4rem]'>
<div className='p-[2rem] flex flex-col gap-[0.2rem] bg-[#F9FAFB] rounded-[16px] mt-[2.4rem]'>
<h1 className='text-[1.8rem] font-bold'>{stockName}</h1>
<h1 className='text-[1.8rem] font-bold'>{stock?.stockName}</h1>
<p className='text-[1.4rem] text-[#6A7282] font-normal'>{stockCode}</p>
<p className='text-[3rem] font-bold'>{price.toLocaleString()} <span className='text-[2rem] font-bold'>원</span></p>
</div>
Expand All @@ -51,8 +75,8 @@ const BuyModal = ({stockName, stockCode, stockPrice, myAsset, onClose}: OrderMod
</div>
<div className='text-[#6A7282] text-[1.4rem] font-normal mt-[1.9rem]'> {price.toLocaleString()}원 × {quantity}주</div>
</div>
<button className='w-full mt-[3.2rem] rounded-[16px] bg-[#155DFC] text-white text-[1.6rem] font-bold py-[1.6rem]' onClick={onClose}>
매도하기
<button className='w-full mt-[3.2rem] rounded-[16px] bg-[#155DFC] text-white text-[1.6rem] font-bold py-[1.6rem]' onClick={()=>{onClose(); handleOrder();}}>
매수하기
</button>
</div>

Expand Down
Loading