865 lines
27 KiB
YAML
865 lines
27 KiB
YAML
openapi: 3.0.3
|
|
info:
|
|
title: LMS-BGN API — Draft Skeleton
|
|
version: 0.1.0-draft
|
|
license:
|
|
name: MIT
|
|
url: https://opensource.org/licenses/MIT
|
|
description: Non-implementasi skeleton untuk stories Epik 1 & 2.
|
|
x-status: drafted
|
|
x-contract-principles:
|
|
align_to_frontend_models: true
|
|
sources:
|
|
- path: src/app/course/[courseId]/page.tsx
|
|
note: Course & Module fields
|
|
- path: src/app/exam-session/page.tsx
|
|
note: Exam Session UI shape
|
|
- path: src/app/exam-summary/page.tsx
|
|
note: Exam Summary UI shape
|
|
- path: src/app/exams/page.tsx
|
|
note: Exams listing UI
|
|
servers:
|
|
- url: https://api.lms-bgn.dev
|
|
description: Draft dev server
|
|
tags:
|
|
- name: Courses
|
|
description: Operasi daftar dan detail kursus
|
|
- name: Modules
|
|
description: Progres dan ringkasan modul
|
|
- name: Assignments
|
|
description: Pengumpulan tugas dan respons
|
|
- name: Exams
|
|
description: Sesi ujian, penskoran, dan ringkasan
|
|
- name: Certificates
|
|
description: Sertifikat dan pengiriman ulang email
|
|
security:
|
|
- bearerAuth: []
|
|
paths:
|
|
/api/courses:
|
|
get:
|
|
tags: [Courses]
|
|
summary: List Courses
|
|
operationId: listCourses
|
|
x-story-id: 1-1-course-catalog-read-api
|
|
parameters:
|
|
- name: page
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
default: 1
|
|
- name: pageSize
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 100
|
|
default: 20
|
|
- name: q
|
|
in: query
|
|
schema:
|
|
type: string
|
|
- name: category
|
|
in: query
|
|
schema:
|
|
type: string
|
|
- name: level
|
|
in: query
|
|
schema:
|
|
type: string
|
|
- name: sortBy
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [updatedAt, title]
|
|
- name: order
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [asc, desc]
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CourseListResponse'
|
|
example:
|
|
page: 1
|
|
pageSize: 20
|
|
total: 2
|
|
items:
|
|
- id: "c1"
|
|
title: "Basic Knife Skills"
|
|
instructor: "Chef A"
|
|
progress: 30
|
|
status: "in_progress"
|
|
color: "#FF9800"
|
|
thumbnail: "/images/knife-skills.jpg"
|
|
duration: "6h"
|
|
modules: 12
|
|
enrolled: "1.2k"
|
|
- id: "c2"
|
|
title: "Bread Baking 101"
|
|
instructor: "Chef B"
|
|
progress: 0
|
|
status: "not_started"
|
|
color: "#3F51B5"
|
|
thumbnail: "/images/bread.jpg"
|
|
duration: "4h"
|
|
modules: 8
|
|
enrolled: "860"
|
|
"400":
|
|
description: Invalid query parameters
|
|
/api/courses/{courseId}:
|
|
get:
|
|
tags: [Courses]
|
|
summary: Get Course Detail
|
|
operationId: getCourseDetail
|
|
x-story-id: 1-2-course-detail-read-api
|
|
parameters:
|
|
- name: courseId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CourseDetailResponse'
|
|
example:
|
|
id: "c1"
|
|
title: "Basic Knife Skills"
|
|
description: "Learn core knife handling techniques."
|
|
instructor: "Chef A"
|
|
duration: "6h"
|
|
totalModules: 12
|
|
completedModules: 3
|
|
progress: 25
|
|
thumbnail: "/images/knife-skills.jpg"
|
|
color: "#FF9800"
|
|
modules:
|
|
- id: "m1"
|
|
title: "Intro"
|
|
description: "Overview"
|
|
duration: "15m"
|
|
isUnlocked: true
|
|
isCompleted: false
|
|
progress: 0
|
|
type: "reading"
|
|
hasQuiz: false
|
|
hasAssignment: false
|
|
"404":
|
|
description: Not Found
|
|
/api/modules/{moduleId}/progress:
|
|
post:
|
|
tags: [Modules]
|
|
summary: Update Module Progress
|
|
operationId: updateModuleProgress
|
|
x-story-id: 1-3-module-progress-update-api
|
|
parameters:
|
|
- name: moduleId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ModuleProgressUpdateRequest'
|
|
responses:
|
|
"202":
|
|
description: Accepted
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ModuleProgressUpdateResponse'
|
|
"400":
|
|
description: Invalid request
|
|
/api/assignments/{assignmentId}/submission:
|
|
post:
|
|
tags: [Assignments]
|
|
summary: Persist Assignment Submission
|
|
operationId: persistAssignmentSubmission
|
|
x-story-id: 1-4-assignment-submission-persist
|
|
parameters:
|
|
- name: assignmentId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/AssignmentSubmissionRequest'
|
|
responses:
|
|
"201":
|
|
description: Created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/AssignmentSubmissionResponse'
|
|
"400":
|
|
description: Invalid request
|
|
/api/exam-session:
|
|
post:
|
|
tags: [Exams]
|
|
summary: Create Exam Session
|
|
operationId: createExamSession
|
|
x-story-id: 2-1-exam-session-create-api
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ExamSessionCreateRequest'
|
|
responses:
|
|
"201":
|
|
description: Created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ExamSessionCreateResponse'
|
|
example:
|
|
sessionId: "sess-001"
|
|
status: "created"
|
|
startTime: "2024-01-26T09:00:00Z"
|
|
"400":
|
|
description: Invalid request
|
|
"409":
|
|
description: Conflict (duplicate idempotency key)
|
|
"429":
|
|
description: Too Many Requests
|
|
/api/exam-session/{sessionId}/score:
|
|
post:
|
|
tags: [Exams]
|
|
summary: Score Exam Session
|
|
operationId: scoreExamSession
|
|
x-story-id: 2-2-exam-scoring-api
|
|
parameters:
|
|
- name: sessionId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ExamScoreRequest'
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ExamScoreResponse'
|
|
example:
|
|
totalScore: 85
|
|
totalQuestions: 10
|
|
correctAnswers: 8
|
|
scorePercent: 85
|
|
perQuestion:
|
|
- questionId: "q1"
|
|
isCorrect: true
|
|
earnedPoints: 10
|
|
maxPoints: 10
|
|
- questionId: "q2"
|
|
isCorrect: false
|
|
earnedPoints: 0
|
|
maxPoints: 10
|
|
"400":
|
|
description: Invalid request
|
|
"404":
|
|
description: Session not found
|
|
"409":
|
|
description: Conflict (duplicate idempotency key)
|
|
"429":
|
|
description: Too Many Requests
|
|
/api/exam-session/{sessionId}/summary:
|
|
get:
|
|
tags: [Exams]
|
|
summary: Read Exam Summary
|
|
operationId: readExamSummary
|
|
x-story-id: 2-3-exam-summary-read-api
|
|
parameters:
|
|
- name: sessionId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ExamSummaryResponse'
|
|
example:
|
|
examId: "exam-001"
|
|
examTitle: "Kuis Interaktif - Keamanan Dapur BGN"
|
|
totalQuestions: 10
|
|
correctAnswers: 8
|
|
score: 85
|
|
timeSpent: "25 menit 30 detik"
|
|
completedAt: "2024-01-26T09:30:00Z"
|
|
answers:
|
|
- questionId: "q1"
|
|
question: "Apa langkah pertama memastikan kebersihan dapur?"
|
|
userAnswer: "Mencuci tangan sebelum mulai"
|
|
correctAnswer: "Mencuci tangan sebelum mulai"
|
|
isCorrect: true
|
|
type: "enhanced_mcq"
|
|
- questionId: "q2"
|
|
question: "Pada suhu berapa ayam harus dimasak?"
|
|
userAnswer: "60°C"
|
|
correctAnswer: "75°C"
|
|
isCorrect: false
|
|
type: "enhanced_mcq"
|
|
"400":
|
|
description: Invalid request
|
|
/api/certificates/{certificateId}/resend:
|
|
post:
|
|
tags: [Certificates]
|
|
summary: Resend Certificate Email (Stub)
|
|
operationId: resendCertificateEmail
|
|
x-story-id: 2-4-certificate-resend-email-stub
|
|
parameters:
|
|
- name: certificateId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CertificateResendRequest'
|
|
responses:
|
|
"202":
|
|
description: Accepted
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CertificateResendResponse'
|
|
"400":
|
|
description: Invalid request
|
|
/api/exams:
|
|
get:
|
|
tags: [Exams]
|
|
summary: List Exams
|
|
operationId: listExams
|
|
parameters:
|
|
- name: page
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
default: 1
|
|
- name: pageSize
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 100
|
|
default: 20
|
|
- name: q
|
|
in: query
|
|
schema:
|
|
type: string
|
|
- name: status
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [upcoming, available, completed, missed]
|
|
- name: type
|
|
in: query
|
|
schema:
|
|
type: string
|
|
- name: dateFrom
|
|
in: query
|
|
schema:
|
|
type: string
|
|
format: date
|
|
- name: dateTo
|
|
in: query
|
|
schema:
|
|
type: string
|
|
format: date
|
|
responses:
|
|
"200":
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ExamListResponse'
|
|
example:
|
|
page: 1
|
|
pageSize: 20
|
|
total: 5
|
|
items:
|
|
- id: 1
|
|
title: "Kuis Interaktif - Keamanan Dapur BGN"
|
|
course: "Keamanan Pangan dan Gizi - Badan Gizi Nasional"
|
|
type: "Interactive Quiz"
|
|
date: "2024-01-26"
|
|
startTime: "09:00"
|
|
endTime: "10:00"
|
|
duration: 60
|
|
questions: 10
|
|
status: "available"
|
|
attempts: 0
|
|
maxAttempts: 3
|
|
passingScore: 70
|
|
description: "Kuis interaktif tentang keamanan pangan dan praktik dapur yang baik di lingkungan BGN"
|
|
instructions: ["Kuis ini menggunakan berbagai jenis soal interaktif", "Termasuk drag & drop, simulasi dapur, dan skenario nyata", "Baca instruksi setiap soal dengan teliti"]
|
|
isInteractive: true
|
|
- id: 2
|
|
title: "Ujian Tengah Semester - HACCP"
|
|
course: "HACCP - Hazard Analysis Critical Control Point"
|
|
type: "Midterm Exam"
|
|
date: "2024-01-28"
|
|
startTime: "10:00"
|
|
endTime: "12:00"
|
|
duration: 120
|
|
questions: 50
|
|
status: "upcoming"
|
|
attempts: 0
|
|
maxAttempts: 1
|
|
passingScore: 75
|
|
description: "Ujian tengah semester mencakup materi HACCP dari modul 1-6"
|
|
instructions: ["Pastikan koneksi internet stabil", "Siapkan alat tulis", "Baca soal dengan teliti"]
|
|
"400":
|
|
description: Invalid query parameters
|
|
components:
|
|
securitySchemes:
|
|
bearerAuth:
|
|
type: http
|
|
scheme: bearer
|
|
bearerFormat: JWT
|
|
schemas:
|
|
CourseSummary:
|
|
type: object
|
|
properties:
|
|
id: { type: string }
|
|
title: { type: string }
|
|
instructor: { type: string }
|
|
progress: { type: integer }
|
|
status: { type: string }
|
|
color: { type: string }
|
|
thumbnail: { type: string }
|
|
duration: { type: string }
|
|
modules: { type: integer }
|
|
enrolled: { type: string }
|
|
CourseListResponse:
|
|
type: object
|
|
properties:
|
|
page: { type: integer }
|
|
pageSize: { type: integer }
|
|
total: { type: integer }
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/CourseSummary'
|
|
ModuleSummary:
|
|
type: object
|
|
properties:
|
|
id: { type: string }
|
|
title: { type: string }
|
|
description: { type: string }
|
|
duration: { type: string }
|
|
isUnlocked: { type: boolean }
|
|
isCompleted: { type: boolean }
|
|
progress: { type: integer }
|
|
type: { type: string, enum: [video, reading, quiz, assignment] }
|
|
hasQuiz: { type: boolean }
|
|
hasAssignment: { type: boolean }
|
|
|
|
CourseDetailResponse:
|
|
type: object
|
|
properties:
|
|
id: { type: string }
|
|
title: { type: string }
|
|
description: { type: string }
|
|
instructor: { type: string }
|
|
duration: { type: string }
|
|
totalModules: { type: integer }
|
|
completedModules: { type: integer }
|
|
progress: { type: integer }
|
|
thumbnail: { type: string }
|
|
color: { type: string }
|
|
modules:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/ModuleSummary'
|
|
ModuleProgressUpdateRequest:
|
|
type: object
|
|
properties:
|
|
progressPercent: { type: integer, minimum: 0, maximum: 100 }
|
|
idempotencyKey: { type: string }
|
|
updatedAt: { type: string, format: date-time }
|
|
ModuleProgressUpdateResponse:
|
|
type: object
|
|
properties:
|
|
status: { type: string, enum: [accepted] }
|
|
AssignmentSubmissionRequest:
|
|
type: object
|
|
properties:
|
|
content: { type: string }
|
|
attachments:
|
|
type: array
|
|
items:
|
|
type: string
|
|
idempotencyKey: { type: string }
|
|
AssignmentSubmissionResponse:
|
|
type: object
|
|
properties:
|
|
submissionId: { type: string }
|
|
status: { type: string, enum: [created] }
|
|
ExamSessionCreateRequest:
|
|
type: object
|
|
properties:
|
|
examId: { type: string }
|
|
userId: { type: string }
|
|
isInteractive: { type: boolean }
|
|
idempotencyKey: { type: string }
|
|
clientContext:
|
|
type: object
|
|
properties:
|
|
device: { type: string }
|
|
appVersion: { type: string }
|
|
locale: { type: string }
|
|
ExamSessionCreateResponse:
|
|
type: object
|
|
properties:
|
|
sessionId: { type: string }
|
|
status: { type: string, enum: [created] }
|
|
startTime: { type: string, format: date-time }
|
|
ExamScoreRequest:
|
|
type: object
|
|
properties:
|
|
mode: { type: string, enum: [batch, stream] }
|
|
idempotencyKey: { type: string }
|
|
answers:
|
|
type: array
|
|
items:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/EnhancedMcqAnswer'
|
|
- $ref: '#/components/schemas/VideoScenarioAnswer'
|
|
- $ref: '#/components/schemas/ImageHotspotAnswer'
|
|
- $ref: '#/components/schemas/MediaGalleryAnswer'
|
|
- $ref: '#/components/schemas/PuzzleAnswer'
|
|
- $ref: '#/components/schemas/ScenarioAnswer'
|
|
example:
|
|
mode: batch
|
|
idempotencyKey: score-sess-001
|
|
answers:
|
|
- questionId: mcq-1
|
|
type: enhanced_mcq
|
|
choiceIndex: 2
|
|
confidence: 4
|
|
- questionId: video-1
|
|
type: video_scenario
|
|
scenarioAnswers:
|
|
- stepId: s1
|
|
selectedOptionId: optA
|
|
- stepId: s2
|
|
selectedOptionId: optB
|
|
- questionId: hotspot-1
|
|
type: image_hotspot
|
|
selectedHotspots: [spot1, spot3]
|
|
- questionId: gallery-1
|
|
type: media_gallery
|
|
viewedItems: [itemA, itemB, itemC]
|
|
- questionId: puzzle-1
|
|
type: puzzle
|
|
matches:
|
|
- pieceId: p1
|
|
targetId: t1
|
|
- pieceId: p2
|
|
targetId: t2
|
|
- questionId: scenario-1
|
|
type: scenario
|
|
selectedOptionId: optX
|
|
ExamScoreResponse:
|
|
type: object
|
|
properties:
|
|
totalScore: { type: number }
|
|
totalQuestions: { type: integer }
|
|
correctAnswers: { type: integer }
|
|
scorePercent: { type: number }
|
|
perQuestion:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
questionId: { type: string }
|
|
isCorrect: { type: boolean }
|
|
earnedPoints: { type: number }
|
|
maxPoints: { type: number }
|
|
|
|
# Interactive Answer union items
|
|
EnhancedMcqAnswer:
|
|
type: object
|
|
required: [questionId, type, choiceIndex]
|
|
properties:
|
|
questionId: { type: string }
|
|
type: { type: string, enum: [enhanced_mcq] }
|
|
choiceIndex: { type: integer }
|
|
confidence: { type: integer, minimum: 0, maximum: 5 }
|
|
|
|
VideoScenarioAnswer:
|
|
type: object
|
|
required: [questionId, type, scenarioAnswers]
|
|
properties:
|
|
questionId: { type: string }
|
|
type: { type: string, enum: [video_scenario] }
|
|
scenarioAnswers:
|
|
type: array
|
|
items:
|
|
type: object
|
|
required: [stepId, selectedOptionId]
|
|
properties:
|
|
stepId: { type: string }
|
|
selectedOptionId: { type: string }
|
|
|
|
ImageHotspotAnswer:
|
|
type: object
|
|
required: [questionId, type, selectedHotspots]
|
|
properties:
|
|
questionId: { type: string }
|
|
type: { type: string, enum: [image_hotspot] }
|
|
selectedHotspots:
|
|
type: array
|
|
items: { type: string }
|
|
|
|
MediaGalleryAnswer:
|
|
type: object
|
|
required: [questionId, type, viewedItems]
|
|
properties:
|
|
questionId: { type: string }
|
|
type: { type: string, enum: [media_gallery] }
|
|
viewedItems:
|
|
type: array
|
|
items: { type: string }
|
|
|
|
PuzzleAnswer:
|
|
type: object
|
|
required: [questionId, type, matches]
|
|
properties:
|
|
questionId: { type: string }
|
|
type: { type: string, enum: [puzzle] }
|
|
matches:
|
|
type: array
|
|
items:
|
|
type: object
|
|
required: [pieceId, targetId]
|
|
properties:
|
|
pieceId: { type: string }
|
|
targetId: { type: string }
|
|
|
|
ScenarioAnswer:
|
|
type: object
|
|
required: [questionId, type, selectedOptionId]
|
|
properties:
|
|
questionId: { type: string }
|
|
type: { type: string, enum: [scenario] }
|
|
selectedOptionId: { type: string }
|
|
|
|
# Interactive Summary details union items
|
|
EnhancedMcqDetails:
|
|
type: object
|
|
properties:
|
|
choiceIndex: { type: integer }
|
|
confidence: { type: integer, minimum: 0, maximum: 5 }
|
|
|
|
VideoScenarioDetails:
|
|
type: object
|
|
properties:
|
|
steps:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
stepId: { type: string }
|
|
selectedOptionId: { type: string }
|
|
isCorrect: { type: boolean }
|
|
earnedPoints: { type: number }
|
|
|
|
ImageHotspotDetails:
|
|
type: object
|
|
properties:
|
|
selectedHotspots:
|
|
type: array
|
|
items: { type: string }
|
|
correctHotspots:
|
|
type: array
|
|
items: { type: string }
|
|
|
|
MediaGalleryDetails:
|
|
type: object
|
|
properties:
|
|
viewedItems:
|
|
type: array
|
|
items: { type: string }
|
|
requiredItems:
|
|
type: array
|
|
items: { type: string }
|
|
completionRate: { type: number }
|
|
|
|
PuzzleDetails:
|
|
type: object
|
|
properties:
|
|
matches:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
pieceId: { type: string }
|
|
targetId: { type: string }
|
|
correctPairs: { type: integer }
|
|
earnedPoints: { type: number }
|
|
|
|
ScenarioDetails:
|
|
type: object
|
|
properties:
|
|
selectedOptionId: { type: string }
|
|
isCorrect: { type: boolean }
|
|
ExamSummaryResponse:
|
|
type: object
|
|
properties:
|
|
examId: { type: string }
|
|
examTitle: { type: string }
|
|
totalQuestions: { type: integer }
|
|
correctAnswers: { type: integer }
|
|
score: { type: number }
|
|
timeSpent: { type: string }
|
|
completedAt: { type: string, format: date-time }
|
|
answers:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
questionId: { type: string }
|
|
question: { type: string }
|
|
userAnswer: { type: string }
|
|
correctAnswer: { type: string }
|
|
isCorrect: { type: boolean }
|
|
type: { type: string, enum: [enhanced_mcq, video_scenario, image_hotspot, media_gallery, puzzle, scenario] }
|
|
details:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/EnhancedMcqDetails'
|
|
- $ref: '#/components/schemas/VideoScenarioDetails'
|
|
- $ref: '#/components/schemas/ImageHotspotDetails'
|
|
- $ref: '#/components/schemas/MediaGalleryDetails'
|
|
- $ref: '#/components/schemas/PuzzleDetails'
|
|
- $ref: '#/components/schemas/ScenarioDetails'
|
|
example:
|
|
examId: interactive-dapur-mbg-001
|
|
examTitle: Kuis Interaktif Dapur MBG
|
|
totalQuestions: 10
|
|
correctAnswers: 8
|
|
score: 85
|
|
timeSpent: "25 menit 30 detik"
|
|
completedAt: "2025-11-12T08:30:00Z"
|
|
answers:
|
|
- questionId: "1"
|
|
question: "Apa langkah pertama dalam memastikan keamanan pangan di Dapur MBG?"
|
|
userAnswer: "Melakukan analisis bahaya"
|
|
correctAnswer: "Melakukan analisis bahaya"
|
|
isCorrect: true
|
|
type: enhanced_mcq
|
|
details:
|
|
choiceIndex: 0
|
|
confidence: 4
|
|
- questionId: "video-scenario-1"
|
|
question: "Analisis Situasi Dapur: Identifikasi Masalah Keamanan Pangan"
|
|
userAnswer: "step1: opt4; step2: opt4"
|
|
correctAnswer: "step1: opt4; step2: opt4"
|
|
isCorrect: true
|
|
type: video_scenario
|
|
details:
|
|
steps:
|
|
- stepId: step1
|
|
selectedOptionId: opt4
|
|
isCorrect: true
|
|
earnedPoints: 10
|
|
- stepId: step2
|
|
selectedOptionId: opt4
|
|
isCorrect: true
|
|
earnedPoints: 10
|
|
- questionId: "hotspot-scenario-1"
|
|
question: "Identifikasi Area Bermasalah dalam Tata Letak Dapur"
|
|
userAnswer: "hotspot1,hotspot2,hotspot4"
|
|
correctAnswer: "hotspot1,hotspot2,hotspot3,hotspot4,hotspot5"
|
|
isCorrect: false
|
|
type: image_hotspot
|
|
details:
|
|
selectedHotspots: [hotspot1, hotspot2, hotspot4]
|
|
correctHotspots: [hotspot1, hotspot2, hotspot3, hotspot4, hotspot5]
|
|
- questionId: "3"
|
|
question: "Cocokkan istilah keamanan pangan dengan definisinya"
|
|
userAnswer: "ccp->def1; haccp->def2; sanitasi->def3; kontaminasi->def4"
|
|
correctAnswer: "ccp->def1; haccp->def2; sanitasi->def3; kontaminasi->def4"
|
|
isCorrect: true
|
|
type: puzzle
|
|
details:
|
|
matches:
|
|
- pieceId: ccp
|
|
targetId: def1
|
|
- pieceId: haccp
|
|
targetId: def2
|
|
correctPairs: 4
|
|
earnedPoints: 10
|
|
CertificateResendRequest:
|
|
type: object
|
|
properties:
|
|
recipientEmail: { type: string, format: email }
|
|
idempotencyKey: { type: string }
|
|
CertificateResendResponse:
|
|
type: object
|
|
properties:
|
|
jobId: { type: string }
|
|
status: { type: string, enum: [accepted, throttled] }
|
|
ExamListItem:
|
|
type: object
|
|
properties:
|
|
id: { type: integer }
|
|
title: { type: string }
|
|
course: { type: string }
|
|
type: { type: string }
|
|
date: { type: string, format: date }
|
|
startTime: { type: string }
|
|
endTime: { type: string }
|
|
duration: { type: integer }
|
|
questions: { type: integer }
|
|
status: { type: string, enum: [upcoming, available, completed, missed] }
|
|
attempts: { type: integer }
|
|
maxAttempts: { type: integer }
|
|
passingScore: { type: integer }
|
|
description: { type: string }
|
|
instructions:
|
|
type: array
|
|
items: { type: string }
|
|
isInteractive: { type: boolean }
|
|
score: { type: integer }
|
|
grade: { type: string }
|
|
ExamListResponse:
|
|
type: object
|
|
properties:
|
|
page: { type: integer }
|
|
pageSize: { type: integer }
|
|
total: { type: integer }
|
|
items:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/ExamListItem' |