기획 2026-10-20 docs/planning/2026-10-20-10-기능-정의서.md GitHub에서 보기

Tickmark 기능 정의서

1. 목적과 범위

이 문서는 감사 대상 생성부터 GL 적재, 룰 실행, AI 제안, 사람의 결론과 검토, 감사 로그까지 각 동작이 어떻게 처리되는지 정의한다. 요구사항의 이유·수락 기준·WBS 추적은 요구사항 명세서를 참조한다. 한 기능은 하나의 사용자 동작 또는 독립된 시스템 처리 단위다. 룰 8종은 사용자가 함께 실행하는 하나의 시스템 기능 안에 정의한다.

기준 커밋에서 동작하는 내용과 후속 연결 계획을 분리한다. 구현은 코드와 관련 테스트 존재, 부분은 일부 계층만 구현, 미구현은 계획만 존재, 진행 중은 W6/F1/F2 배정, 확인 필요는 검증 근거 부족을 뜻한다. 이번 작업에서는 애플리케이션 테스트·브라우저·외부 API·배포를 실행하지 않고 코드와 기존 테스트를 읽었다. 이 문서의 테스트 경로는 실행 성공 보고가 아니다.

실제 고객 데이터·ERP 연동·다중 통화·외부 메일 인프라와 2단계 기능은 MVP 동작에 포함하지 않는다. 합성 데이터만 사용한다. AI는 제안하며 사람의 finding.status를 변경하지 않는다. 근거: MVP 2-4장, 계약 2, 7, 8장.

2. 용어와 ID 체계

표기 의미
FN-{S1..S6 또는 AUTH 또는 SYS}-{nn} 화면·인증·시스템별 기능 ID. 계약 12.4 기준
REQ-{영역}-{nn}, NFR-{nn} 요구사항 명세서의 기능·비기능 요구사항
scope Tickmark.Accounts.Scope; 인증된 사용자. 감사 대상 역할은 요청마다 DB에서 확인
finding / conclusion / decision 실행별 적중 전표 / 작성자 결론 / 검토자의 승인·반려
구현 문구 / 목표 문구 기준 커밋에 존재하는 문자열 / 화면설계서 또는 최신 계약에만 존재하는 문자열
감사 없음 이 동작이 새 audit_events 행을 만들지 않음. 기존 이벤트 조회와 구분

본문의 Accounts, Engagements, Ledger, Testing, Triage, Review, AuditTrail, AI는 Tickmark 하위 모듈의 약칭이다. UserAuth, Format, EngagementLive, ImportLive, TestRunLive, FindingLive, AuditLive, UserLive, UserSessionController는 TickmarkWeb 하위다. Module.fun/arity의 기본 인자를 생략한 공개 arity도 표기한다. 위치 경로는 저장소 기준이다.

화면 경로 기준 LiveView / 진입점
S1 /engagements, /engagements/new TickmarkWeb.EngagementLive.Index
S2 /engagements/:id TickmarkWeb.EngagementLive.Show
S3 /engagements/:id/imports/new TickmarkWeb.ImportLive.New
S4 /engagements/:id/runs/:run_id TickmarkWeb.TestRunLive.Show
S5 /engagements/:id/findings/:finding_id TickmarkWeb.FindingLive.Show
S6 /engagements/:id/audit TickmarkWeb.AuditLive.Index
AUTH /users/register, /users/log-in, /users/log-in/:token, /users/settings TickmarkWeb.UserLive.*, UserSessionController
SYS 사용자 라우트 없음 Oban 워커와 내부 context 함수

근거: lib/tickmark_web/router.ex. S1의 공통 셸 기능은 S1-S6 및 AUTH에서 재사용한다. 홈 /은 S1로 들어가는 공통 진입점으로 FN-S1-03에 포함한다.

3. 공통 처리 규칙과 입력 계약

3.1 권한, 트랜잭션, 오류

  • 역할은 실제 코드의 preparer, reviewer 두 가지다. 생성자는 별도 역할이 아니며 생성 시 preparer가 된다. 멤버 추가는 역할표와 별개로 생성자인 멤버만 가능하다.
  • S1-S6은 HTTP의 require_authenticated_user와 LiveView의 :require_authenticated를 거친다. S2-S6 상세는 비멤버·없는 ID·잘못된 UUID를 같은 404로 처리한다. 기준 404 본문은 Not Found이며 한국어 안내는 F1 진행 중이다.
  • 화면 숨김·disabled는 편의 장치다. 변경 함수는 scope로 권한을 다시 검사하고, S2/S4/S5는 URL의 감사 대상과 자식 데이터 소속도 확인한다.
  • 업무 변경·이벤트·잡 등록은 해당 Ecto.Multi 또는 Repo.transact에서 함께 처리한다. PubSub는 커밋 뒤 전송한다. 단순 조회와 폼 검증에는 트랜잭션·잡·감사 기록이 없다.
  • :forbidden 등은 함수 오류 코드다. 화면에는 해당 기능의 고정 문구를 보인다. 아직 연결되지 않은 기능은 목표 문구라고 명시한다.

근거: Engagements.authorize/3, lib/tickmark/engagements/policy.ex, lib/tickmark_web/user_auth.ex, 계약 2, 4, 6, 8장; test/tickmark/engagements/authorization_test.exs, test/tickmark_web/engagement_routes_test.exs.

3.2 GL 입력 필드

FN-S3-01과 FN-SYS-01이 함께 사용하는 실제 계약이다. 헤더 16개는 모두 존재해야 하며 순서는 자유다. UTF-8, BOM, CRLF, 따옴표 안 쉼표·줄바꿈을 지원한다. 완전히 빈 레코드는 건너뛴다. 헤더만 있는 CSV는 파서가 0건으로 허용하지만 크기 0인 파일은 적재 등록 changeset에서 거부한다.

필드 형식·검증 실패 코드
entry_number 필수 문자열, 최대 200자, 같은 번호 행 연속 배치 required, too_long, non_contiguous_entry
line_number 정수 1-999999, 전표 내 유일 invalid_number, duplicate_line_number
posting_date YYYY-MM-DD invalid_date
document_date YYYY-MM-DD invalid_date
entered_at YYYY-MM-DD HH:MM:SS, 오프셋 없는 KST; Z와 +09:00 거부 invalid_date
fiscal_year 정수 1000-9999; 감사 대상 생성 폼의 2000-2100과 다른 제한 invalid_number
fiscal_period 정수 1-13; 13기는 기간 불일치 경고 제외 invalid_number, period_mismatch
account_code 필수 문자열, 최대 200자 required, too_long
account_name 필수 문자열, 최대 200자 required, too_long
debit 음수·지수·쉼표 금지, 원 단위 정수 또는 0 소수부; 빈 셀은 0 invalid_number, both_sides
credit debit과 동일; 행에서 두 금액 중 정확히 하나만 양수 invalid_number, both_sides
description 선택 문자열, 최대 500자 too_long
counterparty 선택 문자열, 최대 200자; 전표 헤더 일관성 검사 too_long, inconsistent_header
entered_by 필수 문자열, 최대 200자 required, too_long
approved_by 선택 문자열, 최대 200자; 수기 전표도 공란 허용 too_long
source manual 또는 system required, invalid_value

각 셀은 유효한 UTF-8이고 NUL이 없어야 한다. 금액의 저장 상한은 numeric(18,2)의 9999999999999999.99이며 정수 금액만 받으므로 허용 정수의 최대는 9999999999999999다. 전표 합계와 전체 차변 합계도 상한을 검사한다. posting_date, document_date, entered_at, fiscal_year, fiscal_period, entered_by, approved_by, source, counterparty는 같은 전표 안에서 같아야 한다. 근거: MVP 5장; Tickmark.Ledger.CsvParser.parse/3, Tickmark.Ledger.CsvParser.Row.parse/2; test/tickmark/ledger/csv_parser_test.exs.

3.3 S3의 실제 오류 문구

코드 / 조건 현재 문구
missing_column 필수 컬럼이 없습니다
unknown_column 알 수 없거나 겹친 컬럼이 있습니다
required 필수 값이 비어 있습니다
too_long 값이 너무 깁니다
invalid_value 허용되지 않는 값입니다
invalid_date 날짜 또는 일시 형식이 잘못되었습니다
invalid_number 금액은 원 단위 정수여야 합니다
both_sides 한 행에는 차변과 대변 중 하나만 있어야 합니다
unbalanced_entry 전표의 차변 합계와 대변 합계가 다릅니다
inconsistent_header 같은 전표의 헤더 값이 행마다 다릅니다
non_contiguous_entry 같은 전표의 행이 이어져 있지 않습니다
duplicate_line_number 같은 전표에 같은 라인 번호가 있습니다
too_many_rows 행 수가 상한(100,000)을 넘었습니다
parse_error CSV 형식을 읽을 수 없습니다
period_mismatch 전기일의 월과 회계기간이 다릅니다
too_large / file_too_large 20MB보다 큰 파일은 올릴 수 없습니다.
not_accepted .csv 파일만 올릴 수 있습니다.
too_many_files 파일은 한 번에 1개만 올릴 수 있습니다.
invalid_seed 정수로 입력하세요.

invalid_number는 금액 외의 정수 필드에도 사용된다. 현재 문구의 한계는 12장에 남긴다. 오류에는 CSV 원래 값이나 알 수 없는 헤더를 다시 넣지 않는다. 근거: lib/tickmark_web/live/import_live/{new,components}.ex, 화면 S3.5와 부록 A; test/tickmark_web/live/import_live_test.exs.

4. AUTH: 계정과 인증

인증 기능은 감사 대상 역할과 무관하다. 아래 영어 문구는 기준 커밋의 실제 값이며 F2 한글화 이후 다시 확인해야 한다. 외부 이메일 전달 성공을 보장하지 않으며 현재 메일은 Swoosh Local에 저장한다 (config/config.exs).

FN-AUTH-01 회원가입

항목 정의
ID FN-AUTH-01
이름 회원가입
화면과 경로 AUTH /users/register
행위자와 권한 비로그인 사용자; 역할 없음
선행 조건 미가입 이메일
입력과 검증·오류 문구 email 필수, 최대 160자, @ 포함·공백 불가; must have the @ sign and no spaces, 중복 has already been taken
출력·flash·PubSub·감사 이벤트 An email was sent to {email}, please access it to confirm your account. 후 /users/log-in; 로그인 지시 메일, 감사 없음
예외와 오류 처리 changeset 오류는 폼 유지; 이미 로그인했으면 signed_in_path로 이동
관련 REQ REQ-ACC-01, REQ-UI-04
구현 상태 구현; 한글화 진행 중 F2
근거 lib/tickmark_web/live/user_live/registration.ex; Accounts.register_user/1, deliver_login_instructions/2; test/tickmark_web/live/user_live/registration_test.exs, test/tickmark/accounts_test.exs

처리 흐름:

  1. Accounts.change_user_email/3으로 입력을 검증하고 저장 시 Accounts.register_user/1을 호출한다.
  2. 계정 저장 뒤 Accounts.deliver_login_instructions/2로 로그인 토큰과 메일을 만든다. 계정 insert와 메일 전송을 하나의 트랜잭션으로 묶지는 않는다.
  3. 안내 flash와 로그인 화면 이동을 적용한다. 업무 Oban 잡과 감사 이벤트는 만들지 않는다.

FN-AUTH-02 매직 링크 요청

항목 정의
ID FN-AUTH-02
이름 매직 링크 요청
화면과 경로 AUTH /users/log-in
행위자와 권한 이메일로 로그인하려는 사용자
선행 조건 로그인 화면 진입
입력과 검증·오류 문구 email; 화면 type=email·required; 이벤트 submit_magic
출력·flash·PubSub·감사 이벤트 등록 여부에 관계없이 If your email is in our system, you will receive instructions for logging in shortly.; 감사 없음
예외와 오류 처리 없는 이메일도 같은 안내; 메일 전달 여부로 계정 존재를 화면에 노출하지 않음
관련 REQ REQ-ACC-02
구현 상태 구현; 한글화 진행 중 F2
근거 lib/tickmark_web/live/user_live/login.ex; test/tickmark_web/live/user_live/login_test.exs

