S5 finding 상세 표시
- 작성일: 2026-10-20
- 작성: 워커 W4 (Codex
gpt-6.1-sol, effort max) - 브랜치:
feature/m3-finding-show - WBS: 3.3 표시, 라운드 4a
- 관련 문서: 시스템 구조와 데이터 모델 (2.1, 3.7-3.12, 6, 8, 12.2), 화면설계서 (3장, S5, 부록 A, C-06, C-09, C-11), 디자인 시스템, 룰과 감사 용어, 라운드 4 계약과 통합
목표
S5 자리 표시 화면을 전표 사실, 룰 근거, AI 제안, 사람 판단과 이력을 읽는 상세 화면으로 바꾼다. 이번 라운드는 조회와 작성자 폼 검증까지이며, AI 요청, 결론 저장, 승인과 반려는 수행하지 않는다.
완료 조건은 계약에 맞는 조회 함수와 LiveView 테스트, MIX_TEST_PARTITION=5 mix precommit 통과, 이 문서를 포함한 기능 커밋이다.
한 일
| 파일 | 변경 |
|---|---|
lib/tickmark/testing.ex |
기존 함수를 바꾸지 않고 get_finding!/2, list_rule_results/2와 접근 확인 private 함수 추가 |
lib/tickmark_web/live/finding_live/show.ex |
권한 확인, engagement 일치 검사, 구독, 한국어 changeset 오류, 초안을 보존하는 갱신, 서버의 폼 모드와 can_decide 계산 |
lib/tickmark_web/live/finding_live/components.ex |
헤더 배지, 전표 사실과 행, 룰 적중, AI 카드, 작성자 판단, 이력, 서명 바 |
test/tickmark/testing_test.exs |
조회의 preload와 정렬, 잘못된 id와 비멤버 거부, 같은 실행과 전표의 룰 결과만 반환하는 테스트 4개 추가 |
test/tickmark_web/live/finding_live_test.exs |
상세 표시, AI 상태, 역할, 검증, PubSub, 초안 보존, 이스케이프를 확인하는 테스트 24개 추가 |
조회는 전표 행을 line_number 오름차순, AI 제안을 생성 시각 내림차순, 결론과 검토 결정을 생성 시각 오름차순으로 읽는다. 결론의 작성자와 검토 결정의 검토자를 preload한다. 전표 행에는 계정 코드와 계정명이 직접 저장되어 있어 계정 association은 필요 없다.
화면에는 전표번호, 점수 미터, AI 위험도, 최신 작성자 판단과 검토 상태를 구분해 보인다. 전표 사실의 적요는 첫 번째 행에서 읽고, 거래처와 적요는 원문을 HEEx로 이스케이프한다. 전표 행 합계는 표시한 행에서 계산한다. 룰 근거 값과 AI 생성 정보는 접힌 details로 제공한다. 룰 가중치 합이 100을 넘으면 점수 식에 min(100, ...)을 보여 상한을 설명한다.
작성자는 open, concluded, rejected에서 새 판단을 입력하고 changeset 검증을 받을 수 있다. 검토자는 최신 결론을 읽고, approved는 역할과 무관하게 잠긴다. AI 요청, 결론 제출, 승인과 반려 단추는 비활성이다. 폼 submit도 검증 이벤트만 처리하며 Triage나 Review 컨텍스트 함수를 호출하지 않는다.
같은 finding의 finding_updated는 배지, AI 카드와 이력을 다시 읽고 작성 중인 판단, 코멘트, 글자 수와 검증 오류를 보존한다. 새 AI 제안을 표시할 때 숨은 ai_triage_id만 갱신한다. 다른 finding의 알림은 무시한다. 구독 토픽의 나머지 메시지도 명시적으로 무시해 화면 프로세스가 종료되지 않는다.
결정과 트레이드오프
- 검증은 작성자 폼만 담당한다. 계약 12.2에는
ReviewDecision.changeset/2도 적혀 있어 코디네이터에게 질문했다. 반려 사유 폼은 반려 단추 이후의 2단계 결정 흐름이므로, 검토 결정 검증과 함께 라운드 4b에서 메인이 연결하기로 확정했다. 메인이 계약과 통합 devlog에 이 범위 조정을 반영한다. - 룰 결과는 전달된 struct의 외래 키 대신 저장된 finding으로 범위를 확인한다.
list_rule_results/2도 finding id로 다시 읽어 멤버십과 실행, 전표를 확인한다. 대안인 struct의engagement_id만 검사하는 방법보다 조회가 하나 더 필요하지만, 다른 감사 대상 id로 바꾼 struct가 접근 경계를 우회하지 못한다. - 연관 데이터는 정렬된 Ecto preload로 읽는다. 여러 has_many를 한 번에 join하면 행, AI 제안과 결론의 조합만큼 결과가 늘어날 수 있다. 별도 preload 쿼리를 택해 각 목록의 정렬과 목적을 코드에서 바로 설명할 수 있게 했다.
- AI의 현재 상태와 최신 성공 제안을 분리한다. queued는 분석 중, failed는 일반 실패 문구만 보인다. 이전 성공 제안을 새 제안처럼 보여주지 않으며, 화면에 성공 제안이 없으면 숨은
ai_triage_id도 빈 값이다. 점수가 실행의 기준 미만이면 경미 적중 안내를 보이고 요청 단추를 두지 않는다. - AI가 사람의 판단을 채우지 않는다. 최초 판단 라디오는 미선택이고, concluded나 rejected에서도 새 폼을 이전 결론으로 미리 채우지 않는다. 사람 입력은 실선, AI 카드는 accent 점선으로 구분한다.
- 폼과 조회 상태를 따로 갱신한다. 알림마다 폼을 새 changeset으로 만들면 초안과 오류가 사라진다. 기존 form의 changeset을 유지하고 화면에 보인 AI 제안 id만 바꾼다. approved 알림은 입력 영역을 잠그며 이전 제출 안내도 지운다.
- 직무분리는 표시 상태에도 반영한다.
can_decide는 검토자, concluded, 최신 결론 존재, 본인 결론이 아님을 서버에서 계산한다. 단추는 이 값과 무관하게 이번 라운드에서는 비활성이다. 저장과 최종 권한 확인은 4b에서 Review 컨텍스트가 담당한다. - 검토 큐와 Presence는 생략했다. 빈 패널로 본문 폭을 줄이는 대신 이번 표시 화면에서 생략했다. 둘 다 배정대로 라운드 4b로 넘긴다.
막힌 점과 해결
- 첫 조회 테스트에서 전표 행의 차변과 대변을 모두 0으로 만들자 DB의
one_side_positive제약이 거부했다. fixture를 한쪽 금액이 양수인 합성 행으로 고쳤고 조회 테스트 20개가 통과했다. - LiveViewTest는 라디오의 존재하지 않는 값인 빈 문자열을 보내면 요청 전에 거부한다. 미선택 상태의 검증 테스트는 실제 브라우저처럼 outcome 필드를 보내지 않도록 고쳤다.
- queued 카드의
aria-busy=true선택자 테스트가 실패했다. HEEx의 boolean 속성 처리에 기대지 않고to_string/1로true와false를 명시해 통과시켰다. - 계약 12.2와 브리핑의 검토 폼 범위 차이는 코디네이터의 응답으로 해결했다. 소유 범위 밖 파일은 수정하지 않았다.
검증
대상 테스트의 최종 출력:
$ MIX_TEST_PARTITION=5 mix test test/tickmark/testing_test.exs test/tickmark_web/live/finding_live_test.exs
Finished in 0.7 seconds (0.7s async, 0.00s sync)
Result: 44 passed
전체 precommit의 실제 마지막 출력:
$ MIX_TEST_PARTITION=5 mix precommit
Finished in 5.5 seconds (4.7s async, 0.7s sync)
Result: 417 passed
- 기존 389개에 조회 4개와 LiveView 24개를 더했다.
compile --warnings-as-errors, 미사용 의존성 정리, 포맷과 전체 테스트가 종료 코드 0으로 통과했다.git diff --check통과.mix.lock과 공유 파일은 변경하지 않았다.- 비멤버 HTTP 요청은
assert_error_sent 404로 확인했다. 다른 감사 대상 URL과 잘못된 finding id도Ecto.NoResultsError로 거부한다. - 승인 알림 후 잠금, 검토자 최신 요약 갱신, 같은 finding의 AI 참조 교체, 다른 메시지 무시, HTML 태그 원문 표시를 LiveViewTest로 확인했다.
- 실제 브라우저의 시각 품질과 좁은 화면 배치는 검증하지 않았다. 라운드 4b 통합 뒤 메인의 브라우저 확인 대상이다.
Elixir 학습 포인트
- 정렬 쿼리를 preload에 넣는다.
Repo.preload/2의 association 값에 Ecto 쿼리를 주면 연관 목록의 순서를 DB에서 정할 수 있다.conclusions쿼리 안의preload: :preparer로 한 단계 더 깊은 연관도 읽는다. - changeset은 검증 결과를 가진 값이다.
Conclusion.changeset/2를 실행하는 것만으로 DB에 쓰지 않는다. action을:validate로 정하고to_form/1에 넘기면 화면이 필드 오류를 읽는다. 스키마를 바꾸지 않고 이 화면의 errors 메시지만 한국어로 바꿨다. - LiveView 알림 처리와 입력 상태는 독립적이다.
handle_info/2는 알림을 받아 조회 assign과 stream을 바꾼다. 기존 form assign을 유지하면 외부 이벤트가 초안을 덮어쓰지 않는다. 알려진 다른 토픽 메시지의 무시 절도 있어야 한다. - 서로 다른 종류의 이력도 같은 시각 축으로 합칠 수 있다. 결론과 검토 결정을 표시용 map으로 바꿔 합치고
DateTime.to_unix(at, :microsecond)로 정렬했다. 생성 시각이 같은 경우 결론을 결정보다 먼저 둔다. 과거 행은 그대로 남는다. - HEEx는 텍스트를 기본으로 이스케이프한다. 적요나 거래처를
raw/1없이 값으로 넣으면 태그가 동작하지 않고 원문으로 보인다. 테스트는 원문 텍스트가 있는지와 실제 script, img 요소가 없는지를 함께 확인한다.
다음 할 일
- 메인: 이 브랜치를 통합하고 docs 색인에 이 문서를 추가한다.
- 라운드 4b: Triage와 Review 컨텍스트 연결, 반려 사유 검증, 승인 확인 단계, 검토자가 본
conclusion_id전달과:stale처리. - 라운드 4b: 검토 큐, S4 탭과 필터 이어받기, Presence, 단축키, 실제 브라우저와 반응형 확인.
- 공통
Layouts.app의 기존 Phoenix 헤더와 영어 공통 문구는 메인 소유 파일에 남아 있다. 이 작업은 S5 본문과 필드 오류만 한국어로 만들었다.
계약 변경 요청: 없음. 작성자 폼만 검증하는 범위 조정은 코디네이터와 합의했다.
관련 커밋
- 기준 커밋:
7cd594d(feature/m2-rule-engine). - 이 문서를 포함한 기능 커밋:
feat(finding): 전표 검토 상세 읽기 화면 구현.