입력 필드 범위 검증·안내 문구 전면 정비 — 전수 조사에서 원인 문구까지 (2026-08-31)
- 기간: 2026-08-31 (세션 1, 오너 지시 "입력필드 입력 범위와 엣지케이스들 전수 조사 + 범위 초과 시 적당한 안내메세지가 나가는지 확인, 온보딩 포함" → "D1~D5 전부 권장안대로 go, 끝까지 진행 후 사후검수")
- 랜딩: PR #981(묶음 1 = Phase 1~3) · PR #986(묶음 2 = Phase 4~5, 마이그레이션 20260831160000) — CI 랜딩 묶음당 1회 원칙 준수
- 설계서: 없음(이슈 #975 본문의 전수 조사 결과 + Phase 계획이 정본, "예상 효과·개선사항" 표 포함)
- 정본:
src/react/userErrorPolicy.ts(원인 문구 사전 — 이번 트랙으로 계획 전용에서 전 쓰기 경로 정본으로) ·src/react/features/onboarding/onboardingBodyValidation.ts(온보딩 검증 공유 정본) ·src/react/ui/shared/writeLimitsCopy.ts(입력 시점 안내용 상한 거울) ·docs/data/limits-registry.md - 도구: 없음
- 게이트:
tests/react/limitsRegistry.test.mjs+2(1RM·신체 3층 잠금, UI 거울 일치) ·onboardingBodyValidation.test.mjs(5) ·inputRangeGuards.test.mjs(9) ·groupTextSafety.test.mjs(6) ·repository.test.mjs/profileHandle.test.mjs각 +1 - 버그리포트: 없음(개별 버그가 아니라 전면 정비 트랙 — 결함 목록은 이슈 #975 조사 절)
- 계약: 마이그레이션
20260831160000_group_text_safety_and_parts_reps_v1.sql(그룹 채팅·보드 댓글 금칙어, parts[].reps 1,000→2,000) · limits-registry 쓰기 한도 4행 추가(1RM·체중·골격근·체지방률)
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 원인 문구 전달 수리(1RM·신체 지표·관리자 폼·아이디 금칙어) | ✅ PR #981 |
| Phase 2 | 모바일 온보딩 범위 검증·안내(데스크톱 정본 공유) | ✅ PR #981 |
| Phase 3 | 무효값 통과·조용한 소실 수리(시각·날짜·소수점) | ✅ PR #981 |
| Phase 4 | 입력 시점 사전 안내(세트·종목 상한 가드, 텍스트 상한, % 통일) | ✅ PR #986 |
| Phase 5 | 그룹·피드 표면(금칙어·원인 문구·잘림 신호·Enter 전송) | ✅ PR #986 |
| Phase 6 | 문서·정본 갱신(이 기록 + 장부 현행화) | ✅ 장부는 #981·#986에 동승, 기록은 본 PR |
1. 배경
8월 31일 오너가 서비스 출시를 앞두고 모든 입력창(온보딩 포함)의 입력 범위·엣지케이스 전수 조사와 "범위 초과 시 적당한 안내가 나가는지" 점검을 지시했다. 5개 영역(운동 기록·온보딩/프로필·계획 편집기·그룹/피드/검색·PR도구/관리자)을 병렬 조사해 125개 입력 위치를 전수 확인했고, 결과는 "서버 상한은 잘 정비돼 있으나(#938 한도 정본) 초과 시 안내가 나가는 곳이 극소수"였다.
2. 문제 제기
조사에서 관통 원인 2가지가 확인됐다.
원인 문구 전달 단절
서버·저장 계층에는 정확한 검사(때로는 정확한 한국어 문구까지)가 있는데, 화면 쪽 오류 처리(catch)가 이를 버리고 "다시 시도해 주세요"류 일반 문구로 덮었다. 사용자 C가 1RM에 9999를 입력하면 서버 계약의 "1RM은 0kg 초과 5,000kg 이하" 문구 대신 "다시 시도해 주세요"만 보였고, 게다가 저장 결과를 기다리지 않고 화면이 닫혀 입력값까지 사라졌다. 신규 가입자가 키 칸에 1750(mm 착각)을 넣으면 마지막 1RM 단계에서 원인 불명으로 가입이 실패했다(데스크톱 온보딩에는 문구가 전부 있는데 모바일만 없었다).
"조용한 처리"가 표준 패턴
maxLength 절삭·클램프에 카운터·힌트·알림이 전무했고, 무효값이 말없이 통과(모바일 세션 시트의 "99:99" 시각)하거나 말없이 사라졌다(데스크톱 시간 입력의 13시/75분, 거리 입력의 "1.2.3"). 세트 64/240 상한은 240세트를 다 작성한 뒤에야 거부됐다(한도 정본 원칙 1 위반).
3. 해결 방안
원칙 (오너 결정 D1~D5, 2026-08-31 — 전부 권장안 채택)
- D1 % 상한: 전 화면 150 안내·차단(#950 준용), 서버 300은 안전망 유지.
- D2 보드 이종 복합 동작별 reps: 1,000 → 2,000 정렬(마이그레이션 1건).
- D3 자유기록 본문: 1,000자 통일(데스크톱 500 상향) — 재편집 절삭 손실 제거.
- D4 닉네임: 24자(서버 정본)로 클라 양쪽 정렬.
- D5 그룹 채팅·보드 댓글 금칙어 필터: 포함(피드의 기존 서버 판정기 재사용).
접근
새 숫자를 발명하지 않는다 — 모든 값은 기존 정본(서버 CHECK·쓰기 계약)을 재사용하고, 부족한 것은 전달(코드 부착 → 문구 사전 → 관문 ① 경유)과 시점(저장 후 거부 → 입력·추가 시점 안내)이다. 문구·값 드리프트는 limitsRegistry 게이트로 잠근다.
4. 적용한 내용
Phase 1 — 원인 문구 전달 수리
1RM(LG_PR_VALUE_LIMIT)·신체 지표 3종(LG_BODY_*_LIMIT, 서버 CHECK와 동일 범위 사전검사)·프로필 사진 2종에 원인 코드를 달고 컨트롤러 catch가 관문 ①(reportedUserError)을 지나게 했다. PR 도구는 저장이 끝나고 성공했을 때만 화면을 닫는다(종전: 실패 토스트가 뜰 때 입력이 이미 소실). 관리자 카탈로그의 검증 문구 4종은 LG_ADMIN_FORM_INVALID 코드로 그대로 안내된다. 아이디 금칙어는 서버 문구에 "Handle"이 들어 있어 형식 오류로 오분류되던 것을 갈라 전용 문구를 달았다. 작업 중 드러난 것: 계획서의 Phase 1-1(완료 기록 상한 8종 문구)은 조사 직후 다른 세션의 PR #968로 이미 랜딩돼 있어 검증만 수행했다.
Phase 2 — 모바일 온보딩
데스크톱에 있던 검증·문구를 공유 모듈 onboardingBodyValidation.ts로 이사해 모바일 신체·1RM 스텝이 입력 즉시 안내한다(키 100~250 / 체중 20~400 / 골격근 5~120 / 체지방률 1~80 / 1RM ≤5,000). 아이디 거부(중복·형식·금칙어)는 아이디 입력 단계로 자동 복귀. 닉네임 24자 통일(D4). 사실과 다르던 힌트("중복 확인은 다음 단계에서 진행돼요" — 그런 단계 없음)를 수정했다.
Phase 3 — 무효값 통과·조용한 소실
모바일 세션 시트 시각에 0~23/0~59 검증+문구+저장 잠금. 데스크톱 시간 입력은 범위 밖 초안을 빨간 표시+문구로 드러낸다(종전: 화면엔 남고 값만 조용히 소실). 종료<시작은 양 플랫폼에서 "자정을 넘긴 운동으로 계산돼요" 안내. 데스크톱 PR 날짜 상한을 UTC → 로컬로(한국 새벽 0~9시 오늘 선택 불가 수리). 분:초·km 입력의 다중 소수점("1.2.3" → NaN → 값 소실)은 첫 소수점만 허용으로 수리(모바일·데스크톱). 의도적 생략: 지수 표기(1e5) 키 차단은 상한 사전검사가 실질 위험을 이미 막고 있어 19개 입력에 흩뿌리지 않았다.
Phase 4 — 입력 시점 사전 안내
세트 64/240·종목 48을 추가 버튼 시점에 안내(모바일 WorkoutFlow·데스크톱 PlanEditor). UI는 데이터 계층 import가 금지라 상한 거울 writeLimitsCopy.ts를 두고 쓰기 계약과의 일치를 게이트로 잠갔다. 상한 없던 텍스트 4곳에 상한 부여(제목 170자·메모/리뷰 1,365자·커스텀 종목명 80자 — 전부 서버 값 정합, 이모지 초과 극단은 Phase 1 문구가 안전망). % 상한 150 통일(D1), 자유기록 본문 1,000자 통일(D3). 잠긴 버튼 3곳에 이유 문구.
Phase 5 — 그룹·피드 표면
마이그레이션 20260831160000으로 그룹 채팅·보드 댓글에 금칙어 필터(D5)와 parts[].reps 2,000 정렬(D2, 행동 postcheck 동반). groupStore의 22023 일괄 "입력값을 확인해 주세요"를 원인별 문구 12종으로 세분화(계획서의 '클라 사전검사'는 서버 검증기 350줄 복제 대신 서버 문구→사용자 문구 매핑으로 갈음). 서버가 내려주던 잘림 신호(truncated)를 화면까지 배선. 라운지 채팅 Enter=전송(키보드 "보내기" 표시와 일치). 초대 검색의 검색 전/결과 없음 구분. 의도적 생략: 초대 검색 "검색 중" 표시(250ms 깜빡임)는 배선 비용 대비 이득이 작아 제외.
작업 중 드러난 것
- GitHub Actions 결제 문제로 CI가 잡 시작조차 못 하는 상태를 발견(PR #981) — "recent account payments have failed or your spending limit needs to be increased". 로컬에서
npm run check전 체인(eslint 포함 — 누락 모듈 @babel/core 임시 설치로 로컬 lint 복구)을 통과시킨 뒤 진행 지시에 따라 admin 머지. 오너 조치 필요: GitHub Settings → Billing & plans. - 마이그레이션 번호 20260831150000을 칼로리 원자 트랙(#932)이 같은 날 선점 — 랜딩 직렬화 절차(#966)의 재번호 도구로 160000 이동, 랜딩 잠금 절차 준수.
5. 적용 결과
| 항목 | 전 | 후 |
|---|---|---|
| 1RM 9999 입력 | "다시 시도해 주세요" + 화면 닫힘·입력 소실 | "0kg보다 크고 5,000kg 이하…" + 화면 유지·입력 보존 |
| 모바일 온보딩 키 1750 | 마지막 단계 원인 불명 가입 실패 | 해당 칸에서 즉시 "키는 100~250cm…" |
| 세션 시트 시각 99:99 | 검증 없이 저장 경로 통과 | 문구 + 저장 버튼 잠금 |
| 데스크톱 13시/75분 | 화면엔 남고 값만 조용히 소실 | 빨간 표시 + 문구 |
| 65번째 세트 추가 | 다 쓰고 저장할 때 거부 | 추가 버튼 시점 "최대 64개…" |
| 자유기록 700자 노트 데스크톱 재편집 | 첫 타이핑에 500자 절삭(데이터 손실) | 1,000자 통일로 보존 |
| 그룹 보드 22023 | 전부 "입력값을 확인해 주세요" | 원인별 문구 12종 |
| 그룹 채팅 금칙어 | 그대로 게시 | 피드와 동일 거부 + 문구 |
| 보드 복합 동작별 반복 | 1,001부터 원인 불명 거부 | 2,000까지(일반 세트와 동일) |
| 검증 | — | 스위트 2,220/2,220 · tsc 0 · 로컬 check 전 체인 통과. CI 미실행(결제 문제)·실기기 미확인 — 오너 사후검수 대기 |
6. 이번 개선으로 향상된 것
PUBLIC_ERROR_MESSAGES가 계획 전용에서 전 쓰기 경로의 원인 문구 정본이 됐다(1RM·신체·사진·아이디 금칙어 추가).- 온보딩 검증이 플랫폼별 사본이 아니라 공유 정본 1곳이 됐다 — 값·문구가 다시 갈라질 수 없다.
- 한도 값·안내 문구·UI 거울이 limitsRegistry 게이트로 3층 잠금 — 값만 바꾸고 문구를 잊는 드리프트가 CI에서 잡힌다.
- UGC 안전 정책(금칙어)이 표면(피드/그룹) 간 동일해졌다.
남은 것
- 오너 사후검수: 신설 문구 전문 검수 + 실기기 확인(이슈 #975 Phase별 보고에 문구 전문 첨부).
- GitHub Actions 결제 복구(오너) — 복구 전까지 모든 PR의 CI가 시작되지 않는다.
- 범위 밖 기록(이슈 #975 본문): 데스크톱 온보딩 아이디 입력 부재, 체지방 입력 단위 불일치(온보딩 % vs 프로필 kg), 그룹 화면 과거 내역 페이지네이션, 초대 검색 "검색 중" 표시.