feat(#76): IMP-47B u11 frontend human_review surfacing (hunk-split from IMP-41)

- AiRepairStatus interface mirrors backend step20 u8 schema
- formatAiRepairHumanReviewMessage(): pure helper for the three failure axes
  (error / coverage_violated / unsupported_kind) — null on success/no-AI
- Home.tsx: toast.error(aiReviewMsg) after run completion
- FramePanel.tsx: reject-click window.confirm guard ("frame 유지 + AI 재구성")
- imp47b_human_review_toast.test.tsx: 6 vitest cases (null/false/3 axes/other)

Verification (frontend node_modules junction from main worktree):
- vitest imp47b_human_review_toast.test.tsx: 6/6 passed
- vitest full suite: 19/19 passed (imp41_application_mode 13 + u11 6, zero regression)

Hunk-split rationale:
- stash@{0} (imp47b-frontend-u11-pre-rebase, captured before IMP-41 merged)
  contained inline IMP-41 helpers alongside u11 changes
- HEAD already has IMP-41 helper-based implementation (buildBadgeTitle /
  mergeApplicationCandidates from services/applicationMode.ts, f358604)
- This commit adds ONLY the u11 surface on top of HEAD's IMP-41 baseline
- No IMP-41 hunk regression: buildBadgeTitle / mergeApplicationCandidates /
  applicationMode forwarding preserved verbatim

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 00:34:32 +09:00
parent 1186ad8ae2
commit 2ef02f5f18
4 changed files with 200 additions and 1 deletions

View File

@@ -47,6 +47,21 @@ export default function FramePanel({
return userSelection.overrides.zone_frames[targetRegion.id] || targetRegion.frame_match_strategy.frame_id;
}, [selectedZone, selectedRegion, userSelection.overrides.zone_frames]);
const handleFrameSelect = React.useCallback(
(candidate: FrameCandidate) => {
const isReject = candidate.label === "reject";
const alreadyApplied = currentFrameId === candidate.id;
if (isReject && !alreadyApplied) {
const ok = window.confirm(
`"${candidate.name}" 은 V4 reject 라벨입니다.\n선택 시 frame 은 유지되고 AI 가 콘텐츠를 frame 구조에 맞게 재구성합니다.\n계속하시겠습니까?`,
);
if (!ok) return;
}
onFrameSelect(candidate.id);
},
[currentFrameId, onFrameSelect],
);
if (!selectedZone) {
return (
<div className="h-full flex flex-col items-center justify-center bg-slate-50 p-8 text-center text-slate-400">
@@ -152,7 +167,7 @@ export default function FramePanel({
className="w-full"
>
<button
onClick={() => onFrameSelect(candidate.id)}
onClick={() => handleFrameSelect(candidate)}
draggable
onDragStart={(e) => {
e.dataTransfer.setData("frameId", candidate.id);