처리 흐름:

  1. Accounts.get_user_by_email/1로 계정을 찾는다.
  2. 계정이 있으면 Accounts.deliver_login_instructions/2를 호출해 토큰을 저장하고 메일을 생성한다.
  3. 동일한 flash를 보여 주고 로그인 화면으로 이동한다. Oban 잡·감사 이벤트는 없다.

FN-AUTH-03 매직 링크 확인 및 로그인

항목 정의
ID FN-AUTH-03
이름 매직 링크 확인 및 로그인
화면과 경로 AUTH GET /users/log-in/:token, POST /users/log-in
행위자와 권한 로그인 링크를 가진 사용자
선행 조건 15분 이내의 유효 토큰; 최초 가입 확인도 이 기능 사용
입력과 검증·오류 문구 token, 선택 remember_me; 토큰 값은 문서·로그에 남기지 않음
출력·flash·PubSub·감사 이벤트 최초 확인 User confirmed successfully., 재로그인 Welcome back!; 세션 발급 및 필요 시 disconnect, 감사 없음
예외와 오류 처리 화면 토큰 실패 Magic link is invalid or it has expired., POST의 미존재·만료 토큰 실패 The link is invalid or it has expired. 후 로그인 이동
관련 REQ REQ-ACC-02
구현 상태 구현; 한글화 진행 중 F2
근거 UserLive.Confirmation, UserSessionController.create/2, Accounts.login_user_by_magic_link/1; test/tickmark_web/live/user_live/confirmation_test.exs, test/tickmark_web/controllers/user_session_controller_test.exs

처리 흐름:

  1. Accounts.get_user_by_magic_link_token/1으로 GET 토큰을 확인하고 확인 폼을 보여 준다.
  2. 확인 제출을 POST로 넘겨 Accounts.login_user_by_magic_link/1을 호출한다. 미확인 계정은 확인·기존 토큰 삭제, 확인된 계정은 사용한 토큰 삭제를 수행한다.
  3. UserAuth.disconnect_sessions/1 및 UserAuth.log_in_user/3으로 세션을 정리·발급한다.
  4. 저장된 복귀 경로 또는 signed_in_path로 이동한다. 기준 코드의 기본 목적지는 /이며 S1 연결은 FN-S1-03에서 진행 중이다.

FN-AUTH-04 비밀번호 로그인

항목 정의
ID FN-AUTH-04
이름 비밀번호 로그인
화면과 경로 AUTH POST /users/log-in
행위자와 권한 비밀번호를 설정한 사용자
선행 조건 설정 화면에서 비밀번호 설정 완료
입력과 검증·오류 문구 email, password, 선택 remember_me; submit_password가 HTTP 폼 제출을 유발
출력·flash·PubSub·감사 이벤트 성공 Welcome back!, 세션·선택 로그인 유지 쿠키; 감사 없음
예외와 오류 처리 실패 Invalid email or password; 이메일 입력값만 최대 160자로 보존하고 로그인 화면 이동
관련 REQ REQ-ACC-03
구현 상태 구현; 한글화 진행 중 F2
근거 UserSessionController.create/2, Accounts.get_user_by_email_and_password/2; test/tickmark_web/controllers/user_session_controller_test.exs, test/tickmark_web/live/user_live/login_test.exs

처리 흐름:

  1. 로그인 폼을 POST /users/log-in으로 제출한다.
  2. Accounts.get_user_by_email_and_password/2가 Bcrypt로 자격을 확인한다. 비밀번호 없는 계정은 성공하지 않는다.
  3. 성공하면 UserAuth.log_in_user/3으로 세션을 갱신하고 복귀 경로로 이동한다. 실패에는 동일 오류 문구를 사용한다.

FN-AUTH-05 로그아웃

항목 정의
ID FN-AUTH-05
이름 로그아웃
화면과 경로 AUTH DELETE /users/log-out, S1-S6 사용자 메뉴
행위자와 권한 현재 사용자; 미로그인 요청도 정리 후 이동
선행 조건 없음
입력과 검증·오류 문구 CSRF 보호를 거친 DELETE 요청; 업무 필드 없음
출력·flash·PubSub·감사 이벤트 Logged out successfully.와 / 이동; 연결 LiveView의 disconnect, 감사 없음
예외와 오류 처리 이미 세션이 없어도 로그아웃 경로 처리; UI 한글 메뉴는 F1/F2 진행 중
관련 REQ REQ-ACC-04
구현 상태 구현
근거 UserSessionController.delete/2, UserAuth.log_out_user/1; test/tickmark_web/controllers/user_session_controller_test.exs, test/tickmark_web/user_auth_test.exs

처리 흐름:

  1. 컨트롤러가 UserAuth.log_out_user/1을 호출한다.
  2. 세션 토큰이 있으면 Accounts.delete_user_session_token/1로 삭제하고 LiveView 세션에 disconnect를 방송한다.
  3. 세션과 로그인 유지 쿠키를 비우고 홈으로 이동한다. 업무 감사 이벤트는 없다.

FN-AUTH-06 이메일 설정 변경

항목 정의
ID FN-AUTH-06
이름 이메일 설정 변경
화면과 경로 AUTH /users/settings, /users/settings/confirm-email/:token
행위자와 권한 최근 인증한 계정 본인; 감사 대상 역할과 무관
선행 조건 기본 20분 이내 인증인 sudo_mode; 이메일 변경 링크 7일 유효
입력과 검증·오류 문구 email은 가입 규칙과 동일; 기존 값이면 did not change, 중복 거부
출력·flash·PubSub·감사 이벤트 요청 A link to confirm your email change has been sent to the new address.; 확인 Email changed successfully.; 감사 없음
예외와 오류 처리 재인증 필요 시 로그인 이동; 확인 실패 Email change link is invalid or it has expired.
관련 REQ REQ-ACC-05
구현 상태 구현; 한글화 진행 중 F2
근거 UserLive.Settings, Accounts.sudo_mode?/1, update_user_email/2; test/tickmark_web/live/user_live/settings_test.exs, test/tickmark/accounts_test.exs

처리 흐름:

  1. UserAuth의 require_sudo_mode와 Accounts.sudo_mode?/1으로 최근 인증을 확인한다.
  2. Accounts.change_user_email/2를 검증하고 Accounts.deliver_user_update_email_instructions/3로 새 이메일 확인 토큰·메일을 만든다. 이 시점에는 기존 이메일을 유지한다.
  3. 확인 링크에서 Accounts.update_user_email/2로 이메일 변경과 관련 토큰 삭제를 트랜잭션으로 처리한다.
  4. 성공·실패 flash와 함께 설정 화면으로 이동한다. Oban 잡·감사 이벤트는 없다.

FN-AUTH-07 비밀번호 설정 변경

항목 정의
ID FN-AUTH-07
이름 비밀번호 설정 변경
화면과 경로 AUTH /users/settings, POST /users/update-password
행위자와 권한 최근 인증한 계정 본인
선행 조건 sudo_mode; 매직 링크로 가입한 사용자도 설정 가능
입력과 검증·오류 문구 password 필수 12-72자·해시 전 72바이트 제한, password_confirmation 일치; does not match password
출력·flash·PubSub·감사 이벤트 Password updated successfully!; 이전 세션 disconnect 후 새 자격으로 로그인, 설정 화면 복귀; 감사 없음
예외와 오류 처리 changeset 오류는 필드에 표시; 재인증 미충족은 로그인 유도
관련 REQ REQ-ACC-05
구현 상태 구현; 한글화 진행 중 F2
근거 UserLive.Settings, UserSessionController.update_password/2, Accounts.update_user_password/2; test/tickmark_web/live/user_live/settings_test.exs, test/tickmark_web/controllers/user_session_controller_test.exs

처리 흐름:

  1. Accounts.change_user_password/3으로 입력 중 검증을 하고 제출 시 해시 생성 가능한 changeset을 만든다.
  2. 유효하면 HTTP POST로 넘겨 sudo_mode를 다시 확인하고 Accounts.update_user_password/2로 비밀번호 변경·기존 토큰 삭제를 처리한다.
  3. UserAuth.disconnect_sessions/1 후 새 비밀번호로 로그인하고 /users/settings로 돌아간다.

5. S1: 감사 대상과 공통 진입

FN-S1-01 감사 대상 목록 조회

항목 정의
ID FN-S1-01
이름 감사 대상 목록 조회
화면과 경로 S1 /engagements
행위자와 권한 로그인 사용자 전원; 목록은 본인 멤버십 범위
선행 조건 인증 성공
입력과 검증·오류 문구 업무 입력 없음
출력·flash·PubSub·감사 이벤트 회사명·연도·결산일·중요성·최근 실행·검토 큐·내 역할; 비면 감사 대상이 없습니다.; 감사 없음
예외와 오류 처리 미로그인 로그인 이동; 실행 없음·실행 중 큐 수는 대시; S1은 PubSub 미구독
관련 REQ REQ-ENG-01, REQ-ACC-06, REQ-RUN-12
구현 상태 구현
근거 EngagementLive.Index.mount/3, Engagements.list_engagements/1, Testing.latest_run_summaries/1; test/tickmark_web/live/engagement_live_test.exs

처리 흐름:

  1. Engagements.list_engagements/1로 본인 멤버십과 감사 대상을 최신 생성순으로 읽는다.
  2. Testing.latest_run_summaries/1로 대상별 최근 실행과 기준 이상 finding 수를 읽는다.
  3. LiveView stream으로 표를 표시한다. 구독·잡·쓰기 없이 재진입 때 최신 값을 읽는다.

FN-S1-02 감사 대상 생성

항목 정의
ID FN-S1-02
이름 감사 대상 생성
화면과 경로 S1 /engagements/new
행위자와 권한 로그인 사용자 전원
선행 조건 없음
입력과 검증·오류 문구 client_name 1-200자, fiscal_year 2000-2100, period_end_date 필수, performance_materiality >0; 기준 오류 can't be blank, must be greater than 0 등 영어 changeset
출력·flash·PubSub·감사 이벤트 감사 대상을 만들었습니다. 후 S2; engagement.created, audit_appended
예외와 오류 처리 필드 오류 시 패널 유지·아무 행도 저장하지 않음; 취소는 /engagements patch; 한국어 목표 문구는 화면 부록 A
관련 REQ REQ-ENG-02, REQ-ENG-03
구현 상태 구현; 검증 문구 한글화 진행 중 F2
근거 EngagementLive.Index.handle_event/3, Engagements.create_engagement/2; test/tickmark/engagements_test.exs, test/tickmark_web/live/engagement_live_test.exs

처리 흐름:

  1. 인라인 패널을 열고 Engagements.change_engagement/2로 입력을 검증한다.
  2. Engagements.create_engagement/2가 scope 사용자 ID로 대상, preparer 멤버, engagement.created 이벤트를 하나의 Multi에 넣는다.
  3. 커밋 뒤 AuditTrail.broadcast_appended/1을 호출하고 S2로 이동한다. Oban 잡은 없다.

FN-S1-03 홈 진입과 공통 메뉴 이동

항목 정의
ID FN-S1-03
이름 홈 진입과 공통 메뉴 이동
화면과 경로 홈 /, S1-S6, AUTH 공통 셸
행위자와 권한 홈은 누구나; 대상·로그 링크는 멤버, 계정 메뉴는 본인
선행 조건 인증 상태와 선택한 engagement
입력과 검증·오류 문구 홈·브랜드·감사 대상 이름·감사 로그·계정 메뉴 클릭; 업무 입력 없음
출력·flash·PubSub·감사 이벤트 목표: 로그인 홈은 S1, 비로그인 홈은 한국어 소개·로그인·가입; 대상 링크와 현재 화면 aria-current; 감사 없음
예외와 오류 처리 비멤버 대상 링크는 404; 목표 문구 페이지를 찾을 수 없습니다. 없는 주소이거나 내가 멤버가 아닌 감사 대상입니다.
관련 REQ REQ-UI-01, REQ-UI-04
구현 상태 진행 중 F1; 기준 코드는 Phoenix 기본 홈·셸
근거 계약 12.4, 화면 3.1-3.3; lib/tickmark_web/controllers/page_controller.ex, components/layouts.ex; test/tickmark_web/controllers/page_controller_test.exs, error_html_test.exs는 기존 기본 동작 근거

