디자인 시스템
1. 디자인 원칙
대상 사용자는 밀도 높은 데이터(전표, 룰 적중, AI 제안, 결론, 감사 로그)를 오래 보는 회계사/감사인이다. 톤은 차분하고 신뢰감 있게, 장식 없이 정보 중심으로 한다.
| # |
원칙 |
구체적 의미 |
| 1 |
밀도 우선 |
한 화면에 전표 행 15개 이상. 본문 14px, 컴팩트 테이블, 그림자 대신 1px 경계. 여백은 구분에만 쓴다. |
| 2 |
추적 가능성 |
모든 수치와 판단은 근거(전표, 룰, 로그)로 한 번에 이동한다. ID는 항상 mono로 전체 표기하고 자르지 않는다. 시각은 hover로 UTC 원본을 확인한다. |
| 3 |
AI와 사람의 구분 |
AI 산출물은 항상 보라색(accent) 점선 박스, "AI 제안" 라벨, 모델 ID를 갖는다. 사람 입력은 실선 박스다. AI가 사람의 입력란을 미리 채우지 않는다. |
| 4 |
상태 명료성 |
위험도, 작성자 판단, 검토 상태는 서로 다른 배지 모양(soft, outline, solid)과 아이콘을 쓴다. 모든 상태는 텍스트 라벨을 가진다. |
| 5 |
되돌릴 수 없는 행위는 분명하게 |
승인과 반려는 하단 sign-off bar에서만 한다. 직무분리로 막힌 이유는 비활성 버튼 옆에 텍스트로 보인다. 감사 로그에는 수정/삭제 UI가 없다. |
| 6 |
접근성은 기본값 |
색만으로 구분하지 않는다. 텍스트 대비 AA 이상. 키보드만으로 검토를 끝낼 수 있다. 라이트/다크가 같은 정보를 보여준다. |
2. 기반
2.1 생성된 설정 (assets/css/app.css, 확인한 현재 상태)
| 항목 |
현재 값 |
| Tailwind |
v4. @import "tailwindcss" source(none) 후 @source로 css, js, lib/tickmark_web, colocated를 명시. tailwind.config.js 없음. mix 의존성 tailwind로 빌드(npm 없음). |
| 아이콘 |
@plugin "../vendor/heroicons". hero-* 클래스 (Heroicons v2.2.0, 접미사 -solid, -mini, -micro). <.icon name="hero-..." /> |
| daisyUI |
@plugin ".../daisyui" { themes: false }. 내장 테마는 끄고 daisyui-theme 플러그인으로 테마 2개만 정의. mix 의존성으로 받음. |
| 테마 |
light(default, primary Phoenix 주황 oklch(70% 0.213 47.6), accent 검정), dark(prefersdark, primary 남보라 oklch(58% 0.233 277)) |
| 토큰 |
테마 블록 안의 --color-base-100/200/300, base-content, primary, secondary, accent, neutral, info, success, warning, error (+ -content), --radius-selector/field/box, --size-selector/field, --border(1.5px), --depth(1), --noise(0) |
| 테마 전환 |
<html data-theme>를 root.html.heex의 인라인 스크립트가 localStorage phx:theme(light/dark/system)로 결정. @custom-variant dark가 [data-theme=dark]에 묶여 있다. |
| 기타 |
phx-click-loading, phx-submit-loading, phx-change-loading variant. LiveView 래퍼 display: contents. |
| 컴포넌트 |
core_components: button(primary/soft), input, header, table(table-zebra), list, flash, icon. layouts.app은 Phoenix 데모 헤더와 max-w-2xl 본문(교체 대상). |
2.2 결정: daisyUI를 프리미티브로 쓴다 (메인 세션 검토 후 채택)
refs/phoenix-guidelines.md는 "daisyUI 대신 직접 작성"을 권한다. 우리는 프리미티브(btn, badge, input, select, textarea, table, progress, avatar, tabs, alert, skeleton, loading, tooltip)만 daisyUI를 쓰고, 도메인 컴포넌트는 Tailwind 유틸리티로 조합한다.
- 이유: (1) 생성된 core_components가 이미 daisyUI에 의존한다. (2) 색 역할 토큰으로 라이트/다크가 자동 전환된다. (3) 1인 7일 일정.
- 트레이드오프: 기본 모양이 흔하다. 색, 반경, 깊이, 밀도 규칙(3장)으로 차별화한다.
- 지킬 규칙:
@apply 금지, 외부 폰트/스크립트 링크 금지(app.css, app.js 번들만), 색은 역할 클래스(bg-base-100, text-error)로 쓰고 dark: 변형은 예외에만 쓴다.
2.3 추가하는 것 (메인 세션이 app.css에 반영, 요청 사항)
- 두 테마 블록의 값 교체 (구조 유지). 색 값은 hex다. 빌드에서 그대로 통과하는지 확인 필요, 실패하면 oklch로 변환한다.
- 폰트 스택
@theme (시스템 폰트, 새 의존성 없음).
- 추가 토큰 2개
muted(보조 텍스트), line-strong(입력 경계). @theme inline으로 text-muted, border-line-strong 유틸리티를 만든다.
- 전역 규칙 3개: focus-visible 링, 테이블 밀도
.tk-table, 입력 경계 대비.
@theme {
--font-sans: system-ui, -apple-system, "Segoe UI", "Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans KR", sans-serif;
--font-mono: ui-monospace, "SF Mono", Menlo, "Cascadia Mono", Consolas, monospace;
}
@theme inline { --color-muted: var(--tk-muted); --color-line-strong: var(--tk-line-strong); }
:root { --tk-muted: #556070; --tk-line-strong: #7C8796; }
:root[data-theme="dark"] { --tk-muted: #9AA6B6; --tk-line-strong: #6F7B8D; }
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
outline: 2px solid var(--color-primary); outline-offset: 2px;
}
.tk-table :where(th, td) { padding: 0.375rem 0.75rem; }
.tk-table :where(th) { font-size: 0.75rem; font-weight: 600; color: var(--tk-muted); }
/* daisyUI 기본 입력 경계는 base-content 20%(약 1.5:1). 3:1 이상으로 올린다. 포커스/에러 상태 유지는 빌드 후 확인 필요 */
.input:not(.input-error, :focus-within), .select:not(.select-error, :focus-within),
.textarea:not(.textarea-error, :focus-within) { --input-color: var(--tk-line-strong); }
@plugin "daisyui/packages/bundle/daisyui-theme" {
name: "light"; default: true; prefersdark: false; color-scheme: "light";
--color-base-100: #FFFFFF; --color-base-200: #F3F5F8; --color-base-300: #DDE2E9; --color-base-content: #18202B;
--color-primary: #1F5AA6; --color-primary-content: #FFFFFF; --color-secondary: #475467; --color-secondary-content: #FFFFFF;
--color-accent: #6B3FC0; --color-accent-content: #FFFFFF; --color-neutral: #475467; --color-neutral-content: #FFFFFF;
--color-info: #1F5AA6; --color-info-content: #FFFFFF; --color-success: #116D40; --color-success-content: #FFFFFF;
--color-warning: #8A5000; --color-warning-content: #FFFFFF; --color-error: #B42318; --color-error-content: #FFFFFF;
--radius-selector: 0.25rem; --radius-field: 0.25rem; --radius-box: 0.375rem;
--size-selector: 0.2rem; --size-field: 0.2rem; --border: 1px; --depth: 0; --noise: 0;
}
@plugin "daisyui/packages/bundle/daisyui-theme" {
name: "dark"; default: false; prefersdark: true; color-scheme: "dark";
--color-base-100: #161D27; --color-base-200: #0E131A; --color-base-300: #2B3544; --color-base-content: #E5E9EF;
--color-primary: #7AA7F2; --color-primary-content: #0B1426; --color-secondary: #A9B4C3; --color-secondary-content: #0E131A;
--color-accent: #B79CF5; --color-accent-content: #1B0E3A; --color-neutral: #A9B4C3; --color-neutral-content: #0E131A;
--color-info: #7AA7F2; --color-info-content: #0B1426; --color-success: #5CC38A; --color-success-content: #06210F;
--color-warning: #EDB257; --color-warning-content: #2A1A00; --color-error: #F28B82; --color-error-content: #2B0907;
--radius-selector: 0.25rem; --radius-field: 0.25rem; --radius-box: 0.375rem;
--size-selector: 0.2rem; --size-field: 0.2rem; --border: 1px; --depth: 0; --noise: 0;
}
- root.html.heex:
<html lang="ko">, <body class="bg-base-200 text-base-content text-sm antialiased">, live_title 접미사를 " | Tickmark"로 교체.
- layouts.ex
app/1: Phoenix 데모 헤더를 Tickmark 셸(상단 바: 로고, 감사 대상, 감사 로그, presence, theme_toggle, 사용자 메뉴)로 교체하고 본문 폭을 mx-auto max-w-screen-2xl px-4 sm:px-6 lg:px-8로 넓힌다.
lib/tickmark_web.ex의 html_helpers에 4장 컴포넌트 모듈 import 추가.
3. 토큰
3.1 색 역할
daisyUI 토큰 이름 그대로 쓴다. 표의 클래스는 Tailwind 유틸리티다. 다크 모드는 표면이 밝을수록 위(elevation)다.
| 역할 |
토큰 / 클래스 |
Light |
Dark |
| 페이지 배경 |
base-200 / bg-base-200 |
#F3F5F8 |
#0E131A |
| 표면 (카드, 테이블, 입력) |
base-100 / bg-base-100 |
#FFFFFF |
#161D27 |
| 구분선, 행 hover, 비활성 |
base-300 / border-base-300 |
#DDE2E9 |
#2B3544 |
| 본문 텍스트 |
base-content |
#18202B |
#E5E9EF |
| 보조 텍스트 |
추가 muted / text-muted |
#556070 |
#9AA6B6 |
| 입력 경계 (UI 3:1) |
추가 line-strong / border-line-strong |
#7C8796 |
#6F7B8D |
| Primary (링크, 주 버튼, 포커스) |
primary + primary-content |
#1F5AA6 + #FFFFFF |
#7AA7F2 + #0B1426 |
| AI 표식 |
accent + accent-content |
#6B3FC0 + #FFFFFF |
#B79CF5 + #1B0E3A |
| Error |
error + error-content |
#B42318 + #FFFFFF |
#F28B82 + #2B0907 |
| Warning |
warning + warning-content |
#8A5000 + #FFFFFF |
#EDB257 + #2A1A00 |
| Success |
success + success-content |
#116D40 + #FFFFFF |
#5CC38A + #06210F |
| Neutral, Secondary |
neutral + neutral-content |
#475467 + #FFFFFF |
#A9B4C3 + #0E131A |
| Info |
= primary |
#1F5AA6 |
#7AA7F2 |
3.2 도메인 의미 매핑 (3계열 배지는 모양이 다르다)
| 의미 |
라벨 |
색 역할 |
배지 스타일 |
아이콘 (hero-) |
| 위험도 높음 |
높음 |
error |
badge-soft |
exclamation-triangle |
| 위험도 중간 |
중간 |
warning |
badge-soft |
exclamation-circle |
| 위험도 낮음 |
낮음 |
neutral |
badge-soft |
minus-circle |
AI 미요청 / 분석 중 / 실패 (triage_status none / queued / failed) |
-, 분석 중, AI 실패 |
neutral, accent, error |
텍스트, loading-dots + 텍스트, badge-ghost + 텍스트 |
-, -, exclamation-circle |
판단 이상 없음 (no_exception) |
이상 없음 |
success |
badge-outline |
check-circle |
판단 추가 확인 필요 (needs_follow_up) |
추가 확인 필요 |
warning |
badge-outline |
question-mark-circle |
판단 예외 (exception) |
예외 |
error |
badge-outline |
flag |
| 결론 없음 (최신 결론 없음) |
미판단 |
neutral |
badge-dash |
pencil-square |
검토 전 (status open) |
- (sr-only "검토 전") |
muted |
텍스트 |
- |
검토 대기 (status concluded) |
검토 대기 |
neutral |
solid (badge-neutral) |
clock |
승인 (status approved) |
승인 |
success |
solid |
check-badge |
반려 (status rejected) |
반려 |
error |
solid |
arrow-uturn-left |
| AI 생성 콘텐츠 |
AI 제안 |
accent |
badge-soft + 점선 박스 |
sparkles |
점수 구간 (findings.score) |
경미 0-24, 중 25-49, 상 50-74, 최상 75-100 |
base-content |
숫자 + 5칸 막대 + 구간 텍스트 (색 없음) |
- |
테스트 실행 (test_runs.status) |
실행 중, 완료, 일부 실패 |
primary, success, warning |
progress-* + 텍스트 |
arrow-path (실행 중) |
룰 실행 (test_run_rules.status) |
대기, 실행 중, 완료, 미실행, 실패 |
neutral, primary, success, neutral, error |
텍스트 + 아이콘 |
clock, arrow-path, check, minus-circle, x-circle |
GL 적재 (ledger_imports.status) |
대기, 적재 중, 완료, 실패 |
neutral, primary, success, error |
텍스트 + 아이콘 |
clock, arrow-path, check, x-circle |
- 위험도 낮음을 녹색으로 하지 않는 이유: "낮은 위험으로 플래그됨"은 "이상 없음"이 아니다. 녹색은 사람이 내린 결론(이상 없음, 승인)과 시스템 작업 완료에만 쓴다.
- 점수에 색을 쓰지 않는 이유: 점수는 룰 가중치 합(시스템 계산)이고, 위험도(AI 제안)와 같은 빨강/노랑을 쓰면 두 값이 같은 것으로 읽힌다. 점수는 크기(막대)와 구간 텍스트로만 보인다.
- "작성자 판단"과 "검토" 열은 따로 둔다. 판단은 최신 결론의
outcome, 검토는 findings.status에서 온다. 반려된 항목은 판단 배지(예: 예외)와 검토 배지(반려)가 함께 보인다.
3.3 대비 검증 (WCAG 2.x, sRGB 상대휘도로 직접 계산)
기준: 텍스트 4.5:1 이상, 비텍스트(경계, 포커스) 3:1 이상. "soft"는 daisyUI badge-soft의 배경(역할색 8%를 base-100과 oklab 혼합, 직접 계산한 근사값) 위 텍스트다. AI 박스는 5%를 쓰므로 계산한 8%보다 대비가 높다.
| 쌍 |
Light |
Dark |
기준 |
| 본문 on base-100 / base-200 |
16.40 / 15.02 |
13.91 / 15.30 |
4.5 |
| muted on base-100 / base-200 / base-300 |
6.38 / 5.84 / 4.90 |
6.86 / 7.55 / 5.02 |
4.5 |
| primary 텍스트 on base-100 / base-200 |
6.84 / 6.26 |
6.97 / 7.67 |
4.5 |
| primary-content on primary |
6.84 |
7.56 |
4.5 |
| error on base-100 / soft |
6.57 / 5.82 |
7.09 / 6.34 |
4.5 |
| warning on base-100 / soft |
6.51 / 5.76 |
8.96 / 7.84 |
4.5 |
| success on base-100 / soft |
6.39 / 5.68 |
7.77 / 6.91 |
4.5 |
| neutral on base-100 / soft |
7.69 / 6.74 |
8.07 / 7.16 |
4.5 |
| accent on base-100 / soft |
6.78 / 6.02 |
7.33 / 6.52 |
4.5 |
| *-content on solid: error, warning, success, neutral, accent |
6.57, 6.51, 6.39, 7.69, 6.78 |
7.68, 8.91, 7.81, 8.88, 7.76 |
4.5 |
| 본문 / muted on AI 박스 틴트 |
14.55 / 5.66 |
12.37 / 6.10 |
4.5 |
| line-strong vs base-100 / base-200 |
3.64 / 3.34 |
3.95 / 4.34 |
3.0 |
| 포커스 링(primary) vs base-100 / base-200 |
6.84 / 6.26 |
6.97 / 7.67 |
3.0 |
border-base-300 구분선은 base-100 대비 1.30(light), 1.37(dark)이다. 의도적으로 3:1 미만이며 정보를 전달하지 않는다(행 구분 보조용). 정보는 텍스트와 아이콘으로 전달한다.
- 입력 경계 기본값(base-content 20%)은 약 1.51(light), 1.78(dark)이라
line-strong으로 교체한다.
- 차트나 열 지도는 MVP에 없다. 필요해지면 이 표에 추가한다.
3.4 타이포그래피
폰트: 시스템 폰트(2.3의 --font-sans, --font-mono). Pretendard 같은 웹폰트를 쓰려면 woff2를 priv/static에 두고 app.css @font-face로 불러오는 방식이 가능하나 MVP에서는 쓰지 않는다.
| 역할 |
클래스 |
크기 / 행간 |
굵기 |
용도 |
| 페이지 제목 |
text-xl |
20 / 28 |
semibold |
화면 제목 |
| 섹션 제목 |
text-base |
16 / 24 |
semibold |
카드, 섹션 헤더 |
| 본문, 폼, 테이블 셀 |
text-sm |
14 / 20 |
normal |
기본값 |
| AI 근거 등 긴 문단 |
text-sm leading-6 |
14 / 24 |
normal |
한글 가독성 |
| 테이블 헤더 |
text-xs |
12 / 16 |
semibold, text-muted |
열 이름 |
| 메타, 캡션, 배지 |
text-xs |
12 / 16 |
normal, 배지는 medium |
보조 정보 |
| 요약 수치 (KPI) |
text-2xl tabular-nums |
24 / 32 |
semibold |
stat_row |
- 12px는 보조 정보에만 쓴다. 판단에 필요한 정보는 14px 이상.
- 금액: sans +
tabular-nums + 우측 정렬(text-right, 열 머리도 우측). 자릿수가 세로로 정렬된다.
- ID, 코드(entry_number, account_code, rule_code, 해시, 모델 ID):
font-mono slashed-zero whitespace-nowrap. 0과 O, 1과 l 혼동을 막고 자르지 않는다.
- 날짜, 시각, 건수:
tabular-nums.
3.5 간격, 반경, 깊이
| 항목 |
값 |
클래스 / 메커니즘 |
| 간격 기준 |
4px |
Tailwind --spacing 기본값 |
| 배지 아이콘-텍스트 / 컨트롤 간격 |
6px / 8px |
gap-1.5 / gap-2 |
| 카드 패딩 |
16px (밀집 12px) |
p-4 / p-3 |
| 섹션 간격 |
24px |
space-y-6 |
| 페이지 좌우 |
16 / 24 / 32px |
px-4 sm:px-6 lg:px-8 |
| 테이블 셀 |
12px x 6px (행 높이 약 33px) |
.tk-table |
| 컨트롤 높이 |
32px (btn-sm 25.6px) |
--size-field: 0.2rem (높이 = 값 x 10) |
| 반경 |
box 6px, field 4px, selector 4px, avatar 원형 |
--radius-box, --radius-field, --radius-selector |
| 경계 |
1px |
--border: 1px |
| 깊이 0 (기본) |
경계만, 그림자 없음 |
카드, 테이블, AI 박스. --depth: 0 |
| 깊이 1 |
shadow-sm + 경계 |
sticky 헤더, sign-off bar |
| 깊이 2 |
shadow-md |
드롭다운, 토스트(flash). 모달은 쓰지 않는다. |
| 모션 |
150ms 이하, motion-safe:만 |
스피너, 새 이벤트 하이라이트. 페이지 전환 효과 없음 |
다크 모드에서는 그림자 대신 표면 밝기 차(base-100이 base-200보다 밝음)로 위계를 표현한다.
4. 컴포넌트 목록
모듈은 두 개로 나눈다(각 300줄 이하 유지). 둘 다 use TickmarkWeb, :html, 함수 컴포넌트 + attr/slot 선언, 테스트는 Phoenix.LiveViewTest.rendered_to_string/1. 이 목록은 설계이며 map 키와 상태값은 설계 문서 3장(데이터 모델)의 컬럼 이름을 그대로 쓴다. 컴포넌트는 Ecto 스키마 struct를 직접 받지 않고 LiveView가 만든 작은 map을 받는다(컴포넌트 테스트가 DB 없이 돈다).
TickmarkWeb.DataComponents (data_components.ex): 데이터 표시 계열
TickmarkWeb.AuditComponents (audit_components.ex): 감사 도메인 계열
4.1 작은 표시 컴포넌트
| 컴포넌트 |
목적 |
attrs |
상태 |
daisyUI / Tailwind |
money/1 |
금액 |
value Decimal | integer (필수), zero :dash | :number (:number), class |
양수, 음수, 0, nil |
tabular-nums whitespace-nowrap, 부모 셀 text-right |
entry_ref/1 |
전표번호 + 행 참조 |
number string (필수), line integer | nil, navigate string | nil |
링크, 비링크 |
font-mono slashed-zero link link-hover, 행은 text-xs text-muted |
score_meter/1 |
전표 점수 (룰 가중치 합) |
score integer 0-100 (필수) |
경미 0-24, 중 25-49, 상 50-74, 최상 75-100 |
숫자 tabular-nums font-semibold w-7 text-right + 5칸 막대(채움 칸 수 = ceil(score / 20), 채움 bg-base-content, 빈칸 bg-base-300, aria-hidden) + 구간 text-xs text-muted. 색 역할을 쓰지 않는다(3.2) |
risk_badge/1 |
AI 위험도 |
level :high | :medium | :low | nil, triage_status :none | :queued | :succeeded | :failed (:succeeded), size :sm | :md (:sm) |
succeeded 3단계, none(-), queued(분석 중), failed(AI 실패) |
badge badge-soft badge-error | badge-warning | badge-neutral, queued loading loading-dots loading-xs text-accent + 텍스트, failed badge badge-ghost + text-error 아이콘, 아이콘 -micro |
conclusion_badge/1 |
작성자 판단 (최신 결론) |
outcome :no_exception | :needs_follow_up | :exception | nil (필수, nil = 미판단) |
4개 |
badge badge-outline badge-success | badge-warning | badge-error, 미판단 badge-dash badge-neutral |
review_badge/1 |
검토 상태 (findings.status) |
status :open | :concluded | :approved | :rejected (필수) |
open은 -, concluded 검토 대기, approved 승인, rejected 반려 |
badge badge-neutral | badge-success | badge-error (solid), open은 text-muted + sr-only "검토 전" |
ai_marker/1 |
AI 산출물 표식 |
model string | nil, prompt_version string | nil |
기본, 툴팁(모델, 프롬프트 버전) |
badge badge-soft badge-accent, tooltip, hero-sparkles-micro |
test_progress/1 |
테스트 실행 진행률 |
status :running | :completed | :completed_with_errors (필수), rules list of %{rule_id, name, status, hit_count} (필수, test_run_rules + 레지스트리 이름), done integer, total integer |
실행 중(최대 4개 룰이 동시에 running, Oban rules: 4), 완료, 일부 실패, 룰별 queued/running/succeeded/skipped/failed |
전체 progress progress-primary | progress-success | progress-warning(네이티브 <progress>) + "5 / 8 룰" tabular-nums. 룰은 grid grid-cols-2 lg:grid-cols-4 칸마다 상태 아이콘 + 이름 + 적중 수. 요약 텍스트만 aria-live="polite". 실패 룰의 오류 원문은 보이지 않고 "실행 실패, 로그 확인" 일반 문구 |
presence_avatars/1 |
같은 화면을 보는 사람 |
users list of %{id, name} (필수), current_user_id string, max integer (4) |
0명, max 이하, 초과(+N), 본인 표시 |
avatar-group -space-x-2, avatar avatar-placeholder, bg-neutral text-neutral-content w-7 rounded-full text-xs, 이니셜 + tooltip 이름, sr-only "지금 보는 사람: ..." |
filter_tabs/1 |
목록 필터 + 건수 |
tabs list of %{key, label, count} (필수), active string (필수), patch_base string |
활성, 비활성 |
tabs tabs-border, tab tab-active, 건수 badge badge-sm, <.link patch> + aria-current |
stat_row/1 |
요약 수치 줄 |
items list of %{label, value, hint} (필수) |
값 있음, 로딩(skeleton) |
grid grid-cols-2 lg:grid-cols-5 gap-3, 각 칸 rounded-box border border-base-300 bg-base-100 p-3, 값 text-2xl tabular-nums |
4.2 복합 컴포넌트
| 컴포넌트 |
목적 |
attrs / slots |
상태 |
daisyUI / Tailwind |
data_table/1 |
core table/1 확장. sticky 헤더, 우측 정렬 숫자 열, 말줄임 |
attrs: id (필수), rows list | stream (필수), row_id, row_click, density :compact | :normal (:compact), max_height string ("max-h-[32rem]"), selected_id. slot col (attrs label, align :left | :right | :center, numeric boolean, mono boolean). slots empty, footer |
로딩(skeleton 행), 비어 있음(empty), 행 hover, 선택 행(aria-current, bg-primary/10), 합계 행(footer, sticky 하단) |
래퍼 overflow-auto rounded-box border border-base-300 bg-base-100, table table-pin-rows tk-table, 행 row-hover, zebra 없음(상태 하이라이트와 충돌), <caption class="sr-only">, <th scope="col">. 100행 이상은 stream + "더 보기" |
entry_lines/1 |
전표 행 + 합계 + 차대 일치 |
attrs: lines list of %{line_number, account_code, account_name, description, debit, credit} (GL 계약 컬럼명), highlight_lines list of integer. 합계는 컴포넌트가 계산 |
일치(hero-check-circle + "차대 일치"), 불일치(text-error + 차이 금액, 정상 데이터에서는 발생하지 않음), 룰 대상 행 강조(좌측 border-l-4 border-warning + 아이콘) |
data_table 위에 구성. 차변/대변 numeric, 0은 "-" |
rule_hit_list/1 |
적중한 룰과 근거 |
attrs: hits list of %{rule_id, name, score, reason, details} (필수. rule_results의 rule_id, score, reason, details + 레지스트리 name/0) |
0건이면 렌더하지 않음, 근거(details) 접힘/펼침, 합계 줄 "점수 55 = 20 + 10 + 25" |
<ul class="divide-y divide-base-300">, rule_id font-mono text-xs text-muted, 이름 font-medium, 가중치 +20 tabular-nums, reason 본문, details는 <details> 안 key-value <dl>(키는 mono) |
ai_suggestion_card/1 |
AI 제안(위험도, 근거, 추가 절차, 부족한 정보) |
attrs: triage %{id, risk_level, rationale, suggested_procedures: [%{procedure, evidence_to_obtain}], missing_information: [string], model, response_model, prompt_version, input_hash, inserted_at} | nil, triage_status :none | :queued | :succeeded | :failed (필수), can_request boolean. 이벤트 phx-click="request_triage" |
none(설명 + "AI 제안 요청" 버튼, 작성자만), queued(skeleton + loading-dots + "AI가 분석 중"), succeeded, failed("AI 제안을 받지 못했습니다" + 다시 요청, 사람 판단은 계속 가능) |
박스 rounded-box border border-dashed border-accent bg-accent/5 p-4. 헤더 = ai_marker + risk_badge. 절차는 <ol> 항목마다 procedure(본문) + "확보할 증거: evidence_to_obtain"(text-muted). missing_information이 있으면 "AI가 판단에 부족하다고 본 정보" 목록. 고정 문구 "이 제안은 판단이 아닙니다. 결론은 작성자가 입력합니다." model, response_model(다르면 둘 다), prompt_version, input_hash 앞 8자, 생성 시각은 <details>로 접고 mono. 결론 입력란을 미리 선택하지 않는다. |
conclusion_form/1 |
작성자 판단 + 코멘트 (사람 입력) |
attrs: form Phoenix.HTML.Form (필수, 필드 outcome, comment, hidden ai_triage_id), author_name string, mode :edit | :read_only | :locked (필수, 서버 계산). 이벤트 phx-change="validate", phx-submit="submit_conclusion" |
edit(open, concluded, rejected인 항목의 작성자), read_only(검토자에게 최신 결론을 보여줌), locked(approved, hero-lock-closed + "승인된 항목은 수정할 수 없습니다"), 저장 중(phx-submit-loading:), 검증 오류(판단 미선택, 코멘트 1-2000자) |
박스 rounded-box border border-line-strong bg-base-100 p-4(실선), <fieldset> + <legend>, 라디오 radio radio-sm radio-primary + 옆에 conclusion_badge, textarea + 글자 수 text-xs tabular-nums, 저장 btn btn-primary btn-sm "결론 제출". 코멘트는 모든 판단에서 필수(도메인 5.2 T3). ai_triage_id는 화면에 보인 최신 AI 제안 id(없으면 빈 값)로 채워 "어떤 제안을 보고 판단했는지"를 남긴다. 반려된 항목은 상단에 반려 사유를 보이고 새 결론을 받는다(T7). Ctrl+Enter 제출 |
sign_off_bar/1 |
승인/반려 (검토자, 항목 단위) |
attrs: status :open | :concluded | :approved | :rejected (필수, findings.status), can_decide boolean (필수, 서버 계산), blocked_reason string | nil, last_decision %{decision, reviewer_name, comment, inserted_at} | nil. 이벤트 phx-click="approve", "reject" |
결정 가능(concluded + 검토자), 결론 전(open: "작성자 결론이 제출되지 않았습니다"), 작성자가 보는 화면("작성자는 승인, 반려할 수 없습니다 (직무분리)"), 이미 결정됨(approved/rejected: 읽기 전용 + review_badge + 처리자/시각, 반려는 사유) |
sticky bottom-0 z-20 border-t border-base-300 bg-base-100 shadow-sm. 승인 btn btn-success, 반려 btn btn-outline btn-error. 승인은 2단계(같은 줄에서 "승인하시겠습니까? [확인] [취소]"), 반려는 사유 입력 필수의 인라인 패널(모달 없음). 이유 텍스트는 aria-describedby로 버튼에 연결. 권한은 서버에서 다시 검증한다(UI는 안내용, {:error, :self_review}는 flash로 알림). |
finding_history/1 |
S5 이력 (AuditComponents) |
attrs: entries list of %{kind :conclusion | :decision, actor_name, outcome | decision, comment, inserted_at} (필수, 시간순) |
비어 있음("아직 결론이 없습니다."), 결론만, 반려 뒤 재제출, 승인으로 끝남 |
<ol> 세로 목록. 결론은 conclusion_badge, 결정은 review_badge + 처리자/시각, 반려 사유와 코멘트는 본문. append-only라 이전 결론도 지우지 않고 남긴다. 수정 버튼 없음. |
audit_event_item/1 |
감사 로그 한 줄 |
attrs: event %{seq, inserted_at, actor_type :user | :agent | :system, actor_label, action, subject_type, subject_label, subject_path | nil, summary, payload map} (audit_events 컬럼. actor_label은 LiveView가 만든다: user는 표시 이름, agent는 actor_id 그대로 "triage:", system은 worker 이름) |
행위자 3종, payload 접힘/펼침, 방금 추가됨(1.5초 하이라이트) |
<li class="flex gap-3 border-b border-base-300 py-2">. 아이콘 원 size-6 rounded-full: 사람 bg-primary/10 text-primary + user-circle, AI bg-accent/10 text-accent + sparkles, 시스템 bg-base-300 + cog-6-tooth. 시각 tabular-nums, action은 mono 원문 + 한글 라벨, payload는 <details> 안 key-value. 수정/삭제 버튼 없음. stream + "더 보기"(before_seq). |
공통: 버튼 변형은 core button/1이 primary/soft만 제공하므로 class attr로 btn btn-success 등을 넘긴다(class를 넘기면 기본 클래스가 대체됨).
5. 화면 구성
모두 데스크톱 우선(1024px 이상 보장). 1024px 미만에서는 S5의 왼쪽 큐가 본문 위로 접히고, 표는 컨테이너 안에서 가로 스크롤한다(페이지 전체는 가로 스크롤하지 않는다). 768px 미만의 편집은 MVP 범위 밖이다. 화면 6종의 시안은 HTML 시안에 라이트/다크로 있고, 값은 도메인 문서 7장의 합성 데이터(누리정밀 주식회사, FY2025)를 따른다. 화면별 영역 번호, 상태 변형, 이벤트와 권한, 문구, 미결 사항은 화면설계서에 있다.
5.1 라우트와 공통 셸
라우트는 메인 세션이 소유한다(설계 문서 12장). 모두 로그인 필요 live_session 안에 있고, engagement 하위 화면은 on_mount에서 멤버십을 확인한다. LiveView 이름은 설계 문서 1장을 따른다.
| 화면 |
경로 |
LiveView |
내용 |
| S1 |
/engagements, /engagements/new |
EngagementLive.Index (:index, :new) |
내가 속한 감사 대상 목록, 생성 패널 |
| S2 |
/engagements/:id |
EngagementLive.Show |
개요, 멤버, GL 적재 목록, 테스트 실행 목록 |
| S3 |
/engagements/:id/imports/new |
ImportLive.New |
CSV 업로드 또는 합성 GL 생성, 적재 상태와 오류 |
| S4 |
/engagements/:id/runs/:run_id |
TestRunLive.Show |
룰 진행률, AI 트리아지 일괄 요청, 플래그 전표 목록 |
| S5 |
/engagements/:id/findings/:finding_id |
FindingLive.Show |
전표 검토: 근거, AI 제안, 작성자 판단, 승인/반려 |
| S6 |
/engagements/:id/audit |
AuditLive.Index |
감사 로그 (읽기 전용) |
공통 셸은 상단 바 하나다: 로고(Tickmark), 현재 감사 대상 이름(S2로 가는 링크), 감사 로그 링크, presence 아바타, theme_toggle, 사용자 메뉴(이메일, 로그아웃). S1에서는 감사 대상, 감사 로그, presence를 숨긴다. 화면 안의 이동 경로는 제목 위 한 줄(감사 대상 / 누리정밀 주식회사 FY2025 / 테스트 실행)로 보인다.
룰 이름은 표 셀에서 짧은 이름을 쓰고, 상세(S5)와 툴팁에서 전체 이름을 쓴다. 짧은 이름은 레지스트리가 아니라 TickmarkWeb.Format이 가진다.
| rule_id |
전체 이름 |
짧은 이름 |
가중치 |
weekend_holiday_posting |
주말/공휴일 전기 |
휴일 |
15 |
off_hours_entry |
업무시간 외 입력 |
시간 외 |
10 |
round_amount |
라운드 금액 |
라운드 |
10 |
period_end_manual |
기말 수기 전표 |
기말 수기 |
20 |
approval_control_gap |
승인 통제 이탈 |
승인 이탈 |
25 |
backdated_posting |
증빙일 괴리 |
증빙일 |
15 |
rare_account_pair |
희귀 계정 조합 |
희귀 조합 |
20 |
duplicate_entry |
중복 전표 의심 |
중복 |
15 |
5.2 S1 감사 대상 목록
- 열: 회사명(링크), 회계연도, 결산일, 수행중요성(원), 최근 테스트(테스트 실행 배지), 검토 큐(건), 내 역할.
- "새 감사 대상"은
:new live_action으로 목록 위에 인라인 패널을 연다(모달 없음). 입력은 회사명, 회계연도, 결산일, 수행중요성 금액. 만든 사람은 그 감사 대상의 작성자 멤버가 된다는 안내를 패널 안에 둔다.
- 빈 상태: "감사 대상이 없습니다." + 생성 버튼.
5.3 S2 감사 대상 상세
- 헤더: 회사명 +
FY2025, 결산일, 수행중요성, presence. 오른쪽에 주 행동 "GL 불러오기"(S3).
stat_row: 최근 완료 실행 기준 전표, 라인, 검토 큐, 경미 적중. 실행이 없으면 -와 "테스트를 실행하면 채워집니다".
- 멤버: 이름, 이메일, 역할 배지(작성자/검토자). 추가 폼(이메일 + 역할)은 감사 대상의 작성자에게만 보인다. 한 사용자는 한 역할만 가진다.
- GL 적재 목록: 출처(업로드 파일명 또는 "합성 GL, 시드 N"), 상태(GL 적재 배지), 전표, 라인, 차변 합계(원), 대변 합계(원), 오류, 경고, 해시 앞 8자, 적재 시각. 완료된 행에는 "테스트 실행" 버튼(작성자만, 같은 적재에 실행 중인 테스트가 없을 때).
{:error, :run_in_progress}는 버튼 옆 안내 문구로 보인다.
- 테스트 실행 목록: 시작 시각, 상태, 룰 완료 수, 적중 전표, 검토 큐, S4 링크.
- 실시간:
engagement:#{id}의 import_updated, test_run_updated.
5.4 S3 GL 적재
- 두 방식을 탭으로 나눈다. CSV 업로드:
allow_upload 1개, .csv, 20MB, 100,000행. 합성 GL 생성: 시드(정수, 비우면 생성기의 기본 시드 20251231). 생성기는 라인 수를 받지 않는다. 같은 시드면 같은 GL이 나온다는 안내를 둔다(도메인 문서 7장).
- 제출하면 같은 화면에서 상태가 대기, 적재 중, 완료/실패로 바뀐다(
import_updated). 완료 요약은 전표, 라인, 차변 합계 = 대변 합계, 경고 수와 "테스트 실행" 버튼.
- 실패하면 오류 표(행, 컬럼, 코드, 내용)를 보인다. 최대 100건, 넘으면 "그 밖에 N건". 내용은 코드별 고정 한글 문구이고 파일의 원래 값은 화면에 다시 쓰지 않는다. 경고(
period_mismatch)는 접힌 별도 표.
| 코드 |
내용 |
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 (경고) |
전기일의 월과 회계기간이 다릅니다 |
5.5 S4 테스트 실행
- 상단
test_progress: 룰 8개, 동시에 최대 4개 실행. 완료 뒤에는 룰별 적중 수가 남는다.
- 트리아지 바(작성자만, 실행 완료 뒤 활성): "검토 큐 52건 중 AI 미요청 52건" + "AI 트리아지 요청". 1회 최대 100건이라는 안내와, 요청하면 감사 로그에 요청자가 남는다는 안내. 요청 직후 대상 행의 AI 위험도는 "분석 중"이 되고 결과가 오는 대로 바뀐다.
filter_tabs: 검토 큐(점수 25 이상) / 경미 적중(점수 25 미만). 보조 필터는 검토 상태(전체, 미판단, 검토 대기, 승인, 반려). AI 위험도 열과 필터는 검토 큐 탭에만 있다(경미 적중은 트리아지 대상이 아니다).
- 표 열: 전표번호, 전기일, 금액(원, 차변 합계), 점수(
score_meter), 적중 룰(짧은 이름), AI 위험도, 작성자 판단, 검토. 기본 정렬은 점수 내림차순, 같으면 전표번호.
- 행의 전표번호가 S5 링크다.
- 실시간:
test_run:#{run_id}의 rule_progress, run_completed와 engagement:#{id}의 finding_updated.
5.6 S5 전표 검토
위에서 아래 순서가 판단 순서다: 사실(전표) -> 룰 적중 -> AI 제안(점선) -> 사람 판단(실선) -> 서명.
- 왼쪽 큐: S4의 현재 탭과 필터를 이어받은 목록, 현재 항목 표시, "다음 미판단".
- 헤더: 전표번호(
h1), score_meter, risk_badge, conclusion_badge, review_badge, presence.
- 전표 사실: 전기일, 증빙일, 입력 일시(KST), 회계기간, 입력자, 승인자, 구분(수기/자동), 거래처, 적요. 룰 근거가 된 값(예: 입력자 = 승인자)은 같은 줄에
text-warning 아이콘과 sr-only 설명.
- 전표 행: 행, 계정(코드 + 이름), 차변(원), 대변(원), 합계 줄과 "차대 일치".
rule_hit_list: 룰별 가중치와 근거, 맨 아래 "점수 55 = 20 + 10 + 25".
ai_suggestion_card.
conclusion_form.
- 이력: 결론과 검토 결정을 시간순으로(작성자 결론 제출, 검토자 반려와 사유, 재제출, 승인). 결론은 append-only라 이전 결론도 남는다.
sign_off_bar (화면 아래 고정).
- 실시간: 같은 finding의
finding_updated를 받으면 다시 읽는다. presence의 view는 "finding:#{id}"라서 같은 전표를 보는 사람만 헤더에 보인다.
- AI 제안과 사람 판단은 시각적으로 분리되고, AI는 결론 라디오를 미리 고르지 않는다.
5.7 S6 감사 로그
- 헤더 고정 문구: "읽기 전용. 기록은 수정하거나 삭제할 수 없습니다."
- 필터: 행위자 유형(전체, 사람, AI, 시스템), 대상(전표번호나 실행 등 subject). 서버 필터이며
AuditTrail.list_events/3의 actor_type, subject 옵션을 쓴다.
- 최신이 위.
stream으로 그리고 아래 "더 보기"는 before_seq로 이어 읽는다.
- 새 이벤트(
audit_appended)는 목록에 바로 끼우지 않고 상단에 "새 이벤트 N건" 버튼을 띄운다. 읽던 위치가 밀리지 않게 하기 위해서다.
- 한 행 =
audit_event_item 하나: seq, 일시(KST, 초까지), 행위자, 행위, 대상, 요약, payload 펼침.
6. 금액, 날짜, ID 표기 규칙
| 대상 |
규칙 |
예 |
| KRW 금액 (표) |
원 단위 정수, 천 단위 쉼표, 통화 기호 없음, 열 머리에 "(원)". DB의 numeric(18,2)는 소수부가 0이면 생략. 0이 아닌 소수부는 숨기지 않고 2자리로 표시한다(데이터 오류 노출). |
12,000,000 |
| KRW 금액 (문장) |
숫자 뒤에 "원" |
수행중요성 225,000,000원 |
| KRW 금액 (KPI 요약) |
억/만 단위 축약 허용, 전체 값은 title. 표에서는 축약 금지. |
2억 2,500만원 |
| 음수 |
앞에 hyphen-minus. 괄호와 빨간색은 쓰지 않는다(색 비의존). |
-1,250,000 |
| 0 |
차변/대변 열은 -(스크린리더용 sr-only "0"), 합계와 요약은 0 |
- |
| 값 없음 |
- + sr-only "없음" |
- |
| 비율, 진행률 |
비율 소수 1자리, 진행률 정수 |
62.5%, 62% |
| 건수 |
천 단위 쉼표 + 단위 |
8,341건 |
| 날짜 |
YYYY-MM-DD. "2025년 12월 31일" 형식과 요일은 쓰지 않는다. |
2025-12-31 |
| 일시 |
표시는 KST 24시간 YYYY-MM-DD HH:MM. 감사 로그와 상세는 초까지. 열 머리에 "(KST)"를 한 번만 쓰고, 단독 표기는 뒤에 " KST". 시스템 시각은 <time datetime>과 title에 UTC 원문(GL의 entered_at은 아래 KST 항목). |
2025-12-30 23:42 |
| entry_number |
원문 그대로 mono, 선행 0 보존, 줄바꿈과 말줄임 금지. 행 참조는 뒤에 L2(muted). |
JE-000123 L2 |
| 계정 |
코드는 mono, 이름은 sans |
4110 제품매출 |
| 회계연도, 기간 |
연도 FY2025, 기간은 숫자, 13은 13(결산조정) |
FY2025, 12 |
| 사용자 |
표시 이름 우선, ID는 title과 상세에서 mono |
김작성 |
| 해시, 모델 ID |
해시는 앞 8자 mono + 전체는 title, 모델 ID는 전체 mono |
a1b2c3d4 |
| 긴 텍스트 (적요) |
목록에서는 1줄 truncate + title, 상세에서 전체 |
|
- KST: 시스템 시각(
inserted_at 등)은 UTC로 저장하고 표시할 때만 고정 오프셋 +9시간을 더한다. KST는 DST가 없어 tzdata 의존성이 필요 없다(메인 세션 결정). GL의 entered_at은 시간대 없는 KST 값이라 변환 없이 그대로 보이고, <time datetime>에도 오프셋을 붙이지 않는다.
- 포맷 함수는 한 모듈(
TickmarkWeb.Format)에 모아 컴포넌트와 LiveView가 같이 쓴다.
7. 접근성
7.1 검토 화면의 키보드 흐름
| 순서 |
영역 |
키보드 동작 |
| 1 |
"본문으로 건너뛰기" 링크 |
첫 Tab에서 나타나고 Enter로 본문 이동 (sr-only focus:not-sr-only) |
| 2 |
상단 바 |
Tab. 현재 위치는 aria-current="page" |
| 3 |
큐 목록 |
항목은 링크, Tab 이동. 현재 항목 aria-current="true". "다음 미판단" 버튼 |
| 4 |
전표 제목 (h1, tabindex="-1") |
항목 전환(live_patch) 뒤 프로그램으로 포커스를 옮기고 aria-live로 "JE-000123 검토 화면" 안내 |
| 5 |
전표 행 테이블 |
스크롤 영역 tabindex="0" role="region" aria-label="전표 행". 방향키 스크롤 |
| 6 |
룰 적중 |
<details> 요약에서 Enter/Space로 펼침 |
| 7 |
AI 제안 |
role="region" aria-label="AI 제안, 사람의 판단 아님". 내부 <details>만 포커스 대상 |
| 8 |
작성자 판단 |
Tab으로 라디오 그룹에 진입, 방향키로 선택, Tab으로 코멘트, Ctrl+Enter 또는 저장 버튼 |
| 9 |
sign-off bar |
반려, 승인 버튼. 비활성 이유는 aria-describedby. 반려 패널은 Esc로 닫고 포커스가 반려 버튼으로 돌아온다 |
- 단일 문자 단축키는 쓰지 않는다(WCAG 2.1.4). 수정자 키 조합은 Ctrl+Enter만 쓴다.
- 판단 저장 뒤 포커스는 저장 버튼에 남기고 결과는
aria-live="polite" flash로 알린다.
7.2 포커스와 가림
- 모든 포커스 가능 요소에 2px primary 링, offset 2px (3.3 대비 표 기준 6.26 이상).
outline: none을 쓰지 않는다.
- sticky 헤더(약 36px)와 sign-off bar(약 56px)가 포커스를 가리지 않도록 스크롤 컨테이너에
scroll-padding-top: 2.5rem, scroll-padding-bottom: 4rem.
- 클릭 대상은 24px 이상(WCAG 2.2 2.5.8). 기본 컨트롤 32px,
btn-sm 25.6px.
7.3 색이 유일한 신호가 아니다
| 신호 |
색 |
함께 쓰는 수단 |
| 위험도 |
error / warning / neutral |
텍스트 높음, 중간, 낮음 + 아이콘 모양 3종 |
| 작성자 판단 |
success / warning / error |
텍스트 + 아이콘 + outline 모양 |
| 검토 상태 |
neutral / success / error |
텍스트 + 아이콘 + solid 모양 |
| AI 콘텐츠 |
accent |
"AI 제안" 라벨, sparkles 아이콘, 점선 경계 |
| 룰 대상 전표 행 |
warning |
좌측 굵은 막대 + 아이콘 + sr-only "룰 대상" |
| 진행률 |
primary |
퍼센트 숫자와 "5 / 8" 텍스트 |
| 점수 |
없음 |
숫자 + 5칸 막대 + 구간 텍스트(경미, 중, 상, 최상) |
| 음수 금액 |
없음 |
hyphen-minus 부호 |
| presence |
neutral |
이니셜 + 이름 툴팁 + sr-only 목록 |
7.4 그 밖의 규칙
<html lang="ko">. 표는 caption(sr-only), th scope="col", 숫자 열 우측 정렬.
- 실시간 갱신: 진행률 텍스트는 10% 단위로 바뀔 때만
aria-live 갱신. 감사 로그 신규 이벤트는 "새 이벤트 N건" 요약만 알린다.
- 모션은
motion-safe:로만 적용(스피너, 하이라이트). 모달은 쓰지 않아 포커스 트랩 문제가 없다.
- 검증 방법: (1) 라이트/다크 각각 키보드만으로 S5 화면(5.6)을 끝까지 수행, (2) 브라우저 도구(Lighthouse 등)로 접근성 점검 - 새 의존성 없이 수동 수행, (3) 3.3 표의 hex 값이 바뀌면 대비를 다시 계산해 갱신.
확인 필요
| 항목 |
내용 |
| hex 테마 값 |
daisyUI 테마 플러그인에서 hex가 그대로 통과하는지 빌드로 확인 (실패 시 oklch 변환) |
| 입력 경계 override |
--input-color override가 포커스, 에러 상태를 유지하는지 브라우저에서 확인 |
| 대비 근사 |
soft 배경은 oklab 8% 혼합을 직접 계산한 근사값. 실제 렌더 색은 브라우저에서 한 번 확인 |
Changelog
- 2026-10-17: 최초 작성
- 2026-10-17: 메인 세션 검토. 설계, 도메인 문서와 상태값과 필드명 정합(
no_exception, needs_follow_up, findings.status 4단계, triage_status), 점수 표시(score_meter, 색 없음), 화면을 라우트 6개(S1-S6)와 화면별 명세로 교체, 승인 단위는 항목별(감사 대상 단위 sign-off는 2단계), 코멘트는 모든 결론에 필수, KST 고정 오프셋, 예시 값을 합성 데이터로 교체, HTML 시안 추가, score_meter 채움 칸 계산식과 finding_history/1 행 추가(HTML 시안과 맞춤)
- 2026-10-17: S3, S4, S6 LiveView 모듈명을
ImportLive.New, TestRunLive.Show, AuditLive.Index 로 확정 (Phoenix 생성기 관례: 디렉터리 + 액션 모듈)
- 2026-10-17: 화면설계서 링크 추가. 5.4 합성 GL 생성에서 목표 라인 수 제거(생성기는 시드만 받음), 오류 코드 표에
unknown_column, too_long, invalid_value, duplicate_line_number 추가
- 2026-10-19: HTML 시안 수정. 라인 수 19,804, 합성 GL 출처 "시드 20251231", 목표 라인 수 칸 삭제, S5 숨은
ai_triage_id에 보인 제안 id, S6 #74를 review.denied에서 승인으로 교체(화면설계서 C-10), 짝 없는 </main> 삭제