@tipp/ui 2.9.0 → 2.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/index.js +17 -17
- package/dist/app/platform/edit-coaching-time.js +17 -17
- package/dist/app/platform/edit-service-type.js +17 -17
- package/dist/app/platform/goal-manage-card-edit.js +17 -17
- package/dist/app/platform/on-offline-radio-card.js +17 -17
- package/dist/app/platform/reservation-card.js +17 -17
- package/dist/app/platform/session-card.js +17 -17
- package/dist/app/roleplay/chart/session-chart.cjs +351 -0
- package/dist/app/roleplay/chart/session-chart.cjs.map +1 -0
- package/dist/app/roleplay/chart/session-chart.d.cts +16 -0
- package/dist/app/roleplay/chart/session-chart.d.ts +16 -0
- package/dist/app/roleplay/chart/session-chart.js +14 -0
- package/dist/app/roleplay/chart/session-chart.js.map +1 -0
- package/dist/app/roleplay/index.cjs +5715 -0
- package/dist/app/roleplay/index.cjs.map +1 -0
- package/dist/app/roleplay/index.d.cts +4 -0
- package/dist/app/roleplay/index.d.ts +4 -0
- package/dist/app/roleplay/index.js +182 -0
- package/dist/app/roleplay/index.js.map +1 -0
- package/dist/app/roleplay/parts/analysis-section.cjs +2178 -0
- package/dist/app/roleplay/parts/analysis-section.cjs.map +1 -0
- package/dist/app/roleplay/parts/analysis-section.d.cts +17 -0
- package/dist/app/roleplay/parts/analysis-section.d.ts +17 -0
- package/dist/app/roleplay/parts/analysis-section.js +97 -0
- package/dist/app/roleplay/parts/analysis-section.js.map +1 -0
- package/dist/app/roleplay/parts/coaching-panel.cjs +2191 -0
- package/dist/app/roleplay/parts/coaching-panel.cjs.map +1 -0
- package/dist/app/roleplay/parts/coaching-panel.d.cts +8 -0
- package/dist/app/roleplay/parts/coaching-panel.d.ts +8 -0
- package/dist/app/roleplay/parts/coaching-panel.js +95 -0
- package/dist/app/roleplay/parts/coaching-panel.js.map +1 -0
- package/dist/app/roleplay/parts/content.cjs +4687 -0
- package/dist/app/roleplay/parts/content.cjs.map +1 -0
- package/dist/app/roleplay/parts/content.d.cts +15 -0
- package/dist/app/roleplay/parts/content.d.ts +15 -0
- package/dist/app/roleplay/parts/content.js +174 -0
- package/dist/app/roleplay/parts/content.js.map +1 -0
- package/dist/app/roleplay/parts/header.cjs +2300 -0
- package/dist/app/roleplay/parts/header.cjs.map +1 -0
- package/dist/app/roleplay/parts/header.d.cts +13 -0
- package/dist/app/roleplay/parts/header.d.ts +13 -0
- package/dist/app/roleplay/parts/header.js +96 -0
- package/dist/app/roleplay/parts/header.js.map +1 -0
- package/dist/app/roleplay/parts/score-summary.cjs +2276 -0
- package/dist/app/roleplay/parts/score-summary.cjs.map +1 -0
- package/dist/app/roleplay/parts/score-summary.d.cts +12 -0
- package/dist/app/roleplay/parts/score-summary.d.ts +12 -0
- package/dist/app/roleplay/parts/score-summary.js +96 -0
- package/dist/app/roleplay/parts/score-summary.js.map +1 -0
- package/dist/app/roleplay/parts/score-trend-section.cjs +3687 -0
- package/dist/app/roleplay/parts/score-trend-section.cjs.map +1 -0
- package/dist/app/roleplay/parts/score-trend-section.d.cts +14 -0
- package/dist/app/roleplay/parts/score-trend-section.d.ts +14 -0
- package/dist/app/roleplay/parts/score-trend-section.js +167 -0
- package/dist/app/roleplay/parts/score-trend-section.js.map +1 -0
- package/dist/app/roleplay/parts/script-transcript.cjs +2288 -0
- package/dist/app/roleplay/parts/script-transcript.cjs.map +1 -0
- package/dist/app/roleplay/parts/script-transcript.d.cts +15 -0
- package/dist/app/roleplay/parts/script-transcript.d.ts +15 -0
- package/dist/app/roleplay/parts/script-transcript.js +96 -0
- package/dist/app/roleplay/parts/script-transcript.js.map +1 -0
- package/dist/app/roleplay/parts/section-nav.cjs +2433 -0
- package/dist/app/roleplay/parts/section-nav.cjs.map +1 -0
- package/dist/app/roleplay/parts/section-nav.d.cts +15 -0
- package/dist/app/roleplay/parts/section-nav.d.ts +15 -0
- package/dist/app/roleplay/parts/section-nav.js +96 -0
- package/dist/app/roleplay/parts/section-nav.js.map +1 -0
- package/dist/app/roleplay/parts/session-replay-dialog.cjs +2598 -0
- package/dist/app/roleplay/parts/session-replay-dialog.cjs.map +1 -0
- package/dist/app/roleplay/parts/session-replay-dialog.d.cts +14 -0
- package/dist/app/roleplay/parts/session-replay-dialog.d.ts +14 -0
- package/dist/app/roleplay/parts/session-replay-dialog.js +97 -0
- package/dist/app/roleplay/parts/session-replay-dialog.js.map +1 -0
- package/dist/app/roleplay/parts/step-detail.cjs +2311 -0
- package/dist/app/roleplay/parts/step-detail.cjs.map +1 -0
- package/dist/app/roleplay/parts/step-detail.d.cts +9 -0
- package/dist/app/roleplay/parts/step-detail.d.ts +9 -0
- package/dist/app/roleplay/parts/step-detail.js +96 -0
- package/dist/app/roleplay/parts/step-detail.js.map +1 -0
- package/dist/app/roleplay/parts/step-radar.cjs +463 -0
- package/dist/app/roleplay/parts/step-radar.cjs.map +1 -0
- package/dist/app/roleplay/parts/step-radar.d.cts +8 -0
- package/dist/app/roleplay/parts/step-radar.d.ts +8 -0
- package/dist/app/roleplay/parts/step-radar.js +14 -0
- package/dist/app/roleplay/parts/step-radar.js.map +1 -0
- package/dist/app/roleplay/parts/step-summary.cjs +2651 -0
- package/dist/app/roleplay/parts/step-summary.cjs.map +1 -0
- package/dist/app/roleplay/parts/step-summary.d.cts +8 -0
- package/dist/app/roleplay/parts/step-summary.d.ts +8 -0
- package/dist/app/roleplay/parts/step-summary.js +101 -0
- package/dist/app/roleplay/parts/step-summary.js.map +1 -0
- package/dist/app/roleplay/rp-session-detail.cjs +5712 -0
- package/dist/app/roleplay/rp-session-detail.cjs.map +1 -0
- package/dist/app/roleplay/rp-session-detail.d.cts +17 -0
- package/dist/app/roleplay/rp-session-detail.d.ts +17 -0
- package/dist/app/roleplay/rp-session-detail.js +179 -0
- package/dist/app/roleplay/rp-session-detail.js.map +1 -0
- package/dist/app/roleplay/types.cjs +19 -0
- package/dist/app/roleplay/types.cjs.map +1 -0
- package/dist/app/roleplay/types.d.cts +42 -0
- package/dist/app/roleplay/types.d.ts +42 -0
- package/dist/app/roleplay/types.js +2 -0
- package/dist/app/roleplay/types.js.map +1 -0
- package/dist/app/roleplay/utils/report-parse.cjs +285 -0
- package/dist/app/roleplay/utils/report-parse.cjs.map +1 -0
- package/dist/app/roleplay/utils/report-parse.d.cts +57 -0
- package/dist/app/roleplay/utils/report-parse.d.ts +57 -0
- package/dist/app/roleplay/utils/report-parse.js +8 -0
- package/dist/app/roleplay/utils/report-parse.js.map +1 -0
- package/dist/app/roleplay/utils/report-parse.test.cjs +491 -0
- package/dist/app/roleplay/utils/report-parse.test.cjs.map +1 -0
- package/dist/app/roleplay/utils/report-parse.test.d.cts +2 -0
- package/dist/app/roleplay/utils/report-parse.test.d.ts +2 -0
- package/dist/app/roleplay/utils/report-parse.test.js +237 -0
- package/dist/app/roleplay/utils/report-parse.test.js.map +1 -0
- package/dist/app/roleplay/utils/session-utils.cjs +204 -0
- package/dist/app/roleplay/utils/session-utils.cjs.map +1 -0
- package/dist/app/roleplay/utils/session-utils.d.cts +38 -0
- package/dist/app/roleplay/utils/session-utils.d.ts +38 -0
- package/dist/app/roleplay/utils/session-utils.js +46 -0
- package/dist/app/roleplay/utils/session-utils.js.map +1 -0
- package/dist/chunk-32WIS4L7.js +131 -0
- package/dist/chunk-32WIS4L7.js.map +1 -0
- package/dist/chunk-3UGBMKSQ.js +166 -0
- package/dist/chunk-3UGBMKSQ.js.map +1 -0
- package/dist/chunk-4YAA25AV.js +161 -0
- package/dist/chunk-4YAA25AV.js.map +1 -0
- package/dist/chunk-5QIT4URV.js +87 -0
- package/dist/chunk-5QIT4URV.js.map +1 -0
- package/dist/chunk-6YIKOYBQ.js +185 -0
- package/dist/chunk-6YIKOYBQ.js.map +1 -0
- package/dist/chunk-7MXY3D3Z.js +84 -0
- package/dist/chunk-7MXY3D3Z.js.map +1 -0
- package/dist/chunk-EBFQFOUQ.js +138 -0
- package/dist/chunk-EBFQFOUQ.js.map +1 -0
- package/dist/chunk-EHDUKUML.js +183 -0
- package/dist/chunk-EHDUKUML.js.map +1 -0
- package/dist/{chunk-NEJCZABQ.js → chunk-FJEDSUGU.js} +7 -7
- package/dist/chunk-GA6W62DO.js +101 -0
- package/dist/chunk-GA6W62DO.js.map +1 -0
- package/dist/chunk-KJPBBBVK.js +142 -0
- package/dist/chunk-KJPBBBVK.js.map +1 -0
- package/dist/{chunk-TQTCT7DC.js → chunk-LRNO4W2P.js} +5 -5
- package/dist/{chunk-6DWUGXSY.js → chunk-N242VJOP.js} +2 -2
- package/dist/chunk-NKW6BO5R.js +264 -0
- package/dist/chunk-NKW6BO5R.js.map +1 -0
- package/dist/{chunk-R5UDNKV2.js → chunk-O332K3SW.js} +2 -2
- package/dist/chunk-OUP3VFXG.js +1 -0
- package/dist/chunk-OUP3VFXG.js.map +1 -0
- package/dist/chunk-QMAINJ4G.js +190 -0
- package/dist/chunk-QMAINJ4G.js.map +1 -0
- package/dist/{chunk-KLJVIEFW.js → chunk-SOKGYD5B.js} +7 -7
- package/dist/chunk-TFJJKM3W.js +261 -0
- package/dist/chunk-TFJJKM3W.js.map +1 -0
- package/dist/{chunk-KQ3ME6XY.js → chunk-U2EAXN3Y.js} +4 -4
- package/dist/chunk-UXAEOCOH.js +150 -0
- package/dist/chunk-UXAEOCOH.js.map +1 -0
- package/dist/chunk-YHP75Y5K.js +242 -0
- package/dist/chunk-YHP75Y5K.js.map +1 -0
- package/dist/chunk-ZE36DFOC.js +142 -0
- package/dist/chunk-ZE36DFOC.js.map +1 -0
- package/dist/index.css +76 -0
- package/dist/index.js +32 -32
- package/dist/molecules/curriculumV2/CurriculumContents/CurriculumReviewSteps/ReviewQuestion/index.js +5 -5
- package/dist/molecules/curriculumV2/CurriculumContents/CurriculumReviewSteps/ReviewQuestion/review-question-body.js +2 -2
- package/dist/molecules/curriculumV2/CurriculumContents/CurriculumReviewSteps/index.js +7 -7
- package/dist/molecules/curriculumV2/CurriculumContents/curriculum-review.js +8 -8
- package/dist/molecules/curriculumV2/CurriculumContents/index.js +9 -9
- package/dist/molecules/curriculumV2/index.js +11 -11
- package/dist/molecules/dynamic-form-editor.js +2 -2
- package/dist/molecules/index.js +29 -29
- package/package.json +28 -2
- package/src/app/roleplay/chart/session-chart.tsx +185 -0
- package/src/app/roleplay/index.ts +8 -0
- package/src/app/roleplay/parts/analysis-section.tsx +98 -0
- package/src/app/roleplay/parts/coaching-panel.tsx +101 -0
- package/src/app/roleplay/parts/content.tsx +146 -0
- package/src/app/roleplay/parts/header.tsx +168 -0
- package/src/app/roleplay/parts/score-summary.tsx +158 -0
- package/src/app/roleplay/parts/score-trend-section.tsx +265 -0
- package/src/app/roleplay/parts/script-transcript.tsx +190 -0
- package/src/app/roleplay/parts/section-nav.tsx +299 -0
- package/src/app/roleplay/parts/session-replay-dialog.tsx +168 -0
- package/src/app/roleplay/parts/step-detail.tsx +173 -0
- package/src/app/roleplay/parts/step-radar.tsx +80 -0
- package/src/app/roleplay/parts/step-summary.tsx +149 -0
- package/src/app/roleplay/rp-session-detail.tsx +176 -0
- package/src/app/roleplay/types.ts +61 -0
- package/src/app/roleplay/utils/report-parse.test.ts +268 -0
- package/src/app/roleplay/utils/report-parse.ts +443 -0
- package/src/app/roleplay/utils/session-utils.ts +215 -0
- package/src/constrant/trainingLog.dummy.json +1 -0
- /package/dist/{chunk-NEJCZABQ.js.map → chunk-FJEDSUGU.js.map} +0 -0
- /package/dist/{chunk-TQTCT7DC.js.map → chunk-LRNO4W2P.js.map} +0 -0
- /package/dist/{chunk-6DWUGXSY.js.map → chunk-N242VJOP.js.map} +0 -0
- /package/dist/{chunk-R5UDNKV2.js.map → chunk-O332K3SW.js.map} +0 -0
- /package/dist/{chunk-KLJVIEFW.js.map → chunk-SOKGYD5B.js.map} +0 -0
- /package/dist/{chunk-KQ3ME6XY.js.map → chunk-U2EAXN3Y.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/app/roleplay/utils/report-parse.ts","../../../../src/app/roleplay/utils/report-parse.test.ts"],"sourcesContent":["// 롤플레잉 평가 리포트(reportData) 제네릭 파서.\n//\n// 보고서 타입(reportType)은 계속 추가되며 그때마다 프론트를 수정할 수 없다.\n// 따라서 프레임워크명 분기 없이 reportData 의 \"키 패턴\"만으로 해석한다.\n//\n// 입력 형태(프레임워크 공통, outputSchema 실측):\n// reportData = { output_text: { todayDate, summary, {name}_items:[{...}], skills:[{...}],\n// comprehensive_analysis? }, ai_traininglog_id }\n// - {name}_items[0] 안에 모든 단계가 `${step}_score|_basis|_issue|_improve|_example|_quality` 평면 필드로 존재\n// - 단계 라벨은 guideline.value[i].title (길이 일치 가드, 없으면 키 humanize 폴백)\n\n/* ───────────────────────── 출력 타입 ───────────────────────── */\n\nconst QUALITIES = ['충족', '부분충족', '미흡'] as const;\nexport type RpQuality = (typeof QUALITIES)[number];\n\nexport interface RpReportStep {\n key: string; // 단계 prefix (예: 'own', 'nurturing_org')\n label: string; // displayMeta.steps.displayName → guideline 제목 → humanize 된 키\n initial: string | null; // displayMeta.steps.initial. 없으면 null → session-utils stepCode 휴리스틱 폴백\n description: string | null; // displayMeta.steps.description → guideline 단계 설명\n idealExample: string | null; // guideline 모범 예시(있을 때)\n score: number | null;\n maxScore: number | null; // 신버전 리포트만 제공(구버전은 null) → 있으면 score/maxScore 표시\n quality: RpQuality | null; // 표준 등급(충족/부분충족/미흡)일 때만. 색·정규화 비율 산출용\n qualityLabel: string | null; // 원문 등급 문자열 그대로(예: 'X 매우 미흡'). 표시용\n basis: string | null;\n issue: string | null;\n improve: string | null;\n example: string | null; // reportData 의 단계별 예시 스크립트\n}\n\nexport interface RpReportAnalysisItem {\n title: string;\n detail: string;\n}\n\nexport interface RpReportSkill {\n title: string;\n description: string;\n example: string;\n}\n\nexport interface RpReport {\n frameworkTitle: string | null; // displayMeta.title — 프레임워크 제목(지표와 별개). 없으면 null\n frameworkDescription: string | null; // displayMeta.description — 프레임워크 설명문. 없으면 null\n todayDate: string | null;\n totalScore: number | null;\n comment: string | null;\n deduction: { score: number; basis: string } | null;\n strengths: RpReportAnalysisItem[];\n improvements: RpReportAnalysisItem[];\n steps: RpReportStep[];\n skills: RpReportSkill[];\n}\n\n/* ───────────────────────── displayMeta (framework 표시 메타) ───────────────────────── */\n\n// framework.displayMeta = { title?, description?, steps:[{order,key,initial,displayName,maxValue,description}] }\n// title/description은 프레임워크 자체(지표와 별개). steps는 지표별 표시 메타.\n// 없거나 형식 불일치면 폴백(기존 동작).\nexport interface RpDisplayMetaStep {\n order: number;\n key: string; // = outputSchema {prefix}_score 의 prefix\n initial: string;\n displayName: string;\n maxValue: number;\n description: string;\n}\n\nexport interface RpDisplayMeta {\n title?: string;\n description?: string;\n steps: RpDisplayMetaStep[];\n}\n\n/* ───────────────────────── 원시 헬퍼 ───────────────────────── */\n\nfunction isObject(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\n// Array.isArray 는 any[] 로 좁혀 unsafe 접근을 유발한다. unknown[] 로 좁히는 가드.\nfunction isUnknownArray(v: unknown): v is unknown[] {\n return Array.isArray(v);\n}\n\nfunction asString(v: unknown): string | null {\n if (typeof v !== 'string') return null;\n const t = v.trim();\n return t.length > 0 ? t : null;\n}\n\nfunction asNumber(v: unknown): number | null {\n return typeof v === 'number' && Number.isFinite(v) ? v : null;\n}\n\nfunction asQuality(v: unknown): RpQuality | null {\n const t = asString(v);\n if (t === null) return null;\n return QUALITIES.find((q) => q === t) ?? null;\n}\n\n// guideline 의 example 은 string | string[] 모두 가능. '-' 플레이스홀더는 무시.\nfunction asGuidelineText(v: unknown): string | null {\n if (isUnknownArray(v)) {\n const parts = v\n .map(asString)\n .filter((s): s is string => s !== null && s !== '-');\n return parts.length > 0 ? parts.join('\\n') : null;\n }\n const t = asString(v);\n return t !== null && t !== '-' ? t : null;\n}\n\nfunction humanize(key: string): string {\n return key\n .split('_')\n .filter((p) => p.length > 0)\n .map((p) => p.charAt(0).toUpperCase() + p.slice(1))\n .join(' ');\n}\n\n/* ───────────────────────── 단계 추출 ───────────────────────── */\n\n// ⚠ 순서 중요: 더 긴/구체 suffix를 먼저 둬야 한다.\n// 예) 'action_max_score' 는 '_score'보다 '_max_score' 가 먼저 매칭돼야\n// prefix='action_max'(오인) 가 아니라 prefix='action', suffix='max_score' 로 잡힌다.\nconst STEP_SUFFIXES = [\n 'max_score',\n 'maxscore',\n 'max',\n 'score',\n 'basis',\n 'issue',\n 'improve',\n 'example',\n 'quality',\n] as const;\ntype StepSuffix = (typeof STEP_SUFFIXES)[number];\n\nfunction splitStepKey(\n key: string\n): { prefix: string; suffix: StepSuffix } | null {\n for (const suffix of STEP_SUFFIXES) {\n const tail = `_${suffix}`;\n if (key.endsWith(tail) && key.length > tail.length) {\n return { prefix: key.slice(0, -tail.length), suffix };\n }\n }\n return null;\n}\n\ninterface GuidelineEntry {\n title: string | null;\n description: string | null;\n example: string | null;\n}\n\n// guideline = { value: [{title, example, description}], version }. 미작성/길이불일치면 빈 배열 취급(=폴백).\nfunction readGuidelineEntries(guideline: unknown): GuidelineEntry[] {\n if (!isObject(guideline) || !isUnknownArray(guideline.value)) return [];\n const entries: GuidelineEntry[] = guideline.value.map((v) => {\n if (!isObject(v)) return { title: null, description: null, example: null };\n const title = asString(v.title);\n return {\n title: title === '-' ? null : title,\n description: asGuidelineText(v.description),\n example: asGuidelineText(v.example),\n };\n });\n // 전부 플레이스홀더면 라벨 소스로 쓰지 않는다.\n if (entries.every((e) => e.title === null)) return [];\n return entries;\n}\n\n// reportData item 객체에서 prefix→(suffix→값) 맵과 첫 등장 순서(order)를 만든다.\nfunction buildFieldsMap(itemObj: Record<string, unknown>): {\n order: string[];\n fields: Map<string, Map<StepSuffix, unknown>>;\n} {\n const order: string[] = [];\n const fields = new Map<string, Map<StepSuffix, unknown>>();\n\n for (const [key, value] of Object.entries(itemObj)) {\n const split = splitStepKey(key);\n if (!split) continue;\n if (!fields.has(split.prefix)) {\n fields.set(split.prefix, new Map());\n order.push(split.prefix);\n }\n fields.get(split.prefix)?.set(split.suffix, value);\n }\n\n return { order, fields };\n}\n\n// 단일 단계(step) 조립. 라벨/이니셜/설명/만점은 호출부(displayMeta 또는 폴백)가 결정해 주입.\nfunction buildStep(\n key: string,\n f: Map<StepSuffix, unknown>,\n meta: {\n label: string;\n initial: string | null;\n description: string | null;\n idealExample: string | null;\n maxScoreFallback: number | null; // reportData 에 max 가 없을 때 쓸 만점(displayMeta.maxValue)\n }\n): RpReportStep {\n const qualityRaw = f.get('quality');\n return {\n key,\n label: meta.label,\n initial: meta.initial,\n description: meta.description,\n idealExample: meta.idealExample,\n score: asNumber(f.get('score')),\n // reportData 의 실제 만점이 우선, 없으면 displayMeta.maxValue 폴백.\n maxScore:\n asNumber(f.get('max_score') ?? f.get('maxscore') ?? f.get('max')) ??\n meta.maxScoreFallback,\n quality: asQuality(qualityRaw),\n qualityLabel: asString(qualityRaw),\n basis: asString(f.get('basis')),\n issue: asString(f.get('issue')),\n improve: asString(f.get('improve')),\n example: asString(f.get('example')),\n };\n}\n\nfunction extractSteps(\n itemObj: Record<string, unknown>,\n guideline: unknown,\n displaySteps: RpDisplayMetaStep[]\n): RpReportStep[] {\n const { order, fields } = buildFieldsMap(itemObj);\n const entries = readGuidelineEntries(guideline);\n\n // ── displayMeta.steps 우선: order 정렬된 steps 를 key 로 reportData 필드에 매칭.\n if (displaySteps.length > 0) {\n // guideline 의 단계 설명/모범예시 보강은 길이 일치 시에만 인덱스 매핑(기존 가드와 동일 정신).\n const useGuideline = entries.length === displaySteps.length;\n return displaySteps.map((ds, i) => {\n const f = fields.get(ds.key) ?? new Map<StepSuffix, unknown>();\n const entry = useGuideline ? entries[i] : null;\n // 지표 설명: displayMeta.steps.description 우선, 없으면 guideline.\n const dsDescription =\n ds.description.trim().length > 0 ? ds.description : null;\n return buildStep(ds.key, f, {\n label: ds.displayName,\n initial: ds.initial.trim().length > 0 ? ds.initial : null,\n description: dsDescription ?? entry?.description ?? null,\n idealExample: entry?.example ?? null,\n maxScoreFallback: ds.maxValue > 0 ? ds.maxValue : null,\n });\n });\n }\n\n // ── 폴백: reportData 키 등장 순서 + guideline 길이일치 인덱스 매칭 + humanize.\n const useGuideline = entries.length === order.length;\n return order.map((prefix, i) => {\n const f = fields.get(prefix) ?? new Map<StepSuffix, unknown>();\n const entry = useGuideline ? entries[i] : null;\n return buildStep(prefix, f, {\n label: entry?.title ?? humanize(prefix),\n initial: null,\n description: entry?.description ?? null,\n idealExample: entry?.example ?? null,\n maxScoreFallback: null,\n });\n });\n}\n\n// framework.displayMeta(unknown) 를 견고하게 정규화.\n// title/description(프레임워크 자체)과 steps(지표 배열)는 독립 — steps 가 비어도 title/description 은 살린다.\nfunction readDisplayMeta(raw: unknown): {\n title: string | null;\n description: string | null;\n steps: RpDisplayMetaStep[];\n} {\n if (!isObject(raw)) return { title: null, description: null, steps: [] };\n\n const steps: RpDisplayMetaStep[] = [];\n if (isUnknownArray(raw.steps)) {\n for (const s of raw.steps) {\n if (!isObject(s)) continue;\n const key = asString(s.key);\n if (key === null) continue; // key 없는 항목은 매칭 불가 → 제외\n steps.push({\n order: asNumber(s.order) ?? steps.length,\n key,\n initial: asString(s.initial) ?? '',\n displayName: asString(s.displayName) ?? humanize(key),\n maxValue: asNumber(s.maxValue) ?? 0,\n description: asString(s.description) ?? '',\n });\n }\n steps.sort((a, b) => a.order - b.order);\n }\n\n return {\n title: asString(raw.title),\n description: asString(raw.description),\n steps,\n };\n}\n\n/* ───────────────────────── 강점/개선점 (numbered-pair 평탄화) ───────────────────────── */\n\n// 객체에서 `${titleBase}${n}` / `${detailBase}${n}` 쌍을 n=1.. 순으로 수집.\nfunction flattenNumberedPairs(\n obj: Record<string, unknown>,\n titleBase: string,\n detailBase: string\n): RpReportAnalysisItem[] {\n const out: RpReportAnalysisItem[] = [];\n for (let n = 1; ; n += 1) {\n const title = asString(obj[`${titleBase}${n}`]);\n if (title === null) break;\n out.push({ title, detail: asString(obj[`${detailBase}${n}`]) ?? '' });\n }\n return out;\n}\n\n// 후보 (titleBase, detailBase) 쌍을 순서대로 시도, 첫 매치를 사용. (구·신 스키마 모두 대응)\nconst ANALYSIS_PAIR_CANDIDATES: readonly (readonly [string, string])[] = [\n ['title', 'detail'],\n ['improvement_title', 'improvement_action'],\n ['issue_title', 'issue_detail'],\n];\n\nfunction flattenAnalysis(source: unknown): RpReportAnalysisItem[] {\n if (!isUnknownArray(source)) return [];\n const out: RpReportAnalysisItem[] = [];\n for (const obj of source) {\n if (!isObject(obj)) continue;\n for (const [titleBase, detailBase] of ANALYSIS_PAIR_CANDIDATES) {\n const items = flattenNumberedPairs(obj, titleBase, detailBase);\n if (items.length > 0) {\n out.push(...items);\n break;\n }\n }\n }\n return out;\n}\n\n/* ───────────────────────── skills 평탄화 ───────────────────────── */\n\nfunction flattenSkills(source: unknown): RpReportSkill[] {\n if (!isUnknownArray(source)) return [];\n const out: RpReportSkill[] = [];\n for (const obj of source) {\n if (!isObject(obj)) continue;\n for (let n = 1; ; n += 1) {\n const title = asString(obj[`skill${n}`]);\n if (title === null) break;\n out.push({\n title,\n description: asString(obj[`description${n}`]) ?? '',\n example: asString(obj[`example${n}`]) ?? '',\n });\n }\n }\n return out;\n}\n\n/* ───────────────────────── 종합/감점 ───────────────────────── */\n\nfunction readDeduction(\n summary: Record<string, unknown> | null\n): { score: number; basis: string } | null {\n if (!summary || !isObject(summary.deduction)) return null;\n const score = asNumber(summary.deduction.score);\n const basis = asString(summary.deduction.basis);\n if (score === null || basis === null) return null;\n return { score, basis };\n}\n\n/* ───────────────────────── 메인 파서 ───────────────────────── */\n\n// airflow 가 output_text 를 dict 로 정규화해 저장하므로(ai_traininglog_request_clova.py),\n// 래퍼는 객체. 래퍼가 없으면(일부 프레임워크) reportData 자체를 루트로 사용.\nfunction unwrapOutputText(reportData: unknown): Record<string, unknown> {\n if (!isObject(reportData)) return {};\n return isObject(reportData.output_text) ? reportData.output_text : reportData;\n}\n\nfunction findItemsArray(\n root: Record<string, unknown>\n): Record<string, unknown> | null {\n for (const [key, value] of Object.entries(root)) {\n if (!key.endsWith('_items') || !isUnknownArray(value)) continue;\n const first = value[0];\n if (isObject(first)) return first;\n }\n return null;\n}\n\nexport function parseReportData(\n reportData: unknown,\n guideline: unknown,\n displayMeta?: unknown\n): RpReport {\n const root = unwrapOutputText(reportData);\n const meta = readDisplayMeta(displayMeta);\n\n const summary = isObject(root.summary) ? root.summary : null;\n const comprehensive = isObject(root.comprehensive_analysis)\n ? root.comprehensive_analysis\n : null;\n\n // 종합 코멘트: 신(comprehensive_analysis) → 구(summary) 순.\n const comment =\n asString(comprehensive?.comment) ??\n asString(summary?.comment) ??\n asString(summary?.basis);\n\n // 강점: comprehensive_analysis.strengths(신) → summary.good_point(구)\n const strengths = flattenAnalysis(\n comprehensive?.strengths ?? summary?.good_point\n );\n // 개선점: comprehensive_analysis.improvements(신) → summary.improvement(구)\n const improvements = flattenAnalysis(\n comprehensive?.improvements ?? summary?.improvement\n );\n\n const itemObj = findItemsArray(root);\n const steps = itemObj ? extractSteps(itemObj, guideline, meta.steps) : [];\n\n return {\n frameworkTitle: meta.title,\n frameworkDescription: meta.description,\n todayDate: asString(root.todayDate),\n totalScore: asNumber(summary?.total_score),\n comment,\n deduction: readDeduction(summary),\n strengths,\n improvements,\n steps,\n skills: flattenSkills(root.skills),\n };\n}\n","import { parseReportData } from './report-parse';\n\ndescribe('parseReportData', () => {\n it('신버전: output_text 래퍼 + comprehensive_analysis + {name}_items 를 파싱한다', () => {\n const reportData = {\n output_text: {\n todayDate: '2026-06-19',\n summary: {\n total_score: 82,\n deduction: { score: -3, basis: '시간 초과' },\n },\n comprehensive_analysis: {\n comment: '전반적으로 우수합니다.',\n strengths: [{ title1: '경청', detail1: '잘 들었다' }],\n improvements: [{ title1: '질문', detail1: '더 구체적으로' }],\n },\n eval_items: [\n {\n own_score: 8,\n own_max_score: 10,\n own_quality: '충족',\n own_basis: '근거',\n own_issue: '아쉬움',\n own_improve: '개선',\n own_example: '예시 스크립트',\n },\n ],\n skills: [\n { skill1: '미러링', description1: '상대 말 반영', example1: '예시' },\n ],\n },\n ai_traininglog_id: 'log-1',\n };\n const guideline = {\n value: [{ title: '자기이해', description: '설명', example: '모범' }],\n version: 1,\n };\n\n const r = parseReportData(reportData, guideline);\n\n expect(r.todayDate).toBe('2026-06-19');\n expect(r.totalScore).toBe(82);\n expect(r.comment).toBe('전반적으로 우수합니다.');\n expect(r.deduction).toEqual({ score: -3, basis: '시간 초과' });\n expect(r.strengths).toEqual([{ title: '경청', detail: '잘 들었다' }]);\n expect(r.improvements).toEqual([{ title: '질문', detail: '더 구체적으로' }]);\n expect(r.skills).toEqual([\n { title: '미러링', description: '상대 말 반영', example: '예시' },\n ]);\n\n expect(r.steps).toHaveLength(1);\n const step = r.steps[0];\n // own_max_score 가 '_score' 보다 '_max_score' 로 먼저 매칭되어야 한다.\n expect(step.key).toBe('own');\n expect(step.score).toBe(8);\n expect(step.maxScore).toBe(10);\n expect(step.quality).toBe('충족');\n expect(step.basis).toBe('근거');\n // guideline 길이가 단계 수와 일치하므로 라벨/설명/예시를 사용한다.\n expect(step.label).toBe('자기이해');\n expect(step.description).toBe('설명');\n expect(step.idealExample).toBe('모범');\n });\n\n it('구버전: summary.good_point / improvement / basis 폴백을 파싱한다', () => {\n const reportData = {\n output_text: {\n summary: {\n total_score: 70,\n basis: '구버전 코멘트',\n good_point: [{ title1: '강점A', detail1: '내용A' }],\n improvement: [{ title1: '개선A', detail1: '내용B' }],\n },\n skill_items: [{ a_score: 5, a_quality: 'X 매우 미흡' }],\n },\n };\n\n const r = parseReportData(reportData, null);\n\n expect(r.totalScore).toBe(70);\n expect(r.comment).toBe('구버전 코멘트');\n expect(r.strengths).toEqual([{ title: '강점A', detail: '내용A' }]);\n expect(r.improvements).toEqual([{ title: '개선A', detail: '내용B' }]);\n\n expect(r.steps).toHaveLength(1);\n const step = r.steps[0];\n // guideline 이 없으므로 humanize 폴백.\n expect(step.label).toBe('A');\n // 비표준 등급은 quality=null, qualityLabel 에 원문 보존.\n expect(step.quality).toBeNull();\n expect(step.qualityLabel).toBe('X 매우 미흡');\n });\n\n it('래퍼(output_text)가 없으면 reportData 자체를 루트로 사용한다', () => {\n const reportData = {\n summary: { total_score: 55 },\n eval_items: [{ x_score: 3 }],\n };\n\n const r = parseReportData(reportData, null);\n\n expect(r.totalScore).toBe(55);\n expect(r.steps).toHaveLength(1);\n expect(r.steps[0].score).toBe(3);\n });\n\n it('잘못된/빈 입력은 빈 리포트로 안전 처리한다', () => {\n const r = parseReportData(null, undefined);\n expect(r.totalScore).toBeNull();\n expect(r.comment).toBeNull();\n expect(r.steps).toEqual([]);\n expect(r.strengths).toEqual([]);\n expect(r.improvements).toEqual([]);\n expect(r.skills).toEqual([]);\n expect(r.deduction).toBeNull();\n });\n\n it('guideline 길이가 단계 수와 불일치하면 humanize 폴백한다', () => {\n const reportData = {\n output_text: {\n eval_items: [{ nurturing_org_score: 7, own_score: 9 }],\n },\n };\n // 단계는 2개(nurturing_org, own)인데 guideline 은 1개 → 불일치 → 폴백.\n const guideline = { value: [{ title: '단 하나' }], version: 1 };\n\n const r = parseReportData(reportData, guideline);\n\n expect(r.steps).toHaveLength(2);\n expect(r.steps[0].label).toBe('Nurturing Org');\n expect(r.steps[1].label).toBe('Own');\n });\n\n it('displayMeta.steps: order 정렬 + key 매칭 + initial/maxValue/title/description 적용', () => {\n const reportData = {\n output_text: {\n eval_items: [\n {\n // 등장 순서는 progress 먼저, opening 나중 — displayMeta.order 로 뒤집힌다.\n progress_score: 6,\n progress_basis: 'p근거',\n opening_score: 9,\n opening_basis: 'o근거',\n },\n ],\n },\n };\n const displayMeta = {\n title: 'OPENFANS 코칭',\n description: '단계 코칭 프레임워크',\n steps: [\n {\n order: 1,\n key: 'opening',\n initial: 'O',\n displayName: '오프닝',\n maxValue: 10,\n description: '첫 인상',\n },\n {\n order: 2,\n key: 'progress',\n initial: 'P',\n displayName: '진행',\n maxValue: 20,\n description: '',\n },\n ],\n };\n\n const r = parseReportData(reportData, null, displayMeta);\n\n expect(r.frameworkTitle).toBe('OPENFANS 코칭');\n expect(r.frameworkDescription).toBe('단계 코칭 프레임워크');\n\n expect(r.steps).toHaveLength(2);\n // order 정렬: opening(1) → progress(2). reportData 등장 순서(progress 먼저)와 무관.\n expect(r.steps[0].key).toBe('opening');\n expect(r.steps[0].label).toBe('오프닝');\n expect(r.steps[0].initial).toBe('O');\n expect(r.steps[0].score).toBe(9);\n expect(r.steps[0].basis).toBe('o근거');\n // reportData 에 max 없음 → displayMeta.maxValue 폴백.\n expect(r.steps[0].maxScore).toBe(10);\n expect(r.steps[0].description).toBe('첫 인상');\n\n expect(r.steps[1].key).toBe('progress');\n expect(r.steps[1].initial).toBe('P');\n expect(r.steps[1].maxScore).toBe(20);\n // displayMeta.steps.description 이 빈 문자열 + guideline 없음 → null.\n expect(r.steps[1].description).toBeNull();\n });\n\n it('displayMeta: reportData 의 실제 max_score 가 maxValue 보다 우선한다', () => {\n const reportData = {\n output_text: { eval_items: [{ opening_score: 8, opening_max_score: 12 }] },\n };\n const displayMeta = {\n steps: [\n {\n order: 0,\n key: 'opening',\n initial: 'O',\n displayName: '오프닝',\n maxValue: 100,\n description: '',\n },\n ],\n };\n\n const r = parseReportData(reportData, null, displayMeta);\n expect(r.steps[0].maxScore).toBe(12);\n });\n\n it('displayMeta.steps 에 있으나 reportData 에 없는 key 도 행을 유지한다(점수 null)', () => {\n const reportData = { output_text: { eval_items: [{ opening_score: 7 }] } };\n const displayMeta = {\n steps: [\n {\n order: 0,\n key: 'opening',\n initial: 'O',\n displayName: '오프닝',\n maxValue: 10,\n description: '',\n },\n {\n order: 1,\n key: 'closing',\n initial: 'C',\n displayName: '클로징',\n maxValue: 10,\n description: '',\n },\n ],\n };\n\n const r = parseReportData(reportData, null, displayMeta);\n expect(r.steps).toHaveLength(2);\n expect(r.steps[1].key).toBe('closing');\n expect(r.steps[1].score).toBeNull();\n // 데이터가 없어도 displayMeta.maxValue 폴백은 적용.\n expect(r.steps[1].maxScore).toBe(10);\n });\n\n it('displayMeta 가 없으면 frameworkTitle/Description=null, step.initial=null (폴백)', () => {\n const reportData = { output_text: { eval_items: [{ own_score: 5 }] } };\n const r = parseReportData(reportData, null);\n expect(r.frameworkTitle).toBeNull();\n expect(r.frameworkDescription).toBeNull();\n expect(r.steps[0].initial).toBeNull();\n });\n\n it('displayMeta.title/description 만 있고 steps 가 비면 텍스트만 적용하고 단계는 폴백한다', () => {\n const reportData = {\n output_text: { eval_items: [{ own_score: 5, progress_score: 6 }] },\n };\n const displayMeta = { title: '제목만', description: '설명만', steps: [] };\n\n const r = parseReportData(reportData, null, displayMeta);\n expect(r.frameworkTitle).toBe('제목만');\n expect(r.frameworkDescription).toBe('설명만');\n // steps 비었으므로 폴백(등장 순서 + humanize).\n expect(r.steps).toHaveLength(2);\n expect(r.steps[0].key).toBe('own');\n expect(r.steps[0].initial).toBeNull();\n });\n});\n"],"mappings":"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","names":["useGuideline"]}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseReportData
|
|
3
|
+
} from "../../../chunk-TFJJKM3W.js";
|
|
4
|
+
import "../../../chunk-CRTRMMJ7.js";
|
|
5
|
+
|
|
6
|
+
// src/app/roleplay/utils/report-parse.test.ts
|
|
7
|
+
describe("parseReportData", () => {
|
|
8
|
+
it("\uC2E0\uBC84\uC804: output_text \uB798\uD37C + comprehensive_analysis + {name}_items \uB97C \uD30C\uC2F1\uD55C\uB2E4", () => {
|
|
9
|
+
const reportData = {
|
|
10
|
+
output_text: {
|
|
11
|
+
todayDate: "2026-06-19",
|
|
12
|
+
summary: {
|
|
13
|
+
total_score: 82,
|
|
14
|
+
deduction: { score: -3, basis: "\uC2DC\uAC04 \uCD08\uACFC" }
|
|
15
|
+
},
|
|
16
|
+
comprehensive_analysis: {
|
|
17
|
+
comment: "\uC804\uBC18\uC801\uC73C\uB85C \uC6B0\uC218\uD569\uB2C8\uB2E4.",
|
|
18
|
+
strengths: [{ title1: "\uACBD\uCCAD", detail1: "\uC798 \uB4E4\uC5C8\uB2E4" }],
|
|
19
|
+
improvements: [{ title1: "\uC9C8\uBB38", detail1: "\uB354 \uAD6C\uCCB4\uC801\uC73C\uB85C" }]
|
|
20
|
+
},
|
|
21
|
+
eval_items: [
|
|
22
|
+
{
|
|
23
|
+
own_score: 8,
|
|
24
|
+
own_max_score: 10,
|
|
25
|
+
own_quality: "\uCDA9\uC871",
|
|
26
|
+
own_basis: "\uADFC\uAC70",
|
|
27
|
+
own_issue: "\uC544\uC26C\uC6C0",
|
|
28
|
+
own_improve: "\uAC1C\uC120",
|
|
29
|
+
own_example: "\uC608\uC2DC \uC2A4\uD06C\uB9BD\uD2B8"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
skills: [
|
|
33
|
+
{ skill1: "\uBBF8\uB7EC\uB9C1", description1: "\uC0C1\uB300 \uB9D0 \uBC18\uC601", example1: "\uC608\uC2DC" }
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
ai_traininglog_id: "log-1"
|
|
37
|
+
};
|
|
38
|
+
const guideline = {
|
|
39
|
+
value: [{ title: "\uC790\uAE30\uC774\uD574", description: "\uC124\uBA85", example: "\uBAA8\uBC94" }],
|
|
40
|
+
version: 1
|
|
41
|
+
};
|
|
42
|
+
const r = parseReportData(reportData, guideline);
|
|
43
|
+
expect(r.todayDate).toBe("2026-06-19");
|
|
44
|
+
expect(r.totalScore).toBe(82);
|
|
45
|
+
expect(r.comment).toBe("\uC804\uBC18\uC801\uC73C\uB85C \uC6B0\uC218\uD569\uB2C8\uB2E4.");
|
|
46
|
+
expect(r.deduction).toEqual({ score: -3, basis: "\uC2DC\uAC04 \uCD08\uACFC" });
|
|
47
|
+
expect(r.strengths).toEqual([{ title: "\uACBD\uCCAD", detail: "\uC798 \uB4E4\uC5C8\uB2E4" }]);
|
|
48
|
+
expect(r.improvements).toEqual([{ title: "\uC9C8\uBB38", detail: "\uB354 \uAD6C\uCCB4\uC801\uC73C\uB85C" }]);
|
|
49
|
+
expect(r.skills).toEqual([
|
|
50
|
+
{ title: "\uBBF8\uB7EC\uB9C1", description: "\uC0C1\uB300 \uB9D0 \uBC18\uC601", example: "\uC608\uC2DC" }
|
|
51
|
+
]);
|
|
52
|
+
expect(r.steps).toHaveLength(1);
|
|
53
|
+
const step = r.steps[0];
|
|
54
|
+
expect(step.key).toBe("own");
|
|
55
|
+
expect(step.score).toBe(8);
|
|
56
|
+
expect(step.maxScore).toBe(10);
|
|
57
|
+
expect(step.quality).toBe("\uCDA9\uC871");
|
|
58
|
+
expect(step.basis).toBe("\uADFC\uAC70");
|
|
59
|
+
expect(step.label).toBe("\uC790\uAE30\uC774\uD574");
|
|
60
|
+
expect(step.description).toBe("\uC124\uBA85");
|
|
61
|
+
expect(step.idealExample).toBe("\uBAA8\uBC94");
|
|
62
|
+
});
|
|
63
|
+
it("\uAD6C\uBC84\uC804: summary.good_point / improvement / basis \uD3F4\uBC31\uC744 \uD30C\uC2F1\uD55C\uB2E4", () => {
|
|
64
|
+
const reportData = {
|
|
65
|
+
output_text: {
|
|
66
|
+
summary: {
|
|
67
|
+
total_score: 70,
|
|
68
|
+
basis: "\uAD6C\uBC84\uC804 \uCF54\uBA58\uD2B8",
|
|
69
|
+
good_point: [{ title1: "\uAC15\uC810A", detail1: "\uB0B4\uC6A9A" }],
|
|
70
|
+
improvement: [{ title1: "\uAC1C\uC120A", detail1: "\uB0B4\uC6A9B" }]
|
|
71
|
+
},
|
|
72
|
+
skill_items: [{ a_score: 5, a_quality: "X \uB9E4\uC6B0 \uBBF8\uD761" }]
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const r = parseReportData(reportData, null);
|
|
76
|
+
expect(r.totalScore).toBe(70);
|
|
77
|
+
expect(r.comment).toBe("\uAD6C\uBC84\uC804 \uCF54\uBA58\uD2B8");
|
|
78
|
+
expect(r.strengths).toEqual([{ title: "\uAC15\uC810A", detail: "\uB0B4\uC6A9A" }]);
|
|
79
|
+
expect(r.improvements).toEqual([{ title: "\uAC1C\uC120A", detail: "\uB0B4\uC6A9B" }]);
|
|
80
|
+
expect(r.steps).toHaveLength(1);
|
|
81
|
+
const step = r.steps[0];
|
|
82
|
+
expect(step.label).toBe("A");
|
|
83
|
+
expect(step.quality).toBeNull();
|
|
84
|
+
expect(step.qualityLabel).toBe("X \uB9E4\uC6B0 \uBBF8\uD761");
|
|
85
|
+
});
|
|
86
|
+
it("\uB798\uD37C(output_text)\uAC00 \uC5C6\uC73C\uBA74 reportData \uC790\uCCB4\uB97C \uB8E8\uD2B8\uB85C \uC0AC\uC6A9\uD55C\uB2E4", () => {
|
|
87
|
+
const reportData = {
|
|
88
|
+
summary: { total_score: 55 },
|
|
89
|
+
eval_items: [{ x_score: 3 }]
|
|
90
|
+
};
|
|
91
|
+
const r = parseReportData(reportData, null);
|
|
92
|
+
expect(r.totalScore).toBe(55);
|
|
93
|
+
expect(r.steps).toHaveLength(1);
|
|
94
|
+
expect(r.steps[0].score).toBe(3);
|
|
95
|
+
});
|
|
96
|
+
it("\uC798\uBABB\uB41C/\uBE48 \uC785\uB825\uC740 \uBE48 \uB9AC\uD3EC\uD2B8\uB85C \uC548\uC804 \uCC98\uB9AC\uD55C\uB2E4", () => {
|
|
97
|
+
const r = parseReportData(null, void 0);
|
|
98
|
+
expect(r.totalScore).toBeNull();
|
|
99
|
+
expect(r.comment).toBeNull();
|
|
100
|
+
expect(r.steps).toEqual([]);
|
|
101
|
+
expect(r.strengths).toEqual([]);
|
|
102
|
+
expect(r.improvements).toEqual([]);
|
|
103
|
+
expect(r.skills).toEqual([]);
|
|
104
|
+
expect(r.deduction).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
it("guideline \uAE38\uC774\uAC00 \uB2E8\uACC4 \uC218\uC640 \uBD88\uC77C\uCE58\uD558\uBA74 humanize \uD3F4\uBC31\uD55C\uB2E4", () => {
|
|
107
|
+
const reportData = {
|
|
108
|
+
output_text: {
|
|
109
|
+
eval_items: [{ nurturing_org_score: 7, own_score: 9 }]
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const guideline = { value: [{ title: "\uB2E8 \uD558\uB098" }], version: 1 };
|
|
113
|
+
const r = parseReportData(reportData, guideline);
|
|
114
|
+
expect(r.steps).toHaveLength(2);
|
|
115
|
+
expect(r.steps[0].label).toBe("Nurturing Org");
|
|
116
|
+
expect(r.steps[1].label).toBe("Own");
|
|
117
|
+
});
|
|
118
|
+
it("displayMeta.steps: order \uC815\uB82C + key \uB9E4\uCE6D + initial/maxValue/title/description \uC801\uC6A9", () => {
|
|
119
|
+
const reportData = {
|
|
120
|
+
output_text: {
|
|
121
|
+
eval_items: [
|
|
122
|
+
{
|
|
123
|
+
// 등장 순서는 progress 먼저, opening 나중 — displayMeta.order 로 뒤집힌다.
|
|
124
|
+
progress_score: 6,
|
|
125
|
+
progress_basis: "p\uADFC\uAC70",
|
|
126
|
+
opening_score: 9,
|
|
127
|
+
opening_basis: "o\uADFC\uAC70"
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const displayMeta = {
|
|
133
|
+
title: "OPENFANS \uCF54\uCE6D",
|
|
134
|
+
description: "\uB2E8\uACC4 \uCF54\uCE6D \uD504\uB808\uC784\uC6CC\uD06C",
|
|
135
|
+
steps: [
|
|
136
|
+
{
|
|
137
|
+
order: 1,
|
|
138
|
+
key: "opening",
|
|
139
|
+
initial: "O",
|
|
140
|
+
displayName: "\uC624\uD504\uB2DD",
|
|
141
|
+
maxValue: 10,
|
|
142
|
+
description: "\uCCAB \uC778\uC0C1"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
order: 2,
|
|
146
|
+
key: "progress",
|
|
147
|
+
initial: "P",
|
|
148
|
+
displayName: "\uC9C4\uD589",
|
|
149
|
+
maxValue: 20,
|
|
150
|
+
description: ""
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
};
|
|
154
|
+
const r = parseReportData(reportData, null, displayMeta);
|
|
155
|
+
expect(r.frameworkTitle).toBe("OPENFANS \uCF54\uCE6D");
|
|
156
|
+
expect(r.frameworkDescription).toBe("\uB2E8\uACC4 \uCF54\uCE6D \uD504\uB808\uC784\uC6CC\uD06C");
|
|
157
|
+
expect(r.steps).toHaveLength(2);
|
|
158
|
+
expect(r.steps[0].key).toBe("opening");
|
|
159
|
+
expect(r.steps[0].label).toBe("\uC624\uD504\uB2DD");
|
|
160
|
+
expect(r.steps[0].initial).toBe("O");
|
|
161
|
+
expect(r.steps[0].score).toBe(9);
|
|
162
|
+
expect(r.steps[0].basis).toBe("o\uADFC\uAC70");
|
|
163
|
+
expect(r.steps[0].maxScore).toBe(10);
|
|
164
|
+
expect(r.steps[0].description).toBe("\uCCAB \uC778\uC0C1");
|
|
165
|
+
expect(r.steps[1].key).toBe("progress");
|
|
166
|
+
expect(r.steps[1].initial).toBe("P");
|
|
167
|
+
expect(r.steps[1].maxScore).toBe(20);
|
|
168
|
+
expect(r.steps[1].description).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
it("displayMeta: reportData \uC758 \uC2E4\uC81C max_score \uAC00 maxValue \uBCF4\uB2E4 \uC6B0\uC120\uD55C\uB2E4", () => {
|
|
171
|
+
const reportData = {
|
|
172
|
+
output_text: { eval_items: [{ opening_score: 8, opening_max_score: 12 }] }
|
|
173
|
+
};
|
|
174
|
+
const displayMeta = {
|
|
175
|
+
steps: [
|
|
176
|
+
{
|
|
177
|
+
order: 0,
|
|
178
|
+
key: "opening",
|
|
179
|
+
initial: "O",
|
|
180
|
+
displayName: "\uC624\uD504\uB2DD",
|
|
181
|
+
maxValue: 100,
|
|
182
|
+
description: ""
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
};
|
|
186
|
+
const r = parseReportData(reportData, null, displayMeta);
|
|
187
|
+
expect(r.steps[0].maxScore).toBe(12);
|
|
188
|
+
});
|
|
189
|
+
it("displayMeta.steps \uC5D0 \uC788\uC73C\uB098 reportData \uC5D0 \uC5C6\uB294 key \uB3C4 \uD589\uC744 \uC720\uC9C0\uD55C\uB2E4(\uC810\uC218 null)", () => {
|
|
190
|
+
const reportData = { output_text: { eval_items: [{ opening_score: 7 }] } };
|
|
191
|
+
const displayMeta = {
|
|
192
|
+
steps: [
|
|
193
|
+
{
|
|
194
|
+
order: 0,
|
|
195
|
+
key: "opening",
|
|
196
|
+
initial: "O",
|
|
197
|
+
displayName: "\uC624\uD504\uB2DD",
|
|
198
|
+
maxValue: 10,
|
|
199
|
+
description: ""
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
order: 1,
|
|
203
|
+
key: "closing",
|
|
204
|
+
initial: "C",
|
|
205
|
+
displayName: "\uD074\uB85C\uC9D5",
|
|
206
|
+
maxValue: 10,
|
|
207
|
+
description: ""
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
};
|
|
211
|
+
const r = parseReportData(reportData, null, displayMeta);
|
|
212
|
+
expect(r.steps).toHaveLength(2);
|
|
213
|
+
expect(r.steps[1].key).toBe("closing");
|
|
214
|
+
expect(r.steps[1].score).toBeNull();
|
|
215
|
+
expect(r.steps[1].maxScore).toBe(10);
|
|
216
|
+
});
|
|
217
|
+
it("displayMeta \uAC00 \uC5C6\uC73C\uBA74 frameworkTitle/Description=null, step.initial=null (\uD3F4\uBC31)", () => {
|
|
218
|
+
const reportData = { output_text: { eval_items: [{ own_score: 5 }] } };
|
|
219
|
+
const r = parseReportData(reportData, null);
|
|
220
|
+
expect(r.frameworkTitle).toBeNull();
|
|
221
|
+
expect(r.frameworkDescription).toBeNull();
|
|
222
|
+
expect(r.steps[0].initial).toBeNull();
|
|
223
|
+
});
|
|
224
|
+
it("displayMeta.title/description \uB9CC \uC788\uACE0 steps \uAC00 \uBE44\uBA74 \uD14D\uC2A4\uD2B8\uB9CC \uC801\uC6A9\uD558\uACE0 \uB2E8\uACC4\uB294 \uD3F4\uBC31\uD55C\uB2E4", () => {
|
|
225
|
+
const reportData = {
|
|
226
|
+
output_text: { eval_items: [{ own_score: 5, progress_score: 6 }] }
|
|
227
|
+
};
|
|
228
|
+
const displayMeta = { title: "\uC81C\uBAA9\uB9CC", description: "\uC124\uBA85\uB9CC", steps: [] };
|
|
229
|
+
const r = parseReportData(reportData, null, displayMeta);
|
|
230
|
+
expect(r.frameworkTitle).toBe("\uC81C\uBAA9\uB9CC");
|
|
231
|
+
expect(r.frameworkDescription).toBe("\uC124\uBA85\uB9CC");
|
|
232
|
+
expect(r.steps).toHaveLength(2);
|
|
233
|
+
expect(r.steps[0].key).toBe("own");
|
|
234
|
+
expect(r.steps[0].initial).toBeNull();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
//# sourceMappingURL=report-parse.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/app/roleplay/utils/report-parse.test.ts"],"sourcesContent":["import { parseReportData } from './report-parse';\n\ndescribe('parseReportData', () => {\n it('신버전: output_text 래퍼 + comprehensive_analysis + {name}_items 를 파싱한다', () => {\n const reportData = {\n output_text: {\n todayDate: '2026-06-19',\n summary: {\n total_score: 82,\n deduction: { score: -3, basis: '시간 초과' },\n },\n comprehensive_analysis: {\n comment: '전반적으로 우수합니다.',\n strengths: [{ title1: '경청', detail1: '잘 들었다' }],\n improvements: [{ title1: '질문', detail1: '더 구체적으로' }],\n },\n eval_items: [\n {\n own_score: 8,\n own_max_score: 10,\n own_quality: '충족',\n own_basis: '근거',\n own_issue: '아쉬움',\n own_improve: '개선',\n own_example: '예시 스크립트',\n },\n ],\n skills: [\n { skill1: '미러링', description1: '상대 말 반영', example1: '예시' },\n ],\n },\n ai_traininglog_id: 'log-1',\n };\n const guideline = {\n value: [{ title: '자기이해', description: '설명', example: '모범' }],\n version: 1,\n };\n\n const r = parseReportData(reportData, guideline);\n\n expect(r.todayDate).toBe('2026-06-19');\n expect(r.totalScore).toBe(82);\n expect(r.comment).toBe('전반적으로 우수합니다.');\n expect(r.deduction).toEqual({ score: -3, basis: '시간 초과' });\n expect(r.strengths).toEqual([{ title: '경청', detail: '잘 들었다' }]);\n expect(r.improvements).toEqual([{ title: '질문', detail: '더 구체적으로' }]);\n expect(r.skills).toEqual([\n { title: '미러링', description: '상대 말 반영', example: '예시' },\n ]);\n\n expect(r.steps).toHaveLength(1);\n const step = r.steps[0];\n // own_max_score 가 '_score' 보다 '_max_score' 로 먼저 매칭되어야 한다.\n expect(step.key).toBe('own');\n expect(step.score).toBe(8);\n expect(step.maxScore).toBe(10);\n expect(step.quality).toBe('충족');\n expect(step.basis).toBe('근거');\n // guideline 길이가 단계 수와 일치하므로 라벨/설명/예시를 사용한다.\n expect(step.label).toBe('자기이해');\n expect(step.description).toBe('설명');\n expect(step.idealExample).toBe('모범');\n });\n\n it('구버전: summary.good_point / improvement / basis 폴백을 파싱한다', () => {\n const reportData = {\n output_text: {\n summary: {\n total_score: 70,\n basis: '구버전 코멘트',\n good_point: [{ title1: '강점A', detail1: '내용A' }],\n improvement: [{ title1: '개선A', detail1: '내용B' }],\n },\n skill_items: [{ a_score: 5, a_quality: 'X 매우 미흡' }],\n },\n };\n\n const r = parseReportData(reportData, null);\n\n expect(r.totalScore).toBe(70);\n expect(r.comment).toBe('구버전 코멘트');\n expect(r.strengths).toEqual([{ title: '강점A', detail: '내용A' }]);\n expect(r.improvements).toEqual([{ title: '개선A', detail: '내용B' }]);\n\n expect(r.steps).toHaveLength(1);\n const step = r.steps[0];\n // guideline 이 없으므로 humanize 폴백.\n expect(step.label).toBe('A');\n // 비표준 등급은 quality=null, qualityLabel 에 원문 보존.\n expect(step.quality).toBeNull();\n expect(step.qualityLabel).toBe('X 매우 미흡');\n });\n\n it('래퍼(output_text)가 없으면 reportData 자체를 루트로 사용한다', () => {\n const reportData = {\n summary: { total_score: 55 },\n eval_items: [{ x_score: 3 }],\n };\n\n const r = parseReportData(reportData, null);\n\n expect(r.totalScore).toBe(55);\n expect(r.steps).toHaveLength(1);\n expect(r.steps[0].score).toBe(3);\n });\n\n it('잘못된/빈 입력은 빈 리포트로 안전 처리한다', () => {\n const r = parseReportData(null, undefined);\n expect(r.totalScore).toBeNull();\n expect(r.comment).toBeNull();\n expect(r.steps).toEqual([]);\n expect(r.strengths).toEqual([]);\n expect(r.improvements).toEqual([]);\n expect(r.skills).toEqual([]);\n expect(r.deduction).toBeNull();\n });\n\n it('guideline 길이가 단계 수와 불일치하면 humanize 폴백한다', () => {\n const reportData = {\n output_text: {\n eval_items: [{ nurturing_org_score: 7, own_score: 9 }],\n },\n };\n // 단계는 2개(nurturing_org, own)인데 guideline 은 1개 → 불일치 → 폴백.\n const guideline = { value: [{ title: '단 하나' }], version: 1 };\n\n const r = parseReportData(reportData, guideline);\n\n expect(r.steps).toHaveLength(2);\n expect(r.steps[0].label).toBe('Nurturing Org');\n expect(r.steps[1].label).toBe('Own');\n });\n\n it('displayMeta.steps: order 정렬 + key 매칭 + initial/maxValue/title/description 적용', () => {\n const reportData = {\n output_text: {\n eval_items: [\n {\n // 등장 순서는 progress 먼저, opening 나중 — displayMeta.order 로 뒤집힌다.\n progress_score: 6,\n progress_basis: 'p근거',\n opening_score: 9,\n opening_basis: 'o근거',\n },\n ],\n },\n };\n const displayMeta = {\n title: 'OPENFANS 코칭',\n description: '단계 코칭 프레임워크',\n steps: [\n {\n order: 1,\n key: 'opening',\n initial: 'O',\n displayName: '오프닝',\n maxValue: 10,\n description: '첫 인상',\n },\n {\n order: 2,\n key: 'progress',\n initial: 'P',\n displayName: '진행',\n maxValue: 20,\n description: '',\n },\n ],\n };\n\n const r = parseReportData(reportData, null, displayMeta);\n\n expect(r.frameworkTitle).toBe('OPENFANS 코칭');\n expect(r.frameworkDescription).toBe('단계 코칭 프레임워크');\n\n expect(r.steps).toHaveLength(2);\n // order 정렬: opening(1) → progress(2). reportData 등장 순서(progress 먼저)와 무관.\n expect(r.steps[0].key).toBe('opening');\n expect(r.steps[0].label).toBe('오프닝');\n expect(r.steps[0].initial).toBe('O');\n expect(r.steps[0].score).toBe(9);\n expect(r.steps[0].basis).toBe('o근거');\n // reportData 에 max 없음 → displayMeta.maxValue 폴백.\n expect(r.steps[0].maxScore).toBe(10);\n expect(r.steps[0].description).toBe('첫 인상');\n\n expect(r.steps[1].key).toBe('progress');\n expect(r.steps[1].initial).toBe('P');\n expect(r.steps[1].maxScore).toBe(20);\n // displayMeta.steps.description 이 빈 문자열 + guideline 없음 → null.\n expect(r.steps[1].description).toBeNull();\n });\n\n it('displayMeta: reportData 의 실제 max_score 가 maxValue 보다 우선한다', () => {\n const reportData = {\n output_text: { eval_items: [{ opening_score: 8, opening_max_score: 12 }] },\n };\n const displayMeta = {\n steps: [\n {\n order: 0,\n key: 'opening',\n initial: 'O',\n displayName: '오프닝',\n maxValue: 100,\n description: '',\n },\n ],\n };\n\n const r = parseReportData(reportData, null, displayMeta);\n expect(r.steps[0].maxScore).toBe(12);\n });\n\n it('displayMeta.steps 에 있으나 reportData 에 없는 key 도 행을 유지한다(점수 null)', () => {\n const reportData = { output_text: { eval_items: [{ opening_score: 7 }] } };\n const displayMeta = {\n steps: [\n {\n order: 0,\n key: 'opening',\n initial: 'O',\n displayName: '오프닝',\n maxValue: 10,\n description: '',\n },\n {\n order: 1,\n key: 'closing',\n initial: 'C',\n displayName: '클로징',\n maxValue: 10,\n description: '',\n },\n ],\n };\n\n const r = parseReportData(reportData, null, displayMeta);\n expect(r.steps).toHaveLength(2);\n expect(r.steps[1].key).toBe('closing');\n expect(r.steps[1].score).toBeNull();\n // 데이터가 없어도 displayMeta.maxValue 폴백은 적용.\n expect(r.steps[1].maxScore).toBe(10);\n });\n\n it('displayMeta 가 없으면 frameworkTitle/Description=null, step.initial=null (폴백)', () => {\n const reportData = { output_text: { eval_items: [{ own_score: 5 }] } };\n const r = parseReportData(reportData, null);\n expect(r.frameworkTitle).toBeNull();\n expect(r.frameworkDescription).toBeNull();\n expect(r.steps[0].initial).toBeNull();\n });\n\n it('displayMeta.title/description 만 있고 steps 가 비면 텍스트만 적용하고 단계는 폴백한다', () => {\n const reportData = {\n output_text: { eval_items: [{ own_score: 5, progress_score: 6 }] },\n };\n const displayMeta = { title: '제목만', description: '설명만', steps: [] };\n\n const r = parseReportData(reportData, null, displayMeta);\n expect(r.frameworkTitle).toBe('제목만');\n expect(r.frameworkDescription).toBe('설명만');\n // steps 비었으므로 폴백(등장 순서 + humanize).\n expect(r.steps).toHaveLength(2);\n expect(r.steps[0].key).toBe('own');\n expect(r.steps[0].initial).toBeNull();\n });\n});\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/app/roleplay/utils/session-utils.ts
|
|
21
|
+
var session_utils_exports = {};
|
|
22
|
+
__export(session_utils_exports, {
|
|
23
|
+
COLOR_VARS: () => COLOR_VARS,
|
|
24
|
+
QUALITY_RATIO: () => QUALITY_RATIO,
|
|
25
|
+
SPEECH_OVERTALK_THRESHOLD: () => SPEECH_OVERTALK_THRESHOLD,
|
|
26
|
+
SYNC_TOLERANCE: () => SYNC_TOLERANCE,
|
|
27
|
+
computeSpeechShare: () => computeSpeechShare,
|
|
28
|
+
formatDate: () => formatDate,
|
|
29
|
+
formatDuration: () => formatDuration,
|
|
30
|
+
formatTimestamp: () => formatTimestamp,
|
|
31
|
+
gradeColorFromLabel: () => gradeColorFromLabel,
|
|
32
|
+
hasStepSignal: () => hasStepSignal,
|
|
33
|
+
isLengthClose: () => isLengthClose,
|
|
34
|
+
qualityColor: () => qualityColor,
|
|
35
|
+
qualityRatio: () => qualityRatio,
|
|
36
|
+
statusLabelOf: () => statusLabelOf,
|
|
37
|
+
stepCode: () => stepCode,
|
|
38
|
+
stepColor: () => stepColor,
|
|
39
|
+
stepGradeLabel: () => stepGradeLabel,
|
|
40
|
+
stepRatio: () => stepRatio,
|
|
41
|
+
stepScoreText: () => stepScoreText,
|
|
42
|
+
stepVars: () => stepVars
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(session_utils_exports);
|
|
45
|
+
var import_date_fns = require("date-fns");
|
|
46
|
+
var COLOR_VARS = {
|
|
47
|
+
green: {
|
|
48
|
+
fg: "var(--green-11)",
|
|
49
|
+
bg: "var(--green-a3)",
|
|
50
|
+
border: "var(--green-a6)"
|
|
51
|
+
},
|
|
52
|
+
amber: {
|
|
53
|
+
fg: "var(--amber-11)",
|
|
54
|
+
bg: "var(--amber-a3)",
|
|
55
|
+
border: "var(--amber-a6)"
|
|
56
|
+
},
|
|
57
|
+
red: { fg: "var(--red-11)", bg: "var(--red-a3)", border: "var(--red-a6)" },
|
|
58
|
+
gray: { fg: "var(--gray-9)", bg: "var(--gray-a3)", border: "var(--gray-5)" }
|
|
59
|
+
};
|
|
60
|
+
function qualityColor(quality) {
|
|
61
|
+
switch (quality) {
|
|
62
|
+
case "\uCDA9\uC871":
|
|
63
|
+
return "green";
|
|
64
|
+
case "\uBD80\uBD84\uCDA9\uC871":
|
|
65
|
+
return "amber";
|
|
66
|
+
case "\uBBF8\uD761":
|
|
67
|
+
return "red";
|
|
68
|
+
default:
|
|
69
|
+
return "gray";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
var QUALITY_RATIO = {
|
|
73
|
+
\uCDA9\uC871: 1,
|
|
74
|
+
\uBD80\uBD84\uCDA9\uC871: 0.6,
|
|
75
|
+
\uBBF8\uD761: 0.3
|
|
76
|
+
};
|
|
77
|
+
function qualityRatio(quality) {
|
|
78
|
+
return quality === null ? null : QUALITY_RATIO[quality];
|
|
79
|
+
}
|
|
80
|
+
function gradeColorFromLabel(label) {
|
|
81
|
+
if (!label) return "gray";
|
|
82
|
+
if (label.includes("\uBBF8\uD761") || label.includes("\uBD80\uC871") || label.includes("\uBBF8\uB2EC"))
|
|
83
|
+
return "red";
|
|
84
|
+
if (label.includes("\uBD80\uBD84") || label.includes("\uBCF4\uD1B5")) return "amber";
|
|
85
|
+
if (label.includes("\uCDA9\uC871") || label.includes("\uC6B0\uC218") || label.includes("\uCDA9\uBD84"))
|
|
86
|
+
return "green";
|
|
87
|
+
return "gray";
|
|
88
|
+
}
|
|
89
|
+
function stepColor(step) {
|
|
90
|
+
return step.quality !== null ? qualityColor(step.quality) : gradeColorFromLabel(step.qualityLabel);
|
|
91
|
+
}
|
|
92
|
+
function stepVars(step) {
|
|
93
|
+
return COLOR_VARS[stepColor(step)];
|
|
94
|
+
}
|
|
95
|
+
function stepRatio(step) {
|
|
96
|
+
if (step.maxScore !== null && step.maxScore > 0 && step.score !== null) {
|
|
97
|
+
return Math.max(0, Math.min(1, step.score / step.maxScore));
|
|
98
|
+
}
|
|
99
|
+
return qualityRatio(step.quality);
|
|
100
|
+
}
|
|
101
|
+
function stepScoreText(step) {
|
|
102
|
+
if (step.score === null) return null;
|
|
103
|
+
return step.maxScore !== null ? `${step.score}/${step.maxScore}` : `${step.score}`;
|
|
104
|
+
}
|
|
105
|
+
function stepGradeLabel(step) {
|
|
106
|
+
return step.qualityLabel;
|
|
107
|
+
}
|
|
108
|
+
function hasStepSignal(step) {
|
|
109
|
+
return step.score !== null || step.qualityLabel !== null;
|
|
110
|
+
}
|
|
111
|
+
var LETTER_RE = /[A-Za-z]/;
|
|
112
|
+
function stepCode(step) {
|
|
113
|
+
if (step.initial !== null && step.initial.trim().length > 0) {
|
|
114
|
+
return step.initial.trim();
|
|
115
|
+
}
|
|
116
|
+
const label = step.label.trim();
|
|
117
|
+
const first = label.charAt(0);
|
|
118
|
+
const second = label.charAt(1);
|
|
119
|
+
if (LETTER_RE.test(first) && (second === " " || second === "\u2014" || second === "-")) {
|
|
120
|
+
return first.toUpperCase();
|
|
121
|
+
}
|
|
122
|
+
if (first === "[" && LETTER_RE.test(second) && label.charAt(2) === "]") {
|
|
123
|
+
return second.toUpperCase();
|
|
124
|
+
}
|
|
125
|
+
return step.key.split("_")[0].slice(0, 4).toUpperCase();
|
|
126
|
+
}
|
|
127
|
+
function formatDuration(seconds) {
|
|
128
|
+
if (seconds === null) return "\u2014";
|
|
129
|
+
const m = Math.floor(seconds / 60);
|
|
130
|
+
const s = seconds % 60;
|
|
131
|
+
return `${m}\uBD84 ${String(s).padStart(2, "0")}\uCD08`;
|
|
132
|
+
}
|
|
133
|
+
function formatDate(iso) {
|
|
134
|
+
const d = new Date(iso);
|
|
135
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
136
|
+
return (0, import_date_fns.format)(d, "M/d HH:mm");
|
|
137
|
+
}
|
|
138
|
+
function formatTimestamp(ms) {
|
|
139
|
+
const total = Math.floor(ms / 1e3);
|
|
140
|
+
const m = Math.floor(total / 60);
|
|
141
|
+
const s = total % 60;
|
|
142
|
+
return `${m}:${String(s).padStart(2, "0")}`;
|
|
143
|
+
}
|
|
144
|
+
var SYNC_TOLERANCE = 0.05;
|
|
145
|
+
function isLengthClose(mediaSec, sttSec) {
|
|
146
|
+
if (!mediaSec || mediaSec <= 0 || sttSec <= 0) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
return Math.abs(mediaSec - sttSec) / Math.max(mediaSec, sttSec) <= SYNC_TOLERANCE;
|
|
150
|
+
}
|
|
151
|
+
var SPEECH_OVERTALK_THRESHOLD = 40;
|
|
152
|
+
function computeSpeechShare(lines) {
|
|
153
|
+
let userChars = 0;
|
|
154
|
+
let partnerChars = 0;
|
|
155
|
+
for (const line of lines) {
|
|
156
|
+
const count = line.text.replace(/\s/g, "").length;
|
|
157
|
+
if (line.speaker === "user") {
|
|
158
|
+
userChars += count;
|
|
159
|
+
} else {
|
|
160
|
+
partnerChars += count;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const total = userChars + partnerChars;
|
|
164
|
+
if (total === 0) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const userPct = Math.round(userChars / total * 100);
|
|
168
|
+
return { userPct, partnerPct: 100 - userPct };
|
|
169
|
+
}
|
|
170
|
+
var STATUS_LABEL = {
|
|
171
|
+
complete: "\uBD84\uC11D \uC644\uB8CC",
|
|
172
|
+
completed: "\uBD84\uC11D \uC644\uB8CC",
|
|
173
|
+
analyzing: "AI \uBD84\uC11D \uC911",
|
|
174
|
+
partial: "\uC77C\uBD80 \uC0B0\uCD9C\uBB3C \uC5C6\uC74C"
|
|
175
|
+
};
|
|
176
|
+
function statusLabelOf(status) {
|
|
177
|
+
var _a;
|
|
178
|
+
if (!status) return "\uC138\uC158";
|
|
179
|
+
return (_a = STATUS_LABEL[status]) != null ? _a : status;
|
|
180
|
+
}
|
|
181
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
182
|
+
0 && (module.exports = {
|
|
183
|
+
COLOR_VARS,
|
|
184
|
+
QUALITY_RATIO,
|
|
185
|
+
SPEECH_OVERTALK_THRESHOLD,
|
|
186
|
+
SYNC_TOLERANCE,
|
|
187
|
+
computeSpeechShare,
|
|
188
|
+
formatDate,
|
|
189
|
+
formatDuration,
|
|
190
|
+
formatTimestamp,
|
|
191
|
+
gradeColorFromLabel,
|
|
192
|
+
hasStepSignal,
|
|
193
|
+
isLengthClose,
|
|
194
|
+
qualityColor,
|
|
195
|
+
qualityRatio,
|
|
196
|
+
statusLabelOf,
|
|
197
|
+
stepCode,
|
|
198
|
+
stepColor,
|
|
199
|
+
stepGradeLabel,
|
|
200
|
+
stepRatio,
|
|
201
|
+
stepScoreText,
|
|
202
|
+
stepVars
|
|
203
|
+
});
|
|
204
|
+
//# sourceMappingURL=session-utils.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/app/roleplay/utils/session-utils.ts"],"sourcesContent":["// 롤플레잉 세션 상세 순수 유틸 함수 모음.\n// 원본: website/src/modules/customer/roleplay/pages/RpSessionDetail/RpSessionDetail.tsx\n// 'use client' 없음 — 순수 함수, 클라이언트 전용 아님.\n\nimport { format } from 'date-fns';\nimport type { RpSessionScriptLine } from '../types';\nimport type { RpQuality, RpReportStep } from './report-parse';\n\n/* ───────────────────────── 타입 ───────────────────────── */\n\nexport interface SpeechShare { userPct: number; partnerPct: number }\n\nexport type QColor = 'green' | 'amber' | 'red' | 'gray';\n\n/* ───────────────────────── 색 변수 ───────────────────────── */\n\nexport const COLOR_VARS: Record<QColor, { fg: string; bg: string; border: string }> = {\n green: {\n fg: 'var(--green-11)',\n bg: 'var(--green-a3)',\n border: 'var(--green-a6)',\n },\n amber: {\n fg: 'var(--amber-11)',\n bg: 'var(--amber-a3)',\n border: 'var(--amber-a6)',\n },\n red: { fg: 'var(--red-11)', bg: 'var(--red-a3)', border: 'var(--red-a6)' },\n gray: { fg: 'var(--gray-9)', bg: 'var(--gray-a3)', border: 'var(--gray-5)' },\n};\n\n/* ───────────────────────── 등급 색 ───────────────────────── */\n\nexport function qualityColor(quality: RpQuality | null): QColor {\n switch (quality) {\n case '충족':\n return 'green';\n case '부분충족':\n return 'amber';\n case '미흡':\n return 'red';\n default:\n return 'gray';\n }\n}\n\nexport const QUALITY_RATIO: Record<RpQuality, number> = {\n 충족: 1,\n 부분충족: 0.6,\n 미흡: 0.3,\n} as const;\n\nexport function qualityRatio(quality: RpQuality | null): number | null {\n return quality === null ? null : QUALITY_RATIO[quality];\n}\n\n// 표준 등급이 아닌 자유 등급 문자열에서 색만 키워드로 추정(비율은 산출하지 않음).\nexport function gradeColorFromLabel(label: string | null): QColor {\n if (!label) return 'gray';\n if (\n label.includes('미흡') ||\n label.includes('부족') ||\n label.includes('미달')\n )\n return 'red';\n if (label.includes('부분') || label.includes('보통')) return 'amber';\n if (\n label.includes('충족') ||\n label.includes('우수') ||\n label.includes('충분')\n )\n return 'green';\n return 'gray';\n}\n\n/* ───────────────────────── 단계(Step) 헬퍼 ───────────────────────── */\n\n// 단계 색: 표준 등급 우선, 없으면 라벨 키워드 추정.\nexport function stepColor(step: RpReportStep): QColor {\n return step.quality !== null\n ? qualityColor(step.quality)\n : gradeColorFromLabel(step.qualityLabel);\n}\n\nexport function stepVars(step: RpReportStep): { fg: string; bg: string; border: string } {\n return COLOR_VARS[stepColor(step)];\n}\n\n// progress/radar 정규화 비율(0~1). score/maxScore 우선, 없으면 표준 등급 비율, 둘 다 없으면 null.\nexport function stepRatio(step: RpReportStep): number | null {\n if (step.maxScore !== null && step.maxScore > 0 && step.score !== null) {\n return Math.max(0, Math.min(1, step.score / step.maxScore));\n }\n return qualityRatio(step.quality);\n}\n\n// 점수 텍스트: max 있으면 'score/max', 없으면 'score', score 없으면 null.\nexport function stepScoreText(step: RpReportStep): string | null {\n if (step.score === null) return null;\n return step.maxScore !== null\n ? `${step.score}/${step.maxScore}`\n : `${step.score}`;\n}\n\n// 표시할 등급 라벨(자유 문자열 포함). 없으면 null.\nexport function stepGradeLabel(step: RpReportStep): string | null {\n return step.qualityLabel;\n}\n\n// 단계에 표시할 평가 정보가 하나라도 있는지(점수 또는 등급).\nexport function hasStepSignal(step: RpReportStep): boolean {\n return step.score !== null || step.qualityLabel !== null;\n}\n\n// radar 축/요약 라벨용 짧은 코드.\n// displayMeta.steps.initial 이 지정돼 있으면 최우선(이니셜 충돌 해소).\n// 없으면 기존 휴리스틱: acronym 프레임워크는 선두 글자, 아니면 키 약식.\nconst LETTER_RE = /[A-Za-z]/;\n\nexport function stepCode(step: RpReportStep): string {\n if (step.initial !== null && step.initial.trim().length > 0) {\n return step.initial.trim();\n }\n const label = step.label.trim();\n const first = label.charAt(0);\n const second = label.charAt(1);\n // \"O — Opening\" / \"O - Opening\" / \"O Opening\" 형태\n if (\n LETTER_RE.test(first) &&\n (second === ' ' || second === '—' || second === '-')\n ) {\n return first.toUpperCase();\n }\n // \"[I] Introduction\" 형태\n if (first === '[' && LETTER_RE.test(second) && label.charAt(2) === ']') {\n return second.toUpperCase();\n }\n return step.key.split('_')[0].slice(0, 4).toUpperCase();\n}\n\n/* ───────────────────────── 포맷 유틸 ───────────────────────── */\n\nexport function formatDuration(seconds: number | null): string {\n if (seconds === null) return '—';\n const m = Math.floor(seconds / 60);\n const s = seconds % 60;\n return `${m}분 ${String(s).padStart(2, '0')}초`;\n}\n\nexport function formatDate(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return iso;\n return format(d, 'M/d HH:mm');\n}\n\nexport function formatTimestamp(ms: number): string {\n const total = Math.floor(ms / 1000);\n const m = Math.floor(total / 60);\n const s = total % 60;\n return `${m}:${String(s).padStart(2, '0')}`;\n}\n\n/* ───────────────────────── 미디어 싱크 ───────────────────────── */\n\n// 미디어 길이와 STT 길이가 이 비율 이내로 가까우면 싱크(클릭 seek/재생 하이라이트) 활성.\nexport const SYNC_TOLERANCE = 0.05;\n\nexport function isLengthClose(mediaSec: number | null, sttSec: number): boolean {\n if (!mediaSec || mediaSec <= 0 || sttSec <= 0) {\n return false;\n }\n return (\n Math.abs(mediaSec - sttSec) / Math.max(mediaSec, sttSec) <= SYNC_TOLERANCE\n );\n}\n\n/* ───────────────────────── 발화량 ───────────────────────── */\n\n// 내 발화량이 이 비율을 넘으면 과다 발화로 간주(빨간 막대 + 주의 문구).\nexport const SPEECH_OVERTALK_THRESHOLD = 40;\n\n// 화자별 발화량 비율. 기준은 공백·개행 제거 글자 수(실제 말한 양에 근접, 공백 패턴 왜곡 방지).\n// 발화 글자가 전혀 없으면 null → 미표시.\nexport function computeSpeechShare(lines: RpSessionScriptLine[]): SpeechShare | null {\n let userChars = 0;\n let partnerChars = 0;\n for (const line of lines) {\n const count = line.text.replace(/\\s/g, '').length;\n if (line.speaker === 'user') {\n userChars += count;\n } else {\n partnerChars += count;\n }\n }\n const total = userChars + partnerChars;\n if (total === 0) {\n return null;\n }\n const userPct = Math.round((userChars / total) * 100);\n return { userPct, partnerPct: 100 - userPct };\n}\n\n/* ───────────────────────── 세션 상태 ───────────────────────── */\n\nconst STATUS_LABEL: Record<string, string> = {\n complete: '분석 완료',\n completed: '분석 완료',\n analyzing: 'AI 분석 중',\n partial: '일부 산출물 없음',\n};\n\nexport function statusLabelOf(status: string | null): string {\n if (!status) return '세션';\n return STATUS_LABEL[status] ?? status;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { RpSessionScriptLine } from '../types.cjs';
|
|
2
|
+
import { RpQuality, RpReportStep } from './report-parse.cjs';
|
|
3
|
+
|
|
4
|
+
interface SpeechShare {
|
|
5
|
+
userPct: number;
|
|
6
|
+
partnerPct: number;
|
|
7
|
+
}
|
|
8
|
+
type QColor = 'green' | 'amber' | 'red' | 'gray';
|
|
9
|
+
declare const COLOR_VARS: Record<QColor, {
|
|
10
|
+
fg: string;
|
|
11
|
+
bg: string;
|
|
12
|
+
border: string;
|
|
13
|
+
}>;
|
|
14
|
+
declare function qualityColor(quality: RpQuality | null): QColor;
|
|
15
|
+
declare const QUALITY_RATIO: Record<RpQuality, number>;
|
|
16
|
+
declare function qualityRatio(quality: RpQuality | null): number | null;
|
|
17
|
+
declare function gradeColorFromLabel(label: string | null): QColor;
|
|
18
|
+
declare function stepColor(step: RpReportStep): QColor;
|
|
19
|
+
declare function stepVars(step: RpReportStep): {
|
|
20
|
+
fg: string;
|
|
21
|
+
bg: string;
|
|
22
|
+
border: string;
|
|
23
|
+
};
|
|
24
|
+
declare function stepRatio(step: RpReportStep): number | null;
|
|
25
|
+
declare function stepScoreText(step: RpReportStep): string | null;
|
|
26
|
+
declare function stepGradeLabel(step: RpReportStep): string | null;
|
|
27
|
+
declare function hasStepSignal(step: RpReportStep): boolean;
|
|
28
|
+
declare function stepCode(step: RpReportStep): string;
|
|
29
|
+
declare function formatDuration(seconds: number | null): string;
|
|
30
|
+
declare function formatDate(iso: string): string;
|
|
31
|
+
declare function formatTimestamp(ms: number): string;
|
|
32
|
+
declare const SYNC_TOLERANCE = 0.05;
|
|
33
|
+
declare function isLengthClose(mediaSec: number | null, sttSec: number): boolean;
|
|
34
|
+
declare const SPEECH_OVERTALK_THRESHOLD = 40;
|
|
35
|
+
declare function computeSpeechShare(lines: RpSessionScriptLine[]): SpeechShare | null;
|
|
36
|
+
declare function statusLabelOf(status: string | null): string;
|
|
37
|
+
|
|
38
|
+
export { COLOR_VARS, type QColor, QUALITY_RATIO, SPEECH_OVERTALK_THRESHOLD, SYNC_TOLERANCE, type SpeechShare, computeSpeechShare, formatDate, formatDuration, formatTimestamp, gradeColorFromLabel, hasStepSignal, isLengthClose, qualityColor, qualityRatio, statusLabelOf, stepCode, stepColor, stepGradeLabel, stepRatio, stepScoreText, stepVars };
|