처리 흐름:

  1. 목표 동작: /에서 현재 scope를 읽어 로그인 상태에 맞게 S1 이동 또는 소개 화면을 선택한다. 현재 PageController.home/2는 home 렌더만 한다.
  2. 목표 동작: TickmarkWeb.Layouts.app/1에 engagement와 active를 받아 대상·로그·계정 링크를 그린다.
  3. 선택한 링크의 기존 라우트로 이동하고 그 진입점의 인증·멤버십 검사를 적용한다. 로그아웃은 FN-AUTH-05다.

FN-S1-04 테마 선택

항목 정의
ID FN-S1-04
이름 테마 선택
화면과 경로 S1-S6, AUTH 공통 셸
행위자와 권한 누구나; 멤버 권한과 무관
선행 조건 브라우저에서 앱 화면 열림
입력과 검증·오류 문구 system, light, dark 중 선택
출력·flash·PubSub·감사 이벤트 현재 data-theme 변경 및 phx:theme 저장; 목표 저장키 tk:theme; 서버 쓰기·감사 없음
예외와 오류 처리 저장소 사용 불가 시 현재 페이지에만 적용하는 목표는 기준 코드에 try/catch 없음; 확인 필요
관련 REQ REQ-UI-02
구현 상태 진행 중 F1; 기존 전환 코드 있음, 목표 계약 정합 대기
근거 화면 3.1; TickmarkWeb.Layouts.theme_toggle/1, lib/tickmark_web/components/layouts/root.html.heex:15; 전용 동작 테스트 확인 필요

처리 흐름:

  1. 테마 버튼이 phx:set-theme 브라우저 이벤트를 보낸다.
  2. 현재 스크립트는 system이면 저장값을 지우고 OS 선호를 읽으며, light/dark면 localStorage에 저장한다.
  3. 루트 data-theme를 바꾸고 storage 및 OS 변경도 반영한다. 목표 tk:theme·저장 실패 처리는 F1 통합 후 검증한다.

6. S2: 감사 대상 상세

FN-S2-01 상세·멤버·적재·실행 이력 조회

항목 정의
ID FN-S2-01
이름 상세·멤버·적재·실행 이력 조회
화면과 경로 S2 /engagements/:id
행위자와 권한 preparer, reviewer의 :view
선행 조건 감사 대상 멤버
입력과 검증·오류 문구 id UUID; 실행 열기에서 해당 run_id
출력·flash·PubSub·감사 이벤트 멤버·GL·실행 표와 최신 통계, 실행 열기는 S4; 감사 없음
예외와 오류 처리 비멤버 404; 성공 적재·종료 실행이 없으면 대시; finding_updated만으로 승인 통계 갱신하지 않는 제한
관련 REQ REQ-ENG-06, REQ-GL-07, REQ-GL-08
구현 상태 구현; 검토 후 요약 자동 갱신은 제한
근거 EngagementLive.Show.mount/3, Ledger.list_imports/2, Testing.list_test_runs/2, count_findings/2; test/tickmark_web/live/engagement_live_test.exs

처리 흐름:

  1. Engagements.get_engagement!/2, list_members/2, Ledger.list_imports/2, Testing.list_test_runs/2를 호출한다.
  2. 최신 성공 적재로 전표·행·차대 합계를, 최신 종료 실행으로 큐·경미·승인 수를 구성한다.
  3. 연결된 mount에서 Engagements.subscribe/2를 호출하고 import_updated·test_run_updated 수신 시 데이터를 다시 읽는다.
  4. 열기 링크는 기존 실행 S4로 이동한다. 조회 트랜잭션·새 잡·감사 이벤트는 없다.

FN-S2-02 멤버 추가

항목 정의
ID FN-S2-02
이름 멤버 추가
화면과 경로 S2 /engagements/:id
행위자와 권한 생성자인 멤버만 :manage_members; 일반 preparer와 reviewer는 불가
선행 조건 추가 대상 사용자가 이미 가입
입력과 검증·오류 문구 email 앞뒤 공백 제거; role=preparer/reviewer; 없음 가입한 사용자 중 이 이메일이 없습니다., 중복 이미 이 감사 대상의 멤버입니다., 잘못된 역할 역할을 다시 선택하세요.
출력·flash·PubSub·감사 이벤트 멤버를 추가했습니다.; 현재 멤버 stream 추가; engagement.member_added, audit_appended
예외와 오류 처리 권한 없음 멤버를 추가할 권한이 없습니다.; invalid changeset은 저장 없음; 별도 member_added 방송 없음
관련 REQ REQ-ENG-04, REQ-ENG-05
구현 상태 구현
근거 Engagements.add_member/4, EngagementLive.Show.handle_event/3; test/tickmark/engagements_test.exs, test/tickmark_web/live/engagement_live_test.exs

처리 흐름:

  1. Engagements.authorize/3가 멤버십과 created_by_id를 검사하고 가입된 이메일을 찾는다.
  2. Tickmark.Engagements.Member.changeset/2로 역할·중복을 검증해 멤버와 감사 이벤트를 Multi로 저장한다.
  3. 커밋 후 AuditTrail.broadcast_appended/1을 호출하고 현재 화면에 멤버를 추가하며 폼을 초기화한다.

FN-S2-03 GL 테스트 실행

항목 정의
ID FN-S2-03
이름 GL 테스트 실행
화면과 경로 S2 /engagements/:id; 성공 시 S4
행위자와 권한 preparer의 :start_test_run
선행 조건 현재 대상의 succeeded 적재; 같은 GL의 running 실행 없음
입력과 검증·오류 문구 start_run 이벤트 id=ledger_import UUID; 화면은 rule_ids=:all 고정
출력·flash·PubSub·감사 이벤트 S4 이동; test_run.started, test_run_updated, audit_appended; RuleWorker 잡 8개
예외와 오류 처리 적재가 끝난 GL만 테스트할 수 있습니다., 이 GL에 실행 중인 테스트가 있습니다., 테스트를 실행할 권한이 없습니다., 일반 실패 테스트를 시작하지 못했습니다. 다시 시도하세요.
관련 REQ REQ-RUN-01, REQ-RUN-02
구현 상태 구현
근거 EngagementLive.Show.handle_event/3, Testing.start_test_run/3; test/tickmark/testing/runner_test.exs, test/tickmark_web/live/engagement_live_test.exs

처리 흐름:

  1. Ledger.get_import!/2로 읽고 현재 S2의 engagement_id와 일치하는지 확인한다.
  2. Testing.start_test_run/3이 권한과 DB의 적재 상태를 다시 읽고 룰 레지스트리를 해석한다.
  3. 실행, 룰 기본값 스냅샷, 감사 이벤트, 룰별 Oban 잡을 한 Multi로 저장한다. partial unique가 같은 적재의 running 중복을 막는다.
  4. 커밋 뒤 방송하고 S4로 이동한다. 룰 수행은 FN-SYS-02에서 계속된다.

7. S3: GL 불러오기

FN-S3-01 CSV 업로드 적재 요청

항목 정의
ID FN-S3-01
이름 CSV 업로드 적재 요청
화면과 경로 S3 /engagements/:id/imports/new?source=upload
행위자와 권한 preparer의 :import_ledger
선행 조건 감사 대상 멤버; 업로드가 완료된 파일 1개
입력과 검증·오류 문구 .csv 1개, 최대 20,000,000바이트; 필드·오류는 3.2-3.3; 사용자 파일명은 basename으로 보존
출력·flash·PubSub·감사 이벤트 대기 결과 패널; ledger_import.created, import_updated, audit_appended; ImportWorker 등록
예외와 오류 처리 검토자 GL을 불러올 권한이 없습니다.; 파일 제한은 3.3; 처리 실패 파일을 처리하지 못했습니다. 같은 파일을 다시 올려 주세요. 계속 실패하면 관리자에게 알려 주세요.
관련 REQ REQ-GL-01, REQ-GL-02, REQ-GL-08
구현 상태 구현
근거 ImportLive.New.handle_event/3, Ledger.create_import/3; test/tickmark_web/live/import_live_test.exs, test/tickmark/ledger_test.exs

처리 흐름:

  1. allow_upload(:ledger)와 validate로 파일 형식·수·크기를 확인하고 save에서 Engagements.authorize/3을 다시 호출한다.
  2. consume_uploaded_entries 콜백 안에서 Ledger.create_import/3을 호출해 임시 파일이 사라지기 전에 관리 디렉터리에 UUID 이름으로 복사한다.
  3. 서버에서도 크기를 검사하고 SHA-256을 계산한 뒤 pending 적재·감사 이벤트·ImportWorker 잡을 하나의 Multi로 저장한다.
  4. 커밋 뒤 import_updated와 audit_appended를 전송하고 현재 화면에 대기 패널을 둔다. 검증·실제 적재는 FN-SYS-01이다.

FN-S3-02 업로드 파일 선택 취소

항목 정의
ID FN-S3-02
이름 업로드 파일 선택 취소
화면과 경로 S3 /engagements/:id/imports/new
행위자와 권한 preparer; 폼 표시 권한 :import_ledger
선행 조건 아직 consume하지 않은 파일 선택
입력과 검증·오류 문구 cancel_upload 이벤트의 ref=현재 업로드 참조
출력·flash·PubSub·감사 이벤트 선택 파일·업로드 진행 표시 제거; DB 변경·감사·PubSub 없음
예외와 오류 처리 이미 등록된 ImportWorker의 취소 기능이 아님; 현재 업로드 참조가 아닌 값의 직접 이벤트는 별도 수락 시나리오 없음
관련 REQ REQ-GL-01
구현 상태 구현
근거 ImportLive.New.handle_event/3, Phoenix.LiveView.cancel_upload/3; test/tickmark_web/live/import_live_test.exs: switching source tabs preserves a selected file and selection can be cancelled

처리 흐름:

  1. 사용자가 선택 파일 옆의 취소 버튼을 누른다.
  2. LiveView가 ref로 cancel_upload/3을 호출한다.
  3. 업로드 상태에서 그 파일을 제거한다. context 호출·트랜잭션·잡 취소는 없다.

FN-S3-03 합성 GL 생성과 적재 요청

항목 정의
ID FN-S3-03
이름 합성 GL 생성과 적재 요청
화면과 경로 S3 /engagements/:id/imports/new?source=generated
행위자와 권한 preparer의 :import_ledger
선행 조건 감사 대상 멤버
입력과 검증·오류 문구 sample.seed 정수, 공백은 기본 20251231; 정수가 아니면 정수로 입력하세요.; 목표 행 수 입력 없음
출력·flash·PubSub·감사 이벤트 합성 GL과 별도 labels.csv, pending 적재; ledger_import.created, import_updated, audit_appended; ImportWorker 등록
예외와 오류 처리 권한 없음은 업로드와 같은 문구; 미지원 opts는 내부 API ArgumentError; 파일 오류의 화면 처리는 일반 실패 문구
관련 REQ REQ-GL-05, REQ-GL-06
구현 상태 구현
근거 Ledger.generate_sample_import/3, Tickmark.Ledger.Generator.generate/1; test/tickmark/ledger/generator_test.exs, test/tickmark/ledger_test.exs, test/tickmark_web/live/import_live_test.exs

처리 흐름:

  1. LiveView에서 권한을 확인하고 seed를 정수로 파싱한다.
  2. Ledger.generate_sample_import/3이 권한을 다시 확인하고 Generator.generate/1로 GL CSV와 정답 라벨을 만든다.
  3. 두 파일을 저장하고 원본 이름을 synthetic-gl-seed-N.csv로 등록한다. GL만 적재 대상이며 라벨은 DB·룰·AI 입력에 넣지 않는다.
  4. 업로드와 같은 등록 트랜잭션으로 적재·이벤트·잡을 저장하고 대기 패널을 표시한다. 등록 오류 반환 시 파일을 정리한다.

