Skip to content

관리자 종목 특성 한 줄 편집 — 수정 버튼이 없던 종목 상세에서 연필 + 선택지 모달까지 (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.tsxAdmExerciseDetail(행별 연필·모달 6종) · DesktopAdminAddModal.tsxAdmPickModal · adminController.ts adminExercisePayload의 "맨몸이 아니면 계수 0" 규칙
  • 도구: 없음(브라우저 실측은 워크트리 일회용 vite 하네스, 커밋 안 함)
  • 게이트: adminExerciseTraitRows.test.mjs(렌더·컨트롤러 계약 5건) · adminUiWriteContract.test.mjs 계수 기대값
  • 버그리포트: bug-056-20260902.md — 타겟 부위 모달 안 부위 목록 잘림(후속 수리)
  • 계약: designContract.tsadminPickModal 마커 등록

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)과 저장 경로(editAdminExerciseupdateExercise)는 그대로 살아 있었지만, 폼을 여는 버튼이 없어 종목 특성을 화면에서 고칠 방법이 없었다. 이슈 #1069가 09-01에 "수정 버튼 복원"으로 등록됐고, 09-02 오너가 요구를 구체화했다 — 행별 인라인 편집, 옵션은 모달, 체중 반영 계수는 필요한 종목만.

2. 문제 제기

종목 특성을 고치려면 DB를 직접 만져야 했다

관리자가 '풀업'을 열면 분류·타겟 부위·기록 필드·체중 반영 계수가 표로 보이지만 어느 행도 편집이 안 됐다. 잘못 등록된 타겟 부위 한 줄을 바꾸려면 SQL로 가야 했다.

남아 있던 폼은 "종목 전체" 폼이라 요구와 맞지 않았다

종목명·별칭·소속 계열·식별자·특성이 한 화면에 다 있는 폼이었다. 이름·별칭은 이미 동의어 카드에서 편집하므로 중복이고, 오너가 원한 것은 특성 행 하나씩이었다.

체중 반영 계수 0의 뜻이 두 가지였다

'스내치'는 계수 0 = 해당 없음, '플랭크'는 계수 0 = 맨몸이지만 시간 기록이라 반영 안 함. 화면은 둘 다 0이라 구분이 안 됐다. Production 실측(09-02):

분류활성 종목계수 > 0계수 = 0
맨몸26018773(플랭크·L싯·핸드스탠드 홀드 등 시간/거리 기록)
바벨·덤벨·머신·케틀벨·밴드·유산소·기타5350535
미분류(외부 인입·커스텀)1880188

맨몸이 아닌데 계수가 있는 종목은 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). AdmAddModalvalidate 옵션(타겟 부위 합계 100% 검사).
  • DesktopAdminExercises.tsx: AdmExerciseDetail의 종목 특성 표를 행별 연필 구조로 교체, 모달 6종 배선. 측정 단위 행 삭제. 종목 전체 폼(AdmInlineEditor) 진입 코드·초기값 조립 제거. 체중 반영 계수 표시 3분기(없음 / 0 (체중 반영 안 함) / 숫자). 기록 필드를 줄여 논리식 원자가 밖으로 나가면 reqinputs: ""를 함께 보내 기본값으로 되돌림(서버 CHECK required_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 경계 게이트가 논리식 데이터 키 anyany 토큰으로 오탐(#947의 recordingRequirement.ts와 같은 사례) — 검토 목록에 사유 등재.

주요 결정과 그 근거

  • 바꾼 키만 전송: 컨트롤러 adminExercisePayload가 폼에 없는 키를 기존 값(fallback)으로 채우는 계약이 이미 있어(#1060) 행 단위 저장이 추가 코드 없이 성립. 별칭은 기존 값을 그대로 다시 쓰므로 되덮기 없음(대표 표기 별칭이 정본).
  • 계수 규칙은 컨트롤러에: 화면에서 연필을 숨기는 것만으로는 등록 폼·외부 경로를 막지 못한다.
  • equipment: 앵커 유지: exerciseCatalogClassification.test.mjsequipment: 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개의 계수 표시00 (체중 반영 안 함)
필수 입력 편집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 저장). 등록 폼도 선택형으로 맞추는 것은 범위 밖.
  • 기록 필드 변경 시 세트 입력 화면이 바뀐다는 안내는 모달 문구 한 줄뿐 — 영향받는 진행 중 세션 수 표시 등은 필요 시 후속.