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/hois 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)이 추가 비용 없이 풀린다.
막히는 것 둘.
- EC2가 S3에 닿을 방법이 없다. 인스턴스 역할을 못 만들고, 대안이던 “권한 좁힌 IAM 사용자 + 키”도 그 사용자를 못 만들어 함께 막힌다
- 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.md2-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, 백성검).
min9번(리포트 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개는 무관한 플레이키 테스트로 단독 실행 시 통과 확인)
- 관련:
min9번(같은 세션에서 발견) ·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로 옮길 때(paik7) 그 자리에서 정리하면 됩니다 - 제 몫과 붙어 있습니다 —
jin24번이 저에게 배정한 「리포트 산출물 키를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가 태그를 달아 규칙에서 빠집니다) - 정본:
jin24번. 이 항목은 닫고 위 두 가지만 그쪽에 남깁니다 - 담당: 없음(정본은
jin24번) · 제기: 정상호 · 기한: —
원래 적었던 제안 (기록용 — 채택되지 않았습니다)
지금은 「분석」을 누른 순간 영상이 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번(리포트 읽는 경로) → 이 항목. 반대로 하면 사용자가 임시 리포트를 볼 수 없습니다 |
- 관련:
paik7번 · 계약 3-3절 ·jin17번(동작을 담을 자리 —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 머리 주석 |
- 관련:
paik7번(읽기 경로 — 여기에 실려야 합니다) ·jin1번(POST /analyses적재) · 같은 구역 20번(구간 위 0등급) · 23·24번(근거 문장에 등급 없음) - 담당: 정어진(읽기 경로에 필드 추가) · 백성검(화면 표시) · 제기: 정상호(사용자 요청) · 기한:
paik7번과 함께
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-09main병합으로 새 이름이 들어오면서 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)가 전부 그 값으로 짝을 맞춥니다 |
- 관련:
paik8번(집중 항목을 보낼 자리) · 같은 구역 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번입니다 |
- 관련:
jin11번(정본·숫자) · 같은 구역 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.md3-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% 입니다.
판단해 주실 것
- 3장 검증 기준 표를 바꿀 것인가. QWK·MAE 를 「골든셋 확보 시」 조건부로 내리고 재현성·불변성·타당성을 전면에 두는 안을 권합니다. 지우지 말고 조건부로 남기는 편이 낫습니다 — 나중에 지도자가 생기면 그대로 씁니다
- 8장 KPI 표([TBD])에 그 셋을 넣을 것인가. 자리가 비어 있습니다
- 임계값 출처 조사를 제 일감으로 잡을 것인가. 하면 스프린트 2~3 중 며칠입니다
| 확인 | 3장 「검증 기준」 표 · 8장 「검증 지표(KPI 예시)」 표 · 6장 「실행 구조와 성능」의 재현성 문장 |
| 하지 말 것 | 🔴 분포에 맞춰 임계값을 긋지 마세요 · 🔴 QWK·MAE 를 다른 방법으로 낼 수 있다고 적지 마세요 — 사람 라벨이 없으면 정의되지 않습니다 |
- 관련: 3장 「검증 기준」 · 8장 KPI · 6장 재현성 문장 · 같은 구역 2번(지도자 섭외) · 7번(fps 37%) · 22번(타당성) · 31번(산출물 이름)
- 담당: 박민호(제안서 검증 기준) · 제기: 정상호 · 기한:
스프린트 2 리뷰 전 (09-14)결정함
✅ 결정 (2026.09.11, 박민호)
권고안 그대로 갑니다. 셋 다 예입니다.
- 3장 검증 기준 표 변경 — QWK·MAE는 지우지 않고 「골든셋 확보 시」 조건부로 낮춤. 재현성·fps 불변성·지표 타당성을 사람 라벨 없이도 잴 수 있는 1차 지표로 전면에 둠. 반영:
03-서비스제안.markdown「검증 기준」 표 - 8장 KPI 표에 셋 추가 — 채점 재현성·fps 불변성·지표 타당성 세 행을
08-테스트및검증계획.markdownKPI 표에 넣음(목표치 미정인 둘은[TBD]로 남김) - 임계값 출처 조사를 정상호 님 일감으로 — 스프린트 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개는 인스텝보다 근거가 한 칸 더 얕은 상태로 올라갑니다.
🔴 정정 둘을 드러냅니다.
- 2026.09.12 에 조사를 끝내고 정본 반영을 통째로 빠뜨렸습니다.
RESULTS.md가 「rubric_evidence.yaml에 적었다」고 써 두었는데 하나도 안 적혀 있었고 파일이 커밋조차 안 돼 있었습니다. 2026.09.14 에 실제로 반영했습니다. - 판정 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번에 냈습니다 |
제 쪽에서 막힌 것이 아닙니다. 지표는 나오고 점수도 나옵니다 — 비교할 상대와 기준이 없습니다.
판단해 주실 것
- 스프린트 3에서 적합도를 정말 할 것인가. 하려면 위 셋 중 최소 「요구 실력 구간」과 「포지션 시드」가 먼저 들어와야 하고, 그건 정어진 님 일감이 늘어난다는 뜻입니다 — 그쪽은 이미 벡터 저장·검색을 맡고 계십니다
- 아니라면 제 칸을 무엇으로 둘 것인가. 실제로 할 수 있고 값이 있는 것은 「실력 검증」 쪽입니다 — 임계값 검수(미결 2번)가 열리면 draft 루브릭 3개 승격과 미결 20·22·23번이 한꺼번에 풉니다. 🔴 그 병목도 결국 지도자 한 분 입니다
- 역할 축의 「포지션 → 지표」 매핑은 열어도 제가 혼자 못 정합니다. 지도자 검수 범위에 이것도 넣어 주시면 한 번에 받을 수 있습니다
| 확인 | 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, 박민호)
- 스프린트 3에서 적합도 판단은 뺍니다. 세 축(수준·역할·성향)이 전부 없고, 그중 둘(요구 실력 구간·포지션 시드)은 정어진 님 스키마 작업이 먼저인데 지금 범위에 없습니다. 억지로 열면 3번이 경고한 대로 모든 지원자가 같은 값으로 나오고 그게 화면에 안 보입니다 — 하지 않습니다.
- 정상호 님 스프린트 3 칸을 이렇게 바꿉니다: 「채점 시스템 신뢰성 강화 — fps 불변성 원인 규명·개선 + 임계값 출처 조사(
34번결정)」. 둘 다 지도자 없이 진행 가능하고,34번에서 막 1차 검증 지표로 올린 것들이라 지금 우선순위가 맞습니다. - 포지션→지표 매핑은 보류합니다. 지도자 검수가 열릴 때(
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.md6절 - 에이전트 몫 —
pose.detect_candidates·scripts/detect_subjects.py(51d7c75). 🔴 좌표는 정규화 0~1 이고 낸 박스가 그대로--subject-box로 돌아갑니다(변환 금지) · 프레임은--subject-at-ms와 같은 함수 (anchor_frame_for) · 순서는 넓이 내림차순 · 사람 0명은 실패가 아닙니다 · 결과는--result-json파일로 (🔴 stdout 을 긁지 않습니다,paik11번) - 🔴 「공에 가장 가까운 사람」 추천은 넣지 않았습니다 — 임팩트 뒤에는 공이 이미 떠나가 그 순간 공에 가까운 사람이 찬 사람이 아닌 경우가 흔합니다. 공 위치는 주되 자동 선택은 하지 않습니다(틀리면 사용자가 틀린 줄 모릅니다)
- 워커 배선 (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가
- DB는 같은 EC2 인스턴스에(RDS 아님,
DATABASE_URL만 바꾸면 이전 가능). 백업은 하루 1회pg_dump(같은 디스크 — 별도 반출은 비용 결정 필요). 🟡 아직 없는 것: HSTS·client_max_body_size(기본 1MB)·proxy_read_timeout(기본 60초,ho17번과 연결). 상세 절차 전부:fastapi/docs/deployment.md6·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 필드 추가 완료. 🔴 정정 (2026-09-23): 반영된 적이 없습니다 — MAX_DURATION_MS를 60→10초로 반영.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) → 박민호(제품 판단, 정본ho30번) →정어진(60초 유지라 할 일 없음 (2026-09-23) · 제기: 정어진 · 기한: 스프린트 2 안MAX_DURATION_MS)
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.md2절 - 담당: 정어진 · 제기: 박민호(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로 확인 부탁드립니다말씀하신 두 걱정 다 확인했습니다.
- 종목 코드 불일치(
footballvssoccer) — 이번엔 해당 없었습니다.coach테이블(부록 D)에 애초에 종목 컬럼이 없어서 옮길 것 자체가 없습니다. 대신 이게 새 문제입니다 — 아래 참고- 미결 10번(웹이 mock 고정) 순서 — 이미 해소돼 있어서 걸리지 않았습니다
🔴 다만 그 대신 부록 D 변경이 필요한 게 하나 나왔습니다 —
coach에 종목 컬럼이 없어market/coaches의 종목 필터를 실제 데이터로 못 채웁니다. 혼자 정하지 않고 paik 구역 14번으로 새로 올렸습니다 — 급하지 않으니 편하실 때 봐 주십시오.상세:
fastapi/docs/api-contract.md3-10절 ·fastapi/docs/backend-work-split.md「패킷 A」 · 클라이언트 반영은fastapi/docs/client-contract-changes.md19번
- 담당: 백성검 · 제기: 박민호 · 기한: 확인되는 대로
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, Sourcemain· 커밋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건이 남아 있습니다(
ho23번) - 🔴 없는 것을 지어내지 않습니다 — 전부 잘했으면 아쉬운 점을 만들지 않고, 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, 백성검).
paik7번에서 정어진이 읽는 경로(GET /videos/{id}/report)를 냈고, 그걸 받아AnalysisStage.tsx의 하드코딩REPORT상수를 걷어내고 실제 API를 부르도록 갈아 끼웠습니다 — 자세한 내용·확인 결과는paik7번에 이미 적어 뒀습니다(중복 기록하지 않습니다).grep -n 'const REPORT' www/src/components/analysis/AnalysisStage.tsx→ 0건으로 재확인했습니다.이 항목의 뒤늦은 정정 하나 — 위 표의 “확인(3번만)” 명령은 원래
grep -n 'const REPORT' ...였는데 실제 상수 선언이const REPORT = {형태라 그대로도 걸립니다(정정할 것 없음, 확인만 했습니다).🔴 1·2·3이 다 됐어도 이 체인에 물려 있던
ho28번(오버롤 등급 화면 표시)은 아직 별도로 열려 있습니다 —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주석 ·ho28번(이 항목이 풀려도 남는 것) - 담당:
정상호✅ 1조각 싣기 + 요약 문장 (2026.09.09) →정어진✅ 받는 칸·읽는 경로 (2026.09.10,jin27번) →백성검✅ 화면 배선 (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-aiGPU 인스턴스는 이미 정상호 소유로 별도 — 거긴 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건입니다 — 나머지는 저장 안 하고 나가서 정리된 것으로 보입니다 (
jin24번 ⑴ 의 형태). 기전 확인은 정어진 님 영역이라 단정하지 않습니다 - 🔴 같은 클립을 아홉 번 다시 올려 아홉 번 같은 이유로 떨어졌습니다 — 따로 올렸습니다(같은 구역 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건으로 셌습니다
세어서 나온 수는 쓰기 전에 걸린 줄을 눈으로 볼 것 — 로그 집계에서 반복될 형태입니다.