FN-S3-04 CSV와 합성 생성 탭 전환

항목 정의
ID FN-S3-04
이름 CSV와 합성 생성 탭 전환
화면과 경로 S3 /engagements/:id/imports/new?source=upload 또는 generated
행위자와 권한 preparer; reviewer는 거부 패널
선행 조건 S3 열림
입력과 검증·오류 문구 source는 generated일 때만 생성 탭, 나머지는 upload
출력·flash·PubSub·감사 이벤트 같은 LiveView에서 폼 표시만 전환; 선택 파일 보존; 감사 없음
예외와 오류 처리 잘못된 source 값은 upload로 정규화; 검토자 직접 URL 진입은 현재 화면의 권한 없음 안내
관련 REQ REQ-GL-01
구현 상태 구현
근거 ImportLive.New.handle_params/3; test/tickmark_web/live/import_live_test.exs

처리 흐름:

  1. 탭 링크가 source 쿼리만 push_patch한다.
  2. ImportLive.New.handle_params/3가 source를 허용 값으로 정규화한다.
  3. 해당 폼을 표시하고 기존 업로드 상태는 유지한다. context·트랜잭션·잡·감사 호출은 없다.

FN-S3-05 적재 결과와 오류 확인

항목 정의
ID FN-S3-05
이름 적재 결과와 오류 확인
화면과 경로 S3 /engagements/:id/imports/new; 전체 이력은 S2
행위자와 권한 preparer; S2 이력은 양 역할의 :view
선행 조건 현재 화면에서 요청한 ledger_import 또는 S2 적재 이력
입력과 검증·오류 문구 진행 메시지의 import_id; 경고 펼침은 details; 오류 필드·코드는 3.3
출력·flash·PubSub·감사 이벤트 상태·전표·행·차대 합계·경고·해시 표시; 성공 시 감사 대상에서 테스트 실행으로 S2 이동; 조회 감사 없음
예외와 오류 처리 오류가 있으면 전체 수와 보존 최대 100건, 초과 그 밖에 N건; 오류 0건의 시스템 실패는 고정 안내만 표시
관련 REQ REQ-GL-04, REQ-GL-07, REQ-GL-08
구현 상태 구현
근거 ImportLive.New.handle_info/2, ImportLive.Components.import_result/1, Ledger.get_import!/2; test/tickmark_web/live/import_live_test.exs

처리 흐름:

  1. 연결된 S3는 engagement 토픽을 구독하고 현재 선택된 적재와 같은 import_updated만 처리한다.
  2. Ledger.get_import!/2로 권한을 다시 확인하며 최신 결과를 읽는다. pending·processing은 상태만 표시하고 행별 진행률은 계산하지 않는다.
  3. 실패이면 고정 코드 문구·위치·개수를, 성공이면 집계를 보여 준다. 경고 details는 브라우저에서 펼친다.
  4. 성공 뒤 실행 링크를 누르면 S2로 이동한다. 이 화면은 Testing.start_test_run/3을 직접 호출하지 않는다.

8. S4: 테스트 실행

FN-S4-01 룰 진행률과 실행 결과 조회

항목 정의
ID FN-S4-01
이름 룰 진행률과 실행 결과 조회
화면과 경로 S4 /engagements/:id/runs/:run_id
행위자와 권한 preparer, reviewer의 :view
선행 조건 해당 감사 대상의 실행
입력과 검증·오류 문구 id, run_id UUID; rule_progress, run_completed, finding_updated
출력·flash·PubSub·감사 이벤트 룰 상태·적중 수·기준점수·실행자 및 종료 후 findings; 실행 중 룰이 모두 끝나면 검토 큐가 열립니다.; 조회 감사 없음
예외와 오류 처리 비멤버·다른 대상 실행 404; 일부 실패는 고정 안내, raw error 비표시; AI·판단 열은 현재 자리표시
관련 REQ REQ-RUN-03, REQ-RUN-12, REQ-AI-05
구현 상태 부분 (룰·결과 구현, AI·판단 열 미구현)
근거 Testing.get_test_run!/2, subscribe_test_run/2, TestRunLive.Show.handle_info/2; test/tickmark_web/live/test_run_live_test.exs

처리 흐름:

  1. Engagements.get_engagement!/2, Testing.get_test_run!/2, Ledger.get_import!/2를 읽고 실행 소속을 비교한다.
  2. 연결된 mount에서 engagement와 test_run 토픽을 구독한다.
  3. rule_progress·run_completed 수신 시 DB에서 실행을 다시 읽고 성공·실패·미실행 종료 수로 진행률을 표시한다.
  4. 종료 뒤 Testing.list_findings/3, count_findings/2로 목록과 큐 수를 표시한다. 트랜잭션·잡·감사 생성은 없다.

FN-S4-02 발견 목록 탭과 필터 변경

항목 정의
ID FN-S4-02
이름 발견 목록 탭과 필터 변경
화면과 경로 S4 경로에 ?tab=queue&status=...&rule_id=...
행위자와 권한 preparer, reviewer의 :view
선행 조건 실행 종료 뒤 발견 조회 가능
입력과 검증·오류 문구 tab, status=open/concluded/approved/rejected, rule_id는 실행 룰 목록의 값; 잘못된 값은 기본값·필터 없음
출력·flash·PubSub·감사 이벤트 점수 내림차순, 전표번호 오름차순 목록; 비면 조건에 맞는 전표가 없습니다.; 감사 없음
예외와 오류 처리 실행 중은 findings 비표시; minor는 AI 영역 숨김; 현재 페이지 나누기와 limit 입력 없음
관련 REQ REQ-RUN-13
구현 상태 구현
근거 TestRunLive.Show.handle_params/3, Testing.list_findings/3, count_findings/2; test/tickmark_web/live/test_run_live_test.exs, test/tickmark/testing_test.exs

처리 흐름:

  1. 탭 또는 filter 이벤트로 쿼리를 push_patch한다.
  2. 허용 값만 atom으로 매핑하고 queue=:review/:minor/:all과 status·rule_id 필터를 만든다. 임의 atom 생성은 하지 않는다.
  3. Testing.list_findings/3으로 조회한 목록을 stream reset하고 count_findings/2로 큐 수를 읽는다. 탭 수는 상태 필터 결과 수가 아닌 큐 전체 수다.

FN-S4-03 AI 트리아지 일괄 요청

항목 정의
ID FN-S4-03
이름 AI 트리아지 일괄 요청
화면과 경로 S4 /engagements/:id/runs/:run_id
행위자와 권한 preparer의 :request_triage
선행 조건 목표 UI는 실행 종료 후; context는 실행 중 0건 반환
입력과 검증·오류 문구 run_id; 화면 입력 옵션 없음; 기본 상한 100은 AI.config; context의 limit 옵션은 테스트용
출력·flash·PubSub·감사 이벤트 연결 후 목표 AI 트리아지 N건을 요청했습니다. 또는 새로 요청할 항목이 없습니다.; triage_requested; 실제 1건 이상이면 ai_triage.requested, audit_appended
예외와 오류 처리 권한 없음 목표 AI 트리아지를 요청할 권한이 없습니다.; none만 선택하므로 failed 재요청은 S5; 중복 요청은 추가 job 없음
관련 REQ REQ-AI-01
구현 상태 부분 (context·테스트 구현, 현재 버튼 disabled)
근거 계약 5.5.1, 화면 S4.3·부록 A; Triage.request_triage/2, /3; test/tickmark/triage_test.exs; 현재 UI test/tickmark_web/live/test_run_live_test.exs

처리 흐름:

  1. 목표 UI는 Triage.request_triage/2에 현재 실행 ID를 넘긴다. context에서 실행과 권한을 확인한다.
  2. 점수가 실행 threshold 이상이고 triage_status=none인 후보를 점수·금액 내림차순, 전표번호 오름차순으로 최대 100건 선택한다.
  3. 트랜잭션에서 none 조건을 다시 검사하는 update_all로 claim한 ID만 queued로 바꾸고 TriageWorker 잡을 만든다. 1건 이상이면 실행 단위 요청 감사 이벤트 하나를 넣는다.
  4. 커밋 뒤 enqueued 수와 triage_requested를 전송한다. UI 연결 후 해당 수로 안내하고 나머지 항목은 다시 요청할 수 있다.

FN-S4-04 전표 검토 화면 열기

항목 정의
ID FN-S4-04
이름 전표 검토 화면 열기
화면과 경로 S4에서 S5 /engagements/:id/findings/:finding_id
행위자와 권한 preparer, reviewer의 :view
선행 조건 실행 종료 후 발견 행 존재
입력과 검증·오류 문구 전표번호 링크의 finding_id; 목표는 현재 tab·status도 전달
출력·flash·PubSub·감사 이벤트 S5에서 전표·룰·AI·판단 표시; 감사 없음
예외와 오류 처리 S5 진입에서 다시 권한·소속 검사; 기준 링크는 쿼리 없이 이동해 선택 맥락을 넘기지 않음
관련 REQ REQ-RUN-13, REQ-UI-06
구현 상태 부분 (이동 구현, 필터 맥락 유지 미구현)
근거 화면 S4.3; lib/tickmark_web/live/test_run_live/components.ex:173; test/tickmark_web/live/test_run_live_test.exs, test/tickmark_web/live/finding_live_test.exs

처리 흐름:

  1. 발견 표에서 전표번호 링크를 선택한다.
  2. 현재 코드는 engagement와 finding ID로 navigate한다. 목표 tab·status 전달은 라운드 4b에 남아 있다.
  3. S5가 Testing.get_finding!/2를 호출하여 상세를 읽는다. 새 감사 이벤트·잡은 없다.

9. S5: 전표 검토

기준 커밋에는 읽기·초안 검증·갱신이 구현되어 있고, 결론 제출 및 승인·반려 단추는 비활성이다. FN-S5-02의 제출과 FN-S5-03/04는 W6 진행 중이다. 아래의 처리 흐름은 구현된 Review context와 계약 12.3의 연결 계획을 구분해서 적었다.

FN-S5-01 전표 사실·행·룰 근거 조회

항목 정의
ID FN-S5-01
이름 전표 사실·행·룰 근거 조회
화면과 경로 S5 /engagements/:id/findings/:finding_id
행위자와 권한 preparer, reviewer의 :view
선행 조건 해당 감사 대상의 finding
입력과 검증·오류 문구 id·finding_id UUID; 근거 펼침은 브라우저 details
출력·flash·PubSub·감사 이벤트 전기일·증빙일·입력시각·회계기간·입력자·승인자·source·거래처·적요, 행과 차대 합계, 룰 점수 산식; 감사 없음
예외와 오류 처리 비멤버·없는 ID·다른 대상의 finding은 동일 404; 자유문은 이스케이프, 내부 오류 원문 비표시
관련 REQ REQ-RUN-04, REQ-RUN-05, REQ-RUN-06, REQ-RUN-07, REQ-RUN-08, REQ-RUN-09, REQ-RUN-10, REQ-RUN-11, REQ-RUN-14
구현 상태 구현
근거 Testing.get_finding!/2, list_rule_results/2, FindingLive.Show.mount/3; test/tickmark/testing_test.exs, test/tickmark_web/live/finding_live_test.exs

처리 흐름:

  1. Testing.get_finding!/2와 Engagements.get_engagement!/2로 데이터를 읽고 URL 소속을 비교한다.
  2. Testing.list_rule_results/2로 같은 실행·전표의 룰 적중을 레지스트리 순서로 읽는다. 원본 행은 line_number 순서다.
  3. 원본·합계와 점수 산식을 표시하고 근거 details를 펼칠 수 있게 한다. 합계가 100을 넘으면 min(100, 합계)로 표시한다.
  4. 연결된 mount는 engagement 토픽을 구독한다. 현재 항목의 finding_updated만 재조회하고 unrelated 메시지는 무시한다.

FN-S5-02 결론 작성·제출·재제출

