Files
auto-trade/app/api/kis/domestic/search/route.ts

115 lines
4.0 KiB
TypeScript
Raw Normal View History

2026-02-11 16:31:28 +09:00
import { KOREAN_STOCK_INDEX } from "@/features/trade/data/korean-stocks";
2026-02-06 17:50:35 +09:00
import type {
DashboardStockSearchItem,
DashboardStockSearchResponse,
KoreanStockIndexItem,
2026-02-11 16:31:28 +09:00
} from "@/features/trade/types/trade.types";
import { hasKisApiSession } from "@/app/api/kis/_session";
2026-02-06 17:50:35 +09:00
import { NextRequest, NextResponse } from "next/server";
const SEARCH_LIMIT = 10;
/**
* @file app/api/kis/domestic/search/route.ts
* @description / API
* @remarks
* - [] API Route
* - [ ] /
* - [ ] dashboard-main.tsx -> /api/kis/domestic/search -> KOREAN_STOCK_INDEX / -> JSON
2026-02-11 16:31:28 +09:00
* - [ ] features/trade/data/korean-stocks.ts, features/trade/components/dashboard-main.tsx
2026-02-06 17:50:35 +09:00
* @author jihoon87.lee
*/
/**
* API
* @param request query string의 q()
* @returns
2026-02-11 16:31:28 +09:00
* @see features/trade/components/dashboard-main.tsx .
2026-02-06 17:50:35 +09:00
*/
export async function GET(request: NextRequest) {
const hasSession = await hasKisApiSession();
if (!hasSession) {
return NextResponse.json({ error: "로그인이 필요합니다." }, { status: 401 });
}
2026-02-06 17:50:35 +09:00
// [Step 1] query string에서 검색어(q)를 읽고 공백을 제거합니다.
const { searchParams } = new URL(request.url);
const query = (searchParams.get("q") ?? "").trim();
// [Step 2] 검색어가 없으면 빈 목록을 즉시 반환해 불필요한 계산을 줄입니다.
if (!query) {
const response: DashboardStockSearchResponse = {
query,
items: [],
total: 0,
};
return NextResponse.json(response);
}
const normalized = normalizeKeyword(query);
// [Step 3] 인덱스에서 코드/이름 포함 여부로 1차 필터링 후 점수를 붙입니다.
const ranked = KOREAN_STOCK_INDEX.filter((item) => {
const symbol = item.symbol;
const name = normalizeKeyword(item.name);
return symbol.includes(normalized) || name.includes(normalized);
})
.map((item) => ({
item,
score: getSearchScore(item, normalized),
}))
.filter((entry) => entry.score > 0)
.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
if (a.item.market !== b.item.market) return a.item.market.localeCompare(b.item.market);
return a.item.name.localeCompare(b.item.name, "ko");
});
// [Step 4] UI에서 필요한 최소 필드만 남겨 SEARCH_LIMIT 만큼 반환합니다.
const items: DashboardStockSearchItem[] = ranked.slice(0, SEARCH_LIMIT).map(({ item }) => ({
symbol: item.symbol,
name: item.name,
market: item.market,
}));
const response: DashboardStockSearchResponse = {
query,
items,
total: ranked.length,
};
// [Step 5] DashboardStockSearchResponse 형태로 응답합니다.
return NextResponse.json(response);
}
/**
* ( + )
* @param value
* @returns
* @see app/api/kis/domestic/search/route.ts / .
*/
function normalizeKeyword(value: string) {
return value.replaceAll(/\s+/g, "").toLowerCase();
}
/**
*
* @param item
* @param normalizedQuery
* @returns
* @see app/api/kis/domestic/search/route.ts .
*/
function getSearchScore(item: KoreanStockIndexItem, normalizedQuery: string) {
const normalizedName = normalizeKeyword(item.name);
const normalizedSymbol = item.symbol.toLowerCase();
if (normalizedSymbol === normalizedQuery) return 120;
if (normalizedName === normalizedQuery) return 110;
if (normalizedSymbol.startsWith(normalizedQuery)) return 100;
if (normalizedName.startsWith(normalizedQuery)) return 90;
if (normalizedName.includes(normalizedQuery)) return 70;
if (normalizedSymbol.includes(normalizedQuery)) return 60;
return 0;
}