Skip to content

Commit

Permalink
Merge pull request #83 from codestates-seb/dev-client#17/order
Browse files Browse the repository at this point in the history
[FE] 우측 주식주문 컴포넌트 추가 기능 구현 ( 거래 제한 기능, 키보드 입력 이벤트 보완 )
  • Loading branch information
novice1993 authored Sep 12, 2023
2 parents 8daf55c + d980f1d commit 46b1b2c
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 47 deletions.
6 changes: 6 additions & 0 deletions client/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 client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@hyunbinseo/holidays-kr": "^2.2024.4",
"@reduxjs/toolkit": "^1.9.5",
"axios": "^1.5.0",
"boxicons": "^2.1.4",
Expand Down
131 changes: 107 additions & 24 deletions client/src/components/StockOrderSection/PriceSetting.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useEffect } from "react";
import { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { styled } from "styled-components";
import { setStockOrderPrice, plusStockOrderPrice, minusStockOrderPrice } from "../../reducer/StockOrderPrice-Reducer";
import { StateProps } from "../../models/stateProps";
import { StockInfoprops } from "../../models/stockInfoProps";
import { StockInfoProps } from "../../models/stockInfoProps";

const priceSettingTitle: string = "가격";
const unitText: string = "원";
Expand All @@ -14,13 +14,53 @@ const PriceSetting = (props: OwnProps) => {
const dispatch = useDispatch();
const orderPrice = useSelector((state: StateProps) => state.stockOrderPrice);

// 가격 조정 관련 타이머 상태
const [priceChangeTimer, setPriceChangeTimer] = useState<NodeJS.Timeout | null>(null);

// 초기 설정값 및 가격 변동폭 설정
const { askp1, askp2, askp3, askp4, askp5 } = stockInfo;
const sellingPrice = [parseInt(askp1), parseInt(askp2), parseInt(askp3), parseInt(askp4), parseInt(askp5)];
const existSellingPrice = sellingPrice.filter((price) => price !== 0); // price 0인 경우 제거
const defaultPrice = existSellingPrice[0];
const priceInterval = existSellingPrice[1] - existSellingPrice[0];

// 🔴 [TestCode] 거래가능 안내 메세지 테스트 -> 🟢 구현 성공하여 코드 정리할 예정
const orderType = useSelector((state: StateProps) => state.stockOrderType);
const [orderPossibility, setOrderPossibility] = useState(true);

const { bidp1, bidp2, bidp3, bidp4, bidp5 } = stockInfo;
const buyingPrice = [parseInt(bidp1), parseInt(bidp2), parseInt(bidp3), parseInt(bidp4), parseInt(bidp5)];
const existBuyingPrice = buyingPrice.filter((price) => price !== 0); // price 0인 경우 제거

// 거래 가능여부 판별 함수
const handleCheckTradePossibility = () => {
if (orderType) {
// 매수 주문
if (orderPrice !== 0 && !existBuyingPrice.includes(orderPrice)) {
setOrderPossibility(false);
} else {
setOrderPossibility(true);
}
} else {
// 매도 주문
if (orderPrice !== 0 && !existSellingPrice.includes(orderPrice)) {
setOrderPossibility(false);
} else {
setOrderPossibility(true);
}
}
};

useEffect(() => {
handleCheckTradePossibility();
}, [orderPrice, orderType]);

// 가격 설정란에서 포커스 제거 -> 안내 메세지 제거
const handleRemoveNoVolumeNotification = () => {
setOrderPossibility(true);
};
// 🔴 [TestCode] 거래가능 안내 메세지 테스트 -> 🟢 구현 성공하여 코드 정리할 예정

// 거래가 증가/감소
const handlePlusOrderPrice = () => {
dispatch(plusStockOrderPrice(priceInterval));
Expand All @@ -30,20 +70,44 @@ const PriceSetting = (props: OwnProps) => {
dispatch(minusStockOrderPrice(priceInterval));
};

// 위-아래 방향키 입력 시
const handleInputArrowBtn = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.code === "ArrowUp") {
handlePlusOrderPrice();
} else if (event.code === "ArrowDown") {
handleMinusOrderPrice();
}
};

// 거래가 직접 기입 시
const handleWriteOrderPrice = (event: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = event.target.value;
const numberInputValue = parseInt(inputValue, 10);
const inputPrice = event.target.value;
const numberInputPrice = parseInt(inputPrice, 10);

// 1) 음수를 임력하거나, 숫자 아닌 값 기입 시 -> 입력 무시 2) 값을 다 지워서 빈 문자열인 경우 -> 0으로 설정
if (numberInputValue < 0 || isNaN(numberInputValue)) {
if (inputValue === "") {
if (numberInputPrice < 0 || isNaN(numberInputPrice)) {
if (inputPrice === "") {
dispatch(setStockOrderPrice(0));
}
return;
}

dispatch(setStockOrderPrice(numberInputValue));
// priceInterval로 나누어 떨어지지 않는 값을 기입 시 -> 0.8초 후에 나누어 떨어지는 값으로 변경
if (priceChangeTimer !== null) {
clearTimeout(priceChangeTimer);
}

dispatch(setStockOrderPrice(numberInputPrice));

if (numberInputPrice > priceInterval && numberInputPrice % priceInterval !== 0) {
const newTimer = setTimeout(() => {
const remainder = numberInputPrice % priceInterval;
const modifiedInputValue = numberInputPrice - remainder;
dispatch(setStockOrderPrice(modifiedInputValue));
}, 800);

setPriceChangeTimer(newTimer);
}
};

// 종목이 달리지면 -> 가격도 변경
Expand All @@ -52,30 +116,43 @@ const PriceSetting = (props: OwnProps) => {
}, [companyId]);

return (
<Container>
<div className="PriceCategoryBox">
<div className="Title">{priceSettingTitle}</div>
</div>
<div className="PriceSettingBox">
<PriceController defaultValue={orderPrice} value={orderPrice} onChange={handleWriteOrderPrice} />
<UnitContent>{unitText}</UnitContent>
<div className="DirectionBox">
<button className="PriceUp" onClick={handlePlusOrderPrice}>
&#8896;
</button>
<button className="PriceDown" onClick={handleMinusOrderPrice}>
&#8897;
</button>
<>
<Container>
<div className="PriceCategoryBox">
<div className="Title">{priceSettingTitle}</div>
</div>
<div className="PriceSettingBox">
<PriceController defaultValue={orderPrice} value={orderPrice} onChange={handleWriteOrderPrice} onKeyDown={handleInputArrowBtn} onFocus={handleCheckTradePossibility} onBlur={handleRemoveNoVolumeNotification} />
<UnitContent>{unitText}</UnitContent>
<div className="DirectionBox">
<button className="PriceUp" onClick={handlePlusOrderPrice} onBlur={handleRemoveNoVolumeNotification}>
&#8896;
</button>
<button className="PriceDown" onClick={handleMinusOrderPrice} onBlur={handleRemoveNoVolumeNotification}>
&#8897;
</button>
</div>
</div>
</div>
</Container>
</Container>

{/* 거래 불가 테스트 */}
{!orderPossibility && (
<NoTradingVolume>
<div className="container">
거래 불가하며 예약 거래 됨을 공지
<div></div>
<div></div>
</div>
</NoTradingVolume>
)}
</>
);
};

export default PriceSetting;

interface OwnProps {
stockInfo: StockInfoprops;
stockInfo: StockInfoProps;
companyId: number;
}

Expand Down Expand Up @@ -167,3 +244,9 @@ const UnitContent = styled.div`
border-bottom: 1px solid darkgray;
background-color: #ffffff;
`;

const NoTradingVolume = styled.div`
position: absolute;
top: 222px;
right: 4%;
`;
24 changes: 21 additions & 3 deletions client/src/components/StockOrderSection/StockOrder.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useSelector, useDispatch } from "react-redux";
import { isHoliday } from "@hyunbinseo/holidays-kr";
import { closeDecisionWindow } from "../../reducer/SetDecisionWindow-Reducer";
import { styled } from "styled-components";
import { StateProps } from "../../models/stateProps";
Expand All @@ -12,6 +13,8 @@ import dummyImg from "../../asset/CentralSectionMenu-dummyImg.png";
const orderFailureMessage01: string = "주문 실패";
const orderFailureMessage02: string = "주문 수량이 없습니다";
const orderFailureMessage03: string = "입력하신 가격이 올바르지 않습니다";
const orderFailureMessage04: string = "주문 가능한 시간이 아닙니다";
const openingTimeIndicator: string = "주문 가능 : 평일 오전 9시 ~ 오후 3시 30분";
const orderFailureButtonText: string = "확인";

