gwimong's blog Software Engineer

artifact-transformer 작업 일지 #35

형식만 남은 절차를 걷어낸 날 — 관리자 바로 반영, 대상을 먼저 묻는 작업 만들기, 그리고 실제 데이터가 정하는 폼 폭


이 글은 2026-08-18 하루의 기록이다. 화면 쪽 빚을 네 갈래로 갚았다. 검토 절차, 작업 생성 동선, 데이터 편집기 레이아웃, 도구 창 문법. 코드로는 네 커밋에 걸쳐 25개 파일 +2,033/-576이고, 마지막에 조용히 실패하던 스케줄러 하나를 덤으로 잡았다.

배경 — 절차가 형식만 남으면 사람은 우회로를 찾는다

오늘 손댄 것들은 서로 달라 보이지만 증상이 같다. 화면이 요구하는 절차가 실제로 필요한 것보다 길다.

반영으로 가는 길은 검토 승인 하나뿐이었다. 오타 한 글자를 고치는 데도 리뷰를 올리고 스스로 승인하는 두 걸음을 밟아야 했다. 검토자가 따로 없는 자리에서 그 기다림은 형식이다.

[작업 관리]의 [+ 작업 만들기]는 구성 요소 상세에서 아무 일도 하지 않았다. 눌러도 토스트조차 없었다.

데이터 편집기는 큰 산출물을 열면 어디에 무엇이 있는지 알 방법이 스크롤뿐이었다. 폼 자체도 문제였는데, 모든 필드가 같은 폭이라 code 한 칸과 세 문단짜리 description이 나란히 섰다.

도구 창은 창마다 목록과 상세의 경계, 필터가 놓이는 자리, 전체 펼치기/접기의 순서가 제각각이었다.

문제

  1. 검토를 건너뛰되 기록은 남기려면 무엇을 서버가 강제해야 하나.
  2. [+ 작업]이 눌리는 자리마다 대상이 다른데, 대상 선택을 어디까지 화면이 대신 물어야 하나.
  3. 스키마가 값의 크기를 안 알려 주면 폼은 무엇을 근거로 폭을 정하나.
  4. 도구 창이 여럿일 때 “같은 일은 같은 자리에서”를 무엇으로 보장하나.

결정 1 — 검토를 건너뛰되, 검토자를 자신으로 둔다

POST /reviews에 applyNow를 붙여 생성과 반영이 한 호출로 끝나게 했다.

핵심은 기록을 없애는 것이 아니라 검토자를 자신으로 두는 것이다. 리뷰 레코드는 그대로 만들어지고 reviewer·decidedBy가 작성자 본인이 되어 “스스로 검토했다”가 이력에 남는다.

# 요청 body 에 남을 검토자를 적어 보내도 서버가 본인으로 덮어쓴다
reviewer = who if apply_now else str(body.get("reviewer", "")).strip()

검토를 건너뛰면서 남의 이름을 기록에 남기면 그 기록은 사실과 어긋난다. 그래서 이건 검증이 아니라 덮어쓰기로 처리했다.

권한 판정은 관리자 페이지와 같은 축(_require_admin)을 그대로 썼다. SSO가 켜져 있으면 SERVER_ADMIN_ROLE 역할, 꺼진 사내망이면 전원이다. 권한 규칙이 둘로 갈리면 화면의 isAdmin과 서버 게이트가 언젠가 어긋난다.

반영 본체는 _apply_review로 뽑아냈다. 검토자의 [승인·반영]과 관리자의 바로 반영이 같은 함수를 탄다. “누가 눌렀는가”만 다르고 결과 — 3-way 병합, 검증 게이트, 다이어그램 동반 반영, 작업 완료 — 는 같아야 한다. 검증은 우회하지 않는다.

건너뛴 것은 곁가지들이다.

건너뛴 것이유
PR 브리지열자마자 머지하면 브랜치만 어지럽힌다
검토 Task 생성아무도 처리하지 않을 이슈로 남는다
검토자 인계인계할 상대가 자기 자신이다

