snp500맵 배치 완료.
This commit is contained in:
parent
27689133ec
commit
21d77f79da
@ -717,6 +717,75 @@ const ENERGY_INDUSTRIES = [
|
||||
}
|
||||
];
|
||||
|
||||
// UTILITIES 섹터 산업군 (256px × 258px)
|
||||
const UTILITIES_INDUSTRIES = [
|
||||
// 상단 70% (180.6px)
|
||||
{
|
||||
name: "UTILITIES - REGULATED ELECTRIC",
|
||||
x1: 0, y1: 30, x2: 256, y2: 210.6,
|
||||
isSmall: false
|
||||
},
|
||||
// 하단 30% (77.4px) - 3개를 1:1:1 가로 분할
|
||||
{
|
||||
name: "UTILITIES - RENEWABLE",
|
||||
x1: 0, y1: 210.6, x2: 85.33, y2: 258,
|
||||
isSmall: true
|
||||
},
|
||||
{
|
||||
name: "UTILITIES - INDEPENDENT POWER PRODUCERS",
|
||||
x1: 85.33, y1: 210.6, x2: 170.67, y2: 258,
|
||||
isSmall: true
|
||||
},
|
||||
{
|
||||
name: "UTILITIES - DIVERSIFIED",
|
||||
x1: 170.67, y1: 210.6, x2: 256, y2: 258,
|
||||
isSmall: true
|
||||
}
|
||||
];
|
||||
|
||||
// BASIC MATERIALS 섹터 산업군 (256px × 205px)
|
||||
const BASIC_MATERIALS_INDUSTRIES = [
|
||||
// 좌측 50% (128px) - 세로 전체
|
||||
{
|
||||
name: "SPECIALTY CHEMICALS",
|
||||
x1: 0, y1: 30, x2: 128, y2: 205,
|
||||
isSmall: false
|
||||
},
|
||||
// 우측 50% (128px)
|
||||
// 상단 40% (70px) - 1:1 가로 분할
|
||||
{
|
||||
name: "GOLD",
|
||||
x1: 128, y1: 30, x2: 192, y2: 100,
|
||||
isSmall: true
|
||||
},
|
||||
{
|
||||
name: "BUILDING MATERIALS",
|
||||
x1: 192, y1: 30, x2: 256, y2: 100,
|
||||
isSmall: true
|
||||
},
|
||||
// 하단 60% (105px) - 2×2 그리드
|
||||
{
|
||||
name: "AGRICULTURAL INPUTS",
|
||||
x1: 128, y1: 100, x2: 192, y2: 152.5,
|
||||
isSmall: true
|
||||
},
|
||||
{
|
||||
name: "COPPER",
|
||||
x1: 192, y1: 100, x2: 256, y2: 152.5,
|
||||
isSmall: true
|
||||
},
|
||||
{
|
||||
name: "STEEL",
|
||||
x1: 128, y1: 152.5, x2: 192, y2: 205,
|
||||
isSmall: true
|
||||
},
|
||||
{
|
||||
name: "CHEMICALS",
|
||||
x1: 192, y1: 152.5, x2: 256, y2: 205,
|
||||
isSmall: true
|
||||
}
|
||||
];
|
||||
|
||||
// 날짜 형식 변환: YYYY-MM-DD → YYYYMMDD
|
||||
function formatDateToAPI(dateStr: string): string {
|
||||
return dateStr.replace(/-/g, "");
|
||||
@ -6229,6 +6298,563 @@ export function SP500Page() {
|
||||
);
|
||||
})}
|
||||
|
||||
{/* UTILITIES 섹터 산업군 배치 */}
|
||||
{box.name === "UTILITIES" &&
|
||||
UTILITIES_INDUSTRIES.map((industry, idx) => {
|
||||
const headerHeight = industry.isSmall ? 8 : 10;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="industry-box"
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${industry.x1 + 1}px`,
|
||||
top: `${industry.y1 + 1}px`,
|
||||
width: `${industry.x2 - industry.x1 - 2}px`,
|
||||
height: `${industry.y2 - industry.y1 - 2}px`,
|
||||
border: "none",
|
||||
background: "rgba(30, 40, 50, 0.6)",
|
||||
overflow: "hidden",
|
||||
transition: "all 0.2s ease",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = "rgba(74, 144, 226, 1)";
|
||||
e.currentTarget.style.background = "rgba(74, 144, 226, 0.15)";
|
||||
e.currentTarget.style.boxShadow = "0 0 12px rgba(74, 144, 226, 0.5)";
|
||||
e.currentTarget.style.transform = "scale(1.02)";
|
||||
handleIndustryHover(industry.name, e);
|
||||
}}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = "rgba(74, 144, 226, 0.8)";
|
||||
e.currentTarget.style.background = "rgba(30, 40, 50, 0.6)";
|
||||
e.currentTarget.style.boxShadow = "none";
|
||||
e.currentTarget.style.transform = "scale(1)";
|
||||
handleIndustryLeave();
|
||||
}}
|
||||
>
|
||||
{/* Industry 헤더 배경색 */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: "0px",
|
||||
width: `${industry.x2 - industry.x1 - 2}px`,
|
||||
height: `${headerHeight}px`,
|
||||
background: (() => {
|
||||
if (!currentData || !previousData) return "rgba(75, 80, 85, 0.9)";
|
||||
|
||||
const stocks = Object.entries(currentData.data)
|
||||
.filter(([, stock]) => stock.industry === industry.name)
|
||||
.map(([ticker, currentStock]) => {
|
||||
const previousStock = previousData.data[ticker];
|
||||
if (!previousStock) return null;
|
||||
const changePercent = ((currentStock.price - previousStock.price) / previousStock.price) * 100;
|
||||
return { marketCap: currentStock.marketCap, changePercent };
|
||||
})
|
||||
.filter((s): s is { marketCap: number; changePercent: number } => s !== null);
|
||||
|
||||
if (stocks.length === 0) return "rgba(75, 80, 85, 0.9)";
|
||||
|
||||
const totalMarketCap = stocks.reduce((sum, s) => sum + s.marketCap, 0);
|
||||
const avgChange = stocks.reduce((sum, s) => sum + (s.changePercent * s.marketCap / totalMarketCap), 0);
|
||||
|
||||
return getStockColor(avgChange);
|
||||
})(),
|
||||
borderRadius: "3px 3px 0 0",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
paddingLeft: "4px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: industry.isSmall ? "7px" : "8px",
|
||||
fontWeight: 700,
|
||||
color: "#fff",
|
||||
textShadow: "1px 1px 2px rgba(0, 0, 0, 0.8)",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "clip",
|
||||
}}
|
||||
>
|
||||
{industry.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Industry 내용 영역 */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: `${headerHeight}px`,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: `${industry.y2 - industry.y1 - headerHeight - 2}px`,
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
if (!currentData || !previousData) return null;
|
||||
|
||||
const contentWidth = industry.x2 - industry.x1 - 2;
|
||||
const contentHeight = industry.y2 - industry.y1 - headerHeight - 2;
|
||||
|
||||
const industryStocks = Object.entries(currentData.data)
|
||||
.filter(([, stock]) => stock.industry === industry.name)
|
||||
.map(([ticker, currentStock]) => {
|
||||
const previousStock = previousData.data[ticker];
|
||||
if (!previousStock) return null;
|
||||
const changePercent = ((currentStock.price - previousStock.price) / previousStock.price) * 100;
|
||||
return { ticker, marketCap: currentStock.marketCap, changePercent };
|
||||
})
|
||||
.filter((s): s is { ticker: string; marketCap: number; changePercent: number } => s !== null);
|
||||
|
||||
if (industryStocks.length === 0) return null;
|
||||
|
||||
// 수동 배치 또는 기본 로직
|
||||
let layout: Array<{ ticker: string; x: number; y: number; width: number; height: number; changePercent: number }> = [];
|
||||
|
||||
if (industry.name === "UTILITIES - REGULATED ELECTRIC") {
|
||||
// UTILITIES - REGULATED ELECTRIC 수동 배치 - 4×3 그리드
|
||||
const cellWidth = contentWidth / 4;
|
||||
const cellHeight = contentHeight / 3;
|
||||
|
||||
// 시가총액 기준 정렬
|
||||
const sortedStocks = industryStocks.sort((a, b) => b.marketCap - a.marketCap);
|
||||
const top11Stocks = sortedStocks.slice(0, 11);
|
||||
|
||||
// 4×3 그리드 (11개 주식 배치)
|
||||
top11Stocks.forEach((stock, index) => {
|
||||
const col = index % 4;
|
||||
const row = Math.floor(index / 4);
|
||||
|
||||
layout.push({
|
||||
ticker: stock.ticker,
|
||||
x: col * cellWidth,
|
||||
y: row * cellHeight,
|
||||
width: cellWidth,
|
||||
height: cellHeight,
|
||||
changePercent: stock.changePercent
|
||||
});
|
||||
});
|
||||
|
||||
// 12번째 칸 - 집계 박스 (나머지 주식들의 평균 등락폭)
|
||||
const aggregatedStocks = sortedStocks.slice(11);
|
||||
if (aggregatedStocks.length > 0) {
|
||||
const totalMarketCap = aggregatedStocks.reduce((sum, s) => sum + s.marketCap, 0);
|
||||
const weightedAvgChange = aggregatedStocks.reduce((sum, s) =>
|
||||
sum + (s.changePercent * s.marketCap / totalMarketCap), 0
|
||||
);
|
||||
|
||||
layout.push({
|
||||
ticker: `+${aggregatedStocks.length}`,
|
||||
x: 3 * cellWidth,
|
||||
y: 2 * cellHeight,
|
||||
width: cellWidth,
|
||||
height: cellHeight,
|
||||
changePercent: weightedAvgChange
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 기본 로직: 시가총액 기준 상위 2개만 선택하여 1:1 가로 분할
|
||||
const top2Stocks = industryStocks
|
||||
.sort((a, b) => b.marketCap - a.marketCap)
|
||||
.slice(0, 2);
|
||||
|
||||
if (top2Stocks.length === 1) {
|
||||
layout.push({
|
||||
ticker: top2Stocks[0].ticker,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: contentWidth,
|
||||
height: contentHeight,
|
||||
changePercent: top2Stocks[0].changePercent
|
||||
});
|
||||
} else if (top2Stocks.length === 2) {
|
||||
layout.push({
|
||||
ticker: top2Stocks[0].ticker,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: contentWidth / 2,
|
||||
height: contentHeight,
|
||||
changePercent: top2Stocks[0].changePercent
|
||||
});
|
||||
layout.push({
|
||||
ticker: top2Stocks[1].ticker,
|
||||
x: contentWidth / 2,
|
||||
y: 0,
|
||||
width: contentWidth / 2,
|
||||
height: contentHeight,
|
||||
changePercent: top2Stocks[1].changePercent
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return layout.map((item, i) => {
|
||||
const boxArea = item.width * item.height;
|
||||
let tickerFontSize = 16;
|
||||
let percentFontSize = 11;
|
||||
let showPercent = true;
|
||||
let showPercentSign = true;
|
||||
|
||||
if (boxArea > 15000) {
|
||||
tickerFontSize = 32;
|
||||
percentFontSize = Math.round(tickerFontSize * 0.65);
|
||||
} else if (boxArea > 8000) {
|
||||
tickerFontSize = 24;
|
||||
percentFontSize = Math.round(tickerFontSize * 0.65);
|
||||
} else if (boxArea > 3000) {
|
||||
tickerFontSize = 16;
|
||||
percentFontSize = Math.round(tickerFontSize * 0.7);
|
||||
} else if (boxArea > 1000) {
|
||||
tickerFontSize = Math.max(9, Math.min(12, Math.round(Math.sqrt(boxArea) / 4)));
|
||||
percentFontSize = Math.round(tickerFontSize * 0.75);
|
||||
showPercentSign = false;
|
||||
} else {
|
||||
tickerFontSize = Math.max(6, Math.min(9, Math.round(Math.sqrt(boxArea) / 5)));
|
||||
showPercent = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${item.x}px`,
|
||||
top: `${item.y}px`,
|
||||
width: `${item.width}px`,
|
||||
height: `${item.height}px`,
|
||||
background: getStockColor(item.changePercent),
|
||||
border: "1px solid rgba(0, 0, 0, 0.3)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "2px",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = "scale(1.05)";
|
||||
e.currentTarget.style.zIndex = "10";
|
||||
e.currentTarget.style.boxShadow = "0 0 8px rgba(255, 255, 255, 0.5)";
|
||||
handleStockHover(item.ticker, e);
|
||||
}}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = "scale(1)";
|
||||
e.currentTarget.style.zIndex = "1";
|
||||
e.currentTarget.style.boxShadow = "none";
|
||||
handleStockLeave();
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: `${tickerFontSize}px`, fontWeight: 700, color: "#fff", textShadow: "1px 1px 2px rgba(0, 0, 0, 0.8)" }}>
|
||||
{item.ticker}
|
||||
</div>
|
||||
{showPercent && (
|
||||
<div style={{ fontSize: `${percentFontSize}px`, fontWeight: 600, color: "#fff", textShadow: "1px 1px 2px rgba(0, 0, 0, 0.8)" }}>
|
||||
{item.changePercent > 0 ? '+' : ''}{item.changePercent.toFixed(2)}{showPercentSign ? '%' : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* BASIC MATERIALS 섹터 산업군 배치 */}
|
||||
{box.name === "BASIC MATERIALS" &&
|
||||
BASIC_MATERIALS_INDUSTRIES.map((industry, idx) => {
|
||||
const headerHeight = industry.isSmall ? 8 : 10;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="industry-box"
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${industry.x1 + 1}px`,
|
||||
top: `${industry.y1 + 1}px`,
|
||||
width: `${industry.x2 - industry.x1 - 2}px`,
|
||||
height: `${industry.y2 - industry.y1 - 2}px`,
|
||||
border: "none",
|
||||
background: "rgba(30, 40, 50, 0.6)",
|
||||
overflow: "hidden",
|
||||
transition: "all 0.2s ease",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = "rgba(74, 144, 226, 1)";
|
||||
e.currentTarget.style.background = "rgba(74, 144, 226, 0.15)";
|
||||
e.currentTarget.style.boxShadow = "0 0 12px rgba(74, 144, 226, 0.5)";
|
||||
e.currentTarget.style.transform = "scale(1.02)";
|
||||
handleIndustryHover(industry.name, e);
|
||||
}}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = "rgba(74, 144, 226, 0.8)";
|
||||
e.currentTarget.style.background = "rgba(30, 40, 50, 0.6)";
|
||||
e.currentTarget.style.boxShadow = "none";
|
||||
e.currentTarget.style.transform = "scale(1)";
|
||||
handleIndustryLeave();
|
||||
}}
|
||||
>
|
||||
{/* Industry 헤더 배경색 */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: "0px",
|
||||
width: `${industry.x2 - industry.x1 - 2}px`,
|
||||
height: `${headerHeight}px`,
|
||||
background: (() => {
|
||||
if (!currentData || !previousData) return "rgba(75, 80, 85, 0.9)";
|
||||
|
||||
const stocks = Object.entries(currentData.data)
|
||||
.filter(([, stock]) => stock.industry === industry.name)
|
||||
.map(([ticker, currentStock]) => {
|
||||
const previousStock = previousData.data[ticker];
|
||||
if (!previousStock) return null;
|
||||
const changePercent = ((currentStock.price - previousStock.price) / previousStock.price) * 100;
|
||||
return { marketCap: currentStock.marketCap, changePercent };
|
||||
})
|
||||
.filter((s): s is { marketCap: number; changePercent: number } => s !== null);
|
||||
|
||||
if (stocks.length === 0) return "rgba(75, 80, 85, 0.9)";
|
||||
|
||||
const totalMarketCap = stocks.reduce((sum, s) => sum + s.marketCap, 0);
|
||||
const avgChange = stocks.reduce((sum, s) => sum + (s.changePercent * s.marketCap / totalMarketCap), 0);
|
||||
|
||||
return getStockColor(avgChange);
|
||||
})(),
|
||||
borderRadius: "3px 3px 0 0",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
paddingLeft: "4px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: industry.isSmall ? "7px" : "8px",
|
||||
fontWeight: 700,
|
||||
color: "#fff",
|
||||
textShadow: "1px 1px 2px rgba(0, 0, 0, 0.8)",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "clip",
|
||||
}}
|
||||
>
|
||||
{industry.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Industry 내용 영역 */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: `${headerHeight}px`,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: `${industry.y2 - industry.y1 - headerHeight - 2}px`,
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
if (!currentData || !previousData) return null;
|
||||
|
||||
const contentWidth = industry.x2 - industry.x1 - 2;
|
||||
const contentHeight = industry.y2 - industry.y1 - headerHeight - 2;
|
||||
|
||||
const industryStocks = Object.entries(currentData.data)
|
||||
.filter(([, stock]) => stock.industry === industry.name)
|
||||
.map(([ticker, currentStock]) => {
|
||||
const previousStock = previousData.data[ticker];
|
||||
if (!previousStock) return null;
|
||||
const changePercent = ((currentStock.price - previousStock.price) / previousStock.price) * 100;
|
||||
return { ticker, marketCap: currentStock.marketCap, changePercent };
|
||||
})
|
||||
.filter((s): s is { ticker: string; marketCap: number; changePercent: number } => s !== null);
|
||||
|
||||
if (industryStocks.length === 0) return null;
|
||||
|
||||
// 수동 배치 또는 기본 로직
|
||||
let layout: Array<{ ticker: string; x: number; y: number; width: number; height: number; changePercent: number }> = [];
|
||||
|
||||
if (industry.name === "SPECIALTY CHEMICALS") {
|
||||
// SPECIALTY CHEMICALS 수동 배치
|
||||
const stockMap = new Map(industryStocks.map(s => [s.ticker, s]));
|
||||
|
||||
// 1. LIN - 상단 40% (가로 전체)
|
||||
const lin = stockMap.get('LIN');
|
||||
if (lin) {
|
||||
layout.push({
|
||||
ticker: 'LIN',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: contentWidth,
|
||||
height: contentHeight * 0.4,
|
||||
changePercent: lin.changePercent
|
||||
});
|
||||
}
|
||||
|
||||
// 하단 60% - 3×2 그리드
|
||||
const bottomY = contentHeight * 0.4;
|
||||
const bottomHeight = contentHeight * 0.6;
|
||||
const cellWidth = contentWidth / 3;
|
||||
const cellHeight = bottomHeight / 2;
|
||||
|
||||
// 시가총액 기준 정렬 (LIN 제외)
|
||||
const sortedStocks = industryStocks
|
||||
.filter(s => s.ticker !== 'LIN')
|
||||
.sort((a, b) => b.marketCap - a.marketCap);
|
||||
|
||||
const top5Stocks = sortedStocks.slice(0, 5);
|
||||
|
||||
// 3×2 그리드 (5개 주식 배치)
|
||||
top5Stocks.forEach((stock, index) => {
|
||||
const col = index % 3;
|
||||
const row = Math.floor(index / 3);
|
||||
|
||||
layout.push({
|
||||
ticker: stock.ticker,
|
||||
x: col * cellWidth,
|
||||
y: bottomY + row * cellHeight,
|
||||
width: cellWidth,
|
||||
height: cellHeight,
|
||||
changePercent: stock.changePercent
|
||||
});
|
||||
});
|
||||
|
||||
// 6번째 칸 - 집계 박스 (나머지 주식들의 평균 등락폭)
|
||||
const aggregatedStocks = sortedStocks.slice(5);
|
||||
if (aggregatedStocks.length > 0) {
|
||||
const totalMarketCap = aggregatedStocks.reduce((sum, s) => sum + s.marketCap, 0);
|
||||
const weightedAvgChange = aggregatedStocks.reduce((sum, s) =>
|
||||
sum + (s.changePercent * s.marketCap / totalMarketCap), 0
|
||||
);
|
||||
|
||||
layout.push({
|
||||
ticker: `+${aggregatedStocks.length}`,
|
||||
x: 2 * cellWidth,
|
||||
y: bottomY + cellHeight,
|
||||
width: cellWidth,
|
||||
height: cellHeight,
|
||||
changePercent: weightedAvgChange
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 기본 로직: 시가총액 기준 상위 2개만 선택하여 1:1 가로 분할
|
||||
const top2Stocks = industryStocks
|
||||
.sort((a, b) => b.marketCap - a.marketCap)
|
||||
.slice(0, 2);
|
||||
|
||||
if (top2Stocks.length === 1) {
|
||||
layout.push({
|
||||
ticker: top2Stocks[0].ticker,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: contentWidth,
|
||||
height: contentHeight,
|
||||
changePercent: top2Stocks[0].changePercent
|
||||
});
|
||||
} else if (top2Stocks.length === 2) {
|
||||
layout.push({
|
||||
ticker: top2Stocks[0].ticker,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: contentWidth / 2,
|
||||
height: contentHeight,
|
||||
changePercent: top2Stocks[0].changePercent
|
||||
});
|
||||
layout.push({
|
||||
ticker: top2Stocks[1].ticker,
|
||||
x: contentWidth / 2,
|
||||
y: 0,
|
||||
width: contentWidth / 2,
|
||||
height: contentHeight,
|
||||
changePercent: top2Stocks[1].changePercent
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return layout.map((item, i) => {
|
||||
const boxArea = item.width * item.height;
|
||||
let tickerFontSize = 16;
|
||||
let percentFontSize = 11;
|
||||
let showPercent = true;
|
||||
let showPercentSign = true;
|
||||
|
||||
if (boxArea > 15000) {
|
||||
tickerFontSize = 32;
|
||||
percentFontSize = Math.round(tickerFontSize * 0.65);
|
||||
} else if (boxArea > 8000) {
|
||||
tickerFontSize = 24;
|
||||
percentFontSize = Math.round(tickerFontSize * 0.65);
|
||||
} else if (boxArea > 3000) {
|
||||
tickerFontSize = 16;
|
||||
percentFontSize = Math.round(tickerFontSize * 0.7);
|
||||
} else if (boxArea > 1000) {
|
||||
tickerFontSize = Math.max(9, Math.min(12, Math.round(Math.sqrt(boxArea) / 4)));
|
||||
percentFontSize = Math.round(tickerFontSize * 0.75);
|
||||
showPercentSign = false;
|
||||
} else {
|
||||
tickerFontSize = Math.max(6, Math.min(9, Math.round(Math.sqrt(boxArea) / 5)));
|
||||
showPercent = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${item.x}px`,
|
||||
top: `${item.y}px`,
|
||||
width: `${item.width}px`,
|
||||
height: `${item.height}px`,
|
||||
background: getStockColor(item.changePercent),
|
||||
border: "1px solid rgba(0, 0, 0, 0.3)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "2px",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = "scale(1.05)";
|
||||
e.currentTarget.style.zIndex = "10";
|
||||
e.currentTarget.style.boxShadow = "0 0 8px rgba(255, 255, 255, 0.5)";
|
||||
handleStockHover(item.ticker, e);
|
||||
}}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = "scale(1)";
|
||||
e.currentTarget.style.zIndex = "1";
|
||||
e.currentTarget.style.boxShadow = "none";
|
||||
handleStockLeave();
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: `${tickerFontSize}px`, fontWeight: 700, color: "#fff", textShadow: "1px 1px 2px rgba(0, 0, 0, 0.8)" }}>
|
||||
{item.ticker}
|
||||
</div>
|
||||
{showPercent && (
|
||||
<div style={{ fontSize: `${percentFontSize}px`, fontWeight: 600, color: "#fff", textShadow: "1px 1px 2px rgba(0, 0, 0, 0.8)" }}>
|
||||
{item.changePercent > 0 ? '+' : ''}{item.changePercent.toFixed(2)}{showPercentSign ? '%' : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* COMMUNICATION SERVICES 섹터 산업군 배치 */}
|
||||
{box.name === "COMMUNICATION SERVICES" &&
|
||||
COMMUNICATION_SERVICES_INDUSTRIES.map((industry, idx) => {
|
||||
|
59
백엔드/check_utilities.py
Normal file
59
백엔드/check_utilities.py
Normal file
@ -0,0 +1,59 @@
|
||||
import psycopg2
|
||||
|
||||
# PostgreSQL 연결
|
||||
conn = psycopg2.connect(
|
||||
host="3.38.180.110",
|
||||
port=8088,
|
||||
database="if_invest",
|
||||
user="eldsoft",
|
||||
password="eld240510"
|
||||
)
|
||||
|
||||
cursor = conn.cursor()
|
||||
|
||||
# UTILITIES 섹터의 모든 산업군 확인
|
||||
cursor.execute("""
|
||||
SELECT industry, COUNT(*) as stock_count
|
||||
FROM invest_product_code
|
||||
WHERE sector = 'UTILITIES'
|
||||
AND is_sp500 = 'Y'
|
||||
GROUP BY industry
|
||||
ORDER BY stock_count DESC, industry
|
||||
""")
|
||||
|
||||
print("UTILITIES 섹터 산업군별 주식 개수:")
|
||||
print("=" * 80)
|
||||
|
||||
results = cursor.fetchall()
|
||||
for industry, count in results:
|
||||
print(f"{industry}: {count}개")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
|
||||
# 각 산업군별 상위 2개 주식 확인
|
||||
industries = [
|
||||
"UTILITIES - RENEWABLE",
|
||||
"UTILITIES - INDEPENDENT POWER PRODUCERS",
|
||||
"UTILITIES - DIVERSIFIED",
|
||||
"UTILITIES - REGULATED ELECTRIC"
|
||||
]
|
||||
|
||||
for industry in industries:
|
||||
print(f"\n{industry}:")
|
||||
print("-" * 80)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT invest_code, code_desc_en
|
||||
FROM invest_product_code
|
||||
WHERE sector = 'UTILITIES'
|
||||
AND industry = %s
|
||||
AND is_sp500 = 'Y'
|
||||
ORDER BY invest_code
|
||||
""", (industry,))
|
||||
|
||||
stocks = cursor.fetchall()
|
||||
for ticker, name in stocks:
|
||||
print(f" {ticker}: {name}")
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
Loading…
Reference in New Issue
Block a user