jekyll/pages/pending.markdown(미결 항목)에서 이미 해소된 항목을 옮겨 둔 아카이브입니다. jin 구역에 시험 적용한 뒤(2026.09.11) ho·min·paik 구역까지 확장했습니다(2026.09.15) — /pending/이 열린 항목 위주로 보이도록. 구역·번호 규칙은 원본과 같습니다(CLAUDE.md 「미결 항목」 절 참고) — 번호는 구역 안에서만 세고, 이 파일로 옮겨도 번호는 바꾸지 않습니다(다른 문서가 번호·제목으로 참조할 수 있습니다) — min 구역의 원래 번호 중복 하나만 예외로 옮기며 바로잡았고, 해당 항목에 표시해 두었습니다.

헤딩에 ✅가 있어도 본문에 남은 하위 작업이 있어 보이는 항목은 옮기지 않고 pending.markdown에 그대로 두었습니다 — 애매하면 옮기지 않는 쪽을 택했습니다(ho 35·44번, paik 8·11번).

ho (정상호)

3. 최초 지원 종목과 동작 범위 ✅ 해소 (2026.08.26)

루브릭은 (종목, 동작) 단위로 작성한다. 같은 지표가 동작에 따라 반대로 채점되므로 종목 단위 루브릭은 성립하지 않는다 — 임팩트 시 무릎각 176도는 인스텝 슈팅에서 1등급, 인사이드 패스에서는 0등급이다.

결정: 종목은 축구·야구·농구 셋, 동작은 종목당 하나로 연다.

루브릭 상태
축구 인스텝 슈팅 · 야구 투구 · 농구 점프슛 열림(status: active)
축구 인사이드 패스 · 농구 레이업 검수 대기(status: draft) — 파일은 유지, 선택지에서 제외

동작을 하나 여는 실제 비용은 YAML 작성이 아니라 임계값 실측·지도자 검수·검증 클립 확보다. 검수(미결 2번)가 끝나는 대로 draft를 하나씩 연다.

  • 결정: 정상호 · 해소일: 2026.08.26

🔴 2026.09.11 — 이 결정은 39번이 덮었습니다. 팀 방향이 축구로 모이면서 야구·농구 루브릭 4개를 지웠습니다. 위 표는 그때의 기록으로 남깁니다.

10. 서비스와 평가가 target_fps를 서로 다른 방식으로 얻는다 ✅ 해소 (2026.09.02)

해소됐다. pose.DEFAULT_TARGET_FPS를 단일 진실원으로 두고, 평가 12곳과 CLI 2곳이 그 상수를 import한다. pose.py의 기본값 3곳(read_frames, extract_keypoints, PoseResult.target_fps)도 같은 상수를 쓴다.

리터럴 30으로 바꾸는 방안은 택하지 않았다 — 지금은 맞지만 다음에 값을 옮길 때 또 갈라진다. 이 항목이 경고한 것이 바로 그것이다. 기본값에만 맡기는 방안도 택하지 않았다 — 호출부에서 어떤 fps로 도는지 안 보이게 된다. 값은 한 곳, 의존은 눈에 보이게가 결론이다.

바꾸지 않은 리터럴 15가 셋 있고 전부 의도된 것이다. targets.py의 LABEL_TARGET_FPS는 라벨이 실제로 15fps 격자에 붙어 있다는 사실이라 상수로 남는다(라벨 재매핑이 이 값을 쓴다). eval/pending7_fps/step5_sampling.py는 미결 7번 조사의 의도된 기준점이다. tests/는 15와 30을 각각 명시적으로 검증한다.

경고가 현실이 된 사례도 함께 기록해 둔다 — 이 항목은 “기본값만 바꾸면 조용히 갈라진다”고 적었는데, 실제로 갈라진 것은 fps가 아니라 코드 사본이었다 (미결 11번 참고). 같은 종류의 결함이다.

  • 값 자체(15)의 근거가 없다는 지적은 유효했다. 30의 근거는 남겼다 — 밴드 적중 31.8% → 49.8%, 30→60은 +3.7pp뿐, 평가셋 39클립 중 38건이 30fps 이하.
  • 커밋 f2dacdc. 담당: 정상호 · 기한: 해소
해소 이전 기록

target_fps=15가 두 경로에서 다르게 도달한다.

