refactor(portfolio): formatPriceTime util + PriceSessionBadge 추출 + 테스트

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS
This commit is contained in:
2026-07-10 10:10:42 +09:00
parent afc28c64cf
commit 81c9a5d692
5 changed files with 55 additions and 20 deletions

View File

@@ -5,26 +5,7 @@ import {
Tooltip as ChartTooltip, Tooltip as ChartTooltip,
} from 'recharts'; } from 'recharts';
import { formatNumber, formatPercent, toNumeric, profitColorClass, numFitClass } from '../stockUtils'; import { formatNumber, formatPercent, toNumeric, profitColorClass, numFitClass } from '../stockUtils';
import PriceSessionBadge from './portfolio/PriceSessionBadge';
const formatPriceTime = (iso) => {
if (!iso) return '';
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return '';
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
};
const PriceSessionBadge = ({ session, asOf }) => {
if (session !== 'NXT_AFTER' && session !== 'NXT_PRE') return null;
const isPre = session === 'NXT_PRE';
const label = isPre ? 'NXT 프리' : 'NXT';
const desc = isPre ? 'NXT 프리마켓 거래가' : 'NXT 야간거래 (15:30~20:00)';
const time = formatPriceTime(asOf);
return (
<span className="pf-nxt-badge" title={time ? `${desc} · ${time}` : desc}>
{label}
</span>
);
};
const PortfolioTab = ({ pf, asset, handleSell, handleSaveSnapshot }) => ( const PortfolioTab = ({ pf, asset, handleSell, handleSaveSnapshot }) => (
<> <>

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { formatPriceTime } from './portfolioUtils';
const PriceSessionBadge = ({ session, asOf }) => {
if (session !== 'NXT_AFTER' && session !== 'NXT_PRE') return null;
const isPre = session === 'NXT_PRE';
const label = isPre ? 'NXT 프리' : 'NXT';
const desc = isPre ? 'NXT 프리마켓 거래가' : 'NXT 야간거래 (15:30~20:00)';
const time = formatPriceTime(asOf);
return (
<span className="pf-nxt-badge" title={time ? `${desc} · ${time}` : desc}>
{label}
</span>
);
};
export default PriceSessionBadge;

View File

@@ -0,0 +1,17 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import PriceSessionBadge from './PriceSessionBadge.jsx';
describe('PriceSessionBadge', () => {
it('NXT_AFTER → "NXT", NXT_PRE → "NXT 프리"', () => {
const { unmount } = render(<PriceSessionBadge session="NXT_AFTER" />);
expect(screen.getByText('NXT')).toBeInTheDocument();
unmount();
render(<PriceSessionBadge session="NXT_PRE" />);
expect(screen.getByText('NXT 프리')).toBeInTheDocument();
});
it('그 외 세션 → 렌더 없음', () => {
const { container } = render(<PriceSessionBadge session="REGULAR" />);
expect(container).toBeEmptyDOMElement();
});
});

View File

@@ -0,0 +1,6 @@
export const formatPriceTime = (iso) => {
if (!iso) return '';
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return '';
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
};

View File

@@ -0,0 +1,14 @@
import { describe, it, expect } from 'vitest';
import { formatPriceTime } from './portfolioUtils.js';
describe('formatPriceTime', () => {
it('유효 ISO → HH:MM (제로패드)', () => {
expect(formatPriceTime('2026-07-10T09:05:00')).toBe('09:05');
expect(formatPriceTime('2026-07-10T18:30:00')).toBe('18:30');
});
it('falsy/invalid → 빈 문자열', () => {
expect(formatPriceTime('')).toBe('');
expect(formatPriceTime(null)).toBe('');
expect(formatPriceTime('not-a-date')).toBe('');
});
});