const orderPriceText: string = "주문단가";
Expand All @@ -38,6 +41,21 @@ const StockOrder = ({ corpName }: { corpName: string }) => {
dispatch(closeDecisionWindow());
};

// 1) 주말, 공휴일 여부 체크
const today = new Date();
const nonBusinessDay = isHoliday(today, { include: { saturday: true, sunday: true } }); // 토요일, 일요일, 공휴일 (임시 공휴일 포함)

// 2) 개장시간 여부 체크
const currentHour = today.getHours();
const currentMinute = today.getMinutes();
const isBefore9AM = currentHour < 9;
const isAfter330PM = currentHour > 15 || (currentHour === 15 && currentMinute >= 30);
const closingTime = isBefore9AM || isAfter330PM;

// 주문 실패 케이스 1) 개장시간 2) 가격/거래량 설정
const orderFailureCase01 = nonBusinessDay || closingTime;
const orderFailureCase02 = orderPrice === 0 || orderVolume === 0;

return (
<>
{/* 주문 버튼 클릭 안했을 때 */}
Expand All @@ -48,11 +66,11 @@ const StockOrder = ({ corpName }: { corpName: string }) => {

{/* 주문 버튼 클릭 했을 때 */}
{decisionWindow ? (
orderVolume === 0 || orderPrice === 0 ? (
orderFailureCase01 || orderFailureCase02 ? (
<OrderFailed>
<div className="Container">
<div className="message01">{orderFailureMessage01}</div>
<div className="message02">{orderPrice !== 0 ? `${orderFailureMessage02}` : `${orderFailureMessage03}`}</div>
<div className="message01">{orderFailureCase01 ? `${orderFailureMessage04}` : orderFailureMessage01}</div>
<div className="message02">{orderFailureCase01 ? `${openingTimeIndicator}` : orderPrice !== 0 ? `${orderFailureMessage02}` : `${orderFailureMessage03}`}</div>
<button onClick={handleCloseDecisionWindow}>{orderFailureButtonText}</button>
</div>
</OrderFailed>
Expand Down
22 changes: 13 additions & 9 deletions client/src/components/StockOrderSection/StockPrice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { StockProps } from "../../models/stockProps";

import { useState, useEffect, useRef } from "react";
import { useSelector, useDispatch } from "react-redux";
import { isHoliday } from "@hyunbinseo/holidays-kr";
import { styled } from "styled-components";
import { setStockOrderPrice } from "../../reducer/StockOrderPrice-Reducer";
import { StateProps } from "../../models/stateProps";
Expand Down Expand Up @@ -39,22 +40,26 @@ const StockPrice = (props: StockPriceProps) => {
return;
}

// 전날 종가 데이터 -> 1) 일/월 : 금요일 종가로 설정 2) 화~토 : 전날 종가로 설정
// 전날 종가 데이터 -> 1) 화~토 : 전날 종가로 설정 2) 공휴일/월요일 : 맨 마지막 종가로 설정 (전날 데이터 없음)
let previousDayStockClosingPrice: number;

const today = new Date();
const getToday = today.getDay();
const daysOfWeek = ["일", "월", "화", "수", "목", "금", "토"];
const getToday = new Date().getDay();
const today = daysOfWeek[getToday];

if (today === "일" || today === "월") {
const nonBusinessDay = isHoliday(today, { include: { sunday: true } }); // 일요일, 공휴일 (임시 공휴일 포함) 체크
const isMonday = daysOfWeek[getToday] === "월"; // 월요일인지 체크

if (nonBusinessDay || isMonday) {
previousDayStockClosingPrice = stockPrice[stockPrice.length - 1].stck_prpr;
} else {
const yesterday = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
const nowInKoreanTime = new Date(today.getTime() + 9 * 60 * 60 * 1000); // UTC 시간에 9시간 더해서 한국 시간대로 변환
const yesterday = new Date(nowInKoreanTime);
yesterday.setDate(nowInKoreanTime.getDate() - 1);
const yesterdayYymmdd = yesterday.toISOString().slice(0, 10);

const yesterdayStockInfo = stockPrice.filter((stockInfo: StockProps) => {
const dayInfo = stockInfo.stockTradeTime.slice(0, 10);

if (dayInfo === yesterdayYymmdd) {
return stockInfo;
}
Expand Down Expand Up @@ -111,12 +116,11 @@ const Container = styled.div<{ index: number; price: number; orderPrice: number
width: 100%;
height: 36px;
margin-bottom: 2px;
background-color: ${(props) => (props.index > 9 ? "#FDE8E7" : "#E7F0FD")};
border: ${(props) => (props.price === props.orderPrice ? "1.5px solid #2F4F4F" : "none")};
background-color: ${(props) => (props.price === props.orderPrice ? (props.index > 9 ? "#e9c2bf" : "#bed1eb") : props.index > 9 ? "#FDE8E7" : "#E7F0FD")};
border-left: ${(props) => (props.price === props.orderPrice ? "3px solid red" : props.index > 9 ? "3px solid #FDE8E7" : "3px solid #E7F0FD")};
display: flex;
flex-direction: row;
transition: border 1s ease;
transition: border 0.8s ease, background-color 0.8s ease;
&:hover {
cursor: pointer;
Expand Down
1 change: 1 addition & 0 deletions client/src/components/StockOrderSection/StockPriceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const StockPriceList = () => {
}

/*
🔴 삭제 예정인 코드
[문제점] 주가 리스트 개수가 너무 적음 (매도호가 5개 + 매수호가 5개 = 총 10개) → UX를 저해하는 요소로 판단되어, 더미데이터를 추가 (매도/매수 각각 5개씩)
[해결방안] 1) fetching 해온 데이터 중 가격 0인 데이터 제외 (한국투자증권 API에서 간혹 보내는 경우 있음) → 호가 간격 계산 후, 더미 데이터 추가 (거래량은 0으로 설정)
*/
Expand Down
13 changes: 10 additions & 3 deletions client/src/components/StockOrderSection/VolumeSetteing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ const VolumeSetting = () => {
const orderPrice = useSelector((state: StateProps) => state.stockOrderPrice);
const orderVolume = useSelector((state: StateProps) => state.stockOrderVolume);

// 🎾 임시로직 추가
// const maximumBuyingVolume = Math.trunc(dummyMoney / orderPrice);
const maximumBuyingVolume = orderPrice !== 0 ? Math.trunc(dummyMoney / orderPrice) : Math.trunc(dummyMoney / 1);

const handlePlusOrderVolume = () => {
Expand All @@ -45,6 +43,15 @@ const VolumeSetting = () => {
}
};

// 위-아래 방향키 입력 시
const handleInputArrowBtn = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.code === "ArrowUp") {
handlePlusOrderVolume();
} else if (event.code === "ArrowDown") {
handleMinusOrderVolume();
}
};

// 거래량 직접 기입 시
const handleWriteOrderVolume = (event: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = event.target.value;
Expand Down Expand Up @@ -97,7 +104,7 @@ const VolumeSetting = () => {
</div>
</TitleContainer>
<VolumeSettingBox>
<VolumeController defaultValue={orderVolume} value={orderVolume} onChange={handleWriteOrderVolume} />
<VolumeController defaultValue={orderVolume} value={orderVolume} onChange={handleWriteOrderVolume} onKeyDown={handleInputArrowBtn} />
<UnitContent>{volumeUnit}</UnitContent>
<div className="DirectionContainer">
<button className="VolumeUp" onClick={handlePlusOrderVolume}>
Expand Down
8 changes: 4 additions & 4 deletions client/src/hooks/useGetStockData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ const useGetStockData = (companyId: number) => {
const { data, isLoading, error, refetch } = useQuery(`chartData${companyId} ${queryKey}`, () => getChartData(companyId), {
enabled: true,
refetchInterval: autoRefetch ? 60000 * 10 : false, // 정각 혹은 30분에 맞춰서 10분 마다 데이터 리패칭
onSuccess: () => {
console.log(new Date());
console.log(data);
},
// onSuccess: () => {
// console.log(new Date());
// console.log(data);
// },
});

return { stockPrice: data, stockPriceLoading: isLoading, stockPriceError: error };
Expand Down
8 changes: 4 additions & 4 deletions client/src/hooks/useGetStockInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ const useGetStockInfo = (companyId: number) => {
const { data, isLoading, error, refetch } = useQuery(`stockInfo${companyId} ${queryKey}}`, () => getStockInfo(companyId), {
enabled: true,
refetchInterval: autoRefetch ? 60000 * 10 : false, // 정각 혹은 30분에 맞춰서 10분 마다 데이터 리패칭
onSuccess: () => {
console.log(new Date());
console.log(data);
},
// onSuccess: () => {
// console.log(new Date());
// console.log(data);
// },
});

return { stockInfo: data, stockInfoLoading: isLoading, stockInfoError: error };
Expand Down

0 comments on commit 46b1b2c

Please sign in to comment.