# krx-rs: KRX Open API Rust 클라이언트 상세 설계 명세서

## 📋 목차

1. [개요](#1-개요)
2. [프로젝트 구조](#2-프로젝트-구조)
3. [핵심 의존성](#3-핵심-의존성)
4. [오류 처리](#4-오류-처리)
5. [로깅 전략](#5-로깅-전략)
6. [핵심 클라이언트](#6-핵심-클라이언트)
7. [API 빌더 패턴](#7-api-빌더-패턴)
8. [데이터 구조체](#8-데이터-구조체)
9. [사용 예제](#9-사용-예제)
10. [개발 가이드라인](#10-개발-가이드라인)

---

## 1. 개요

`krx-rs`는 한국거래소(KRX) Open API를 위한 현대적이고 타입 안전한 Rust 클라이언트 라이브러리입니다.

### 주요 특징
- ✅ **타입 안전성**: Rust의 강력한 타입 시스템 활용
- ✅ **비동기 지원**: tokio 기반의 비동기 API
- ✅ **빌더 패턴**: 직관적이고 유연한 API 구성
- ✅ **데이터 처리**: Polars DataFrame으로 즉시 사용 가능한 데이터 제공
- ✅ **명확한 오류 처리**: 상세한 오류 타입과 컨텍스트 제공

---

## 2. 프로젝트 구조

```
krx-rs/
├── Cargo.toml              # 프로젝트 설정 및 의존성
├── src/
│   ├── lib.rs             # 라이브러리 진입점
│   ├── client.rs          # HTTP 클라이언트 핵심 로직
│   ├── error.rs           # 오류 타입 정의
│   ├── data/              # API 응답 데이터 구조체
│   │   ├── mod.rs         # 공통 데이터 타입
│   │   ├── stock.rs       # 주식 관련 응답
│   │   ├── index.rs       # 지수 관련 응답
│   │   ├── bond.rs        # 채권 관련 응답
│   │   ├── etp.rs         # ETP (ETF/ETN/ELW) 응답
│   │   ├── derivative.rs  # 파생상품 응답
│   │   └── general.rs     # 일반상품 응답
│   └── api/               # API 엔드포인트 빌더
│       ├── mod.rs         # API 모듈 진입점
│       ├── stock.rs       # 주식 API
│       ├── index.rs       # 지수 API
│       ├── bond.rs        # 채권 API
│       ├── etp.rs         # ETP API
│       ├── derivative.rs  # 파생상품 API
│       └── general.rs     # 일반상품 API
├── examples/              # 사용 예제
│   ├── simple_fetch.rs    # 기본 사용법
│   └── advanced_query.rs  # 고급 기능
└── tests/                 # 테스트 코드
    ├── integration/       # 통합 테스트
    └── unit/             # 단위 테스트
```

---

## 3. 핵심 의존성

### Cargo.toml

```toml
[package]
name = "krx-rs"
version = "0.1.0"
edition = "2024"
authors = ["Your Name <your.email@example.com>"]
license = "MIT OR Apache-2.0"
description = "KRX Open API를 위한 Rust 클라이언트"
repository = "https://github.com/yourusername/krx-rs"
keywords = ["krx", "korea-exchange", "stock", "finance", "api"]
categories = ["api-bindings", "finance"]

[dependencies]
# 비동기 런타임
tokio = { version = "1.45", features = ["full"] }

# HTTP 클라이언트
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }

# 데이터 처리
polars = { version = "0.49", features = ["lazy", "serde", "json", "temporal"] }

# 직렬화/역직렬화
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# 오류 처리
thiserror = "2.0"

# 날짜/시간 처리
chrono = { version = "0.4", features = ["serde"] }

# 로깅
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }

[dev-dependencies]
# 테스트용
tokio-test = "0.4"
wiremock = "0.6"
pretty_assertions = "1.4"

# CLI 예제용
clap = { version = "4.0", features = ["derive"] }
```

---

## 4. 오류 처리

### src/error.rs

```rust
use thiserror::Error;

/// krx-rs 라이브러리의 모든 오류를 포함하는 열거형
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
    /// 네트워크 요청 실패
    #[error("Network request failed: {0}")]
    Network(#[from] reqwest::Error),

    /// API 응답 파싱 오류
    #[error("Failed to parse API response: {details}")]
    Parsing {
        details: String,
        #[source]
        source: serde_json::Error,
        response_body: String,
    },

    /// API 서버 오류 응답
    #[error("API error (status {status_code}): {message}")]
    ApiError {
        status_code: u16,
        message: String,
    },

    /// 잘못된 입력 파라미터
    #[error("Invalid input: {0}")]
    InvalidInput(String),

    /// 데이터프레임 변환 오류
    #[error("DataFrame operation failed")]
    DataFrame(#[from] polars::prelude::PolarsError),

    /// 인증 오류
    #[error("Authentication failed: {0}")]
    Authentication(String),

    /// 속도 제한 초과
    #[error("Rate limit exceeded, retry after {retry_after} seconds")]
    RateLimit { retry_after: u64 },
}

pub type Result<T> = std::result::Result<T, Error>;
```

---

## 5. 로깅 전략

### 5.1 로깅 프레임워크 선택

`krx-rs`는 **tracing** 크레이트를 사용하여 구조화된 로깅을 제공합니다.

#### tracing 선택 이유
- ✅ **구조화된 로깅**: JSON 형태의 구조화된 로그 출력
- ✅ **비동기 지원**: tokio와 완벽한 호환성
- ✅ **성능**: 낮은 오버헤드와 고성능
- ✅ **유연성**: 다양한 출력 포맷과 필터링 지원
- ✅ **분산 추적**: span을 통한 요청 추적

### 5.2 로깅 레벨 정의

| 레벨 | 용도 | 예시 |
|------|------|------|
| **ERROR** | 심각한 오류 | API 인증 실패, 네트워크 연결 오류 |
| **WARN** | 경고 상황 | Rate limiting 발생, 재시도 시도 |
| **INFO** | 일반 정보 | API 요청 시작/완료, 주요 작업 상태 |
| **DEBUG** | 디버깅 정보 | 요청/응답 상세 내용, 파라미터 값 |
| **TRACE** | 상세 추적 | 함수 진입/종료, 내부 처리 과정 |

### 5.3 로깅 구현

#### src/logging.rs

```rust
use tracing::{info, warn, error, debug, trace};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
use std::env;

/// 로깅 설정 구조체
#[derive(Debug, Clone)]
pub struct LoggingConfig {
    /// 로깅 레벨 (기본값: "info")
    pub level: String,
    /// JSON 형태 출력 여부
    pub json_format: bool,
    /// 민감한 정보 필터링 여부
    pub filter_sensitive: bool,
    /// 파일 출력 경로 (옵션)
    pub file_path: Option<String>,
}

impl Default for LoggingConfig {
    fn default() -> Self {
        Self {
            level: "info".to_string(),
            json_format: false,
            filter_sensitive: true,
            file_path: None,
        }
    }
}

/// 로깅 초기화
pub fn init_logging(config: &LoggingConfig) -> Result<(), Box<dyn std::error::Error>> {
    let env_filter = EnvFilter::try_from_default_env()
        .or_else(|_| EnvFilter::try_new(&config.level))?;

    let subscriber = tracing_subscriber::registry()
        .with(env_filter);

    if config.json_format {
        // JSON 형태 출력
        let json_layer = tracing_subscriber::fmt::layer()
            .json()
            .with_current_span(true)
            .with_span_list(true);
        
        subscriber.with(json_layer).try_init()?;
    } else {
        // 일반 텍스트 출력
        let fmt_layer = tracing_subscriber::fmt::layer()
            .with_target(true)
            .with_thread_ids(true)
            .with_file(true)
            .with_line_number(true);
            
        subscriber.with(fmt_layer).try_init()?;
    }

    info!("krx-rs logging initialized with level: {}", config.level);
    Ok(())
}

/// 민감한 정보 필터링 매크로
#[macro_export]
macro_rules! log_filtered {
    ($level:ident, $msg:expr, $($key:ident = $value:expr),*) => {
        $level!(
            message = $msg,
            $(
                $key = if stringify!($key).contains("key") || 
                         stringify!($key).contains("auth") || 
                         stringify!($key).contains("token") {
                    "***FILTERED***"
                } else {
                    &$value.to_string()
                }
            ),*
        );
    };
}
```

### 5.4 클라이언트 로깅 통합

#### src/client.rs (로깅 추가)

```rust
use tracing::{info, warn, error, debug, trace, instrument, Span};
use crate::logging::LoggingConfig;

impl Client {
    /// 로깅 설정과 함께 클라이언트 생성
    pub fn with_logging(auth_key: impl Into<String>, logging_config: LoggingConfig) -> Result<Self> {
        crate::logging::init_logging(&logging_config)?;
        Ok(Self::new(auth_key))
    }

    /// HTTP GET 요청 (로깅 포함)
    #[instrument(skip(self, params), fields(endpoint = %endpoint))]
    pub(crate) async fn get<T>(&self, endpoint: &str, params: &[(&str, &str)]) -> Result<T>
    where
        T: DeserializeOwned,
    {
        let url = format!("{}{}", self.base_url, endpoint);
        let start_time = std::time::Instant::now();
        
        // 요청 시작 로깅
        info!(
            endpoint = %endpoint,
            params_count = params.len(),
            "Starting API request"
        );
        
        debug!(
            url = %url,
            params = ?params,
            "Request details"
        );

        let response = self.http_client
            .get(&url)
            .query(params)
            .send()
            .await
            .map_err(|e| {
                error!(
                    endpoint = %endpoint,
                    error = %e,
                    duration_ms = start_time.elapsed().as_millis(),
                    "Network request failed"
                );
                Error::Network(e)
            })?;

        let status_code = response.status().as_u16();
        let duration = start_time.elapsed();

        if response.status().is_success() {
            let body = response.text().await?;
            
            debug!(
                endpoint = %endpoint,
                status_code = status_code,
                response_size = body.len(),
                duration_ms = duration.as_millis(),
                "Received successful response"
            );

            // 응답 파싱
            match serde_json::from_str(&body) {
                Ok(parsed) => {
                    info!(
                        endpoint = %endpoint,
                        status_code = status_code,
                        duration_ms = duration.as_millis(),
                        "API request completed successfully"
                    );
                    Ok(parsed)
                }
                Err(e) => {
                    error!(
                        endpoint = %endpoint,
                        error = %e,
                        response_body = %body.chars().take(500).collect::<String>(),
                        "Failed to parse response"
                    );
                    Err(Error::Parsing {
                        details: format!("Failed to deserialize response from {}", endpoint),
                        source: e,
                        response_body: body,
                    })
                }
            }
        } else {
            let message = response.text().await.unwrap_or_default();
            
            // Rate limiting 특별 처리
            if status_code == 429 {
                let retry_after = response
                    .headers()
                    .get("retry-after")
                    .and_then(|v| v.to_str().ok())
                    .and_then(|s| s.parse().ok())
                    .unwrap_or(60);
                
                warn!(
                    endpoint = %endpoint,
                    retry_after = retry_after,
                    duration_ms = duration.as_millis(),
                    "Rate limit exceeded"
                );
                
                return Err(Error::RateLimit { retry_after });
            }
            
            error!(
                endpoint = %endpoint,
                status_code = status_code,
                error_message = %message,
                duration_ms = duration.as_millis(),
                "API request failed"
            );
            
            Err(Error::ApiError {
                status_code,
                message,
            })
        }
    }
}

impl ClientBuilder {
    /// 로깅 설정 추가
    pub fn logging(mut self, config: LoggingConfig) -> Self {
        self.logging_config = Some(config);
        self
    }
    
    pub fn build(self) -> Result<Client> {
        // 로깅 초기화
        if let Some(config) = &self.logging_config {
            crate::logging::init_logging(config)?;
        }
        
        // ... 기존 빌드 로직
    }
}
```

### 5.5 사용 예제

#### 기본 로깅 설정

```rust
use krx_rs::{Client, logging::LoggingConfig};
use tracing::info;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 로깅 설정
    let logging_config = LoggingConfig {
        level: "debug".to_string(),
        json_format: false,
        filter_sensitive: true,
        file_path: None,
    };
    
    // 클라이언트 생성 (로깅 포함)
    let client = Client::builder()
        .auth_key(std::env::var("KRX_API_KEY")?)
        .logging(logging_config)
        .build()?;
    
    info!("Starting KRX data collection");
    
    // API 호출 (자동으로 로깅됨)
    let data = client
        .stock()
        .kospi_daily()
        .date("20240105")
        .fetch()
        .await?;
        
    info!(record_count = data.height(), "Data collection completed");
    
    Ok(())
}
```

#### 프로덕션 환경 설정

```rust
// 환경 변수 기반 로깅 설정
let logging_config = LoggingConfig {
    level: std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
    json_format: std::env::var("LOG_FORMAT").unwrap_or_default() == "json",
    filter_sensitive: true,
    file_path: std::env::var("LOG_FILE").ok(),
};

// 구조화된 로깅으로 메트릭 수집
use tracing::{info_span, Instrument};

async fn collect_market_data() -> Result<(), Error> {
    let span = info_span!("market_data_collection", 
        market = "kospi", 
        date = "20240105"
    );
    
    async move {
        // API 호출들...
        info!(metrics.api_calls = 1, metrics.records = 100, "Collection completed");
    }
    .instrument(span)
    .await
}
```

### 5.6 로깅 모범 사례

1. **민감한 정보 보호**
   - 인증키, 토큰 등은 자동 필터링
   - 개인정보는 해시화 또는 마스킹

2. **성능 모니터링**
   - API 응답 시간 측정
   - 처리량 및 오류율 추적

3. **구조화된 로깅**
   - 일관된 필드명 사용
   - 검색 가능한 키-값 형태

4. **환경별 설정**
   - 개발: DEBUG 레벨, 텍스트 형태
   - 프로덕션: INFO 레벨, JSON 형태

---

## 6. 핵심 클라이언트

### src/client.rs

```rust
use crate::{api, error::{Error, Result}};
use reqwest::{Client as HttpClient, header::{HeaderMap, HeaderValue}};
use serde::de::DeserializeOwned;
use std::time::Duration;

const BASE_URL: &str = "http://data-dbg.krx.co.kr/svc/apis";
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);

/// KRX API 클라이언트
pub struct Client {
    http_client: HttpClient,
    auth_key: String,
    base_url: String,
}

impl Client {
    /// 새로운 클라이언트 인스턴스 생성
    ///
    /// # Arguments
    /// * `auth_key` - KRX Open API 인증키
    ///
    /// # Example
    /// ```
    /// let client = Client::new("your_auth_key");
    /// ```
    pub fn new(auth_key: impl Into<String>) -> Self {
        Self::builder()
            .auth_key(auth_key)
            .build()
            .expect("Failed to build client with default settings")
    }

    /// 클라이언트 빌더 반환
    pub fn builder() -> ClientBuilder {
        ClientBuilder::default()
    }

    /// 내부 HTTP GET 요청 처리
    pub(crate) async fn get<T>(&self, endpoint: &str, params: &[(&str, &str)]) -> Result<T>
    where
        T: DeserializeOwned,
    {
        let url = format!("{}{}", self.base_url, endpoint);
        
        let response = self.http_client
            .get(&url)
            .query(params)
            .send()
            .await?;

        if response.status().is_success() {
            let body = response.text().await?;
            serde_json::from_str(&body).map_err(|e| Error::Parsing {
                details: format!("Failed to deserialize response from {}", endpoint),
                source: e,
                response_body: body,
            })
        } else {
            let status = response.status().as_u16();
            let message = response.text().await.unwrap_or_default();
            
            // 속도 제한 체크
            if status == 429 {
                let retry_after = response
                    .headers()
                    .get("retry-after")
                    .and_then(|v| v.to_str().ok())
                    .and_then(|s| s.parse().ok())
                    .unwrap_or(60);
                
                return Err(Error::RateLimit { retry_after });
            }
            
            Err(Error::ApiError {
                status_code: status,
                message,
            })
        }
    }

    /// 주식 API 접근
    pub fn stock(&self) -> api::stock::StockApi {
        api::stock::StockApi::new(self)
    }

    /// 지수 API 접근
    pub fn index(&self) -> api::index::IndexApi {
        api::index::IndexApi::new(self)
    }

    /// 채권 API 접근
    pub fn bond(&self) -> api::bond::BondApi {
        api::bond::BondApi::new(self)
    }

    /// ETP API 접근
    pub fn etp(&self) -> api::etp::EtpApi {
        api::etp::EtpApi::new(self)
    }

    /// 파생상품 API 접근
    pub fn derivative(&self) -> api::derivative::DerivativeApi {
        api::derivative::DerivativeApi::new(self)
    }

    /// 일반상품 API 접근
    pub fn general(&self) -> api::general::GeneralApi {
        api::general::GeneralApi::new(self)
    }
}

/// 클라이언트 빌더
#[derive(Default)]
pub struct ClientBuilder {
    auth_key: Option<String>,
    base_url: Option<String>,
    timeout: Option<Duration>,
    user_agent: Option<String>,
}

impl ClientBuilder {
    /// 인증키 설정
    pub fn auth_key(mut self, key: impl Into<String>) -> Self {
        self.auth_key = Some(key.into());
        self
    }

    /// 기본 URL 재정의 (테스트용)
    pub fn base_url(mut self, url: impl Into<String>) -> Self {
        self.base_url = Some(url.into());
        self
    }

    /// 요청 타임아웃 설정
    pub fn timeout(mut self, timeout: Duration) -> Self {
        self.timeout = Some(timeout);
        self
    }

    /// User-Agent 헤더 설정
    pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
        self.user_agent = Some(user_agent.into());
        self
    }

    /// 클라이언트 빌드
    pub fn build(self) -> Result<Client> {
        let auth_key = self.auth_key
            .ok_or_else(|| Error::InvalidInput("auth_key is required".to_string()))?;

        let mut headers = HeaderMap::new();
        headers.insert("AUTH_KEY", HeaderValue::from_str(&auth_key)
            .map_err(|_| Error::InvalidInput("Invalid auth_key format".to_string()))?);
        headers.insert("Content-Type", HeaderValue::from_static("application/json"));

        let http_client = HttpClient::builder()
            .default_headers(headers)
            .timeout(self.timeout.unwrap_or(DEFAULT_TIMEOUT))
            .user_agent(self.user_agent.unwrap_or_else(|| 
                format!("krx-rs/{}", env!("CARGO_PKG_VERSION"))
            ))
            .build()?;

        Ok(Client {
            http_client,
            auth_key,
            base_url: self.base_url.unwrap_or_else(|| BASE_URL.to_string()),
        })
    }
}
```

---

## 7. API 빌더 패턴

### src/api/stock.rs

```rust
use crate::{client::Client, data::stock::*, error::Result};
use polars::prelude::DataFrame;

/// 주식 관련 API 엔드포인트
pub struct StockApi<'a> {
    client: &'a Client,
}

impl<'a> StockApi<'a> {
    pub(crate) fn new(client: &'a Client) -> Self {
        Self { client }
    }

    /// 유가증권 일별매매정보 조회
    pub fn kospi_daily(&self) -> KospiDailyBuilder<'a> {
        KospiDailyBuilder::new(self.client)
    }

    /// 코스닥 일별매매정보 조회
    pub fn kosdaq_daily(&self) -> KosdaqDailyBuilder<'a> {
        KosdaqDailyBuilder::new(self.client)
    }

    /// 코넥스 일별매매정보 조회
    pub fn konex_daily(&self) -> KonexDailyBuilder<'a> {
        KonexDailyBuilder::new(self.client)
    }

    /// 유가증권 종목기본정보 조회
    pub fn kospi_info(&self) -> KospiInfoBuilder<'a> {
        KospiInfoBuilder::new(self.client)
    }
}

/// 유가증권 일별매매정보 빌더
#[must_use = "Builder does nothing unless you call .fetch()"]
pub struct KospiDailyBuilder<'a> {
    client: &'a Client,
    base_date: Option<String>,
}

impl<'a> KospiDailyBuilder<'a> {
    fn new(client: &'a Client) -> Self {
        Self {
            client,
            base_date: None,
        }
    }

    /// 조회 기준일자 설정 (YYYYMMDD)
    ///
    /// # Example
    /// ```
    /// .date("20240105")
    /// ```
    pub fn date(mut self, date: impl Into<String>) -> Self {
        self.base_date = Some(date.into());
        self
    }

    /// 오늘 날짜로 설정
    pub fn today(mut self) -> Self {
        use chrono::Local;
        self.base_date = Some(Local::now().format("%Y%m%d").to_string());
        self
    }

    /// API 호출 및 데이터 조회
    pub async fn fetch(self) -> Result<DataFrame> {
        let base_date = self.base_date
            .ok_or_else(|| Error::InvalidInput("date is required".to_string()))?;

        // 날짜 형식 검증
        if !is_valid_date_format(&base_date) {
            return Err(Error::InvalidInput(
                "date must be in YYYYMMDD format".to_string()
            ));
        }

        let response: ApiResponse<KospiDailyRecord> = self.client
            .get(
                "/sto/stk_bydd_trd",
                &[("basDd", &base_date)],
            )
            .await?;

        parse_kospi_daily(response)
    }
}

/// 날짜 형식 검증 (YYYYMMDD)
fn is_valid_date_format(date: &str) -> bool {
    date.len() == 8 && date.chars().all(|c| c.is_numeric())
}
```

---

## 8. 데이터 구조체

### src/data/mod.rs

```rust
use serde::{Deserialize, Deserializer};

pub mod stock;
pub mod index;
pub mod bond;
pub mod etp;
pub mod derivative;
pub mod general;

/// KRX API 공통 응답 구조
#[derive(Debug, Deserialize)]
pub struct ApiResponse<T> {
    #[serde(rename = "OutBlock_1")]
    pub data: Vec<T>,
}

/// 문자열을 Option<f64>로 파싱하는 헬퍼
pub fn deserialize_optional_number<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
where
    D: Deserializer<'de>,
{
    let s: String = Deserialize::deserialize(deserializer)?;
    if s.is_empty() || s == "-" {
        Ok(None)
    } else {
        s.replace(',', "")
            .parse::<f64>()
            .map(Some)
            .map_err(serde::de::Error::custom)
    }
}
```

### src/data/stock.rs

```rust
use super::{ApiResponse, deserialize_optional_number};
use polars::prelude::*;
use serde::Deserialize;
use crate::error::Result;

/// 유가증권 일별매매정보 레코드
#[derive(Debug, Deserialize)]
pub struct KospiDailyRecord {
    /// 기준일자 (YYYY/MM/DD)
    #[serde(rename = "BAS_DD")]
    pub base_date: String,
    
    /// 종목코드
    #[serde(rename = "ISU_CD")]
    pub issue_code: String,
    
    /// 종목명
    #[serde(rename = "ISU_NM")]
    pub issue_name: String,
    
    /// 시장구분
    #[serde(rename = "MKT_NM")]
    pub market_name: String,
    
    /// 소속부
    #[serde(rename = "SECT_TP_NM")]
    pub sector_type: String,
    
    /// 종가
    #[serde(rename = "TDD_CLSPRC")]
    pub close_price: String,
    
    /// 대비
    #[serde(rename = "CMPPREVDD_PRC")]
    pub price_change: String,
    
    /// 등락률
    #[serde(rename = "FLUC_RT")]
    pub fluctuation_rate: String,
    
    /// 시가
    #[serde(rename = "TDD_OPNPRC")]
    pub open_price: String,
    
    /// 고가
    #[serde(rename = "TDD_HGPRC")]
    pub high_price: String,
    
    /// 저가
    #[serde(rename = "TDD_LWPRC")]
    pub low_price: String,
    
    /// 거래량
    #[serde(rename = "ACC_TRDVOL")]
    pub trading_volume: String,
    
    /// 거래대금
    #[serde(rename = "ACC_TRDVAL")]
    pub trading_value: String,
    
    /// 시가총액
    #[serde(rename = "MKTCAP")]
    pub market_cap: String,
    
    /// 상장주식수
    #[serde(rename = "LIST_SHRS")]
    pub listed_shares: String,
}

/// KOSPI 일별매매정보를 DataFrame으로 변환
pub fn parse_kospi_daily(response: ApiResponse<KospiDailyRecord>) -> Result<DataFrame> {
    let records = response.data;
    
    if records.is_empty() {
        return Ok(DataFrame::empty());
    }
    
    // 각 필드를 벡터로 수집
    let mut dates = Vec::with_capacity(records.len());
    let mut codes = Vec::with_capacity(records.len());
    let mut names = Vec::with_capacity(records.len());
    let mut close_prices = Vec::with_capacity(records.len());
    let mut changes = Vec::with_capacity(records.len());
    let mut change_rates = Vec::with_capacity(records.len());
    let mut volumes = Vec::with_capacity(records.len());
    let mut values = Vec::with_capacity(records.len());
    let mut market_caps = Vec::with_capacity(records.len());
    
    for record in records {
        dates.push(record.base_date);
        codes.push(record.issue_code);
        names.push(record.issue_name);
        close_prices.push(parse_number(&record.close_price));
        changes.push(parse_number(&record.price_change));
        change_rates.push(parse_number(&record.fluctuation_rate));
        volumes.push(parse_number(&record.trading_volume));
        values.push(parse_number(&record.trading_value));
        market_caps.push(parse_number(&record.market_cap));
    }
    
    // DataFrame 생성
    let df = df! {
        "날짜" => &dates,
        "종목코드" => &codes,
        "종목명" => &names,
        "종가" => &close_prices,
        "대비" => &changes,
        "등락률" => &change_rates,
        "거래량" => &volumes,
        "거래대금" => &values,
        "시가총액" => &market_caps,
    }?;
    
    Ok(df)
}

/// 문자열을 숫자로 파싱 (콤마 제거)
fn parse_number(s: &str) -> Option<f64> {
    if s.is_empty() || s == "-" {
        None
    } else {
        s.replace(',', "").parse().ok()
    }
}
```

---

## 9. 사용 예제

### examples/simple_fetch.rs

```rust
use krx_rs::{Client, Error};
use std::env;

#[tokio::main]
async fn main() -> Result<(), Error> {
    // 환경 변수에서 인증키 읽기
    let auth_key = env::var("KRX_API_KEY")
        .expect("KRX_API_KEY 환경 변수를 설정해주세요");
    
    // 클라이언트 생성
    let client = Client::new(auth_key);
    
    // KOSPI 일별 매매정보 조회
    println!("📊 2024년 1월 5일 KOSPI 시장 데이터 조회 중...");
    
    let df = client
        .stock()
        .kospi_daily()
        .date("20240105")
        .fetch()
        .await?;
    
    // 결과 출력
    println!("\n조회 결과 (상위 10개):");
    println!("{}", df.head(Some(10)));
    
    // 거래대금 상위 5개 종목
    let top_5 = df
        .lazy()
        .sort("거래대금", SortOptions::default().with_descending(true))
        .limit(5)
        .collect()?;
    
    println!("\n💰 거래대금 TOP 5:");
    println!("{}", top_5);
    
    Ok(())
}
```

### examples/advanced_query.rs

```rust
use krx_rs::{Client, Error};
use polars::prelude::*;
use chrono::{Local, Duration};

#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = Client::builder()
        .auth_key(std::env::var("KRX_API_KEY")?)
        .timeout(std::time::Duration::from_secs(60))
        .build()?;
    
    // 최근 5일간의 데이터 수집
    let mut all_data = Vec::new();
    let today = Local::now().date_naive();
    
    for i in 0..5 {
        let date = today - Duration::days(i);
        let date_str = date.format("%Y%m%d").to_string();
        
        match client.stock().kospi_daily().date(&date_str).fetch().await {
            Ok(df) => {
                println!("✅ {} 데이터 수집 완료", date_str);
                all_data.push(df);
            }
            Err(e) => {
                eprintln!("❌ {} 데이터 수집 실패: {}", date_str, e);
            }
        }
        
        // Rate limit 방지
        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    }
    
    // 데이터 병합
    if !all_data.is_empty() {
        let combined = concat(&all_data, true, true)?;
        
        // 분석: 종목별 평균 거래량
        let analysis = combined
            .lazy()
            .group_by(&[col("종목명")])
            .agg(&[
                col("거래량").mean().alias("평균거래량"),
                col("등락률").mean().alias("평균등락률"),
            ])
            .sort("평균거래량", SortOptions::default().with_descending(true))
            .limit(10)
            .collect()?;
        
        println!("\n📈 최근 5일 평균 거래량 TOP 10:");
        println!("{}", analysis);
    }
    
    Ok(())
}
```

---

## 10. 개발 가이드라인

### 코드 스타일
- Rust 공식 스타일 가이드 준수 (`cargo fmt`)
- Clippy 경고 모두 해결 (`cargo clippy`)
- 모든 공개 API에 문서화 주석 작성

### 테스트
- 각 API 엔드포인트별 단위 테스트 작성
- Mock 서버를 사용한 통합 테스트
- 실제 API 호출 예제는 `examples/` 디렉토리에

### 버전 관리
- Semantic Versioning 준수
- 변경사항은 CHANGELOG.md에 기록
- Breaking changes는 메이저 버전 업데이트

### 성능 고려사항
- 대량 데이터 처리 시 스트리밍 지원
- 연결 재사용을 위한 Client 재사용 권장
- Rate limiting 자동 처리

### 보안
- 인증키는 환경 변수나 설정 파일로 관리
- HTTPS 사용 (가능한 경우)
- 민감한 정보는 로그에 남기지 않음 (자동 필터링 적용)

### 로깅 가이드라인
- 적절한 로깅 레벨 선택 (개발시 DEBUG, 운영시 INFO)
- 구조화된 로깅으로 검색 및 분석 편의성 제공
- 성능에 영향을 주지 않는 효율적인 로깅
- 민감한 정보 자동 필터링으로 보안 강화

---

이 설계서는 `krx-rs` 라이브러리의 전체적인 구조와 구현 방향을 제시합니다. 각 모듈과 기능은 확장 가능하도록 설계되었으며, 사용자 친화적인 API를 제공하는 것을 목표로 합니다.