feat(photoValidation): validatePose TDD 3 케이스 (v0-plan Task 5)
src/features/photoValidation/validatePose.ts:
- 필수 6개 joint(어깨/골반/발목) 신뢰도 0.5↑ 검사
- 어깨 y 차이 30px 초과 시 기울어짐 사유 추가
- score = 100 - reasons × 25, passed = score ≥ 80
- 한국어 격조사 정확히 ('어깨가', '골반이', '발목이')
src/features/photoValidation/__tests__/validatePose.test.ts:
- 3 케이스 TDD: 정상 / 어깨 누락 / 기울어짐
- RED(Cannot find module) → GREEN(3 passed) 사이클 확인
검증:
- npx jest src/features/photoValidation: 3 passed
- npx tsc --noEmit: 무에러
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
43
src/features/photoValidation/__tests__/validatePose.test.ts
Normal file
43
src/features/photoValidation/__tests__/validatePose.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { validatePose } from '../validatePose';
|
||||
import type { PoseResult } from '../../pose';
|
||||
|
||||
describe('validatePose', () => {
|
||||
it('필수 6개 joint(어깨/골반/발목)가 모두 신뢰도 0.5↑이면 통과한다', () => {
|
||||
const pose: PoseResult = {
|
||||
left_shoulder_joint: { x: 100, y: 200, confidence: 0.9 },
|
||||
right_shoulder_joint: { x: 300, y: 200, confidence: 0.9 },
|
||||
left_hip_joint: { x: 120, y: 400, confidence: 0.85 },
|
||||
right_hip_joint: { x: 280, y: 400, confidence: 0.85 },
|
||||
left_ankle_joint: { x: 130, y: 700, confidence: 0.7 },
|
||||
right_ankle_joint: { x: 270, y: 700, confidence: 0.7 },
|
||||
};
|
||||
const result = validatePose(pose);
|
||||
expect(result.score).toBeGreaterThanOrEqual(80);
|
||||
expect(result.passed).toBe(true);
|
||||
expect(result.reasons).toEqual([]);
|
||||
});
|
||||
|
||||
it('어깨가 누락되면 실패하고 어깨 사유가 포함된다', () => {
|
||||
const pose: PoseResult = {
|
||||
left_hip_joint: { x: 120, y: 400, confidence: 0.85 },
|
||||
right_hip_joint: { x: 280, y: 400, confidence: 0.85 },
|
||||
};
|
||||
const result = validatePose(pose);
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.reasons).toContain('어깨가 보이지 않습니다');
|
||||
});
|
||||
|
||||
it('어깨 y 차이가 30px 초과하면 기울어짐 사유가 포함된다', () => {
|
||||
const pose: PoseResult = {
|
||||
left_shoulder_joint: { x: 100, y: 200, confidence: 0.9 },
|
||||
right_shoulder_joint: { x: 300, y: 250, confidence: 0.9 },
|
||||
left_hip_joint: { x: 120, y: 400, confidence: 0.85 },
|
||||
right_hip_joint: { x: 280, y: 400, confidence: 0.85 },
|
||||
left_ankle_joint: { x: 130, y: 700, confidence: 0.7 },
|
||||
right_ankle_joint: { x: 270, y: 700, confidence: 0.7 },
|
||||
};
|
||||
const result = validatePose(pose);
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.reasons).toContain('몸이 기울어져 있습니다');
|
||||
});
|
||||
});
|
||||
52
src/features/photoValidation/validatePose.ts
Normal file
52
src/features/photoValidation/validatePose.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { PoseResult, Joint } from '../pose';
|
||||
|
||||
export interface ValidationResult {
|
||||
score: number;
|
||||
passed: boolean;
|
||||
reasons: string[];
|
||||
}
|
||||
|
||||
const REQUIRED_JOINTS: readonly Joint[] = [
|
||||
'left_shoulder_joint',
|
||||
'right_shoulder_joint',
|
||||
'left_hip_joint',
|
||||
'right_hip_joint',
|
||||
'left_ankle_joint',
|
||||
'right_ankle_joint',
|
||||
] as const;
|
||||
|
||||
const MIN_CONFIDENCE = 0.5;
|
||||
const MAX_SHOULDER_TILT_PX = 30;
|
||||
const PASS_SCORE = 80;
|
||||
const REASON_PENALTY = 25;
|
||||
|
||||
function reasonForMissingJoint(joint: Joint): string {
|
||||
if (joint.includes('shoulder')) return '어깨가 보이지 않습니다';
|
||||
if (joint.includes('hip')) return '골반이 보이지 않습니다';
|
||||
if (joint.includes('ankle')) return '발목이 보이지 않습니다';
|
||||
return '관절이 보이지 않습니다';
|
||||
}
|
||||
|
||||
export function validatePose(pose: PoseResult): ValidationResult {
|
||||
const reasons: string[] = [];
|
||||
|
||||
for (const joint of REQUIRED_JOINTS) {
|
||||
const kp = pose[joint];
|
||||
if (!kp || kp.confidence < MIN_CONFIDENCE) {
|
||||
const msg = reasonForMissingJoint(joint);
|
||||
if (!reasons.includes(msg)) reasons.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
const ls = pose.left_shoulder_joint;
|
||||
const rs = pose.right_shoulder_joint;
|
||||
if (ls && rs) {
|
||||
const yDiff = Math.abs(ls.y - rs.y);
|
||||
if (yDiff > MAX_SHOULDER_TILT_PX) {
|
||||
reasons.push('몸이 기울어져 있습니다');
|
||||
}
|
||||
}
|
||||
|
||||
const score = Math.max(0, 100 - reasons.length * REASON_PENALTY);
|
||||
return { score, passed: score >= PASS_SCORE, reasons };
|
||||
}
|
||||
Reference in New Issue
Block a user