항목 정의
ID FN-S5-02
이름 결론 작성·제출·재제출
화면과 경로 S5 /engagements/:id/findings/:finding_id
행위자와 권한 preparer의 :submit_conclusion
선행 조건 finding.status가 open·concluded·rejected; AI 제안은 필수 아님
입력과 검증·오류 문구 outcome=no_exception/needs_follow_up/exception, comment 1-2000자 필수, 화면에 보인 ai_triage_id 선택; 실제 검증 판단을 선택하세요, 코멘트를 입력하세요 (1-2000자)
출력·flash·PubSub·감사 이벤트 연결 후 목표 결론을 제출했습니다. 검토자에게 검토 대기로 보입니다.; 새 conclusion, concluded, conclusion.submitted, finding_updated, audit_appended
예외와 오류 처리 approved 목표 승인된 항목은 수정할 수 없습니다., 권한 없음 목표 결론을 제출할 권한이 없습니다.; 다른 finding 제안은 이 검토 항목의 AI 제안을 선택해 주세요; 재제출은 새 행
관련 REQ REQ-REV-01, REQ-REV-02, REQ-REV-03, REQ-REV-07, REQ-AI-06
구현 상태 진행 중 W6; context와 초안 검증·보존 구현, 제출 disabled
근거 계약 8, 12.3장; 화면 S5.4·부록 A; Review.submit_conclusion/3, Tickmark.Review.Conclusion.changeset/2; test/tickmark/review_test.exs, test/tickmark_web/live/finding_live_test.exs

처리 흐름:

  1. 입력 중 validate는 Conclusion.changeset/2만 호출하여 글자 수와 필드 오류를 표시한다. DB에 초안을 저장하지 않는다.
  2. 연결 후 제출은 화면에 그린 ai_triage_id와 입력값으로 Review.submit_conclusion/3을 호출한다. 사용자·finding 소속은 DB와 scope에서 확인한다.
  3. finding을 FOR UPDATE로 먼저 잠그고 현재 상태를 다시 검사한다. 같은 finding의 AI 제안인지 확인한 뒤 새 결론 행, concluded 상태, 감사 이벤트를 한 Multi에 저장한다.
  4. 커밋 뒤 finding_updated와 audit_appended를 전송한다. 재제출도 기존 결론을 수정하지 않고 새 행으로 남긴다.
  5. 다른 사람의 제출을 수신하면 기존 초안·오류를 보존하고 안내한다. 승인 수신 시 읽기 전용으로 잠근다. 서버에 중복 제출 키는 없으므로 제출 버튼 잠금은 W6 검증 대상이다.

FN-S5-03 최신 결론 승인

항목 정의
ID FN-S5-03
이름 최신 결론 승인
화면과 경로 S5 /engagements/:id/findings/:finding_id
행위자와 권한 reviewer의 :review_conclusion, 결론 작성자와 다른 사용자
선행 조건 finding.status=concluded, 최신 결론 존재
입력과 검증·오류 문구 화면에 표시된 conclusion_id, decision=approved, 선택 comment 최대 2000자; 확인/취소 목표
출력·flash·PubSub·감사 이벤트 연결 후 서명 바 승인·작성자 폼 잠금; review.approved, finding_updated, audit_appended; 성공 flash 별도 확정 없음
예외와 오류 처리 stale 목표 작성자가 새 결론을 제출했습니다. 최신 결론을 확인한 뒤 다시 결정하세요.; invalid_state 목표 이 항목은 이미 처리되었습니다. 최신 상태로 다시 불러왔습니다.; forbidden·self_review 문구는 아래
관련 REQ REQ-REV-04, REQ-REV-05, REQ-REV-06
구현 상태 진행 중 W6; context·DB 제약 구현, 화면 승인 disabled
근거 계약 8, 12.3장; Review.decide/4; test/tickmark/review_test.exs; test/tickmark_web/live/finding_live_test.exs는 disabled 기준

처리 흐름:

  1. 목표 UI는 승인 버튼 뒤 확인/취소를 보여 주며 확인 시 화면에 그려진 conclusion_id를 전송한다. 서버 assign의 최신 ID로 대체하지 않는다.
  2. Review.decide/4가 멤버 역할을 검사하고 finding을 FOR UPDATE로 잠근 뒤 concluded 상태를 확인한다.
  3. 최신 결론을 inserted_at·id 내림차순으로 읽어 전달 ID와 비교하고 자기 결론 여부를 검사한다. stale 검사가 self_review보다 먼저다.
  4. 검토 결정·approved 상태·감사 이벤트를 같은 Multi에 저장한다. unique conclusion_id, reviewer_not_preparer CHECK, 복합 FK가 DB에서도 검사한다.
  5. 커밋 후 방송한다. 역할 거부는 목표 승인, 반려할 권한이 없습니다., 자기 검토는 목표 본인이 제출한 결론은 승인, 반려할 수 없습니다.로 안내하며 멤버의 거부만 review.denied를 별도 기록한다.

FN-S5-04 사유를 붙여 결론 반려

항목 정의
ID FN-S5-04
이름 사유를 붙여 결론 반려
화면과 경로 S5 /engagements/:id/findings/:finding_id
행위자와 권한 reviewer의 :review_conclusion, 자기 결론 제외
선행 조건 finding.status=concluded, 최신 결론 존재
입력과 검증·오류 문구 화면 conclusion_id, decision=rejected, 공백 아닌 comment 필수·최대 2000자; 목표 반려 사유를 입력하세요
출력·flash·PubSub·감사 이벤트 연결 후 rejected·반려 사유·이력 표시; review.rejected, finding_updated, audit_appended; 작성자 재제출 가능
예외와 오류 처리 stale·invalid_state·forbidden·self_review는 FN-S5-03과 같음; 사유 검증 실패는 결정·상태·이벤트 모두 저장 안 함
관련 REQ REQ-REV-04, REQ-REV-05, REQ-REV-06
구현 상태 진행 중 W6; context·DB 제약 구현, 화면 반려 disabled
근거 계약 8, 12.3장; 화면 S5.2, S5.4; Review.decide/4, Tickmark.Review.ReviewDecision.changeset/2; test/tickmark/review_test.exs

처리 흐름:

  1. 목표 UI는 반려 버튼 뒤 사유 폼과 확인/취소를 열고 changeset으로 입력을 검증한다.
  2. 확인 시 화면 conclusion_id와 사유를 Review.decide/4에 보낸다. finding 잠금·상태·최신 ID·직무 분리 검사는 승인과 동일하다.
  3. 결정과 rejected 상태, 사유를 포함한 review.rejected를 같은 Multi에 저장한다. DB의 comment_required_on_reject가 공백 사유도 거부한다.
  4. 커밋 후 방송하며 작성자는 새 결론을 제출할 수 있다. 다른 finding_updated가 오면 검토 확인 단계를 해제하는 UI는 W6 확인 대상이다.

FN-S5-05 건별 AI 제안 요청·재요청

항목 정의
ID FN-S5-05
이름 건별 AI 제안 요청·재요청
화면과 경로 S5 /engagements/:id/findings/:finding_id
행위자와 권한 preparer의 :request_triage
선행 조건 finding.score가 해당 실행 threshold 이상; triage_status가 queued가 아님
입력과 검증·오류 문구 finding_id; 사용자가 모델·프롬프트를 임의 입력하는 폼 없음
출력·flash·PubSub·감사 이벤트 연결 후 분석 중 카드; ai_triage.requested, finding_updated, audit_appended; TriageWorker job 하나
예외와 오류 처리 already_queued 목표 이미 AI가 분석 중입니다.; forbidden 목표 AI 제안을 요청할 권한이 없습니다.; 기준 미만은 카드에 점수가 기준(N)보다 낮아 AI 트리아지 대상이 아닙니다.
관련 REQ REQ-AI-02
구현 상태 부분 (context·테스트 구현, 현재 요청 버튼 disabled)
근거 계약 5.5.1; 화면 S5.2-S5.4; Triage.request_finding_triage/2; test/tickmark/triage_test.exs, test/tickmark_web/live/finding_live_test.exs

처리 흐름:

  1. 목표 UI는 Triage.request_finding_triage/2에 현재 finding ID를 전달한다.
  2. context가 DB 소속과 request_triage 권한을 검사하고 finding을 FOR UPDATE로 잠근다.
  3. 점수가 threshold 미만이면 below_threshold, 이미 queued이면 already_queued로 거부한다. none·failed·succeeded는 queued 전이가 가능하다.
  4. queued 상태·Oban 잡·건별 요청 감사 이벤트를 한 Multi로 저장하고 커밋 뒤 방송한다. 결과 생성은 FN-SYS-03에서 수행한다.

FN-S5-06 AI 제안과 생성 정보 확인

항목 정의
ID FN-S5-06
이름 AI 제안과 생성 정보 확인
화면과 경로 S5 /engagements/:id/findings/:finding_id
행위자와 권한 preparer, reviewer의 :view
선행 조건 finding 조회 완료; 제안이 없거나 실패해도 화면 제공
입력과 검증·오류 문구 AI 생성 정보 details 펼침; 별도 입력 없음
출력·flash·PubSub·감사 이벤트 최신 성공 제안의 위험도·근거·절차·부족 정보 및 model·다른 response_model·prompt_version·input_hash·생성시각; 감사 없음
예외와 오류 처리 queued는 이전 결과 숨김; failed는 AI 제안을 받지 못했습니다.와 판단은 AI 제안 없이도 진행할 수 있습니다.; 내부 오류 미표시
관련 REQ REQ-AI-04, REQ-AI-05, REQ-AI-06
구현 상태 구현 (요청 버튼은 FN-S5-05의 미연결 범위)
근거 Testing.get_finding!/2, FindingLive.Components.ai_suggestion_card/1; test/tickmark_web/live/finding_live_test.exs

처리 흐름:

  1. Testing.get_finding!/2가 ai_triages를 inserted_at·id 내림차순으로 preload한다.
  2. triage_status=succeeded일 때만 최신 제안을 선택하고 AI 제안 상자에 사람 판단과 구분해 표시한다.
  3. 생성 정보를 details로 펼치며 요청 모델과 응답 모델이 같으면 중복 표시하지 않는다. 브라우저 펼침은 서버 쓰기·잡·감사 호출이 없다.

FN-S5-07 결론과 검토 이력 확인

항목 정의
ID FN-S5-07
이름 결론과 검토 이력 확인
화면과 경로 S5 /engagements/:id/findings/:finding_id
행위자와 권한 preparer, reviewer의 :view
선행 조건 finding 조회 완료
입력과 검증·오류 문구 별도 필드 없음
출력·flash·PubSub·감사 이벤트 결론·반려·재제출·승인의 행위자 이메일·시각·판단·사유를 시간순 표시; 비면 아직 결론이 없습니다.; 감사 없음
예외와 오류 처리 다른 사람 제출은 초안 유지 후 안내; approved는 잠금; 과거 결론을 지우는 동작 없음
관련 REQ REQ-REV-07
구현 상태 구현
근거 Testing.get_finding!/2, FindingLive.Show.handle_info/2, FindingLive.Components.finding_history/1; test/tickmark_web/live/finding_live_test.exs

처리 흐름:

  1. Testing.get_finding!/2에서 conclusions와 preparer, review_decisions와 reviewer를 preload한다.
  2. 두 이력을 시간순으로 합쳐 표시하고 최신 결론·마지막 결정으로 배지와 서명 바를 구성한다.
  3. 동일 finding_updated 수신 시 다시 읽는다. 조회로 새 감사 이벤트는 만들지 않는다.

FN-S5-08 검토 큐에서 다음 미판단 이동

항목 정의
ID FN-S5-08
이름 검토 큐에서 다음 미판단 이동
화면과 경로 S5 /engagements/:id/findings/:finding_id
행위자와 권한 preparer, reviewer의 :view
선행 조건 목표: S4의 tab·status와 같은 실행 큐
입력과 검증·오류 문구 현재 finding과 tab·status; 추가 폼 없음
출력·flash·PubSub·감사 이벤트 목표: 큐 현재 항목 aria-current, 다음 open 항목으로 이동; 감사 없음
예외와 오류 처리 현재 뒤에 open이 없으면 비활성; 다른 대상·비멤버 404; 기준 코드는 큐 및 이벤트 없음
관련 REQ REQ-UI-06
구현 상태 미구현 (라운드 4b 메인 예정)
근거 화면 S5.1·S5.4; 계약 12.3; lib/tickmark_web/live/finding_live/show.ex에 해당 handler 없음; 전용 테스트 없음

