선행 작업과 두 번째 병렬 라운드
- 작성일: 2026-10-17
- 관련 문서: WBS와 마일스톤 (M1), 시스템 구조와 데이터 모델 (2.1, 4.2, 9.2, 12장), 분개 테스트 룰과 감사 용어 (7.1), 워커 보고 -9 Engagements, -10 GL CSV 적재, -11 합성 GL 생성기
목표
M1(데이터 기반과 GL 적재)을 10/18 계획에서 하루 당겨 끝낸다. 메인 세션이 공유 파일(의존성, 마이그레이션, 스키마, 라우트)과 접근 제어 핵심을 먼저 만들고, 코드 워커 3명이 그 위에서 병렬로 구현한다. 완료 조건은 합성 GL 2만 행 적재 테스트 통과다(WBS 1.17).
한 일
선행 작업 (메인, 순차, WBS 1.1-1.10)
| 커밋 | 내용 |
|---|---|
| 58e0183 | Oban 2.24.2, NimbleCSV 1.3.0, 큐 ingest 1 / rules 4 / llm 2 |
| f01ecbd | Oban, Presence 를 슈퍼비전 트리에 추가 |
| 3ee2579 | 마이그레이션 8개 (oban_jobs 부터 audit_events append-only 트리거까지) |
| 60bc32d | 도메인 스키마 13개와 DB 제약 테스트 |
| e073c9a | AuditTrail.append/3 |
| 54d19a8 | Engagements.Policy 역할별 권한표 |
| 9a50daa | engagement 라우트 7개와 자리표시 LiveView |
| 44dbb30, 92fa9e2 | 계약 문서 반영, Ledger.Generator 계약과 분담 조정 |
| 2f104a1 | 접근 제어 핵심 (fetch_role/2, authorize/3, topic/1, subscribe/2, broadcast/2) |
선행 작업 끝에서 mix precommit 통과, 테스트 145개. 워커 브랜치 3개는 모두 92fa9e2 에서 갈라졌고 테스트 DB 는 MIX_TEST_PARTITION (_a, _b, _c) 으로 나눴다.
병렬 라운드 (워커 3명, WBS 1.11-1.13)
| 워커 | 범위 | 병합 | 병합 후 테스트 |
|---|---|---|---|
| A | Engagements 나머지 공개 함수, S1 목록과 S2 상세 | f699d52, 메인 보정 2601443 | 179 |
| C | Ledger.Generator, 정답 라벨, Testing.Holidays |
56c701d | 191 |
| B | CsvParser, Row, Importer, ImportWorker | ece19bd | 238 |
병합은 끝난 순서대로 A, C, B 로 했다. 세 브랜치 모두 공유 파일(AGENTS.md 5.3)을 건드리지 않아서 충돌은 없었다. B 는 자기 worktree 에서 192개(기존 145 포함)로 끝났고, A, C 가 먼저 들어간 위에 병합해도 실패 없이 238개가 됐다.
병합 뒤 메인 작업 (WBS 1.15-1.18)
| 커밋 | 내용 | 테스트 |
|---|---|---|
| cf923bb | generate_sample_import/3: 생성기 출력을 <uuid>.csv, 라벨을 <uuid>.labels.csv 로 쓰고 업로드와 같은 register_import/3 경로로 등록. 기본 시드 적재 통합 테스트 |
246 (e2e2fd4 포함) |
| e2e2fd4 | S2 화면에 GL 적재 목록 연결 (stream, 상태 배지, KST 시각, 출처 표기) | |
| 8a49862 | CSV 오류 코드 4종 추가 (unknown_column, invalid_value, too_long, duplicate_line_number) |
247 |
| f81367d | .formatter.exs import_deps 에 :oban 추가 |
|
| 6ecab07 | 10/18 구현 결과를 계약 문서(2.1, 4.2, 9.2)와 도메인 문서(7.1)에 반영 |
측정
모두 Apple M3 Pro, 로컬 PostgreSQL 16.4 에서 잰 실제 출력이다.
| 대상 | 결과 | 출처 |
|---|---|---|
B 의 2만 행 적재 (process_import/1, 7,000 전표 / 20,000 라인, Sandbox 안) |
748.3 / 663.6 / 663.3 ms | devlog -10 |
같은 파일 파싱만 (CsvParser.parse/3, DB 없음) |
121.3 / 113.3 / 110.5 ms | devlog -10 |
기본 시드 생성 (mix run, 1회) |
130 ms, GL 2,709,233 바이트 (약 2.7 MB), 라벨 339,614 바이트 | 메인 |
| 통합 테스트: 생성, 등록, 적재까지 (8,341 전표 / 19,804 라인) | 981.8 ms (mix test 의 가장 느린 테스트 출력) |
메인 |
통합 테스트는 CRLF 줄 끝을 바꾸지 않고 그대로 넣었고, 오류 없이 succeeded 로 끝났다. 적재된 전표번호 집합은 라벨 파일의 전표번호 집합과 같았다. WBS 리스크 표의 "Generator 출력과 CsvParser 입력 형식 불일치" 는 이것으로 닫는다.
결정과 트레이드오프
- 병합 순서는 끝난 순서대로. 계약을 먼저 고정했기 때문에 A, B, C 사이에 코드 의존이 없었다. 셋이 다 끝나길 기다렸다 한 번에 넣는 것보다, 끝난 브랜치를 바로 검토하고 넣는 쪽이 검토 부담이 나뉜다. 대신 병합마다
mix precommit을 돌려 앞 병합이 뒤 브랜치를 깨지 않았는지 확인했다. - B 검토. 계약 문서 2.1 의 Ledger 시그니처와 반환값, 9.2 의 검증 순서를 대조했고 위반은 없었다. B 가 계약에 없는 오류 코드 4개(
too_long,invalid_value,unknown_column, 선택으로duplicate_line_number)를 요청했다. 그동안은parse_error와invalid_number에 섞여 있었다. - 오류 코드 4종을 지금 넣은 이유. 10/19 의 ImportLive 가 코드별 한글 메시지 표를 만든다. 그 표가 생기기 전에 코드를 나누면 바뀌는 곳이 파서와 테스트뿐이지만, 표가 생긴 뒤에 나누면 메시지 표, LiveView 테스트, 이미 저장된 적재의
errors배열까지 같이 바뀐다. 지금이 가장 싸다.unknown_column은 알 수 없는 헤더 이름을 오류에 되풀이하지 않는다(사용자 입력을 그대로 화면에 다시 쓰지 않는다). target_lines는 MVP 에서 지원하지 않는다. 도메인 문서 7.1 은 행 수 조정 옵션을 적었지만 계약은seed하나만 받는다. 기본 시드가 19,804행으로 "약 2만 행" 안에 들어가므로 데모에는 충분하다. 도메인 문서에 미지원과 기본 시드 실측값을 적었다.- 합성 GL 의 원본 파일명에 시드를 남긴다.
synthetic-gl-seed-<seed>.csv로 저장하면 별도 컬럼 없이 S2 출처 칸에서 "합성 GL, 시드 N" 으로 보여줄 수 있다. 업로드 파일명이 우연히 같은 모양이면 합성으로 보이는 단점이 있지만,source_kind가generated일 때만 이 해석을 쓰므로 문제 없다. - 300줄 넘는 파일. 생성된 파일(
core_components.ex505,user_auth_test.exs401,accounts_test.exs397) 말고csv_parser.ex404,ledger.ex392,ledger_test.exs474,generator_test.exs381,csv_parser_test.exs330,engagement_live_test.exs325 줄이 권장 기준을 넘는다. 지금 나누면 M2 워커가 계약 문서와 다른 모듈 경로를 보게 되므로 이번 라운드에서는 두고, M2 에서 Ledger 가 더 커질 때 다시 본다.
막힌 점과 해결
- S2 토픽 메시지 테스트가 깨졌다. e2e2fd4 에서
:import_updated처리가Ledger.get_import!/2로 적재를 다시 읽게 되자, A 가 만든 "토픽 메시지를 받아도 화면이 죽지 않는다" 테스트가 없는 id 로 보낸:import_updated때문에Ecto.NoResultsError로 실패했다. 이 메시지는 실제 적재를 만들어 보내는 GL 적재 테스트가 이미 다루므로, 그 테스트에서는:import_updated를 빼고 나머지 세 메시지(:test_run_updated,:finding_updated,:audit_appended)만 남겼다. 없는 id 를 보내는 일은 브로드캐스트가 커밋 뒤에만 나가는 계약상 생기지 않는다. Decimal.to_string/1형식 차이. succeeded 감사 payload 의total_debit,total_credit는 기본(:scientific) 형식이고, engagement.created 의performance_materiality는:normal이다. 지수 표기는 지수가 양수인 Decimal 에서만 나오는데, Row 의 금액 검사가e를 받지 않고("1e3"은invalid_number, csv_parser_test) 정수 덧셈은 지수를 키우지 않으므로 실제로는 같은 문자열이 나온다. 표기 통일은 다음 Ledger 수정 때 함께 한다.
Elixir 학습 포인트
- 바이너리 패턴 매칭.
%{original_filename: "synthetic-gl-seed-" <> rest}는 함수 머리에서 접두사가 맞을 때만 매칭되고, 나머지를rest로 꺼낸다. 정규식 없이 접두사 검사와 추출이 한 번에 된다. stream_insert(socket, :imports, row, at: 0). stream 은 DOM id 로 행을 식별한다. 같은 id 의 행이 이미 있으면 위치는 그대로 두고 내용만 바꾸고, 없으면at: 0으로 맨 위에 넣는다. 그래서 상태 변경(queued 에서 succeeded)과 새 적재를 같은 handle_info 하나로 처리한다.- 빈 상태 행.
<tr id="imports-empty" class="hidden only:table-row">는 tbody 에 이 행 하나만 남았을 때만 보인다(Tailwindonly:). stream 은 서버가 목록 길이를 들고 있지 않으므로, 개수를 assign 으로 따로 세지 않고 CSS 로 빈 상태를 처리한다. - KST 는 고정 +9시간. 한국은 1988년 이후 서머타임이 없어서 UTC 에 9시간을 더하면 정확하다. 시간대 데이터베이스(tzdata) 의존성을 넣지 않았다. 원본 UTC 값은
<time datetime>과title에 남겨 감사 추적에서 기준 시각이 흐려지지 않게 했다. .formatter.exs의import_deps. Oban 은assert_enqueued등을 괄호 없이 쓰도록locals_without_parens를 내보낸다.import_deps에 넣어야mix format이 괄호를 강제로 붙이지 않는다.
다음 할 일
- 1.19:
feature/foundation을main으로 보내는 PR (사용자 승인 뒤 생성). - M2 (10/19): 실행 경로, 룰 모듈, TestRunLive 와 ImportLive 를 워커 3명에게 나눈다. 시작 전에 계약 문서 12장의 M2 배정과 오류 코드별 한글 메시지 표를 확정한다.
- 확인 필요: 헤더만 있는 파일은 지금 전표 0건 succeeded 로 끝난다(파서 테스트 "accepts a file with a header and no rows"). 거부할지 ImportLive 설계 때 정한다.
- 확인 필요: C 가 남긴 명세 해석 17건(devlog -11). 룰 적중 대 정답 라벨 비교 테스트(2.4)를 쓸 때 함께 검토한다.
- B 가 남긴
ImportWorker마지막 시도 분기 테스트는 장애 주입 수단이 생기면 추가한다(devlog -10). - 배포 대상(Azure
az containerapp up또는 Fly.io)은 사용자 결정 대기 (WBS 5.1).
관련 커밋
선행: 58e0183, f01ecbd, 3ee2579, 60bc32d, e073c9a, 54d19a8, 9a50daa, 44dbb30, 2f104a1, 92fa9e2. 병합: f699d52, 2601443, 56c701d, ece19bd. 병합 뒤: cf923bb, e2e2fd4, 8a49862, f81367d, 6ecab07.