라운드 5 화면 셸과 명세 문서
- 작성일: 2026-10-20
- 작성: 메인 세션 (오케스트레이터), 브랜치
feature/m2-rule-engine - 관련 문서: 계약 12.4, WBS와 마일스톤 (4.3, 5.2, 5.3, 6.1), 워커 보고 -12 공통 셸과 홈 화면, -13 인증 화면과 한글 문구, 산출 문서 요구사항 명세서, 기능 정의서, API 연동 규격서
목표
사용자 지시 (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/1attr, 문서 ID 체계 (REQ, NFR, FN, IF) 가 들어 있다. - 선행 작업으로 Gettext 기본 로케일을
ko로 바꿨다 (fbb30b8, 519 통과).config/*는 공유 파일이라 메인이 한다. - F1, F2 는
MIX_TEST_PARTITION8, 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 을 열고 데모 작성자로 다음 순서를 확인했다.
- 홈
- 로그인 (메일 링크,
/dev/mailbox) - S1, S2, S4
- S5. AI 제안을 요청하면 Fake 제안이 같은 화면에 뜨고 S6 에 요청과 완료 이벤트가 남았다.
- 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)