처리 흐름:

  1. 목표: S4에서 받은 tab·status를 허용 필터로 정규화하고 Testing.list_findings/3으로 같은 실행 큐를 읽는다.
  2. 목표: 현재 항목 뒤에서 첫 open 항목을 찾는다. 필터 결과에 open이 없다면 이동하지 않는다.
  3. 목표: 쿼리를 유지한 S5 경로로 navigate한다. 정확한 UI 이벤트명·통합 테스트는 구현 때 확정한다.

10. S6: 감사 로그

FN-S6-01 감사 로그 목록 조회

항목 정의
ID FN-S6-01
이름 감사 로그 목록 조회
화면과 경로 S6 /engagements/:id/audit
행위자와 권한 preparer, reviewer의 :view
선행 조건 감사 대상 멤버
입력과 검증·오류 문구 id UUID
출력·flash·PubSub·감사 이벤트 최신 seq순 50건, KST 시각·행위자·한글 동작명·action·대상·요약; 읽기 전용; 조회 감사 없음
예외와 오류 처리 비멤버 404; 비면 아직 감사 기록이 없습니다.; 역할별로 이벤트를 숨기지 않음
관련 REQ REQ-AUD-02
구현 상태 구현
근거 AuditTrail.list_events/3, AuditLive.Index.mount/3, handle_params/3; test/tickmark/audit_trail_test.exs, test/tickmark_web/live/audit_live_test.exs

처리 흐름:

  1. Engagements.get_engagement!/2와 list_members/2를 읽어 권한과 사용자 이메일 라벨을 구성한다.
  2. AuditTrail.list_events/3을 기본 limit 50으로 호출해 seq 내림차순 이벤트를 stream으로 표시한다.
  3. 연결 시 engagement 토픽을 구독한다. SQL 잠금·Oban 잡·새 감사 이벤트는 없다.

FN-S6-02 감사 로그 필터 적용·해제

항목 정의
ID FN-S6-02
이름 감사 로그 필터 적용·해제
화면과 경로 S6 ?actor=user/agent/system&subject=전표번호&run=UUID
행위자와 권한 preparer, reviewer의 :view
선행 조건 S6 열림
입력과 검증·오류 문구 actor 허용 세 값, subject 원문 최대 200 codepoint·공백만/제어문자 불가, run UUID; 잘못된 값은 필터 없음
출력·flash·PubSub·감사 이벤트 필터 AND 결과로 목록 reset·새 이벤트 수 0; 비면 조건에 맞는 이벤트가 없습니다.; 감사 없음
예외와 오류 처리 subject는 앞 0·공백을 보존하는 완전 일치; 일괄 AI 요청은 subject=test_run이라 전표 필터에서 나오지 않음
관련 REQ REQ-AUD-03
구현 상태 구현
근거 AuditLive.Index.handle_params/3, AuditTrail.list_events/3; test/tickmark/audit_trail_test.exs, test/tickmark_web/live/audit_live_test.exs

처리 흐름:

  1. 행위자 버튼이나 전표번호 적용이 현재 쿼리를 push_patch한다. run 필터는 URL 및 해제 링크로 제공한다.
  2. URL 값을 정규화하여 actor_type, entry_number, subject={test_run, run_id} 옵션으로 변환한다.
  3. AuditTrail.list_events/3을 다시 호출해 앞서 읽은 목록과 커서를 reset한다. 무효 필터를 오류 페이지로 만들지는 않는다.

FN-S6-03 이전 감사 이벤트 더 보기

항목 정의
ID FN-S6-03
이름 이전 감사 이벤트 더 보기
화면과 경로 S6 /engagements/:id/audit
행위자와 권한 preparer, reviewer의 :view
선행 조건 현재 페이지가 50건이어 더 보기 가능
입력과 검증·오류 문구 load_more; 커서는 서버에 저장한 oldest_seq 사용
출력·flash·PubSub·감사 이벤트 기존 목록 뒤에 최대 50건 추가; 끝이면 처음 기록까지 모두 보았습니다.; 감사 없음
예외와 오류 처리 50건 미만이면 버튼 제거; 정확히 50건이면 다음 조회가 0건일 수 있음; more? false면 noop
관련 REQ REQ-AUD-03
구현 상태 구현
근거 AuditLive.Index.handle_event/3, AuditTrail.list_events/3; test/tickmark_web/live/audit_live_test.exs, test/tickmark/audit_trail_test.exs

처리 흐름:

  1. 현재 more?와 oldest_seq를 확인한다.
  2. 현재 필터와 before_seq=oldest_seq, limit=50으로 AuditTrail.list_events/3을 호출한다. 커서는 strict less-than이다.
  3. 응답을 stream 뒤에 붙이고 마지막 seq·more?를 바꾼다. 읽기 외 변경은 없다.

FN-S6-04 새 이벤트 수동 갱신

항목 정의
ID FN-S6-04
이름 새 이벤트 수동 갱신
화면과 경로 S6 /engagements/:id/audit
행위자와 권한 preparer, reviewer의 :view
선행 조건 audit_appended로 새 항목을 감지했거나 refresh 이벤트
입력과 검증·오류 문구 refresh; 새 이벤트 식별은 seq·actor_type·subject·entry_number
출력·flash·PubSub·감사 이벤트 새 이벤트 N건 버튼, 클릭 후 최신 50건·카운터 0; 브라우저 audit:focus-first로 첫 행 포커스; 감사 없음
예외와 오류 처리 현재 필터와 불일치·이미 본 seq 무시; 중복 방송은 MapSet으로 한 번만 집계; 목록 자동 삽입 안 함
관련 REQ REQ-AUD-04, REQ-UI-05
구현 상태 구현
근거 AuditLive.Index.handle_info/2, handle_event/3; test/tickmark_web/live/audit_live_test.exs

처리 흐름:

  1. audit_appended의 seq가 newest_seq보다 크고 현재 필터에 맞으면 pending_seqs에 추가한다.
  2. 새 이벤트 수만 aria-live 영역에 알리고 사용자가 읽던 stream은 그대로 유지한다.
  3. 갱신 클릭 시 AuditTrail.list_events/3으로 다시 읽고 목록·커서·카운터를 reset한다.
  4. 첫 이벤트가 있으면 audit:focus-first를 push_event하여 해당 행으로 포커스를 옮긴다.

FN-S6-05 감사 payload와 대상 열기

항목 정의
ID FN-S6-05
이름 감사 payload와 대상 열기
화면과 경로 S6 /engagements/:id/audit; 대상은 S2·S4·S5
행위자와 권한 preparer, reviewer의 :view
선행 조건 해당 이벤트 표시 중
입력과 검증·오류 문구 payload details 또는 대상 링크 선택
출력·flash·PubSub·감사 이벤트 저장된 JSON 내용 표시, 대상 유형에 맞는 화면 이동; 조회 감사 없음
예외와 오류 처리 허용되지 않은 subject_type은 링크 없음; 연결 대상 진입에서 권한 검사; 과거 payload는 수정 불가
관련 REQ REQ-AUD-02
구현 상태 구현
근거 lib/tickmark_web/live/audit_live/components.ex, AuditTrail.list_events/3; test/tickmark_web/live/audit_live_test.exs

처리 흐름:

  1. 이벤트의 payload는 화면에 이미 들어 있으며 details를 열면 서버 왕복 없이 확인한다.
  2. finding은 S5, test_run은 S4, ledger_import·engagement는 S2로 향하는 현재 대상 링크를 선택한다.
  3. 대상 화면에서 기존 인증·멤버십·소속 검사를 수행한다. payload 펼침과 이동 자체는 감사 이벤트를 만들지 않는다.

11. SYS: 사용자 화면이 없는 처리

워커는 승인된 context가 등록한 식별자만 받는다. 웹의 역할 검사를 대체하는 공개 실행 API가 아니다. 아래 시스템 행위자는 system 또는 agent이고 멤버 역할 preparer, reviewer와 구분한다.

FN-SYS-01 GL 적재 잡 실행

항목 정의
ID FN-SYS-01
이름 GL 적재 잡 실행
화면과 경로 SYS 라우트 없음; 결과 S2·S3
행위자와 권한 system ledger.import_worker; 사용자 권한은 등록 시 :import_ledger
선행 조건 ingest 큐의 등록 잡, 관리 디렉터리의 GL 파일
입력과 검증·오류 문구 args ledger_import_id; 3.2 입력 계약, 100,000행·물리 줄 65,536바이트, 오류·경고 각 최대 100건 보존
출력·flash·PubSub·감사 이벤트 succeeded 또는 failed 적재, ledger_import.succeeded 또는 ledger_import.failed, import_updated, audit_appended
예외와 오류 처리 검증 실패·파일 없음은 실패 결과로 취소; DB 오류는 최대 3회 시도, timeout 10분; 최종 예외는 FailureHandler, 화면은 고정 오류
관련 REQ REQ-GL-02, REQ-GL-03, REQ-GL-07
구현 상태 구현
근거 Ledger.ImportWorker.perform/1, Ledger.process_import/1, Tickmark.Ledger.Importer.run/2, Tickmark.Ledger.CsvParser.parse/3; test/tickmark/ledger_test.exs, test/tickmark/ledger/import_worker_test.exs, test/tickmark/jobs/failure_handler_test.exs

처리 흐름:

  1. ImportWorker가 Ledger.process_import/1을 호출한다. 적재 행을 FOR UPDATE로 읽고 이미 종료된 경우 결과를 재사용한다.
  2. 짧은 트랜잭션으로 processing을 기록·방송한 뒤 파일 존재를 확인한다.
  3. 적재 행을 다시 잠근 트랜잭션에서 CsvParser가 전표 단위로 검증하고 Importer가 전표 1,000건, 행 1,000건씩 insert_all한다.
  4. 성공이면 집계·succeeded·감사 이벤트를 같은 트랜잭션에 저장한다. 검증 실패면 넣었던 전표·행을 모두 롤백한 뒤 별도 트랜잭션으로 failed와 분류된 이벤트를 기록한다.
  5. 커밋 뒤 결과를 방송한다. 성공 job은 :ok, 실패 적재는 cancel, 재시도 가능한 오류는 error를 반환한다.
  6. 마지막 시도의 예외·timeout은 Tickmark.Jobs.FailureHandler.handle_event/4가 Ledger.mark_import_failed/2로 정리하며 종료된 적재는 다시 바꾸지 않는다.

FN-SYS-02 룰 실행과 finding 집계

항목 정의
ID FN-SYS-02
이름 룰 실행과 finding 집계
화면과 경로 SYS 라우트 없음; 결과 S2·S4·S5
행위자와 권한 system testing.rule_worker; 사용자 권한은 시작 시 :start_test_run
선행 조건 rules 큐의 실행·룰별 job
입력과 검증·오류 문구 args test_run_id·rule_id; test_run_rules에 저장한 기본 params; 실제 룰은 아래 표
출력·flash·PubSub·감사 이벤트 rule_results, 마지막 종료 시 findings; rule_progress, test_run_updated, run_completed; 실패 시 test_run_rule.failed, 종료 test_run.completed, audit_appended
예외와 오류 처리 succeeded·skipped·failed는 재처리 noop; 최대 3회·5분 timeout; 최종 실패도 실행을 종료할 수 있게 기록; raw error는 화면 비노출
관련 REQ REQ-RUN-02, REQ-RUN-03, REQ-RUN-04, REQ-RUN-05, REQ-RUN-06, REQ-RUN-07, REQ-RUN-08, REQ-RUN-09, REQ-RUN-10, REQ-RUN-11, REQ-RUN-12, REQ-GL-06
구현 상태 구현
근거 Testing.RuleWorker.perform/1, Testing.run_rule/2, Testing.fail_rule/3, Tickmark.Testing.Rules.all/0; test/tickmark/testing/runner_test.exs, rule_worker_test.exs, runner_label_test.exs, rules_label_test.exs

