개발 기록 2026-10-19 docs/devlog/2026-10-19-4-적재-화면과-테스트-실행-화면.md GitHub에서 보기

적재 화면과 테스트 실행 화면

목표

  • WBS 2.3의 S3 GL 불러오기와 S4 테스트 실행 자리표시를 실제 LiveView로 교체한다.
  • Ledger와 Testing의 선행 공개 함수만 사용하고, 병렬 작업 중인 Runner를 기다리지 않고 화면 계약을 검증한다.
  • 내부 오류와 파일 원문을 노출하지 않으면서 권한, 감사 대상 경계, 실시간 갱신을 확인한다.

한 일

  • ImportLive.New: CSV 1개, .csv, 20,000,000바이트 제한의 LiveView 업로드와 취소, 합성 GL 시드 입력과 생성을 연결했다. 공백 시드는 Generator.default_seed/0를 사용하고 잘못된 정수는 고정 필드 오류로 안내한다.
  • ImportLive.Components: 대기, 적재 중, 완료, 실패 패널과 전표/라인/금액/경고 요약을 만들었다. 오류 14종과 period_mismatch 경고를 고정 한국어로 표시하고, 최대 100건과 나머지 건수를 표시한다. 컬럼 이름도 GL 계약의 허용 목록으로 제한했다.
  • TestRunLive.Show: 룰 상태와 적중 수, 검토 큐/경미 적중/전체 탭, 룰 및 검토 상태 필터, 전표 링크를 연결했다. 탭 숫자는 Testing.count_findings/2, 목록은 Testing.list_findings/3를 사용한다.
  • TestRunLive.Components: 룰 아이콘과 진행률, 실패 안내, 점수 막대와 발견 표를 만들었다. failed의 error는 HTML에 넣지 않고, skipped의 한국어 사유만 표시한다.
  • TickmarkWeb.Format: 짧은 룰 이름 8개, 레지스트리 순 정렬, 금액/건수/날짜/KST/UTC 툴팁, 실행/룰/검토 상태 라벨을 제공한다.
  • 기존 로그인 필수 :require_authenticated_user live_session의 경로를 사용한다. 라우터는 수정하지 않았고, 멤버십과 역할은 컨텍스트에서 확인한다.
  • 신규 테스트 24개를 추가했다. 업로드 성공과 거부, 시드 생성, PubSub 갱신, 오류 문구, 권한, 임계 점수 경계, 탭/필터와 stream 교체, 다른 감사 대상의 실행 차단을 검증한다.

결정과 트레이드오프

  • 금액/건수와 분 단위 KST 포맷은 기존 EngagementLive.Components 함수를 재사용했다. 공통 함수 전체를 새 모듈로 옮기는 대안은 S1/S2의 소유 파일까지 수정해야 하므로 이번 범위에서 제외했다. 기존 적재 상태 컴포넌트와 출처 표기도 그대로 쓴다.
  • 룰 이름은 Rules.fetch/1과 룰 모듈의 name/0, 짧은 이름은 Format을 사용한다. finding.rule_ids의 저장 순서 대신 매번 Rules.ids/0 순서로 표시한다. 등록되지 않은 id는 뒤에 알파벳 순으로 두고 원문 id를 표시한다.
  • S4 발견 목록은 크기가 커지고 탭/필터/실시간 변경이 있으므로 LiveView stream으로 관리한다. 목록을 바꿀 때 컨텍스트에서 다시 읽고 reset: true로 교체하여 남아 있는 행과 순서 오류를 방지한다. 건수는 stream을 세지 않고 별도 assign으로 둔다.
  • 룰 진행은 고정된 작은 목록이고, S3는 현재 적재 한 개와 최대 100개의 오류/경고만 다루므로 일반 assign을 쓴다. 두 목록까지 stream으로 바꾸는 대안은 메모리 이득보다 상태 관리가 늘어난다.
  • 공개 list_findings/3에 페이지 옵션이 없어 더 보기 버튼을 만들지 않았다. stream은 소켓이 전체 행을 계속 보관하는 비용을 줄이지만 최초 조회와 브라우저 DOM 크기까지 제한하지는 않는다. 페이지 계약은 화면설계서 C-08의 후속 항목이다.
  • S3는 현재 보고 있는 적재의 :import_updated만 다시 읽는다. S4는 해당 실행의 :rule_progress, :run_completed에서 실행과 목록을 다시 읽고, :finding_updated에서도 필터 결과를 갱신한다. 메시지의 수치만 믿고 화면 상태를 조립하는 대안보다 DB 상태와 일치시키기 쉽다.
  • 소스 탭은 ?source=upload|generated, S4 탭은 ?tab=queue|minor|all을 사용하고 필터는 rule_id, status로 보존한다. 입력한 필터는 허용된 상태와 해당 실행의 룰 id로 제한한다.
  • 업로드 탭의 폼을 제거하는 대신 hidden으로 전환하여 탭을 바꿔도 선택한 파일과 업로드 상태를 유지한다.
  • 권한 없는 S3 방문은 주소를 유지한 채 고정 안내와 이동 경로를 보여준다. S2로 자동 이동하는 방식은 화면설계서의 제안이므로 이번에는 요청된 권한 없음 상태를 직접 확인할 수 있게 했다. 제출 이벤트에서는 다시 권한을 확인한다.
  • S3 완료 후에는 S2로 가는 "감사 대상에서 테스트 실행" 링크를 둔다. 실제 실행 단추는 메인 소유 S2에 있으므로 Testing.start_test_run/3는 호출하지 않는다.
  • AI 위험도, 작성자 판단, 트리아지 요약과 요청 단추는 -로 표시하며 요청 단추는 비활성이다. Triage/Review 함수는 호출하지 않는다. 경미 적중에서는 AI 열과 트리아지 바를 숨긴다.