전부 “이제부터 기다릴 검토”를 위한 준비다. 기다리지 않기로 했으면 준비도 필요 없다.

가장 늦게 발견한 건 실패 경로였다. 검증 게이트에 걸리면 방금 만든 리뷰를 거둬야 한다. 거두지 않으면 대상 산출물이 열린 리뷰에 잠긴 채라, 고쳐서 다시 시도해도 “이미 검토 요청 중”(409)에 막힌다. 실패가 다음 시도를 막는 구조였다.

화면은 모드 카드 둘([검토 요청] / [바로 반영])로 두고 기본값은 언제나 검토 요청으로 했다. 건너뛰기가 기본이면 실수로 반영되는 쪽이 잦아지는데, 되돌리려면 커밋을 되돌려야 한다.

테스트 4건으로 잠갔다.

test_apply_now_skips_review_wait          # 검토자=작성자=결정자
test_apply_now_rejects_reviewer_override  # body 의 reviewer 무시
test_apply_now_still_passes_validation_gate  # 422 뒤 잠긴 산출물도 대기 리뷰도 없음
test_apply_now_requires_admin_when_sso_on    # 일반 사용자 403 (일반 검토 요청은 그대로 가능)

결정 2 — 대상이 정해지지 않으면 화면이 먼저 묻는다

[+ 작업 만들기]가 구성 요소 상세에서 무반응이던 원인은 단순했다. 그 이벤트를 처리하는 리스너가 전체 보드(ImplDock)에만 있었는데, 구성 요소를 열면 그 판이 언마운트된다.

한 번 고친 뒤에도 문제가 남았다. “대상이 여럿이면 [연관 산출물]로 가서 행에서 고르라”고 떠넘겼기 때문이다. 버튼을 눌러도 작업은 한 걸음도 나가지 않는 건 그대로였다.

그래서 TaskTargetPicker를 만들어 대상이 하나로 정해지지 않는 모든 경우에 띄웠다. 기능·시퀀스·단위 시험을 한 목록에서 검색해 고르면 곧장 생성 모달로 이어진다. 명령 팔레트와 같은 문법(검색창 · ↑↓ · Enter · Esc)이라 새로 배울 게 없다.

전제 조건도 하나 걷어냈다. 기능 배정은 “코드가 생기기 전에 소속을 정하는” 계획일 뿐인데, 그것부터 하지 않으면 작업을 못 만들었다. 이제 배정 패널에서 기능을 고르기만 하면 [+ 작업]으로 바로 만들 수 있고, 만들면서 배정이 함께 기록된다.

대상 종류가 늘어나니 “무엇을 작업 내용으로 삼을지”가 종류마다 달라졌다. FunctionTaskModal에 규약 표를 뒀다.

대상작업 내용·완료 조건의 원천
기능기존 기능 필드
시퀀스description / remarks
단위 시험preconditions / expectedOutput / passCriteria

AI 제안·편집의 컨텍스트 스코프도 같은 표를 따른다.

시퀀스 초안에는 한 가지를 더 넣었다. 여러 구성 요소가 참여하는 흐름을 통째로 베끼면 “여기서 정확히 뭘 해야 하는지”가 메시지 더미에 묻힌다. 지금 보고 있는 구성 요소가 주고받는 메시지만 골라 세운다.

검토 중에 되돌린 결정도 하나 있다. 시퀀스·단위 시험에는 배정을 두지 않기로 했다.

participants[]와 component는 이미 그 산출물 자신의 정의다. 별도 배정을 얹으면 “산출물은 A라는데 배정은 B”처럼 같은 질문에 다른 답이 두 곳에 생긴다.

기능은 정의에 구성 요소 필드가 아예 없어(impl_map이 유일한 출처) 이 문제가 없다. 소속을 바꾸려면 그 필드 자체를 고치면 된다.

