moyro
요약. moyro: 자율 개선 회차 20회, 릴리즈 8건. 최근 릴리즈 v0.2.28 (자산 1개). 건강 C
- 20회차
- 1프로젝트
- 7배포 준비 완료
- 1릴리즈 진행 중
- 2병합 완료
- 3검토 대기
- 5검증 실패
- 1변경 없음
- 1실행 오류
- $68.27비용
- 3시간 18분에이전트 시간
현황
- 저장소
- https://github.com/hkjang/moyro
- 마지막 회차
- 2026-09-12 20:18 KST — • 기타 review held, PR open PR #13
- 최근 릴리즈
- v0.2.28 — released · 자산 1개 (이전 v0.2.27: 1개) 전체 릴리즈 →
회차 이력
| 일시 | 프로젝트 | 결과 |
|---|---|---|
| 2026-09-12 20:18 | moyro | 검토 대기 review held, PR open PR #13 |
| 2026-09-11 06:44 | moyro | 검토 대기 review held, PR open PR #12 |
| 2026-09-10 21:21 | moyro | 병합 완료 merged PR #11, release hold (budget) |
| 2026-09-10 20:41 | moyro | 실행 오류 hold: budget |
| 2026-09-10 15:22 | moyro | 배포 준비 완료 merged PR #10, released v0.2.27 |
| 2026-09-10 00:13 | moyro | 검토 대기 guarded files, PR open PR #9 |
| 2026-09-09 10:30 | moyro | 배포 준비 완료 manual: merged PR #8, released v0.2.26 |
| 2026-09-09 08:52 | moyro | 검증 실패 verify failed: 실패한 검증: cd webapp && ([ -d node_modules ] || npm ci --no-audit --no-fund) && npm run typecheck --silent && npm |
| 2026-09-09 02:12 | moyro | 검증 실패 verify failed: 실패한 검증: cd webapp && ([ -d node_modules ] || npm ci --no-audit --no-fund) && npm run typecheck --silent && npm |
| 2026-09-08 21:20 | moyro | 검증 실패 verify failed: 실패한 검증: cd webapp && ([ -d node_modules ] || npm ci --no-audit --no-fund) && npm run typecheck --silent && npm |
| 2026-09-07 02:51 | moyro | 검증 실패 verify failed: 실행한 검증 명령이 없음 — 정책(verify) 또는 자동 감지 필요 |
| 2026-09-06 16:41 | moyro | 검증 실패 verify failed: 실행한 검증 명령이 없음 — 정책(verify) 또는 자동 감지 필요 |
| 2026-09-04 10:01 | moyro | 릴리즈 진행 중 merged PR #7, released v0.2.15, ASSETS MISSING |
| 2026-09-03 23:43 | moyro | 변경 없음 no change |
| 2026-09-03 16:10 | moyro | 병합 완료 merged PR #6, release missing |
| 2026-09-03 09:27 | moyro | 배포 준비 완료 merged PR #5, released v0.2.14 |
| 2026-09-03 03:44 | moyro | 배포 준비 완료 merged PR #4, released v0.2.13 |
| 2026-09-02 21:39 | moyro | 배포 준비 완료 merged PR #3, released v0.2.12 |
| 2026-09-02 21:20 | moyro | 배포 준비 완료 merged PR #2, released v0.2.11 |
| 2026-09-02 15:00 | moyro | 배포 준비 완료 merged PR #1, released v0.2.10 |
비용·사용량
| 시각 | 프로젝트 | 단계 | 시간 | 턴 | 비용 | 토큰 입력/출력 | 종료 |
|---|---|---|---|---|---|---|---|
| 20:18 | moyro | review | 6분 | 26 | $2.00 | 1.6M / 16K | success |
| 20:12 | moyro | 개선 | 18분 | 101 | $11.45 | 14.3M / 80K | success |
| 17:09 | moyro | 릴리즈 | 3분 | 26 | $1.41 | 1.3M / 10K | success |
| 06:44 | moyro | review | 7분 | 49 | $3.44 | 3.8M / 25K | success |
| 06:38 | moyro | 개선 | 18분 | 114 | $12.41 | 16.4M / 75K | success |
| 21:12 | moyro | review | 4분 | 23 | $1.36 | 1.0M / 15K | success |
| 21:08 | moyro | 개선 | 29분 | 84 | $5.77 | 7.2M / 43K | success |
| 14:52 | moyro | 릴리즈 | 3분 | 25 | $1.11 | 979K / 7K | success |
| 14:41 | moyro | review | 5분 | 39 | $1.79 | 1.8M / 17K | success |
| 14:36 | moyro | 개선 | 5분 | 41 | $1.96 | 1.9M / 18K | success |
| 00:12 | moyro | 개선 | 12분 | 72 | $3.83 | 3.9M / 35K | success |
| 10:00 | moyro | 릴리즈 | 3분 | 29 | $1.13 | 888K / 9K | success |
| 09:49 | moyro | review | 2분 | 11 | $0.55 | 288K / 9K | success |
| 09:46 | moyro | 개선 | 24분 | 28 | $1.39 | 1.1M / 15K | success |
| 08:51 | moyro | 개선 | 11분 | 62 | $3.68 | 3.9M / 30K | success |
| 02:12 | moyro | 개선 | 12분 | 62 | $3.76 | 3.9M / 35K | success |
| 21:19 | moyro | 개선 | 11분 | 60 | $3.37 | 3.3M / 33K | success |
| 02:51 | moyro | 개선 | 12분 | 70 | $3.93 | 4.1M / 34K | success |
| 16:41 | moyro | 개선 | 12분 | 61 | $3.94 | 4.1M / 36K | success |
아이디어 백로그 — 대기 13 / 전체 14
| 아이디어 | 가치/위험/크기 | 상태 | 메모 | 갱신 |
|---|---|---|---|---|
| getPreferenceByName이 모든 오류를 404로 뭉개 DB 장애를 '설정 없음'으로 위장 | 3/1/S | 대기 | compat_wave_handlers_early.go. errors.Is(pgx.ErrNoRows) 로 404/500 분리. 미머지 브랜치 auto/2026-09-07-0240 이 같은 파일을 손대므로 그 뒤에. | 2026-09-12 |
| post_reminders에 사용자당 상한이 없어 무한 적재 가능 | 3/1/S | 대기 | remind_at 상한(1년)도 함께. 미머지 브랜치 auto/2026-09-08-2109 가 reminders/service.go·worker.go 를 크게 고치므로 그 뒤에. | 2026-09-12 |
| sidebar 쓰기 경로: 비멤버 채널 허용 + 원소당 한 문장씩 도는 루프 | 3/2/M | 대기 | sidebar.Update/Create 가 비멤버 채널 ID 기록 허용(읽기 경로가 걸러 노출은 없음), replaceChannelsTx/UpdateOrder 를 unnest 단일 문장으로. 미머지 브랜치 충돌 없음, 통합 테스트 있음. | 2026-09-12 |
| 다이제스트가 채널의 미읽음 전체를 '멘션'으로 취급 | 3/3/L | 대기 | 게시 시점의 mentionedIDs 기록이 필요(스키마 변경). | 2026-09-12 |
| 커스텀 상태가 write-only — 저장되지만 어떤 API로도 다시 읽히지 않음 | 3/3/L | 대기 | auth.User 에 props 를 더하고 userColumns 쿼리 10여 개 수정. 공식 클라이언트 한정 이득. | 2026-09-12 |
| 메시지 목록 가상화 — 로드된 모든 행이 DOM에 남음 | 3/4/L | 대기 | 스크롤/점프/읽음 표시와 얽혀 회귀 위험이 커서 자율 세션에서는 보류. | 2026-09-12 |
| 방문 추적 후속: 'Momento 연결 확인' 버튼과 SPA 내부 이동 시 include_admin=false 준수 | 2/1/S | 대기 | 서버가 프록시로 tracker.js 를 HEAD 해 도달 여부를 알려 주는 검사 엔드포인트, 그리고 /today 에서 시작해 /admin 으로 내부 이동한 경우 이미 실린 스니펫이 남는 문제(SPA 특성)를 클라이언트가 pageview 를 끊는 식으로 보완. 가이드에 한계로 적어 둠. | 2026-09-12 |
| 지식 검색(/knowledge) 화면이 캡처 카탈로그에 없어 가이드에 그림 없이 글로만 설명됨 | 2/1/S | 대기 | 레일 10개 중 /knowledge 만 routedPages 에 없음. 이번 회차의 캡처 절차(docker build → 일회용 postgres/app → npx playwright test -g '<file>.jpg')로 스펙+expectedScreenshots+HTML 참조를 한 커밋에 늘리면 됨. | 2026-09-12 |
| postacks/savedposts 통합 테스트 보강 | 2/1/M | 대기 | 두 패키지 테스트 없음. ack 핸들러가 IsMember 오류를 `ok, _ :=` 로 삼켜 DB 장애가 403 으로 나가는 흠을 함께. | 2026-09-12 |
| 전달 중인 리마인더가 소유자에게 보이지도 지워지지도 않음 | 2/1/S | 대기 | ListPending/Delete 가 delivered_at=0 만 봄. auto/2026-09-08-2109 의 리스 도입에 의존. | 2026-09-12 |
| scripts/verify-product-ui.sh 에 관리자 비밀번호·암호화 키가 글자로 적혀 있음 | 2/2/S | 대기 | admin_password/encryption_key 상수. 일회용 컨테이너 전용이지만 GUIDE-STANDARD 위반. CI 워크플로와 release gate 가 같은 스크립트를 부르므로 함께 고칠 것. | 2026-09-12 |
| manual 상태가 영구히 고정돼 사용자가 자동 프레즌스로 돌아갈 수 없음 | 2/2/S | 대기 | Mattermost 규칙(online 선택 시 manual 해제)을 따를지 결정 필요. 미머지 브랜치 auto/2026-09-09-0201 뒤에. | 2026-09-12 |
| webapp e2e 스펙이 lint 를 전혀 받지 않음 (ESLint 자체가 없음) | 2/2/M | 대기 | eslint + @typescript-eslint + eslint-plugin-playwright 를 좁은 규칙으로 시작. 참고: 맨 `npx vitest run` 은 e2e 스펙까지 주워 5개 파일이 '실패'로 보이므로 항상 `npm test`(src 스코프)를 쓸 것. | 2026-09-12 |
| 캠페인 tracking-2026-09: 관리자가 화면에서 방문 추적 스크립트를 붙이는 체계(nonce CSP·Momento 프록시·차단 출처 기록) | 5/2/M | 완료 | server/internal/tracking + webui nonce 주입 + settings 'tracking' 섹션 + /momento/* 프록시 + csp-report 수신 + /admin/tracking 화면 + ADMIN_GUIDE 3.9 + PDF. 기본 꺼짐, 'unsafe-inline' 없음. 실제 이미지로 라이브 검증. | 2026-09-12 |
교훈 (깨졌던 변경)
- 2026-09-09 verify-false-positive — webapp/tsconfig.json 에 "types" 가 없어 node_modules/@types/* 가 전부 스코프에 들어온다. @types/node 가 끼면 window.setInterval 이 Node 의 Timeout 반환으로 해석돼 useDraft.test.tsx:196 이 TS2345 로 깨진다. main 은 깨끗하다(clean npm ci 후 typecheck exit 0). webapp 을 건드리지 않은 Go 전용 회차도 이 오류로 폐기되므로, 이 오류가 보이면 자기 변경 탓으로 여기지 말 것.
원장 (에이전트가 남긴 기록)
2026-09-02
- 선택: 링크 프리뷰 SSRF 가드의 DNS 리바인딩 취약점 수정 및 links 패키지 첫 테스트 추가 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
server/internal/links의safeDialContext가 호스트를 조회해 모든 응답 주소를 검사한 뒤 원래 호스트명을net.Dialer에 그대로 넘겨 DNS를 두 번 해석했다. 리바인딩 응답이 검사 시점에는 공인 IP, 연결 시점에는 127.0.0.1을 반환하면 인증 사용자용/api/v4/link_preview_image프록시가 루프백·사설망으로 유도될 수 있었으므로, 검증한 주소로 직접 다이얼하도록dialTargets를 분리하고 CGNAT·192.0.0.0/24·멀티캐스트·예약 대역을 차단 목록에 추가했다. 테스트가 하나도 없던 패키지에 URL 추출, OpenGraph 파싱, 주소 정책, 주소 고정, 캐시 축출 테스트 12개를 추가했고go vet ./...,go test -race ./...(44 패키지 통과),scripts/check-source-sizes.sh로 검증했다. 웹 변경이 없어 webapp 빌드는 손대지 않았다. - 보류 아이디어: (1) 로드맵의 create-post 인가·멤버십 2회 쿼리를 단일 쿼리로 병합 — PostgreSQL 통합 테스트 환경이 필요해 보류. (2)
ratelimit.Limiter.Middleware가 JSON 본문을http.Error로 써서 Content-Type이 text/plain으로 나가는 문제. (3) 테스트가 전혀 없는invites/sidebar/userstatus/postacks패키지의 단위 테스트 보강. (4) 로드맵의 메시지 목록 가상화(로드된 모든 행이 DOM에 남음) — 작업량 L이라 단일 세션 범위 초과. - 릴리즈: v0.2.10 (2026-09-02)
2026-09-02
- 선택: @멘션 추출을 Mattermost 규칙에 맞춰 수정 (문장부호·대소문자·이메일 오탐) (가치 4 / 위험 2 / 작업량 S)
- 결과: 성공
- 요약:
postcommand.ExtractMentions가 정규식@([a-zA-Z0-9._-]+)결과를 그대로 사용자명 조회에 넘겨서, 문장 끝 멘션(cc @alice.)은 “alice.”로 조회돼 아무도 알림을 받지 못했고@Alice같은 대소문자 차이도 registration이 소문자로 저장하는 사용자명과 어긋나 실패했다. 반대로 이메일 주소(ops@example.com)는 “example.com”이라는 유령 후보를 만들어 매 게시마다 불필요한 조회를 유발했다. 정규식 앞에\B와 영숫자 시작 조건을 추가해 단어 중간@를 제외하고, 후보에 뒤쪽._-를 제거한 형태와 소문자 형태를 함께 실어 보내며 후보 수를 200개로 제한했다.mentions_test.go에 표 기반 테스트 8건과 상한 테스트를,service_test.go에 종단 해석 테스트 1건을 추가했고go vet ./...,go test -race ./...(전 패키지 통과),scripts/check-source-sizes.sh로 검증했다. 웹 변경이 없어 webapp 빌드는 손대지 않았다. - 보류 아이디어: (1)
ratelimit.Limiter.Middleware가 JSON 본문을http.Error로 써서 Content-Type이 text/plain으로 나가는 문제. (2) 테스트가 전혀 없는invites/sidebar/userstatus/postacks패키지의 단위 테스트 보강 — PostgreSQL 통합 환경 필요. (3) 로드맵의 create-post 인가·멤버십 2회 쿼리를 단일 쿼리로 병합. (4) 로드맵의 메시지 목록 가상화 — 작업량 L이라 단일 세션 범위 초과. - 릴리즈: v0.2.11 (2026-09-02)
2026-09-02
- 선택: 429 응답을 JSON Content-Type과 실제 대기 시간으로 교정 (가치 4 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
ratelimit.Limiter.Middleware가 JSON 문자열을http.Error로 내보내 Content-Type이 text/plain으로 나갔고, 버킷 속도와 무관하게Retry-After: 1을 고정 반환했다. 회원가입 버킷은 5초에 토큰 하나를 채우므로 거절된 클라이언트가 4초 일찍 재시도해 헛된 거절을 네 번 더 받는 구조였다.take()가 판정과 함께 버킷 상태를 돌려주도록 나눠 남은 토큰과 설정된 rate 기반 대기 시간을 계산하고, 응답에 공용 API 오류 봉투와 Mattermost 호환X-Ratelimit-Limit/Remaining/Reset헤더를 실었다. 또 버킷 정리(GC)를 판정보다 앞으로 옮겨 계속 throttle 상태인 키가 다른 버킷을 메모리에 붙잡아두지 못하게 했다. 미들웨어·봉투·재시도 지연·빈 키 우회·정리 테스트 6건을 추가했고go vet ./...,go test -race ./...(전 패키지 통과),scripts/check-source-sizes.sh로 검증했다. 웹 변경이 없어 webapp 빌드는 손대지 않았다. - 보류 아이디어: (1) 테스트가 전혀 없는
invites/sidebar/userstatus/postacks패키지의 단위 테스트 보강 — 대부분 DB 경로라 PostgreSQL 통합 환경 필요. (2)invites.normalizeChannelIDs같은 순수 함수만 골라 단위 테스트 추가. (3) 로드맵의 create-post 인가·멤버십 2회 쿼리를 단일 쿼리로 병합. (4) 로드맵의 메시지 목록 가상화 — 작업량 L이라 단일 세션 범위 초과. - 릴리즈: v0.2.12 (2026-09-02)
2026-09-03
- 선택: 커스텀 이모지 검색 누락·
emoji/namesN+1·삭제된 이름 재사용 불가 수정 (가치 4 / 위험 2 / 작업량 M) - 결과: 성공
- 요약:
writeEmojiSearch가 최신 200개만 가져와 Go에서strings.Contains로 걸러서, 커스텀 이모지가 한 페이지를 넘는 워크스페이스에서는 오래된 이모지가 자동완성·검색 양쪽에서 영구히 사라졌다. 매칭을 DB로 옮기고LIKE대신strpos를 써서 사용자가 입력한%·_가 와일드카드로 새지 않게 했으며, 요청당 최대 200회 순차 쿼리를 돌던POST /emoji/names는= ANY($1::text[])단일 쿼리로 바꾸고 요청 순서를 유지했다. 또 v0.1 베이스라인이emojis.name에 테이블 전역 UNIQUE를 걸어둔 탓에 소프트 삭제된 이모지 이름을 다시 등록하면 라이브 전용 충돌 검사는 통과하고 업로드까지 끝난 뒤 INSERT만 실패해 이름이 영구히 잠기고 매 시도마다 고아 파일이 남았으므로, 마이그레이션 000017로 제약을delete_at=0부분 유니크 인덱스로 교체했다. 테스트가 없던emojis패키지에 통합 테스트 5건과store에 마이그레이션 업그레이드 테스트 1건을 추가하고 CI PostgreSQL 잡 대상에./internal/emojis를 넣었다. 로컬 postgres:16 컨테이너를 띄워go vet ./...,MOYRO_TEST_POSTGRES_DSN설정 후go test -race -p 1 ./...(전 패키지 통과),scripts/check-source-sizes.sh로 검증했다. 웹 변경이 없어 webapp 빌드는 손대지 않았다. - 보류 아이디어: (1)
emojis.Create가 크기 초과 파일을 업로드한 뒤에야 거절해 고아 file_infos 행을 남기는 문제 — files.Service에 정리 경로가 필요. (2) 테스트가 전혀 없는invites/sidebar/userstatus/postacks패키지의 통합 테스트 보강. (3) 로드맵의 create-post 인가·멤버십 2회 쿼리를 단일 쿼리로 병합. (4) 로드맵의 메시지 목록 가상화 — 작업량 L이라 단일 세션 범위 초과. - 릴리즈: v0.2.13 (2026-09-03)
2026-09-03
- 선택: 거절된 커스텀 이모지 업로드가 남기는 고아 파일 제거 및 업로드 MIME 스니핑 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
emojis.Create가 256KB 상한을files.Service.Upload가 돌려준 FileInfo로 검사해서, 초과 이미지는 이미 스토리지에 기록되고file_infos행까지 생성된 뒤에야ErrTooLarge로 거절됐다. 그 뒤 아무도 참조하지 않고 회수 경로도 없어, 로그인한 사용자라면 누구나 멀티파트 1MiB 한도 아래의 초과 이모지를 반복 업로드해 수백 KB씩 스토리지를 무한히 늘릴 수 있었다. 이미지를io.LimitReader(body, maxEmojiSize+1)로 먼저 버퍼링해 업로드 전에 상한을 판정하도록 순서를 뒤집고, 클라이언트가 마음대로 보내는 멀티파트 Content-Type 대신 매직 바이트를http.DetectContentType으로 스니핑한 결과를 저장 MIME으로 삼았다(같은 오리진에서 되돌려주는 바이트라 선언값을 믿을 이유가 없다). 바이트 저장 이후에만 도달 가능한 중단 경로 — 업로드 전 충돌 검사를 통과한 shortcode 경합 — 를 위해files.Service.Discard를 추가했고(첨부된 파일은 건드리지 않고, 없는 id는 무해한 no-op), 리사이즈 도중 행이 사라진 썸네일도 회수하도록generateThumbnailAsync가 UPDATE의 RowsAffected를 확인하게 했다.emojis에 상한 경계·스니핑·경합 회수 통합 테스트 3건,files에 Discard 테스트 1건을 추가했다. 로컬 postgres:16 컨테이너를 띄워go vet ./...,MOYRO_TEST_POSTGRES_DSN설정 후go test -race -p 1 ./...(전 패키지 통과),scripts/check-source-sizes.sh로 검증했고, 정리 코드를 잠시 제거해 경합 회수 테스트가 실제로 실패하는 것까지 확인했다. 웹 변경이 없어 webapp 빌드는 손대지 않았다. - 보류 아이디어: (1) 테스트가 전혀 없는
invites/sidebar/userstatus/postacks패키지의 통합 테스트 보강. (2) 로드맵의 create-post 인가·멤버십 2회 쿼리를 단일 쿼리로 병합. (3)getEmojiImage가 사용자 업로드 바이트를X-Content-Type-Options: nosniff없이 서빙(전역 미들웨어 유무 확인 필요). (4) 로드맵의 메시지 목록 가상화 — 작업량 L이라 단일 세션 범위 초과. - 릴리즈: v0.2.14 (2026-09-03)
2026-09-03
- 선택: 사용자 업로드 바이트가 같은 오리진 문서로 렌더링되는 저장형 XSS 차단 (가치 5 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약: 파일 업로드는 멀티파트 파트 헤더의 Content-Type을 그대로
file_infos.mime_type에 저장하고(바이트 검사 없음), 바이트를 되돌려주는 라우트들이 그 값을 응답 Content-Type에 그대로 실었다./api/v4응답에는 CSP가 없어(CSP는 webui 핸들러가 SPA 문서에만 붙인다) 아무 멤버나<script>가 든 SVG를image/svg+xml로 올린 뒤/files/{id}/preview나/users/{id}/image링크를 건네면 피해자 세션으로 moyro 오리진에서 스크립트가 실행됐다. 링크 프리뷰 프록시도FetchImage가image/접두사만 검사해 원격 호스트의 SVG를 우리 오리진으로 재생해주는 같은 구멍이 있었다.usercontent.go에 SVG를 제외한 래스터 허용목록 하나를 두고 모든 바이트 서빙 경로를 단일 정책(nosniff 항상, 허용목록만 인라인, 나머지는 불투명 attachment)으로 통일했다 —/preview는 비래스터에 원본 폴백 대신 404, 프로필 이미지 업로드는image/*대신 같은 허용목록, 프록시는 스크립트 가능한 상류 타입에 502. 더불어Content-Disposition이 저장된 이름을 그대로 넣던 문제(files.sanitize는 경로 구분자만 제거하므로a";filename="evil.html같은 이름이 따옴표 문자열을 닫고 파라미터를 덧붙일 수 있었다)를 인용 가능한 ASCII로 축약 +filename*병기로 고쳤고, 같은 결함이 있던 컴플라이언스 리포트 다운로드의 reportID 경로 세그먼트도 함께 처리했다. 허용목록·Disposition 이스케이프·헤더 정책 순수 단위 테스트 3건과 SVG 업로드가 preview/avatar/download 어디로도 인라인 렌더되지 않음을 확인하는 PostgreSQL 통합 테스트 1건을 추가했다. 로컬 postgres:16 컨테이너를 띄워go vet ./...,MOYRO_TEST_POSTGRES_DSN설정 후go test -race -p 1 ./...(전 패키지 통과),scripts/check-source-sizes.sh로 검증했고, 수정 전 게이트로 되돌려 통합 테스트가 실제로 200 + SVG 본문을 잡아내며 실패하는 것까지 확인했다. 웹 변경이 없어 webapp 빌드는 손대지 않았다. - 보류 아이디어: (1) 테스트가 전혀 없는
invites/sidebar/userstatus/postacks패키지의 통합 테스트 보강. (2)userstatus.Get이 실제 DB 오류까지 삼키고 offline을 반환해 장애가 “전원 오프라인”으로 위장되는 문제. (3) 로드맵의 create-post 인가·멤버십 2회 쿼리를 단일 쿼리로 병합. (4) 로드맵의 메시지 목록 가상화 — 작업량 L이라 단일 세션 범위 초과.
2026-09-04
- 선택: 사이드바 카테고리가 접근 불가 채널·타 팀 즐겨찾기를 계속 노출하는 문제 수정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
sidebar.fillChannelIDs의 주석은 “매 호출마다channel_members에서 멤버십을 다시 계산해 오래된 행이 노출되지 않는다”고 선언했지만, 실제로는 자동 분류되는 나머지 채널에만 그 규칙을 적용했다.sidebar_category_channels행은 채널 아카이브(delete_at)나 멤버 제거로 지워지지 않으므로(FK는 채널 행 삭제에만 cascade) 사용자가 나간 채널·아카이브된 채널이 커스텀 카테고리에 영구히 고정됐고,favorite_channel프리퍼런스에는 팀 정보가 없어 한 팀에서 찍은 즐겨찾기가 모든 팀의 Favorites에 나타났다. 살아있는 멤버십 집합을 먼저 읽어 세 소스 전부를 그 집합으로 거르도록 바꾸고, 명시적 배치 기록을 요청된 카테고리 밖까지 확장해 기본 카테고리에 대한Get이 커스텀 카테고리가 이미 가진 채널을 다시 분류하거나 즐겨찾기가 중복 노출되지 않게 했다. 테스트가 하나도 없던sidebar패키지에 접근 상실·팀 간 즐겨찾기·중복 노출을 각각 잡는 PostgreSQL 통합 테스트 3건을 추가하고 CI PostgreSQL 잡 대상에./internal/sidebar를 넣었다. 로컬 postgres:16 컨테이너를 띄워go vet ./...,MOYRO_TEST_POSTGRES_DSN설정 후go test -race -p 1 ./...(전 패키지 통과),scripts/check-source-sizes.sh로 검증했고, 수정 전 코드로 되돌려 세 테스트가 모두 실제로 실패하는 것까지 확인했다. 웹 변경이 없어 webapp 빌드는 손대지 않았다. - 보류 아이디어: (1)
userstatus.Get이 실제 DB 오류까지 삼키고 offline을 반환해 장애가 “전원 오프라인”으로 위장되는 문제. (2) 테스트가 전혀 없는invites/postacks/savedposts/bookmarks패키지의 통합 테스트 보강. (3)sidebar.Update가 사용자가 멤버가 아닌 채널 ID도 카테고리에 쓰도록 허용(읽기 경로에서 걸러지지만 쓰기 시점 거부가 더 정확). (4) 로드맵의 메시지 목록 가상화 — 작업량 L이라 단일 세션 범위 초과. - 릴리즈: v0.2.15 (2026-09-04)
2026-09-06
- 선택: 채널 북마크 재정렬이 실제로 재정렬하지 않는 문제 수정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
bookmarks.Reorder의 주석은 “채널 전체를 조밀하고 충돌 없는 위치로 재배치한다”고 했지만 실제로는 끌어놓은 행 하나의sort_order만 썼다. 이미 그 위치를 가진 행과 값이 같아지고List의 tie-break가create_at이라 더 오래된 행이 계속 앞에 서므로, 아래쪽으로 끄는 드래그는 전부 조용히 제자리로 튕겼고 반복하면 중복·희소한 sort_order가 쌓여 이후 드래그까지 망가졌다. 이제 트랜잭션에서 살아있는 행을FOR UPDATE로 읽어 요청된 0-기반 인덱스에 끼워 넣고(범위 밖은 양끝으로 클램프) 한 번의 UPDATE로 0..n-1로 재번호를 매긴 뒤 같은 트랜잭션에서 정렬된 목록을 돌려주므로 동시 드래그가 뒤섞이지 않는다. HTTP 계층은{"sort_order": n}만 받았는데 공식 Mattermost 클라이언트는 이 라우트에 맨 정수를 POST하고 갱신된 북마크 목록을 응답으로 읽으므로, 두 본문 형태를 모두 받고 정렬된 목록을 응답·브로드캐스트하도록 맞췄다. 테스트가 하나도 없던bookmarks패키지에 중간 삽입·클램프·소프트삭제 행 케이스를 각각 잡는 PostgreSQL 통합 테스트 3건과 본문 디코더 표 테스트 1건을 추가하고 CI PostgreSQL 잡 대상에./internal/bookmarks를 넣었다. 로컬 postgres:16 컨테이너로go vet ./...,MOYRO_TEST_POSTGRES_DSN설정 후go test -race -p 1 ./...(전 패키지 통과),scripts/check-source-sizes.sh로 검증했고, 수정 전 구현으로 되돌려 통합 테스트 3건이 모두 실제로 실패하는 것까지 확인했다(첫 시도의 테스트는 옛 구현에서도 통과해 실패를 잡도록 다시 설계했다). 웹 변경이 없어 webapp 빌드는 손대지 않았다. - 보류 아이디어: (1) 커스텀 상태가 저장만 되고 어떤 API로도 다시 읽히지 않아(write-only) 새로고침하면 사라지는 문제 — 사용자 객체에 props를 실어야 해서 L. (2)
userstatus.Get이 실제 DB 오류까지 삼키고 offline을 반환해 장애가 “전원 오프라인”으로 위장되는 문제. (3)sidebar.Update가 사용자가 멤버가 아닌 채널 ID도 카테고리에 쓰도록 허용(읽기 경로에서 걸러지지만 쓰기 시점 거부가 더 정확). (4) 로드맵의 create-post 인가·멤버십 2회 쿼리를 단일 쿼리로 병합 — PostgreSQL 통합 환경 필요.
2026-09-07
- 선택: 클라이언트가 preferences 테이블에 무한정 적재할 수 있던 경로 차단 (가치 4 / 위험 1 / 작업량 M)
- 결과: 성공
- 요약:
preferences테이블에는 어떤 상한도 없었다 —value는 TEXT이고, 행은 (user_id, category, name) 키에 소유자 본인이나 사용자 삭제 캐스케이드 외에는 아무도 지우지 않는다. 그런데 두 쓰기 핸들러(PUT /users/{id}/preferences,POST .../preferences/delete)는MaxBytesReader없이 요청 본문을 곧바로 무제한[]Preference로 디코드했고, 서비스는 각 행의 user_id가 행위자와 같은지만 확인했다. 따라서 인증된 계정이면 누구나 임의 크기 배열을 서버 메모리에 버퍼링시킨 뒤 영구 저장할 수 있었고, 그 전량이 이후 매 로그인마다ListAll로 되읽히고 WS 브로드캐스트로 자기 소켓에 다시 실려 나갔다. Upsert/Delete가 공유하는Validate하나에 배치 크기·category/name/value 길이(바이트가 아니라 룬 기준이라 한국어 값이 4배 빨리 거절되지 않는다)·PostgreSQL이 TEXT에 저장 자체를 못 해 드라이버 500으로 새어나가던 NUL 바이트 거절을 넣었다. 상한은 Mattermost 자신의 32/32/2000보다 느슨하게 잡았는데, moyro는 채널을 36자 uuid로 식별하므로 32룬 name 상한이면favorite_channel이 통째로 거절되기 때문이다. Upsert는 기존 트랜잭션 안에서 계정의 행 수를 세어 사용자당 상한을 넘기는 배치를 통째로 롤백한다(한 행 넘긴 상태로 눌러앉지 않도록). 핸들러는 본문에MaxBytesReader를 씌우고, 뭉뚱그려 400으로 내보내던 두 실패 모드를 분리해 거절된 페이로드만 400, 실제 저장 실패는 500이 되게 했다. 테스트가 하나도 없던 패키지에 검증 표 테스트 2건과 PostgreSQL 통합 테스트 3건(상한·롤백·정상 경로)을 추가하고 CI PostgreSQL 잡 대상에./internal/preferences를 넣었다. 로컬 postgres:16-alpine 컨테이너로go vet ./...,MOYRO_TEST_POSTGRES_DSN설정 후go test -race -p 1 ./...(전 패키지 통과),scripts/check-source-sizes.sh로 검증했고, 검증 코드를 잠시 무력화해 통합 테스트 2건이 실제로 실패하는 것까지 확인했다. 웹 변경이 없어 webapp 빌드는 손대지 않았다. - 보류 아이디어: (1) 커스텀 상태가 write-only — 재확인 결과
userstatus.GetCustomStatus는 여전히 호출자가 없고 moyro 웹앱은 커스텀 상태를 읽지도 쓰지도 않아 이득이 공식 클라이언트 한정이라 가치를 3으로 내림. (2)userstatus.Get이 실제 DB 오류까지 삼키고 offline을 반환해 장애가 “전원 오프라인”으로 위장되는 문제. (3)getPreferenceByName이 모든 오류를 404로 뭉개 DB 장애를 “설정 없음”으로 위장하는 문제 — 이번 회차의 400/500 분리와 같은 부류. (4)sidebar.Update가 사용자가 멤버가 아닌 채널 ID도 카테고리에 쓰도록 허용. (5) 로드맵의 create-post 인가·멤버십 2회 쿼리를 단일 쿼리로 병합.
2026-09-08
- 선택: 리마인더 전달이 중간에 끊기면 알림이 영구히 묶이는 문제 수정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
reminders워커는 전달 중delivered_at을 -1로 뒤집고 브로드캐스트가 끝난 뒤에야 실제 시각을 찍었는데, 그 사이를 회수하는 장치가 없었다.fire()가 워커 컨텍스트에서 파생되므로 종료 중 tick이면MarkDelivered가 취소로 실패하고,fire()안의 패닉은 recover가 한 단계 위tick()에 있어 배치의 나머지를 같은 방식으로 묶는다. -1로 남은 행은ClaimDue(delivered_at=0만 조회)·ListPending·Delete어디서도 닿지 않아, 사용자는 알림을 받지 못하고 그 사실을 알거나 지울 방법도 없었다. 예약 게시물이 000003에서 이미 해결한 문제인데 reminders 패키지 주석만 “같은 claim 규율”이라고 주장하고 있었다. 마이그레이션 000018로post_reminders에 claimed_at/lease_until/claim_token/attempt_count를 추가하고 이미 -1인 행에는 마이그레이션 시각 기준 회수 리스를 부여했으며,ClaimDue가 만료된 리스를 다시 집고MarkDelivered는 claim token 범위로 좁혀 리스가 끝난 워커가 후임의 행을 덮어쓰지 못하게 했다. 재시도는 3회에서 멈추고 그 뒤에는 종료 상태로 찍는다. 로컬 postgres:16-alpine으로go vet ./...,MOYRO_TEST_POSTGRES_DSN설정 후go test -race -p 1 ./...(전 패키지 통과),scripts/check-source-sizes.sh로 검증했고, 회수 조건을 빼면 새 통합 테스트 3건이, 새 컬럼이 없으면 마이그레이션 업그레이드 테스트가 실제로 실패하는 것까지 확인했다. 웹 변경이 없어 webapp 빌드는 손대지 않았다. - 보류 아이디어: (1)
userstatus.Get이 실제 DB 오류까지 삼키고 offline을 반환해 장애가 “전원 오프라인”으로 위장되는 문제 — 같은 패키지GetMany·tos.GetForUser는 이미 pgx.ErrNoRows만 분기하므로 그 형태를 따르면 된다. (2)getPreferenceByName이 모든 오류를 404로 뭉개 DB 장애를 “설정 없음”으로 위장하는 문제. (3)sidebar.Update가 사용자가 멤버가 아닌 채널 ID도 카테고리에 쓰도록 허용. (4) 커스텀 상태가 write-only — 저장되지만 어떤 API로도 다시 읽히지 않음(작업량 L).
2026-09-09
- 선택: 재접속이 접속 중인 사용자를 offline로 굳히는 프레즌스 경합 수정 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
ws.Hub는 프레즌스를 엣지(첫 소켓 등록 / 마지막 소켓 해제)로만 알리면서 각 엣지를go cb(...)로 던졌다. 탭 새로고침이나 네트워크 blip은 disconnect와 connect 엣지를 연달아 만들고, 두 콜백이 각자 DB에 쓰기 때문에 disconnect가 나중에 도착하면 connect를 덮어썼다. 허브는 엣지만 보고하므로 이를 되돌릴 후속 신호가 없어, 지금 접속해 있는 사용자가 그 소켓이 살아 있는 내내 모두에게 offline로 보였다. 사용자별 notifier를 넣어 한 사용자에 대해 동시에 한 패스만 돌게 하고, 진행 중에 도착한 엣지들은 goroutine을 늘리는 대신 후속 패스 하나로 합치며, 각 패스가 자신을 큐에 넣은 엣지를 믿는 대신 허브의 현재 클라이언트 맵을 다시 읽게 해 합쳐진 버스트가 실제 상태로 수렴하게 했다(서로 다른 사용자끼리는 여전히 병렬 — 원래 goroutine을 쓴 이유). 같은 손상의 다른 절반인userstatus.Get도 고쳤다: 모든 오류를 offline 상태 합성으로 답해 DB 장애가 “가입한 적 없는 사용자”와 구분되지 않았고, 허브가 그대로 브로드캐스트하는SetAuto의 반환값이 소켓 접속과 동시에 offline를 알릴 수 있었다. 이제pgx.ErrNoRows만 기본값이 되고 나머지는 올라간다(호출자 3곳 모두 이미 오류를 처리하고 있었다). 테스트가 없던userstatus에 PostgreSQL 통합 테스트 2건,ws에 DB 없이 도는 프레즌스 순서·합병·사용자 간 비직렬화 테스트 3건을 추가했다. 로컬 postgres:16-alpine으로go vet ./...,MOYRO_TEST_POSTGRES_DSN설정 후go test -race -p 1 ./...(전 패키지 통과),scripts/check-source-sizes.sh로 검증했고, 수정 전 구현으로 되돌려 새 테스트 3건이 실제로 실패하는 것(재접속 후 마지막 쓰기가 “disconnect”, 테이블이 사라진 상태의 Get이 offline 반환)까지 확인했다. 웹 변경이 없어 webapp 빌드는 손대지 않았다. CI는 이미 DSN을 준 채./...를 돌리므로 워크플로 수정은 필요 없었다. - 보류 아이디어: (1)
getPreferenceByName이 모든 오류를 404로 뭉개 DB 장애를 “설정 없음”으로 위장 — 이번 회차userstatus.Get과 같은 부류. (2)post_reminders에 사용자당 상한이 없어 무한 적재 가능(remind_at 상한도 함께). (3)sidebar.Update가 사용자가 멤버가 아닌 채널 ID도 카테고리에 기록하도록 허용. (4)postacks/savedposts통합 테스트 보강 — 명확한 결함은 없고 순수 회귀 방지. (5) 커스텀 상태가 write-only — 저장되지만 어떤 API로도 다시 읽히지 않음(작업량 L).
2026-09-09
- 선택: 일간 다이제스트가 본인 글을 멘션으로 인용하고 한국어를 글자 중간에서 자르는 문제 수정 (가치 4 / 위험 1 / 작업량 M)
- 결과: 성공
- 요약:
digest.loadMentions가 mention_count>0인 채널의 미읽음 게시물을 전부 나열하면서 수신자 본인이 쓴 글까지 포함했다.channels.BumpUnread는 작성자 자신에게는 mention_count를 올리지 않으므로 본인 글은 배지가 세고 있는 대상이 될 수 없는데, 채널을 열지 않은 채 글을 쓰거나 과거 메시지를 “안 읽음”으로 표시하면 last_viewed_at 뒤에 남아 “아래 멘션이 도착했습니다” 아래에 자기 글이 인용됐다. 발췌는msg[:160]바이트 슬라이스라 한글(3바이트)이면 세 번 중 두 번 잘못된 UTF-8로 끝나 메일 클라이언트가 U+FFFD로 렌더링했고, 상한도 실제로는 160자가 아니라 53자였다 — 리마인더·활동 피드가 이미 쓰는 룬 기반·공백 정규화 헬퍼와 같은 형태로 바꿨다. 미읽음 경계를 이미 조인해둔 행 대신 상관 서브쿼리로 다시 읽던 부분도cm.last_viewed_at으로 단순화했다. 테스트가 하나도 없던 패키지에 발췌 표 테스트 1건과 PostgreSQL 통합 테스트 3건을 추가했다. 로컬 postgres:16-alpine으로go vet ./...,MOYRO_TEST_POSTGRES_DSN설정 후go test -race -p 1 ./...(전 패키지 통과),scripts/check-source-sizes.sh로 검증했고, 수정 전 구현으로 되돌려 통합 테스트 3건이 실제로 실패하는 것(본인 글이 목록에 포함, 발췌가 55룬에서 잘리고 마지막이 맨\xea)까지 확인했다. 웹 변경이 없어 webapp 빌드는 손대지 않았다. CI는 이미 DSN을 준 채./...를 돌리므로 워크플로 수정은 필요 없었다. - 보류 아이디어: (1)
getPreferenceByName이 모든 오류를 404로 뭉개 DB 장애를 “설정 없음”으로 위장 — 미머지 브랜치 auto/2026-09-07-0240이 같은 파일을 손대므로 그 뒤에. (2)post_reminders에 사용자당 상한이 없어 무한 적재 가능 — 미머지 브랜치 auto/2026-09-08-2109가 같은 파일을 크게 고침. (3)sidebar.Update가 사용자가 멤버가 아닌 채널 ID도 카테고리에 기록하도록 허용. (4) manual 상태가 영구히 고정돼 사용자가 자동 프레즌스로 돌아갈 수 없음 — 미머지 브랜치 auto/2026-09-09-0201이 userstatus를 손댐. (5) 다이제스트 후보 조회가 채널의 미읽음 전체를 “멘션”으로 취급 — 실제 @멘션만 골라내려면 멘션 사실을 게시 시점에 기록해야 해서 스키마 변경(L).
2026-09-09
- 선택: webapp 타입 스코프를 고정해 검증 오탐(TS2345)으로 회차가 폐기되는 문제 수정 (가치 5 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
webapp/tsconfig.json에"types"가 없어 tsc 가 찾을 수 있는 모든node_modules/@types/*를 전역 스코프에 넣었고, 그 탐색이 webapp 을 넘어 상위 디렉터리까지 올라갔다 — 실제 원인은 명세가 추정한webapp/node_modules/@types/node가 아니라/home/hkjang/node_modules/@types/node(검증 환경 홈)였고, 그래서 cleannpm ci로도 재현되면서 webapp 을 전혀 건드리지 않은 Go 전용 회차 3번이 같은 오류로 폐기됐다. 스코프를["vite/client"]로 고정했는데(src 는process·Buffer·node:*등 Node 전역을 전혀 참조하지 않아 이것만으로 충분하다), 필드가 다시 조용히 사라지지 않도록src/tsconfig.test.ts가드 테스트를 추가했다. 검증: 수정 후npm run typecheckexit 0,npm test45파일/189테스트 통과,npm run build성공. 수용 기준대로npm i -D --no-save @types/node로 일부러 넣은 상태에서도 typecheck exit 0 이었고,types를 도로 지우면 원래의useDraft.test.tsx(196,76) TS2345가 그대로 재현되고 가드 테스트 2건도 실패하는 것까지 확인했다.useDraft.test.tsx는 손대지 않았고skipLibCheck(기존에 이미 켜져 있던 값)·@ts-ignore·any를 새로 쓰지 않았으며 server(Go) 변경도 없다. -
보류 아이디어: (1)
getPreferenceByName이 모든 오류를 404로 뭉개 DB 장애를 “설정 없음”으로 위장 — 미머지 브랜치 auto/2026-09-07-0240 이 같은 파일을 손대므로 그 뒤에. (2)post_reminders에 사용자당 상한이 없어 무한 적재 가능(remind_at 상한도 함께). (3)sidebar.Update가 사용자가 멤버가 아닌 채널 ID도 카테고리에 기록하도록 허용. (4)webapp/e2e와vite.config.ts가include: ["src"]밖이라 어떤 타입체크도 받지 않음 — 별도 tsconfig 로 덮으면 되고, Node 전역이 필요하므로 이번 고정과 분리해야 한다. (5) 다이제스트가 채널의 미읽음 전체를 ‘멘션’으로 취급 — 스키마 변경이 필요해 L. - 릴리즈: v0.2.26 (2026-09-09, run 2026-09-09-092150-moyro-improve)
2026-09-10
- 선택: 검색어가 LIKE 패턴으로 해석되던 문제 수정 (가치 4 / 위험 1 / 작업량 M)
- 결과: 성공
- 요약: 사용자·채널·팀·파일·PAT·감사로그 검색이 모두 클라이언트가 보낸 term 을 그대로
LIKE/ILIKE패턴에 이어붙여,%는 “전부”,_는 “아무 글자 하나”,\는 “다음 글자 이스케이프” 로 새어 나갔다 — 맨%하나면 볼 수 있는 계정·채널·팀·파일 목록이 통째로 나오고(이메일 포함),kim_lead검색이kimxlead를,@team_notes가teamxnotes를 함께 잡았으며, 반대로 이름에 진짜%가 들어간 채널(“50% 할인”)은 이름을 그대로 쳐도 찾을 수 없었다.emojis는 예전 회차에 strpos 로 피해 갔지만 이 경로들은 대소문자 무시 prefix/contains 매칭이라 strpos 로 바꿀 수 없어 ILIKE 를 유지하고 term 을 이스케이프했다. 12개 call site 가 각자 와일드카드를 손으로 붙이던 것이 누락이 번진 원인이라store.EscapeLike와LikeContains/LikePrefix로 한 곳에 모았다(PostgreSQL 기본 LIKE 이스케이프가 백슬래시라 ESCAPE 절은 불필요). 파일 검색 핸들러 2곳은 SQL 안에서'%' || $1 || '%'로 붙이던 것을 완성된 패턴 바인딩으로 바꿨고,audit.List는 한 파라미터가 패턴 베이스와 “필터 없음” 센티널을 겸하고 있어 이스케이프된 패턴만 별도 파라미터로 분리했다. 이스케이퍼 표 테스트 2건과 채널 검색·멘션 자동완성·사용자 디렉터리에 대한 PostgreSQL 통합 테스트 3건을 추가했다(각각 와일드카드 해석이었다면 함께 걸렸을 미끼 행을 심어 둔다). 로컬 postgres:16-alpine 컨테이너로go vet ./...,MOYRO_TEST_POSTGRES_DSN설정 후go test -race -p 1 ./...(전 패키지 통과),scripts/check-source-sizes.sh로 검증했고, 이스케이퍼를 빈 Replacer 로 무력화해 새 테스트 5건이 모두 실제로 실패하는 것(맨%가 채널 4개·멤버 3명·계정 3개를 반환)까지 확인했다. 웹 변경이 없어 webapp 빌드는 손대지 않았다. - 보류 아이디어: (1)
webapp/e2e와vite.config.ts가include: ["src"]밖이라 어떤 타입체크도 받지 않음 — 별도 tsconfig 와@types/node명시가 필요. (2)getPreferenceByName이 모든 오류를 404로 뭉개 DB 장애를 “설정 없음”으로 위장 — 미머지 브랜치 auto/2026-09-07-0240 이 같은 파일을 손대므로 그 뒤에. (3)post_reminders에 사용자당 상한이 없어 무한 적재 가능 — 미머지 브랜치 auto/2026-09-08-2109 가 같은 파일을 크게 고침. (4)sidebar.Update가 사용자가 멤버가 아닌 채널 ID도 카테고리에 기록하도록 허용. (5)postacks/savedposts통합 테스트 보강 — 명확한 결함은 없고 순수 회귀 방지.
2026-09-10
- 선택: webapp/e2e 와 빌드 설정을 타입체크 범위 안으로 넣기 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
webapp/tsconfig.json이include: ["src"]라서 Playwright 스펙 5개와vite.config.ts/playwright.config.ts총 2,419줄이 어떤 타입체크도 받지 않았다 — Playwright 는 스펙을 트랜스파일만 하므로 이름이 바뀐 헬퍼나 잘못 쓴page.evaluate콜백은 CI 브라우저 잡이 늦게 깨져야 드러나거나(브라우저 잡까지 못 간 브랜치에서는) 아예 드러나지 않았다. 그 나머지 집합만 덮는tsconfig.node.json을 추가하고(types: ["node"]— 세 그룹 전부process.env·node:*를 쓴다)@types/node를 devDependency 로 명시했다(지난 회차가 앱 프로젝트의types를 고정해 둔 덕에 src 스코프는 오염되지 않으며, 상위 디렉터리의 떠도는 @types/node 를 주워 쓰지 않도록 명시가 필요하다). 켜자마자 실제 결함 1건이 잡혔다:product-pages.spec.ts가page.evaluate안에서window.__moyro_plugin_fetch_original__을 읽는데 그 선언은 e2e 프로젝트가 끌어오지 않는src/plugins/runtime.ts에 있어, Redux 스토어까지 딸려오는 import 대신e2e/globals.d.ts로 재선언했다.npm run typecheck이 두 프로젝트를 모두 돌게 하고 그 배선이 조용히 빠지지 않도록 기존src/tsconfig.test.ts가드에 4건을 더했다. 검증:npm run typecheckexit 0,npm test45파일/193테스트 통과(신규 4건 포함),npm run build성공,npm audit --omit=dev --audit-level=high및 전체 audit 모두 0건,scripts/check-source-sizes.sh통과,npx playwright test --list가 여전히 5파일 76테스트를 수집(새.d.ts는 testMatch 에 걸리지 않음). e2e 스펙에 일부러 타입 오류를 넣어 typecheck 가 실제로 잡는 것(TS2322)까지 확인했다. 서버(Go) 변경은 없다. -
보류 아이디어: (1)
getPreferenceByName이 모든 오류를 404로 뭉개 DB 장애를 ‘설정 없음’으로 위장 — 미머지 브랜치 auto/2026-09-07-0240 이 같은 파일을 손대므로 그 뒤에. (2)post_reminders에 사용자당 상한·remind_at 상한이 없어 무한 적재 가능 — 미머지 브랜치 auto/2026-09-08-2109 가 같은 파일을 크게 고침. (3)sidebar.Update가 사용자가 멤버가 아닌 채널 ID도 카테고리에 기록하도록 허용 — 미머지 브랜치가 없어 지금 바로 가능하나 가치 낮음. (4)postacks/savedposts통합 테스트 보강 — ack 핸들러가ok, _ :=로 IsMember 오류를 삼켜 DB 장애가 403 으로 나가는 흠을 함께 잡을 것. (5) manual 상태가 영구히 고정돼 자동 프레즌스로 못 돌아옴 — 미머지 브랜치 auto/2026-09-09-0201 이 userstatus 를 손댐. - 릴리즈: v0.2.27 (2026-09-10, run 2026-09-10-143108-moyro-improve)
2026-09-10
- 선택: 클라이언트가 보낸 컬렉션을 무제한으로 버퍼링하던 요청 본문에 상한 적용 (가치 3 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
internal/의json.NewDecoder(r.Body)117곳 중MaxBytesReader가 씌워진 것은 30곳뿐이었고, 상한이 없는 쪽에는 대량 id 배열·멤버십 배치·props 맵처럼 클라이언트가 크기를 정하는 컬렉션 본문이 그대로 들어 있었다. compat 핸들러 곳곳의if len(ids) > 200 { ids = ids[:200] }가드는 “무엇을 질의하는가”만 묶을 뿐 “무엇을 버퍼링하는가”는 묶지 못한다 — 그 줄이 실행될 시점에json.Decode는 이미 배열 전체를 힙에 올려 둔 상태다. 게다가 이 핸들러 여럿은 원소당 DB 왕복을 한 번씩 쓰는데(사이드바 카테고리 재정렬은 id 당 UPDATE 한 번, 팀 멤버 배치는 원소당 AddMember 한 번) 이 둘은 개수 상한조차 없었다. 그래서 인증 계정 하나가 요청 한 번으로 임의 크기의 힙과 임의 개수의 질의를 사올 수 있었다. 컬렉션형 본문 전부를 새decodeCollectionBody(1 MiB 상한, 초과분은 구문 오류가 아니라 413)로 보내고, 원소당 쓰기를 하는 경로에는 개수 상한도 걸어 절반만 적용되는 대신 배치를 거절하게 했다(읽기 계열 대량 조회는 Mattermost 호환을 위해 200 절단을 유지). 일부러 잘못된 본문을 관대하게 넘기는 compat 스텁들은decodeCappedBody로 읽기만 묶어 200 이 400 으로 바뀌지 않게 했다. 검증: 로컬 postgres:16-alpine 컨테이너로go vet ./...,MOYRO_TEST_POSTGRES_DSN설정 후go test -race -p 1 ./...(전 패키지 통과),scripts/check-source-sizes.sh통과. 상한을 빼면 끝나지 않는 배열을 읽는 새 테스트 2건이 실제로 걸리는 것(테스트 프로세스가 845초 동안 본문을 계속 버퍼링하다 타임아웃으로 죽음)과 개수 가드를 무력화하면 배치 거절 테스트가 깨지는 것까지 확인했다. 웹 변경이 없어 webapp 빌드는 손대지 않았다. 미머지 브랜치 auto/2026-09-07-0240 이 이미 상한을 거는 preferences 핸들러 2곳은 충돌을 피하려 건드리지 않았다. - 보류 아이디어: (1)
getPreferenceByName이 모든 오류를 404로 뭉개 DB 장애를 ‘설정 없음’ 으로 위장 — 미머지 브랜치 auto/2026-09-07-0240 이 같은 파일을 손대므로 그 뒤에. (2)post_reminders에 사용자당 상한·remind_at 상한이 없어 무한 적재 가능 — 미머지 브랜치 auto/2026-09-08-2109 가 같은 파일을 크게 고침. (3)sidebar.Update/Create가 사용자가 멤버가 아닌 채널 ID도 카테고리에 기록하도록 허용, 그리고replaceChannelsTx/UpdateOrder가 원소당 한 문장씩 도는 것을 unnest 한 문장으로 접을 것. (4)postacks/savedposts통합 테스트 보강 — ack 핸들러가ok, _ :=로 IsMember 오류를 삼켜 DB 장애가 403 으로 나가는 흠을 함께 잡을 것. (5) webapp e2e 스펙이 ESLint 를 전혀 받지 않아 await 누락 같은 Playwright 플레이크 원인이 잡히지 않음.
2026-09-11
- 선택: 사용자·관리자 가이드를 표준에 맞춰 Markdown+PDF 정본으로 완성 (가치 4 / 위험 1 / 작업량 M)
- 결과: 성공
- 요약: 이 저장소는 드물게 이미 실제 캡처 46장과 캡처 도구(
scripts/verify-product-ui.sh+webapp/e2e/product-pages.spec.ts)를 갖고 있었지만 가이드는 GitHub Pages용 HTML 두 장뿐이었고 표준이 요구하는docs/USER_GUIDE.md|pdf·docs/ADMIN_GUIDE.md|pdf가 없었다. 릴리즈와 같은 방법으로 v0.2.27 이미지를 직접 빌드해 일회용 PostgreSQL과 함께 띄우고 캡처 스펙을 돌려(54개 시나리오 전부 통과) 42장을 이번 회차 화면으로 다시 찍은 뒤, 그 그림만으로 두 가이드를 표준 목차대로 새로 썼다. 화면·탭·버튼 이름은 캡처를 눈으로 확인하고 소스에서 되읽어 맞췄고(레일 항목이 8개가 아니라 지식·자동화를 포함한 10개, 컨텍스트 패널 탭은 스레드·요약·최근 파일·고정·멤버·정보, 운영 현황 배지는 ‘확인 필요’가 아니라 ‘미확인’, 메시지 메뉴는 ‘작업으로 만들기’·’결정으로 기록’·’나중에 알림’·’대화에서 문서 만들기’), 환경 변수 표는server/internal/config/config.go에서, 역할·권한 표는 마이그레이션 000001에서,/healthz·/metrics·/api/v4/system/ping의 메서드는 라우터 등록 자리에서 읽어 만들었다. 막혔을 때 절과 장애 대응 절에는 코드에 실제로 있는 오류 문구만 실었다. PDF는 공용tools/guide/md2pdf.mjs로 구웠고(사용자 39쪽, 관리자 23쪽) 중간 HTML을 headless Chrome으로 렌더해 표지·표·그림·한국어가 깨지지 않은 것을 확인했다. 정본이 둘이 되지 않도록 README와docs/index.md링크를 Markdown으로 바꾸고 기존 사이트 페이지 두 장에는 정본을 가리키는 줄을 넣었다. 검증:node scripts/verify-pages.mjs(5 HTML·46 캡처·15 라우트·7 브랜드 PNG) 통과,scripts/check-source-sizes.sh통과, 캡처 스펙 54개 통과. 서버·웹 소스 변경은 없다. -
보류 아이디어: (1)
getPreferenceByName이 모든 오류를 404로 뭉개 DB 장애를 ‘설정 없음’으로 위장 — 미머지 브랜치 auto/2026-09-07-0240이 같은 파일을 손댐. (2)post_reminders에 사용자당 상한·remind_at 상한이 없어 무한 적재 가능 — 미머지 브랜치 auto/2026-09-08-2109가 같은 파일을 크게 고침. (3)sidebar.Update/Create가 비멤버 채널 ID도 카테고리에 기록하도록 허용하고,replaceChannelsTx/UpdateOrder가 원소당 한 문장씩 돌아 unnest 단일 문장으로 접을 여지가 있음. (4)postacks/savedposts통합 테스트 보강 — ack 핸들러가ok, _ :=로 IsMember 오류를 삼켜 DB 장애가 403으로 나감. (5) 지식 검색(/knowledge) 화면만 캡처 카탈로그에 없어 가이드에 그림 없이 글로만 설명됨 — 캡처 스펙·카탈로그·HTML 참조를 함께 늘리면 해결. - 릴리즈: v0.2.28 (2026-09-11, run 2026-09-11-170538-moyro-approve)
2026-09-12
- 선택: 캠페인 tracking-2026-09 — 관리자가 화면에서 방문 추적 스크립트를 붙이는 체계(nonce 기반 CSP·Momento 프록시·차단 출처 기록) (가치 5 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약: moyro 는 사람이 보는 SPA 를
internal/webui가script-src 'self' blob:으로 잠근 채 서빙하므로 스니펫을 붙일 길이 없었다. TRACKING-STANDARD 대로 새 패키지server/internal/tracking에 설정(enabled·provider·momento_url/site_id·momento_proxy·measurement_id·matomo_*·custom_snippet(8KB)·allowed_hosts·include_admin·placement)·검증·스니펫 렌더·모든<script>에 nonce 부착·스니펫 안 http(s) 출처 추출·와일드카드 허용 판정·100개 고리 버퍼 위반 기록기를 넣고,webui.Handler가 HTML 응답마다 nonce 를 한 번 정해 스니펫과 CSP 헤더가 같은 값을 쓰게 했다(켜진 동안만'nonce-…'·스니펫 출처·report-uri가 더해지고'unsafe-inline'은 절대 넣지 않으며, nonce 가 실린 페이지는 modtime 없이 보내 304 로 stale nonce 가 남지 않게 함;/admin·/settings는 include_admin 일 때만,/api등 예약 경로는 원래 정책 유지). 설정은 settings 저장소tracking섹션에 저장돼GET/PATCH /api/moyro/v1/admin/settings/tracking(manage_settings) 으로 편집되고 저장 즉시 다음 응답부터 적용된다. provider 첫 자리는 Momento 이고 기본값인 같은 오리진 프록시(/momento/*→ 수집기, Cookie·Authorization 제거, 256KB 본문 상한, 켜져 있지 않으면 404)를 쓰면 정책에 외부 출처가 하나도 들어가지 않는다. 브라우저 신고는 무인증POST /api/moyro/v1/tracking/csp-report(8KB, 추적 꺼져 있으면 버림)로 받고 관리 화면시스템 → 방문 추적(/admin/tracking) 이 막힌 출처·지시어를 표로 보여 주며 ‘허용’ 한 번으로 allowed_hosts 에 넣어 저장한다. 기본값은 꺼짐이라 새 설치는 아무것도 달라지지 않는다. 검증:go vet ./...,go test -race ./...49 패키지 통과(tracking 9건·webui 4건·httpapi 3건 신규), webapptypecheck·npm test46파일/198테스트(신규 5건)·build통과,scripts/check-source-sizes.sh통과. verify-pages 가 관리 내비 라우트마다 캡처를 강제하므로 v0.2.28 이미지를 직접 빌드해 일회용 PostgreSQL 과 띄우고 product-pages 스펙으로admin-tracking.jpg를 찍었고(node scripts/verify-pages.mjs47캡처·16라우트 통과), 같은 인스턴스에 curl 로 꺼짐→켜짐→꺼짐을 돌려 정책이 원래대로 좁아지는 것, nonce 가 응답마다 바뀌고 스니펫과 일치하는 것, /admin 에는 붙지 않는 것, 프록시가 켜진 동안만 답하는 것, 신고가 기록되는 것을 실제로 확인했다(첫 라이브 실행에서 fresh 설정의allowed_hosts가null로 나가 화면이 깨지는 것을 잡아 서버·클라이언트 양쪽을 고침).docs/ADMIN_GUIDE.md에 3.9 방문 추적(설정 표·Momento 권장 절차·CSP 설명·붙지 않는 곳)과 7.2 의 새 무인증 경로 2건을 더하고 PDF 를 다시 구웠으며, 사이트 가이드 HTML·갤러리·architecture.md 도 맞췄다. - 보류 아이디어: (1) 지식 검색(/knowledge) 화면이 캡처 카탈로그에 없어 가이드에 그림 없이 설명됨 — 이번에 만든 캡처 절차(이미지 빌드→일회용 스택→
-g단일 스펙)로 싸게 가능. (2) scripts/verify-product-ui.sh 의 관리자 비밀번호·암호화 키가 글자로 박혀 있음 — CI·release 워크플로와 함께 환경 변수로. (3)getPreferenceByName이 모든 오류를 404 로 뭉갬 — 미머지 브랜치 auto/2026-09-07-0240 뒤에. (4)post_reminders사용자당·remind_at 상한 — 미머지 브랜치 auto/2026-09-08-2109 뒤에. (5) 방문 추적 후속: 관리 화면에서 ‘Momento 연결 확인’(프록시로 tracker.js HEAD) 버튼과 SPA 내부 이동 시 include_admin=false 를 지키는 클라이언트 측 스니펫 비활성화.