개발 기록 2026-10-20 docs/devlog/2026-10-20-14-라운드-5-화면-셸과-명세-문서.md GitHub에서 보기

라운드 5 화면 셸과 명세 문서

목표

사용자 지시 (10/20) 가 두 가지였다.

  • 화면설계서의 화면이 안 보인다. 메인 화면, 로그인, 회원가입, 감사 대상 선택을 Sol 6.1 max (fast) 로 빨리 맞춘다.
  • 요구사항 명세서, 기능 정의서, API 연동 규격서가 없다. Astra max (fast) 로 쓴다.

워커 4명이 이 일을 하는 동안 메인은 다른 일을 맡았다. W6 병합, S5 AI 제안 요청 단추, 데모 시드, 병합 뒤 브라우저 확인이다.

한 일

배정 (f5f7f78)

워커 범위 에이전트 / 모델 이유
F1 공통 상단 바, 디자인 토큰 (app.css), 홈, 404/500, 한글 연결 끊김 문구 Codex gpt-6.1-sol max (fast) 화면설계서 3장과 디자인 시스템에 크기와 색이 정해져 있다. 추론보다 빠른 구현이 필요
F2 로그인, 회원가입, 설정 화면 한글화와 디자인, Gettext ko 번역 Codex gpt-6.1-sol max (fast) 문구 교체와 테스트 단언 교체가 대부분
D1 요구사항 명세서, 기능 정의서 Codex gpt-6-astra max (fast) 코드, 계약, 화면설계서를 대조해 빠짐없이 정리
D2 API 연동 규격서 Codex gpt-6-astra max (fast) 라우트, LiveView 이벤트, PubSub, job, 외부 API 경계를 코드 근거와 함께 정리
  • 워커를 띄우기 전에 계약 12.4 를 먼저 적었다. 소유 파일, Layouts.app/1 attr, 문서 ID 체계 (REQ, NFR, FN, IF) 가 들어 있다.
  • 선행 작업으로 Gettext 기본 로케일을 ko 로 바꿨다 (fbb30b8, 519 통과). config/* 는 공유 파일이라 메인이 한다.
  • F1, F2 는 MIX_TEST_PARTITION 8, 9 로 테스트 DB 를 나눴다. D1, D2 는 문서만 쓰고 각자 브랜치에 커밋했다.

메인 작업 (워커와 병렬)

W6 병합 (2718ff9, 546 통과). 라운드 4b 의 S5 결론 제출과 승인/반려다. 경위는 라운드 4 계약과 통합 의 "W6 병합" 절에 적었다.

데모 시드 (f196012, WBS 5.3). mix setup 한 번으로 데모가 가능한 상태를 만든다.

  • 작성자 kim.writer@example.com, 검토자 park.reviewer@example.com. 감사 대상은 가상의 "누리정밀 주식회사" FY2025 다.
  • 합성 GL 생성, 적재, 룰 8종 실행까지 화면에서 누르는 것과 같은 context 함수를 부른다. 그래서 감사 이벤트도 똑같이 남는다.
  • 적재와 실행은 Oban job 이다. 시드는 상태를 0.5초마다 확인하며 끝날 때까지 기다린다 (최대 2분). 개발 DB 실측은 3.4초, 전표 8,341건, finding 265건이었다.
  • 데모 작성자가 이미 있으면 아무것도 하지 않는다.
  • 비밀번호는 TICKMARK_DEMO_PASSWORD 가 있을 때만 넣는다. 없으면 메일 로그인 링크로 들어간다.

S5 AI 제안 요청 단추 (0f53b7a, 550 통과, WBS 4.3).

  • 작성자가 누르면 Triage.request_finding_triage/2 로 건별 트리아지를 큐에 넣고 화면을 "분석 중" 으로 바꾼다.
  • 단추는 점수가 기준 이상이고 분석 중이 아닐 때만 보인다. 검토자에게는 비활성으로 보인다. 실패하거나 끝난 제안은 "다시 요청" 할 수 있다.
  • 결과는 flash 4가지로 나뉜다: 요청함, 이미 분석 중, 기준 미달, 권한 없음.

사용자 질문: "mix phx.server 가 맞나, 4000 포트에 아무것도 없다". 실행 명령은 맞았다. 그때 홈은 F1 병합 전이라 Phoenix 기본 페이지였다. 업무 화면은 로그인과 감사 대상이 있어야 보이는데 데모 데이터도 없었다. 시드와 F1 병합으로 해결하고, README 의 실행과 로그인 절을 고쳤다 (cfbed56). 내용은 데모 계정, 메일 로그인 순서, 화면이 비어 보일 때 확인할 것이다.

병합

순서 커밋 내용 병합 뒤 mix precommit
1 1e0574d F2 병합 (9fc4cd4) 554 passed
2 9a20b16 W6 테스트의 부재 단언을 한글 기준으로 수정 (아래 "막힌 점") 554 passed
3 4115351 F1 병합 (f13ec6c). 충돌 없음 562 passed
4 f300f2f S5 상단 바에 감사 대상과 감사 로그 연결 563 passed
5 8502ed7 브라우저 확인에서 나온 화면 수정 3건 563 passed
6 cfbed56 README 실행과 로그인 방법 문서만
7 60781a3 D1 병합 (e3a2948, 2개 문서 1,562줄) 문서만
8 164788e D2 병합 (7d5bc12, 723줄) 문서만
  • 병합 전에 F1, F2 의 diff 를 직접 읽었다. 소유 파일 밖 변경이 없는지, 공유 파일을 건드리지 않았는지 확인했다. F1 은 S1-S4, S6 의 <Layouts.app ...> 여는 태그 한 줄씩만 고쳤다. S5 는 W6 소유라 손대지 않았다.
  • F1 과 F2 는 둘 다 test/ 아래 파일을 고쳤지만 겹치는 파일이 없어 충돌이 없었다.

브라우저 확인

Chrome 에서 localhost:4000 을 열고 데모 작성자로 다음 순서를 확인했다.

  1. 홈
  2. 로그인 (메일 링크, /dev/mailbox)
  3. S1, S2, S4
  4. S5. AI 제안을 요청하면 Fake 제안이 같은 화면에 뜨고 S6 에 요청과 완료 이벤트가 남았다.
  5. S6

S3 은 이번에 따로 열어 보지 않았다 (다음 할 일).

화면 문제 3건이 나와 8502ed7 에서 고쳤다.

  • S5 의 거래처, 적요, AI 근거, 이력 코멘트 앞뒤에 빈 줄과 공백이 보였다. 아래 "막힌 점" 참고.
  • 내용이 짧은 화면에서 body 배경이 화면 끝까지 차지 않았다. min-h-screen 을 줬다.
  • 로그인, 회원가입, 계정 설정, 로그인 확인 화면의 탭 제목이 "Tickmark | Tickmark" 였다. 화면마다 한글 page_title 을 넣었다.

산출 문서 (D1, D2)

문서 내용 ID 수 (고유)
요구사항 명세서 기능 요구사항 8개 영역, 비기능 요구사항 REQ 57, NFR 17
기능 정의서 화면 S1-S6, 인증, 시스템 기능과 REQ 추적 FN 42
API 연동 규격서 HTTP, LiveView, PubSub, Oban, Claude, CSV, context 경계 IF 114

ID 수는 각 문서에서 grep -oE 로 고유 ID 를 센 값이다.

세 문서 모두 기준 커밋 f5f7f78 에서 썼다. 그 뒤 바뀐 점은 메인이 API 연동 규격서 14장에 따로 적었다. 홈 리다이렉트, 한글 flash, W6 이벤트, AI 요청 단추, 문서 위키 라우트가 해당한다.

결정과 트레이드오프

  • 화면 워커 둘을 파일 단위로 나눴다. 대안은 "화면 묶음" 으로 나누는 것이었다. 예를 들어 홈과 로그인을 한 워커에게 주는 방식이다. 그러면 두 워커가 layouts.ex 와 app.css 를 함께 고치게 되어 거의 확실히 충돌한다. 대신 셸, 홈, 오류 화면은 F1, 인증 화면은 F2 에게 줬다. F2 는 인증 화면의 폭을 자기 마크업 안에서 좁히게 했다 (계약 12.4). 병합 충돌은 0건이었다.
  • S5 의 상단 바 연결은 F1 병합 뒤 메인이 했다. S5 (finding_live/*) 는 그때 W6 소유였다. F1 이 같이 고치면 W6 와 충돌한다. 대가는 S5 연결이 커밋 하나 늦어지는 것뿐이었다 (f300f2f).
  • 기본 로케일 ko 는 워커 시작 전에 메인이 넣었다. ko 번역이 없는 msgid 는 영어 원문으로 돌아간다. 그래서 이 설정만으로는 화면과 테스트가 바뀌지 않아 미리 넣어도 안전했다. F2 는 PO 파일만 추가하면 됐다.
  • AI 요청 단추의 권한 확인은 Triage 에 맡겼다. 대안은 LiveView 에서 역할, 점수, 상태를 먼저 확인하고 통과할 때만 Triage 를 부르는 것이었다. 화면은 단추를 숨기거나 비활성으로 만들 뿐이다. 실제 확인은 request_finding_triage/2 가 한다. 역할은 DB 의 멤버십으로 확인하고, 점수와 상태는 finding 행을 잠근 뒤 다시 본다. 검토자의 위조 클릭, 다른 탭에서 이미 요청한 낡은 단추, 두 탭의 동시 클릭이 모두 같은 경로로 막힌다. 확인 규칙이 두 곳에 있으면 한쪽만 바뀌는 일이 생긴다.
  • 시드는 context 를 거친다. 대안은 Repo.insert_all 로 행을 직접 넣는 것이었다. 더 빠르지만 감사 이벤트, 상태 전이, 방송을 따로 흉내 내야 한다. context 를 거치면 시드가 "S1 생성 -> S3 적재 -> S2 실행" 시나리오를 처음부터 끝까지 한 번 돌리는 셈이다. 데모 데이터의 감사 로그도 실제와 같다. 대가는 Oban job 을 기다리는 3.4초다.
  • D1, D2 는 기준 커밋을 고정해 썼다. 진행 중인 F1, F2, W6 코드를 쫓아가면 문서가 계속 어긋난다. f5f7f78 을 기준으로 쓰고, 진행 중인 부분은 "불일치" 표에 남기게 했다. 대가로 병합 시점에 이미 낡은 내용이 있었고, 메인이 14장에 차이를 적어 보완했다.
  • D1, D2 는 서로의 ID 를 참조하지 않았다. 동시에 쓰는 문서라 상대의 FN, REQ 번호를 알 수 없다. D2 는 화면 (S1-S6) 과 계약 절을 참조했다. FN 과 IF 의 교차 링크는 아직 달지 않았다.

막힌 점과 해결

  • W6 의 부재 단언이 F2 병합 뒤 늘 참이 됐다. W6 테스트는 S5 결론 폼에서 화면 전용 문구가 기본 오류 문구를 대신하는지 확인했다. 방법은 "can't be blank" 가 화면에 없다는 refute 였다. F2 의 ko 번역이 들어가자 기본 문구 자체가 "필수 입력 항목입니다." 로 바뀌었다. 그 뒤로는 영어 문자열이 화면에 없다는 단언이 코드가 어떻든 참이다. 테스트는 통과하지만 아무것도 확인하지 않는다. F2 병합 직후 이를 찾아 한글 기본 문구가 없는지 확인하도록 고쳤다 (9a20b16). 부재 단언은 그 문자열이 실제로 나올 수 있는 조건에서만 의미가 있다. 라운드 3 의 #imports th 단언과 같은 종류의 실수다.
  • whitespace-pre-wrap 아래 템플릿 공백 (8502ed7). HEEx 는 <%= if ... do %> 블록 안의 줄바꿈과 들여쓰기를 텍스트로 출력한다. 보통은 브라우저가 공백을 합쳐 보이지 않는다. 그런데 여러 줄 적요를 보이려고 whitespace-pre-wrap 을 준 칸에서는 그대로 빈 줄이 된다. optional_value 를 if 블록 대신 함수 두 절 (값이 있을 때, 없을 때) 로 나눴다. 여러 줄이 필요 없는 칸에서는 pre-wrap 을 뺐다.
  • 탭 제목 "Tickmark | Tickmark". 루트 레이아웃의 .live_title 은 page_title 이 없으면 기본값 "Tickmark" 에 접미사 " | Tickmark" 를 붙인다. F1 은 홈에서 같은 문제를 찾아 고쳤다. 인증 화면 4개는 F2 범위였지만 F2 테스트가 탭 제목을 보지 않아 남아 있었다. 브라우저에서 찾았다.

Elixir 학습 포인트

  • Gettext 의 도메인과 로케일. CoreComponents.translate_error/1 은 changeset 오류의 msgid (예: "can't be blank") 를 errors 도메인에서 찾는다. 기본 로케일이 ko 이고 priv/gettext/ko/LC_MESSAGES/errors.po 에 그 msgid 가 있으면 번역을, 없으면 원문을 돌려준다. 그래서 스키마 코드를 바꾸지 않고 PO 파일만으로 문구가 바뀐다. 한국어는 복수형을 구분하지 않으므로 PO 헤더가 nplurals=1 이고 msgstr[0] 하나만 쓴다.
  • mix run 시드와 Oban. mix run priv/repo/seeds.exs 는 애플리케이션 전체를 시작한 뒤 스크립트를 돈다. Oban 도 떠서 백그라운드에서 job 을 처리한다. 스크립트가 끝나면 VM 이 바로 멈추므로, 기다리지 않으면 적재나 룰 실행이 중간에 끊긴다. 시드는 Enum.reduce_while/3 와 Process.sleep/1 로 상태를 폴링한다. 테스트에서는 Oban.Testing 이 job 을 같은 프로세스에서 바로 돌리므로 이런 대기가 필요 없다.
  • 함수 컴포넌트 레이아웃. Phoenix 1.8 은 라우터에서 앱 레이아웃을 씌우지 않는다. 각 LiveView 가 <Layouts.app ...> 로 자기 본문을 감싼다. 그래서 화면마다 engagement, active 같은 attr 을 넘겨 상단 바를 바꿀 수 있다. attr 기본값 (nil, []) 이 "감사 대상 밖" 상단 바가 된다.
  • 잠금 뒤 재확인 (Ecto.Multi.run). request_finding_triage/2 는 역할을 확인한 뒤 Multi 의 첫 단계에서 finding 을 FOR UPDATE 로 잠근다. 그 안에서 점수와 상태 (queued 인지) 를 다시 본 뒤 상태 변경, job 생성, 감사 이벤트를 한 트랜잭션에 넣는다. 화면이 본 상태와 지금 상태가 달라도 결과가 맞다.

다음 할 일

  • 라운드 4b 나머지 (메인): S4 AI 열과 일괄 요청 (3.4), Presence 와 상단 바 아바타 (4.3, 4.4), 검토 대기열과 "다음 미판단", 통합 테스트.
  • S3 브라우저 확인.
  • D1 의 FN 과 D2 의 IF 교차 링크.
  • 배포 대상 결정 (5.1, 사용자): Render + Neon 제안. 결정되면 5.4.
  • M6: 데모 GIF (6.2), 공개 전 점검과 저장소 공개 전환 (6.3).

관련 커밋

  • f5f7f78: 라운드 5 배정과 Layouts.app 계약
  • fbb30b8: Gettext 기본 로케일 ko
  • 2718ff9: W6 병합 (649a0b1)
  • f196012: 데모 시드
  • 0f53b7a: S5 AI 제안 요청 단추
  • 1e0574d: F2 병합 (9fc4cd4)
  • 9a20b16: W6 부재 단언 수정
  • 4115351: F1 병합 (f13ec6c)
  • f300f2f: S5 상단 바 연결
  • 8502ed7: 브라우저 확인 수정
  • cfbed56: README 실행과 로그인
  • 60781a3: D1 병합 (e3a2948)
  • 164788e: D2 병합 (7d5bc12)