막힌 점과 해결

  • 첫 경고 0 컴파일에서 기존 CoreComponents.button/1이 type attr를 받지 않아 경고가 발생했다. 공용 컴포넌트를 바꾸지 않고 폼 안의 기본 submit 동작을 사용하도록 해당 attr를 제거했다.
  • S4 첫 시험은 9개 중 7개가 실패했다. 테스트의 finished_at을 Ecto.Changeset.change/2로 쓸 때 :utc_datetime_usec가 요구하는 마이크로초 6자리 정밀도를 갖추지 못한 것이 원인이었다. ~U[2026-10-19 01:02:19.000000Z]로 수정했고, 재실행에서 9개 모두 통과했다.
  • upload_errors/1은 업로드 전체 오류만 반환하므로 버튼 비활성 판정에서는 UploadConfig.errors 전체를 확인한다. 개별 파일의 확장자/크기 오류는 upload_errors/2로 표시한다.
  • 공유 파일이나 컨텍스트 계약을 바꿔야 하는 차단 요인은 없었다.

검증

  • MIX_TEST_PARTITION=4 MIX_ENV=test mix compile --warnings-as-errors: 종료 코드 0, 컴파일 경고 없음.
  • MIX_TEST_PARTITION=4 mix test test/tickmark_web/format_test.exs test/tickmark_web/live/import_live_test.exs: Result: 15 passed.
  • MIX_TEST_PARTITION=4 mix test test/tickmark_web/live/test_run_live_test.exs: 수정 후 Result: 9 passed.
  • 최종 MIX_TEST_PARTITION=4 mix precommit: 종료 코드 0, 실제 마지막 출력은 아래와 같다. 미사용 의존성 정리와 포맷, 전체 테스트를 포함하며 mix.lock은 바뀌지 않았다.
Result: 288 passed
  • git diff --check: 출력 없이 종료 코드 0.
  • S3는 실제 업로드 파일이 저장 위치로 복사되고 Ledger.process_import/1로 완료되는 흐름도 검증했다. S4는 SchemaFixtures로 실행과 발견을 만들고 DB 변경 뒤 계약 6장의 메시지를 Phoenix.PubSub.broadcast/3로 직접 보내 검증했다.
  • 브라우저와 실제 Runner 경로의 통합 검증은 수행하지 않았다. 작업 배정에 따라 병합 후 메인이 수행한다. 운영 반영은 하지 않았다.

Elixir 학습 포인트

  • LiveView mount/3는 정적 HTTP 렌더와 WebSocket 연결에서 각각 실행된다. 구독은 connected?/1이 참일 때만 수행하여 메시지를 받을 프로세스만 PubSub에 등록한다. 조회와 초기 assign은 두 번 모두 필요하다.
  • allow_upload/3는 파일 수/종류/크기와 업로드 상태를 관리한다. consume_uploaded_entries/3 콜백 안에서 Ledger.create_import/3를 호출해야 임시 파일이 살아 있을 때 영구 저장 위치로 복사된다. 콜백의 {:ok, 결과}와 컨텍스트의 {:ok, import}는 서로 다른 단계의 반환값이다.
  • handle_info/2는 브라우저 이벤트를 받는 handle_event/3와 달리 프로세스 메시지를 처리한다. 작은 PubSub 메시지의 id로 컨텍스트를 재조회하고 새 socket을 반환해야 변경이 렌더에 반영된다.
  • stream은 전체 목록을 일반 assign으로 보관하지 않으며, 각 항목의 고유 DOM id와 부모의 phx-update="stream"이 필요하다. 필터 변경에는 stream(..., reset: true)를 사용한다. 빈 상태와 건수는 stream 자체를 열거해서 계산하지 않는다.
  • Ecto의 cast와 change는 같지 않다. 이미 타입이 정해진 값을 change/2로 넣는 시험에서는 :utc_datetime_usec의 정밀도까지 직접 맞춰야 했다.

다음 할 일과 남은 제한

  • 메인: S1/S2 실행 단추와 실행 목록 연결 후 실제 Runner/룰 8종을 병합하고 S3/S4를 브라우저에서 확인한다.
  • 메인: 공용 셸, 너비, 라이트/다크, 키보드 이동과 접근성은 병합된 레이아웃/CSS를 기준으로 확인한다.
  • 후속 단계: M3/M4 AI/판단 값을 자리표시에 연결하고, 필요하면 C-08의 발견 목록 페이지 계약을 확정한다.
  • docs 색인 갱신은 메인 소유이므로 이번 브랜치에서는 수정하지 않았다.

계약 변경 요청

  • 없음.

관련 커밋

  • 기준: 456da8d (라운드 3 선행 계약과 읽기 함수).
  • 구현과 시험: c62a65c feat(web): GL 적재와 테스트 실행 화면 구현.