# Design Notes — Pemetaan sampleInteractiveExamData ke OpenAPI

Tanggal: 2025-11-12
Tujuan: Menyelaraskan struktur data kuis interaktif dari frontend (`sampleInteractiveExamData`) dengan kontrak backend (OpenAPI) untuk create/score/summary, agar diskusi skema dan wiring berikutnya berjalan mulus.

## Ruang Lingkup & Asumsi
- Frontend: sumber data contoh berada di `src/app/exam-session/page.tsx` (tipe soal: enhanced MCQ, video scenario, image hotspot, media gallery, puzzle, scenario).
- Backend: spesifikasi saat ini di `docs/api/openapi.yaml` mendukung penilaian sederhana; perluasan diperlukan untuk tipe interaktif.
- Kompatibilitas: halaman ringkasan (`exam-summary`) saat ini menampilkan `userAnswer` sebagai string; detail interaktif akan ditambahkan secara opsional.

## Tipe Soal & Pemetaan Payload

1) enhanced_mcq
- Struktur frontend: `{ id, content, options[], correctAnswers[], hints?, explanation?, points?, confidenceEnabled? }`
- Score payload: `{ questionId, type: "enhanced_mcq", choiceIndex, confidence? }`
- Summary payload (kompatibel): `userAnswer` sebagai teks; `details` (opsional) `{ choiceIndex, confidence }`
- Catatan: `confidence` untuk analitik, tidak memengaruhi skor kecuali aturan penilaian menggunakannya.

2) video_scenario
- Struktur frontend: `{ id, title, steps: [{ stepId, prompt, options[] }], scoringRules? }`
- Score payload: `{ questionId, type: "video_scenario", scenarioAnswers: [{ stepId, selectedOptionId }] }`
- Summary payload: `userAnswer` diringkas; `details` (opsional) `{ steps: [{ stepId, selectedOptionId, isCorrect, earnedPoints }] }`
- Catatan: skor dihitung agregat per-step mengikuti `scoringRules`.

3) image_hotspot
- Struktur frontend: `{ id, imageUrl, hotspots: [{ hotspotId, label }], scoringRules? }`
- Score payload: `{ questionId, type: "image_hotspot", selectedHotspots: [hotspotId] }`
- Summary payload: `userAnswer` diringkas; `details` (opsional) `{ selectedHotspots, correctHotspots }`
- Catatan: titik hotspot adalah ID referensial (koordinat didefinisikan di konten soal).

4) media_gallery
- Struktur frontend: `{ id, items: [{ id, type: "video|image|document", title }], trackingEnabled: true, scoringRules? }`
- Score payload: `{ questionId, type: "media_gallery", viewedItems: [itemId] }`
- Summary payload: `details` (opsional) `{ viewedItems, requiredItems?, completionRate }`
- Catatan: biasanya non-graded; dapat diberi syarat minimal keterlihatan (mis. harus melihat `document1`).

5) puzzle (matching)
- Struktur frontend: `{ id, pairs: [{ pieceId, targetId }], points?, scoringRules? }`
- Score payload: `{ questionId, type: "puzzle", matches: [{ pieceId, targetId }] }`
- Summary payload: `details` (opsional) `{ matches, correctPairs, earnedPoints }`
- Catatan: penilaian bisa per-pasangan (parsial) atau all-or-nothing.

6) scenario (single-step)
- Struktur frontend: `{ id, prompt, options[], correctOptionId, points? }`
- Score payload: `{ questionId, type: "scenario", selectedOptionId }`
- Summary payload: `details` (opsional) `{ selectedOptionId, isCorrect }`

## Proposal Perluasan OpenAPI

1) ExamSessionCreateRequest
- Tambah (opsional) `isInteractive: boolean` dan `clientContext: { device, appVersion, locale }`.
- Idempotensi create (opsional): `idempotencyKey`.

2) ExamScoreRequest
- `answers[]` menjadi union berdasarkan `type`:
```json
{
  "questionId": "string",
  "type": "enhanced_mcq | video_scenario | image_hotspot | media_gallery | puzzle | scenario",
  "choiceIndex": 0,
  "confidence": 4,
  "scenarioAnswers": [{ "stepId": "string", "selectedOptionId": "string" }],
  "selectedHotspots": ["string"],
  "viewedItems": ["string"],
  "matches": [{ "pieceId": "string", "targetId": "string" }],
  "selectedOptionId": "string"
}
```
- Wajib: `questionId`, `type`. Field lain kondisional sesuai tipe.
- Tetap dukung bentuk sederhana untuk MCQ sebagai compatibility path.