경로 호출 target_fps
서비스 api.py의 POST /api/analyze/video extract_keypoints(tmp_path, rubric_key=rubric) 기본값에 의존 (인자를 넘기지 않음)
평가 13개 파일 (eval/phaseA/**) read_frames(..., target_fps=15) 리터럴 15로 명시
CLI scripts/analyze.py·measure.py --fps argparse 기본값 15

지금은 값이 우연히 같아 문제가 없다. 그러나 pose.py의 기본값만 바꾸면 서비스만 움직이고 평가 기준선은 15에 그대로 남는다. 조용히 갈라지고 아무 신호가 없다. 반대로 평가 쪽만 고쳐도 마찬가지다.

서비스 핸들러에는 fps 오버라이드 지점이 아예 없다 — 쿼리 파라미터가 rubric과 side 둘뿐이라 업로드하는 쪽도 핸들러도 이 값을 건드릴 수 없다.

  • 두 경로가 같은 출처를 보게 할지(상수 하나로 모으기), 아니면 서비스도 명시적으로 넘기게 할지 미정
  • 값 자체(15)의 근거는 커밋 이력·문서 어디에도 없다 — pose.py가 처음 들어온 커밋(2026-08-25)에 이미 그 값이었고 정당화된 적이 없다
  • 정답 불필요 — 두 경로가 같은 값을 쓰는지는 정답 없이 확인된다
  • 담당: 정상호 · 기한: 미결 7번 수정 착수 전

12. 관측 sink 우회는 흔적을 남기지 않는다 ✅ 해소 (2026.09.03)

observability.record()는 기록에 실패하면 경고를 남기지만 SUPERSUB_METRICS_SINK로 우회했을 때는 흔적이 없다. 따라서 “서비스 sink 0건”은 고장과 우회를 구분하지 못한다. 2026-08-31 조사에서 실제로 이 혼선이 발생했다 — 개발 확인용으로 sink를 /tmp로 돌려 둔 상태에서 프로덕션 경로로 분석 3건이 돌았는데, 뒤에 이를 “서비스 분석 0건”으로 읽었다.

해소 — 우회하면 기본 위치에 흔적을 남긴다.

문제의 핵심은 읽는 시점에는 환경변수가 이미 사라져 있다는 것이었다. 그래서 경고 로그만으로는 되짚을 수 없고, 증거가 기본 위치에 남아야 한다.

  • record()가 환경변수 우회를 감지하면 data/observability/sink_redirects.jsonl 에 한 줄 남긴다 (시각·대상 sink·출처·pid, 프로세스당 1회, 최선 노력)
  • 보고 스크립트가 읽는 sink와 그 출처를 항상 먼저 밝히고, 0건일 때 흔적이 있으면 “다른 sink로 기록된 적이 있다”고 알린다. 흔적이 없으면 “정말 분석이 없었을 가능성이 높다”고 적는다 — 세 경우가 이제 갈린다
  • resolve_sink()가 경로와 출처(argument·env·default)를 함께 돌려준다. 쓰는 쪽·읽는 쪽·스크립트에 흩어져 있던 sink or env or DEFAULT 를 한 곳으로 모았다. 셋이 갈리면 “어디에 썼는지”와 “어디를 읽는지”가 어긋난다

흔적은 별도 파일이다. 관측 레코드는 “그대로 한 행으로 INSERT할 수 있는 평면 구조”여야 해서(모듈 설명), 표식을 같은 JSONL에 섞으면 그 계약이 깨진다.

명시적 sink= 인자에는 흔적을 남기지 않는다. 인자는 호출부 코드에 그대로 보여 되짚을 수 있고, 사고를 낸 것은 눈에 안 보이는 환경변수 쪽이다. 인자까지 남기면 테스트·도구가 부를 때마다 쌓여 신호가 죽는다.

「확인」: 사고를 그대로 재연했다 — sink를 /tmp로 돌린 채 3건을 기록하고, 환경변수가 없는 상태에서 기본 위치를 읽었다. 보고 스크립트가 SERVICE_INPUT_AVAILABLE = FALSE 와 함께 “🔴 다른 sink로 기록된 적이 있다 (1회)” 와 그 경로를 출력한다. 테스트 161개 통과(+7).

  • 정답 불필요 — 어느 sink로 기록됐는지는 정답 없이 확인된다
  • 담당: 정상호 · 기한: 해소

14. 평가 스크립트에 /mnt/d 데이터 경로가 하드코딩돼 있다 ✅ 해소 (2026.09.11)

코드 사본 문제는 해소했지만(미결 11번) 데이터 경로 상수는 그대로다. ROOT = Path("/mnt/d/supersub-phaseA")가 13개 스크립트에 박혀 있다.

드라이브 문자가 바뀌거나 다른 기계에서 돌리면 전부 고쳐야 한다. 지금은 한 사람의 한 기계에서만 돌아서 드러나지 않는다.

방향이 정해졌고 한 개를 옮겼다 (2026.09.03). 저장소에 캐시를 들여오면서 eval/phaseA/paths.py를 만들었다 — 환경변수(SUPERSUB_PHASEA_ROOT)로 빼되 저장소에 있는 것은 저장소를 먼저 본다. 심볼릭 링크 방안은 위 이유로 뺐다.

진행 (2026.09.08) — 급소부터 옮겼다

labeling/targets.py 를 옮긴 것이 핵심이다. 이 모듈 하나를 18개 스크립트가 import 하고, load_candidates()·clip_ids() 가 전부 여기를 거친다. 이제 저장소 사본(candidates_target{15,30}/)을 먼저 본다.

옮긴 것 왜
labeling/targets.py 18개 스크립트의 후보 적재 공통 경로
eval_b6/selector_downstream.py B-6 재실행 경로(미결 11번). 절대 홈경로 AGENT 도 뺐다 — 다른 기계·EC2에서 돈다
cand_stats.py · sim_selectors.py · alt_events.py · viz_cand.py ROOT/"candidates"·ROOT/"cache" 를 직접 읽던 것들
paths.default_target() 신설 동작점 결정 규칙을 한 곳에. 파일마다 복사하면 그게 다시 미결 10번이다

🔴 RERUN.md 의 재실행 명령이 없는 파일을 가리키고 있었다. uv run python /mnt/d/supersub-phaseA/eval_b6/selector_downstream.py — /mnt/d 의 .py 38개는 2026-09-02에 전부 지웠다. 저장소 경로로 고쳤다.

🔴 옮기다가 드러난 것 — /mnt/d/cand_stats.csv 는 target 15 산출물이다

동작 불변을 확인하려고 cand_stats.py 를 돌려 저장된 CSV와 대조했더니 달랐다. 원인을 갈랐다 — SUPERSUB_PHASEA_TARGET=15 로 돌리니 바이트 동일이었다.

   
리팩터는? 정확하다. 옛 동작점을 주면 옛 산출물을 그대로 재현한다
그럼 무엇이 문제인가 저장된 CSV 가 낡았다. target 15 때 만든 것인데 /mnt/d/candidates 는 2026-09-02 이후 target 30 이다. 읽는 사람은 그 사실을 알 수 없다
대조 근거 /mnt/d/candidates 전수가 candidates_target30/ 과 39/39 바이트 동일(target 15 와는 1/39 — 8gmHKqDxXdg, 원본 10fps라 두 동작점이 같은 클립)

즉 이 변경은 오늘의 동작을 바꾸지 않는다 — 바꾸기 전에도 /mnt/d/candidates (=target 30)를 읽고 있었다. 달라진 것은 무엇을 읽는지가 이름으로 보인다는 것뿐이다. 🔴 덮어썼던 cand_stats.csv 는 원본으로 복원했다(백업 대조 확인).

낡은 산출물을 지금 다시 만들지 않았다. 산출물은 그때 무엇을 돌렸는지의 증거라 말없이 갈아 끼우면 안 된다. 필요해지는 회차에 target 을 명시해 다시 낸다.

  • 남은 것: /mnt/d 하드코딩 28개 파일 · 절대 홈경로(/home/ho/...) 14개. 대부분 이미 돌린 일회성 분석이라 위험도는 낮다 — 다시 돌릴 일이 생길 때 옮긴다
  • 🔴 candidates.py(생성기)는 안 옮겼다. 이건 읽는 게 아니라 쓰는 쪽이고, 쓰는 위치를 candidates_target{N}/ 로 바꾸면 PRESERVED_ASSETS.md 의 자산 배치 계약이 바뀐다. 별도 결정이 필요하다
  • 캐시·검출후보는 저장소에서 읽히지만 clips/(130MB)·labeling/(31MB)은 여전히 /mnt/d에만 있다. paths.py의 require_external()이 없을 때 왜 없는지 말하고 멈춘다 — 조용히 빈 결과를 내지 않게
  • 경로가 바뀌면 labels.json·캐시를 못 찾아 즉시 실패하므로 조용히 틀리지는 않는다. 코드 사본 문제보다 위험도는 낮다
  • 정답 불필요 ✅ 모았다 (2026.09.11) — 코드에 박힌 기계별 경로 56곳 → 7곳.

세어 보니 항목이 적어 둔 「14곳」보다 많았다. phaseA/ 최상단만 센 값이었고, eval_b2/·eval_b3/·eval_b4/·labeling/·pending9·11·18/ 에도 있었다.

무엇 전 후
Path("/mnt/d/supersub-phaseA...") 33곳 0
sys.path.insert(0, "/home/ho/projects/.../agent/src") 14곳 0
Path("/home/ho/projects/.../agent/data...") 4곳 0
남긴 것 (이유 있음) — 7곳
  • 데이터 경로는 external_root()(= paths.py)로, 코드 경로는 Path(__file__).resolve().parents[N] 상대 계산으로 바꿨다. 파일 35개
  • 🔴 기본값은 그대로다 — external_root() 의 기본이 /mnt/d/supersub-phaseA 라서 이 기계에서 읽는 위치가 한 곳도 안 바뀐다. 값을 바꾼 것이 아니라 「어디서 읽는지」를 한 곳으로 모은 것이다
  • 🔴 일부러 남긴 것: paths.py·probe_3dsp.py 의 기본값 선언(둘 다 환경변수로 덮는다) · dataset_pipeline/config.py 의 WSL 경로 번역 (/mnt/<드라이브> 를 만드는 코드 자체다) · 🔴 label_cli.py·set_labels.py 의 "source": "/mnt/d/supersub-phaseA/" — 라벨 파일에 적히는 출처 기록이라 바꾸면 그 라벨이 어디서 왔는지를 잘못 적는 것이 된다
  • 🆕 tests/test_eval_paths.py: eval/·src/·scripts/ 를 훑어 새 하드코딩을 막는다(주석·독스트링은 안 센다). 예외는 이유와 함께만 늘린다 — 그렇지 않으면 다음 사람이 그 줄을 보고 또 박는다
  • 덤으로 잡힌 결함 둘: report_b1.py 가 sys 를 쓰기 전에 import 하지 않고 있었다(파일 중간에서 import) · eval_b4/clean_review_analysis.py 는 이 작업 전부터 KeyError ('3USSmzO001k', 119) 로 죽는다(경로가 아니라 데이터 문제다 — git stash 로 대조해 확인했다). 후자는 안 고쳤다
  • 확인: cd agent && uv run pytest tests/test_eval_paths.py -q · 배선 검증은 경로 표현 23곳을 실제로 계산해 존재를 봤고(맞음 23 · 틀림 0), 무거운 의존이 없는 스크립트 22개는 import 까지 해 봤다
  • 🔴 수정 금지 자산도 건드렸다 — 무엇을 건드렸는지 밝힌다: eval_b2/eval_b2.py 의 ROOT 한 줄이다. selector 가중치는 손대지 않았다(diff 가 경로 배선 7줄뿐이다). 기본값이 같으므로 B-2~B-5 결과는 그대로다

  • 담당: 정상호 · 기한: 미정 (남은 14개는 그 스크립트를 다시 돌릴 일이 생길 때 함께 옮긴다 — 미리 전부 고치면 검증 없이 바꾸는 것이 된다)

16. AWS 계정에서 ho가 IAM·할당량을 못 쓴다 (2026.09.02) ✅ 해소 (2026.09.03)

분석 에이전트를 올릴 AWS 자원을 콘솔에서 직접 만들고 있는데(계정 <AWS 계정 ID>, 서울), IAM 사용자 ho의 권한이 서비스별로 갈려 있다.

서비스 되나 확인된 것
S3 ✅ 버킷 supersub-ai 생성, 버킷 정책 적용
EC2 · VPC ✅ VPC·서브넷·IGW·라우팅·S3 엔드포인트·보안그룹 생성
IAM ❌ ListUsers·ListRoles 거부, 정책·역할 생성 거부
Service Quotas ❌ ListAWSDefaultServiceQuotas 거부, 증액 요청 버튼 비활성

모두 “no identity-based policy allows the action”이다.

🔴 2026.09.03 — GPU 할당량이 0으로 확정됐다. 이제 EC2가 막힌다 (16번 갱신)

Service Quotas 화면을 못 보므로 인스턴스 시작을 눌러서 확인했다. 이것이 권한 없이 할당량을 아는 유일한 방법이고, 거부되면 과금이 없어 공짜다.

You have requested more vCPU capacity than your current vCPU limit of 0 allows for the instance bucket that the specified instance type belongs to.

g4dn.xlarge · 서울 · 온디맨드. 인스턴스는 만들어지지 않았고 과금 없다.

아래에 “이 항목이 EC2 진행을 막지는 않는다”고 적었던 것을 정정한다. 그것은 S3 우회 경로가 있다는 뜻이었고 그 부분은 지금도 맞다. 그러나 할당량 0은 우회 경로가 없다 — GPU 인스턴스를 아예 못 만든다. 증액 승인 전까지 EC2 검증 전체가 멈춘다.

✅ 신청은 했다 (2026.09.03) — 승인 대기

“Running On-Demand G and VT instances” 증액을 신청했다. Service Quotas 화면이 막혀 있어도 거부 메시지에 붙어 있는 aws.amazon.com/contact-us/ec2-request 링크로 지원 케이스를 열면 된다. servicequotas:* 권한이 필요 없는 경로다 — 어제 “신청도 못 한다”고 적은 것을 정정한다.

승인은 AWS가 결정하고 수 시간~며칠 걸린다. 승인되면 5-3(인스턴스 시작)부터 이어가면 되고, 이미 만들어 둔 VPC·서브넷·보안그룹·키 페어를 그대로 쓴다.

✅ 할당량은 해소 (2026.09.03) — GPU 검증이 끝까지 돌았다

승인됐고 g4dn.xlarge 가 떴다. <인스턴스 ID> (supersub-ai, ap-northeast-2a, Ubuntu 26.04 DLAMI, 150GB gp3). 위 「확인」대로 VcpuLimitExceeded 없이 시작됐다. 만들어 둔 VPC·서브넷·보안그룹·키 페어를 그대로 썼다.

여기까지 EC2 에서 확인된 것:

   
GPU Tesla T4 15360MiB · torch.cuda.is_available() True (cu126 휠, DLAMI 드라이버)
테스트 uv run python -m pytest -q — 161 통과
vLLM EXAONE 4.0 1.2B 기동, /v1/chat/completions 생성 확인. GPU 5441/15360MiB
자동 종료 타이머 enabled·active, 5분 주기 판정. 종료 동작이 중지인 것을 콘솔에서 확인한 뒤 설치
런북 그대로 따라가면 막히는 곳 다섯을 고쳤다 (커밋 b3be03b·788bbe1)

과금이 시작됐다. 온디맨드 시간당 $0.647 이고, 자동 종료가 유일한 방어선이다. 오래 걸리는 작업 앞에는 sudo supersub-hold 4h.

앞서 「유휴 30분/접속만 120분/최대 12시간」이라고 적은 것을 정정합니다 (2026.09.04). 그날 supersub-ai의 /etc/supersub/autostop.conf를 90분 / 240분 / 18시간으로 올렸다(사용자 요청). 30분 한도가 작업 중간에 인스턴스를 내려 결과 회수가 끊긴 일이 있었다. 최악의 경우 비용이 $7.8 → $11.6으로 늘었다. 스크립트 기본값과 autostop.conf.example은 그대로다 — 절차와 되돌리는 법은 agent/deploy/README.md 자동 종료 절.

✅ IAM 역할도 해소 (2026.09.03) — 이 항목은 닫는다

역할 <EC2 역할> 가 만들어졌고 인스턴스에 붙었다. EC2 안에서 확인:

Arn: arn:aws:sts::<AWS 계정 ID>:assumed-role/<EC2 역할>/<인스턴스 ID>

권한이 의도한 만큼만 열렸다는 것까지 확인했다.

검사 결과
videos/·models/·reports/ 나열 ✅
reports/ 쓰기 ✅
videos/ 쓰기 ✅ AccessDenied — 이것이 정상이다. 영상 업로드는 사람이, EC2 는 읽기만
analyze_s3.py 한 바퀴 S3 다운로드 → 분석 → 리포트·미리보기 업로드

⚠️ aws s3 ls s3://supersub-ai/ (루트)는 이 정책에서 항상 거부된다. ListBucket 에 s3:prefix 조건을 걸었기 때문이고, 거부가 정상이다. 런북의 확인 명령이 루트 나열로 되어 있어 “역할이 안 붙었다”로 오독되기 쉬웠다 — 접두사 기준으로 고쳤다(커밋 94aa3f1).

models/ 에는 쓰기 권한이 없다. 그래서 EXAONE 가중치는 S3 에 올리지 않고 HF 에서 직접 받은 로컬본을 쓴다(SUPERSUB_MODEL_S3 를 비워 둔다). S3 경유가 필요해지면 콘솔에서 사람이 올린다.

아래는 닫히기까지의 경과 기록이다. 지우지 않는다.

(경과) IAM 역할이 막혀 있던 동안 — 요청이 하나 늘었던 이유

작업 → 보안 → IAM 역할 수정 을 열어 보니 드롭다운이 비어 있고 이렇게 뜬다:

User: arn:aws:iam::<AWS 계정 ID>:user/ho is not authorized to perform: iam:ListInstanceProfiles … because no identity-based policy allows the action

아래 (b)만으로는 부족하다는 뜻이다. 관리자가 역할을 만들어 줘도 ho 는 그 목록을 못 읽어 인스턴스에 붙일 수 없다. 그래서 둘 중 하나가 더 필요하다:

  • 관리자가 붙이는 것까지 해 준다 (인스턴스 <인스턴스 ID>), 또는
  • ho 에게 iam:ListInstanceProfiles · iam:PassRole · ec2:AssociateIamInstanceProfile 을 준다

⚠️ 이 화면을 열었다면 IAM 역할 없음 상태로 「IAM 역할 업데이트」를 누르지 말 것. “연결된 역할을 모두 제거”로 동작한다. 지금은 붙은 게 없어 결과가 같지만, 역할이 붙은 뒤에 같은 실수를 하면 떼어낸다. 볼 일이 끝나면 취소.

이것 때문에 검증이 멈추지는 않는다. 모델은 HuggingFace 에서 직접 받고 (EXAONE 2.4G · RT-DETR 165M · ViTPose 328M 적재 완료), 영상은 scp 로 올리고, 리포트는 로컬에 떨어진다. analyze_s3.py 는 입출력 경로만 다르고 측정·판정 절차가 같아서, 권한이 열리는 날 켜면 된다.

스팟 할당량도 같이 신청해 두면 좋다. All G and VT Spot Instance Requests는 버킷이 따로라 온디맨드가 0이어도 별개로 잡힌다. 서울 스팟은 시간당 $0.2219로 온디맨드($0.647)의 34%이고, 검증 작업은 회수를 감당할 수 있다. 스팟이면 g4dn.2xlarge(RAM 32GB)가 시간당 약 $0.44로 온디맨드 xlarge보다 싸서, 미결 9번(4K host RAM)이 추가 비용 없이 풀린다.

막히는 것 둘.

  1. EC2가 S3에 닿을 방법이 없다. 인스턴스 역할을 못 만들고, 대안이던 “권한 좁힌 IAM 사용자 + 키”도 그 사용자를 못 만들어 함께 막힌다
  2. GPU 인스턴스 할당량을 확인·신청할 수 없다. 신규 계정은 G 계열 vCPU가 0인 경우가 많은데, 0이면 g4dn.xlarge 시작이 거부된다. 신청도 못 한다

지금은 막히지 않는다. S3 없이도 검증이 된다 — 모델은 EC2가 HuggingFace에서 직접 받고, 영상·리포트는 scp로 오간다. analyze_s3.py는 코드와 테스트가 이미 있으니 권한이 열리는 날 켜면 된다. 그래서 이 항목이 EC2 진행을 막지는 않는다.

요청 — 계정 관리자에게. 아래 중 하나면 된다.

  • (a) ho에게 iam:CreateRole·CreatePolicy·AttachRolePolicy·PassRole과 servicequotas:ListAWSDefaultServiceQuotas·RequestServiceQuotaIncrease 부여
  • (b) 관리자가 대신 만들어 주기: EC2용 역할 <EC2 역할> — 신뢰 주체 EC2, 권한은 supersub-ai 버킷의 videos/·models/ 읽기와 reports/ 쓰기만. 정책 JSON은 agent/deploy/README-console.md 2-1에 그대로 있다
  • 🔴 그리고 이것이 지금 급합니다 — “Running On-Demand G and VT instances” 할당량을 8 이상으로 신청. 현재 0으로 확정이라 승인 전에는 GPU 검증을 시작할 수 없습니다. 4가 아니라 8인 것은, 4면 g4dn.xlarge 하나로 묶여서 호스트 RAM이 모자라 g4dn.2xlarge로 올릴 때(미결 9번) 승인을 다시 며칠 기다리게 되기 때문입니다. 승인 소요는 AWS가 정하고 수 시간~며칠입니다

(b)가 최소 권한 원칙에 맞는다. (a)는 ho에게 IAM 권한을 상시로 주는 것이라 팀 계정에서는 과할 수 있다.

둘의 급한 정도가 다릅니다. 역할(IAM)은 없어도 S3 우회로 검증이 되지만, 할당량은 우회가 없습니다. 할당량부터 부탁드립니다.

   
확인 승인 뒤 g4dn.xlarge 시작이 VcpuLimitExceeded 없이 뜨면 된 것입니다
하지 말 것 이미 만들어 둔 VPC·서브넷·보안그룹(<VPC ID> 등)을 새로 만들지 않기 — 그대로 씁니다
  • 담당: 정어진(배포·AWS 계정) · 제기: 정상호 · 기한: ✅ 전부 해소 (2026.09.03) — 할당량 승인 → g4dn.xlarge 기동 → IAM 역할 부착까지 끝났다. 남은 조치 없음

21. hip_rotation_range_deg가 못 잰 것을 0.0으로 지어낸다 (2026.09.04) ✅ 해소 (2026.09.08)

고쳤고 B-6을 다시 돌렸다. 사전 등록 기준 A~F 전 항목 합격이라 채택했다. 근거는 agent/eval/pending21_hip_rotation/ (사전 등록 · 기준선 CSV · 대조 스크립트 · 결과).

못 쟀으면 키를 넣지 않는다. LIMB_DEPENDENT_METRICS에 이름을 더해 verify_rubric_coverage가 면제한다 — hip_shoulder_separation_deg가 이미 쓰던 규약이다.

   
B-6 재실행 Track1 330초 · Track2 142초 · 총 472초 (RTX 3050)
바뀐 것 Track 1의 10행만 0.0 → 빈칸. 2클립 × 5 selector
그 클립 YNMHMKb5Md4(항목이 지목한 그것) · GS-PcxmaHmQ(다리 유효 1.3%)
안 바뀐 것 frames·detected_frames·usable_ratio_*·impact_frame·나머지 지표 8개 — 완전 일치. 어긋났다면 내 변경이 아니라 환경 변화였다
확인 grep -n "hip_rotation_range = 0.0" agent/src/supersub_agent/features.py → 0건
테스트 249 → 251 (다리를 못 본 조건에서 키가 빠지는지, 면제 선언이 있는지)

🔴 합격했지만 등급 변동 0건은 공허한 결과다. Track 1은 애초에 채점을 하지 않고(195행 전부 rubric='n/a', grade='no_batting_rubric' — 타격 루브릭이 draft다), Track 2는 0.0인 행이 0개였다. “항목이 빠지면 가중치가 재정규화되어 등급이 어떻게 되는가”는 이 재실행으로 확인되지 않았다. 확인된 것은 “지어낸 값이 사라졌고 나머지는 한 비트도 안 바뀌었다”까지다. 그 경로는 단위 검사로 막았다.

✅ 축구 경로도 확인했다 (같은 날, eval/pending21_hip_rotation/football_path.py). B-6 Track 2 가 축구를 19편 × 5 selector = 95행 채점하고 있었는데, 그 19편은 다리 유효 비율이 0.93 이상이라 문제의 조건이 한 번도 발생하지 않았다 (축구 행의 빈칸 15개는 이 결함이 아니라 PLAUSIBLE_RANGE 초과, 미결 20번이다). 그래서 실측 features 위에서 반사실로 쟀다 — 포즈를 다시 뽑지 않고, 루브릭 (football_instep_shot, active)과 채점 코드는 production 을 그대로 쓴다.

클립 15편 중  
0.0 을 지어내면 점수가 떨어진 클립 14/15
그중 등급 문자까지 떨어진 클립 10/15
키를 빼면 (나)보다 점수가 오른 클립 15/15 ← 가중치 재정규화가 도는 증거

본보기 13_freekick.avi(실측 회전 47.9도): 실측 82점 B → 0.0 지어내면 65점 C(0등급 「잠긴 골반」) → 키 없으면 79점 B(항목 제외). 다리를 못 봤다는 이유로 등급이 한 칸 떨어지던 것이 사라진다.

🔴 이것은 빈도가 아니라 결과의 크기다. 그 조건이 얼마나 자주 나는지는 여기서 알 수 없다(이 19편에서는 0건). 나면 무슨 일이 벌어지는지를 쟀다.

features.py는 준비 구간에 다리 유효 프레임이 2개 미만이면 골반 회전량을 0.0으로 둔다. 그 값이 PLAUSIBLE_RANGE(0~180)를 그대로 통과해 루브릭 밴드의 0등급(“잠긴 골반”) 으로 간다.

가설이 아니라 실제로 났다. 실클립 YNMHMKb5Md4에서 0.0이 나왔고, 그 클립의 Phase A 메타데이터 다리 게이트가 leg0.0이다 — 다리를 못 본 클립이다. 판정 문장까지 그대로 나갔다:

0등급 골반 회전 — “골반 회전 각도 0.0도로 … 하체 회전 부재로 상체 중심 스윙이”

선수는 “하체를 안 썼다”는 지적을 받지만 실제로 일어난 일은 다리를 못 본 것이다.

왜 이것이 20번과 다른 항목인가

20번은 잰 값이 범위를 벗어난 경우고 이쪽은 아예 재지 못했는데 값이 만들어지는 경우다. 처방도 다르다 — 20번은 어디까지를 범위 밖으로 볼지의 문제이고, 이쪽은 없는 값을 내지 않으면 된다. 같은 파일에 그렇게 하는 지표가 이미 있다:

if len(sep_idx) >= 2:
    features["hip_shoulder_separation_deg"] = ...   # 몸통이 부실하면 아예 안 넣는다

hip_rotation_range_deg도 len(span_idx) >= 2를 이미 검사하고 있다. else: hip_rotation_range = 0.0 대신 키를 넣지 않으면 되고, 그러면 도구 미검출과 같은 규약으로 그 항목만 판정에서 빠지고 가중치가 재정규화된다. LIMB_DEPENDENT_METRICS에 이름을 더해야 verify_rubric_coverage가 면제한다.

🔴 타격만의 문제가 아니다

football_instep_shot(active)의 hip_rotation 항목이 같은 지표를 쓴다. 지금 열려 있는 축구 루브릭에서도 다리가 안 보이는 클립은 “골반이 잠겼다”로 채점된다.

왜 이번에 안 고쳤나

features 딕셔너리에서 키가 빠지는 변경이라 판정 입력이 달라진다 — 미결 11번이 “B-6 재실행을 부른다”고 적어 둔 형태다. E-3(시간 표기)은 형제 블록이라 해당 없었지만 이건 해당한다. 고치는 것과 재실행을 한 묶음으로 잡아야 한다.

   
확인 grep -n "hip_rotation_range = 0.0" agent/src/supersub_agent/features.py — 이 줄이 남아 있으면 안 고쳐진 것이다
하지 말 것 0.0을 다른 기본값으로 바꾸지 말 것. 값을 안 내는 것이 답이지 더 그럴듯한 값을 지어내는 것이 아니다 (E-3에서 fps=12.0이 그랬다)
근거 agent/eval/jhmdb_batting/realclip/README.md 2절
  • 담당: 정상호 · 제기: 정상호 · 기한: B-6 재실행 자산(11번)이 정리된 뒤

24. 근거 문장에 등급이 안 적혀 있습니다 — 화면이 붙여 주세요 (2026.09.07) ✅ 해소 (2026.09.10)

계약이 깨지는 변경은 아닙니다. 필드가 하나 늘었을 뿐입니다. 다만 문장의 성질이 바뀌었으니 카드를 만들기 전에 아셔야 합니다.

미결 23번을 고치면서 근거 문장에서 등급 번호와 기준 구간을 뺐습니다. 모델은 이제 자세 서술만 씁니다.

  예전 지금
evidence “무릎각 141.7도로 2등급 기준 140~165도 범위의 하단에 있다” “무릎각 141.7도로 차는 다리가 거의 다 펴져 있다”

왜 뺐나 — 모델이 그 숫자를 옮겨 쓰면서 틀렸습니다. 감점 문장 11건 중 6건이 자기 등급과 반대로 말했고, 없는 상한을 지어내 화면에 나간 적도 있습니다 (“40~165도”, 실제 기준은 “40도 이상”). 프롬프트에 없는 숫자는 베껴 쓸 수 없다는 것이 처방이었고, 검사에서 등급 표기 17/19 → 0/19가 됐습니다.

그래서 화면이 해야 할 것

등급 맥락은 코드가 붙입니다. result.breakdown[]의 항목마다 이미 있습니다.

필드 내용 화면에서
grade 0·1·2 그대로
title 그 등급의 칭호 (「감아 도는 팔」) 칭호를 먼저, 문장을 뒤에. 데모 화면이 그 형태입니다
band 그 등급의 수치 구간 (「25~120」) 🔴 선수에게 보이지 마세요. 아래 참고
evidence 자세 서술 문장 그대로
  • 🔴 band를 선수 화면에 내지 마세요. 임계값이 지도자 검수 전 임시값이라 (미결 2번) 숫자를 보여 주면 다음 검수에서 말이 바뀝니다. 개발 확인용입니다 — 에이전트 데모 화면도 접힌 상세에만 찍습니다.
  • 문장만 따로 떼어 쓰지 마세요. 예전에는 문장 안에 등급이 있어서 혼자서도 말이 됐지만 지금은 아닙니다. title 없이 evidence만 있으면 선수는 그것이 칭찬인지 지적인지 모릅니다.

아직 남은 것 — 알고 쓰세요

문장이 자기 등급과 반대로 말하는 경우가 아직 1/19 있습니다(2회차 기준, 처음엔 6/19였습니다). 없앴다고 말하지 않겠습니다. 카드에 문구를 고정으로 박아 두는 형태라면 미결 23번의 진행을 먼저 봐 주세요.

   
확인 cd agent && uv run python -m supersub_agent.api 후 데모 화면의 「개발 확인용 상세」 — 등급 아래에 구간이, 장단점 카드에는 칭호+문장만
하지 말 것 문장에서 등급을 정규식으로 뽑으려 하지 마세요. 이제 없습니다
상세 미결 23번 · agent/eval/pending23_evidence/RESULTS.md

✅ 화면 몫 끝냈습니다 (2026.09.10, 백성검). min 9번(리포트 mock→실데이터 연결) 작업 중 발견했는데, API 연결은 됐지만 칭호와 문장이 서로 다른 두 리스트로 쪼개져 있었습니다(www/src/lib/savedReports.ts가 title· evidence를 각각 titles·traits 배열로 따로 뽑고, ReportView.tsx가 그걸 연결 없는 두 <ul>로 그렸습니다) — 정확히 이 항목이 경고한 “문장만 따로 떼기”였습니다.

  • SavedReport.points: { title: string | null; evidence: string }[]로 합쳐서 항목당 한 덩어리로 옮기도록 고쳤습니다(title이 없어도 evidence는 버리지 않습니다 — 호칭 못 받았다고 문장까지 없어지면 안 됩니다)
  • ReportView.tsx가 항목마다 칭호 알약을 먼저, 문장을 뒤에 그립니다 (요구하신 순서 그대로)
  • band는 애초에 서버 응답(GET /videos/{id}/report, CCC 31)에 없어서 가릴 것도 없었습니다. grade도 화면에 안 씁니다
  • 확인: www/src/lib/savedReports.test.ts(항목 옮김) · npx tsc --noEmit · npm test(관련 3개 파일 85건, 전체 570/571 — 나머지 1개는 무관한 플레이키 테스트로 단독 실행 시 통과 확인)
  • 관련: min 9번(같은 세션에서 발견) · www/src/lib/savedReports.ts · www/src/components/analysis/ReportView.tsx
  • 담당: 백성검(카드 화면) ✅ 해소 (2026.09.10) · 정어진(적재 시 title·band 보존) · 제기: 정상호 · 기한: 카드를 만드는 자리(jin 7번)에 착수하기 전

25. 부록 D.3의 metric_definition이 A안 결정과 어긋납니다 ✅ 해소 (2026.09.08)

박민호 님이 고치셨습니다 (a74f5ee). 제기자로서 확인했습니다 — 외래키 표에서 metric_definition · sport_code → sport 줄이 빠졌고, 도메인 ② 설명도 「어느 종목에서 쓰는지는 metric_definition 이 아니라 그 지표를 참조하는 루브릭·항목 쪽이 안다」로 바뀌었습니다.

🔴 ERD .svg 그림에는 sport_code 가 아직 남아 있습니다. 제가 「그림도 함께 봐 주세요」로 올린 부분인데, 좌표가 박힌 수작업 SVG라 갱신 비용이 커서 「표가 최신이다」라는 주석을 달아 두는 쪽으로 처리하셨습니다. 글과 그림이 어긋난다는 것이 문서에 드러나 있으므로 조용히 틀리지는 않습니다 — 그 판단을 존중하고 다시 올리지 않습니다.

jin 1번(적재 규격)이 A안 — metric_definition에서 sport_code를 없앤다로 정해졌습니다. 그런데 부록 D(공개 제안서)는 아직 종목별 지표를 전제합니다. 제안서와 실제 스키마가 갈라진 채로 두면, 심사 때 어느 쪽이 정본인지 물었을 때 답이 없습니다.

만족해야 할 성질은 “부록 D가 A안 스키마를 설명한다”이고, 세 자리가 걸립니다.

어디 지금 왜 어긋나나
외래키 표 metric_definition · sport_code → sport A안에서 이 컬럼이 없어집니다
도메인 ② 표 「종목별 지표 항목의 정의와 단위」 지표는 물리량이라 종목에 속하지 않습니다
정규화 근거 「지표 항목이 종목마다 달라 컬럼 고정이 불가능하므로」 결론(항목을 데이터로 둔다)은 그대로 맞습니다. 이유 문장만 어긋납니다

왜 A안인지 한 줄로: 루브릭 6개가 쓰는 지표 11개 중 7개가 종목을 넘나듭니다(trunk_forward_lean_deg_at_impact는 축구·야구·농구 전부). 같은 물리량을 종목마다 나누면 선수 벡터·유사도 검색(SFR-005)에서 비교가 불가능해집니다.

참고로 B안(복합키)을 안 고른 이유 중 하나가 부록 D입니다 — 「모든 테이블이 단일 컬럼 기본키를 쓴다. 복합키는 review_selection 하나뿐」이라는 제2정규형 서술과 정면으로 부딪힙니다. A안은 그 서술을 그대로 지킵니다.

   
확인 grep -n "metric_definition" jekyll/chapters/부록D-데이터베이스ERD.markdown — 위 세 자리가 A안을 설명하는지
하지 말 것 🔴 ERD .svg 이미지도 함께 봐 주세요 — 표만 고치면 그림과 글이 어긋납니다 · 결론(“항목을 데이터로 둔다”)을 바꾸지 마세요, 바뀐 것은 이유뿐입니다
상세 fastapi/docs/api-contract.md 3-1절 「✅ 결정 — A안」 · jin 1번
제가 안 한 이유 공개 제안서 문서라 담당이 아닙니다. 직접 고치면 머지 충돌이 되고, 부록 D 수정 여부는 이미 jin 17번에서도 PM 판단으로 올라가 있습니다
  • 담당: 박민호(부록 D 수정) · 제기: 정상호 · 기한: 스프린트 3 (급하지 않습니다 — 스키마가 실제로 바뀔 때 같이 가면 됩니다)

✅ 제가 고쳤습니다 (박민호, 2026-09-08). 확인 명령(grep -n "metric_definition" jekyll/chapters/부록D-데이터베이스ERD.markdown)을 돌려 지적된 세 자리가 여전히 어긋나 있는 것을 먼저 확인한 뒤 고쳤습니다.

  • 외래키 표에서 metric_definition · sport_code → sport 행을 없앴습니다
  • 도메인 ② 표·본문 설명에서 “종목별 지표 항목”을 “물리량이라 종목에 속하지 않는다”로 바꿨습니다 — 결론(항목을 데이터로 둔다)은 그대로 두고 이유만 고쳤습니다
  • 정규화 근거(“지표 항목이 종목마다 달라 컬럼 고정이 불가능하므로”)를 “루브릭이 정의하고 계속 늘어나므로”로 바꿨습니다
  • 🔴 ERD .svg 이미지도 봤습니다 — domain2-video-analysis.svg가 좌표를 직접 박은 수작업 SVG라 metric_definition 표에서 행 하나를 지우려면 그 아래 모든 좌표를 다시 계산해야 합니다. 도메인 ①이 이미 같은 상황(그림이 user_credential·user_identity를 반영 못함)에서 쓴 방식을 그대로 따랐습니다 — 표가 최신이라는 문구를 그림 바로 아래에 남기고 그림 자체는 고치지 않았습니다. 그림을 실제로 갱신하는 것은 별도 작업입니다
  • 덧붙여 D.8 미확정 사항의 “종목별 항목 목록”도 같은 이유로 손봤습니다(원래 지적된 세 자리 외의 것이지만 같은 문제라 함께 고쳤습니다)

26. 워커는 올렸는데 백엔드 API가 응답하지 않습니다 ✅ 오진 — 의도된 정지였습니다 (2026.09.08) · 완전 종결

🔴 제 진단이 틀렸습니다. 백엔드는 고장이 아니라 일부러 꺼 둔 것입니다. 이 항목을 처음에 “정어진 님이 고쳐 주셔야 할 것”으로 올렸던 것을 정정하고, 담당을 뺍니다. 조치할 것이 없습니다.

지운 게 아니라 남기는 이유는 아래 측정이 다음에 같은 증상을 볼 때 쓰이기 때문입니다 — “타임아웃이면 서버가 죽은 것”이 아니라 “꺼져 있는 것과 죽은 것이 밖에서는 똑같이 보인다” 가 결론입니다. 같은 조사를 반복하지 않도록 그대로 둡니다.

워커 쪽에서 확인된 것은 그대로 유효합니다 — 설정·URL·서비스 등록은 검증됐고, 백엔드가 올라오면 45초 안에 알아서 붙습니다. 손댈 것이 없습니다.

jin 18·20번의 분석 워커를 GPU 인스턴스에 설치했습니다. 서비스는 떠 있는데 claim 이 매번 타임아웃이라 큐를 한 건도 못 집었습니다.

[worker] API <API 호스트> · 버킷 supersub-ai · 리포트 s3://supersub-ai/reports
[worker] 루브릭 …/agent/rubrics · 폴링 45초
[worker] claim 실패(1) — URLError: <urlopen error timed out>

워커 쪽 문제가 아닙니다. GPU 인스턴스 안에서 확인했습니다:

   
DNS ✅ <API 호스트> 가 정상적으로 풀립니다
443 · 80 🔴 둘 다 무응답 (timeout, 접속 거부가 아닙니다)
인스턴스의 인터넷 ✅ 정상 (1.1.1.1:443 열림)
제 로컬(WSL)에서도 🔴 같은 증상 — 두 곳 모두에서 안 되므로 방화벽·보안그룹보다 서버가 안 떠 있는 쪽으로 보입니다

접속 거부가 아니라 타임아웃인 것이 단서였습니다 — 포트가 닫힌 것이 아니라 패킷이 버려집니다. 🔴 그래서 “꺼진 것”과 “죽은 것”이 구별되지 않습니다. 저는 이걸 장애로 읽었지만 정지 상태였습니다. 밖에서 보는 증상만으로는 가릴 수 없으니, 다음에 같은 것을 보면 먼저 켜져 있는지부터 물어보는 것이 맞습니다.

   
하지 말 것 🔴 워커 쪽을 고치지 마세요 — 토큰·URL·설정은 검증했습니다. 서버가 올라오면 워커는 45초 안에 알아서 붙습니다. 연달아 실패하면 폴링 간격을 최대 4배까지 늘려 저널을 안 채웁니다
✅ 토큰 검증 끝났습니다 (2026.09.08 저녁). 존재하지 않는 UUID 로 찔러 볼 필요가 없어졌습니다 — 백엔드가 켜지자 워커가 실제로 claim 에 성공해 작업을 처리했습니다(ce4c292a… 성공, 68초). 인증이 통한다는 가장 강한 증거입니다. 상세는 jin 20번
상세 jin 18·20번 · agent/deploy/README.md 워커 절
  • 담당: 없음(조치 불요) · 제기: 정상호 · 기한: — 토큰 검증만 백엔드를 켜는 시점에 정상호가 붙입니다

27. 분석 한 번이 곧 영구 저장입니다 — 저장을 누른 것만 남기고 싶습니다 ✅ 해소 (2026.09.08) — jin 24번이 앞서 있었습니다

🔴 중복이었습니다. main을 받아 보니 같은 문제가 jin 24번 「영상 수명 주기 — 분석은 임시, “저장”을 눌러야 남는다」로 이미 올라와 있었고, 사용자 답변으로 결정까지 끝나 있었습니다(TTL 24시간 · 이동 주체 · 파일명 규칙 · 소유 신원). 제가 이 항목을 쓸 때는 ho 브랜치에 그게 없었습니다.

제 제안(A안, 보존 태그)은 채택되지 않았습니다. jin 24번은 B안 쪽입니다 — keep 처리 중에 fastapi가 videos/<user_id>/… → reports/<user_id>/<video_id>/source.mp4 로 CopyObject+DeleteObject 하고, 미저장분은 수명주기 규칙이 아니라 DELETE /videos/{id} + 서버 스윕(TTL 24h, claim 에 얹음) 으로 지웁니다. 「S3 수명주기 주인이 앱이지 워커가 아니다」는 판단이고, 그 판단을 따릅니다. 아래 두 가지만 그 결정 위에서도 남아 있어 jin 24번에 옮겨 답니다.

🔴 남은 것 (1) — 리포트의 source_video 가 지워진 키를 가리킵니다

리포트 JSON은 분석 시점에 만들어지고(scripts/analyze_s3.py), 그 안에 "source_video": "s3://…/videos/<user_id>/…" 가 박힙니다. 그런데 keep 이 원본을 reports/ 로 옮기면서 videos/ 쪽을 지웁니다. 저장된 리포트에서 원본으로 되짚으면 없는 객체입니다.

  • 미리보기(previews)는 괜찮습니다 — 처음부터 reports/ 아래에 씁니다
  • 낡는 것은 source_video 하나입니다. keep 이 옮긴 뒤 값을 갱신하거나, 리포트를 DB로 옮길 때(paik 7) 그 자리에서 정리하면 됩니다
  • 제 몫과 붙어 있습니다 — jin 24번이 저에게 배정한 「리포트 산출물 키를 reports/<user_id>/<video_id>/ 로 정렬」이 지금 report_slug(상위폴더+stem)를 바꾸는 일이라, 그때 함께 봅니다

🔴 남은 것 (2) — 등록 안 된 고아 객체는 스윕이 못 잡습니다

jin 24번의 서버 스윕은 video DB 행을 기준으로 지웁니다. 그런데 POST /videos/upload-url 로 자리를 받아 PUT 까지 했는데 POST /videos 를 안 부른 객체는 DB에 행이 없어서 영영 안 걸립니다 (브라우저를 닫았거나 등록이 실패한 경우). 2026.09.08에 큐에서 본 404 HeadObject 실패 2건이 이 형태의 반대편입니다.

  • 이건 DB로는 못 잡으므로 S3 수명주기 규칙이 맞습니다 — videos/ 아래 태그 없는 객체 1일 만료 같은 형태. keep 흐름과 충돌하지 않습니다 (등록된 것은 POST /videos 가 태그를 달아 규칙에서 빠집니다)

  • 정본: jin 24번. 이 항목은 닫고 위 두 가지만 그쪽에 남깁니다
  • 담당: 없음(정본은 jin 24번) · 제기: 정상호 · 기한: —
원래 적었던 제안 (기록용 — 채택되지 않았습니다)

지금은 「분석」을 누른 순간 영상이 S3에 영구히 올라갑니다. 사용자가 결과를 보고 마음에 안 들어 버려도 원본은 그대로 남습니다. 시험 삼아 돌려 본 클립, 잘못 찍은 클립, 같은 동작 여러 번 찍어 하나만 고른 클립이 전부 쌓입니다.

바꾸고 싶은 흐름: 임시 저장 → 임시 분석 → 임시 리포트 → 「리포트 저장」을 누르면 그때 영상과 리포트가 남는다.

🔴 지금 화면과 저장소가 이미 어긋나 있습니다 — 이 항목의 진짜 값어치

프론트에는 이미 「저장」이 있습니다(www/src/lib/savedReports.ts). 그런데 그건 localStorage 에 리포트를 두는 것이고, 영상은 저장을 누르든 말든 이미 S3에 영구히 있습니다. 사용자가 보는 것(“저장해야 남는다”)과 실제(“이미 남았다”)가 다릅니다. 이 제안은 비용 절감이기 이전에 그 어긋남을 없애는 일입니다.

🔴 선행 조건 — paik 7번이 먼저입니다

「임시 리포트를 보고 저장을 누른다」가 성립하려면 리포트를 읽을 수 있어야 하는데, 읽는 경로가 계약에 없습니다(paik 7번 「분석 리포트를 읽는 경로가 없습니다」, 담당 정어진). GET /videos 는 analysis_status 까지만 줍니다. 그게 없으면 이 항목은 구현해도 사용자가 쓸 수 없습니다. 순서가 있습니다.

지금 흐름 (계약 3-3절)

(1) POST /videos/upload-url  ->  storage_key: videos/<user_id>/<uuid>.mp4
(2) PUT  <presigned url>     ->  S3 에 직접 (앱 서버를 지나지 않는다 — PER-002)
(3) POST /videos             ->  규격 검사 · analysis_job(queued)
(4) 워커가 집어 분석 -> reports/<슬러그>/<타임스탬프>.json

🔴 원본이 앱 서버를 지나지 않는 것이 PER-002 입니다. 그래서 「S3 밖에 임시로 둔다」는 구조를 깹니다 — 임시 저장도 S3 안에서 갈라야 합니다.

제안 — A안: 보존 태그. 키를 안 바꾸고 수명주기가 지우게 합니다

  A안 (추천) B안
임시 표시 객체 태그 retention=temp 접두사 tmp/ 로 올린다
저장할 때 태그를 retention=keep 으로 바꾼다 videos/ 로 복사하고 원본 삭제
지우는 주체 S3 수명주기 규칙(태그 조건) 같음(접두사 조건)
객체 키 안 바뀝니다 바뀝니다
리포트의 URI 그대로 유효 🔴 낡습니다 — source_video·previews 가 옛 키를 가리켜 리포트를 다시 써야 합니다
저장 비용 태그 API 호출 1회 200MB 복사
워커·에이전트 리포트 업로드에 태그만 답니다 무변경

A안을 권합니다. 저장을 누르는 순간 200MB를 복사하지 않고, 무엇보다 이미 만들어진 리포트가 안 낡습니다. B안은 눈에 잘 보이는 대신, 저장할 때마다 리포트 JSON을 다시 쓰는 일이 붙습니다.

태그는 POST /videos 시점에 서버가 답니다 — 이미 HeadObject로 용량을 실측하고 있어서 그 자리에 하나 더 붙는 것이고, 사전 서명 URL의 서명을 건드리지 않아도 됩니다.

🔴 규칙이 둘 필요합니다. 올려만 놓고 등록을 안 한 객체는 태그가 없어서 retention=temp 규칙에 안 걸립니다. videos/ 아래 태그 없는 객체를 1일 만료 시키는 규칙을 함께 둬야 고아 객체가 안 쌓입니다 — 2026.09.08에 큐에서 본 404 HeadObject 실패 2건의 반대편이 이것입니다.

구간별 소유

구간 무엇 소유
S3 수명주기 규칙 둘 · 보존 기간 결정 태그/접두사 조건, 며칠 정어진
POST /videos 에서 임시 태그 달기 HeadObject 옆 정어진
「저장」 경로 (예: POST /videos/{id}/retain) 영상·리포트 태그를 keep 으로 · DB에 retained_at 정어진
워커 IAM 에 s3:PutObjectTagging 지금은 reports/ 쓰기만 있습니다 정어진
리포트·미리보기 업로드에 태그 달기 storage.upload_json/upload_file (Tagging 인자) 정상호
「리포트 저장」 버튼을 그 경로에 배선 지금은 localStorage 입니다 백성검

보존 기간은 PM/정어진 판단입니다. 분석 실패 재시도와 문의 대응을 생각하면 7일쯤이 무난해 보이지만 근거 있는 값은 아닙니다.

   
만족해야 할 성질 「저장」을 누르지 않은 영상과 리포트는 정해진 기간 뒤 저절로 사라질 것. 누른 것은 안 사라질 것. 경로 이름·태그 이름은 자유입니다
확인 저장 안 한 클립 하나로 aws s3api get-object-tagging → retention=temp · 저장 후 다시 → retention=keep. 수명주기는 aws s3api get-bucket-lifecycle-configuration 에 규칙 둘
하지 말 것 🔴 원본을 앱 서버로 통과시키지 마세요 — PER-002 입니다. 임시 저장도 S3 안에서 가릅니다. 🔴 cron 으로 지우지 마세요 — 수명주기 규칙이 하는 일이고, 스크립트로 지우면 EC2 가 꺼져 있는 동안 안 돕니다
순서 paik 7번(리포트 읽는 경로) → 이 항목. 반대로 하면 사용자가 임시 리포트를 볼 수 없습니다
  • 관련: paik 7번 · 계약 3-3절 · jin 17번(동작을 담을 자리 — POST /videos 본문을 같이 여실 때 함께 보시면 좋겠습니다)

28. 오버롤 등급이 계산은 되는데 화면까지 안 갑니다 — 읽기 경로에 실어 주세요 (2026-09-08 신설) ✅ 해소 (2026.09.11)

사용자 요청: 「분석 리포트에 그레이드 시스템을 넣고 싶다. A·B·C·D를 유지하고 루브릭을 통합해 점수를 내고 그 점수를 근거로 등급을 나눠 달라.」

🔴 그 계산은 이미 있습니다. scoring.aggregate 가 하는 일이 정확히 그것이라 에이전트 쪽에 새로 만들 것이 없습니다. 없는 것은 전달 경로입니다.

지금 리포트 JSON 이 이미 싣고 있는 것

필드 값
result.score 총점 0~100 — 항목 가중합. 🔴 못 잰 항목은 0점이 아니라 제외하고 남은 가중치를 재정규화합니다(촬영 조건으로 선수가 감점되지 않게)
result.grade A / B / C / D — grade_bands A≥85 · B≥70 · C≥50 · D≥0. 루브릭 6종 전부 같은 값입니다
result.breakdown[].grade 항목별 0 / 1 / 2
result.breakdown[].contribution 그 항목이 총점에 넣은 점수
result.breakdown[].title · .band 항목별 칭호와 등급 구간 텍스트
result.breakdown[].out_of_band 그 0등급이 「못했다」인지 「구간 위」인지 (20번)
result.breakdown[].stat 🆕 항목별 연속 점수 0~100 — 레이더 차트 축 값입니다 (아래 참고)
result.provisional 검수 전 루브릭이라는 표시 — 지금 6종 전부 true 입니다

농구 점프슛으로 실제 확인한 값(모델 없이 채점 로직만): 전부 2등급 → 100점 A · 전부 1등급 → 50점 C · 전부 0등급 → 0점 D · 섞으면 68점 C.

🆕 stat — 레이더 차트를 위한 항목별 점수 (2026-09-08 추가)

사용자 요청으로 오각형 레이더 차트를 붙였습니다(에이전트 데모 화면에서 확인 가능). 축을 등급 0·1·2로 그리면 꼭짓점이 중심·중간·끝 세 자리에만 찍혀 모양에서 읽을 것이 없어서, 항목마다 연속 점수를 함께 냅니다.

뜻은 「이상 구간(2등급 구간)에서 얼마나 떨어져 있는가」 입니다.

등급 stat 안에서의 위치
2 85~100 등급이 떨어지는 끝에서 멀수록 높습니다
1 50~85 2등급 구간에 가까울수록 높습니다
0 0~50 바로 위 등급 구간에 가까울수록 높습니다
  • 🔴 총점은 stat 의 평균이 아닙니다. 총점·등급은 지금까지대로 등급의 가중합이고 stat 은 거기에 관여하지 않습니다. 화면에 둘을 나란히 놓을 때 「축 점수의 평균이 총점」으로 읽히지 않게 해 주세요 — 데모 화면은 캡션 한 줄로 그 구분을 적어 두었습니다
  • 등급과 반대로 그려지지 않습니다. 등급별 점수대가 겹치지 않아 2등급 항목이 1등급 항목보다 안쪽에 찍히는 일이 없습니다(검사로 고정: test_item_score_never_contradicts_the_grade)
  • 못 잰 항목은 null 입니다 — 축을 0으로 그리지 마세요. 0으로 채우면 촬영 조건 때문에 「그 항목을 못했다」로 보입니다. 그 항목은 skipped 에 있고 축에서 빼는 것이 맞습니다
  • 축 개수는 루브릭이 정합니다 — 지금 4~6개입니다(점프슛·투구 5, 인스텝 슈팅 6, 레이업 4). 오각형으로 못 박지 마세요

🔴 paik 7번이 요구한 필드에 등급이 빠져 있습니다

거기 적힌 것은 요약 · 특징 · 받은 호칭 · 근거가 된 장면 넷입니다. 그대로 만들면 등급은 여전히 화면에 안 나옵니다. 지금 화면이 수치를 안 그리는 것은 디자인 선택이 아니라 읽을 데가 없어 localStorage 목업이기 때문입니다.

   
만족해야 할 성질 리포트를 읽는 경로가 총점과 오버롤 등급을 함께 줄 것. 위 표의 필드가 이미 리포트 JSON 에 있으므로 새로 계산할 것은 없습니다
🔴 함께 줘야 하는 것 provisional. 임계값이 지도자 검수 전이라 지금 등급은 전부 잠정입니다. 등급만 주고 이걸 빼면 잠정값이 확정값으로 읽힙니다
계약과 안 부딪힙니다 계약 3장 4가 막은 것은 report.summary 문장 안에 숫자를 넣는 것이고, 총점·항목별 등급은 「metrics 에 넣고 리포트 경로로만 나간다」입니다. 부록 D.5가 막은 것은 player_card 의 능력치 컬럼입니다 — 리포트 경로로 등급을 주는 것은 둘 다 허용합니다
확인 분석이 succeeded 인 클립에서 읽기 경로가 score·grade·provisional·breakdown[].stat 을 주면 된 것입니다
하지 말 것 🔴 등급을 카드에 올리지 마세요 — 부록 D.5가 막아 둔 결정이고, 뒤집으려면 박민호 님 판단과 제안서 수정이 함께 가야 합니다. 🔴 근거 문장에서 등급을 정규식으로 뽑지 마세요 — 문장에는 등급이 없습니다(23·24번). 🔴 stat 을 카드로 흘리지 마세요 — 이름이 stat 이지만 리포트 경로 전용입니다(card_rules.FORBIDDEN_CARD_FIELDS 가 막고 있습니다)

✅ 「오버롤」의 단위가 정해졌습니다 — 영상 하나에 하나 (2026-09-08)

사용자 확인: 「오버롤은 영상당 하나로」. 지금 result.score·result.grade 가 이미 그 단위(클립 하나 · 루브릭 하나)이므로 읽기 경로에 그대로 실으면 됩니다. 새로 계산할 것도, 새로 저장할 자리도 없습니다.

선수 한 명을 하나로 합치는 오버롤은 만들지 않았습니다. 선수가 3편을 올리면 등급이 3개 나옵니다. 필요해지면 그때 새 설계입니다 — 몇 건을 어떻게 합칠지 (최근 N건? 최고점? 종목별로 따로?), 어디에 저장할지, 카드 노출 금지와 어떻게 맞출지가 전부 안 정해져 있습니다. 지금 화면에서 「이 선수의 오버롤」이라고 쓰지 마세요 — 그 클립의 오버롤입니다.

에이전트 쪽 확인 cd agent && uv run python -m uvicorn supersub_agent.api:app 후 데모 화면 — 맨 위에 등급·총점·「잠정」 칩과 레이더 차트가 함께 나옵니다. 예전에는 총점이 「개발 확인용 상세」에 접혀 있었습니다

🔴 목업을 갈아 끼우는 것만으로는 안 됩니다 — 숫자가 들어갈 칸이 없습니다 (2026-09-08 확인)

사용자 질문(「실제 리포트가 이제 목업을 대체하나」)을 받고 실제 코드를 봤습니다. www/src/lib/savedReports.ts 는 「경로가 생기면 이 파일만 갈아 끼운다」로 잘 만들어져 있는데, 그 타입에 수치 칸이 없습니다.

export type SavedReport = {
  summary: string; traits: string[]; titles: string[]
  scenes: { at: string; what: string }[]; savedAt: string
}

빠뜨린 것이 아니라 일부러 그렇게 둔 것입니다 — 그 파일이 계약 3장 4와 부록 D.5를 근거로 「🔴 수치가 없다. 서버 경로가 생겨도 이 모양은 그대로여야 한다」고 적어 두었습니다. 백엔드도 같은 갈래입니다: analysis_report 테이블은 문장만 담고 「항목별 등급과 총점은 여기에 없다 — analysis_metric_value 로 간다」 고 적혀 있습니다.

정리하면 문장은 리포트 경로 · 숫자는 지표 경로로 갈라져 있고, 이 항목이 말하는 오버롤·stat 은 숫자 쪽입니다. 그래서 필요한 것이 하나 더 있습니다.

   
만족해야 할 성질 화면이 리포트를 읽을 때 문장과 함께 숫자도 받을 것. SavedReport 를 갈아 끼우는 것만으로는 오버롤·레이더가 안 붙습니다 — 그 타입은 숫자를 버립니다
어느 경로로 줄지 🔴 제가 정할 일이 아닙니다. 계약상 숫자는 analysis_metric_value, 문장은 analysis_report 입니다. 한 응답에 합쳐 줄지, 화면이 둘을 각각 부를지는 정어진 님 판단입니다
하지 말 것 🔴 report.summary 문장 안에 총점·등급을 넣지 마세요 — 계약 3장 4가 막은 것이 정확히 그것이고, 근거 문장에서 등급을 뺀 이유(23·24번)와도 같은 방향입니다
근거 www/src/lib/savedReports.ts 머리 주석 · fastapi/app/analysis/adapter/outbound/orm/analysis_report_orm.py 머리 주석
  • 관련: paik 7번(읽기 경로 — 여기에 실려야 합니다) · jin 1번(POST /analyses 적재) · 같은 구역 20번(구간 위 0등급) · 23·24번(근거 문장에 등급 없음)
  • 담당: 정어진(읽기 경로에 필드 추가) · 백성검(화면 표시) · 제기: 정상호(사용자 요청) · 기한: paik 7번과 함께

29. 루브릭 항목 이름을 쉬운 말로 바꿨습니다 — rubricFocus.ts 를 맞춰 주세요 (2026-09-08 신설) ✅ 해소 (2026.09.10)

맞췄습니다 (2026-09-10, 백성검). www/src/lib/rubricFocus.ts 의 라벨 13개를 새 이름으로 바꿨습니다. id·가중치는 한 글자도 안 건드렸습니다.

표가 아니라 agent/rubrics/*.yaml 에서 직접 뽑아 맞췄습니다 — 사본을 또 손으로 옮기면 같은 어긋남이 되풀이됩니다.

🔴 rubricFocus.test.ts 가 이미 빨간색이었습니다. 그 시험이 루브릭 파일을 실제로 읽어 대조하는데, 09-09 main 병합으로 새 이름이 들어오면서 3건이 깨져 있었습니다 — 설계대로 먼저 잡아 준 것입니다. 지금은 4건 다 통과합니다.

화면 시험(AnalysisStage.test.tsx)의 기대 문구도 새 이름으로 옮겼습니다.

| 확인 | cd www && npx vitest run src/lib/rubricFocus.test.ts → 4 passed. 「신전」·「굴곡」이 rubricFocus.ts 에 안 남았습니다 | |—|—|

사용자 요청으로 채점 항목 이름에서 해부학 용어를 걷어냈습니다 (「차는 다리 무릎 신전」 같은 말). 25개를 바꿨습니다.

🔴 www/src/lib/rubricFocus.ts 가 이 이름들을 「글자까지 같게」 베껴 두었습니다 (그 파일이 스스로 그렇게 적어 두었습니다). 지금은 갈려 있습니다 — 집중 항목 고르는 화면에는 옛 이름이, 리포트에는 새 이름이 나옵니다.

  • 고장은 아닙니다. 짝은 id 로 맞으므로 화면이 고른 것과 서버가 채점한 것이 어긋나지는 않습니다. 보이는 글자만 두 벌입니다
  • 남의 폴더라 고치지 않았습니다. label 만 아래 표대로 바꾸시면 됩니다
종목·동작 id 옛 이름 새 이름
축구 인스텝 슈팅 plant_knee_flexion 디딤발 무릎 굴곡 디딤발 무릎 굽히기
  swing_knee_extension 차는 다리 무릎 신전 차는 다리 뻗기
  hip_rotation 골반 회전 골반 돌리기
  follow_through 팔로스루 차고 난 뒤 마무리
야구 투구 release_arm_extension 릴리스 팔 신전 공 놓는 순간 팔 뻗기
  hip_shoulder_separation 골반-어깨 분리 골반이 어깨보다 먼저 열리기
  stride_leg_block 디딤발 버팀 앞다리 버티기
  arm_deceleration 팔 감속 던진 뒤 팔 속도 줄이기
농구 점프슛 release_arm_extension 슛하는 팔 신전 슛하는 팔 뻗기
  guide_hand 가이드 핸드 공 받치는 손
  follow_through 팔로스루 던진 뒤 손목 마무리
  trunk_alignment 상체 정렬 상체 곧게 세우기
  leg_drive 하체 신전 다리로 밀어 올리기

rubricFocus.ts 에 실린 것은 active 3종뿐이라 위 표가 전부입니다. draft 3종(축구 인사이드 패스 · 야구 타격 · 농구 레이업)도 함께 바꿨으니 그 동작을 여실 때 루브릭 파일에서 그대로 옮기시면 됩니다.

  • 🔴 id·가중치·bands·titles 는 한 글자도 안 바꿨습니다. 이름은 표시용이라 채점이 달라지지 않습니다 — B-6 재실행을 부르지 않습니다
  • 안 바꾼 것: 「상체 기울기」·「디딤발 위치」는 이미 평이해서 그대로 뒀습니다. deferred 항목도 그대로입니다(화면에 안 나갑니다)
  • 아직 남은 전문어: 항목 이름 말고 anchors[].evidence(모델이 근거 문장을 쓸 때 흉내 내는 예시)에는 「과도한 굴곡」 같은 말이 남아 있습니다. 근거 문장 말투까지 손보려면 별도 회차입니다 — 요청이 항목 이름이라 거기서 멈췄습니다
확인 cd agent && uv run python -c "import sys;sys.path.insert(0,'src');from pathlib import Path;from supersub_agent.scoring import load_rubric;[print(c.name) for p in sorted(Path('rubrics').glob('*.yaml')) for c in load_rubric(p).criteria]" — 「신전」·「굴곡」이 안 나오면 된 것입니다
하지 말 것 🔴 id 를 바꾸지 마세요 — 채점·measured_by·적재(metric_definition)가 전부 그 값으로 짝을 맞춥니다
  • 관련: paik 8번(집중 항목을 보낼 자리) · 같은 구역 28번(등급 전달)
  • 담당: 백성검(rubricFocus.ts 라벨) · 제기: 정상호(사용자 요청) · 기한: 급하지 않음(고장은 아닙니다)

30. 업로드 60초 상한을 정해 주세요 — 기술 판단은 끝났고 제품 판단만 남았습니다 ✅ 결정 (2026.09.11, 박민호)

이 항목은 jin 11번을 박민호 님께 보이게 하려고 냅니다. 그쪽 담당이 정상호로 되어 있어 grep '담당.*박민호' 에 안 걸립니다 — 그래서 결정이 스프린트 2를 넘겼습니다. 상세와 숫자는 전부 그쪽에 있고 여기 옮겨 적지 않습니다(사본은 한쪽만 고쳐집니다).

무엇이 어긋나 있나. 업로드 상한은 60초인데 에이전트가 실제로 보는 창은 10초입니다. 60초 클립을 올리면 앞 10초만 채점되고 나머지는 안 봅니다. (잘렸다는 사실 자체는 timebase 에 남습니다 — 조용한 절단은 아닙니다.)

기술 쪽은 답이 나왔습니다 (2026.09.09 실측, jin 11번의 「다시 쟀습니다」 절):

  • 60초를 모든 입력에 보장할 수는 없습니다. 1080p 천장이 51초, 4K 는 13초입니다
  • 창을 넓히는 것 자체는 값이 쌉니다 — 60초까지 넓혀도 평가셋 39클립이 안 변해 B-6 재실행을 부르지 않습니다
  • 선택지는 셋입니다: (가) 상한을 10~15초로 ← 기술 권고 · (나) 창을 20~30초로 넓히고 상한을 맞춤(4K 는 잘림) · (다) 60초 유지(권하지 않음)

🔴 제가 정하지 않는 이유. 60초는 사용자에게 보이는 값이라 왜 그 값이었는지 아는 분이 정해야 합니다. 기술만 보면 (가)지만, 그것만으로 지우면 「왜 60초였나」가 사라집니다. 루브릭이 채점하는 것이 동작 하나(2~3초)라는 점도 함께 봐 주세요.

   
판단해 주실 것 (가)·(나)·(다) 중 하나. 값을 정해 주시면 고치는 것은 저와 정어진 님이 합니다
확인 cd agent && uv run python eval/pending11_window/window_ceiling.py — 판단에 필요한 숫자가 전부 나옵니다
하지 말 것 🔴 이 항목에 숫자를 옮겨 적지 마세요 — 정본은 jin 11번입니다
  • 관련: jin 11번(정본·숫자) · 같은 구역 5번(임팩트 탐색 범위 — 긴 클립에서 동작을 찾는 문제) · 9번(4K 메모리 예산)
  • 담당: 박민호(제품 판단) 결정함 → 정어진(MAX_DURATION_MS)·정상호(에이전트 쪽 변경 필요시) · 제기: 정상호 · 기한: 스프린트 3 초 결정 완료, 반영은 각자 편할 때

✅ 결정 (2026.09.11, 박민호) — (가) 상한을 10초로

jin 11번 권고 그대로 갑니다. MAX_DURATION_MS를 60초 → 10초로 낮춥니다 — 분석 창(DEFAULT_MAX_SECONDS)과 정확히 맞춰서, 규격을 지킨 업로드는 잘리는 구간이 아예 없게 합니다. (나)(창을 넓히는 안)는 안 갑니다 — 4K만 못 지키는 예외가 생기고, 애초에 루브릭이 채점하는 것도 동작 하나(2~3초)라 60초는커녕 20~30초로 넓혀도 못 채점하는 건 마찬가지입니다(임팩트 탐색 범위 문제, 같은 구역 5번 — 아직 안 풀림).

「왜 60초였나」에 대한 기록: 처음 정할 때 특별한 근거는 없었고 임의로 넉넉하게 잡은 값이었습니다. 지금 평가셋 39클립이 전부 10초 이하라는 실측이 있으니 10초가 실제 사용 패턴과도 맞습니다.

  • 정어진: fastapi/app/analysis/domain/rules/video_rules.py의 MAX_DURATION_MS 한 줄
  • 정상호: 이번 결정으로 에이전트 쪽 추가 변경은 없음(창은 이미 10초)
  • 반영 뒤 fastapi/docs/api-contract.md 3-6절과 클라이언트 쪽 업로드 안내 문구(있다면)도 60초→10초로

31. 제 스프린트 산출물 이름이 실제로 만든 것과 다릅니다 — 「RAG 검증」 ✅ 결정·반영 (2026.09.11, 박민호)

스프린트 2가 09-14에 끝나는데 제 칸이 「RAG 검증 로직 프로토타입」입니다. 그런데 지금 에이전트에 retrieval 이 없습니다. 늦기 전에 올립니다.

사실부터

judge.py 가 하는 것은 루브릭의 anchors 를 전부 무조건 프롬프트에 넣는 정적 few-shot 입니다. 코퍼스도 색인도 유사도 검색도 없습니다.

이것은 빠뜨린 것이 아니라 정해진 설계입니다. 부록 E 결정기록 3)절에 「파인튜닝 금지, 루브릭·RAG로 해결」 결론이 오픈 웨이트 전환으로 뒤집혔다 고 이미 적혀 있고, 대신 자리 잡은 원칙이 측정과 판단의 분리입니다 — 코드가 재고 코드가 등급을 정하고, 모델은 근거 문장만 씁니다. 🔴 등급 결정을 모델로 되돌리는 것은 닫힌 경로입니다(경계값을 재현되게 틀렸습니다).

제안서 본문 1~6장에 「RAG」가 한 번도 안 나옵니다. 남은 것은 이름뿐입니다.

「RAG」가 붙어 있는 자리 — 전수

자리 무엇
_config.yml 사이트 제목 브라우저 탭·검색 결과에 나갑니다
jekyll/pages/index.markdown 3곳 표지 부제 · 한 줄 설명 · 영문 제목
jekyll/chapters/07 팀 역할 「정상호 — AI 에이전트 개발 (RAG 검증, 실력 판단 로직)」
jekyll/chapters/07 스프린트 2 「RAG 검증 로직 프로토타입」
루트 CLAUDE.md 2곳 저장소 설명·팀 표
fastapi/README.md 정어진 님 영역
부록 E · 개발 로그(08-25) 고칠 필요 없습니다 — 「뒤집혔다」고 이미 적힌 역사 기록입니다

두 갈래로 읽힙니다 — (가)를 권합니다

  읽는 법 그러면
(가) 권고 「RAG」는 매칭 쪽을 가리킨다 — 선수 카드 벡터 검색·추천 플랫폼 이름은 그대로 두고 제 칸 두 개만 고칩니다. 표에서도 「벡터 저장·검색」(S3)·「추천 API」(S4)가 정어진 님 칸입니다
(나) 「RAG 로 실력을 검증한다」 부록 E 에 기록된 설계와 정면으로 어긋나 이름 전체를 다시 봐야 합니다

(가)면 바꿀 곳이 둘뿐입니다. 초안을 냅니다 — 문구는 PM 판단입니다.

지금 제안
정상호 — AI 에이전트 개발 (RAG 검증, 실력 판단 로직) 정상호 — AI 에이전트 개발 (루브릭 기반 실력 검증, 근거 생성)
스프린트 2 · RAG 검증 로직 프로토타입 스프린트 2 · 루브릭 기반 실력 검증 로직 프로토타입

🔴 같이 봐 주실 것 — Hit Rate·Recall@5 가 제 칸에 있습니다

표지가 「Hit Rate 90%, Recall@5 90% 달성」 을 내걸고, 스프린트 4 제 칸이 「에이전트 정확도 검증(Hit Rate/Recall)」입니다. 둘 다 검색 지표입니다. 그런데 검색을 만드는 것은 정어진 님입니다(S3 벡터 저장·검색 · S4 추천 API).

제가 잴 것이 없습니다. 루브릭 판정에는 Hit Rate·Recall 이 정의되지 않고, 정의하려면 정답 라벨이 필요한데 그것이 미결 2번입니다. 표지의 숫자는 발표에서 반드시 질문이 나오는 자리라 누가 무엇으로 재는지를 정해 두셔야 합니다.

   
판단해 주실 것 ⑴ (가)냐 (나)냐 ⑵ (가)면 위 문구 두 줄 ⑶ Hit Rate·Recall 의 주인과 대상
확인 grep -rn 'RAG' jekyll/ _config.yml \| grep -v pending — 위 표의 자리들이 그대로 나옵니다
하지 말 것 🔴 등급 결정을 모델로 되돌리는 방향으로 이름을 맞추지 마세요 — 닫힌 경로입니다. 이름을 실물에 맞추자는 것이지 실물을 이름에 맞추자는 것이 아닙니다 · 부록 E·개발 로그의 「RAG」는 지우지 마세요(무엇이 왜 뒤집혔는지가 사라집니다)
  • 관련: 부록 E 결정기록 3)절 · jekyll/chapters/07-개발구현계획.markdown · agent/CLAUDE.md 「무엇이 무엇을 정하는가」 · 미결 2번(정답 라벨)
  • 담당: 박민호(제안서 문구·역할 정의) ✅ 반영함 · 제기: 정상호 · 기한: 스프린트 2 리뷰 전 (09-14) 완료

✅ 결정·반영 (2026.09.11, 박민호)

(가)로 갑니다 — 「RAG」는 매칭·추천(정어진 영역, S3·S4)을 가리키는 것으로 읽고, 플랫폼 이름(_config.yml·index.markdown·CLAUDE.md 저장소 설명·fastapi/README.md)은 그대로 둡니다. 정상호 님 개인 칸만 고쳤습니다:

  • CLAUDE.md 팀 표, 07-개발구현계획.markdown 팀 역할·스프린트2 로드맵 칸: 「RAG 검증, 실력 판단 로직」 → 「루브릭 기반 실력 검증, 근거 생성」
  • 07-개발구현계획.markdown 스프린트3 칸: 「실력 검증·적합도 판단 모델 고도화」 → 「채점 시스템 신뢰성 강화(fps 불변성 개선, 임계값 출처 조사)」 — 33·34번 결정과 같이 반영
  • Hit Rate·Recall@5(⑶): 정어진 님 몫으로 이동. 스프린트4 칸을 정어진 「추천 API 정확도 검증(Hit Rate/Recall)」으로, 정상호 님 칸은 「채점 검증 결과 반영·통합 테스트 지원(골든셋 확보 시 QWK·MAE 측정)」으로 바꿨습니다 — 표지의 “Hit Rate 90%, Recall@5 90%”는 그대로 정어진 님 매칭·추천 시스템의 목표치로 유지됩니다

부록 E·개발 로그(08-25)의 「RAG」는 손대지 않았습니다(역사 기록).

34. 지도자 섭외가 어렵다면 3장 검증 기준을 바꿔야 합니다 ✅ 결정 (2026.09.11, 박민호)

지도자 섭외 여건이 안 된다고 팀 안에서 확인됐습니다. 그러면 3장 「검증 기준」이 그대로 설 수 없습니다. 대안을 정리해 올립니다 — 제가 정할 일이 아니라 제안서 문구가 걸린 문제입니다.

🔴 먼저 나쁜 소식 — QWK·MAE 는 대안이 없습니다

3장이 내건 목표 셋 중 둘은 모델 등급을 사람 등급과 비교하는 지표입니다.

지표 목표 지도자 없이
QWK (사람-모델 일치도) 0.6 이상 🔴 불가능 — 비교할 사람 라벨이 없습니다
MAE (총점 절대 오차) 8점 이내 🔴 불가능 — 같은 이유
재현성 (총점 표준편차) 3점 이내 ✅ 가능

문헌으로 임계값을 잘 잡아도 이 둘은 안 나옵니다. 임계값의 출처 문제와 정답 라벨의 부재는 다른 문제입니다. 「골든셋 50~100건」을 조금 줄이거나 대체하는 식으로 풀리지 않습니다.

그런데 3장 자신이 길을 적어 두었습니다

채점 시스템의 성패는 정확도보다 재현성에서 갈린다. 같은 영상을 세 번 넣어 87 / 72 / 91이 나오면 서비스가 성립하지 않는다.

그리고 8장 KPI 표에는 채점 정확도 항목이 아예 없습니다 (매칭 성사율 · 노쇼율 · 만족도 · 재사용률, 전부 [TBD]). 정확도를 앞세운 것은 3장뿐입니다.

🔴 정정 — 「재현성은 실측으로 확인됐다」는 조건이 다릅니다

6장에 “재현성은 실측으로 확인됐다 — 동일 측정값으로 5개 항목 판정을 3회 반복해 표준편차 0.00” 이라고 적혀 있는데, 3장 표의 정의는 「동일 영상 5회 반복」입니다. 실측은 판정 단계만 반복한 것이고 영상→포즈→지표 구간은 들어 있지 않습니다. 6장은 “동일 측정값으로”라고 정확히 적어 두었으니 6장이 틀린 것은 아니고, 두 문서가 다른 것을 재현성이라 부르고 있습니다.

3장 정의대로는 아직 안 쟀습니다. 지도자 없이 잴 수 있고, 제가 잽니다.

정답 없이 잴 수 있는 것 — 셋 다 지금 값이 좋지 않습니다

갈아탈 곳이 있다는 뜻입니다. 개선을 보여 줄 여지가 남아 있습니다.

재는 것 지금 정답
재현성 (3장 정의, 동일 영상 반복) ✅ σ = 0.00 · 4/4 합격 (2026.09.09 측정) 불필요
fps 불변성 🔴 30↔60fps 에서 최종 등급 37% 변동 불필요
지표 타당성 🔴 골반 회전이 회전의 일부만 담음(24%) 불필요
결측 정합성 ✅ 해소 (ho 21번) 불필요

🔴 fps 불변성이 특히 큽니다. 같은 영상을 다른 프레임레이트로 넣으면 등급이 37% 바뀝니다 — 3장이 «87/72/91이 나오면 서비스가 성립하지 않는다»고 한 것과 같은 종류의 결함이고, 임계값이 틀린 것보다 사용자에게 더 직접적입니다.

임계값은 「검증」을 포기하고 「출처」로 낮춥니다

루브릭은 지금 “일반적인 지도 지침을 근거로” 라고만 적혀 있고 출처가 없습니다(부록 C 에 생체역학 문헌 0건). 문헌·공개 지도 자료를 인용해 붙이면 「임의값」에서 「인용값」이 됩니다. 정확도 주장은 못 하지만 「왜 이 숫자냐」에는 답할 수 있고, 발표에서 나올 질문이 그쪽입니다.

🔴 분포에 맞춰 긋는 길은 여전히 안 됩니다. 이미 기각된 경로입니다 — “동작점을 옮긴 것이지 맞아진 것이 아니다”. 지도자가 없다는 이유로 되살리면 그때는 근거가 아니라 편의가 됩니다.

✅ 재현성은 쟀습니다 — 3장 정의로 4/4 합격 (2026.09.09)

사전 등록을 코드보다 먼저 걸고(cd353f0) 쟀습니다. 근거: agent/eval/pending34_repro/.

클립 총점 5회 표준편차
축구 3편 35·35·30 각 5회 전부 동일 0.00
농구 점프슛 1편 62 ×5 0.00

features 가 비트 동일했습니다 — 총점만 같은 것이 아니라 그 앞 지표값이 소수 끝자리까지 같습니다. GPU 비결정 커널이 우려됐지만 이 경로에서는 안 나타났습니다.

🔴 합격이지만 자랑할 것은 아닙니다. 사전 등록이 「전부 합격」을 예상했고 그대로 나왔습니다 — 파이프라인에 난수가 없습니다. 얻은 것은 「그럴 것이다」를 「재 봤다」로 바꾼 것이고 그 이상으로 읽으면 안 됩니다.

🔴 그리고 이 기준은 사용자가 겪는 것을 다 담지 못합니다.

무엇을 바꾸면 총점·등급이
아무것도 안 바꾸고 5회 안 변한다 (σ 0.00)
프레임레이트를 절반으로 (실영상 재인코딩, 2026.09.09 측정) 🔴 총점 평균 17.1점 · 최악 38점 · 최종 등급 29% 변동 · 1건은 아예 반려
프레임레이트 30↔60 (외부 포즈 모의) 🔴 최종 등급 37% 변동 (ho 7번)
해상도·촬영 각도 미측정

🔴 3장 허용치가 3점인데 평균 17.1점입니다 — 5.7배, 최악은 12.7배입니다. 근거: agent/eval/pending7_realfps/. 같은 표본으로 잰 「동일 파일」 재현성은 σ 0.00 이므로, 두 지표의 값이 극단적으로 갈립니다.

3장 기준은 통과하지만 «87/72/91이 나오면 서비스가 성립하지 않는다»가 겨냥한 위험은 fps 쪽에 남아 있습니다. 사용자에게는 「같은 경기 장면」이 「같은 파일」보다 자연스러운 단위입니다.

그래서 3장 표를 고치실 때 재현성을 한 줄로 두지 마시길 권합니다 — 「동일 파일」과 「동일 장면·다른 조건」은 다른 지표이고, 지금 값이 하나는 0.00, 하나는 37% 입니다.

판단해 주실 것

  1. 3장 검증 기준 표를 바꿀 것인가. QWK·MAE 를 「골든셋 확보 시」 조건부로 내리고 재현성·불변성·타당성을 전면에 두는 안을 권합니다. 지우지 말고 조건부로 남기는 편이 낫습니다 — 나중에 지도자가 생기면 그대로 씁니다
  2. 8장 KPI 표([TBD])에 그 셋을 넣을 것인가. 자리가 비어 있습니다
  3. 임계값 출처 조사를 제 일감으로 잡을 것인가. 하면 스프린트 2~3 중 며칠입니다
   
확인 3장 「검증 기준」 표 · 8장 「검증 지표(KPI 예시)」 표 · 6장 「실행 구조와 성능」의 재현성 문장
하지 말 것 🔴 분포에 맞춰 임계값을 긋지 마세요 · 🔴 QWK·MAE 를 다른 방법으로 낼 수 있다고 적지 마세요 — 사람 라벨이 없으면 정의되지 않습니다
  • 관련: 3장 「검증 기준」 · 8장 KPI · 6장 재현성 문장 · 같은 구역 2번(지도자 섭외) · 7번(fps 37%) · 22번(타당성) · 31번(산출물 이름)
  • 담당: 박민호(제안서 검증 기준) · 제기: 정상호 · 기한: 스프린트 2 리뷰 전 (09-14) 결정함

✅ 결정 (2026.09.11, 박민호)

권고안 그대로 갑니다. 셋 다 예입니다.

  1. 3장 검증 기준 표 변경 — QWK·MAE는 지우지 않고 「골든셋 확보 시」 조건부로 낮춤. 재현성·fps 불변성·지표 타당성을 사람 라벨 없이도 잴 수 있는 1차 지표로 전면에 둠. 반영: 03-서비스제안.markdown 「검증 기준」 표
  2. 8장 KPI 표에 셋 추가 — 채점 재현성·fps 불변성·지표 타당성 세 행을 08-테스트및검증계획.markdown KPI 표에 넣음(목표치 미정인 둘은 [TBD]로 남김)
  3. 임계값 출처 조사를 정상호 님 일감으로 — 스프린트 2~3 중 진행. 생체역학 문헌·공개 지도 자료를 인용해 등급 경계값을 「임의값」에서 「인용값」으로. 분포에 맞춰 긋지 않는다는 원칙은 그대로

05-요구사항분석.markdown의 CON-005·ASM-004도 같은 방향으로 갱신함(ASM-004는 「깨짐 — 섭외 불가 확인」으로 표기).

같은 구역 33번(스프린트 3 적합도 판단)도 이 결정과 묶어서 같이 정함 — 33번 참고.

✅ 결정 3(임계값 출처 조사) 완료 — 「임의값」이 「인용값」이 됐고, 그 인용이 우리 값과 어긋납니다 (2026.09.14)

사전 등록을 찾기 전에 걸고(b8dd698) 돌렸습니다. 근거: agent/eval/pending34_thresholds/(PREREGISTRATION.md · RESULTS.md) · 정본 agent/contracts/rubric_evidence.yaml.

기준 내용 결과  
A 11개 세부기준 각각에 찾았다/못 찾았다가 정본에 남는가 11/11 ✅
B citation_verified: true 3건 이상 3건 ✅
C threshold_validity 가 11/11 unverified·blocked 그대로 11/11 ✅
D 문헌과 어긋나는 자리를 전부 열거 3건 ✅
E uv run pytest tests/test_rubric_evidence.py -q 7 통과 ✅

쓴 출처 셋: Kapidžić 외 2014(J Hum Kinet 42:81–90, 원 논문 전문) · Petrolo 외 인스텝 킥 체계적 문헌고찰(🔴 미간행 원고 — 학술지 게재 표기가 없고 OSF 등록만 있습니다. 본문 65쪽 열람) · CoachingAmericanSoccer.com 지도 자료.

🔴 가장 중요한 결과는 「출처가 붙었다」가 아니라 「붙여 보니 어긋난다」입니다.

항목 우리 2등급 문헌  
차는 다리 뻗기 내각 140~165 엘리트 남 접촉 시 내각 125~145 🔴 보고된 범위 대부분이 우리 0등급(「풀린 채찍」)
차고 난 뒤 마무리 추가 굴곡 30도 이상 엘리트 추가 69~91도 🔴 우리 문턱이 1/3 — 엘리트가 사실상 전원 2등급
디딤발 위치 어깨너비 0.5배(≈20cm) 이내 엘리트 27cm · 33cm 🔴 엘리트가 우리 1등급(유소년만 2등급)

🔴 그래도 임계값을 하나도 안 옮겼습니다. 사전 등록이 그렇게 정했고, 옮기려면 시점 문제가 먼저 풀려야 합니다 — 문헌은 공 접촉 시점이고 우리 임팩트는 extension_peak(무릎 신전 각속도 최대)인데 둘이 같다는 근거가 우리에게 없습니다(event_validity: unverified). rubrics/*.yaml · src/ 무변경이라 B-6 재실행을 부르지 않습니다.

🔴 공개 지도 자료는 이 수치화 자체를 권하지 않습니다 — “Some coaches have tried to quantify the distance in inches that the plant foot should be placed to the side of the ball. This is not recommended.” 우리가 하는 일이 정확히 그 수치화입니다. 지도자 검수(같은 구역 2번) 질문지에 위 셋과 함께 올립니다 — 문헌은 「엘리트 성인 최대 파워 슛」이라 생활체육 표본과 같지 않고, 그 차이 자체가 검수에서 물어볼 것입니다.

인사이드 패스(draft) 5개 항목은 문헌을 하나도 못 찾았습니다. 🔴 이것도 결과입니다 — active 로 올리는 날 이 5개는 인스텝보다 근거가 한 칸 더 얕은 상태로 올라갑니다.

🔴 정정 둘을 드러냅니다.

  1. 2026.09.12 에 조사를 끝내고 정본 반영을 통째로 빠뜨렸습니다. RESULTS.md 가 「rubric_evidence.yaml 에 적었다」고 써 두었는데 하나도 안 적혀 있었고 파일이 커밋조차 안 돼 있었습니다. 2026.09.14 에 실제로 반영했습니다.
  2. 판정 B 를 5건 → 3건으로 내렸습니다. 둘(plant_knee_flexion · hip_rotation)은 수치가 리뷰 근거표에만 있는데, 사전 등록 조건 1이 「2차 요약(리뷰의 표 포함)만으로는 안 된다」고 못박아 두었습니다. 합격선 3은 그대로 넘습니다. 🔴 그 조건이 헛돈 것이 아닙니다 — 같은 리뷰에서 본문과 근거표가 어긋난 자리를 실제로 잡았습니다(본문 「지지발 무릎 굴곡 21.4도」 대 근거표 「굴곡 ROM −21.4도」). 본문만 봤으면 「내각 158.6도 = 우리 2등급 한가운데」라는 없는 근거를 만들 뻔했습니다.
   
확인 cd agent && uv run pytest tests/test_rubric_evidence.py -q → 7 통과 · git status --porcelain agent/rubrics/ agent/src/ → 0줄(임계값 무변경)
하지 말 것 🔴 이 문헌 값으로 밴드를 옮기지 마세요 — 시점(extension_peak 대 공 접촉)이 안 풀렸습니다 · 🔴 인스텝 값을 인사이드 패스로 옮기지 마세요 — 리뷰가 인스텝 전용입니다 · 🔴 인용이 붙었다고 threshold_validity 를 올리지 마세요 — 문헌이 값을 든다는 것과 우리가 그 값을 옳게 잰다는 것은 다른 말이고, 측정 축이 이미 셋 반증됐습니다
  • 담당: 정상호 · 제기: 박민호(결정 3) · 기한: 스프린트 2~3 완료

33. 스프린트 3의 「적합도 판단」은 지금 착수할 수 없습니다 ✅ 결정 (2026.09.11, 박민호)

스프린트 3(09.15~)의 제 칸이 「실력 검증·적합도 판단 모델 고도화」인데, 착수 조건을 확인해 보니 세 축이 전부 없는 것에 걸려 있습니다. 6일 뒤에 알게 되는 것보다 지금 아시는 편이 낫다고 보고 올립니다.

SFR-006 의 정의(3장 4절)는 이렇습니다.

축 정의 지금 상태
수준 모집 팀이 요구한 실력 구간과 지원자 점수의 거리 🔴 요구 실력 구간을 담는 자리가 없습니다. match 는 team_id·played_at, match_position_need 는 position_id·head_count 뿐입니다 — 「어느 수준을 원하는가」가 어디에도 없습니다
역할 포지션 요구와 지원자 분석 지표의 부합도 🔴 position 테이블이 비어 있습니다(시드 없음). 그리고 포지션 → 어느 지표가 중요한가는 코칭 판단이라 제가 지어낼 수 없습니다 — 미결 2번(지도자 검수)입니다
성향 player_vector 유사도 🔴 player_vector 가 아직 없습니다(계약 「아직 없는 것」). 차원 설계는 같은 구역 32번에 냈습니다

제 쪽에서 막힌 것이 아닙니다. 지표는 나오고 점수도 나옵니다 — 비교할 상대와 기준이 없습니다.

판단해 주실 것

  1. 스프린트 3에서 적합도를 정말 할 것인가. 하려면 위 셋 중 최소 「요구 실력 구간」과 「포지션 시드」가 먼저 들어와야 하고, 그건 정어진 님 일감이 늘어난다는 뜻입니다 — 그쪽은 이미 벡터 저장·검색을 맡고 계십니다
  2. 아니라면 제 칸을 무엇으로 둘 것인가. 실제로 할 수 있고 값이 있는 것은 「실력 검증」 쪽입니다 — 임계값 검수(미결 2번)가 열리면 draft 루브릭 3개 승격과 미결 20·22·23번이 한꺼번에 풉니다. 🔴 그 병목도 결국 지도자 한 분 입니다
  3. 역할 축의 「포지션 → 지표」 매핑은 열어도 제가 혼자 못 정합니다. 지도자 검수 범위에 이것도 넣어 주시면 한 번에 받을 수 있습니다
   
확인 git grep -n 'fitness' -- fastapi/app → 결과가 없으면 아직입니다 (2026-09-09 기준 0건)
하지 말 것 🔴 요구 실력 구간 없이 적합도를 만들지 마세요 — 기준이 없으면 「거리」가 정의되지 않습니다. 그럴듯한 기본값을 두면 모든 지원자의 적합도가 같은 값으로 나오고 그 사실이 화면에 안 보입니다
  • 관련: 3장 4절 「적합도 판단」 · SFR-006 · 부록 D fitness_score · 같은 구역 32번(성향 축) · 미결 2번(지도자 검수) · 같은 구역 31번(산출물 이름) · 같은 구역 34번(검증 기준 결정)
  • 담당: 박민호(스프린트 3 범위 판단) · 제기: 정상호 · 기한: 스프린트 3 계획 전 (09-14) 결정함

✅ 결정 (2026.09.11, 박민호)

  1. 스프린트 3에서 적합도 판단은 뺍니다. 세 축(수준·역할·성향)이 전부 없고, 그중 둘(요구 실력 구간·포지션 시드)은 정어진 님 스키마 작업이 먼저인데 지금 범위에 없습니다. 억지로 열면 3번이 경고한 대로 모든 지원자가 같은 값으로 나오고 그게 화면에 안 보입니다 — 하지 않습니다.
  2. 정상호 님 스프린트 3 칸을 이렇게 바꿉니다: 「채점 시스템 신뢰성 강화 — fps 불변성 원인 규명·개선 + 임계값 출처 조사(34번 결정)」. 둘 다 지도자 없이 진행 가능하고, 34번에서 막 1차 검증 지표로 올린 것들이라 지금 우선순위가 맞습니다.
  3. 포지션→지표 매핑은 보류합니다. 지도자 검수가 열릴 때(2번)까지 손대지 않습니다 — 코칭 판단이 필요한 자리라 혼자 정할 수 없다는 말씀 그대로 받습니다.

요구 실력 구간·포지션 시드는 정어진 님과 스프린트 4 이후 별도로 다시 잡습니다 (지금 스프린트 3 범위에 넣지 않음).

38. breakdown[] 에 필드가 하나 늘었습니다 — view_dependent (2026-09-10 신설) ✅ 해소 (2026.09.16 확인 — 실제로는 2026.09.10에 이미 됨)

  • 정어진(적재 컬럼 판단) ✅ 이미 됨 · 제기 정상호

미결 37번 처방 (다)를 넣으며 result.breakdown[] 에 view_dependent(string, ""·"metric"·"grade")가 늘었습니다 — 그 항목의 등급이 촬영 방향에 의존하는가입니다. 🔴 정정: 필드만 늘리고 봉투 버전을 안 올렸습니다 — 계약 파일에 적어 둔 절차를 제가 어겼고, 그 사이 받는 쪽은 열한 개짜리와 열두 개짜리 봉투를 구분할 방법이 없었습니다. schema_version 1.0 → 1.1 로 맞추고 변경 이력을 남겼습니다. 적재 쪽은 major 만 보므로 안 깨집니다.

40. breakdown[] 에 필드가 또 하나 늘었습니다 — title_earned (2026-09-11 신설) ✅ 해소 (2026.09.16)

  • 정어진(적재) ✅ 완료 · 백성검(화면 규칙) ✅ 완료 (2026.09.16) · 제기 정상호

breakdown[] 에 title_earned(boolean, 항상 옴)가 늘고 schema_version 1.1 → 1.2(38번에서 어겼던 절차를 이번엔 지켰습니다). 왜: title 은 모든 등급에 있는데 받는 쪽이 그걸 응답만으로 알 수 없어 화면이 「title 이 있으면 받은 호칭」으로 읽었고, 0등급 항목에 「무너지는 축」을 호칭으로 달고 있었습니다. 참이 되는 조건은 최고 등급(2) + 루브릭이 그 등급 문구를 실제로 적었을 것입니다. 38번과 달리 이건 선수 화면이 실제로 쓰는 값이라 적재까지 받았습니다.

44. 검출된 사람 목록을 화면에 주는 경로가 없습니다 — 자리를 정해 주세요 (2026-09-11 신설) ✅ 정어진 몫(결정+배선) 끝 (2026.09.15) · ✅ 워커 배선 끝 (2026.09.16) · ✅ 화면은 안 붙이기로 했습니다 (2026.09.17) · ✅ 실물 확인 끝 (2026.09.18) — 종결

  • 정어진(내주는 자리) ✅ · 정상호(워커 배선·실물 확인) ✅ · 백성검(화면) ✅ 안 붙이기로 · 제기 정상호(사용자 요청)

43번 ㉯(여러 명 중 골라서 분석)의 계약 조각. 분석을 걸기 전에 「이 영상에 잡힌 사람들」을 화면이 받는 경로가 없었고, 🔴 결정이 필요했던 이유는 GPU 인스턴스가 autostop 으로 잠들어 동기 API 가 성립하지 않는다는 것이었습니다.

  • 결정: 안 (가) 큐 기반 detect 작업 (정어진, 2026.09.15) — 기존 analysis_job 의 같은 큐·클레임·폴링을 재사용하고 job_type(analyze| detect)·detection_result 컬럼만 추가. 계약은 worker-interface.md 6절
  • 에이전트 몫 — pose.detect_candidates · scripts/detect_subjects.py (51d7c75). 🔴 좌표는 정규화 0~1 이고 낸 박스가 그대로 --subject-box 로 돌아갑니다(변환 금지) · 프레임은 --subject-at-ms 와 같은 함수 (anchor_frame_for) · 순서는 넓이 내림차순 · 사람 0명은 실패가 아닙니다 · 결과는 --result-json 파일로 (🔴 stdout 을 긁지 않습니다, paik 11번)
  • 🔴 「공에 가장 가까운 사람」 추천은 넣지 않았습니다 — 임팩트 뒤에는 공이 이미 떠나가 그 순간 공에 가까운 사람이 찬 사람이 아닌 경우가 흔합니다. 공 위치는 주되 자동 선택은 하지 않습니다(틀리면 사용자가 틀린 줄 모릅니다)
  • 워커 배선 (2026.09.16) — 모르는 job_type 은 failed(추측하면 틀린 결과가 succeeded 로 갑니다) · detect 성공엔 report_key 없음 · 후보 JSON 무변환 · subject_at_ms 없으면 failed · 검출의 종료 코드 2를 분석 문구로 쓰지 않음(41번의 형태). 🔴 분석 경로는 한 줄도 안 바뀌었습니다(구 백엔드 호환). 🔴 autostop 의 BUSY_PATTERN 에 detect_subjects 를 더했습니다 — 배포된 /etc/.../autostop.conf 는 자동으로 안 바뀝니다
  • 🔴 화면은 안 붙이기로 (2026.09.17, 사용자 판단) — 브라우저가 이미 직접 찾고(www/src/lib/personDetector.ts), 네모가 없으면 가장 큰 박스를 고르는 규칙이 에이전트의 _largest_person_box 와 같습니다. 서버 경로로 갈아타면 느려지기만 합니다. 되살릴 조건: 브라우저 검출이 못 하는 것이 드러나면 (저사양 기기에서 느리다 · 화면과 서버의 검출이 실제로 갈린다)
  • 실물 확인 (2026.09.18) — 워커가 만드는 명령 그대로 GPU 에서: 10.2초, 후보 2명(score 0.887·0.815). 🔴 큐 leg(claim → job_type=detect)은 안 돌렸고 앞으로도 안 돕니다 — 화면이 안 붙어 그 작업을 큐에 넣을 사람이 없습니다. 분기는 tests/test_worker.py·tests/test_detect_candidates.py 가 잡고 있고, 호출자가 생기는 날 실물로 확인합니다
  • 🔴 하지 말 것: PERSON_ELIGIBLE_THRESHOLD(0.5)를 낮춰 후보를 늘리기 (selector 동작 기준이라 B-1~B-6 이 전부 무효) · 좌표를 화면 픽셀로 주고받기
  • 이 검출은 EC2 의 Pil 전처리 경로에서 돌았습니다(49번). 47번 5회차가 「검출 박스는 19/19 그대로, 흔들리는 것은 포즈 이후」라고 쟀으므로 후보 목록은 그 차이에 덜 민감하지만, 무관하다고 재 본 적은 없습니다

54. 모델 가중치의 사본이 EC2 한 대뿐이었습니다 ✅ S3 백업 완료 (2026-09-17 신설·해소)

  • 정상호 ✅ 끝 (2026-09-17) · 제기 정상호

인스턴스를 종료하면 판정 모델 가중치가 사라지는 상태였습니다(실물이 EC2 EBS와 개발 기계 HF 캐시 둘뿐). models/exaone-4.0-1.2b/ 13객체 2.4GiB 를 S3에 올리고 deploy/sync_model.sh 로 되받아 왕복 검증했습니다 — S3에서 받은 model.safetensors sha256 = EC2 원본 = 고정 리비전 블롭, 셋 다 동일. 🔴 배운 것: 미결 11번이 리비전을 커밋 해시로 고정하며 닫혔는데 사본은 안 봤습니다. 해시는 「무엇을 받아야 하는지」만 말하지 그것이 계속 거기 있다를 보장하지 않습니다 — 업스트림이 내리면 B-6 재실행이 그 자리에서 막힙니다. 쓰기 권한은 버킷 정책으로 잠깐 열었다가 회수했습니다.

55. reports/ 에 probe 파일 두 개가 남아 있습니다 — 지워 주세요 (2026-09-17 신설) ✅ 해소 (2026.09.17)

  • ✅ 사용자가 콘솔에서 지웠습니다 (2026.09.17) · 제기 정상호

접근 확인용 1~6바이트 probe 파일 둘이 reports/ 에 남아 있었습니다(EC2 역할에 s3:DeleteObject 가 없어 인스턴스 안에서 못 지웁니다). 확인: aws s3 ls … reports/ --recursive | grep -i probe → 결과 없음. 🔴 이전 버전까지 지워졌는지는 확인 못 했습니다 — 역할에 s3:ListBucketVersions 가 없어 목록에서 사라진 것까지가 본 것입니다. 🔴 재발 방지: 지울 수 없는 자리에 확인용 파일을 쓰지 않습니다 — 두 번째 중 하나는 제가 흘린 것이고, 회수 확인은 models/ 쓰기 한 번으로 충분했습니다.

jin (정어진)

1. 분석 결과 적재 규격 — 지표 코드가 종목을 넘나든다 ✅ 해소 (2026.09.08)

A안 채택 — metric_definition에서 sport_code 제거(스키마 반영 5db18b239336, 2026-09-08). 근거: 공유 지표 7/11(64%), 부록 D 제2정규형(복합키는 review_selection 하나뿐)과 충돌. 🔴 항목별 등급은 A안으로 안 풀림 — grade.{sport}.{motion}.{criterion_id} 로 별도 해결(jin 17·23번). 시드·부록 D.3 반영은 각각 jin 23번·박민호 몫으로 이어짐.

  • 담당: 정상호 · 제기: 정어진 · 기한: 스프린트 2

2. 클라이언트의 백엔드 계약 반영 — www ✅ 해소 (2026.09.10) · flutter ✅ 해소 (2026.09.10)

www·flutter 둘 다 완료 (2026.09.10). 429 Retry-After 뒤 재요청 억제 — BackendError/handler.ts/client.ts/rateLimit.ts 세 층을 이어 헤더가 프록시를 관통하게 함. 구글 로그인 버튼은 잠그지 않음(클릭 자체가 무시되므로). Flutter는 AuthException에 code·retryAfter를 추가해 www와 같은 모양을 그대로 따름 — 헤더가 프록시 없이 바로 옴.

  • 담당: 백성검 · 제기: 정어진 · 기한: 스프린트 2

3. 스프린트 1이 끝났는데 칸반·스프린트 로그가 갱신되지 않았다 ✅ 해소 (2026.09.03)

박민호가 로드맵 표 연결·스프린트 로그 채움·칸반 교체 셋 다 완료(2026.09.03).

  • 담당: 박민호 · 제기: 정어진 · 기한: 스프린트 2 초

4. 부록 D의 종목 서술이 실물과 다릅니다 (한 줄) ✅ 해소 (2026.09.03)

부록 D sport 행을 “풋살·야구·현재 2행”에서 “축구·야구·농구·현재 3행”으로 정정 완료(박민호, 2026.09.03).

  • 담당: 박민호 · 제기: 정어진 · 기한: 문서 정리 시

5. 병합할 때 이 파일의 구역 구조를 유지해 주세요 ✅ 해소 (2026.09.11)

2026-09-11 ho·jin을 main에 --no-ff로 병합, 남의 구역 번호는 그대로 둠. 「확인」 grep -n '^## [0-9]' jekyll/pages/pending.markdown → 결과 없음. 규칙은 CLAUDE.md 「미결 항목」에 계속 있어 다음 병합에도 적용됨.

  • 담당: 박민호(병합하는 사람) · 제기: 정어진 · 기한: 다음 main 병합 시

6. 미결 9번의 담당 표기를 확인해 주세요 (정상호 님) ✅ 해소 (2026.09.03)

착오 맞았음 — e59c0b6에서 잘못 적힌 것. ho 9번 담당을 정상호로 정정(2026.09.03). jin에 남은 담당: 정어진(구역 4·16번)은 조달·계정 권한이라 의도된 것.

  • 담당: 정상호 · 제기: 정어진 · 기한: 미결 7번 수정 착수 전

7. 카드를 만드는 자리를 화면에 두어 주세요 (2026-09-02 신설) ✅ 해소 (2026.09.04)

www에 붙임(2026.09.04) — /me?edit=1 프로필 카드 수정 안에 카드 만들기, 누를 때만 POST /api/me/card 호출. og_image_key는 이미지로 안 그림(자리에 파일 없음). 🔴 만든 뒤 손댈 게 없음(별명·사진이 붙박이) — paik 3번으로 이어짐. Flutter는 아직.

  • 담당: 백성검 · 제기: 정어진 · 기한: 스프린트 2

8. 배포 — 백엔드가 EC2에서 돕니다 ✅ 해소 (2026.09.03)

전부 끝남 (2026.09.03~04). 탄력적 IP·IAM 인스턴스 역할(S3 videos/* 한정)· A 레코드·nginx+Let’s Encrypt(80→443 리다이렉트) 다 붙었고, <API 호스트>/health가

  1. DB는 같은 EC2 인스턴스에(RDS 아님, DATABASE_URL만 바꾸면 이전 가능). 백업은 하루 1회 pg_dump(같은 디스크 — 별도 반출은 비용 결정 필요). 🟡 아직 없는 것: HSTS·client_max_body_size(기본 1MB)·proxy_read_timeout(기본 60초, ho 17번과 연결). 상세 절차 전부: fastapi/docs/deployment.md 6·7·9절.
  • 담당: 박민호(A 레코드 · 그 뒤 포트·인증서) · 제기: 정어진 · 기한: 외부 공개를 결정할 때 ✅ 2026-09-03 에 다 하셨습니다

9. agent/·flutter/ 에 진입점 문서가 없습니다 (2026-09-02) ✅ 해소 (2026.09.10)

agent/CLAUDE.md(정상호)·flutter/CLAUDE.md(백성검, 2026.09.10) 둘 다 생김 — 네 폴더(fastapi·www·agent·flutter) 전부 진입점 문서를 가짐. 각자 그 폴더에서만 통하는 관례만 적고 루트 규칙은 복사하지 않음.

  • 담당: 정상호(agent/) · 백성검(flutter/) · 제기: 정어진 · 기한: 스프린트 2 안

10. 웹이 가짜 데이터에 고정돼 있습니다 — 백엔드는 떠 있습니다 (2026-09-02) ✅ 해소 (2026.09.03)

박민호가 paik-1(영상 저장) 테스트 중 로그인이 mock인 것을 발견해 선제 처리 (2026.09.03) — fastapiBackend.ts에 Backend 10개 메서드 구현, getBackend()가 USE_MOCK=1 아니면 실제 백엔드로. 🔴 Vercel Preview 환경엔 BACKEND_BASE_URL이 없어 Preview 로그인은 503 — mock으로 확인하려면 Preview에 USE_MOCK=1 추가 필요. 이후 로그인·회원가입·프로필·관리자 화면이 실제 DB를 봄(mock 데모 계정 안 먹힘).

  • 담당: 백성검 · 제기: 정어진 · 기한: 스프린트 2 안

11. 업로드 길이 상한 60초와 max_frames=300이 안 맞습니다 (정상호 님, 2026-09-03) ✅ 제 몫은 끝났습니다 (2026.09.09) — 남은 것은 제품 판단(ho 30번)

기술 판단은 정상호가 냄, 제품 판단(60초 유지 여부)은 박민호가 결정 — 10초로 (ho 30번, 2026.09.11). 근거: 분석 창은 DEFAULT_MAX_SECONDS(메모리 가드 max_frames와 별개), 해상도별 창 상한 실측(4K 10초·1080p 40초·720p 90초), 조용한 절단 방지용 timebase.truncated·source_seconds·limited_by 필드 추가 완료. MAX_DURATION_MS를 60→10초로 반영. 🔴 정정 (2026-09-23): 반영된 적이 없습니다 — MAX_DURATION_MS 는 09-03(3030c94)부터 60초 그대로입니다(git log --all -G'MAX_DURATION_MS\s*=' → 그 커밋 하나).

🔴 결정 (2026-09-23, 정어진) — 60초로 둡니다

09-11 의 10초 결정을 되돌립니다. 코드·계약은 처음부터 60초였으므로 바뀌는 것은 없고 기록만 실물에 맞춥니다. 60초 클립을 올리면 분석은 여전히 앞 10초만 보고, 잘린 사실은 결과(timebase.truncated)에 남습니다 — 조용한 절단이 아닙니다. 부록 E 결정 기록 47·75번.

  • 담당: 정상호(기술 판단) ✅ 냈습니다 (2026.09.09) → 박민호(제품 판단, 정본 ho 30번) → 정어진(MAX_DURATION_MS) 60초 유지라 할 일 없음 (2026-09-23) · 제기: 정어진 · 기한: 스프린트 2 안

12. 클립 업로드가 열렸습니다 — 붙일 자리가 생겼습니다 (2026-09-03 신설) ✅ 해소 (2026.09.03)

박민호가 웹에 붙임(2026.09.03) — 영상 분석 화면(AnalysisStage.tsx)의 저장 버튼, 예상 위치(/videos/upload)와 다르지만 목적 달성. 🔴 paik 1번의 대가와 연결 — 영상만 저장되고 화면의 가짜 리포트는 같이 안 저장됨(그 저장 형식은 정어진 몫으로 남음). Flutter는 아직.

  • 담당: 백성검 · 제기: 정어진 · 기한: 스프린트 2 안

13. 백엔드 미구현 분담 — 과금 도메인을 맡아 주십시오 (백성검 님, 2026-09-03) ✅ 해소 (2026.09.10)

app/billing/ 세 테이블(analysis_credit·coach·coach_referral) 전 계층 완료 (2026.09.10) — 마이그레이션 20260908_billing_tables.py, 테스트 tests/billing/, app/main.py 배선까지. 잔량 컬럼 없이 SUM(delta), coach에 user_id 없음 지켜짐. 종목 코드 불일치(soccer/football)는 www/src/lib/sports.ts의 SPORT_CODE 경계 변환 한 곳으로 해소.

  • 담당: 백성검 · 제기: 정어진 · 기한: 스프린트 3

14. 백엔드 미구현 분담 — 평가·신뢰 도메인을 맡아 주십시오 (박민호 님, 2026-09-03) ✅ 해소 (2026.09.04)

🔴 박민호가 마이그레이션까지 하고 남은 응용 계층을 정어진이 이어받아 완료 (2026.09.04, 사용자 지시) — review_router 등 전 계층, 516 passed. 정한 값 셋: 평가 가능 기간 경기 후 14일 · 불참 기록 권한 주최 팀 주장만 · 선택지 노출 순서 sort_order 컬럼(근거는 review_rules.py). 신뢰도 점수·review 총점· report/no_show↔review 연결 금지는 설계대로 지켜짐.

  • 담당: 박민호 · 제기: 정어진 · 기한: 스프린트 3

15. 경기 탐색이 열렸습니다 — /matches 화면이 그려집니다 (백성검 님, 2026-09-03) ✅ 해소 (2026.09.10)

백성검이 거르는 자리(종목·지역 알약)를 붙임(2026.09.10) — 서버 사이드 필터, sport_code= 생략이 “전체”(빈 값 아님 — 빈 값은 422), soccer↔football 경계 변환. ⚠️ 포지션 필터·지원(신청)은 이번 범위 밖으로 아직 없음.

  • 담당: 백성검 · 제기: 정어진 · 기한: 스프린트 3

16. 지원이 붙은 경기를 취소할 방법이 없습니다 — 결정이 필요합니다 (박민호 님, 2026-09-03) ✅ 해소 (2026.09.04)

A-1 결정 — 거절=행 삭제(정어진 판단·구현, 2026.09.04). DELETE /matches/{id} /applications/{id} → 204, 당사자면 무르기·주장이면 거절, 둘 다 행 삭제. 부록 D 안 건드림(넷 중 유일). 지난 경기는 422로 막음(평가가 근거 행을 잃으므로). 잃는 것은 거절 이력 하나 — 나중에 필요하면 별도 테이블로.

  • 담당: 박민호 · 제기: 정어진 · 기한: 스프린트 3

18. 분석 워커 — 백엔드 쪽은 냈습니다. 폴링 루프를 부탁드립니다 (정상호 님, 2026-09-04) ✅ 해소 (2026.09.07)

정상호가 agent/scripts/worker.py + supersub-worker.service로 완료 (2026.09.07), EC2 설치·WORKER_TOKEN 주입까지 끝(2026.09.08). 종료 코드 계약 정정(SystemExit가 삼켜져 실패가 성공으로 보고되던 버그 수정) · BUSY_PATTERN은 늘리지 않음(자식 프로세스로 이미 걸림, 늘리면 autostop이 영원히 “작업 중”으로 봄). POST /analyses 적재는 jin 1번(A안) 이후.

  • 담당: 정상호 · 제기: 정어진 · 기한: 스프린트 3

19. 부록 D 에 컬럼 둘을 더해 주십시오 — sort_order · tagline (박민호 님, 2026-09-04) ✅ 해소 (2026.09.08)

박민호가 부록 D 도메인 ③·⑤ 표에 review_option.sort_order(정수, 필수)· player_card.tagline(문자 20, 선택) 추가 완료(2026.09.08). ERD .svg 그림은 아직(표가 최신이라는 문구만 남김 — 그림 갱신은 별도 작업).

  • 담당: 박민호 · 제기: 정어진 · 기한: 문서 정리 시

20. 미결 1번(적재 규격)·18번(워커 폴링 루프) 회신을 부탁드립니다 (정상호 님, 2026-09-07) ✅ 해소 (2026.09.08)

1번(A안 결정)·18번(폴링 루프)·워커 방식 수렴(/internal/* 하나로, --skip-analyzed 제거) 셋 다 완료·EC2 설치까지 끝(2026.09.08). 🎉 같은 날 저녁 끝까지 실물로 시연 — claim→분석→보고 성공(68초). 남은 것은 POST /analyses 적재뿐(jin 1번 스키마 반영 후).

  • 담당: 정상호 · 제기: 정어진 · 기한: 스프린트 3 (17번 시연 목표에 걸림)

21. 공개 사이트에서 인프라 식별자를 걷어냈습니다 (2026-09-07) ✅ 해소 (2026.09.15, S3 콘솔 확인까지 완료)

문서·코드에 남아있던 AWS 계정 ID·공인 IP·VPC/서브넷/보안그룹/인스턴스/EIP·IAM 역할명·API 호스트명을 자리표시자로 교체 완료(2026-09-07~10, agent/deploy/ 값은 22번으로 분리 — 정상호 영역). 실효 방어인 S3 버킷(supersub-ai) 콘솔 확인도 2026-09-15에 완료 — Block Public Access 4개 전부 켜짐, 버킷 정책엔 익명 허용 없이 HTTPS 강제 Deny 규칙뿐, ACL엔 AllUsers·AuthenticatedUsers 권한 없음(스크린샷 확인). git 히스토리에 남은 옛 값은 회수 불가라 노출 최소화가 전부. API 원본 IP를 Cloudflare 프록시로 숨기는 것은 검토만 하고 안 함 — 필요해지면 재검토.

  • 담당: 정어진 · 제기: 정어진 · 기한: 해소됨

22. agent/deploy/ 에 AWS 계정 ID·리소스 ID·API 호스트가 값으로 남아 있습니다 (2026-09-08) ✅ 해소 (2026.09.09)

정상호가 agent/deploy/README*.md·worker.env.example의 계정 ID·리소스 ID·API 호스트를 자리표시자로 교체, worker.py의 하드코딩 fallback 제거(빈 값이면 ConfigError로 미기동)(2026.09.09). 안 건드린 것: S3 버킷 이름·IAM 정책/역할 이름(계정 안에서만 의미 있는 이름이라 판단 보류, 손대지 않고 올림). ami-... 1건은 공개 AMI ID라 의도적으로 남김.

  • 담당: 정상호(agent/ 문서·worker.py 설정) · 제기: 정어진 · 기한: 계정 ID·공인 IP 는 이번 주 · 리소스 ID·호스트는 스프린트 3 → 해소 (2026.09.09, 두 조각 함께)

23. metric_definition 을 누가·어떻게 채웁니까 — POST /analyses 착수 전에 필요합니다 (2026-09-08) ✅ 시드 해소 (2026.09.10)

정상호가 agent/contracts/metric_definitions.yaml로 목록·형식을 냄(2026.09.09) — 🔴 처음 「11개」가 아니라 29개(측정 12 + 총점 1 + 항목별 등급 16, impact_frame 포함). 등급 코드는 종목이 들어가야 함 — grade.{sport}.{motion}.{criterion_id}(항목 id만으로는 종목 간 충돌). 정어진이 시드 마이그레이션 완료(2026.09.10, ca31a2180b54, 45행 — stat 16개 포함, jin 25번과 합류). 남은 것(적재 경로·읽기 규격)은 jin 24번에서 이어짐.

  • 담당: 정상호(코드 목록·형식) ✅ 냈습니다 (2026.09.09) → 정어진(시드 마이그레이션) ✅ (2026.09.10) · 제기: 정어진 · 기한: 스프린트 3 초 (POST /analyses 착수에 걸림)

24. 영상 수명 주기 — 분석은 임시, “저장”을 눌러야 남는다 · videos/ 는 원본, reports/ 는 저장된 것 (2026-09-08 신설) ✅ 6조각 전부 해소 (2026.09.11, 5조각이 마지막이었다)

전체 6조각 완료(2026.09.08~11). 설계: video.kept 불리언 — /analysis 분석은 기본 미저장(GET /videos에서 숨음), “내 프로필에 저장” 누르면 POST /videos/{id}/keep이 kept=true로 바꾸고 S3 원본을 videos/→ reports/<user_id>/<video_id>/source.<ext>로 이동(CopyObject+DeleteObject, 멱등). 미저장분은 두 겹으로 정리 — 브라우저 beforeunload가 DELETE /videos/{id} 호출(빠른 길) + 서버 스윕이 PROVISIONAL_VIDEO_TTL_HOURS(기본 24)로 백스톱(워커 claim 호출에 편승, 새 타이머 없음). DELETE /videos/{id}는 DB 연쇄(SEC-006)+S3 전체 삭제.

S3 키: videos/<user_id>/<닉네임 슬러그>-<원본이름 슬러그>-<YYYYMMDD-HHMM>- <video_id 앞 8자>.<ext> — <user_id>/ 접두사는 소유 검사용이라 유지, 닉네임은 업로드 시점 값(rename해도 옛 키는 안 바뀜, 소유는 user_id로 유지). video.original_filename 컬럼 추가. GET /admin/videos?user=· DELETE /admin/videos/{id} 신설.

🔴 5조각 정정 (2026.09.11) — 사용자가 “분석 실패 영상이 안 지워진다”고 지적해 발견: 전환 조건을 kept = not analyze가 아니라 kept = not make_job으로 바꿈 — 반려된 클립(작업 자체가 안 생김)은 즉시 미저장 처리되면 반려 사유를 보여준 클립이 곧장 사라지는 문제가 있었음.

정상호 조각: 리포트 산출물 키를 reports/<user_id>/<video_id>/로 정렬 (2026.09.08~11, 확인해보니 이미 계약 자리로 가고 있어 추가 수정 불필요). 리포트 봉투에 video_id 추가 — keep 뒤 source_video가 죽는 키를 가리키는 문제를 우회(읽는 쪽이 video_id로 되짚음). 🔴 등록 안 된 고아 S3 객체(업로드만 하고 POST /videos 안 부른 것)는 DB 스윕이 못 잡음 — S3 lifecycle 규칙이 맞다고 보되 판단은 보류.

  • 담당: 정어진(백엔드 수명 주기) · 정상호(리포트 키 정렬) · 제기: 정어진(사용자 요청) · 기한: 스프린트 3

25. metric_definition 시드에 항목별 stat 코드를 추가해 주세요 — jin 23 후속 (2026-09-09) ✅ 해소 (2026.09.10)

정상호가 stat.{sport}.{motion}.{criterion_id} 코드 산출 완료(2026.09.09, active 45행, 최장 code 48자 < String(50) — 컬럼 확장 불필요). impact_frame은 metrics[] 행 유지. 정어진이 시드 마이그레이션(ca31a2180b54, jin 23번과 한 커밋)으로 반영 완료(2026.09.10).

  • 담당: 정상호(stat 코드 산출) ✅ 냈습니다 (2026.09.09, 확인은 2026.09.10) → 정어진(시드 마이그레이션) · 제기: 정어진 · 기한: jin 23 과 함께 (스프린트 3 초)

26. GET /positions 를 냈습니다 — 포지션 하드코딩을 걷어 주세요 (2026-09-09) ✅ 해소 (2026.09.10)

정어진이 GET /api/v1/positions?sport_code=를 냄(응답 [{sport_code, code, label}], 없는 종목은 422 UNKNOWN_SPORT). 백성검이 세 곳(챗봇 프롬프트·스쿼드 판·모집 등록)의 하드코딩을 걷어내 갈아 끼움 (2026.09.10) — 코드만으로 이름을 찾지 않음(야구 C·농구 C는 다름), 빈 sport_code는 안 보냄.

  • 담당: 백성검(www 3곳 반영) · 제기: 정어진 · 기한: 스프린트 3 (급하지 않음 — 지금 하드코딩도 동작함)

27. 분석 리포트 적재 방식이 초안과 실물이 어긋납니다 — 통일해야 POST /analyses 를 짤 수 있습니다 (2026-09-10 신설) ✅ 제 몫 회신 (2026.09.10)

권고안 채택 — 적재 입력은 S3 report.json 하나(metrics[]를 본문에 중복 제출하지 않음). 스키마는 agent/contracts/report_schema.yaml(정상호, schema_version 필드로 버전 관리 — 지금 1.1, minor는 필드 추가·major 변경은 적재 거부)로 고정. breakdown[] 11필드(criterion_id·name·grade·weight· contribution·title·band·out_of_band·stat·evidence·metric_ref)를 담을 새 테이블 analysis_metric_criterion(analysis_metric_id당 여러 행, uq(analysis_metric_id, criterion_id))로 결정(2026.09.10) — 항목당 1행 구조라 DTO 허용목록이 명시적. analysis_report에 provisional·previews· keypoint_quality·schema_version·overall_grade 컬럼 추가.

정어진이 3단계(마이그레이션→적재→읽기) 구현 완료(2026.09.10~11) — GET /videos/{id}/report가 DB에서 조립, total_score·overall_grade· breakdown[].stat까지 포함(ho 28번). view_dependent(등급이 촬영 방향에 의존하는가)는 개발 확인용으로 저장하되 선수 화면 DTO에는 안 넣음(ho 38번, band·out_of_band와 같은 취급). report_schema.yaml이 agent/contracts/에 이미 전 브랜치에 퍼져 있던 것을 확인(박민호, 2026.09.11).

  • 담당: 정어진(적재·읽기 API 구현) · 정상호(스키마 계약 고정) ✅ 냈습니다 (2026.09.10) · 제기: 정어진 · 기한: 스프린트 3

29. k3s 트라이얼 파드가 크래시 루프 중 — 이미지가 레포와 어긋나 있습니다 ✅ 해소 (2026.09.11)

원인: 운영 호스트 Postgres에 pgvector가 OS 레벨로 미설치 — initContainer의 ADD COLUMN ... VECTOR(768) 마이그레이션이 계속 실패. PGDG 저장소로 설치 + CREATE EXTENSION vector로 해소(정어진, 2026.09.11) — 함정 둘(GPG 검증 일시 실패, Amazon 빌드 PostgreSQL 경로 어긋남)과 절차는 deployment.md §1. 파드 1/1 Running, supersub-cd.service 정상화. 상세는 31번(k3s cutover 전체).

  • 담당: 박민호(k3s 이관 소유 — min 11·14) · 제기: 정어진 · 기한: k3s cutover(min 14 step 5) 전

31. 운영 백엔드가 09-08(d15806c)에 멈춰 있다 — k3s cutover 로 최신화 ✅ 해소 (2026.09.11, 4단계까지 전부 완료)

09-08 이후 정체돼 있던 운영 백엔드를 k3s cutover로 최신화 완료(2026.09.11) — (1) pgvector 설치+확장 생성(정어진) (2) 마이그레이션 자동 적용 확인, DB가 단일 head로 전진 (3) k3s 파드 1/1 Running 확인 (4) 박민호가 nginx proxy_pass를 :8000→:8080으로 전환, supersub-api.service는 disable만(롤백 경로 유지). 스모크(/health 200, 인증 필요 라우트들 401) 통과 — 이때부터 실제 트래픽이 k3s 파드로 나감. 진행 중 발견된 “매니페스트가 저장소에 없음” 문제는 별도 32번으로 분리.

  • 담당: 정어진(1~3: pgvector·확장·마이그레이션 적용·파드 확인) ✅ 완료 (2026.09.11) · 박민호(4: min 14 step 5 트래픽 전환) ✅ 완료 (2026.09.11) · 제기: 정어진 · 기한: 스프린트 3 / k3s cutover(min 14 step 5) 해소

32. supersub-api-trial 배포 매니페스트가 저장소에 없습니다 — ~/k3s-trial/에만 있습니다 (2026-09-11 신설) ✅ 해소 (2026.09.15)

서버에서 실제로 돌던 Deployment를 그대로 옮겨 fastapi/deploy/k8s/deployment.yaml 로 커밋(구조가 서버 실물과 동일한지 대조 완료). 이름·네임스페이스는 supersub-api-trial/default 그대로 두고(정리는 별도), deployment.md의 ns: supersub·deploy/api 오기재도 같이 정정. Secret(supersub-api-env) 키 목록은 이미 .env.example에 8개 다 있어서(확인 완료) 별도 템플릿은 안 만듦 — 값은 여전히 서버에만 있고 커밋 안 함. 🔴 파드는 .env 가 아니라 이 Secret 에서 값을 읽는다 — .env 만 고치고 재시작해도 반영되지 않는다(2026-09-11 ADMIN_EMAILS 등록 때 겪음).

  • 상세: fastapi/deploy/k8s/README.md · fastapi/docs/deployment.md 2절
  • 담당: 정어진 · 제기: 박민호(31번 진행 중 발견) · 기한: 해소됨

34. ✅ 해소 (2026.09.11) — 사용자가 화면에서 직접 잡은 버그 둘, 양쪽 다 고쳤습니다

사용자가 화면에서 직접 지적한 버그 셋, 전부 조치 완료.

(가) 분석 실패가 “아직 안 끝남”과 구별 안 됨 → 404 ANALYSIS_FAILED(사유 포함) 신설, www도 반영(client-contract-changes.md 34번).

(나) 업로드 해상도 상한이 1080p로 막혀 있었음 → ho 9번이 이미 4K를 메모리 안전으로 확정해 뒀는데 video_rules.py만 후속 안 됨. 4K(2160×3840, 방향 무관)로 상향(02e853e). 곁가지로 세로 촬영 1080p가 방향 때문에 반려되던 버그도 같이 수정.

(다) 진행 체크리스트가 고정 타이머로만 돌아 실제 완료와 어긋남 → 고정 타이머 종료로 “끝났다” 선언하던 것을 없애고, GET /videos/{id}/report를 폴링(4초 간격)해 실제 상태로만 선언하도록 수정(c702801). 🔴 부분 해소 — 네 칸(전처리· 추적·판단·근거검증) 중 정확히 어느 단계인지는 여전히 모름(워커가 중간 보고를 안 함). 필요하면 새 항목으로.

  • 담당: 없음(체크리스트-완료 불일치는 해소, 단계별 세분화는 미착수) · 제기: 정어진 · 기한: 세분화는 급하지 않음

min (박민호)

1. 패킷 A(과금) 진행 상황을 알려주세요 ✅ 회신 (2026.09.08)

늦어서 죄송합니다 — 착수 전이었던 것을 오늘 끝까지 만들었습니다. 처음 걸어 주신 git grep -n "analysis_credit" -- fastapi/app가 실제로 0건이었고, 최근엔 www/ 화면(경기 탐색·클립 업로드·경기장 예약 등) 쪽에 밀려 있었습니다.

오늘 app/billing/(domain·application·adapter·dependencies)·라우터· 마이그레이션·테스트를 전부 만들어 paik 브랜치에 있습니다. 공유 파일 5곳(main.py·alembic/env.py·tests/conftest.py)은 배선이 정어진 몫이라 건드리지 않았습니다.

   
확인 git -C fastapi log --oneline paik -- app/billing
계약 테스트 .venv/bin/pytest -q tests/billing → 15 passed(스텁, 아직 main.py에 안 붙어 있어 라우터 하나만 올린 별도 앱으로 검사합니다 — tests/billing/conftest.py)
DB 통합 테스트 써 뒀지만 이 환경에서 Postgres 인증이 막혀 못 돌렸습니다(password authentication failed for user "supersub") — test_review_db.py 등 기존 DB 테스트도 이 환경에서 전부 같은 이유로 실패해서 제 코드 문제는 아닌 것 같습니다. 로컬에서 .venv/bin/pytest -q -m db tests/billing 로 확인 부탁드립니다

말씀하신 두 걱정 다 확인했습니다.

  • 종목 코드 불일치(football vs soccer) — 이번엔 해당 없었습니다. coach 테이블(부록 D)에 애초에 종목 컬럼이 없어서 옮길 것 자체가 없습니다. 대신 이게 새 문제입니다 — 아래 참고
  • 미결 10번(웹이 mock 고정) 순서 — 이미 해소돼 있어서 걸리지 않았습니다

🔴 다만 그 대신 부록 D 변경이 필요한 게 하나 나왔습니다 — coach에 종목 컬럼이 없어 market/coaches의 종목 필터를 실제 데이터로 못 채웁니다. 혼자 정하지 않고 paik 구역 14번으로 새로 올렸습니다 — 급하지 않으니 편하실 때 봐 주십시오.

상세: fastapi/docs/api-contract.md 3-10절 · fastapi/docs/backend-work-split.md 「패킷 A」 · 클라이언트 반영은 fastapi/docs/client-contract-changes.md 19번

  • 담당: 백성검 · 제기: 박민호 · 기한: 확인되는 대로

2. 패킷 B — review_option 초기 목록·마이그레이션 확인 부탁드립니다 ✅ 해소 (2026.09.04)

jin 구역 14번에 진행 상황을 남겼습니다 — review_option 초기 9개를 확정하고 5테이블 마이그레이션(fastapi/alembic/versions/20260903_review_trust_tables.py)을 min에 푸시했습니다. down_revision은 비워 뒀습니다.

  • 확인: min 브랜치의 fastapi/alembic/versions/20260903_review_trust_tables.py (docstring에 review_option 목록과 설계 근거가 있습니다)
  • 여쭤보고 싶은 것: (1) 스키마·제약조건이 부록 D·backend-work-split.md 패킷 B와 어긋난 곳이 있는지 (2) alembic upgrade/downgrade --sql로만 확인했고 로컬 Postgres 인증 문제로 DB 통합 테스트는 못 돌렸는데, 배선하실 때 같이 돌려서 문제 있으면 알려주시면 감사하겠습니다
  • 문제없으면 이어서 애플리케이션·어댑터·라우터·테스트를 쓰겠습니다
  • 담당: 정어진 · 제기: 박민호 · 기한: 확인되는 대로

회신 (2026-09-04, 정어진)

두 가지 다 했습니다. 스키마는 부록 D·패킷 B와 어긋난 데가 없고, DB 통합 테스트도 통과했습니다. 이어서 애플리케이션·어댑터·라우터·테스트를 쓰셔도 됩니다 — 다만 아래 (2) 선택지 정렬은 화면 구성이 걸린 것이라 함께 정하고 가는 편이 낫습니다.

아직 배선(병합)은 하지 않았습니다. 마이그레이션만으로는 alembic/env.py에 등록할 ORM도, app/main.py에 붙일 라우터도 없어 지금 이을 것이 down_revision 하나뿐입니다. 나머지가 오면 한 번에 배선하겠습니다 — docs/backend-work-split.md 「다 만들면」 그대로입니다.

돌린 것 (실제 PostgreSQL 18)

down_revision을 로컬에서만 현재 head(d52e8f1a6b34)로 임시로 채워 돌렸습니다. 커밋하지 않았고 DB도 원래 리비전으로 되돌려 두었습니다.

검사 결과
alembic heads a3d0764cefa5 하나 — head 갈라짐 없음
alembic upgrade head (오프라인 아닌 실물) 통과
테이블 5개 생성 review·review_option·review_selection·report·no_show 전부
유일 제약 uq_review_once_per_match·uq_no_show_once_per_match 둘 다 걸림
외래키 9개 전부 부록 D 166~169행과 일치, ON DELETE는 전부 NO ACTION
review_option 시드 9행
alembic downgrade -1 통과 — 다섯 테이블 0개 남음
다시 upgrade 후 pytest -q 439 passed, skipped 0 (붙이기 전과 같음 — 회귀 없음)
「하지 말 것」 넷 — 전부 지키셨습니다

신뢰도 점수 테이블 없음(D.4) · review에 총점·별점 없음(3.4) · report·no_show가 review를 참조하지 않음(3.5) · review_selection이 대리키 없는 복합 기본키.

server_default 없는 타임스탬프와 인덱스 없음은 20260902_match_tables.py와 같은 모양이라 관례를 따르신 것으로 보고 지적하지 않았습니다.

고칠 것 · 정할 것 셋

(1) docstring 개수 — 오타로 보입니다

17행 소제목이 ## 초기 선택지 8개인데 표(32~40행)도 _REVIEW_OPTIONS(76~86행)도 9개이고 실제 삽입도 9행입니다. 소제목만 고치면 됩니다.

(2) 🔴 “순서가 화면 노출 순서다”가 지금은 지켜지지 않습니다 — 정하실 것

_REVIEW_OPTIONS 위에 「순서가 화면 노출 순서다」라고 적으셨는데, review_option에 정렬 컬럼이 없어 SQL이 조회 순서를 보장하지 않습니다. 갓 적재한 직후에는 우연히 맞게 나와서 문제가 안 드러납니다. 실제로 확인해 봤습니다 — label 하나를 고치자(운영에서 오타 수정으로 흔히 생깁니다) 그 행이 맨 끝으로 갔습니다.

UPDATE 전 : manner_time · manner_respect · … · caution_would_not_repeat
label 1건 수정 후 : manner_respect · … · caution_would_not_repeat · manner_time

매너 카테고리 첫 항목이 화면 맨 아래로 내려갑니다. 선택지 셋 중 하나입니다.

     
A 조회할 때 ORDER BY category, code 스키마 안 바꿈. 다만 category 알파벳순은 caution→manner→repeat→skill이라 의도하신 매너·실력·재매칭·주의 순서가 안 나옵니다
B review_option에 sort_order 컬럼을 늘린다 의도한 순서가 그대로 나옵니다. 부록 D를 고치는 결정입니다(ERD에 code·category·label 셋뿐)
C 순서를 계약에서 뺀다 — 화면이 카테고리별로 묶어 그린다 스키마·문서 안 바꿈. 대신 docstring의 그 문장을 지워야 합니다

제 의견은 B입니다. 카테고리 순서까지 화면 구성이라고 이미 적어 두셨으니 그 의도를 스키마가 담는 편이 맞고, A는 지금 의도와 어긋나는 순서가 나옵니다. 다만 부록 D 수정이라 PM이신 박민호 님 판단입니다.

(3) 자기 자신을 평가하는 것을 막는 것이 없습니다 — 정하실 것

reviewer_id = reviewee_id인 행이 들어갑니다. 부록 D에도 패킷 B 문서에도 명시가 없어서 결함이 아니라 안 정해진 것으로 봅니다. DB CHECK로 막을지 응용 계층에서 막을지 정해 주시면 됩니다. (유일 제약은 (match_id, reviewer_id, reviewee_id)라 자기 평가를 막지 못합니다.)

아직 안 정해진 것 — 패킷 B 문서 「정해야 할 것」 대조
   
review_option 초기 목록 ✅ 확정 (9개)
신고 reason 형태 🟡 sa.Text()이니 자유 텍스트로 정하신 것으로 읽었습니다. 맞는지만 알려주세요
평가 가능 기간 ❌ 미정
불참을 누가 기록하나 ❌ 미정 — 스키마에 기록자 컬럼이 없어 “주장만”으로 좁히려면 응용에서 막아야 합니다
설계에 대해

D.8이 “3.4의 피해 상한 설계와 함께 정한다”고 미뤄 둔 자리를, 매너·실력은 긍정형만 두고 부정 신호는 선호 표현 둘로 제한하는 선택지 구성 자체로 푸신 것 — 근거가 docstring에 남아 있어 나중에 왜 그런지 되짚을 수 있습니다. 좋았습니다.

3. 패킷 A — 리뷰 부탁드립니다 ✅ 회신 (2026.09.08)

스스로 확인한 넷 다 지켰습니다.

확인할 것 결과
잔량이 SUM(delta)인가 ✅ 컬럼을 두지 않았습니다 — ViewCreditsInteractor가 credit_history()를 합산합니다(app/billing/application/use_cases/billing_interactors.py)
coach에 user_id가 없는가 ✅ id·name·contact 셋뿐입니다(coach_orm.py)
크레딧 차감이 POST /videos에 직접 연결되지 않았는가 ✅ analysis 컨텍스트를 임포트하지 않습니다 — 연결 지점은 없고, 수동 조정(POST /admin/credits/adjustments, 관리자 전용)만 있습니다
종목 코드(football/soccer) 처리 ⚠️ 그대로 두지도, 변환하지도 않았습니다 — coach에 종목 컬럼 자체가 없어서 다룰 대상이 없었습니다. 새로 생긴 문제라 paik 14번으로 올렸습니다

1번 회신에 만든 것·테스트 현황을 전부 적어 뒀습니다 — 여기서 되풀이하지 않습니다.

  • 담당: 백성검 · 제기: 박민호 · 기한: 확인되는 대로

4. 골든셋 라벨링 진행 상황 여쭙습니다 ✅ 회신 (2026.09.08)

세 가지 다 답변드립니다. 결론부터: 접촉 중인 지도자는 없고, 라벨링을 대신하고 있는 것도 없습니다. 다만 「필요한 것」이 한 덩어리가 아니라 두 종류이고, 그중 하나는 지금 적은 수로 바로 시작할 수 있습니다.

1) 접촉 중인 지도자 — 없습니다

제 쪽에 섭외 경로·후보가 없습니다. ho 2번의 「섭외 경로 미정 · 보상 방식 미정」이 그대로입니다. 그쪽 정보는 제가 가진 것이 없어 도움을 못 드립니다.

2) 🔴 50~100건을 한 번에? — 두 종류를 나눠야 합니다

이게 이 회신의 핵심입니다. 지금 「골든셋」으로 뭉뚱그려 부르는 것이 실제로는 성격이 다른 두 가지이고, 필요한 수량과 시작 조건이 반대입니다.

  (A) 임계값 검수 (B) 정답 라벨
지도자가 하는 일 루브릭의 구간(임계값)을 정한다 — 「좋은 팔로스루는 몇 도부터인가」 클립을 보고 이 선수가 잘했는지 매긴다
필요한 것 지도자 1명의 판단. 클립 수는 부차적 표본 수. 통계 검정이라 수가 안 차면 결론이 안 난다
적은 수로 시작 가능? ✅ 가능합니다 🔴 무의미합니다
막고 있는 항목 20 · 22 · 23(나) · 6 · 루브릭 draft 3개 열기 5 · 8

(B)가 왜 적은 수로 안 되는지는 이미 겪었습니다. 미결 6번에서 12건을 판독받아 계산했더니 다리 자동 판별 정확도가 4/11 = 36.4% 인데 신뢰구간이 [15.2%, 64.6%] 였습니다. 이 폭이면 “고쳤다/안 고쳤다”를 가를 수 없습니다. 미결 8번은 필요 표본이 340건인데 데이터셋 상한이 49건이고, 미결 5번은 25~60클립이 필요한데 유일하게 남은 경로가 자체 촬영입니다.

반대로 (A)는 지금 시작할 수 있습니다. 지도자가 클립을 많이 볼 필요가 없고, 이미 나와 있는 실측 분포를 놓고 「이 구간이 맞느냐」를 묻는 자리라 반나절이면 상당 부분 정해집니다. 제가 들고 갈 자료가 준비돼 있습니다:

  • 야구 타격 5개 항목의 JHMDB 46클립 실측 분포(중앙값·0/1/2 분포)
  • 오늘 잰 것: 0등급 117건 중 23건(20%)이 구간 위에서 왔다 — 상한을 어디에 둘지가 바로 이 검수 항목입니다 (미결 20번)
  • 상한을 넘긴 값이 어디로 가야 하는지 본보기가 이미 저장소 안에 있습니다 — football_inside_pass는 초과를 0등급이 아니라 1등급으로 받습니다

3) 라벨링 없이 대신하고 있는 것 — 없습니다

정확히 말씀드리면 골든셋을 대신하는 것은 하나도 없습니다. 지금까지 한 것은 「정답 없이도 할 수 있는 일」을 소진해 온 것입니다.

한 것 얻은 것 🔴 얻지 못한 것
등급 판정을 수치 구간으로 코드화 재현성 — 같은 입력이 같은 등급을 낸다 그 등급이 맞는지는 모른다
target 30 전환 측정 타당성 — 15fps에서는 임팩트가 격자에 아예 없었다 정확도가 아니다. 동작점을 옮긴 것이다
지어낸 값 제거(21번)·근거 문장 정리(23번) 결함 제거 — 안 잰 것으로 감점하지 않는다 잰 것이 맞는지는 여전히 모른다
B-3~B-5의 AI 판독 라벨 부정적 결론의 근거(“이 방법으로는 안 갈린다”) 🔴 정답으로 승격하지 않습니다

요약하면 “틀리지 않게” 하는 일은 많이 했지만 “맞는지” 재는 일은 한 건도 못 했고, 그건 정의상 골든셋 없이는 안 됩니다.

권하는 순서

(A)를 먼저, 그것도 지도자 1명으로 시작하는 것을 권합니다. 지금 6개 항목이 (A) 하나에 걸려 있고, (B)는 촬영·수집이 따라오는 별건이라 같은 일정에 묶으면 (A)까지 늦어집니다. 자리를 잡아 주시면 자료를 정리해 들고 가겠습니다.

ho 구역 2번(골든셋 라벨링 주체 확보)이 제 담당으로 되어 있는데, 지도자 섭외·라벨링 진행 쪽에서 이미 아시는 게 있으신지 여쭙니다 — 후보로 접촉 중인 지도자가 있는지, 50~100건을 한 번에 채워야 하는지 아니면 적은 수로 먼저 시작해도 루브릭 개선 판별에 쓸모가 있는지, 라벨링 없이 지금까지 진행하신 검증(등급 판정 코드화 등)이 어디까지 골든셋을 대신하고 있는지 궁금합니다.

  • 담당: 정상호 · 제기: 박민호 · 기한: 확인되는 대로

5. 로컬 미리보기 서비스가 딴 저장소를 보고 있었습니다 ✅ 해소 (2026.09.03)

CLAUDE.md는 supersub-preview.service가 이 저장소 루트를 WorkingDirectory로 상시 구동 중이라고 적어 두었는데, 실제로는 /home/hi/projects/supersub.parkminho.cloud(전혀 다른 git remote를 가진 별개 프로젝트)를 보고 있었습니다. 그래서 스프린트2 로그를 새로 만들어도 http://localhost:4000/스프린트2/가 404였습니다.

  • 원인: 서비스 파일(/etc/systemd/system/supersub-preview.service)의 WorkingDirectory가 옛 경로 그대로 남아 있었습니다. demo/용 jekyll-preview.service(4001포트)는 처음부터 올바르게 설정돼 있었습니다.
  • 처리: WorkingDirectory를 /home/hi/projects/super-sub.cloud로 고치고 daemon-reload·restart 했습니다(박민호, sudo 필요해 직접 실행).
  • 확인: curl localhost:4000/스프린트2/ · /pending/ 둘 다 200, 칸반 보드도 Sprint 2 · 09.01 표시 확인.
  • 담당: 박민호 · 기한: —

6. supersub-ai.com이 main을 안 보고 있었습니다 ✅ 해소 (2026.09.03)

Vercel 프로젝트 super-sub-cloud(접미사 없는 쪽 — supersub-ai.com· www.supersub-ai.com 도메인이 여기 붙어 있습니다)의 Production Branch가 paik로 되어 있었습니다. 그래서 AI SCOUTING 문구·”저장” 버튼 등 main에 올린 변경이 며칠째 실제 사이트에 하나도 안 보이고 있었습니다 — -dev 프로젝트에만 배포되고 있었고, 그쪽엔 도메인이 안 붙어 있었습니다.

  • 원인: Settings → Environments → Production → Branch Tracking이 main이 아니라 paik를 보고 있었습니다
  • 처리: Branch Tracking을 main으로 바꾸고 저장(박민호)
  • 확인: 저장 직후 배포(9Ed6BioQr, Source main · 커밋 a14acd8)가 Ready로 뜨고 supersub-ai.com 도메인에 연결됨을 확인
  • 담당: 박민호 · 기한: —

8. fastapi/CLAUDE.md의 컨텍스트 목록이 코드보다 뒤처져 있습니다 ✅ 해소 (2026-09-08)

fastapi/CLAUDE.md 「구조」 절은 “컨텍스트는 user·card·analysis 셋”이라고 적어 두었는데, 실제로는 match 컨텍스트가 이미 있습니다.

  • 확인: fastapi/tests/test_architecture.py의 CONTEXTS = ("user", "card", "analysis", "match") · fastapi/app/match/ 디렉터리 존재
  • 이 문서는 정어진 소유라 제가 직접 고치지 않고 근거만 남깁니다 — 문서 규칙(루트 CLAUDE.md 「남의 영역 문서와 어긋날 때」)대로 처리했습니다
  • review(패킷 B)도 아직 컨텍스트로 추가되지 않았다면 그것도 같이 반영하시는 편이 나을 것 같습니다 — 위 8번 항목의 review_option 등 5테이블 진행 상황 참고

처리 (2026-09-08, 정어진, a1d3069): fastapi/CLAUDE.md 「구조」 절과 test_architecture.py 의 CONTEXTS 를 user·card·analysis·match·review 다섯으로 맞췄습니다. review 를 CONTEXTS 에 넣어도 경계 검사 12건 전부 통과 — review 컨텍스트는 이미 경계가 깨끗합니다. 손 관리 목록이 다시 뒤처지지 않게 test_CONTEXTS가_실제_디렉터리와_일치한다(app/ 디렉터리 ↔ CONTEXTS 정합)를 추가했습니다. 전체 545 통과.

  • 담당: 정어진 · 제기: 박민호 · 기한: 확인되는 대로

9. 분석 리포트가 아직도 mock — 세 조각을 스프린트 3으로 한데 묶습니다 ✅ 1·2·3 전부 해소 (2026.09.10)

/analysis에서 영상을 올려도 화면에 뜨는 리포트는 항상 같은 고정 문구입니다 (www/src/components/analysis/AnalysisStage.tsx의 REPORT 상수, “자리 표시 리포트”). 에이전트는 이미 진짜 리포트를 S3에 쓰고 있는데(agent/scripts/analyze_s3.py), 그 결과와 화면 사이가 세 군데 다 끊겨 있어 하나도 이어지지 않습니다.

순서 무엇 상세 담당
1 완료 보고에 그 리포트가 어디 있는지 값이 없다 paik 11번 정상호(싣기) · 정어진(받는 칸)
2 그걸 읽는 API가 계약에 없다 paik 7번 정어진
3 화면이 그 API를 부르지 않는다 — AnalysisStage.tsx가 서버에 아무것도 안 묻고 REPORT 상수를 그대로 그린다 (새로 올림, 아래 참고) 백성검

1 → 2 → 3 순서로 이어져야 합니다 — 하나만 되면 나머지 둘이 없어 화면은 그대로 mock입니다.

3번(화면 배선)은 지금까지 담당자가 명시된 항목이 없어 여기서 새로 답니다. www/src/lib/savedReports.ts 주석에 이미 “경로가 생기면 이 파일만 갈아 끼운다”고 적혀 있으니 그 계획대로 하시면 됩니다 — AnalysisStage.tsx· MyVideos.tsx는 savedReports.ts의 함수 시그니처만 알면 되고 저장 방식은 몰라도 됩니다.

   
확인(전체) 영상 하나를 올려 분석을 succeeded까지 돌린 뒤 화면 리포트가 그 영상 내용에 따라 달라지면(고정 문구가 아니면) 된 것입니다
확인(3번만) grep -n 'const REPORT' www/src/components/analysis/AnalysisStage.tsx가 안 걸리면 하드코딩을 걷어낸 것입니다
하지 말 것 🔴 수치(총점·등급)를 리포트에 넣지 않기 — paik 7번에 적힌 계약 3장 4 원칙 그대로입니다

✅ 1번 주자 뛰었습니다 — 체인이 정어진 님께 넘어갑니다 (2026.09.09, 정상호)

1번 조각의 제 절반(싣기)이 끝났습니다. 완료 보고에 report_key 가 실립니다 (상세는 paik 11번). FinishJobSchema 가 extra="ignore" 라 받는 칸이 없어도 지금 배포해도 됩니다 — 확인했습니다.

그리고 2번을 하려면 필요한데 없던 것을 하나 채웠습니다. paik 7번이 요구한 네 가지 중 「요약 문장」이 제 산출물에 아예 없었습니다. 계약 3-1 의 report.summary 자리이기도 한데, 🔴 백엔드가 지어낼 수 있는 값이 아닙니다.

result.summary  "디딤발 무릎 굽히기가 「흔들리지 않는 축」으로 이번 동작의 강점입니다.
                 상체 기울기는 「젖혀진 상체」로 가장 아쉬웠습니다."
  • 🔴 모델이 쓰지 않습니다. 코드가 짓습니다 — 숫자를 쓸 자리를 안 만들어 계약 3장 4(총점·등급 금지)를 구조로 지킵니다. 근거 문장 쪽에서는 같은 것을 지키는 데 2회차가 걸렸고 아직 1건이 남아 있습니다(ho 23번)
  • 🔴 없는 것을 지어내지 않습니다 — 전부 잘했으면 아쉬운 점을 만들지 않고, 1등급을 「강점」이라 부르지 않습니다
  • 점수와 무관합니다(stat·out_of_band 와 같은 성질) → B-6 재실행 없음

📄 agent/report-contract.md 를 냈습니다. paik 7번의 네 요구가 각각 JSON 어디에 있는지, 화면에 낼 때 지켜야 할 것(🔴 band 는 선수에게 내지 않기 · skipped 는 0점이 아니라 제외)이 한 장에 있습니다. 정어진 님은 읽는 경로를, 백성검 님은 화면 배선을 이 문서만 보고 하실 수 있습니다.

   
확인 cd agent && uv run pytest tests/test_summary.py -q — 57건(루브릭 6종 × 등급 조합)
하지 말 것 🔴 summary 에 점수를 덧붙이지 마세요 — 숫자는 score 가 따로 가집니다

✅ 3번(화면 배선)도 끝났습니다 (2026.09.10, 백성검). paik 7번에서 정어진이 읽는 경로(GET /videos/{id}/report)를 냈고, 그걸 받아 AnalysisStage.tsx의 하드코딩 REPORT 상수를 걷어내고 실제 API를 부르도록 갈아 끼웠습니다 — 자세한 내용·확인 결과는 paik 7번에 이미 적어 뒀습니다(중복 기록하지 않습니다). grep -n 'const REPORT' www/src/components/analysis/AnalysisStage.tsx → 0건으로 재확인했습니다.

이 항목의 뒤늦은 정정 하나 — 위 표의 “확인(3번만)” 명령은 원래 grep -n 'const REPORT' ...였는데 실제 상수 선언이 const REPORT = { 형태라 그대로도 걸립니다(정정할 것 없음, 확인만 했습니다).

🔴 1·2·3이 다 됐어도 이 체인에 물려 있던 ho 28번(오버롤 등급 화면 표시)은 아직 별도로 열려 있습니다 — GET /videos/{id}/report가 일부러 score·grade·breakdown[].stat을 안 주기 때문입니다(계약 3장4·부록D.5, “카드 경로가 읽는다”). 같은 데이터 소스로는 못 풀립니다.

  • 관련: paik 7번(읽는 경로) · paik 11번(리포트 위치) · agent/report-contract.md(읽는 쪽 지도) · www/src/lib/savedReports.ts 주석 · ho 28번(이 항목이 풀려도 남는 것)
  • 담당: 정상호 ✅ 1조각 싣기 + 요약 문장 (2026.09.09) → 정어진 ✅ 받는 칸·읽는 경로 (2026.09.10, jin 27번) → 백성검 ✅ 화면 배선 (2026.09.10) · 제기: 박민호 · 기한: 스프린트 3

10. Vercel 빌드가 24시간 rate limit에 걸렸습니다 — 기다리기로 결정 ✅ 해소 (2026.09.15)

main에 push할 때마다 Vercel(두 프로젝트 super-sub-cloud·super-sub-cloud-dev 전부)이 빌드를 거부합니다.

Vercel – super-sub-cloud      | failure | Deployment rate limited — retry in 24 hours.
Vercel – super-sub-cloud-dev  | failure | Deployment rate limited — retry in 24 hours.

GitHub 커밋 상태로 확인 — 첫 실패 2026-09-08 08:33 UTC(커밋 c304c5f), 이후 push마다(fe58bfb 등, 08:56 UTC) 자동 재시도됐지만 매번 같은 이유로 실패했습니다. push 방식·코드 문제가 아니라 Vercel 계정(무료 플랜)의 빌드 횟수 제한입니다.

   
확인 curl -s https://api.github.com/repos/pmhllll12/super-sub.cloud/commits/main/status — Vercel – 두 컨텍스트가 success면 풀린 것입니다
결정 Pro 업그레이드 대신 24시간 대기하기로 함(2026-09-08, 박민호)
다시 시도하는 법 별도 조치 필요 없음 — 그 뒤 main에 아무 push나 있으면 자동으로 다시 빌드됩니다. 급하면 Vercel 콘솔에서 수동 재배포
  • 담당: 박민호 · 제기: 박민호 · 기한: 2026-09-09 08:33 UTC 이후 재확인 완료 — curl .../commits/main/status로 재확인(2026.09.15), super-sub-cloud·super-sub-cloud-dev 둘 다 state: success

11. supersub(백엔드) 서버에 k3s 도입 ✅ 진행함 (2026.09.09, 회신 없이 박민호 판단)

목적은 백엔드뿐 아니라 다른 서비스도 함께 오케스트레이션하는 것입니다. 아직 구현은 안 했고, 이번 세션에서 대상 서버 상태만 확인했습니다.

   
확인한 것 ssh supersub 'free -h; df -h /; nproc; which k3s' — k3s는 없음. AL2023, t3.large(vCPU 2 · 메모리 7.6GB), 디스크 30GB 중 28GB 여유
지금 상태 supersub-api(FastAPI)·postgresql 둘 다 systemd로 active — fastapi/docs/deployment.md 그대로입니다

🔴 혼자 정하지 않고 여쭙습니다 — 이 서버 배포 방식(docs/deployment.md)의 정본이 정어진 몫이라, systemd→k8s 전환은 여기서 판단할 일이 아니라고 봤습니다. 걱정되는 지점: 2 vCPU·7.6GB 박스에 이미 API+DB가 떠 있는 상태에서 k3s 컨트롤플레인까지 얹었을 때 여유가 되는지, 그리고 오케스트레이션 대상이 될 “다른 서비스”가 구체적으로 무엇인지(같은 서버 안인지, 새 노드를 붙이는 것인지) 정하고 가는 게 나을 것 같습니다.

  • 관련: agent/CLAUDE.md(supersub-ai GPU 인스턴스는 이미 정상호 소유로 별도 — 거긴 systemd로 vLLM·autostop만 돌고 있고 이번 항목과는 다른 서버입니다)

✅ 정정 (2026.09.09) — 정어진 회신 기다리지 않고 진행함

위에 “답 오기 전에 직접 설치하지 않는다”고 적었던 것을 정정합니다. 박민호 판단으로 회신 없이 바로 설치했습니다. 결과는 다음과 같습니다.

   
설치 공식 설치 스크립트(https://get.k3s.io)로 단일 노드 server 모드 설치
확인 sudo k3s kubectl get nodes → Ready, v1.36.4+k3s1
기존 서비스 supersub-api·postgresql 둘 다 설치 전후로 계속 active — 영향 없음
자원 설치 전 메모리 사용 311MiB → 설치 후(노드 뜬 직후) 1.1GiB, 여유 5.0GiB. 디스크 27GB 여유

아직 안 한 것: 이 위에 무엇을 실제로 배포할지(오케스트레이션 대상 서비스 목록·매니페스트)는 정하지 않았습니다. supersub-api·postgresql을 k3s 안으로 옮길지, 아니면 k3s는 새 워크로드 전용으로 옆에 둘지는 정어진 판단이 필요합니다 — 배포 방식(docs/deployment.md)의 정본이 그쪽 소유라서입니다.

✅ 정어진 회신 (2026.09.09) — (A) 지금은 아무것도 옮기지 않습니다

k3s 설치 자체는 문제없습니다(기존 서비스 무영향, 확인됨). 다만 supersub-api· postgresql은 systemd 그대로 두고 k3s에는 아직 아무 워크로드도 배포하지 않습니다. 실제로 오케스트레이션이 필요한 두 번째 서비스가 특정될 때 API 합류 여부를 그때 정합니다.

근거:

  • 오케스트레이션 대상이 될 “다른 서비스”가 아직 이름이 없습니다. 두 번째 워크로드 0인 상태에서 도는 systemd 배포를 k8s로 옮기는 것은 순수 비용이고, 프로덕션을 서비스하는 유일한 박스에 클러스터 네트워킹·이미지 레지스트리· 인그레스 실패면을 더합니다.
  • 2 vCPU / 7.6GB 박스에 k8s idle 오버헤드(설치만으로 이미 +0.8GiB)에 더해 부하 시 kubelet·containerd·coredns·traefik이 얹힙니다.
  • systemd 배포는 마지막 배포(2026-09-08)에서 스모크까지 통과했고 fastapi/docs/deployment.md가 그 절차의 정본입니다. 이걸 매니페스트 기준으로 다시 쓰는 것은 실제 필요가 끌고 가야 하는 변경입니다.
  • 🔴 Postgres는 옮기지 않습니다. 디스크 여유 27GB 박스에서 StatefulSet + local-path PV는 PV 오설정이나 delete pvc 한 번에 프로덕션 데이터가 사라집니다. 온박스 systemd Postgres + 알려진 백업 경로가 더 안전합니다.

해 둔 것: 로컬(개인 개발) k3s용 매니페스트를 fastapi/deploy/k8s/에 만들어 검증해 뒀습니다 — API Deployment + in-cluster pgvector + initContainer 마이그레이션

  • NodePort. 나중에 EC2로 API를 옮길 필요가 생기면 Postgres 부분만 빼고 이걸 출발점으로 씁니다.
   
결정 (A) k3s는 설치된 채 유지, 워크로드 미배포. supersub-api·postgresql systemd 유지
다시 볼 조건 오케스트레이션이 필요한 두 번째 서비스가 구체화될 때 — 그때 fastapi/docs/deployment.md 개정 + 이 항목 재개
하지 말 것 🔴 supersub-api·postgresql을 k3s로 옮기지 않기(특히 Postgres) · 🔴 deployment.md를 매니페스트 기준으로 미리 고치지 않기
확인 ssh supersub 'systemctl is-active supersub-api postgresql' → 둘 다 active (k3s 설치 후에도 배포 방식은 systemd 그대로)

✅ 추가 진행 (2026.09.09) — 백엔드 API를 파드로 띄워 트라이얼 (박민호)

“다른 서비스”의 첫 대상으로 백엔드(supersub-api)를 일단 파드로 올려봤습니다. 기존 systemd 서비스는 손대지 않고 옆에 나란히 띄운 것뿐입니다.

   
이미지 ~/k3s-trial/Dockerfile(서버에만 있음, fastapi/ 저장소엔 커밋 안 함) — python:3.14-slim 위에 requirements.lock.txt 그대로 설치. docker build → k3s ctr images import로 클러스터에 반입
설정 주입 기존 .env를 그대로 kubectl create secret generic supersub-api-env --from-env-file=...로 옮김. 값은 여기 적지 않았습니다
배포 ~/k3s-trial/deployment.yaml — hostNetwork: true(DB가 localhost만 듣고 있어서, 파드가 호스트 네트워크를 그대로 씀), 포트는 8080(기존 8000과 안 겹치게)
확인 sudo k3s kubectl get pods → Running · curl localhost:8080/health → 200(DB 연결 포함) · 동시에 기존 curl localhost:8000/health도 200 — 서로 영향 없음

🔴 이건 트라이얼이지 전환이 아닙니다. 실제 트래픽은 여전히 8000(systemd)이 받고 있고, 8080 파드는 “떠는지 확인”용으로 켜둔 상태입니다. 계속 켜 둘지, 지울지(kubectl delete deployment supersub-api-trial), 아니면 이걸 발판으로 fastapi/에 정식 Dockerfile을 커밋하고 트래픽을 옮길지는 정어진 판단이 필요합니다 — 특히 Dockerfile을 저장소에 정식으로 둘 위치·이미지 태그/레지스트리 전략은 배포 관례(docs/deployment.md)에 맞춰 그가 정하는 게 맞다고 봤습니다.

✅ 정어진 후속 회신 (2026.09.09) — 트라이얼은 (A)와 어긋나지 않습니다, 정식화는 보류

⚠️ 28f101d가 push된 시점에 제 (A) 회신(58515c0)은 아직 main에 없었습니다 (로컬 jin 에만). 그래서 두 판단이 엇갈려 보일 수 있는데 결론은 같습니다 — 파드는 systemd 옆에, 전환은 나중.

  • 트라이얼은 (A) 그대로입니다. “파드를 systemd 옆에 나란히” 가 (A) 가 말한 것이고, 트라이얼은 유용한 데이터를 줬습니다: hostNetwork: true 로 localhost-only DB 문제가 우회된다는 것, 두 프로세스가 서로 무영향이라는 것.
  • 🔴 정식화(레포에 Dockerfile 커밋 + 트래픽 8000→파드 이전)는 아직 안 합니다. 이유는 (A) 그대로 — 명명된 2번째 서비스도, cutover·롤백 계획도 없습니다. 트라이얼 파드는 박민호 님이 유지하든 지우든(kubectl delete deployment supersub-api-trial) 무방합니다.
  • 정식화하기로 하면 (그때):
    • Dockerfile·매니페스트 위치는 fastapi/deploy/k8s/ 입니다. 제가 로컬 k3s 개발용으로 이미 만들어 검증해 뒀고(API Deployment + pgvector + initContainer 마이그레이션 + NodePort), EC2용은 거기서 in-cluster Postgres 부분만 빼고 hostNetwork 또는 selector 없는 Service/Endpoints 로 호스트 PostgreSQL 을 가리키게 하면 됩니다. 서버에만 있는 ~/k3s-trial/ 사본과 갈리기 전에 그걸 정본으로 삼습니다.
    • 이미지 태그/레지스트리 전략 + docs/deployment.md 개정은 그 시점에 제가 냅니다.

🔴 정정 (2026.09.09) — min 14 정책으로 「정식화 보류」를 거둡니다

박민호 님이 min 14 로 k3s-only 를 팀 정책으로 정했습니다(로컬·EC2 둘 다). 위 후속 회신의 「정식화는 아직 안 합니다 / 2번째 서비스가 나올 때까지」 조항을 거둡니다 — PM 결정이 났으니 전환은 진행합니다.

  • 위 회신의 나머지는 그대로 유효합니다: fastapi/deploy/k8s/ 가 매니페스트 정본, EC2 는 in-cluster Postgres 빼고 호스트 PG, ~/k3s-trial/ 사본이 갈리기 전에 레포로 끌어올 것, cutover·롤백 계획이 필요할 것. 이제 이게 전환의 체크리스트입니다.
  • 배포 정본(docs/deployment.md) 담당으로서 할 일과 순서는 min 14 회신에 적었습니다.

  • 담당: 정어진 ✅ 회신함 (2026.09.09 — (A) → min 14 정책으로 정식화 진행. 계획은 min 14) · 제기: 박민호 · 기한: min 14 와 함께

✅ 추가 진행 (2026.09.09) — Docker Hub pull 방식으로 전환

로컬 import 대신 레지스트리 pull로 바꿨습니다. 이유: 로컬 import 방식은 그 서버에서 직접 빌드해야만 이미지가 생기는데, Docker Hub를 거치면 어디서 빌드하든 같은 이미지를 여러 서버(로컬·EC2·다른 EC2)가 그대로 받아 쓸 수 있습니다.

   
레포 pmhllll12/supersub (Private — 소스가 이미지에 그대로 들어가 있어 Public은 안 씀)
인증 supersub 서버에서 sudo docker login(Access Token) 후 그 자격으로 k8s Secret(dockerhub-cred, kubernetes.io/dockerconfigjson) 생성
Deployment 변경 image: pmhllll12/supersub:latest · imagePullPolicy: Always · imagePullSecrets: [dockerhub-cred]
확인 kubectl describe pod 이벤트에 Pulling image "pmhllll12/supersub:latest" → Successfully pulled 찍힘 (진짜로 레지스트리에서 받은 것 확인) · curl localhost:8080/health → 200
걸린 것 롤링 업데이트 중 새 파드가 hostNetwork라 기존 파드와 포트 충돌로 Pending(didn't have free ports) — 기존 파드를 수동으로 지워서 넘겼습니다. 단일 노드에서 hostNetwork 쓸 땐 롤링 업데이트가 이렇게 걸린다는 걸 알아두면 됩니다

절차는 www/docs/2026-09-09-K3S-harness.md에도 반영했습니다.

🔴 추가 진행 (2026.09.09) — .env가 이미지에 그대로 들어가 push된 적이 있었습니다

직접 이미지를 열어봐서 발견했습니다. Dockerfile에 .dockerignore 없이 COPY . .만 있어서, supersub 서버의 .env·.env.bak이 그대로 이미지에 들어갔고 그 상태로 Docker Hub(pmhllll12/supersub, digest 56b21535...)에 한동안 올라가 있었습니다. 노출된 값: DATABASE_URL(DB 비밀번호), JWT_SECRET, WORKER_TOKEN.

   
즉시 조치 .dockerignore 추가 후 재빌드·재push(새 digest 4ae283b3...) — 확인: docker run --rm pmhllll12/supersub:latest ls /app에 .env 없음
노출 범위 레포는 Private, Collaborator는 아직 안 추가한 상태라 박민호 계정 밖으로 나갔을 가능성은 낮습니다. 다만 예전 digest 자체가 Docker Hub에서 완전히 지워졌는지는 확인 안 했습니다
🔴 아직 안 한 것 JWT_SECRET·DATABASE_URL 비밀번호·WORKER_TOKEN 교체(rotation) — 노출됐던 값이라 안전하게 하려면 바꾸는 게 맞다고 봅니다. 다만 JWT_SECRET을 바꾸면 로그인해 있는 모든 사용자가 로그아웃되고, WORKER_TOKEN을 바꾸면 워커 쪽 설정도 같이 바꿔야 해서 운영에 영향이 갑니다. 혼자 정하지 않고 여쭙습니다 — 언제 바꿀지, 바꾸는 김에 워커 쪽 배선도 같이 손볼지 판단 부탁드립니다

✅ 추가 진행 (2026.09.09) — Dockerfile·.dockerignore를 저장소에 정식으로 커밋, GitHub Actions로 CD 연결

“Dockerfile을 저장소에 커밋할지”를 이번엔 진행하는 쪽으로 정했습니다 — 위 보안 사고 때문에 .dockerignore를 git으로 관리해서 다음 사람이 또 .env를 굽지 않게 하는 게 더 급하다고 판단했습니다. 이미지 태그 전략은 아직 latest 하나뿐입니다 — 손 안 댔습니다.

   
커밋된 파일 fastapi/Dockerfile · fastapi/.dockerignore(.env·.env.*·.venv·tests·.git 제외, .env.example만 예외) · .github/workflows/backend-docker-build.yml
CD 흐름 main에 fastapi/** 변경 push → GitHub Actions가 빌드해 pmhllll12/supersub:latest로 push → supersub 서버의 supersub-cd.timer(2분마다 폴링)가 새 digest를 감지해 kubectl rollout restart
포트 하나도 새로 안 열었습니다 — Docker Hub Webhook(인바운드, 서명 없음)이 아니라 EC2가 밖으로 나가서 확인하는 폴링 방식을 택했습니다
롤아웃 충돌 방지 Deployment strategy: Recreate로 바꿔서, hostNetwork 포트 충돌 없이 자동 재배포되게 했습니다
🔴 직접 해주셔야 하는 것 GitHub 저장소 Settings → Secrets and variables → Actions에 DOCKERHUB_USERNAME(pmhllll12)·DOCKERHUB_TOKEN(Docker Hub Access Token, Read & Write)을 추가해주셔야 GitHub Actions가 push를 할 수 있습니다. 토큰은 대화·문서 어디에도 남기지 않았습니다

fastapi/ 소유가 정어진이라, Dockerfile 내용·위치·CI 트리거 조건이 관례에 맞는지 검토 부탁드립니다. 어긋나면 고쳐서 알려주세요 — 급하게 진행한 것이라 그쪽 확인 전까지는 「임시」로 봐 주시면 됩니다.

✅ 정어진 회신 (2026.09.09) — 보안 사고 대응 권고 + Dockerfile 검토

1. 🔴 노출 시크릿 — 셋 다 교체 권고, 단계로

레포가 Private·Collaborator 없음이라 유출 가능성은 낮지만, 옛 digest 가 Docker Hub 에 남아 있을 수 있고 나중에 레포 공개·Collaborator 추가·계정 토큰 유출 중 하나만 생겨도 그 이미지에서 셋이 다 나옵니다. 교체 비용이 지금 가장 쌉니다(dev·데모 단계) — 미루면 런칭 뒤엔 비쌉니다.

값 어떻게 영향 순서
WORKER_TOKEN 새 값 생성(python -c "import secrets;print(secrets.token_urlsafe(32))") → 서버 .env + 파드 Secret + agent/ 워커 설정 동시에 사용자 0. 워커 재시작만 먼저 (박민호 님이 “서버 .env 반영 미확인”도 앞서 남기셨으니 이참에 확인)
DB 비밀번호 (DATABASE_URL) Postgres role 비번 ALTER ROLE ... PASSWORD → 서버 .env + 파드 Secret → 파드 재시작 사용자 0 (세션은 JWT라 DB 세션 아님). 재시작 수 초 그다음
JWT_SECRET 새 값 → 서버 .env + 파드 Secret → 재시작 🔴 로그인한 전원 로그아웃. 지금은 데모·소수라 사실상 무비용 지금 — 유일하게 미루면 비싸지는 값

추가로 옛 이미지 digest(56b21535…) 를 Docker Hub 에서 삭제해 주세요(박민호 님 계정). 안 지우면 태그만 바꿔도 그 digest 를 직접 pull 하면 나옵니다.

🔴 저는 실행 안 합니다 — ssh supersub 접근이 없고, 전원 로그아웃을 독단으로 할 수 없습니다. 위 순서대로 박민호 님이 서버에서 하시고, JWT_SECRET 타이밍만 사용자 확인 받으시면 됩니다. 워커 배선(WORKER_TOKEN)은 같은 교체에 묶어서 한 번에.

2. ✅ Dockerfile·.dockerignore·CI 검토 — 좁혔습니다 (이 커밋)

관례에 맞습니다. 다만 좁혔습니다(포트 8080·python:3.14-slim 은 CD 가 물고 있어 유지):

  • COPY . . → 명시적 COPY app/ · alembic/ · alembic.ini. 원래는 docs/· scripts/·CLAUDE.md·Dockerfile 자신까지 이미지에 굽고 있었습니다. .env 사고 뒤라 범위를 좁게 잡는 게 맞습니다. alembic/ 은 남깁니다 — 배포 때 alembic upgrade head 를 이미지 안에서 돌 수 있어야 합니다.
  • non-root 유저(supersub, uid 10001) 추가.
  • .dockerignore 에 docs·scripts·deploy·*.md·.mypy_cache·.ruff_cache 추가 — COPY . . 로 되돌아가더라도 방어.
  • CI 트리거(main + fastapi/**)는 그대로 둡니다 — 맞습니다.

🔴 하나 확인 필요: 배포되는 파드가 alembic upgrade head 를 도나요? 이미지에 alembic/ 이 있으니 돌 수는 있는데, 지금 ~/k3s-trial 의 Deployment 에 그 initContainer 나 CD 훅이 있는지 문서(www/docs/2026-09-09-K3S-harness.md)에서 못 봤습니다. 없으면 새 마이그레이션이 배포돼도 스키마가 안 따라가서 런타임에서만 터집니다. Deployment 에 initContainer(같은 이미지로 sh -c "alembic upgrade head", env 는 앱과 동일)를 두거나, supersub-cd.timer 재배포 훅에 kubectl exec deploy/... -- alembic upgrade head 를 앞에 넣는 편이 안전합니다.

3. GitHub Actions Secrets — 사용자가 직접

Settings → Secrets and variables → Actions 에 DOCKERHUB_USERNAME(pmhllll12) · DOCKERHUB_TOKEN(Docker Hub Access Token, Read & Write). GitHub UI 작업이라 제가 못 합니다. 이게 없으면 CD 워크플로가 push 단계에서 실패합니다.

  • 담당: 박민호(시크릿 교체·digest 삭제·마이그레이션 훅) · 사용자(JWT 타이밍·GitHub Secrets) · 정어진(Dockerfile — 이 커밋으로 완료) · 제기: 박민호

🔴🔴 사고 보고 (2026.09.09) — 제 k3s 설치 때문에 <API 호스트>이 몇 시간 동안 안 됐습니다

정어진이 오늘 새벽(01:07 UTC, 이 인스턴스가 뜨자마자) 이미 nginx+Certbot으로 <API 호스트>을 실제 서비스로 올려둔 상태였습니다 — 이걸 저는 몰랐고, fastapi/docs/deployment.md엔 “80·443 닫혀있음”이라고만 적혀 있어 이미 지난 인스턴스 기준의 낡은 정보였습니다. 그 위에 제가 k3s를 설치하면서 사고가 났습니다.

   
1단계 원인 k3s가 기본 설치하는 Traefik(ingress)이 80·443을 같이 차지했습니다. 사용자분이 브라우저로 <서버 IP>/docs에 접속했을 때 뜬 404 page not found가 사실 nginx가 아니라 Traefik의 응답이었습니다 — 그 순간부터 진짜 API 트래픽이 nginx 대신 Traefik으로 새고 있었을 가능성이 있습니다
1단계 조치 /etc/rancher/k3s/config.yaml에 disable: [traefik, servicelb] 추가 후 k3s 재시작
🔴 2단계 원인(제가 만든 사고) 재시작 뒤 완전히 먹통이 됐습니다(curl이 타임아웃). tcpdump로 원인을 추적한 결과, k3s의 traefik Service가 지워지다 만 채로(finalizer 걸림) 남아서, kube-router가 “이 IP(172.31.27.61 — 하필 호스트 자체의 사설 IP와 같음):80·443엔 이제 받아줄 게 없다”며 명시적 REJECT(ICMP port unreachable) 규칙을 걸어놨고, 이게 nginx가 쓰는 같은 IP·포트까지 막아버렸습니다
2단계 조치 막힌 finalizer를 강제로 지우고(kubectl patch svc traefik --type=merge -p '{"metadata":{"finalizers":[]}}') Service를 완전히 삭제 → REJECT 규칙 소멸
최종 확인 curl https://<API 호스트>/health → 200, {"status":"ok","env":"production","db_configured":true,"stub":false} — 실제 프로덕션 응답 복구 확인
영향 범위 k3s 설치 시점부터 방금 고치기 전까지 외부에서 <API 호스트>으로 오는 요청이 정상 처리 안 됐을 가능성이 있습니다. 정확한 시작 시각은 특정 못 했습니다(k3s 설치 자체가 이 세션 진행 중 여러 단계에 걸쳐 있었습니다)

🔴 교훈을 www/docs/2026-09-09-K3S-harness.md에도 남겼습니다 — 앞으로 이미 다른 서비스(nginx 등)가 host 네트워크를 쓰고 있는 서버에 k3s를 설치할 땐 설치 직후 곧바로 disable: [traefik, servicelb]를 넣습니다. k3s의 기본 LoadBalancer(ServiceLB)가 노드 자신의 IP를 “external IP”로 그대로 쓰기 때문에, 호스트가 이미 쓰는 포트와 충돌하는 게 예외가 아니라 기본 동작입니다.

정어진께 죄송합니다. 미리 여쭤보지 않고 진행한 것이 이 사고로 이어졌습니다. 실제 서비스 영향이 있었던 시간대에 요청이 실패한 사용자가 있었는지, 로그로 확인이 필요하시면 말씀해 주세요.

✅ 추가 진행 (2026.09.09) — 노출됐던 시크릿 3개 전부 교체 완료

정어진이 권고한 순서(WORKER_TOKEN → DB 비밀번호 → JWT_SECRET) 그대로 진행했습니다. 각 단계마다 supersub 서버의 systemd(:8000)·k3s 파드(:8080) 둘 다 갱신하고 헬스체크했습니다.

   
WORKER_TOKEN 교체 완료. 다만 supersub-ai가 지금 정지 상태라 그쪽 /etc/supersub/worker.env는 아직 옛 값입니다 — 접근 권한이 없어 저는 못 고칩니다. 정상호가 다음에 supersub-ai를 켤 때 새 값으로 갱신 필요(fail-closed라 안 고쳐도 워커가 401로 멈추기만 하고 조용히 잘못되진 않습니다)
DB 비밀번호 ALTER ROLE supersub WITH PASSWORD ...로 교체, .env·k8s Secret 갱신. db_configured: true로 확인
JWT_SECRET 교체 완료 — 로그인해 있던 사용자는 전부 로그아웃됩니다
확인 매 단계 curl localhost:8000/health·curl localhost:8080/health·curl https://<API 호스트>/health 전부 200 유지
예전 Docker Hub digest(56b21535...) 삭제 안 했습니다 — 레지스트리 삭제 API는 인증(토큰)이 필요한데 대화에 토큰을 넣지 않는 원칙이라 여기서는 못 합니다. 다만 시크릿을 이미 교체해서 그 이미지 안 값은 이제 전부 무효라 급하지 않습니다. 원하시면 Docker Hub UI에서 직접 지워 주세요

✅ 해소 (2026.09.11, 정상호) — 넣었고, 워커가 큐를 실제로 비웠습니다

인스턴스를 켜 주셔서 /etc/supersub/worker.env 를 새 값으로 갱신했습니다. 🔴 결과가 「401 이 안 난다」보다 셉니다 — 밀려 있던 큐 18건을 실제로 처리했습니다.

   
고치기 전 상태 서비스 failed, 종료 코드 78/CONFIG, 저널에 「claim 이 401 이다」. 🔴 fail-closed 가 설계대로 동작한 것입니다 — 조용히 잘못 돌지 않고 멈춰 있었습니다
옛 값 / 새 값 길이 43 → 40 (값은 안 적습니다)
반영 확인 🔴 값을 안 찍고 확인했습니다 — 로컬에서 미리 구한 기대 해시와 서버 파일의 해시가 2f8b049d 로 일치. 토큰 줄 1개, 다른 키 6/6 보존, 권한 600 root:root, 백업 .bak.20260911-030113
재기동 뒤 (03:01~) active · NRestarts 0 · 진짜 401 0건 · Traceback 0건
처리한 작업 18건 집음 → 성공 8 · 실패 10
실패 10건 전부 품질 게이트 미달(하반신 키포인트 유효율 53% < 70%)입니다 — 고장이 아니라 설계된 거절이고 failed + 사유로 정상 보고됐습니다
리포트 자리 성공 8건 모두 reports/<user_id>/<video_id>/report.json — 🔴 계약 자리가 실서버에서 처음 확인됐습니다(jin 24번. 09-08 이전 것들은 타임스탬프 폴백 자리입니다)

곁가지로 확인된 것 둘 (제 조각은 아니고 관찰입니다):

  • 영상 수명 주기가 살아 있습니다. 성공 8건 중 지금 S3 에 남은 리포트는 2건입니다 — 나머지는 저장 안 하고 나가서 정리된 것으로 보입니다 (jin 24번 ⑴ 의 형태). 기전 확인은 정어진 님 영역이라 단정하지 않습니다
  • 🔴 같은 클립을 아홉 번 다시 올려 아홉 번 같은 이유로 떨어졌습니다 — 따로 올렸습니다(같은 구역 41번)

🔴 값은 저장소 어디에도 안 적었습니다 — .gitignore 가 worker.env 를 막고, 공개 저장소라 한 번 올라가면 회수가 안 됩니다. 위 표에 값 대신 길이·해시만 있는 것도 같은 이유입니다.

(경과) 넣기 전에 값부터 검증했습니다 — 인스턴스 없이 됩니다

검증 상대는 supersub-ai 가 아니라 백엔드라 켜기 전에 할 수 있었습니다. 🔴 claim 은 부르면 작업을 가로채므로 쓰지 않고, 없는 job id 에 PATCH 를 걸어 갈랐습니다: 새 토큰 → 404 JOB_NOT_FOUND(인증 통과) · 대조군으로 일부러 틀린 토큰 → 401 INVALID_TOKEN. 🔴 대조군이 핵심입니다 — 없으면 「무엇을 넣어도 404 아닌가」를 배제 못 합니다.

🔴 제 grep 이 두 번 틀렸던 것도 적어 둡니다

집계하다 「401 이 3건」·「성공 0건」이 나와 잘못 보고할 뻔했습니다.

  • 「401」은 파일명의 -0401-(업로드 시각) 에 걸린 것이었습니다. 진짜 401 은 0건
  • 성공 로그 문구가 완료 가 아니라 성공 이라 0건으로 셌습니다

세어서 나온 수는 쓰기 전에 걸린 줄을 눈으로 볼 것 — 로그 집계에서 반복될 형태입니다.

이전 기록 — 값 검증만 끝났던 시점 (2026.09.11 오전) 새 `WORKER_TOKEN` 을 전달받아 **인스턴스를 켜기 전에 먼저 검증**했습니다. 검증 상대는 `supersub-ai` 가 아니라 **백엔드**라 인스턴스 없이 됩니다. 🔴 **작업을 소비하지 않는 방법으로 쟀습니다.** `claim` 은 부르면 작업을 가로채므로(워커 인터페이스 문서의 경고) 쓰지 않고, **없는 job id 에 `PATCH`** 를 걸어 갈랐습니다: | 무엇 | 결과 | |---|---| | 백엔드 `/health` | `200` | | 새 토큰 + 존재하지 않는 job id | **`404 JOB_NOT_FOUND`** — 인증 통과 | | **대조군**: 일부러 틀린 토큰, 같은 job id | **`401 INVALID_TOKEN`** | 🔴 **대조군을 같이 돌린 것이 이 확인의 핵심입니다** — 없으면 「무엇을 넣어도 404 가 나오는 것 아닌가」를 배제하지 못합니다. 랜덤 UUID 라 실재하는 작업을 건드리지 않았고, 없는 작업에 대한 `PATCH` 는 아무것도 바꾸지 않습니다. **남은 것은 `/etc/supersub/worker.env` 반영 하나**인데 **인스턴스에 못 닿습니다**: ``` aws ec2 describe-instances → NoCredentials (자격증명이 깨져 조회·기동 불가) ssh <알고 있는="" IP=""> → Connection timed out ``` 🔴 **값은 저장소 어디에도 안 적었습니다** — 루트 `.gitignore` 가 `worker.env` 를 막고 있고, 이 저장소는 공개라 한 번 올라가면 히스토리에서 회수가 안 됩니다. 위 표에 값이 없는 것도 같은 이유입니다. **켜 주시면 바로 넣겠습니다.** 필요한 것은 ⑴ `supersub-ai` 기동 ⑵ 현재 퍼블릭 IP ⑶ 보안그룹 22번에 제 IP 허용(제 IP 가 바뀌었을 수 있습니다 — 그때 값을 알려 드리겠습니다). 넣은 뒤에는 `systemctl restart supersub-worker` + 저널 확인까지 하겠습니다. </details> - **담당**: ~~정상호(`supersub-ai` worker.env 갱신)~~ **✅ 해소 (2026.09.11)** — 넣었고 큐 18건을 실제로 처리했습니다 · **제기**: 박민호 · **기한**: ~~`supersub-ai` 다음 기동 전~~ → **완료** #### ✅ 추가 진행 (2026.09.09) — alembic 마이그레이션 훅 추가 (정어진 지적 반영) 정어진이 물었던 "배포되는 파드가 `alembic upgrade head`를 실제로 도나요?"에 대한 답 — **안 돌고 있었습니다.** initContainer로 추가했습니다. | | | |---|---| | 방식 | Deployment에 `initContainers`로 같은 이미지·같은 env(`supersub-api-env`)를 써서 `alembic upgrade head`만 실행. **성공해야만** 메인 컨테이너(`uvicorn`)가 뜹니다 — 마이그레이션이 실패하면 앱도 안 뜹니다(의도된 동작입니다, 스키마 안 맞는 채로 뜨는 것보다 낫습니다) | | 확인 | `kubectl logs -c migrate` → alembic이 정상 접속·실행(지금은 이미 head라 실제로 옮긴 리비전은 없음) · 메인 컨테이너 `Running` · `/health` → `200` | | CD와의 관계 | `supersub-cd.timer`가 새 digest를 감지해 `rollout restart`를 걸 때마다 이 initContainer도 같이 다시 돕니다 — **새 마이그레이션이 든 이미지가 배포되면 자동으로 반영됩니다** | 절차는 `www/docs/2026-09-09-K3S-harness.md`에도 남겼습니다. - **담당**: 정어진(검토) · **제기**: 박민호 · **기한**: 확인되는 대로 ### 12. 이 WSL의 로컬 Postgres — DB 통합 테스트 막던 원인, 고쳐졌습니다 ✅ 해소 (2026.09.09) **min 1번 회신**에서 "이 환경에서 `password authentication failed for user "supersub"`로 DB 통합 테스트를 못 돌렸다"고 적었던 것과 같은 증상입니다. 이번에 로컬 k3s 작업을 하다가 원인을 찾아 고쳤습니다. | | | |---|---| | 원인 | `supersub-postgres` 컨테이너(2026-08-26에 만들어져 5432 포트로 떠 있었음)가 최근 다른 프로젝트 컨테이너에 **5432 포트를 뺏겨서** 못 뜨고 있었습니다. `.env`의 `DATABASE_URL`은 여전히 5432를 가리키고 있었으니, 뭘 시도해도 인증이 안 됐던 것입니다 | | 조치 | 기존 컨테이너의 **데이터 볼륨은 그대로 보존**하고, 포트만 **5433**으로 옮겨 재생성. 이미지도 `pgvector/pgvector:pg18`로 바꿔 확장까지 추가(`CREATE EXTENSION vector`) | | `.env` | `DATABASE_URL`의 포트를 5432 → 5433으로 수정 (로컬 파일, `.gitignore`라 커밋 대상 아님) | | 확인 | `alembic upgrade head` 끝까지 통과 (18개 리비전 전부) · `.venv/bin/pytest -q` → **633 passed**, skip 없음 | **다른 사람 컴퓨터에는 해당 없습니다** — 이건 이 WSL(제 로컬 개발 환경)에만 있던 문제였고, 각자 자기 로컬 DB 설정은 그대로면 됩니다. `docs/deployment.md`나 공용 설정은 건드리지 않았습니다. - **담당**: 박민호 · **제기**: 박민호 · **기한**: 해소됨 ### 13. 로컬 k3s 트라이얼 배포 — 백엔드가 8080이 아니라 18080을 씁니다 ✅ 해소 (2026.09.09) 이 WSL에서 백엔드를 파드로 띄워 브라우저(Windows)로 확인하려는데 `http://127.0.0.1:8080`이 계속 빈 화면으로 멈췄습니다. | | | |---|---| | 원인 | **Windows 쪽에 이미 8080을 쓰는 프로세스(`svchost.exe`)가 있었습니다** — `cmd.exe /c "netstat -ano \| findstr :8080"`으로 확인. WSL2는 Windows에 없는 포트만 `127.0.0.1`로 대신 열어주기 때문에, 이미 Windows가 쓰는 포트는 WSL 안에 뭘 띄워도 절대 안 뚫립니다 | | 확인 순서 | WSL 안에서 `curl 127.0.0.1:8080/health`는 됐는데(200) Windows 브라우저만 안 됨 → WSL IP(`172.30.91.165:8080`) 직접 접속은 Windows에서도 됨 → 그래서 "앱 문제"가 아니라 "포트 충돌"로 좁혔습니다 | | 조치 | 파드 포트를 **18080**으로 바꿔 재배포. `netstat`에 8080만 있고 18080은 비어 있어서 그걸로 정함 | | 확인 | `http://127.0.0.1:18080/health` — Windows 브라우저에서 정상 응답 | **다른 사람 컴퓨터에서 같은 걸 하다 8080이 막히면**, 원인이 같을 수도, 아닐 수도 있습니다 — `netstat -ano | findstr :<포트>`(Windows 쪽)로 먼저 그 포트를 이미 Windows가 쓰고 있는지부터 보는 게 빠릅니다. 이건 이 컴퓨터의 로컬 트라이얼 배포에만 해당하고, AWS `supersub`의 포트(8080, `min` 11번)와는 무관합니다. - **담당**: 박민호 · **제기**: 박민호 · **기한**: 해소됨 ### 15. 영상 업로드 「요청 값이 올바르지 않습니다: filename」 — 원인 찾아 고쳤습니다 ✅ 해소 (2026.09.09) 사이트에서 직접 영상을 올려 분석을 시작해보다가 겪었습니다. | | | |---|---| | 원인 | `www/`가 `POST /videos/upload-url`에 `filename`을 안 보내고 있었습니다 — `fastapi/docs/client-contract-changes.md` 21절(2026-09-08, 정어진이 이미 요청해둔 것)이 필수라고 명시한 필드인데 아직 반영이 안 된 상태였습니다 | | 조치 | `www/src/lib/uploadClip.ts`(업로드/등록 두 요청 모두에 `filename: file.name` 추가) · `www/src/app/api/videos/upload-url/route.ts`(필수 검증에 `filename` 추가, register 쪽 `route.ts`는 원래 body를 그대로 넘기는 구조라 타입만 보강) | | 확인 | `npx tsc --noEmit` 통과 · 관련 vitest(`uploadClip`·`AnalysisStage`·`MyVideos`) 84개 중 83 통과, 나머지 1개는 제가 고친 부분과 무관한 리포트 폴링 타임아웃이고 **단독 실행하면 통과**(전체 스위트 동시 실행 시 리소스 경합) · `npx eslint` 새 경고 없음 | `www/` 소유가 백성검이라, 검토 부탁드립니다 — 급한 버그라 바로 고쳤습니다. - **담당**: 백성검(검토) · **제기**: 박민호 · **기한**: 확인되는 대로 ### 23. 폰 실제 설치 → 가입 테스트(`retopia12@naver.com`) — DB 반영 확인함 ✅ 확인 (2026.09.14) 폰에 앱 설치 후 `retopia12@naver.com`으로 가입. 서버(`supersub`) DB를 직접 조회해 정상 반영을 확인했다. | | | |---|---| | 확인 | `ssh supersub` → `sudo -u postgres psql -d supersub` 로 `user`·`user_credential` 조회 | | 결과 | `user` 행 생성(닉네임 `pmh12`, 2026-09-14 04:08:05 UTC) + `user_credential` 행 동시 생성. 소셜 로그인이 아니라 이메일/비번 가입 경로(`user_identity` 없음) | 🔴 **DB는 AWS RDS가 아니라 앱과 같은 EC2 인스턴스의 로컬 PostgreSQL이다** (`fastapi/docs/deployment.md` "DB 위치는 정해졌다" 절 — 2026-09-02 결정, 아직 RDS로 안 옮김). RDS라고 알고 있었다면 정정. - **담당**: 박민호 · **제기**: 박민호 · **기한**: 해소됨 ### 24. `deployment.md`의 k3s 배포 위치가 문서와 실제가 다릅니다 — 정어진 확인 부탁드립니다 ✅ 해소 (2026.09.15) 23번 확인하며 같이 봤다. `deployment.md`는 `supersub` 네임스페이스의 `deploy/api`라고 적혀 있는데(상단 요약 "확인" 줄 포함), **실제로는 `default` 네임스페이스의 `supersub-api-trial`**로 떠 있다. ``` sudo k3s kubectl -n supersub get pods → No resources found (네임스페이스 자체가 없음) sudo k3s kubectl get deploy -A → default 에 supersub-api-trial (1/1 Running) ``` 동작 자체는 정상이었다(`/health` → `{"status":"ok",...,"db_configured":true}`). **기능 문제는 아니고 문서-실제 불일치다.** `www/docs/2026-09-09-K3S-harness.md`도 같은 값을 쓰고 있을 수 있어 함께 확인이 필요해 보인다. 남의 영역 문서라 직접 고치지 않고 여기 올린다. - **담당**: 정어진 · **제기**: 박민호 · **기한**: 확인되는 대로 **확인 (2026.09.15)**: `ssh supersub`로 재확인 — 같은 결과(`default`/ `supersub-api-trial`, 1/1 Running). **새로 발견된 게 아니라 `jin` 32번에서 09-11에 이미 같은 사실(이름·네임스페이스 포함)을 남겨 둔 것**이다 — 실제로 도는 k3s Deployment 매니페스트 자체가 저장소에 없고 서버의 `~/k3s-trial/`에만 있어서, `deployment.md`도 그 실물을 못 따라간 상태다(min 14 원래 2번 미착수). `www/docs/2026-09-09-K3S-harness.md`는 네임스페이스를 `<네임스페이스>` 자리표시자로만 써서 어긋남 없다. `deployment.md` 자체를 고치는 것은 매니페스트를 저장소로 옮기는 `jin` 32번 작업과 함께 하는 게 맞아 보여 지금은 문서만 고치지 않았다 — 상세·다음 조치는 `jin` 32번 참고. ### 26. 부록 D ERD가 실제 DB 테이블과 다릅니다 — 정어진 확인 부탁드립니다 ✅ 해소 (2026.09.15) 🔴 **번호 예외**: `pending.markdown`에는 이 항목도 원래 `23.`으로 남아 있었다 — 바로 위 `24. deployment.md...` 항목과 짝을 이루는 `23.`(폰 실제 설치 확인)이 이미 있어 번호가 중복돼 있던 것을 미처 못 보고 또 `23.`으로 올렸다. 두 항목 다 해소돼 함께 아카이브로 옮기며 `26.`으로 바꿨다(뒤 QA 체크리스트도 `24.`→`27.`) — 옮기는 김에 번호 중복을 없앤 유일한 예외이며, 이 둘을 번호로 참조하는 곳은 없음을 확인했다. 21번 확인 김에 `\dt`로 `supersub` DB 전체 테이블 목록을 뽑아 부록 D(34개 테이블)와 대조했다. | | | |---|---| | 실제 DB에만 없음(ERD엔 있음) | `title_criteria` · `player_vector` · `fitness_score` · `recommendation` — 4개, 아직 마이그레이션이 안 올라간 것으로 보인다 | | 실제 DB에만 있음(ERD엔 없음) | `analysis_metric_criterion` — 1개, 문서에 안 적힌 테이블 | 실제 DB는 31개 도메인 테이블(+ `alembic_version`)이고 부록 D는 34개라고 적혀 있다. 스키마 변경 자체가 문제는 아니고 — 어느 쪽이 최신인지, `analysis_metric_criterion`이 뭘 대신하는 테이블인지(`analysis_metric_value`와 이름이 겹쳐 보인다) 확인이 필요해서 올린다. 부록 D는 공개 문서라 직접 고치지 않았다. | | | |---|---| | 확인 | `ssh supersub` → `sudo -u postgres psql -d supersub -c '\dt'` | | 하지 말 것 | 마이그레이션이 진행 중일 수 있으니 4개가 "빠졌다"고 단정하고 부록 D에서 지우지 않기 — 정어진 확인 먼저 | - 관련: `jekyll/chapters/부록D-데이터베이스ERD.markdown` · 21·22번(같은 세션에서 발견) - **담당**: 정어진 · **제기**: 박민호 · **기한**: 확인되는 대로 **확인 (2026.09.15)**: `\dt` 재확인 결과 같음(31개 + `alembic_version`). 두 갈래로 원인이 다르다. 1. **없는 4개는 "빠진" 게 아니라 아직 착수 전이다.** `fastapi/app`·`alembic/versions` 어디에도 ORM·마이그레이션이 없다 — 부록 D 본문 그대로 SFR-005(`player_vector` 벡터 매칭)·SFR-006(`fitness_score` 적합도)·SFR-007(`recommendation` 추천) 기능을 위한 **설계 단계 스키마**다. 부록 D는 계획 문서로서 정확하므로 지우지 않는다. 2. **`analysis_metric_criterion`은 반대로 문서 쪽이 밀렸다.** `ho` 38번(정상호 제기·내 판단, 2026-09-09~10)으로 실제 구현된 테이블이고, `analysis_metric_value` (지표 항목별 값)와는 다른 테이블 — 등급의 `band`/`out_of_band`/`view_dependent` 같은 개발 확인용 메타를 담는다. 부록 D 본문에 이 절을 추가해 반영함 (`jekyll/chapters/부록D-데이터베이스ERD.markdown`). ### 27. QA 체크리스트 페이지를 만들었습니다 ✅ 완료 (2026.09.14) 8장(테스트 및 검증 계획) 하위에 **QA 체크리스트**(`/qa-체크리스트/`)를 추가했다. 5장 요구사항(SFR·SEC·PER·QUA)을 근거로 기능 단위 인수 테스트 체크박스로 옮긴 것이다 — 가입·로그인부터 매칭·평가·보안·성능까지. - 파일: `jekyll/qa/qa-체크리스트.markdown` (신규 폴더). `08-테스트및검증계획.markdown`에 `has_children: true`를 추가하고 QA 프로세스 절에 링크를 걸었다 - **체크는 마크다운 직접 편집(`[ ]` → `[x]`)으로 한다** — 정적 사이트라 브라우저 클릭으로는 저장되지 않는다(2026.09.14 확인). 박민호가 기능을 검증할 때마다 알려주면 체크 반영 - `min` → `main` 및 `ho`·`jin`·`paik` 전 브랜치에 sync 완료 - 확인: http://localhost:4000/qa-체크리스트/ (로컬) · `git log --oneline main -- jekyll/qa` - **담당**: 박민호 · **제기**: 박민호 · **기한**: 완료됨 ## paik (백성검) ### 1. 분석한 영상을 우리 서버에 저장하는 경로 ✅ 해소 (2026.09.03) 영상 분석 화면(`/analysis`)의 리포트 머리글에 **저장** 단추를 달았습니다. 리포트가 다 나오면 풀리고, 지금은 **눌러도 아무 데도 안 보냅니다** — 올릴 곳이 없어서입니다. | | | |---|---| | 만족해야 할 성질 | 브라우저가 고른 영상 파일과 그 리포트를 **한 벌로** 우리 서버(EC2)에 올릴 수 있을 것. 엔드포인트 이름·형식은 자유입니다 | | 웹이 할 일 | 라우트 핸들러(`www/src/app/api/…`)에서 중계합니다. 브라우저는 FastAPI 를 직접 부르지 않는 것이 이 앱의 규칙입니다 | | 확인 | `grep -n '경로가 생기면 여기서 부른다' www/src/components/analysis/AnalysisStage.tsx` → 걸리면 아직 안 붙은 것입니다 | | 하지 말 것 | 단추를 지우지 않기 · 리포트가 끝나기 전에 풀리게 하지 않기(저장되는 것이 분석 결과까지 한 벌이라서입니다) | 🔴 **객체 저장소가 먼저 정해져야 합니다**(계약 5장 ASM-003). 계약 3-1 은 적재만 있고 조회는 "화면이 정해진 뒤에 낸다"로 미뤄져 있는데, 이 화면이 그 규격의 근거입니다 — 리포트 조회 규격과 같이 정하는 편이 낫습니다. - **담당**: 정어진 · **제기**: 백성검 · **기한**: 스프린트 3 (조정 가능) #### 진행 — 붙였지만 "리포트를 저장"이 아니라 "영상을 올려 진짜 분석" (2026-09-03, 박민호) 객체 저장소는 이미 정해져 있었습니다 — jin-12번에서 연 클립 업로드(계약 3-6절, S3 사전 서명 URL)가 그것입니다. 그래서 **새 저장 형식을 정하는 대신 그 경로를 그대로 재사용**했습니다. 대가가 하나 있습니다: **이 화면의 가짜(mock) 리포트는 같이 저장되지 않습니다.** 버튼을 누르면 영상만 올라가고, 서버가 그 영상을 **진짜로** 분석합니다 — "분석 결과까지 한 벌"이라던 원래 설계와 다릅니다. 리포트를 정말 한 벌로 저장하려면 그 저장 형식(담당 정어진)이 먼저 필요해서, 이번엔 하지 않았습니다. - 만든 것: `www/src/app/api/videos/upload-url`·`www/src/app/api/videos` (라우트 핸들러) · `www/src/server/backend/fastapiCall.ts` - 🔴 **`Backend`/`getBackend()` 를 거치지 않습니다.** 그 인터페이스는 아직 영상을 모르고 `getBackend()` 는 `USE_MOCK` 과 무관하게 늘 mock 을 반환합니다 (jin-10, 아직 미완료) — `fastapiCall.ts` 는 그 앞에 좁게 낸 임시 길입니다. jin-10 이 진짜 게이트웨이(`fastapiBackend`)를 만들면 이 파일은 그리로 흡수돼야 합니다 - 종목 코드는 화면(`soccer`) → 백엔드(`football`) 로 경계에서 변환합니다 (`SPORT_CODE` 상수, `AnalysisStage.tsx`) - 확인: `grep -n "callFastApi" www/src/app/api/videos/route.ts www/src/app/api/videos/upload-url/route.ts` · 시험 `npx vitest run src/components/analysis/AnalysisStage.test.tsx` - 검증: `tsc --noEmit`·`next build`·`eslint`·시험(새 시험 2개 포함 250개) 전부 통과. 다만 이 환경엔 EC2 로 가는 경로(DNS 미설정·포트 미개방)도 SSH 도 없어 **실제 EC2 까지 붙는 것은 확인 못 했습니다** — 로컬에서 SSH 터널로 확인 부탁드립니다 - 다음: 리포트까지 한 벌로 저장하려면 그 규격(정어진)이 필요합니다. 정해지면 이어서 붙이겠습니다 #### ✅ 실제로 끝까지 확인했습니다 (2026-09-03, 박민호) `supersub-ai.com/analysis`에서 영상 올리고 저장 눌러서 **S3에 실제로 올라가는 것까지 확인했습니다**(`aws s3 ls s3://supersub-ai/videos/` 로 직접 대조). 위에서 "EC2 까지 못 붙여봤다"고 적었던 것을 포함해 그날 하루 동안 막혀 있던 것들을 전부 풀었습니다 — 순서대로: 1. **jin 8번**: Vercel `super-sub-cloud` 프로젝트의 Production Branch가 `paik`였던 것을 `main`으로 고쳤습니다(min 6번과 같은 뿌리 — 이번엔 Settings → **Environments** → Production → Branch Tracking 이었습니다, Git 페이지가 아니었습니다) 2. **nginx + Let's Encrypt HTTPS**를 EC2에 새로 올렸습니다 (`https://`), uvicorn에 `--proxy-headers` 를 빠뜨려서 요청 제한(SEC-009)이 전부 한 IP로 뭉치던 것도 같이 고쳤습니다. 인증서 자동 갱신 타이머(`certbot-renew.timer`, 매일 03:27 UTC)도 걸었습니다 3. Vercel에 `BACKEND_BASE_URL=https:///api/v1` 추가 4. **jin 10번(로그인이 mock)도 오늘 같이 끝냈습니다** — 저장 버튼이 "토큰이 유효하지 않습니다"로 막혀서 보니, `USE_MOCK=1`이 Vercel Production/Preview 환경변수에 **8월 28일부터 이미 걸려 있었습니다.** 지우고 `fastapiBackend.ts`(Backend 10개 메서드 전부)를 만들어 `getBackend()`가 실제 서버를 보게 했습니다 — 자세한 내용은 jin 10번에 5. **S3 버킷 CORS**가 없어서 브라우저의 S3 직접 PUT이 "Failed to fetch"로 막혔습니다. 콘솔 → `supersub-ai` 버킷 → 권한 → CORS에 `PUT`· `Content-Type`·`https://supersub-ai.com`(`www.` 포함) 허용 규칙을 추가했습니다 🔴 **정상호 님께**: S3 CORS는 버킷 전체에 걸리는 설정이라(문서 「CORS — 브라우저에서 올릴 때만 필요하다」절에서 미리 알려드리라고 하셨던 부분) 알려드립니다. `models/`·`reports/` 접두사 쓰시는 데는 영향 없을 것으로 보이지만, 이상 있으면 여기 남겨주세요. ### 2. 팀 구성원의 **카드 식별자**가 없어 스쿼드에 등재할 수 없습니다 (2026-09-04 신설) ✅ 해소 (2026.09.04) > **`GET /teams/{team_id}` 의 `members[]` 에 두 값을 실었습니다.** > > ```json > { "user_id": "…", "nickname": "홍길동", "role": "owner", "joined_at": "…", > "player_card_id": "7b2d…", "card_public_slug": "brave-tiger-1234" } > ``` > > **말씀하신 대로 등재에 쓸 값과 링크에 쓸 값을 나눠** 실었습니다 — 하나만 주면 > 화면이 나머지를 얻을 경로가 없습니다. > > | | | > |---|---| > | 확인 | `curl -s -H "Authorization: Bearer $T" $API/teams/$TEAM \| jq '.members[0]'` → `player_card_id` 가 보이면 된 것입니다 | > | 검사 | `cd fastapi && .venv/bin/pytest -q` → **521 passed**(신규 5) | > | 스키마 | **안 바꿨습니다** — `player_card` 를 조인해서 실어 주는 것뿐이라 기존 필드는 그대로입니다 | > > 🔴 **「하지 말 것」 둘 다 지켰습니다.** 카드가 없는 구성원은 두 값이 `null` 이고 > **목록에는 남습니다**(`outerjoin` 입니다 — 안쪽 조인이면 통째로 사라집니다). > 내부 id 는 등재용으로만 쓰고 링크는 `public_slug` 로 갑니다. > > 화면에서 `player_card_id` 가 `null` 인 사람은 **등재 버튼을 비활성**으로 두시는 > 편이 낫습니다 — 누르면 서버가 막지만, 눌러 보고 알게 하는 것보다 낫습니다. > > 상세: `fastapi/docs/client-contract-changes.md` **17번** · 계약 3-3절 09-03 에 스쿼드(3-7절)가 들어와서, 홈의 `MY SQUAD` 판이 이제 서버에서 읽습니다 — 등재된 사람이 자리에 앉고 새로고침해도 남습니다. **그런데 넣을 수가 없습니다.** `POST /teams/{id}/squad/members` 가 `player_card_id` 를 받는데, **팀 구성원의 카드를 가리킬 방법이 없습니다.** `GET /teams/{team_id}` 는 `user_id` · `nickname` · `role` · `joined_at` 까지만 줍니다. `GET /me/card` 는 내 것이고, `GET /cards/{slug}` 는 슬러그를 이미 알아야 합니다 — 남의 카드 슬러그를 알 경로가 없습니다. | | | |---|---| | 만족해야 할 성질 | **팀 구성원 목록에서 그 사람의 카드를 가리킬 수 있을 것.** 카드가 없는 사람도 있으므로 "없음"이 표현돼야 합니다. 필드 이름·위치는 자유입니다 — `GET /teams/{id}` 의 `members[]` 에 얹어도 되고 별도 경로여도 됩니다 | | 확인 | `curl -s -H "Authorization: Bearer $T" $API/teams/$TEAM \| jq '.members[0]'` — `player_card_id` 든 `card_public_slug` 든 **카드를 가리키는 값이 하나라도 있으면** 된 것입니다 | | 하지 말 것 | 🔴 **내부 카드 id 를 공개 화면에 그대로 노출하지 않기** — 카드가 `public_slug` 로 밖을 상대하는 것과 같은 원칙입니다. 등재에 쓸 값과 링크에 쓸 값이 달라도 됩니다 · 카드가 없는 구성원을 **목록에서 빼지 않기**(팀에는 여전히 있는 사람입니다) **급하지 않습니다.** 읽기는 이미 돌고 있어서 화면이 죽어 있지는 않습니다. 다만 그전까지 넣기 · 빼기는 화면 안에서만 일어나고 새로고침하면 서버 값으로 돌아갑니다. - 관련: 계약 3-7절(스쿼드) · 3-3절(팀) · `www/src/components/SquadPanel.tsx` 주석 - **담당**: 정어진 · **제기**: 백성검 · **기한**: 스프린트 3 (조정 가능) ### 3. 카드를 **꾸밀** 수가 없습니다 — 별명 · 사진에 규격이 없습니다 (2026-09-04 신설) ✅ 해소 (2026.09.11) — 사진 빼고 전부 > **`PATCH /me/card` 로 한 줄(`tagline`)을 정할 수 있습니다.** 말씀하신 "최소한 > 별명 하나면 화면이 살아납니다" 그것입니다. > > ```json > {"tagline": "THREE LUNGS"} > ``` > > `GET /me/card` 와 **`GET /cards/{slug}`(공개 카드) 양쪽에 실립니다** — 안 실으면 > 남이 보는 카드만 밋밋해집니다. > > | | | > |---|---| > | 확인 | `curl -s -H "Authorization: Bearer $T" $API/me/card \| jq .tagline` | > | 검사 | `cd fastapi && .venv/bin/pytest -q` → **544 passed**(신규 23) | > | 상세 | `fastapi/docs/client-contract-changes.md` **18번** · 계약 3장 | > > 🔴 **「하지 말 것」 둘 다 지켰습니다.** `public_slug` 는 요청 본문에 **자리를 > 아예 안 뒀고**(보내도 무시됩니다), 사진은 **저장 위치가 안 정해져서 안 얹었습니다** — > `og_image_key` 가 지금 그 상태라고 짚어 주신 그대로입니다. > > **20자까지고 서버가 안 자릅니다.** 넘으면 422 라, 입력란에서 미리 막아 주시는 > 편이 좋습니다 — 조용히 자르면 쓴 것과 보이는 것이 달라지고 알아차리는 시점은 > 공유한 뒤입니다. > > ⚠️ **사진은 안 됐습니다.** 저장 위치가 정해지면 그때 얹겠습니다 — 이 항목을 > 「별명만」으로 닫는 이유입니다. 미결 7번(카드를 만드는 자리)은 오늘 붙였습니다 — `/me?edit=1` 에서 `POST /me/card` 를 부릅니다. 그런데 **만든 뒤에 손댈 것이 하나도 없습니다.** | 카드에 보이는 것 | 지금 어디서 오나 | |---|---| | 별명(`THREE LUNGS`) | **화면의 붙박이 상수**입니다. 계약에 필드가 없어 서버에서 받아올 데가 없습니다 | | 인물 그림 | 붙박이(`public/player_cutout.png`) — 모두에게 같은 그림입니다 | | 호칭 | 서버가 줍니다(분석 결과). 사람이 고르는 것이 아니라 이대로가 맞습니다 | | `og_image_key` | 규칙으로 채워지지만 **그 위치에 파일이 없습니다**(계약 3장에 그렇게 적혀 있습니다) | | | | |---|---| | 만족해야 할 성질 | **사람이 정한 값이 카드에 실릴 것.** 최소한 별명 하나면 화면이 살아납니다 — 지금은 모든 카드가 글자까지 똑같습니다. 필드 이름 · 경로는 자유입니다(`PATCH /me/card` 든 만들 때 함께 받든) | | 확인 | `curl -s -H "Authorization: Bearer $T" $API/me/card \| jq` — 응답에 **사람이 정할 수 있는 값**이 하나라도 있으면 된 것입니다 | | 하지 말 것 | 🔴 **`public_slug` 를 바꾸게 하지 않기** — 이미 공유한 주소가 죽습니다(계약이 무작위 · 멱등으로 둔 이유와 같습니다) · 사진 업로드를 여기 얹기 전에 **저장 위치부터** 정할 것(`og_image_key` 가 지금 그 상태입니다) | **급하지 않습니다.** 카드는 만들어지고 공유 링크도 돕니다. 다만 그때까지 편집기는 "무엇이 담겼는지"만 보여주고 **바꿀 수 없다고 솔직히 적어 두었습니다** — 있는 척하면 눌러 보고서야 알게 됩니다. - 관련: 계약 3장(선수 카드) · `www/src/components/PlayerCardView.tsx` 주석 · `www/src/app/(app)/me/CardEditor.tsx` - **담당**: 정어진 · **제기**: 백성검 · **기한**: 스프린트 3 (조정 가능) > **갱신 (2026.09.11, 정어진)** — 사용자가 화면에서 편집기를 써 보고 직접 > "저장이 안 되는데 저장되게 하고 싶다"고 다시 요청. 확인해 보니 백성검 > 님이 그새 바탕 · 로고 · 글자 · 글자 색 · 글자 자리 · 사진 · 붓자국까지 > 꾸미는 편집기를 만들어 두셨는데(`CardEditor.tsx`), **서버 계약에 필드가 > 없어** `localStorage`에만 담고 계셨습니다(이 브라우저에만 남고 공개 카드 > 링크에는 안 실림 — 화면이 그렇게 정직하게 밝히고 있었습니다). > > `PATCH /me/card`에 `style`(바탕 · 로고 · 글자 색 · 글자 자리 · 붓자국)을 > 신설했습니다(`c4f1a6e29b73`). **가운데 큰 글자는 새 필드를 안 만들고 > 위의 `tagline`으로 합쳤습니다** — 편집기의 `style.text` 기본값이 위 > `tagline` 예시와 같은 문구("THREE LUNGS")였던 것이 같은 개념이 두 갈래로 > 갈라져 있었다는 근거였습니다. 남의 영역(`www/`)이라 `www/AGENTS.md` 먼저 > 읽고 편집기 배선까지 같이 고쳤습니다 — `PlayerCardView`가 `look`을 안 > 받아도 `card.style`을 스스로 읽게 해서, 편집 중이 아닐 때·공개 카드 > 화면도 손댈 것 없이 자동으로 꾸며진 대로 그려집니다. > > ⚠️ **사진은 여전히 안 됐습니다** — 09-04 그때와 같은 이유(저장 위치 > 미정)로, 이번에도 그대로 브라우저에만(이 세션 동안만) 남습니다. 그래서 > 제목을 「사진 빼고 전부」로 닫습니다. > > | | | > |---|---| > | 확인 | `curl -s -X PATCH -H "Authorization: Bearer $T" -H 'Content-Type: application/json' -d '{"style":{"bg":"#91ea92","logo":"#0b0b0b","text_color":"#0b0b0b","text_x":50,"text_y":34,"brush":0,"brush_color":"#0b0b0b","brush_scale":1,"brush_x":0,"brush_y":0}}' $API/me/card \| jq .style` | > | 검사 | `cd fastapi && .venv/bin/pytest -q` → 776 passed · `cd www && npx tsc --noEmit && npx vitest run` → 574 passed(1 skipped, 무관) | > | 상세 | `fastapi/docs/client-contract-changes.md` **35번** · `fastapi/docs/api-contract.md` 3절 | ### 4. 분석을 걸지 않고 **올리기만** 할 방법이 없습니다 (2026-09-04 신설) ✅ 해소 (2026-09-08) `/me` 에 업로드 단추를 달았습니다. 그런데 계약 3-6절의 `POST /videos` 는 규격을 통과한 클립에 **늘 분석 작업을 겁니다**(`analysis_job_id` 가 채워져 옵니다). 프로필의 영상은 두 갈래로 갈라 보여주는데(「분석 영상」·「업로드 영상」, 가르는 기준은 `analysis_job_id`), 그러면 **「업로드 영상」 갈래가 영영 안 채워집니다** — 규격 반려된 클립만 거기 남습니다. 기록으로 남기려고 올리는 것과 실력을 재려고 올리는 것은 다른 일입니다. 분석은 GPU 를 쓰는 일이라, 안 볼 리포트를 만드는 것은 서버 쪽에도 손해입니다. | | | |---|---| | 만족해야 할 성질 | **분석을 걸지 않고 클립을 등록할 수 있을 것.** 이름·형식은 자유입니다 — 지금 화면은 등록 본문에 `analyze: false` 를 실어 보내고 있습니다 | | 확인 | 그 값을 실어 `POST /videos` 를 부른 뒤 응답의 `analysis_job_id` 가 `null` 이면 된 것입니다 | | 하지 말 것 | 🔴 **기본값을 바꾸지 마세요.** 값을 안 보내면 지금처럼 분석이 걸려야 합니다 — 분석 화면(`/analysis`)의 저장이 그 동작에 기대고 있습니다 · 반려된 클립에 분석을 걸지 않는 규칙은 그대로 두세요 | ⚠️ 화면은 **보낸 뜻이 아니라 돌아온 응답을 믿습니다.** 지금 백엔드가 이 필드를 모르고 분석을 걸어 버리면 화면도 그대로 「분석 영상」에 넣습니다 — 거짓말하지 않으려고 그렇게 짰습니다. 그래서 **이게 없어도 화면은 안 깨집니다.** **처리 (2026-09-08, 정어진, `45a8564`)**: `RegisterVideoSchema`/`Command` 에 `analyze: bool = True` 를 넣고, 인터랙터에서 `make_job = reason is None and command.analyze` 로 작업 생성을 걸었습니다. 화면이 실어 보내던 그 값 그대로입니다. - **확인 결과**: `analyze: false` 로 `POST /videos` → `passed: true` · `analysis_job_id: null`. 규격 반려는 `analyze` 와 무관하게 그대로(`passed: false` + 사유). 기본값(값 미전송)은 작업이 생김. 테스트 5건, 549 통과. - **하지 말 것 확인**: 기본값 안 바꿈(`= True`). 반려 클립 분석 안 거는 규칙 그대로. - 계약: `api-contract.md` 3-6 · `client-contract-changes.md` **19번**. - 관련: `www/src/lib/uploadClip.ts` · `www/src/app/(app)/me/MyVideos.tsx` - **담당**: 정어진 · **제기**: 백성검 · **기한**: 스프린트 3 (조정 가능) ### 5. 클립에 **공개 여부**가 없어 영상 모음을 서버로 못 옮깁니다 (2026-09-04 신설) ✅ 해소 (2026.09.10) > **프론트 반영 끝났습니다 (2026-09-10, 백성검).** `lib/published.ts` 에서 > 브라우저 저장소를 걷어내고 계약 둘로 바꿨습니다(CCC 20). > > | 무엇 | 어디로 | > |---|---| > | 켜고 끄기 · 제목 · 설명 | `PATCH /videos/{id}` | > | 남의 것까지 목록 | `GET /videos/public` — 홈 영상 모음(`HomeFeed`)이 부릅니다 | > | 재생 | 클립마다 `GET /videos/{id}/playback-url` (목록에 주소가 없습니다) | > > - 🔴 **공개와 제목을 한 번에 보냅니다.** 나눠 보내면 그 사이에 끊겼을 때 > **이름 없는 영상이 남에게 보입니다.** > - 🔴 **공개를 풀 때 제목을 안 지웁니다** — 부분 수정이라 다시 공개할 때 적어 > 둔 이름이 살아 있습니다. > - 🔴 **서버가 바꾼 뒤에야 화면을 바꿉니다.** 먼저 내리고 나중에 부르면, > 실패했을 때 비공개로 보이는데 **실제로는 남에게 계속 보입니다.** > - 🔴 **지운 영상에 PATCH 를 쏘지 않습니다.** 공개도 대표도 클립의 성질이라 > 클립이 사라지면서 같이 없어집니다 — 처음엔 삭제 뒤에도 `unpublish` 를 > 부르고 있었고(404), 시험이 그 두 번째 요청을 잡아 줬습니다. > - 화면 문구를 「이 브라우저에만 남습니다」 → **「다른 사람에게도 보입니다」** > 로 바꿨습니다. 앞의 것은 이제 사실이 아닙니다. > > | 확인 | 시험: `lib/published.test.ts` 여덟 · `lib/feed.test.ts` 일곱 · `MyVideos.test.tsx` 의 공개 일곱 | > |---|---| > > ⚠️ **가져다 쓰면서 걸린 구멍 둘을 새 항목으로 올렸습니다** — 아래 **15번** > (화면 비율)과 **16번**(업로더). 둘 다 이 항목의 목적은 막지 않아서 여기서 > 닫습니다. 홈에서 내리면 나오는 **영상 모음**은 아직 화면 안의 붙박이 목록입니다 (`www/src/lib/feed.ts` 의 `FEED`). `/me` 에서 올린 클립을 **공개**로 돌리면 그 앞에 붙도록 했는데, 계약에 공개 여부가 없어서 **그 브라우저의 `localStorage` 에만** 남습니다. 다른 기기에서도, 남에게도 안 보입니다 — 화면에도 그렇게 적어 두었습니다. 남의 영상까지 나오려면 **네 가지**가 필요했습니다. | 무엇 | 왜 | |---|---| | 클립의 **공개 여부** | 지금은 브라우저에만 있습니다 | | **공개 클립 목록** 조회 | `GET /videos` 는 내 것만 줍니다 — 남의 것을 훑을 경로가 없습니다 | | **재생용 주소** | 계약이 주는 것은 저장 키뿐입니다(3-6절 「아직 없는 것」). 지금 화면은 `/` 로 시작하는 목업 키만 틀 수 있습니다 | | **제목 · 한 줄 설명** | 영상 모음이 큰 글자로 얹는 값입니다. 없으면 이름 없는 칸이 됩니다 | | | | |---|---| | 확인 | 공개로 돌린 클립이 **다른 계정으로 로그인해도** 목록에 보이고, 그 주소로 영상이 실제로 재생되면 된 것입니다 | | 하지 말 것 | 🔴 **기본값을 공개로 두지 마세요.** 이미 올라간 클립이 전부 남에게 보이게 됩니다 · 저장 키를 그대로 주소로 쓰지 마세요(버킷이 열려야 합니다) — 사전 서명이 맞습니다 | **백엔드 처리 (2026-09-08, 정어진) — 네 조각 전부**. 1+2 를 `2c8590d`, 3+4 를 `d95617e` 로: | 무엇 | 상태 | |---|---| | 클립 **공개 여부** | ✅ `PATCH /videos/{id}` `{"is_public": true}`. 등록으로는 못 정하고 `false` 로 저장. 남의/없는 클립은 `404 VIDEO_NOT_FOUND` | | **공개 클립 목록** | ✅ `GET /videos/public` — 공개 클립만, 최근순, 최대 100. 🔴 로그인 필요(익명 홈에서 부르셔야 하면 알려 주세요). 저장 키·업로더는 안 실림 | | **재생용 주소** | ✅ `GET /videos/{id}/playback-url` — 사전 서명 GET URL. 공개 클립이거나 자기 클립일 때만(아니면 404). `expires_in` 초 만료, 캐시 말고 재생 직전에 받기 | | **제목 · 한 줄 설명** | ✅ 같은 `PATCH /videos/{id}` `{"title", "description"}`. 보낸 필드만 바뀜. 100/280자, `null`·공백이면 지움. `GET /videos`·`/videos/public` 응답에 실림 | - **확인 결과**: `is_public` 기본 `false`(기존 행도). `PATCH` 로 공개 → 다른 계정으로 `GET /videos/public` 에 뜸. 공개 클립 `playback-url` 을 남도 받고, 비공개 남의 클립은 404. 부분 수정 — `is_public` 만 토글해도 제목 안 지워짐. 테스트 **23건**, **572 통과**, `alembic check` + `downgrade -2` 왕복 클린. - **하지 말 것 확인**: 기본값 공개 아님(`server_default false`). 저장 키를 주소로 안 씀 — 목록에서 뺐고 재생은 사전 서명. 이미 공유한 주소가 죽는 값(`public_slug`)은 안 건드림. - 계약: `api-contract.md` 3-6(`PATCH` · `GET /videos/public` · `GET /videos/{id}/playback-url`) · `client-contract-changes.md` **20번**. 🔴 **프론트는 아직 이 계약을 안 씁니다 (2026-09-08, main 통합 세션이 코드로 재확인).** > **정정 (2026-09-10, 백성검) — 위 진단의 앞 절반은 이제 틀립니다.** 재생 > 주소는 붙었습니다: `MyVideos.tsx` 가 `lib/playbackUrl.ts` 로 > `GET /videos/{id}/playback-url` 을 부릅니다(paik 12번, 커밋 `32cd644`). > **아래 「`previewSrc` 가 목업 키만 재생한다」는 그때 이미 해소된 것**이라 > 여기 남은 것은 낡은 글이었습니다. > > **여전히 안 된 것은 공개 쪽입니다** — `lib/published.ts` 가 아직 브라우저 > 저장소만 쓰고 `is_public`·`GET /videos/public` 어느 것도 안 씁니다. 이 > 항목의 목적("영상 모음을 서버로 옮긴다")은 그래서 아직 열려 있습니다. `lib/published.ts` 도 여전히 `localStorage` 만 씁니다 — `is_public`· `GET /videos/public`·`playback-url` 어느 것도 안 씁니다. **그래서 이 항목의 목적("영상 모음을 서버로 옮긴다")은 아직 안 이루어졌습니다** — 백엔드 계약만 갖춰진 상태입니다. (앞서 정어진 님 처리 기록 아래 붙어 있던 백성검 님의 "재생 주소는 12번으로 뗐다" 메모는 이 정리로 합쳤습니다 — 내용은 그대로 paik 12번에 있습니다.) - 관련: `www/src/lib/published.ts` · `www/src/lib/feed.ts` · `www/src/app/(app)/me/MyVideos.tsx` · **paik 12번**(같은 프론트 반영이 필요) · 계약 3-6절 - **담당**: 백성검(프론트 반영 — 백엔드 몫은 끝났습니다) · **제기**: 백성검 · **기한**: 스프린트 3 (조정 가능) ### 6. **대상 지정 박스**를 올릴 자리가 없습니다 — 계약에도, S3 에도 (2026-09-08 신설) ✅ 해소 (2026.09.09) — 백엔드 · ✅ 프론트도 반영 (2026.09.10) > **프론트 반영 끝났습니다 (2026-09-10, 백성검).** `lib/uploadClip.ts` 가 > 등록할 때 `subject_box`·`subject_at_ms` 를 함께 싣습니다(CCC 26). > > - 🔴 **「예」를 누른 순간 추적기가 잡고 있는 박스**를 보냅니다 — 처음 손으로 > 그린 네모가 아닙니다(`AnalysisStage.subjectAnchor()`). 항목이 그렇게 > 적어 두신 그대로입니다. > - 🔴 **두 값을 한 덩어리(`ClipSubject`)로 받습니다.** 「함께 보내거나 함께 > 생략」을 부르는 쪽이 어길 수가 없게 **구조로** 막았습니다. > - 🔴 **지정이 없으면 통째로 생략합니다** — 「자동으로 고르기」가 정식 경로라 > 억지로 채우지 않습니다. > - 영상 그림 밖으로 걸친 상자는 잘라 냅니다. 규칙을 피하는 클램프가 아니라 > **좌표계 정의**입니다(영상 밖은 영상이 아닙니다). > > | 확인 | `grep -n "subject_box" www/src/lib/uploadClip.ts` → 걸립니다. 시험 `uploadClip.test.ts` 의 「대상 지정 박스」 셋 | > |---|---| > **`POST /videos` 본문에 `subject_box` · `subject_at_ms` 를 넣었습니다** > (정어진, 커밋 `ee401d6` · 마이그레이션 `411d1c83e4ca`). > > **🔴 사이드카가 아니라 claim 응답으로 갑니다.** 항목은 "DB 에만 저장되면 워커가 > 못 봅니다 → S3 사이드카" 라고 적으셨는데, **워커는 이미 `side`·`focus` 를 claim > 응답(`POST /internal/analysis-jobs/claim`)으로 받고 있습니다**(`worker.py` 의 > `job.get("focus")`). 워커가 DB 를 *직접* 안 볼 뿐, claim API 가 그 통로입니다. > 그래서 `subject_box` 도 같은 축으로 갑니다: > `POST /videos` (서버가 정규화·기하 검증) → `analysis_job` 행 → claim 응답 → > `--subject-box "x,y,w,h" --subject-at-ms`. 사이드카를 안 쓰니 백엔드 S3 쓰기도, > 브라우저 사이드카 PUT 도, 스캐너 걱정도 없습니다. > > | | | > |---|---| > | 검증(서버) | `x·y·w·h ∈ [0,1]` 아니면 **422**(픽셀 거부, 클램프 안 함) · `w·h>0` · `x+w≤1`·`y+h≤1` · `at_ms≤duration_ms` · 박스·시각 both-or-neither | > | 지정 없음 | 둘 다 생략 = 「자동으로 고르기」. **실패로 만들지 않습니다.** `analyze:false`·반려면 박스는 버려집니다(담을 작업 행이 없음, 이것도 실패 아님) | > | 브라우저 | 지금 그대로 — 영상만 S3 에 PUT, 박스는 `POST /videos` 본문. IAM 안 건드립니다 | > | 확인 | `grep -n "subject_box" www/src/lib/uploadClip.ts` (프론트) · `git -C fastapi grep -n "subject_box" -- app/analysis` (백엔드, 됨). `pytest -q` 669 passed | > > 🔴 **남은 것 — agent 쪽 배선 (정상호 님).** claim 응답에 값은 실었지만 > `worker.py` 의 `analyze_command` 가 `--subject-box`/`--subject-at-ms` 를 아직 > 안 붙입니다 — **`--focus` 와 똑같은 상태**입니다(백엔드가 내보내고 워커가 아직 > 안 읽음). `job.get("subject_box")` / `job.get("subject_at_ms")` 를 읽어 > `--focus` 옆에 세 줄 붙이면 됩니다. `worker-interface.md` 1절에 적어 뒀습니다. > (담당: 정상호 · agent/) > > ⚠️ 트랙이 도중에 다른 사람으로 갈아타는 문제(정상호 실측 63%)는 이 항목이 > 고치지 못합니다 — 화면 몫은 닻을 좋게 주는 것까지, 그대로입니다. 분석 화면이 「이 사람으로 분석」에서 받은 박스를 **어디로도 못 보냅니다.** 계약 3-6절의 `POST /videos` 본문에 그 자리가 없습니다(`sport_code` · `storage_key` · `duration_ms` · `side` · `width` · `height` — `side` 는 던지는 팔 · 차는 발이라 사람 지정이 아닙니다). **에이전트 쪽은 이미 열려 있습니다**(정상호 님, 커밋 `56e0ada`). 미결 ho 구역 「영상에 여러 사람이 잡힐 때 누구를 분석 대상으로 고를지」 항목의 현황표가 남은 칸을 그대로 가리키고 있고, 그중 **가운데 두 칸이 정어진 님 몫**입니다. ``` --subject-box 0.39,0.35,0.12,0.4 --subject-at-ms 4200 ``` 🔴 **정규화 0~1 좌표만 받습니다.** 화면 픽셀을 보내면 422 로 거부합니다 — 조용히 클램프하면 엉뚱한 사람을 분석하고도 "지정대로 했다"고 답하게 됩니다. #### 🔴 DB 에만 저장되면 워커가 못 봅니다 분석은 `supersub-ai` EC2 에서 도는데 재시작마다 퍼블릭 IP 가 바뀌어 **EC2 가 S3 를 폴링하는 pull 방식**입니다. 그래서 **EC2 는 백엔드 DB 를 보지 않습니다** (IAM 도 S3 만 열려 있습니다). 이건 정상호 님이 ho 구역 항목에 이미 적어 두신 것이고, 저는 화면 쪽에서 같은 벽을 만나 다시 올립니다. 즉 **영상과 함께 S3 에 올라와야 합니다.** 사이드카 JSON (`videos/<키>.subject.json`)이든 객체 메타데이터든 방식은 자유입니다. 스캐너가 접두사 아래에서 **영상 확장자만** 고르도록 이미 고쳐져 있어 (`b899e65`) JSON 이 나란히 있어도 분석에 안 넘어갑니다. | | | |---|---| | 만족해야 할 성질 | 화면이 지정한 박스와 시각이 **워커가 읽는 자리까지** 도달할 것. 엔드포인트 이름 · 필드 이름 · 사이드카냐 메타데이터냐는 자유입니다 | | 웹이 무엇을 줄 수 있나 | 정규화 `x·y·w·h`(0~1)와 `at_ms`. 🔴 **손으로 그린 네모가 아니라 「예」를 누른 순간 추적기가 잡고 있는 박스**입니다 — 손으로 그린 것은 절반이 배경일 수 있습니다(세로 영상에서 하늘이 그랬습니다) | | 브라우저가 직접 올려도 되나 | `POST /videos/upload-url` 이 사이드카 키의 PUT URL 도 서명해 주시면 그렇게 하겠습니다. 지금도 영상은 브라우저가 S3 에 직접 PUT 하므로 IAM 정책(EC2 는 `videos/` 읽기 전용)을 안 건드립니다 | | 확인 | `POST /videos` 응답을 받은 뒤 그 클립의 S3 접두사 아래에 지정 값이 보이면 된 것입니다. 화면 쪽은 `grep -n 'subject_box' www/src/lib/uploadClip.ts` 로 확인합니다 | | 하지 말 것 | 🔴 **지정이 없을 때 실패로 만들지 마세요** — 「자동으로 고르기」가 정식 경로입니다(에이전트도 지정이 없으면 동작이 한 비트도 안 바뀝니다) · 픽셀 좌표를 받아 주지 마세요 | ⚠️ **정직하게 남깁니다** — 정상호 님이 실클립으로 재 보니 닻 프레임은 정확했지만 (찍은 박스와 IoU 0.805, 심판 대신 타자를 잡음) **트랙이 도중에 심판으로 갈아타 63%의 프레임을 다른 사람으로 분석**했습니다. 박스를 전달하는 이 항목이 그것까지 고치지는 못합니다. 화면 쪽 몫은 **닻을 최대한 좋은 것으로 주는 것**까지입니다. - 관련: 미결 ho 구역 「누구를 분석 대상으로 고를지」 · `www/src/lib/uploadClip.ts` · 계약 3-6절 - **담당**: 정어진 · **제기**: 백성검 · **기한**: 스프린트 3 (조정 가능) ### 7. 분석 리포트를 **읽는** 경로가 없습니다 (2026-09-08 신설) ✅ 해소 (2026.09.10) — 백엔드 · ✅ 프론트도 반영 (2026.09.10) paik 1번에서 「저장」이 열리며 *"리포트 조회 규격과 같이 정하는 편이 낫습니다"* 라고 적어 둔 것을, 실제로 막혀서 항목으로 올립니다. 영상은 올라가고 서버가 진짜로 분석하는데, **그 결과를 웹이 되받을 길이 없습니다.** `GET /videos` 가 주는 것은 `analysis_status`(`queued` · `running` · `succeeded` · `failed`)까지고, 리포트 본문 — 요약 · 특징 · 받은 호칭 · 근거가 된 장면 — 을 읽는 경로가 계약에 없습니다. 3-1 절의 `POST /analyses`(적재)도 `metric_definition` 합의 대기 중입니다. 그래서 지금 `/me` 「분석 영상」 아래에 붙는 리포트는 **화면이 만든 자리 표시를 그 브라우저의 `localStorage` 에** 둔 것입니다. 다른 기기에서는 안 보입니다 — 화면에도 그렇게 적어 둡니다(공개 여부 · 카드 꾸미기와 같은 방식입니다). | | | |---|---| | 만족해야 할 성질 | 클립 하나의 분석 결과를 **한 번에** 읽을 수 있을 것. 경로 이름은 자유입니다(`GET /videos/{id}/report` 든 `GET /analyses?video_id=` 든) | | 무엇이 필요한가 | 요약 문장 · 특징 문장들 · 받은 호칭 · **근거가 된 장면**(시각 + 무엇). 이 네 가지가 지금 화면이 그리는 전부입니다 | | 🔴 수치는 빼 주세요 | 계약 3장 4 가 `report.summary` 에 총점 · 등급을 넣지 말라고 못박아 뒀고, 카드에 능력치 컬럼을 두지 않는 원칙(부록 D.5)과 짝입니다. 화면도 그걸 검사합니다(`PlayerCardView.test.tsx`) | | 확인 | 분석이 `succeeded` 인 클립에 대해 그 경로가 위 네 가지를 주면 된 것입니다. 화면 쪽은 `grep -n 'localStorage' www/src/lib/savedReports.ts` 가 안 걸리면 갈아 끼운 것입니다 | | 하지 말 것 | 아직 안 끝난 작업에 빈 리포트를 주지 마세요 — "분석 중"과 "결과가 없다"가 같아 보입니다(스쿼드의 `SQUAD_NOT_FOUND` 와 같은 판단입니다) | 🔴 **경로가 생기면 화면은 파일 하나만 갈아 끼웁니다**(`www/src/lib/savedReports.ts`). 부르는 쪽과 갈라 두었습니다 — `lib/published.ts` · `me/cardStyleStore.ts` 와 같은 방식입니다. > 🔴 **이 항목 앞에 paik 11번이 있습니다**(2026-09-08 추가) — 에이전트는 이미 > 리포트를 S3 에 쓰고 있는데 **그 자리를 아무도 알려 주지 않아서**, 읽는 경로만 > 내주셔도 무엇을 읽을지가 정해지지 않습니다. #### ✅ 앞의 둘이 풀렸습니다 — 네 가지가 다 있습니다 (2026.09.09, 정상호) **⑴ 자리**: `paik` 11번 해소 — 완료 보고에 `report_key` 가 실립니다. **⑵ 요약 문장**: **없었습니다. 만들었습니다.** 요구하신 넷 중 이것 하나가 제 산출물에 아예 없었고, 🔴 백엔드가 지어낼 수 있는 값이 아니었습니다. **📄 `agent/report-contract.md`** 에 네 요구가 각각 어디 있는지 적었습니다: | 요구 | 자리 | |---|---| | 요약 문장 | `result.summary` — 두 문장 이내, 🔴 **숫자 없음**(계약 3장 4) | | 특징 문장들 | `result.breakdown[].evidence` | | 받은 호칭 | `result.breakdown[].title` | | 근거가 된 장면 | `frame_metrics_seconds.impact_frame`(시각) · `breakdown[].metric_ref`(무엇) · `previews`(그림) | 🔴 **수치를 빼 달라신 것 — 구조로 지켰습니다.** `summary` 는 모델이 아니라 코드가 짓고 **숫자를 쓸 자리 자체가 없습니다**(검사가 자릿수 0을 지킵니다). 다만 `band`(구간 문자열)와 `stat`·`out_of_band` 는 봉투에 들어 있으니 **화면에서 걸러 주세요** — 문서에 「하지 말 것」으로 적어 두었습니다. 「아직 안 끝난 작업에 빈 리포트를 주지 말라」신 것도 그대로입니다 — 리포트는 분석이 **끝나야** 생기고, `report_key` 도 `succeeded` 에만 실립니다. > 🔴 **아직 막혀 있습니다 (2026.09.10 오전, 백성검 확인 — 같은 날 오후에 풀렸습니다. > 아래를 보세요. 지우지 않고 남깁니다: 그때 무엇을 보고 막혔다고 판단했는지가 > 남아야 다음 사람이 같은 조사를 반복하지 않습니다).** `fastapi/app/analysis` > 아래 리포트를 읽는 라우트(`GET .../report` 류)를 찾아봤는데 없습니다 > (`grep -rn "APIRouter\|@router\.\(get\|post\)" fastapi/app/analysis` 로 > 확인 — report 관련 GET 라우트 0건). `job_router.py` 에 `report_key` 를 > **받는** 쪽만 있고 **내주는** 경로가 아직 없습니다. 그래서 이 항목에 막혀 > 있는 `min` 9번(3단계 화면 배선) · `ho` 28번(오버롤 화면 표시) · `ho` 24번도 > 같이 대기 중입니다 — 새로 조사할 필요 없이 이 항목이 열리면 셋 다 이어서 > 하면 됩니다. #### ✅ 경로가 났고, 화면도 갈아 끼웠습니다 (2026.09.10, 백성검) **정어진 님이 `GET /videos/{id}/report` 를 내주셨습니다**(CCC 31번 · `jin` 27번, 커밋 `3bacb5c`). 요청한 네 가지가 다 있습니다 — 요약(`summary`) · 특징 (`breakdown[].evidence`) · 호칭(`breakdown[].title`) · 근거 장면(`scenes[]`). - **하드코딩을 걷어냈습니다.** `AnalysisStage.tsx` 안에 리포트 객체가 통째로 박혀 있던 것을 지우고 그 경로를 부릅니다 - **브라우저 저장소도 걷어냈습니다.** `lib/savedReports.ts` 한 파일만 갈아 끼웠습니다 — 부르는 쪽(`AnalysisStage` · `MyVideos`)은 안 고쳤습니다 - 「아직 안 끝난 것」을 갈라 그립니다 — `404 REPORT_NOT_READY` 는 **「분석 중입니다」**, `VIDEO_NOT_FOUND` 는 **「찾을 수 없습니다」**로 다릅니다 - `skipped: true`(=`grade: null`) 항목은 **안 그립니다**. `band`·`stat` 도 응답에 없어 걸러 낼 것이 없었습니다 - 「저장」 단추의 뜻이 바뀌었습니다 — 리포트가 서버에 있으므로 **영상을 남기는 것**만 합니다(저장 없이 떠나면 미저장분 정리가 그 영상을 지웁니다) | 확인 | 결과 | |---|---| | `grep -n 'localStorage' www/src/lib/savedReports.ts` | **0건** (전에는 6건) | | `grep -n 'REPORT\s*=' www/src/components/analysis/AnalysisStage.tsx` | **0건** | | 시험 | 566 통과 (리포트 옮김터 10건 신규) | ⚠️ **배포되기 전까지는 화면에 「분석 중」이 뜹니다** — 그 경로가 아직 `main` 에 없습니다(`jin` 이 앞서 있음). 배선은 끝났으므로 올라가는 즉시 값이 나옵니다. - 관련: paik 1번(해소) · paik 23번(호칭 규칙) · 계약 3-1 · 3-6절 「아직 없는 것」 · min 9번(1~3번을 스프린트 3으로 묶은 자리 — 3번 화면 배선 담당이 여기서 정해짐) - 🔴 **이 항목에 막혀 있던 `min` 9번 · `ho` 28번 · `ho` 24번이 이제 풀립니다** — 읽는 경로가 났고 웹은 붙였습니다(배포는 `jin` 이 `main` 에 올라간 뒤). - **담당**: 정어진 · **제기**: 백성검 · **기한**: 스프린트 3 (조정 가능) ### 9. 스쿼드 판의 **배치**를 서버에 둘 자리가 없습니다 (2026-09-08 신설) ✅ 해소 (2026.09.09) — 백엔드 · ✅ 프론트도 반영 (2026.09.10) > **프론트 반영 끝났습니다 (2026-09-10, 백성검).** `lib/squadBoard.ts` 에서 > 브라우저 저장소를 걷어내고 계약 셋으로 바꿨습니다(CCC 25). > > | 값 | 어디로 | > |---|---| > | 판 크기 | `PATCH /teams/{id}/squad` `{formation}` — 화면의 `'5'` ↔ 계약의 `"5:5"` 는 `sizeToFormation` 한 곳에서 바꿉니다 | > | 칸 · 포지션 | `PATCH …/squad/members/{id}` — 카드를 옮기거나 이름표를 누를 때 | > > - 🔴 **첫 판을 서버 값에서 만듭니다.** 저장소를 읽던 때와 달리 > 하이드레이션이 안 깨집니다 — `squad` 는 서버 컴포넌트가 준 prop 이라 > 서버와 브라우저의 첫 그림이 같습니다. > - 🔴 **자리를 맞바꾸면 둘 다 저장합니다.** 옮긴 쪽만 보내면 밀려난 사람의 > 칸이 서버에 옛 자리로 남아, 다음에 열 때 두 카드가 한 칸에 겹칩니다. > - 🔴 **모르는 `formation` 은 기본 판으로 엽니다.** 계약이 값 집합을 강제하지 > 않아(길이만 봅니다) 화면이 아직 모르는 크기가 올 수 있습니다. > - 🔴 **등재가 아닌 자리는 안 보냅니다** — 지인 판에서 앉힌 사람은 > `player_card_id` 가 없어 등재가 아니고, 없는 등재에 PATCH 하면 404 입니다. > - ⚠️ **주장이 아니면 403** 인데 판은 계속 돕니다(되돌리지도 않습니다 — 방금 > 옮긴 카드가 손 밑에서 제자리로 튀는 편이 더 나쁩니다). > > ⚠️ **한 가지는 계약과 다르게 골랐습니다 — 알려 드립니다.** 계약대로면 > `grid_col`·`grid_row` 가 `null` 인 등재는 「판에 안 올린 것」이라 안 그리는 > 게 맞습니다. 그런데 **기존 행이 전부 null 이라** 그대로 하면 판이 통째로 > 비어 보입니다. 그래서 **포메이션 기본 자리에 포지션으로 맞춰 앉혀서 보여 > 주되, 그것만으로는 저장하지 않습니다**(판을 여는 것만으로 서버가 바뀌면 안 > 되니까요). 한 번 옮기면 그때 칸이 생깁니다. 다르게 보시면 말씀해 주세요 — > `SquadPanel.seatsFromSquad` 의 2)번 갈래 한 곳입니다. > > | 확인 | `grep -n "localStorage" www/src/lib/squadBoard.ts` → **안 걸립니다.** 시험 `SquadPanel.test.tsx` 의 「판 배치가 서버에 남는다」 일곱 | > |---|---| > **안 A로 넣었습니다** (정어진, 커밋 `cbe7f16` · 마이그레이션 `6a0f3d23662c`). > - `squad.formation` `varchar(8)` NULL — 판 크기(`"3:3"`·`"5:5"`·`"7:7"`). > `PATCH /api/v1/teams/{team_id}/squad` `{formation}` 로 저장(주장만). > - `squad_member.grid_col` · `grid_row` `smallint` NULL — 격자 칸. 🔴 픽셀 아님, > `0~15` 밖이면 422. **both-or-neither**(한쪽만 = 422, 둘 다 null = 판에서만 뺌). > - `PATCH /api/v1/teams/{team_id}/squad/members/{member_id}` > `{position_code, grid_col?, grid_row?}` — 이동 + **포지션 바꾸기**(계약 3-7 > 「아직 없는 것」도 함께 해소). 남의 스쿼드 등재는 `404 MEMBER_NOT_FOUND`. > - `POST .../members` 에 `grid_col`/`grid_row` 선택 인자(등재하며 판에 올리기). > - 응답에 `formation` · 멤버별 `grid_col`/`grid_row`. > - 🔴 **격자 뜻의 정본은 계약 3-7 「홈 판 격자」** — 지금 3열(0\~2) × 4행(0\~3), > 행이 포지션 라인(0 FW · 1 MF · 2 DF · 3 GK). 격자 크기가 바뀌면 리매핑 필요. > - 확인: `grep -n "localStorage" www/src/lib/squadBoard.ts` 가 안 걸리면 프론트가 > 갈아 끼운 것. `pytest -q` 658 passed. 클라이언트 반영 안내는 `client-contract-changes.md` 25번. > > ⚠️ **넣기·빼기 배선은 이미 계약에 있었습니다**(3-7 `POST/DELETE .../members`) — > 백성검 님이 배치 저장이 없어 프론트에서 미뤄 둔 것이라 하셨으니, 이제 다 > 열렸습니다. 홈의 스쿼드 판이 이제 **고를 수 있고 옮길 수 있습니다**(사용자 요청) — 판 크기(3:3 · 5:5 · 7:7), 카드가 선 **칸**, 사람이 **손으로 정한 포지션**. 그런데 계약과 부록 D 가 아는 것은 `squad_member` 의 `position_code` 하나뿐이라, 셋 다 담을 데가 없습니다. ``` squad_member : squad_id · player_card_id · position_id ← 이게 전부입니다 화면이 든 것 : 판 크기 · (열, 행) · 손으로 정한 포지션 ``` **그래서 지금은 그 브라우저의 `localStorage` 에 있습니다** (`www/src/lib/squadBoard.ts`). 다른 기기에서는 처음 판으로 열립니다 — 화면에 그렇게 적어 두지도 못했습니다(판에 안내 줄을 놓을 자리가 마땅치 않습니다). ⚠️ **넣기 · 빼기도 아직 서버로 안 갑니다.** paik 2번으로 카드 식별자는 풀렸지만 그 뒤로 등재를 붙이지 않았습니다 — 배치가 로컬에 있는 동안에는 **어느 쪽이 맞는지 헷갈리는 상태**가 되기 때문입니다. 이 항목이 그것까지 같이 푸는 자리라고 봅니다. #### 왜 「포지션이면 충분하다」가 아닌가 `position_code` 만 두면 **같은 포지션 둘을 구별할 수 없습니다.** 1-2-1 은 MF 가 둘이고 2-3-1 은 셋인데, 누가 왼쪽이고 누가 가운데인지가 사라집니다 — `SquadFriends` 주석이 *"1-2-1 은 MF 가 둘이라 포지션 이름만으로는 어느 쪽인지 못 고른다"* 로 적어 둔 것과 같은 문제입니다. 그리고 **손으로 정한 포지션**은 자리에서 역산되지 않습니다. 「올 공격」으로 셋을 윗줄에 올려 두고 한 명만 GK 로 못박아 둘 수 있어서, 칸과 포지션이 서로 다른 값입니다. | | | |---|---| | 만족해야 할 성질 | 판을 그대로 되살릴 수 있을 것 — **크기 · 칸(열·행) · 포지션** 셋이 사람마다(또는 스쿼드마다) 남으면 됩니다. 이름·형식은 자유입니다 | | 어디에 둘지도 결정입니다 | `squad_member` 에 칸을 더할지(A), 판 설정을 따로 둘지(B). **A 를 제안합니다** — 칸은 그 구성원의 성질이고, 판 크기 하나만 `squad` 에 두면 됩니다 | | 확인 | 다른 기기(또는 다른 브라우저)로 로그인해도 같은 판이 열리면 된 것입니다. 화면 쪽은 `grep -n 'localStorage' www/src/lib/squadBoard.ts` 가 안 걸리면 갈아 끼운 것입니다 | | 하지 말 것 | 🔴 **칸을 화면 픽셀로 받지 마세요** — 격자 번호(열 0~2 · 행 0~3)입니다. 픽셀로 두면 카드 크기가 바뀔 때마다 배치가 어긋납니다 · 포지션을 칸에서 역산하지 마세요(손으로 정한 값이 지워집니다) | 🔴 **격자 크기가 바뀌면 저장된 칸의 뜻도 바뀝니다.** 지금은 3열 × 4행이고 행이 포지션을 정합니다(0 FW · 1 MF · 2 DF · 3 GK). 이 값을 규격에 적어 두거나, 칸과 함께 격자 크기도 같이 저장해야 합니다 — 안 그러면 나중에 판을 넓혔을 때 옛 배치가 조용히 다른 자리를 가리킵니다. - 관련: `www/src/lib/squadBoard.ts` · 부록 D 도메인 ③ · 계약 3-7절 - **담당**: 정어진 · **제기**: 백성검 · **기한**: 스프린트 3 (조정 가능) ### 10. **남의 대표 영상**을 읽을 경로가 없습니다 (2026-09-08 신설) ✅ 해소 (2026.09.09) — 백엔드 · ✅ 프론트도 반영 (2026.09.10) > **프론트 반영 끝났습니다 (2026-09-10, 백성검).** `lib/featuredClip.ts` 에서 > 브라우저 저장소를 걷어내고 계약 둘로 바꿨습니다(CCC 27). > > | 무엇 | 어디로 | > |---|---| > | 세우기 · 내리기 | `PATCH /videos/{id}` `{is_featured}` | > | 읽기 | `GET /cards/{slug}/featured-video` — 홈 스쿼드 판이 **내 카드 슬러그**로 부릅니다. 남의 판을 볼 때도 같은 함수가 그 사람 것을 줍니다 | > > - 🔴 **"지금 어느 것이 대표인가"를 화면이 따로 안 들고 있습니다.** 정본은 > `MyVideo.is_featured` 입니다 — 사람당 하나를 서버가 지키므로 화면이 > 기억하면 갈리기만 합니다. > - 🔴 **옛 대표를 먼저 내리지 않습니다.** 두 번 부르면 그 사이에 끊겼을 때 > 대표가 하나도 없는 상태로 남습니다. > - 🔴 **서버가 바꾼 뒤에야 화면을 바꿉니다.** 반려된 클립은 422 > `CANNOT_FEATURE` 인데, 먼저 눌러 두면 세워진 것처럼 보입니다. > - ⚠️ 추천 판 후보의 자리 표시 클립(`/coach-c00N.mp4`)은 그대로 뒀습니다 — > 말씀대로 영상 파일을 더 넣지 않았습니다. > > | 확인 | `grep -n "localStorage" www/src/lib/featuredClip.ts` → **안 걸립니다.** 시험 `MyVideos.test.tsx` 의 「나를 보여주는 대표 영상」 다섯 | > |---|---| > **둘 다 넣었습니다** (정어진, 커밋 `0c89797` · 마이그레이션 `2088e26b34ac`). > - **「대표」 표시**: `video.is_featured` bool. `PATCH /api/v1/videos/{id}` 에 > `{"is_featured": true}`. 🔴 **사람당 하나** — 세우면 옛 대표가 자동으로 > 내려간다(부분 유일 인덱스 `uq_video_featured_per_user` 가 DB 에서 강제). > 🔴 반려된 클립(`passed:false`)은 `422 CANNOT_FEATURE`. > - **남의 대표 읽기**: `GET /api/v1/cards/{card_public_slug}/featured-video` > (로그인 필요). `{video_id, url, expires_in, sport_code, duration_ms}` — > `url` 은 **사전 서명 GET URL**(저장 키 아님, 5번과 같음). 대표가 없거나· > 반려됐거나·슬러그가 없으면 `404 NO_FEATURED_VIDEO`. > - **식별자는 카드 슬러그.** 내부 `user_id` 를 URL 에 안 쓴다(카드와 같은 원칙). > `is_public` 여부와 무관하다 — 대표로 세운 것 자체가 「보여 준다」는 뜻. > - `VideoResponse` 에 `is_featured` 추가. 화면은 `www/src/lib/featuredClip.ts` > 한 파일만 갈아 끼우면 된다 — 반영 안내는 `client-contract-changes.md` 27번. > - 확인: `grep -n "localStorage" www/src/lib/featuredClip.ts` 안 걸리면 프론트 > 갈아 끼운 것. `pytest -q` 681 passed. > > ⚠️ **`paik` 5번(공개 여부·공개 클립 목록·재생 주소)은 별개로 남아 있습니다** — > 이 항목이 5번에 얹으려던 「대표」 칸만 따로 처리했습니다. 5번의 나머지 셋은 > 그 항목에서. 자리 표시 클립(`/coach-c00N.mp4`)은 그대로 두세요(영상 파일 추가 금지). `/me` 의 영상마다 「나를 보여주는 대표 영상」을 고를 수 있게 했습니다(사용자 요청). 뜻은 **사람마다 자기를 한 편으로 보여 주는 장면을 갖는다**는 것이고, 추천 판에서 후보 옆에 도는 영상이 바로 그 사람의 그 장면입니다 — 코치 목록이 코치마다 대표 장면을 두는 것과 같은 결입니다(`lib/market.ts` 의 `report.clipUrl`). **그런데 지금 실제로 갈리는 것은 제 것뿐입니다.** 두 가지가 없습니다. | 없는 것 | 왜 막히나 | |---|---| | 클립에 「대표」 표시 | `video` 에 그 칸이 없습니다(3-6절). 지금은 브라우저에만 있습니다 | | **남의 클립을 읽을 경로** | `GET /videos` 는 **내 것만** 줍니다. 남의 대표 장면을 가져올 데가 없습니다 | ⚠️ 그래서 추천 판의 후보 여덟은 저장소에 있는 자리 표시 클립(`/coach-c00N.mp4`)을 돌려 쓰고 있습니다. **영상 파일을 더 넣지 말아 주세요** — 셋이 이미 16MB 입니다. **paik 5번(클립 공개 여부)과 겹칩니다.** 거기서 요청한 「공개 여부 · 공개 클립 목록 · 재생용 주소」 셋이 그대로 필요하고, 여기서는 **「대표」 표시 하나**가 더 붙습니다. 따로 푸실 필요는 없습니다 — 5번을 푸실 때 이 칸만 같이 봐 주시면 됩니다. | | | |---|---| | 만족해야 할 성질 | 어떤 사람의 **대표 클립 하나**를 그 사람의 식별자로 가져올 수 있을 것 · 대표가 없으면 없다고 답할 것 | | 웹이 줄 수 있는 값 | 지금 고른 클립의 `video.id` 입니다(`lib/featuredClip.ts`) | | 확인 | 다른 계정으로 로그인해서 남의 대표 장면이 추천 판에서 돌면 된 것입니다 | | 하지 말 것 | 🔴 **대표를 여러 개 허용하지 마세요** — 둘이면 어느 것이 그 사람을 보여주는지 정해지지 않습니다 · 반려된 클립(`passed: false`)이 대표가 되지 않게 해 주세요(서버가 안 보는 영상입니다) · 저장 키를 그대로 주소로 주지 마세요(5번과 같습니다 — 사전 서명이 맞습니다) | 🔴 **경로가 생기면 화면은 파일 하나만 갈아 끼웁니다**(`lib/featuredClip.ts`). 부르는 쪽(`MyVideos` · `SquadSuggest`)과 갈라 두었습니다. - 관련: paik 5번(공개 여부 · 재생 주소) · `www/src/lib/featuredClip.ts` · 계약 3-6절 - **담당**: 정어진 · **제기**: 백성검 · **기한**: 스프린트 3 (조정 가능) ### 12. **올린 영상이 배포에서 안 보입니다** — 재생용 주소가 없어서입니다 (2026-09-08 신설) ✅ 해소 (2026.09.08) > **정어진 님이 `GET /videos/{id}/playback-url` 을 내주셨고**(CCC 20번 3조각) 화면에 붙였습니다. > 말씀대로 **`MyVideos.tsx` 의 `previewSrc` 하나**를 고쳤고, 주소를 받아 오는 일은 > `www/src/lib/playbackUrl.ts` 로 뺐습니다 — 만료되는 값이라 컴포넌트가 들고 > 있어야 다시 받을 수 있어서입니다. > > 「확인」: 배포 `/me` 에서 재생 — **로컬에서는 mock 이 `public/` 경로를 줘서 이 > 갈래가 안 돕니다.** 그래서 ⑴ 라우트를 직접 찔러 `{url, expires_in}` · 404 · 401 > 을 확인하고 ⑵ 서버 모양의 저장 키(`videos/u1/abc.mp4`)로 시험 셋을 세웠습니다. > > 🔴 **캐시 안 합니다** — 목록이 바뀔 때만 받습니다. 못 받으면 그 클립만 플레이어 > 없이 그려지고 판은 안 무너집니다(예전에 통째로 무너지던 자리입니다). **paik 5번의 셋째 줄(「재생용 주소」)을 떼어 올립니다. 새 요청이 아니라 급한 것을 드러내는 것입니다** — 5번은 제목이 「공개 여부」라 *영상 모음(남의 클립)* 얘기로 읽히는데, 오늘 배포된 `supersub-ai.com` 에서 재 보니 **내 프로필의 내 영상조차 안 나옵니다.** | | `GET /videos` 가 주는 것 | 화면 | |---|---|---| | localhost (`USE_MOCK=1`) | `/videos/sample-1.mp4` — mock 이 넣는 `public/` 경로 | 재생됩니다 | | **배포 (진짜 백엔드)** | `videos//.mp4` — **저장 키** | **플레이어를 안 그립니다** | 화면이 일부러 안 그립니다 — 저장 키를 그대로 `