Skip to content

feat(export): 메시지/통계 export 명령 추가#19

Merged
Palbahngmiyine merged 6 commits into
solapi:mainfrom
Palbahngmiyine:worktree-virtual-imagining-pancake
May 18, 2026
Merged

feat(export): 메시지/통계 export 명령 추가#19
Palbahngmiyine merged 6 commits into
solapi:mainfrom
Palbahngmiyine:worktree-virtual-imagining-pancake

Conversation

@Palbahngmiyine
Copy link
Copy Markdown
Contributor

Summary

  • solactl messages export / solactl statistics export-daily 추가 (CSV/JSON/JSONL)
  • 7일 초과 범위는 1일 단위 UTC 윈도우로 자동 분할 → 페이지/윈도우 호출 사이 --throttle(기본 500ms) sleep
  • 6개월 lookback, page-size 상한(messages 200/statistics 100), throttle 100ms 최소를 CLI 레벨에서 강제 → messages-v4 단일 큰 호출(limit=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.FormatThousandscmd/balance / pkg/progress의 천 단위 콤마 로직 단일화

Test plan

  • go build ./... && go vet ./... && go test -race -count=1 ./... — 13 패키지 모두 통과
  • Internal endpoint 회귀 차단: production 코드에 messages-internal/ 부분 문자열 부재 (회귀 테스트로 정적 검증)
  • 사용자 운영 로그 시나리오 재현: TestMessagesExport_MultiWindowAutoSplit, TestStatisticsExportDaily_OperationalLogScenario (31일 → 정확히 31개 1일 윈도우)
  • 6개월 lookback / page-size 상한 / throttle 100ms 최소 가드 회귀 테스트 PASS
  • CSV/JSON/JSONL, --append 헤더 mismatch 거부, BOM, resume-token round-trip
  • 실제 sandbox API로 dry-run (배포 전 수동 확인 권장)

🤖 Generated with Claude Code

Palbahngmiyine and others added 2 commits May 11, 2026 14:57
- 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>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread pkg/output/csv.go
Comment thread pkg/output/csv.go
Comment thread cmd/statistics_export_daily.go
Comment thread pkg/types/kakao.go Outdated
Palbahngmiyine and others added 4 commits May 11, 2026 15:07
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>
@Palbahngmiyine Palbahngmiyine merged commit d920f3f into solapi:main May 18, 2026
2 checks passed
@Palbahngmiyine Palbahngmiyine deleted the worktree-virtual-imagining-pancake branch May 18, 2026 00:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant