feat(export): 메시지/통계 export 명령 추가#19
Conversation
- messages export, statistics export-daily 신규 (CSV/JSON/JSONL) - 7일 초과 범위 자동 1일 윈도우 분할 + 페이지/윈도우 throttle (기본 500ms) - 6개월 lookback / page-size 상한 (messages 200, statistics 100) / 100ms throttle 최소 강제로 messages-v4 단일 큰 호출 (limit=500+31일) 회피 - --append, --resume-token, --bom, --progress auto|on|off 지원 - 신규 패키지: pkg/output (CSV/JSON/JSONL writer), pkg/progress (한국어 진행률 UI), pkg/clock (테스트용 Clock 추상화), pkg/exporter (윈도우 분할 + 페이지 루프 엔진) - types.FormatThousands로 cmd/balance, pkg/progress 천 단위 콤마 단일화 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI golangci-lint v2.11.4의 ineffassign 검사기가 다음 라인을 잡았다: got = append(got, 7*time.Hour) // 그 후 사용 안 함 원래 의도는 "반환된 슬라이스를 변조해도 내부 상태에 영향이 없어야 한다"는 defensive copy 검증이었으나, append 결과 변수를 사용하지 않아 lint 경고가 발생했다. append 결과의 길이를 명시적으로 검증하도록 변경해 의도를 명확히 하면서 linter도 통과한다. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request implements a comprehensive export system for message logs and daily statistics, allowing users to retrieve large datasets in CSV, JSON, or JSONL formats. Key features include automatic splitting of large date ranges into daily windows, configurable throttling to manage server load, and a resume-token mechanism to recover from interrupted processes. The review feedback identified several technical improvements: correctly handling UTF-8 BOMs during CSV header validation, using rune-based iteration for safe string sanitization, and mitigating potential memory issues when generating dynamic CSV headers for statistics. It was also suggested to move the FormatThousands utility to a more generic package to enhance code structure.
Go 1.26.1 toolchain의 go fix가 다음을 적용:
- 수동 max-clamp 패턴(`if x > y { x = y }`)을 Go 1.21+ built-in `min(...)`으로
대체 (cmd/send.go, internal/version/semver.go)
- `boolPtr(v bool) *bool { return &v }` helper에 `//go:fix inline` 디렉티브
추가 + 호출처를 새 `new(literal)` 값-인자 형태로 인라인. helper는 더 이상
참조되지 않아 함께 제거 (golangci-lint unused 회피).
- 테스트 파일 다수의 gofmt 정렬 조정 (struct 필드 패딩, 빈 줄 등).
검증: go build/vet/test -race ./..., golangci-lint run ./... 모두 통과.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- output: verifyAppendHeader가 UTF-8 BOM을 건너뛰지 않아 --bom으로 생성된 파일에 --append할 때 항상 헤더 불일치하던 버그 수정. encoding/csv는 BOM을 자동 처리하지 않으므로 bufio.Reader.Peek/Discard로 명시 스킵 (Go 공식 문서 권장 패턴). - output: needsStrip을 바이트 인덱싱 + rune 캐스트에서 `for _, r := range s`로 전환. 멀티바이트 UTF-8 문자에 안전 (Go 공식 idiom). - statistics: union-header 누적 record 수가 임계치(100,000) 도달 시 stderr에 1회 경고. streaming 불가능한 한계를 사용자가 인지하도록 fail-loud (idempotent, writer/threshold 주입 가능). - types: FormatThousands를 kakao.go 도메인 파일에서 범용 numbers.go로 분리. 응집도 개선. 회귀 테스트: - TestCSVWriter_AppendSkipsExistingBOM (6 케이스) + TestCSVWriter_AppendBOMRoundtripWithRealFile - TestNeedsStrip_MultiByteSafe (15 케이스) + TestStripControlChars_PreservesMultiByte (6 케이스) - TestStatisticsCSVRowWriter_MemoryWarn (5 케이스: 미만/도달/nil writer/ threshold 0/잘못된 JSON) - TestFormatThousands / _Boundary / _CommaPositioning + FuzzFormatThousands 검증: go build/vet/test -race ./... (13 패키지 PASS), golangci-lint v2.11.4 0 issues, fuzz 3s × 2종 panic 없음. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
이전 커밋 (d9ebcb2) 이후 멀티 에이전트 리뷰에서 식별된 항목 적용. 모두 주석/테스트 보강 및 에러 분류 명시화로 동작 변경 없음. 코드 변경: - output/csv.go: verifyAppendHeader의 Peek 에러를 EOF/ErrUnexpectedEOF (BOM 없음으로 진행)와 기타 I/O 에러(즉시 fail-loud 반환)로 명시 분류. 기존에는 모든 에러를 swallow하여 아래쪽 Peek(1)이 표면화한다는 암묵적 가정에 의존했음. 주석 정밀화: - output/csv.go: "Go 공식 문서 권장 패턴" 모호 출처 제거, BOM byte 표기 명시화 (UTF-8 BOM, 0xEF 0xBB 0xBF), bufio.Peek/Discard idiom 설명 보강. needsStrip 주석에 utf8.RuneError 동작 명시. - statistics_export_daily.go: CLAUDE.md 직접 인용 제거, 임계치 100,000의 메모리 산출 근거(약 50MB) 주석 추가, 동시성 계약(단일 goroutine 호출 전제) godoc 명시, 필드 주석 응축. - numbers_test.go: 전이적 마이그레이션 컨텍스트 (pkg/progress wrapper 경유 검증) 주석 제거. - 모든 "Gemini 리뷰 회귀" 외부 참조를 회귀 시나리오 본문으로 재작성 (long-term rot 방지). 테스트 보강: - csv_test.go: BOM + trailing LF 없는 헤더 회복 케이스 추가 (Persistence: safe retry — 첫 export SIGKILL 후 재개 시나리오). - csv_test.go: invalid UTF-8 byte (0xFF, lone continuation 0xBF) 회귀 케이스 — for-range가 U+FFFD로 디코드하므로 strip 대상이 되지 않음. 빠른 경로(원본 보존)와 느린 경로(U+FFFD 정규화) 차이도 회귀로 명시. - statistics_test.go: 4개 sub-test에 len(rw.records) 직접 단언 추가 (state consistency). "잘못된 JSON" sub-test를 2개로 분리하여 단일 concern 원칙 준수 (디코드 실패 시 records 무누적 / warned latch가 추가 출력 막음). 검증: go build/vet/test -race ./... (13 패키지 PASS), golangci-lint v2.11.4 0 issues, FuzzCSVWriter_StripRoundtrip 3s panic 없음. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…c latch 미적용 Suggestion 5건을 PR #19에 통합. 동시성 안전망, 사용자 facing OOM 방어 옵션, 중복 보일러플레이트 정리. S3 — --max-records hard cap: - 새 플래그 --max-records (기본 0=무제한). CSV format에만 적용. - 임계치 도달 시 errStatisticsRecordCap sentinel 반환 → exporter가 부분 결과 + resume-token을 보존하며 graceful 종료. - runStatisticsExportDaily는 errors.Is로 sentinel을 감지해 "--max-records=N 도달" 원인 안내를 stderr에 추가 출력. - 2단계 OOM 방어: 1단계 경고(warnThreshold) → 2단계 강제 종료(memCapHard). S4 — 경고 메시지에 즉시 행동 가능한 안내: - "--start-date/--end-date로 기간 분할" 및 "--max-records로 hard cap" 명시. - "(이 경고는 1회만 출력됩니다.)" 명시로 사용자가 추가 출력 기다리지 않도록. S2 — FinalizeWrite idempotency: - finalized atomic.Bool latch 추가. CAS 실패(=이미 finalized) 시 즉시 no-op 반환. defer + 명시 호출 충돌, double close, CSV 헤더 재기록을 차단. S1 — memWarned atomic 보호: - bool → atomic.Bool로 변경. CAS로 정확히 1회만 경고 발화 보장. - records/countKeys는 단일 goroutine 호출 invariant 유지 (godoc 명시). - 잘못된 다중 호출(향후 worker pool 등)에서도 latch는 race-free. S5 — progress/types FormatThousands_Boundary 중복 제거: - progress 패키지의 TestFormatThousands_Boundary를 11 케이스에서 "wrapper가 types.FormatThousands에 그대로 위임한다" 5-샘플 단언으로 축소. - 본체 동작은 pkg/types/numbers_test.go가 책임 (drift 방지). 신규 테스트: - TestStatisticsExportDaily_MaxRecordsCap_E2E: cap 도달 → sentinel + 부분 결과 디스크 보존 + stderr에 cap 원인/resume-token 안내 (CLI 흐름 검증). - TestStatisticsCSVRowWriter_MaxRecordsCap: cap=N 발화 / cap=0 비활성 / cap < threshold 시 cap 우선 (3 sub-test). - TestStatisticsCSVRowWriter_FinalizeIdempotent: 두 번째 호출 no-op 보장. - TestStatisticsCSVRowWriter_FinalizeIdempotent_AppendReaderNotDoubleClosed: countingCloser로 Close 정확히 1회만 호출 검증. - TestStatisticsCSVRowWriter_ConcurrentLatches: 32 goroutine 동시 CAS에서 memWarned/finalized 정확히 1회만 발화 (race 안전성). - 메모리 경고 메시지의 분할 가이드 키워드 검증 sub-test 추가. 검증: go build/vet/test -race ./... (13 패키지 PASS), golangci-lint v2.11.4 0 issues, FuzzCSVWriter_NoPanic / FuzzFormatThousands 각 3s panic 없음. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
solactl messages export/solactl statistics export-daily추가 (CSV/JSON/JSONL)--throttle(기본 500ms) sleeplimit=500+31일) 회피--append,--resume-token,--bom,--progress auto|on|off, 부분 결과 보존 + stderr 재개 안내pkg/output(CSV/JSON/JSONL writer),pkg/progress(한국어 진행률 UI, TTY 자동 감지),pkg/clock(테스트용 Clock 추상화),pkg/exporter(윈도우 분할 + 페이지 루프 + resume-token)pkg/types.FormatThousands로cmd/balance/pkg/progress의 천 단위 콤마 로직 단일화Test plan
go build ./... && go vet ./... && go test -race -count=1 ./...— 13 패키지 모두 통과messages-internal/부분 문자열 부재 (회귀 테스트로 정적 검증)TestMessagesExport_MultiWindowAutoSplit,TestStatisticsExportDaily_OperationalLogScenario(31일 → 정확히 31개 1일 윈도우)--append헤더 mismatch 거부, BOM, resume-token round-trip🤖 Generated with Claude Code