관리자 종목 특성 한 줄 편집 — 수정 버튼이 없던 종목 상세에서 연필 + 선택지 모달까지 (2026-09-02)
- 기간: 2026-09-02 (세션 1개,
3feaf604-f4c8-4a2d-a319-21b18878f081). 오너 지시: "종목 관리자페이지에서 종목 특성들을 수정하는 기능을 넣어줄래? … 체중반영 계수는 그게 필요한 필드에서만 활성화… 스내치 같이 체중반영이 필요없는 종목은 값이 '0'이 아니라 '없음'" → "한줄씩 인라인 편집 가능하게 해주고, 선택가능한 옵션은 모달로 뜨게 해줘" - 랜딩: PR #1104(Phase 1~4,
618a19ac) · #1106(후속 BUG-056 1차,b9d2e4df) · #1108(BUG-056 2차,8bee17ed) · #1111(BUG-056 3차 최종 — 기본 select,5c5f0d9e) · #1117(필수 입력 식 파서,4a4aed7e) · #1118(경계 게이트 오탐 등재 forward-fix,605fd653) · #1123(식의 기록 필드 밖 항목 즉시 차단,abe1ddde). 마이그레이션·엣지 함수 0건(서버 무변경). 관리자 셸은 docs 프로젝트/admin/배포 - 설계서: 이슈 #1069 코멘트(2026-09-02, "예상 효과·개선사항" 절 포함)
- 정본:
DesktopAdminExercises.tsx의AdmExerciseDetail(행별 연필·모달 6종) ·DesktopAdminAddModal.tsx의AdmPickModal·adminController.tsadminExercisePayload의 "맨몸이 아니면 계수 0" 규칙 - 도구: 없음(브라우저 실측은 워크트리 일회용 vite 하네스, 커밋 안 함)
- 게이트:
adminExerciseTraitRows.test.mjs(렌더·컨트롤러 계약 5건) ·adminUiWriteContract.test.mjs계수 기대값 - 버그리포트:
bug-056-20260902.md— 타겟 부위 모달 안 부위 목록 잘림(후속 수리) - 계약:
designContract.ts에adminPickModal마커 등록
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 종목 특성 행별 연필 + 선택지 모달(분류·타겟 부위·기능 분류·기록 필드), 측정 단위 행 삭제 | ✅ PR #1104 (618a19ac) |
| Phase 2 | 체중 반영 계수 조건부 — 맨몸만 편집, 그 외 '없음', 컨트롤러 0 강제 | ✅ PR #1104 |
| Phase 3 | 필수 입력 선택형(기본값/모두/하나만/직접 입력) | ✅ PR #1104 |
| Phase 4 | 로컬 게이트·CI 1회·머지 | ✅ 유닛 2,306건 green |
| 후속 | 타겟 부위 모달 안 부위 콤보 목록 잘림 수리(BUG-056, 오너 보고 09-02) | ✅ PR #1106 (b9d2e4df) → 오너 정정("모달이 커지지 말고 위에 겹치게") → PR #1108 (8bee17ed, 포털 고정 위치 — 실화면 배율에서 어긋남) → 오너 "검색 없이 정해진 것 중 고르게" → PR #1111 (5c5f0d9e, 브라우저 기본 select, 최종). 전부 §18 CI 대기 없이 머지 |
| Phase 3-1 | 필수 입력 직접 입력을 사람이 읽는 식으로 — 시간 and (거리 or 칼로리) 파싱·역표시(오너 지시 09-02) | ✅ PR #1117 (4a4aed7e) + forward-fix #1118 (605fd653, 경계 게이트 오탐 등재 — #1117은 로컬 게이트 실패를 확인하지 않고 머지된 실수) + #1123 (abe1ddde, 그 종목이 기록하지 않는 항목은 오타처럼 즉시 차단·저장 잠금) |
1. 배경
v83 디자인 배송(#1041, 09-01)이 관리자 종목 상세의 상단 연필(종목 전체 수정 폼 진입)을 없앴다. 편집 폼(AdmInlineEditor)과 저장 경로(editAdminExercise → updateExercise)는 그대로 살아 있었지만, 폼을 여는 버튼이 없어 종목 특성을 화면에서 고칠 방법이 없었다. 이슈 #1069가 09-01에 "수정 버튼 복원"으로 등록됐고, 09-02 오너가 요구를 구체화했다 — 행별 인라인 편집, 옵션은 모달, 체중 반영 계수는 필요한 종목만.
2. 문제 제기
종목 특성을 고치려면 DB를 직접 만져야 했다
관리자가 '풀업'을 열면 분류·타겟 부위·기록 필드·체중 반영 계수가 표로 보이지만 어느 행도 편집이 안 됐다. 잘못 등록된 타겟 부위 한 줄을 바꾸려면 SQL로 가야 했다.
남아 있던 폼은 "종목 전체" 폼이라 요구와 맞지 않았다
종목명·별칭·소속 계열·식별자·특성이 한 화면에 다 있는 폼이었다. 이름·별칭은 이미 동의어 카드에서 편집하므로 중복이고, 오너가 원한 것은 특성 행 하나씩이었다.
체중 반영 계수 0의 뜻이 두 가지였다
'스내치'는 계수 0 = 해당 없음, '플랭크'는 계수 0 = 맨몸이지만 시간 기록이라 반영 안 함. 화면은 둘 다 0이라 구분이 안 됐다. Production 실측(09-02):
| 분류 | 활성 종목 | 계수 > 0 | 계수 = 0 |
|---|---|---|---|
| 맨몸 | 260 | 187 | 73(플랭크·L싯·핸드스탠드 홀드 등 시간/거리 기록) |
| 바벨·덤벨·머신·케틀벨·밴드·유산소·기타 | 535 | 0 | 535 |
| 미분류(외부 인입·커스텀) | 188 | 0 | 188 |
맨몸이 아닌데 계수가 있는 종목은 0건, 쓰이는 값은 0 · 0.25 · 0.5 · 0.75 · 1 다섯 가지뿐이다.
측정 단위는 별도 값이 아니었다
기록 필드에서 자동으로 정해지는 표시값(무게+횟수 → kg, 횟수 → 회, 시간 → 초)인데 표에 독립 행으로 있어 "수정 대상"처럼 보였다. 오너 확인(09-02) 후 행 삭제.
3. 해결 방안
원칙 (오너 결정, 2026-09-02)
- D1 맨몸이 아닌 종목의 계수 표시 = '없음' 행 유지(행 숨김 기각 — 표 모양이 종목마다 바뀌지 않고 이유가 바로 보인다).
- D2 계수 입력 = 고정 선택 0 · 0.25 · 0.5 · 0.75 · 1(자유 숫자 기각 — 실사용 값이 이 다섯뿐, 오타 방지).
- D3 필수 입력은 JSON 대신 선택형(기본값/모두/하나만) + 직접 입력(고급) — 오너 "옵션은 모달로" 지시에 포함.
- 편집 방식 = 행별 연필 + 선택지 모달(오너 지시). 종목 전체 폼 진입 복원안(이슈 원안)은 대체.
접근
| 대안 | 판단 |
|---|---|
| 상단 연필로 종목 전체 폼 복원(이슈 원안) | 기각 — 이름·별칭 중복, 한 번에 여러 행을 건드려 실수 범위가 넓다 |
| 행별 인라인 입력(표 안에서 바로 입력) | 기각 — 분류·부위처럼 선택지가 있는 값은 목록이 필요, 오너가 모달을 지정 |
| 행별 연필 → 선택지 모달 → 즉시 저장 | 채택 — 바꾼 키 하나만 기존 저장 경로로 보낸다(컨트롤러가 나머지 필드를 기존 값으로 채움), 서버 무변경 |
| 계수 규칙을 화면에서만 처리 | 기각 — 등록 폼·다른 경로도 있으므로 컨트롤러에서 "맨몸이 아니면 0"을 강제 |
4. 적용한 내용
Phase 1~3 — 행별 편집·계수 조건부·필수 입력 선택형 (#1104)
DesktopAdminAddModal.tsx:AdmPickModal신설(단일/다중 선택,min~max, 옵션별 힌트,extra·validate).AdmAddModal에validate옵션(타겟 부위 합계 100% 검사).DesktopAdminExercises.tsx:AdmExerciseDetail의 종목 특성 표를 행별 연필 구조로 교체, 모달 6종 배선. 측정 단위 행 삭제. 종목 전체 폼(AdmInlineEditor) 진입 코드·초기값 조립 제거. 체중 반영 계수 표시 3분기(없음 /0 (체중 반영 안 함)/ 숫자). 기록 필드를 줄여 논리식 원자가 밖으로 나가면reqinputs: ""를 함께 보내 기본값으로 되돌림(서버 CHECKrequired_inputs_valid_v1거부 예방).adminController.ts:adminExercisePayload에 "분류가 맨몸이 아니면bodyweightFactor = 0" 규칙.designContract.ts:adminPickModal마커.desktop-admin.css:.adm-trait-cell·.adm-pick-*9규칙.- 테스트:
adminExerciseTraitRows.test.mjs신설,adminUiWriteContract.test.mjs의 바벨 종목 계수 기대값"0"→0.
Phase 3-1 — 필수 입력 식 파서 (#1117, forward-fix #1118)
requirementExpression.ts신설: 항목 이름 한글·영문(시간/duration/time …), and·or(그리고·또는·&·|·+), 괄호, and 우선,{로 시작하면 JSON(구 입력 호환). 결과는 기존 논리식으로 정규화(깊이 3·묶음 8 제한은normalizeRecordingRequirement재사용). 역방향formatRequirementExpression으로 모달을 열면 현재 규칙이 같은 식으로 보인다. 틀린 식은 원인 문구를 입력 아래 즉시 표시하고 저장을 잠근다. 테스트 6건(requirementExpression.test.mjs).- #1123:
parseRequirementExpression(text, allowedAtoms)— 식은 맞아도 그 종목의 기록 필드 밖 항목(예: 풀업에 시간)이면시간은(는) 이 종목의 기록 필드에 없습니다. 이 종목이 기록하는 항목: 횟수를 입력 아래 즉시 보여주고 저장을 잠근다(오너: 오타와 똑같이). 컨트롤러의 기존 검사는 2중 안전으로 유지. - forward-fix #1118: TypeScript 경계 게이트가 논리식 데이터 키
any를any토큰으로 오탐(#947의recordingRequirement.ts와 같은 사례) — 검토 목록에 사유 등재.
주요 결정과 그 근거
- 바꾼 키만 전송: 컨트롤러
adminExercisePayload가 폼에 없는 키를 기존 값(fallback)으로 채우는 계약이 이미 있어(#1060) 행 단위 저장이 추가 코드 없이 성립. 별칭은 기존 값을 그대로 다시 쓰므로 되덮기 없음(대표 표기 별칭이 정본). - 계수 규칙은 컨트롤러에: 화면에서 연필을 숨기는 것만으로는 등록 폼·외부 경로를 막지 못한다.
equipment:앵커 유지:exerciseCatalogClassification.test.mjs가equipment: adminEquipmentKey(소스 앵커를 단언 — 계수 규칙은writeValues조립 뒤 한 줄로 덧붙였다.
작업 중 드러난 것
- 랜딩 체인은 게이트 EXIT=0에 조건부여야 한다 — #1117에서 백그라운드 게이트 로그를 grep으로 "출력만" 하고
&&로 잇지 않아 EXIT=1인 채 커밋·머지가 진행됐다(경계 게이트 오탐이라 실코드는 정상, #1118로 즉시 수리). 이후 체인은grep -q EXIT=0을 조건으로 건다. - 관리자 화면은 소셜 로그인 장벽으로 로컬 실물 확인이 안 된다 — props-in 하네스(
_traitprobe.html/tsx+vite.admin.config.mjs, 포트 5183)로 모달·저장 호출을 실측했다. launch.json 항목은cmd /c cd …형식이 실패해node <wt>/node_modules/vite/bin/vite.js --config <wt>/vite.admin.config.mjs로 직접 지정. - 선택 목록은 직접 띄우지 말고 기본 select·AdmPickModal로 — 절대 위치(잘림)·포털 고정 위치(배율 어긋남) 두 번 실패, 하네스는 배율이 없어 둘째를 못 잡았다.
- 모달 안 콤보는 열어서 실측할 것 — 타겟 부위 모달은 텍스트만 확인해 목록 잘림(BUG-056)을 놓쳤다. 스크롤 영역 안 절대 위치 목록은 경계에서 잘린다.
- 하네스 스크린샷은
npm run check가 도는 동안 스타일이 풀린 채 찍혔다(체크가 vite 캐시를 건드린 것으로 추정) — 실측은 체크 전에 끝낼 것.
5. 적용 결과
| 항목 | 전 → 후 |
|---|---|
| 종목 특성 수정 경로 | 없음(DB 직접) → 상세 화면 행별 연필 6곳 |
| 맨몸 외 535종목의 계수 표시 | 0 → 없음(연필 없음) |
| 맨몸 시간·거리 종목 73개의 계수 표시 | 0 → 0 (체중 반영 안 함) |
| 필수 입력 편집 | JSON 손입력 → 기본값/모두/하나만 선택 + 직접 입력 |
| 측정 단위 행 | 있음 → 삭제(기록 필드 파생) |
| 유닛 테스트 | 2,301 → 2,306 (전부 green) |
| 브라우저 실측 | 분류 → 덤벨 저장 {equip:"덤벨"}, 풀업 계수 → 0.75 {bwFactor:0.75}, 필수 입력 하나만 → {"any":["load","reps"]} 호출 확인 |
| 서버 | 변경 0 |
| 미검증 | Production 관리자 화면에서의 실제 저장 왕복(로그인 장벽) — 저장 경로 자체는 #1060부터 쓰던 것과 동일 |
6. 이번 개선으로 향상된 것
관리자가 종목 특성을 화면에서 고친다
잘못 등록된 타겟 부위·기록 필드가 리포트·세트 입력에 다음 카탈로그 재적재부터 반영된다. 한 행씩 저장이라 실수 범위가 한 값으로 좁다.
"0"의 뜻이 하나가 됐다
맨몸이 아니면 계수라는 개념 자체가 없다는 것이 표시('없음')와 저장 규칙(컨트롤러 0 강제)으로 고정됐다. 바벨 종목에 계수를 넣는 사고가 원천 차단된다.
구조적으로 남는 것
AdmPickModal— 관리자 화면의 "선택지 모달" 공용 부품.- 계약 테스트: 연필 유무·계수 3분기·바꾼 키만 보내도 나머지 유지·맨몸 외 0.
남은 것
- 종목 등록 모달의 체중 반영 계수 입력은 아직 자유 숫자 텍스트(컨트롤러 규칙으로 맨몸 외는 0 저장). 등록 폼도 선택형으로 맞추는 것은 범위 밖.
- 기록 필드 변경 시 세트 입력 화면이 바뀐다는 안내는 모달 문구 한 줄뿐 — 영향받는 진행 중 세션 수 표시 등은 필요 시 후속.