결정 3 — 폭은 스키마가 아니라 실제 데이터가 정한다

빈 description 하나가 한 줄을 통째로 먹는 문제의 뿌리는 스키마에 maxLength가 없다는 것이었다. 폼이 값의 크기를 알 길이 없다.

스키마를 고치는 대신 samples/*.json 15종의 문자열 길이를 집계했다. 이름 기반으로 서술 필드(description·preconditions·passCriteria …)는 전폭 또는 8칸, 코드·숫자 같은 짧은 제어값은 좁게 잡는다.

스키마가 못 알려 주는 것을 실제 데이터가 알려 준다.

권장 폭은 최소값으로만 쓰고, 행 끝에 남는 칸은 마지막 텍스트 필드가 받게 했다(packedSpans). 짧은 제어값은 늘리지 않는다 — 넓혀 봐야 시선만 멀어진다.

필드 순서는 짧은 제어값 → 이름·참조 → 서술·중첩으로 재배열하되, 같은 폭 안에서는 스키마 순서를 지킨다. 입력 도중 필드가 자리를 옮기면 손이 기억한 위치가 무너진다.

편집기 맨 위에는 구조 바(data-outline)를 세웠다. 현재 문서의 구조와 항목 규모를 먼저 보여 주고 원하는 영역으로 바로 이동한다. 큰 문서를 훑는 첫 동작이 스크롤이 아니게 된다.

배열 머리글에는 값과 동작을 얹어 접힌 채로도 내용이 읽히게 했다. 단 빈 식별자는 발급 UI가 필요해서 본문에 남겼다.

[이력 > 판 보기]도 같이 고쳤다. 객체 전체를 통으로 비교하면 바뀌지 않은 형제 필드까지 diff로 잡힌다. 실제로 달라진 말단 필드만 뽑도록 바꿨다.

결정 4 — 도구 창은 하나의 문법을 쓴다

[발행 > 개정·배포]와 [정합성 > 추적성 · 게이트 · 개정 영향성]에 같은 골격을 씌웠다. 판정·필터·단계 행동은 위 컨텍스트 바에 고정하고, 근거(문제 목록)는 아래에 둔다. 3분할 창은 목록/상세 경계를 또렷하게 하고, 아무것도 고르지 않았을 때의 안내는 중앙에 세운다.

[발행]은 분석 > 발행 > 산출물 3분할로 갈랐다. 여기서 실제 버그도 하나 정리했다. 파일명 → 산출물 코드 변환이 여기저기 흩어져 있어서 “셀렉터가 가리키는 것”과 “실제 생성된 파일”이 어긋날 수 있었다. 모듈 스코프 codeOf 하나로 고정했다.

전체 펼치기/접기는 아이콘·순서·접근성 문구가 화면마다 달랐다. ExpandCollapseActions 한 컴포넌트로 모으고, 각자 그리던 갈매기 아이콘은 지웠다. 좌측 트리에도 같은 동작을 붙였다(IntelliJ 프로젝트 뷰의 모두 접기/펼치기와 같은 감각).

[설정 > AI]는 저장 버튼을 없애고 자동 저장 폼으로 바꿨다. 마지막 입력 600ms 뒤에 반영한다. 여기서 조심할 게 하나 있었다.

느린 응답이 새 입력을 덮지 않으려면 요청 세대와 전송 당시의 draft를 함께 비교해야 한다. 세대만 보면 같은 세대 안에서 값이 바뀐 경우를 놓친다.

상태는 aria-live로 알린다. 저장 버튼이 없어진 만큼 “지금 저장됐다”를 화면 밖에서도 알 수 있어야 한다.

[설정 > Jira 연동]에는 상태 카드를 붙였다. 연결됨 / 설정됨-미연결 / 미설정 셋을 색과 기호로 구분하고, 미설정이면 무엇을 채워야 하는지(JIRA_OAUTH_CLIENT_ID …)를 번호 매긴 단계로 안내한다. 콜백 URL은 복사 버튼과 함께 뒀다 — 손으로 옮겨 적다 틀리는 값이다.

마지막으로 단계 전환 규칙을 넣었다. 구현 단계로 넘어갔는데 분석 단계 전용 도구 창이 열린 채면 그 화면이 지금 무엇을 보고 있는지가 흐려진다. 단계가 바뀌면 이전 단계 도구는 남기지 않는다. 대신 하단 도크는 높이를 끌어 조절하고 그 값을 기억한다.

이 커밋 하나가 15개 파일 +1,125/-426이고, 그중 theme.css가 +598줄이다. 문법을 통일한다는 건 결국 스타일 층에서 값을 치른다는 뜻이다.

덤 — 도구가 자기 데이터에서 잡아낸 것

낮에 FDSP sequence.json으로 작업 제안을 한 번 돌렸다. 나온 두 건이 둘 다 실제 불일치였다.

  • 리비전 이력 1.2.0에 “기간 통계(MON-002) 신설”이라 적혀 있는데 sequences 배열에 FDSP-SEQ-MON-002가 없다.
  • FDSP-SEQ-MON-001이 order 3에서 cached=true로 반환한 뒤 order 4~7의 개별 점검이 그대로 이어진다. 5초 캐시의 hit/miss 두 경로가 분기 표기 없이 평면으로 섞여 있다.

리비전에 적힌 것과 실제 배열이 어긋나는 건 사람이 훑어서는 잘 안 걸린다. 제안 근거를 “문서에서 실제로 확인한 항목 id”로 강제해 둔 게 여기서 값을 했다.

의도치 않은 함정 — 스케줄러는 조용히 실패한다

저녁에 블로그 자동화가 안 돈다는 걸 알아챘다. Windows 작업 스케줄러의 Claude Daily Blog Draft가 ~/.claude/daily-blog/run.ps1을 실행하는 구조인데, 그 안에 저장소 경로가 하드코딩돼 있었다.

[2026-08-18T09:17:22] starting daily-blog run (cwd=C:\Users\parkg\Git\park-gwimong\park-gwimong.github.io)
[2026-08-18T09:17:22] ERROR: blog repo not found at C:\Users\parkg\Git\park-gwimong\park-gwimong.github.io

저장소를 Documents\Git 아래로 옮긴 시점부터 매일 아침 이 두 줄만 찍고 끝나고 있었다. run.ps1의 $blogRepo와 ~/.claude/settings.json의 additionalDirectories 양쪽을 새 경로로 고쳤다.

스케줄러가 실패하면 아무도 안 본다. 실패 자체보다, 실패가 로그 파일 안에서만 일어난다는 게 문제다.

마무리

오늘 확인한 것은 세 가지다.

  • 절차를 건너뛰는 것과 기록을 건너뛰는 것은 다르다. 바로 반영은 리뷰 레코드를 그대로 만들고 검토자를 본인으로 못 박는다. 서버가 덮어쓰는 쪽으로 정한 이유는, 검증으로 막으면 “왜 막히지”가 되고 덮어쓰면 “기록은 사실대로”가 되기 때문이다.
  • 버튼이 조용한 건 기능이 없는 것보다 나쁘다. 리스너가 언마운트된 판에만 있었던 게 원인인데, 사용자에게는 아무 일도 안 일어나는 것으로만 보였다. 대상이 정해지지 않으면 화면이 먼저 묻게 하는 쪽으로 정리했다.
  • 스키마가 침묵하면 표본을 본다. 필드 폭을 실제 샘플 15종의 길이 분포로 정한 게, 스키마에 maxLength를 새로 박는 것보다 싸고 정확했다.

남은 것은 실패 경로 회수 로직이 다른 반영 경로에도 필요한지 확인하는 일, 그리고 오늘 통일한 도구 창 문법을 아직 손대지 않은 나머지 창에 마저 입히는 일이다.


Comments

Content