Skip to content

Commit b1128f4

Browse files
seapyclaude
andcommitted
Initial commit
DART OpenAPI CLI 도구 초기 구현. - 기업 검색, 개황, 공시 목록, 재무정보, 공시 원문 조회 기능 - DART XML → 마크다운 렌더링 (한글 꺽쇠 등 비표준 XML 처리 포함) - Corp code 로컬 캐싱 (7일 자동 갱신) - GitHub Actions + GoReleaser 릴리즈 자동화 - curl 원라이너 설치 스크립트 (macOS/Linux) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0 parents  commit b1128f4

38 files changed

Lines changed: 2891 additions & 0 deletions

.github/workflows/release.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0
18+
19+
- uses: actions/setup-go@v5
20+
with:
21+
go-version-file: go.mod
22+
cache: true
23+
24+
- uses: goreleaser/goreleaser-action@v6
25+
with:
26+
version: "~> v2"
27+
args: release --clean
28+
env:
29+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# 빌드 결과물
2+
/dartcli
3+
/dartcli.exe
4+
5+
# 환경변수 / API 키
6+
/.env
7+
8+
# 개발 중 루트에서 view --download 로 내려받은 공시 ZIP
9+
/*.zip
10+
11+
# macOS
12+
.DS_Store
13+
14+
# GoReleaser 빌드 디렉토리
15+
/dist/

.goreleaser.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
version: 2
2+
3+
before:
4+
hooks:
5+
- go mod tidy
6+
7+
builds:
8+
- id: dartcli
9+
binary: dartcli
10+
ldflags:
11+
- -s -w
12+
- -X github.com/seapy/dartcli/pkg/dartcli.Version={{.Version}}
13+
- -X github.com/seapy/dartcli/pkg/dartcli.Commit={{.Commit}}
14+
- -X github.com/seapy/dartcli/pkg/dartcli.BuildDate={{.Date}}
15+
goos:
16+
- linux
17+
- darwin
18+
- windows
19+
goarch:
20+
- amd64
21+
- arm64
22+
ignore:
23+
- goos: windows
24+
goarch: arm64
25+
26+
archives:
27+
- id: dartcli
28+
formats:
29+
- tar.gz
30+
format_overrides:
31+
- goos: windows
32+
formats:
33+
- zip
34+
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
35+
files:
36+
- README.md
37+
38+
checksum:
39+
name_template: "checksums.txt"
40+
41+
release:
42+
github:
43+
owner: seapy
44+
name: dartcli
45+
name_template: "v{{.Version}}"
46+
draft: false
47+
prerelease: auto
48+
49+
changelog:
50+
sort: asc
51+
filters:
52+
exclude:
53+
- "^docs:"
54+
- "^test:"
55+
- "^chore:"
56+
- Merge pull request
57+
- Merge branch

CLAUDE.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Commands
6+
7+
```bash
8+
make build # build binary (injects version/commit/date via ldflags)
9+
make install # install to $GOPATH/bin
10+
make test # go test ./...
11+
make lint # golangci-lint run ./...
12+
make clean # remove binary
13+
14+
go build -o dartcli . # quick dev build (no version injection)
15+
go test ./internal/render/... # run tests for a specific package
16+
```
17+
18+
The binary requires a DART API key at runtime. Set `DART_API_KEY` env var or use `~/.dartcli/config.yaml`.
19+
20+
## Architecture
21+
22+
```
23+
main.go → cmd.Execute()
24+
cmd/ CLI layer (cobra commands, global state)
25+
internal/ Core logic (no direct CLI dependencies)
26+
api/ DART OpenAPI HTTP client
27+
cache/ Corp code cache (~12k companies, persisted at ~/.dartcli/cache/corpcode.json)
28+
config/ Viper-based config (~/.dartcli/config.yaml + env vars)
29+
render/ API response → markdown, DART XML → markdown
30+
httpclient/ HTTP client with relaxed TLS (DART servers use legacy TLS 1.0+)
31+
pkg/dartcli/ Version metadata (injected by ldflags at build time)
32+
```
33+
34+
### cmd/root.go — shared state
35+
36+
`root.go` owns three globals used across all commands:
37+
- `cfg` — loaded once via `cobra.OnInitialize`
38+
- `renderer` — glamour-based terminal renderer
39+
- `corpStore` — lazy-loaded on first corp lookup (`loadCorpStore()`)
40+
41+
`resolveCorpCode(query)` searches the corp store (exact stock code → exact corp code → exact name → substring), and if multiple matches are found presents an interactive `huh.Select` prompt.
42+
43+
### internal/api — DART OpenAPI client
44+
45+
All endpoints are under `https://opendart.fss.or.kr`. Status `"000"` = success; `"013"` = no data (treated as empty result, not an error). The client injects the API key as a query parameter on every request.
46+
47+
Financial period codes: `11011` annual, `11013` Q1, `11012` half-year, `11014` Q3.
48+
49+
### internal/cache — corp code store
50+
51+
Downloads `GET /api/corpCode.xml` (returns a ZIP containing CORPCODE.xml), parses the XML, and saves as JSON. Auto-refreshes when older than 7 days; falls back to stale data with a warning if the refresh fails. The in-memory `Store` has three indices: by corp code, by lowercase name, by stock code.
52+
53+
### internal/render/document.go — DART XML → markdown
54+
55+
This is the most complex file. DART discloses documents as ZIP archives containing proprietary `dart4` XML. The parser uses a streaming `xml.Decoder` with an element-name stack to produce markdown:
56+
57+
- **`sanitizeXML()`** must run before decoding — DART XML routinely embeds Korean text in bare angle brackets (e.g. `<이사ㆍ감사 보수현황>`, `< TV 시장점유율 >`) that are visually used as emphasis markers but cause Go's xml decoder to fatal-error. The sanitizer replaces any `<` not followed by a valid XML name start character (`[a-zA-Z_:/?!>]`) with `&lt;`.
58+
- **Do not set `dec.AutoClose = xml.HTMLAutoClose`** — DART XML uses explicit `<IMG>...</IMG>` pairs; auto-closing IMG causes a "unexpected end element `</IMG>`" fatal error.
59+
- `LIBRARY` is a transparent container (like `SECTION-*`), not metadata to skip. It wraps entire document sections in complex reports.
60+
- `SECTION-N` depth determines heading level: `SECTION-1``##`, `SECTION-2``###`, etc. The heading level is derived from the stack at the time a `<TITLE>` element is encountered.
61+
- Single-cell single-row tables are rendered as paragraphs (avoids glamour bullet rendering).
62+
- `DocumentFromZIP` reads up to 3 largest XML/HTML files from the ZIP, sorted by: rcptNo-matching filename first, then by size descending.
63+
64+
### Rendering pipeline
65+
66+
`renderer.Print(md)` uses glamour with TTY detection (`go-isatty`) and auto terminal width (capped at 120). `renderer.PrintWide(md)` sets `WithWordWrap(0)` — used for document view to prevent wide financial table truncation.
67+
68+
Financial amounts are formatted in 억원 (÷100,000,000) when ≥ 1억, otherwise 만원.
69+
70+
## Key DART API quirks
71+
72+
- The API key goes in the query string as `crtfc_key`.
73+
- The corp code (`corp_code`) is an 8-digit zero-padded string, distinct from the 6-digit stock ticker (`stock_code`).
74+
- `GET /api/document.xml?rcpNo=<접수번호>` returns a ZIP (despite the `.xml` endpoint name).
75+
- ZIPs for 사업보고서 can contain multiple XML files (e.g. main + two supplementary audit files). All up to 3 files are rendered in sequence.

Makefile

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
BINARY := dartcli
2+
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
3+
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "none")
4+
DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
5+
PKG := github.com/seapy/dartcli/pkg/dartcli
6+
7+
LDFLAGS := -ldflags "-X $(PKG).Version=$(VERSION) -X $(PKG).Commit=$(COMMIT) -X $(PKG).BuildDate=$(DATE)"
8+
9+
.PHONY: build clean install test lint
10+
11+
build:
12+
go build $(LDFLAGS) -o $(BINARY) .
13+
14+
install:
15+
go install $(LDFLAGS) .
16+
17+
clean:
18+
rm -f $(BINARY)
19+
20+
test:
21+
go test ./...
22+
23+
lint:
24+
golangci-lint run ./...
25+
26+
.DEFAULT_GOAL := build

0 commit comments

Comments
 (0)