처리 흐름:

  1. Testing.run_rule/2가 레지스트리와 실행을 읽고 미종료 룰을 running으로 표시한다. 첫 started_at은 재시도에도 유지한다.
  2. 룰 쿼리는 긴 잠금을 피하도록 트랜잭션 밖에서 수행한다. 저장된 params와 해당 import만 대상으로 사용한다.
  3. 종료 시 run 행을 FOR UPDATE로 먼저 잠그고 아직 pending인 룰만 변경한 뒤 결과를 1,000행씩 저장한다. skipped는 이유를, failed는 제한된 오류를 남긴다.
  4. 미종료 룰이 0이고 run이 running이면 전표별 가중치 합을 상한 100으로 집계해 finding을 한 번 생성한다.
  5. 실패 룰이 있으면 completed_with_errors, 없으면 completed와 test_run.completed를 기록한다. 룰 성공·미실행 자체의 별도 audit action은 없다.
  6. 커밋 뒤 룰 진행·실행 상태·최종 완료·감사 이벤트 순으로 방송한다. 마지막 예외는 FailureHandler가 fail_rule/3으로 멱등 처리한다.

룰 8종의 기본값과 판정

apply_sources와 임계값은 각 모듈의 default_params/0에서 스냅샷한다. 사용자 편집 UI는 없다. 아래 run/1 모듈은 모두 Tickmark.Testing.Rules 아래이며, 동일 이름의 snake_case 테스트가 test/tickmark/testing/rules/에 있다.

rule_id 모듈 기본 판정 가중치
weekend_holiday_posting WeekendHolidayPosting.run/1 manual, 토·일 또는 2025년 holiday_table 및 extra_holidays 15
off_hours_entry OffHoursEntry.run/1 manual, KST 입력시각 08:00 미만 또는 20:00 이상 10
round_amount RoundAmount.run/1 manual, 차변 합계 10,000,000원 이상·1,000,000원 배수 10
period_end_manual PeriodEndManual.run/1 manual, 결산일 포함 직전 3일 또는 기간 13 20
approval_control_gap ApprovalControlGap.run/1 manual, 1,000,000원 이상, 승인자 없음 또는 입력자와 동일 25
backdated_posting BackdatedPosting.run/1 manual/system, 전기일-증빙일 30일 이상 또는 0일 미만 15
rare_account_pair RareAccountPair.run/1 manual/system, 전체 모집단 500건 이상·해당 계정쌍 포함 전표 2건 이하; 500 미만 skipped 20
duplicate_entry DuplicateEntry.run/1 manual/system, 거래처 있음·1,000,000원 이상·동일 거래처/금액/차대 계정 집합·7일 이내; 후속 건만 15

