Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 109 additions & 127 deletions docs/KOSLINK-FRONTEND.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default function Header() {
KOS<span style={{ color: 'var(--o-500)' }}>LINK</span>
</b>
<em
className="text-xs font-normal not-italic"
className="text-xs font-medium not-italic"
style={{ color: 'var(--n-500)' }}
>
뉴스로 읽는 종목 관계
Expand Down Expand Up @@ -55,7 +55,7 @@ export default function Header() {
</nav>

<div
className="ml-auto hidden items-center gap-1.5 text-xs sm:flex"
className="ml-auto hidden items-center gap-1.5 text-xs font-medium sm:flex"
style={{ color: 'var(--n-500)' }}
>
<i
Expand Down
50 changes: 28 additions & 22 deletions src/components/analysis/AnalysisPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useAtom, useAtomValue } from 'jotai'
import { selectedNewsIdAtom, verifyContextAtom, viewAtom } from '#/lib/atoms'
import { useNewsAnalysisQuery } from '#/lib/queries'
import { useNewsImpactQuery } from '#/lib/queries'
import MainStockCard from './MainStockCard'
import RelatedList from './RelatedList'
import RationaleBox from './RationaleBox'
Expand All @@ -10,13 +10,16 @@ export default function AnalysisPanel() {
const [, setVerifyContext] = useAtom(verifyContextAtom)
const [, setView] = useAtom(viewAtom)

const { data: analysis } = useNewsAnalysisQuery(selectedNewsId)
const { data: impact } = useNewsImpactQuery(selectedNewsId)

function openVerifyForThisNews() {
if (!analysis) return
if (!impact) return
setVerifyContext({
label: analysis.title,
names: [analysis.main.name, ...analysis.related.map((r) => r.name)],
label: impact.originStocks.map((o) => o.name).join(', '),
names: [
...impact.originStocks.map((o) => o.name),
...impact.relatedStocks.map((r) => r.name),
],
})
setView('verify')
}
Expand All @@ -26,13 +29,13 @@ export default function AnalysisPanel() {
<div className="phead">
<h2>영향 분석</h2>
<p>
{analysis
? `${analysis.sector} · 영향 종목 ${analysis.related.length + 1}개`
{impact
? `영향 종목 ${impact.originStocks.length + impact.relatedStocks.length}개`
: '뉴스 선택 대기 중'}
</p>
</div>
<div className="pbody">
{!analysis ? (
{!impact ? (
<div
className="px-6 py-16 text-center"
style={{ color: 'var(--n-500)' }}
Expand All @@ -43,7 +46,7 @@ export default function AnalysisPanel() {
>
뉴스를 선택해 주세요
</b>
<p className="text-[13px] leading-[1.6]">
<p className="text-[13px] leading-[1.6] font-medium">
선택한 뉴스가 어떤 종목까지
<br />
이어지는지 단계별로 보여드립니다
Expand All @@ -57,23 +60,23 @@ export default function AnalysisPanel() {
>
<div
className="mb-[9px] text-[11.5px] font-bold"
style={{ color: 'var(--n-500)' }}
style={{ color: 'var(--n-600)' }}
>
기사 요약
</div>
<div
className="flex flex-col gap-[7px] rounded-xl px-3.5 py-[13px]"
style={{ background: 'var(--n-50)' }}
>
{analysis.article.summary.map((line, i) => (
{impact.newsSummary.map((line, i) => (
<p
key={i}
className="flex gap-2 text-[13px] leading-[1.6] tracking-[-0.022em]"
style={{ color: 'var(--n-600)' }}
className="flex gap-2 text-[13px] leading-[1.6] tracking-[-0.022em] font-medium"
style={{ color: 'var(--n-700)' }}
>
<b
className="flex-none font-bold"
style={{ color: 'var(--n-400)' }}
style={{ color: 'var(--n-700)' }}
>
{i + 1}
</b>
Expand All @@ -83,14 +86,14 @@ export default function AnalysisPanel() {
</div>
<div
className="mt-2.5 flex items-center gap-[7px] text-xs"
style={{ color: 'var(--n-500)' }}
style={{ color: 'var(--n-600)' }}
>
<b className="font-semibold" style={{ color: 'var(--n-600)' }}>
{analysis.article.press}
{impact.source.press}
</b>
<span>·</span>
<span>
{new Date(analysis.article.publishedAt).toLocaleTimeString(
{new Date(impact.source.publishedAt).toLocaleTimeString(
'ko-KR',
{
hour: '2-digit',
Expand All @@ -99,7 +102,7 @@ export default function AnalysisPanel() {
)}
</span>
<a
href={analysis.article.originUrl}
href={impact.source.url}
target="_blank"
rel="noreferrer"
className="ml-auto text-xs font-bold hover:underline"
Expand All @@ -116,22 +119,25 @@ export default function AnalysisPanel() {
>
<div
className="mb-[9px] text-[11.5px] font-bold"
style={{ color: 'var(--n-500)' }}
style={{ color: 'var(--n-600)' }}
>
영향 기점
</div>
<MainStockCard analysis={analysis} />
<MainStockCard originStocks={impact.originStocks} />
</section>

<section
className="border-b px-[18px] py-4"
style={{ borderColor: 'var(--n-100)' }}
>
<RelatedList analysis={analysis} />
<RelatedList
relatedStocks={impact.relatedStocks}
graph={impact.graph}
/>
</section>

<section className="px-[18px] py-4">
<RationaleBox analysis={analysis} />
<RationaleBox finalSummary={impact.finalSummary} />
</section>

<section className="px-[18px] py-4">
Expand Down
51 changes: 29 additions & 22 deletions src/components/analysis/MainStockCard.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,33 @@
import { arrow, directionLabel, impactOf } from '#/lib/format'
import type { NewsAnalysis } from '#/types'
import type { OriginStock } from '#/types'

/** 영향 기점 — 뉴스가 직접 가리키는 종목 (hop 0) */
export default function MainStockCard({
analysis,
}: {
analysis: NewsAnalysis
}) {
const { main } = analysis
const down = main.direction === 'DOWN'
function OriginCard({ stock }: { stock: OriginStock }) {
const down = stock.direction === 'DOWN'
const accent = down ? 'var(--d-500)' : 'var(--o-500)'
const tint = down ? 'var(--d-50)' : 'var(--o-50)'
const text = down ? 'var(--d-700)' : 'var(--o-700)'
const border = down ? 'var(--d-100)' : 'var(--o-200)'

return (
<div
className="rounded-[15px] border-2 px-4 py-[15px]"
className="origin-card-glow rounded-[15px] border-2 px-4 py-[15px]"
style={{ borderColor: accent, background: tint }}
>
<div className="flex items-center justify-between gap-2.5">
<div className="text-lg font-extrabold tracking-[-0.035em]">
{main.name}
{main.ticker && (
<small
className="ml-1.5 text-xs font-semibold"
style={{ color: 'var(--n-500)' }}
>
{main.ticker}
</small>
)}
{stock.name}
<small
className="ml-1.5 text-xs font-semibold"
style={{ color: 'var(--n-500)' }}
>
{stock.ticker}
</small>
</div>
<span
className="flex-none rounded-lg px-2.5 py-1 text-[12.5px] font-extrabold text-white"
style={{ background: accent }}
>
{arrow(main.direction)} {directionLabel(main.direction)}
{arrow(stock.direction)} {directionLabel(stock.direction)}
</span>
</div>
<span
Expand All @@ -45,11 +37,26 @@ export default function MainStockCard({
{impactOf(0).label}
</span>
<p
className="mt-[11px] text-[12.5px] leading-[1.55] tracking-[-0.022em]"
className="mt-[11px] text-[12.5px] leading-[1.55] font-medium tracking-[-0.022em]"
style={{ color: text }}
>
{main.reason}
{stock.reason}
</p>
</div>
)
}

/** 영향 기점 — 뉴스에 직접 언급된 당사자 종목(들). 대부분 1개지만 여러 개일 수 있다. */
export default function MainStockCard({
originStocks,
}: {
originStocks: OriginStock[]
}) {
return (
<div className="flex flex-col gap-2.5">
{originStocks.map((stock) => (
<OriginCard key={stock.ticker} stock={stock} />
))}
</div>
)
}
63 changes: 13 additions & 50 deletions src/components/analysis/RationaleBox.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,9 @@
import type { NewsAnalysis } from '#/types'

const ROWS: {
key: string
label: string
field: keyof NewsAnalysis['rationale']
html: boolean
}[] = [
{ key: 'event', label: '이벤트', field: 'event', html: false },
{ key: 'propagation', label: '파급 경로', field: 'propagation', html: true },
{ key: 'precedent', label: '과거 사례', field: 'precedent', html: true },
]

/**
* 판단 근거 3행. propagation/precedent는 서버가 그래프 경로에서 템플릿 생성한
* 신뢰 가능한 정적 문자열이라 굵게 표시된 종목명을 위해 dangerouslySetInnerHTML을
* 쓴다(사용자 입력이 섞이지 않는다).
*/
export default function RationaleBox({ analysis }: { analysis: NewsAnalysis }) {
/** 최종 요약 — 영향 기점과 파급된 관련주 수를 한 줄로 정리한 서버 생성 문구. */
export default function RationaleBox({
finalSummary,
}: {
finalSummary: string
}) {
return (
<div
className="rounded-2xl border px-4 py-[15px]"
Expand All @@ -30,38 +17,14 @@ export default function RationaleBox({ analysis }: { analysis: NewsAnalysis }) {
className="inline-block h-3 w-[3px] rounded-sm"
style={{ background: 'var(--o-500)' }}
/>
판단 근거
최종 요약
</h3>
{ROWS.map((row, i) => (
<div
key={row.key}
className="flex gap-2.5 py-2"
style={i === 0 ? undefined : { borderTop: '1px solid var(--o-100)' }}
>
<div
className="w-[58px] flex-none text-[11.5px] font-extrabold"
style={{ color: 'var(--o-600)' }}
>
{row.label}
</div>
{row.html ? (
<div
className="text-[12.5px] leading-[1.62] tracking-[-0.022em]"
style={{ color: 'var(--n-700)' }}
dangerouslySetInnerHTML={{
__html: analysis.rationale[row.field],
}}
/>
) : (
<div
className="text-[12.5px] leading-[1.62] tracking-[-0.022em]"
style={{ color: 'var(--n-700)' }}
>
{analysis.rationale[row.field]}
</div>
)}
</div>
))}
<p
className="text-[12.5px] leading-[1.62] font-medium tracking-[-0.022em]"
style={{ color: 'var(--n-700)' }}
>
{finalSummary}
</p>
</div>
)
}
Loading