3) ExamSummaryResponse.answers[]
- Tambah `type` dan `details` (opsional) sebagai union detail per tipe:
```json
{
  "questionId": "string",
  "question": "string",
  "userAnswer": "string",
  "correctAnswer": "string",
  "isCorrect": true,
  "type": "...",
  "details": {
    "enhanced_mcq": { "choiceIndex": 0, "confidence": 4 },
    "video_scenario": { "steps": [{ "stepId": "s1", "selectedOptionId": "opt4", "isCorrect": true, "earnedPoints": 10 }] },
    "image_hotspot": { "selectedHotspots": ["hotspot1"], "correctHotspots": ["hotspot1","hotspot2"] },
    "media_gallery": { "viewedItems": ["video1","image1"], "requiredItems": ["document1"], "completionRate": 0.8 },
    "puzzle": { "matches": [{ "pieceId": "ccp", "targetId": "def1" }], "correctPairs": 4, "earnedPoints": 10 },
    "scenario": { "selectedOptionId": "option2", "isCorrect": true }
  }
}
```

## Contoh Payload Ringkas per Tipe

- enhanced_mcq:
```json
{ "questionId": "1", "type": "enhanced_mcq", "choiceIndex": 0, "confidence": 4 }
```
- video_scenario:
```json
{ "questionId": "video-scenario-1", "type": "video_scenario", "scenarioAnswers": [ { "stepId": "step1", "selectedOptionId": "opt4" }, { "stepId": "step2", "selectedOptionId": "opt4" } ] }
```
- image_hotspot:
```json
{ "questionId": "hotspot-scenario-1", "type": "image_hotspot", "selectedHotspots": ["hotspot1","hotspot2","hotspot4"] }
```
- media_gallery:
```json
{ "questionId": "media-gallery-1", "type": "media_gallery", "viewedItems": ["video1","image1","video2","image2","document1","image3"] }
```
- puzzle:
```json
{ "questionId": "3", "type": "puzzle", "matches": [ { "pieceId": "ccp", "targetId": "def1" }, { "pieceId": "haccp", "targetId": "def2" } ] }
```
- scenario:
```json
{ "questionId": "6", "type": "scenario", "selectedOptionId": "option2" }
```

## Kompatibilitas & Migrasi
- Backward: `userAnswer` tetap string untuk semua tipe; `details` opsional.
- Forward: UI interaktif dapat menampilkan rincian dari `details` jika tersedia.
- Penilaian: jika backend belum mendukung tipe interaktif, treat sebagai MCQ atau non-graded dengan zero-points.

## Rencana Wiring Frontend (Ringkas)
- Create: panggil `POST /api/exam-session` dengan `examId`, `userId` (opsional `isInteractive`, `clientContext`). Simpan `sessionId`.
- Score: bentuk `answers[]` union dari state `ExamContext`; kirim `idempotencyKey` unik.
- Summary: ambil `GET /api/exam-session/{sessionId}/summary`; render sesuai compatibility + `details` bila tersedia.

## Catatan Idempotensi
- `idempotencyKey` pada score menjamin hasil konsisten untuk payload yang sama.
- Backend menyimpan fingerprint jawaban untuk kunci yang sama dalam rentang waktu tertentu.

## Referensi
- OpenAPI: `docs/api/openapi.yaml`
- Retrospektif: `docs/retrospectives/epic-2-retrospective-2025-11-12.md`
- Frontend contoh: `src/app/exam-session/page.tsx`, `src/app/exam-summary/page.tsx`

## Diagram Pemetaan (Sederhana)

```mermaid
flowchart LR
  UI[UI Interaktif] --> EC[ExamContext State]
  EC --> MP[Mapper: build answers[]]
  MP --> ASR[ExamScoreRequest.answers[]]
  ASR --> API[POST /api/exam-session/{sessionId}/score]
```

- Sumber: `ExamContext` menyimpan jawaban interaktif per `questionId`.
- Mapper: membentuk union `answers[]` berdasarkan tipe soal.
- Payload: dikirim sebagai `ExamScoreRequest` (mode `batch`) dengan `idempotencyKey`.
- Hasil: backend menghitung skor dan menyediakan ringkasan melalui `ExamSummaryResponse`.