중복의 선후 순서는 posting_date, entered_at, entry_number다. 룰 점수는 AI 위험도 및 사람 결론과 다른 값이다. 기본 검토 큐 기준은 Tickmark.Testing.TestRun의 25이며 각 실행에 저장한다. 근거: 도메인 2-3장, lib/tickmark/testing/rules.ex, lib/tickmark/testing/rules/*.ex, lib/tickmark/testing/test_run.ex.

FN-SYS-03 AI 트리아지 잡 실행

항목 정의
ID FN-SYS-03
이름 AI 트리아지 잡 실행
화면과 경로 SYS 라우트 없음; 결과 S4·S5
행위자와 권한 성공 agent triage:<response_model>; 실패 system triage.triage_worker
선행 조건 llm 큐, queued finding
입력과 검증·오류 문구 args finding_id; Triage.build_input/2의 원본·룰·중요성 맥락; 모델·프롬프트·입력 해시 서버 결정
출력·flash·PubSub·감사 이벤트 불변 ai_triages·succeeded; ai_triage.completed 또는 ai_triage.failed, finding_updated, audit_appended; finding.status는 불변
예외와 오류 처리 최대 5회·3분 timeout; 429 snooze 최대 10회, 기본 30초; 일시 오류 재시도, fatal 즉시 cancel; AI 없이 사람 결론 가능
관련 REQ REQ-AI-03, REQ-AI-04, REQ-AI-06
구현 상태 구현; 실 Claude 호출·전체 UI 흐름 확인 필요
근거 Triage.TriageWorker.perform/1, handle_result/2, Triage.triage_finding/1, mark_failed/2, AI.input_hash/1; test/tickmark/triage_test.exs, test/tickmark/triage/triage_worker_test.exs, test/tickmark/ai/anthropic_test.exs

처리 흐름:

  1. Triage.triage_finding/1이 queued finding과 원본·룰을 읽고 build_input/2로 JSON 입력을 만든다. 적요·거래처는 untrusted_text, 금액은 원 단위 문자열이다.
  2. finding_id·prompt_version·client.model()·input_hash가 같은 저장 결과를 찾는다. 있으면 API를 부르지 않고 재사용한다.
  3. 없으면 트랜잭션 밖에서 현재 AI.client/0의 triage/2를 호출한다. 키 없는 환경과 테스트는 Fake를 쓰며 실제 client는 출력 스키마와 금지 단정을 검증한다.
  4. finding을 FOR UPDATE로 잠가 아직 queued인지 재검사하고 결과 행·succeeded·ai_triage.completed를 한 Multi에 저장한다. 결과 unique 충돌은 저장 행을 다시 읽어 재사용한다.
  5. 성공 뒤 방송한다. 이미 완료된 finding은 noop이다. 재사용 성공도 completed 이벤트에 reused=true를 남긴다.
  6. 오류는 워커가 분류한다. 5xx·전송 오류·max_tokens는 재시도, 429는 snooze, invalid_output·refusal 등 fatal은 Triage.mark_failed/2 후 cancel이다.
  7. 마지막 예외는 FailureHandler가 worker_crashed로 정리한다. 오류 원문 대신 rate_limited·transient·invalid_output·refusal·not_configured·request_rejected·worker_crashed 분류만 감사 payload에 남긴다.

FN-SYS-04 감사 이벤트 append

항목 정의
ID FN-SYS-04
이름 감사 이벤트 append
화면과 경로 SYS 라우트 없음; 표시 S6
행위자와 권한 호출 context의 user·agent·system; 직접 사용자 입력 API 아님
선행 조건 업무 상태 변경 또는 멤버의 검토 거부 시도
입력과 검증·오류 문구 actor 튜플, engagement_id, action, subject_type·subject_id, 문자열 키 payload; action은 소문자/밑줄 두 부분을 점으로 연결
출력·flash·PubSub·감사 이벤트 DB가 seq 부여; 업무별 실제 action은 14장; audit_appended는 커밋 후 별도 호출
예외와 오류 처리 changeset 오류는 업무 트랜잭션 롤백; UPDATE·DELETE·TRUNCATE DB 거부; 비멤버의 review 거부는 기록 없음
관련 REQ REQ-AUD-01, REQ-REV-06
구현 상태 구현
근거 AuditTrail.actor/1, append/3, record/1, Tickmark.AuditTrail.AuditEvent.changeset/1; test/tickmark/audit_trail_test.exs, test/tickmark/audit_trail_truncate_test.exs

처리 흐름:

  1. 호출 context가 scope 또는 고정 작업 식별자로 actor와 업무별 payload를 구성한다. 사용자가 action·actor를 지정하지 않는다.
  2. AuditTrail.append/3은 기존 Multi에 insert를 추가한다. 별도 거부 기록 등은 AuditTrail.record/1을 호출한다.
  3. changeset이 필수값·action 형식·payload 키를 검사하고 DB가 seq와 시각을 저장한다. 성공한 업무와 감사 기록은 함께 커밋된다.
  4. 호출자가 커밋 뒤 AuditTrail.broadcast_appended/1을 호출한다. seq는 전역 정렬값이며 트랜잭션 취소 등으로 결번이 생길 수 있다.

FN-SYS-05 업무 변경 PubSub 방송과 화면 재조회

항목 정의
ID FN-SYS-05
이름 업무 변경 PubSub 방송과 화면 재조회
화면과 경로 SYS 라우트 없음; 수신 S2-S6
행위자와 권한 송신은 내부 context, 구독은 preparer/reviewer의 :view
선행 조건 관련 DB 트랜잭션 커밋 완료; 연결된 LiveView
입력과 검증·오류 문구 engagement 또는 test_run ID, 허용된 작은 tuple/map 메시지; 전체 원장·시크릿은 payload에 넣지 않음
출력·flash·PubSub·감사 이벤트 import_updated·test_run_updated·finding_updated·triage_requested·audit_appended 및 rule_progress·run_completed; 방송 자체 감사 없음
예외와 오류 처리 비멤버 구독 forbidden; 다른 항목 메시지 무시; S1은 미구독; PubSub를 영속 이벤트 저장소로 간주하지 않음
관련 REQ REQ-ACC-06, REQ-AUD-04, REQ-GL-07, REQ-RUN-03, REQ-REV-07
구현 상태 구현; S4 AI 열·S2 승인 통계 반영에는 별도 제한
근거 계약 6장; Engagements.subscribe/2, broadcast/2, Testing.subscribe_test_run/2, AuditTrail.broadcast_appended/1; authorization·runner·audit 및 각 LiveView 테스트

처리 흐름:

  1. 연결된 화면은 Engagements.subscribe/2 또는 Testing.subscribe_test_run/2로 권한 검사 후 토픽을 구독한다.
  2. 변경 context가 트랜잭션 커밋 뒤 engagement:{id} 또는 test_run:{id}로 식별자·상태 중심 메시지를 방송한다.
  3. 화면은 메시지 자체를 전체 데이터로 신뢰하지 않고 scope가 있는 읽기 함수로 최신 값을 조회한다. S6는 필터에 맞는 새 seq 수만 세고 클릭 때 조회한다.
  4. 재진입 mount는 DB를 다시 읽는다. 이 방송으로 추가 audit action을 만들지 않으며 인증 세션 disconnect는 별도 UserAuth 흐름이다.

FN-SYS-06 동시 열람자 Presence 추적

항목 정의
ID FN-SYS-06
이름 동시 열람자 Presence 추적
화면과 경로 SYS; 표시 S2-S6, S5는 같은 전표 머리
행위자와 권한 목표: preparer/reviewer의 :view 확인 후 서버 track
선행 조건 Presence 프로세스 실행; 연결된 LiveView
입력과 검증·오류 문구 user_id key; 목표 meta=email·role·view, S5 view=finding:{id}
출력·flash·PubSub·감사 이벤트 목표 engagement:{id}:presence의 presence_diff, 사람별 아바타; 감사 없음
예외와 오류 처리 비멤버는 track 전에 차단; 여러 탭은 사용자 하나로 합침; 기준 코드에는 화면 track·diff handler·아바타 연결 없음
관련 REQ REQ-UI-03
구현 상태 부분 (Presence child와 topic 구현, 화면 미구현)
근거 화면 1장·3.1·S5.4; lib/tickmark/application.ex:16, TickmarkWeb.Presence.topic/1; WBS 4.3-4.4, 전용 화면 테스트 없음

처리 흐름:

  1. 현재 구현은 애플리케이션이 TickmarkWeb.Presence를 시작하고 topic/1으로 토픽 문자열을 제공한다.
  2. 목표: 연결된 mount에서 멤버를 확인하고 해당 토픽에 user_id와 email·role·view를 track한다.
  3. 목표: presence_diff 수신 시 사용자 단위로 합쳐 표시하며 S5는 같은 finding view만 남긴다. 공통 바는 최대 4개 뒤 +N을 표시한다.
  4. 이 처리에는 DB 트랜잭션·Oban 잡·감사 이벤트를 추가하지 않는다. track와 수명 종료 동작은 화면 통합 시 검증한다.

12. 미결 사항과 불일치

세부 요구사항 및 기존 문서와 코드의 전체 불일치는 요구사항 명세서 7-8장을 따른다. 이 문서는 기존 코드·문서를 변경하지 않았다.

항목 문서 근거 코드 근거 영향 / 확인 필요
S3 실행 이동 화면 S3.4-S3.6 직접 테스트 실행 ImportLive.Components의 S2 링크 FN-S3-05는 현재 이동 방식으로 정의
S3 검토자 화면 S3.3 S2 이동 제안 ImportLive.New는 권한 없음 패널 직접 URL의 결과가 다름
CSV 기간·오류 문구 화면 S3.5 13기는 12월일 때 예외, invalid_number 확인 필요 CsvParser는 13기 전부 경고 제외; 정수 필드에도 금액 오류 문구 파서 동작은 확인, 사용자 문구 정합 필요
결론·결정 호출 화면 S5.4는 decide(scope, finding, decision, comment) 예시 Review.decide(scope, finding, conclusion_id, attrs) 최신 계약 8, 12.3과 코드가 기준; 과거 호출 예시 사용 금지
S5 제출·검토 UX 화면 S5.2 재작성 버튼, 확인·사유 폼 현재 validate만 연결, 모든 쓰기 버튼 disabled W6 병합 뒤 실제 문구·표시 ID 전달·확인 해제 검증 필요
S4 AI·판단·페이지 화면 S4의 활성 요청·배지·더 보기 현재 자리표시와 전체 finding 조회 AI 연결은 WBS 3.4, 더 보기는 C-08 확인 필요
큐·Presence 화면 S5 및 공통 아바타 Presence 모듈만 있고 S5 큐·다음 이동 없음 FN-S5-08, FN-SYS-06은 완료 아님
최신 구현을 반영하지 않은 C-09-C-13 화면 부록 C Triage의 below_threshold, Review의 stale·role 거부 기록, AuditTrail 필터·payload 구현 해결된 백엔드와 아직 미완료인 화면을 분리
AI 실패 actor 화면 S6 agent 실패 예시 Triage.mark_failed/2는 system triage.triage_worker 감사 이벤트표는 코드 actor 사용
모든 행위의 기록 MVP 핵심 시나리오·AGENTS.md 인증·조회·폼 검증·Presence 및 일부 중간 상태에는 audit 없음 14장의 실제 15개 action만 매핑
공통 셸과 한국어 화면 공통·부록 A, 계약 12.4 기본 셸·영어 인증 문구·phx:theme, 일부 ISO title F1/F2 통합 뒤 다시 대조
운영·동시성 검증 WBS 3.4, 4.4, 5.1-5.4 Fake·Req.Test, lock-first 코드와 단위 테스트 실 Claude, Review 두 연결 경합, 데모·메일·파일 영속성 확인 필요

13. 문서 검증 범위

요구사항 57개·비기능 요구사항 17개·기능 42개의 ID 및 상호 참조를 정적 검사했다. 문서 전용 작업 지시에 따라 mix 명령은 실행하지 않았으며 기존 테스트 성공을 재확인했다고 주장하지 않는다.

검사 실제 결과
em dash grep 두 파일 모두 출력 없음, 종료 코드 1 (일치 없음)
시스템 절대 경로 패턴 grep 두 파일 모두 출력 없음, 종료 코드 1 (일치 없음)
스마트 따옴표 검사 없음
상대 링크 대상 ls 기존 문서와 두 산출물 모두 존재; D2 API 규격서만 동시 작성 예정으로 미존재
ID와 추적표 REQ 57개·NFR 17개가 추적표에 각각 한 번 대응; FN 42개 중복 없음; 문서 간 참조 유효
표와 기능 흐름 Markdown 표 열 수 일치; 모든 FN에 3-7단계 처리 흐름
코드·테스트 경로 축약·패턴 경로를 제외한 정확한 경로는 요구사항 62개, 기능 정의서 46개 모두 존재

D2 예정 링크의 최종 존재 확인과 W6/F1/F2 통합 뒤 상태 갱신은 메인이 수행한다.

14. 기능 x 감사 이벤트

아래 action은 기준 코드에 존재하는 이름만 사용한다. S4/S5의 쓰기 화면은 미연결이어도 context 호출 시 생기는 action이므로 화면 상태를 별도로 표시했다. 요청·조회·클라이언트 표시를 모두 감사 기록으로 확장하지 않는다.

기능 audit action actor_type / actor_id subject와 조건
FN-S1-02 engagement.created user / scope 사용자 ID engagement; 대상·작성자 멤버와 함께 저장
FN-S2-02 engagement.member_added user / scope 사용자 ID engagement; 멤버 추가 성공
FN-S3-01, FN-S3-03 ledger_import.created user / scope 사용자 ID ledger_import; 출처·파일명·크기·해시
FN-SYS-01 ledger_import.succeeded system / ledger.import_worker ledger_import; 전표·행·차대 합계·경고 수
FN-SYS-01 ledger_import.failed system / ledger.import_worker ledger_import; validation_failed·file_missing·worker_failed와 오류 수
FN-S2-03 test_run.started user / scope 사용자 ID test_run; 적재 ID·룰 ID 목록·기준점수
FN-SYS-02 test_run_rule.failed system / testing.rule_worker test_run; 실패 rule_id만, raw error 없음
FN-SYS-02 test_run.completed system / testing.rule_worker test_run; status·finding_count
FN-S4-03 ai_triage.requested user / scope 사용자 ID test_run; enqueued>0인 일괄 요청 한 건, 0건은 이벤트 없음
FN-S5-05 ai_triage.requested user / scope 사용자 ID finding; entry_number 포함
FN-SYS-03 ai_triage.completed agent / triage:<response_model> finding; ai_triage_id·risk_level·reused·entry_number
FN-SYS-03 ai_triage.failed system / triage.triage_worker finding; 분류된 reason·entry_number
FN-S5-02 conclusion.submitted user / preparer ID finding; conclusion_id·outcome·ai_triage_id·entry_number
FN-S5-03 review.approved user / reviewer ID finding; conclusion_id·review_decision_id·entry_number
FN-S5-04 review.rejected user / reviewer ID finding; conclusion_id·review_decision_id·comment·entry_number
FN-S5-03, FN-S5-04 review.denied user / 요청한 멤버 ID finding; role 또는 self_review, attempted, entry_number; 비멤버는 없음
FN-SYS-04 위 업무별 action을 append 호출자가 구성한 actor 이 함수 자체를 뜻하는 새 action은 없음
FN-AUTH-01-07 없음 해당 없음 계정·토큰 변경 및 인증 세션 방송에는 AuditTrail 호출 없음
FN-S1-01, FN-S1-03, FN-S1-04, FN-S2-01 없음 해당 없음 조회·이동·테마 선택
FN-S3-02, FN-S3-04, FN-S3-05 없음 해당 없음 선택 취소·탭 변경·결과 확인; 잡 결과 이벤트는 FN-SYS-01
FN-S4-01, FN-S4-02, FN-S4-04 없음 해당 없음 진행·필터·상세 이동
FN-S5-01, FN-S5-06, FN-S5-07, FN-S5-08 없음 해당 없음 상세·제안·이력·다음 이동
FN-S6-01-05 없음 해당 없음 감사 로그의 조회·필터·더 보기·갱신·payload 펼침
FN-SYS-05, FN-SYS-06 없음 해당 없음 업무 알림·Presence 자체의 감사 action 없음

근거: lib/tickmark/{engagements,ledger,triage,review,audit_trail}.ex, lib/tickmark/testing/runner.ex. review.approved, review.rejected는 Review의 decision enum을 붙여 만드는 실제 문자열이다.

15. 기능 x 권한

권한표는 허용 규칙을 나타내며 UI 연결 완료 여부는 각 기능의 상태를 따른다. S1·AUTH는 감사 대상 역할과 무관하다. 생성자는 별도 역할이 아니고 :manage_members의 추가 조건이다. 시스템 기능은 사용자가 직접 호출할 라우트가 없으며, 등록 시 원래 사용자 기능의 권한 검사를 거친다.

기능 preparer reviewer 비멤버·비로그인 추가 조건 / 권한
FN-AUTH-01 역할 무관 역할 무관 비로그인 허용 신규 계정; 로그인 상태면 리다이렉트
FN-AUTH-02 역할 무관 역할 무관 허용 로그인 링크 요청; 계정 존재 비노출
FN-AUTH-03 역할 무관 역할 무관 유효 토큰이면 허용 최초 확인 또는 로그인
FN-AUTH-04 역할 무관 역할 무관 유효 자격이면 허용 설정된 비밀번호
FN-AUTH-05 허용 허용 세션 없으면 정리 후 이동 본인 세션
FN-AUTH-06 허용 허용 로그인 비멤버도 허용 계정 본인, sudo_mode
FN-AUTH-07 허용 허용 로그인 비멤버도 허용 계정 본인, sudo_mode
FN-S1-01 허용 허용 로그인한 비멤버 허용 본인 멤버 대상만 목록
FN-S1-02 허용 허용 로그인한 비멤버 허용 새 대상에서는 preparer가 됨
FN-S1-03 허용 허용 공개 홈 허용 대상별 링크는 :view 검사
FN-S1-04 허용 허용 허용 브라우저 설정
FN-S2-01 허용 허용 거부 :view
FN-S2-02 생성자인 멤버만 생성자인 멤버만 거부 :manage_members; 기본 생성자는 preparer
FN-S2-03 허용 거부 거부 :start_test_run; 성공한 같은 대상 적재
FN-S3-01 허용 거부 거부 :import_ledger
FN-S3-02 허용 폼 없음 거부 업로드 선택 참조; DB 변경 없음
FN-S3-03 허용 거부 거부 :import_ledger
FN-S3-04 허용 거부 패널 거부 탭은 preparer 폼 안에서 제공
FN-S3-05 허용 S2 이력에서 허용 거부 :view; S3 자체는 preparer 결과 패널
FN-S4-01 허용 허용 거부 :view
FN-S4-02 허용 허용 거부 :view
FN-S4-03 허용 거부 거부 :request_triage; UI 연결 대기
FN-S4-04 허용 허용 거부 :view
FN-S5-01 허용 허용 거부 :view
FN-S5-02 허용 거부 거부 :submit_conclusion; approved 제외
FN-S5-03 거부 허용 거부 :review_conclusion; 최신 concluded·본인 결론 제외
FN-S5-04 거부 허용 거부 승인 조건과 동일, 반려 사유 필수
FN-S5-05 허용 거부 거부 :request_triage; 기준 이상·queued 제외
FN-S5-06 허용 허용 거부 :view
FN-S5-07 허용 허용 거부 :view
FN-S5-08 목표 허용 목표 허용 거부 :view; 미구현
FN-S6-01 허용 허용 거부 :view
FN-S6-02 허용 허용 거부 :view
FN-S6-03 허용 허용 거부 :view
FN-S6-04 허용 허용 거부 :view
FN-S6-05 허용 허용 거부 :view, 대상 진입 재검사
FN-SYS-01 직접 호출 없음 직접 호출 없음 직접 호출 없음 적재 요청 context가 등록한 job
FN-SYS-02 직접 호출 없음 직접 호출 없음 직접 호출 없음 테스트 시작 context가 등록한 job
FN-SYS-03 직접 호출 없음 직접 호출 없음 직접 호출 없음 트리아지 요청 context가 등록한 job
FN-SYS-04 직접 호출 없음 직접 호출 없음 직접 호출 없음 내부 context; 거부 이벤트도 멤버 확인
FN-SYS-05 구독 허용 구독 허용 거부 :view; 방송은 내부 context
FN-SYS-06 목표 추적 허용 목표 추적 허용 거부 :view; 화면 track 미구현

근거: Tickmark.Engagements.Policy.allowed?/2, Engagements.authorize/3, Review.decide/4, lib/tickmark_web/router.ex; test/tickmark/engagements/policy_test.exs, authorization_test.exs, test/tickmark/review_test.exs.

Changelog

  • 2026-10-20: 초안 작성 (D1)
  • 2026-10-20: 메인 병합 (60781a3). 병렬 작성 문서 (API 연동 규격서) 도 병합됨 (164788e). 기준 커밋 이후 코드 변경 (홈 리다이렉트, 한글 문구, S5 결론과 결정, AI 제안 요청, 문서 위키) 은 API 연동 규격서 14장 을 본다. FN 과 IF 교차 링크는 아직 없다