@tipp/ui 2.9.0 → 2.10.0

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.
Files changed (198) hide show
  1. package/dist/app/index.js +17 -17
  2. package/dist/app/platform/edit-coaching-time.js +17 -17
  3. package/dist/app/platform/edit-service-type.js +17 -17
  4. package/dist/app/platform/goal-manage-card-edit.js +17 -17
  5. package/dist/app/platform/on-offline-radio-card.js +17 -17
  6. package/dist/app/platform/reservation-card.js +17 -17
  7. package/dist/app/platform/session-card.js +17 -17
  8. package/dist/app/roleplay/chart/session-chart.cjs +351 -0
  9. package/dist/app/roleplay/chart/session-chart.cjs.map +1 -0
  10. package/dist/app/roleplay/chart/session-chart.d.cts +16 -0
  11. package/dist/app/roleplay/chart/session-chart.d.ts +16 -0
  12. package/dist/app/roleplay/chart/session-chart.js +14 -0
  13. package/dist/app/roleplay/chart/session-chart.js.map +1 -0
  14. package/dist/app/roleplay/index.cjs +5706 -0
  15. package/dist/app/roleplay/index.cjs.map +1 -0
  16. package/dist/app/roleplay/index.d.cts +4 -0
  17. package/dist/app/roleplay/index.d.ts +4 -0
  18. package/dist/app/roleplay/index.js +182 -0
  19. package/dist/app/roleplay/index.js.map +1 -0
  20. package/dist/app/roleplay/parts/analysis-section.cjs +2178 -0
  21. package/dist/app/roleplay/parts/analysis-section.cjs.map +1 -0
  22. package/dist/app/roleplay/parts/analysis-section.d.cts +17 -0
  23. package/dist/app/roleplay/parts/analysis-section.d.ts +17 -0
  24. package/dist/app/roleplay/parts/analysis-section.js +97 -0
  25. package/dist/app/roleplay/parts/analysis-section.js.map +1 -0
  26. package/dist/app/roleplay/parts/coaching-panel.cjs +2191 -0
  27. package/dist/app/roleplay/parts/coaching-panel.cjs.map +1 -0
  28. package/dist/app/roleplay/parts/coaching-panel.d.cts +8 -0
  29. package/dist/app/roleplay/parts/coaching-panel.d.ts +8 -0
  30. package/dist/app/roleplay/parts/coaching-panel.js +95 -0
  31. package/dist/app/roleplay/parts/coaching-panel.js.map +1 -0
  32. package/dist/app/roleplay/parts/content.cjs +4683 -0
  33. package/dist/app/roleplay/parts/content.cjs.map +1 -0
  34. package/dist/app/roleplay/parts/content.d.cts +15 -0
  35. package/dist/app/roleplay/parts/content.d.ts +15 -0
  36. package/dist/app/roleplay/parts/content.js +174 -0
  37. package/dist/app/roleplay/parts/content.js.map +1 -0
  38. package/dist/app/roleplay/parts/header.cjs +2300 -0
  39. package/dist/app/roleplay/parts/header.cjs.map +1 -0
  40. package/dist/app/roleplay/parts/header.d.cts +13 -0
  41. package/dist/app/roleplay/parts/header.d.ts +13 -0
  42. package/dist/app/roleplay/parts/header.js +96 -0
  43. package/dist/app/roleplay/parts/header.js.map +1 -0
  44. package/dist/app/roleplay/parts/score-summary.cjs +2276 -0
  45. package/dist/app/roleplay/parts/score-summary.cjs.map +1 -0
  46. package/dist/app/roleplay/parts/score-summary.d.cts +12 -0
  47. package/dist/app/roleplay/parts/score-summary.d.ts +12 -0
  48. package/dist/app/roleplay/parts/score-summary.js +96 -0
  49. package/dist/app/roleplay/parts/score-summary.js.map +1 -0
  50. package/dist/app/roleplay/parts/score-trend-section.cjs +3686 -0
  51. package/dist/app/roleplay/parts/score-trend-section.cjs.map +1 -0
  52. package/dist/app/roleplay/parts/score-trend-section.d.cts +14 -0
  53. package/dist/app/roleplay/parts/score-trend-section.d.ts +14 -0
  54. package/dist/app/roleplay/parts/score-trend-section.js +168 -0
  55. package/dist/app/roleplay/parts/score-trend-section.js.map +1 -0
  56. package/dist/app/roleplay/parts/script-transcript.cjs +2288 -0
  57. package/dist/app/roleplay/parts/script-transcript.cjs.map +1 -0
  58. package/dist/app/roleplay/parts/script-transcript.d.cts +15 -0
  59. package/dist/app/roleplay/parts/script-transcript.d.ts +15 -0
  60. package/dist/app/roleplay/parts/script-transcript.js +96 -0
  61. package/dist/app/roleplay/parts/script-transcript.js.map +1 -0
  62. package/dist/app/roleplay/parts/section-nav.cjs +2433 -0
  63. package/dist/app/roleplay/parts/section-nav.cjs.map +1 -0
  64. package/dist/app/roleplay/parts/section-nav.d.cts +15 -0
  65. package/dist/app/roleplay/parts/section-nav.d.ts +15 -0
  66. package/dist/app/roleplay/parts/section-nav.js +96 -0
  67. package/dist/app/roleplay/parts/section-nav.js.map +1 -0
  68. package/dist/app/roleplay/parts/session-replay-dialog.cjs +2598 -0
  69. package/dist/app/roleplay/parts/session-replay-dialog.cjs.map +1 -0
  70. package/dist/app/roleplay/parts/session-replay-dialog.d.cts +14 -0
  71. package/dist/app/roleplay/parts/session-replay-dialog.d.ts +14 -0
  72. package/dist/app/roleplay/parts/session-replay-dialog.js +97 -0
  73. package/dist/app/roleplay/parts/session-replay-dialog.js.map +1 -0
  74. package/dist/app/roleplay/parts/step-detail.cjs +2311 -0
  75. package/dist/app/roleplay/parts/step-detail.cjs.map +1 -0
  76. package/dist/app/roleplay/parts/step-detail.d.cts +9 -0
  77. package/dist/app/roleplay/parts/step-detail.d.ts +9 -0
  78. package/dist/app/roleplay/parts/step-detail.js +96 -0
  79. package/dist/app/roleplay/parts/step-detail.js.map +1 -0
  80. package/dist/app/roleplay/parts/step-radar.cjs +463 -0
  81. package/dist/app/roleplay/parts/step-radar.cjs.map +1 -0
  82. package/dist/app/roleplay/parts/step-radar.d.cts +8 -0
  83. package/dist/app/roleplay/parts/step-radar.d.ts +8 -0
  84. package/dist/app/roleplay/parts/step-radar.js +14 -0
  85. package/dist/app/roleplay/parts/step-radar.js.map +1 -0
  86. package/dist/app/roleplay/parts/step-summary.cjs +2651 -0
  87. package/dist/app/roleplay/parts/step-summary.cjs.map +1 -0
  88. package/dist/app/roleplay/parts/step-summary.d.cts +8 -0
  89. package/dist/app/roleplay/parts/step-summary.d.ts +8 -0
  90. package/dist/app/roleplay/parts/step-summary.js +101 -0
  91. package/dist/app/roleplay/parts/step-summary.js.map +1 -0
  92. package/dist/app/roleplay/rp-session-detail.cjs +5703 -0
  93. package/dist/app/roleplay/rp-session-detail.cjs.map +1 -0
  94. package/dist/app/roleplay/rp-session-detail.d.cts +17 -0
  95. package/dist/app/roleplay/rp-session-detail.d.ts +17 -0
  96. package/dist/app/roleplay/rp-session-detail.js +179 -0
  97. package/dist/app/roleplay/rp-session-detail.js.map +1 -0
  98. package/dist/app/roleplay/types.cjs +19 -0
  99. package/dist/app/roleplay/types.cjs.map +1 -0
  100. package/dist/app/roleplay/types.d.cts +42 -0
  101. package/dist/app/roleplay/types.d.ts +42 -0
  102. package/dist/app/roleplay/types.js +2 -0
  103. package/dist/app/roleplay/types.js.map +1 -0
  104. package/dist/app/roleplay/utils/report-parse.cjs +285 -0
  105. package/dist/app/roleplay/utils/report-parse.cjs.map +1 -0
  106. package/dist/app/roleplay/utils/report-parse.d.cts +57 -0
  107. package/dist/app/roleplay/utils/report-parse.d.ts +57 -0
  108. package/dist/app/roleplay/utils/report-parse.js +8 -0
  109. package/dist/app/roleplay/utils/report-parse.js.map +1 -0
  110. package/dist/app/roleplay/utils/report-parse.test.cjs +491 -0
  111. package/dist/app/roleplay/utils/report-parse.test.cjs.map +1 -0
  112. package/dist/app/roleplay/utils/report-parse.test.d.cts +2 -0
  113. package/dist/app/roleplay/utils/report-parse.test.d.ts +2 -0
  114. package/dist/app/roleplay/utils/report-parse.test.js +237 -0
  115. package/dist/app/roleplay/utils/report-parse.test.js.map +1 -0
  116. package/dist/app/roleplay/utils/session-utils.cjs +204 -0
  117. package/dist/app/roleplay/utils/session-utils.cjs.map +1 -0
  118. package/dist/app/roleplay/utils/session-utils.d.cts +38 -0
  119. package/dist/app/roleplay/utils/session-utils.d.ts +38 -0
  120. package/dist/app/roleplay/utils/session-utils.js +46 -0
  121. package/dist/app/roleplay/utils/session-utils.js.map +1 -0
  122. package/dist/chunk-2TOASXPK.js +131 -0
  123. package/dist/chunk-2TOASXPK.js.map +1 -0
  124. package/dist/chunk-3UGBMKSQ.js +166 -0
  125. package/dist/chunk-3UGBMKSQ.js.map +1 -0
  126. package/dist/chunk-4YAA25AV.js +161 -0
  127. package/dist/chunk-4YAA25AV.js.map +1 -0
  128. package/dist/chunk-5QIT4URV.js +87 -0
  129. package/dist/chunk-5QIT4URV.js.map +1 -0
  130. package/dist/chunk-6YIKOYBQ.js +185 -0
  131. package/dist/chunk-6YIKOYBQ.js.map +1 -0
  132. package/dist/chunk-7MXY3D3Z.js +84 -0
  133. package/dist/chunk-7MXY3D3Z.js.map +1 -0
  134. package/dist/chunk-EBFQFOUQ.js +138 -0
  135. package/dist/chunk-EBFQFOUQ.js.map +1 -0
  136. package/dist/chunk-EHDUKUML.js +183 -0
  137. package/dist/chunk-EHDUKUML.js.map +1 -0
  138. package/dist/{chunk-NEJCZABQ.js → chunk-FJEDSUGU.js} +7 -7
  139. package/dist/chunk-GA6W62DO.js +101 -0
  140. package/dist/chunk-GA6W62DO.js.map +1 -0
  141. package/dist/chunk-KJPBBBVK.js +142 -0
  142. package/dist/chunk-KJPBBBVK.js.map +1 -0
  143. package/dist/{chunk-TQTCT7DC.js → chunk-LRNO4W2P.js} +5 -5
  144. package/dist/{chunk-6DWUGXSY.js → chunk-N242VJOP.js} +2 -2
  145. package/dist/chunk-NKW6BO5R.js +264 -0
  146. package/dist/chunk-NKW6BO5R.js.map +1 -0
  147. package/dist/{chunk-R5UDNKV2.js → chunk-O332K3SW.js} +2 -2
  148. package/dist/chunk-OUP3VFXG.js +1 -0
  149. package/dist/chunk-OUP3VFXG.js.map +1 -0
  150. package/dist/chunk-QMAINJ4G.js +190 -0
  151. package/dist/chunk-QMAINJ4G.js.map +1 -0
  152. package/dist/{chunk-KLJVIEFW.js → chunk-SOKGYD5B.js} +7 -7
  153. package/dist/chunk-T7LKMJVA.js +142 -0
  154. package/dist/chunk-T7LKMJVA.js.map +1 -0
  155. package/dist/chunk-TFJJKM3W.js +261 -0
  156. package/dist/chunk-TFJJKM3W.js.map +1 -0
  157. package/dist/{chunk-KQ3ME6XY.js → chunk-U2EAXN3Y.js} +4 -4
  158. package/dist/chunk-UXAEOCOH.js +150 -0
  159. package/dist/chunk-UXAEOCOH.js.map +1 -0
  160. package/dist/chunk-XM4GWPXC.js +236 -0
  161. package/dist/chunk-XM4GWPXC.js.map +1 -0
  162. package/dist/index.css +76 -0
  163. package/dist/index.js +32 -32
  164. package/dist/molecules/curriculumV2/CurriculumContents/CurriculumReviewSteps/ReviewQuestion/index.js +5 -5
  165. package/dist/molecules/curriculumV2/CurriculumContents/CurriculumReviewSteps/ReviewQuestion/review-question-body.js +2 -2
  166. package/dist/molecules/curriculumV2/CurriculumContents/CurriculumReviewSteps/index.js +7 -7
  167. package/dist/molecules/curriculumV2/CurriculumContents/curriculum-review.js +8 -8
  168. package/dist/molecules/curriculumV2/CurriculumContents/index.js +9 -9
  169. package/dist/molecules/curriculumV2/index.js +11 -11
  170. package/dist/molecules/dynamic-form-editor.js +2 -2
  171. package/dist/molecules/index.js +29 -29
  172. package/package.json +28 -2
  173. package/src/app/roleplay/chart/session-chart.tsx +185 -0
  174. package/src/app/roleplay/index.ts +8 -0
  175. package/src/app/roleplay/parts/analysis-section.tsx +98 -0
  176. package/src/app/roleplay/parts/coaching-panel.tsx +101 -0
  177. package/src/app/roleplay/parts/content.tsx +146 -0
  178. package/src/app/roleplay/parts/header.tsx +168 -0
  179. package/src/app/roleplay/parts/score-summary.tsx +158 -0
  180. package/src/app/roleplay/parts/score-trend-section.tsx +255 -0
  181. package/src/app/roleplay/parts/script-transcript.tsx +190 -0
  182. package/src/app/roleplay/parts/section-nav.tsx +299 -0
  183. package/src/app/roleplay/parts/session-replay-dialog.tsx +168 -0
  184. package/src/app/roleplay/parts/step-detail.tsx +173 -0
  185. package/src/app/roleplay/parts/step-radar.tsx +80 -0
  186. package/src/app/roleplay/parts/step-summary.tsx +149 -0
  187. package/src/app/roleplay/rp-session-detail.tsx +176 -0
  188. package/src/app/roleplay/types.ts +61 -0
  189. package/src/app/roleplay/utils/report-parse.test.ts +268 -0
  190. package/src/app/roleplay/utils/report-parse.ts +443 -0
  191. package/src/app/roleplay/utils/session-utils.ts +215 -0
  192. package/src/constrant/trainingLog.dummy.json +1 -0
  193. /package/dist/{chunk-NEJCZABQ.js.map → chunk-FJEDSUGU.js.map} +0 -0
  194. /package/dist/{chunk-TQTCT7DC.js.map → chunk-LRNO4W2P.js.map} +0 -0
  195. /package/dist/{chunk-6DWUGXSY.js.map → chunk-N242VJOP.js.map} +0 -0
  196. /package/dist/{chunk-R5UDNKV2.js.map → chunk-O332K3SW.js.map} +0 -0
  197. /package/dist/{chunk-KLJVIEFW.js.map → chunk-SOKGYD5B.js.map} +0 -0
  198. /package/dist/{chunk-KQ3ME6XY.js.map → chunk-U2EAXN3Y.js.map} +0 -0
@@ -0,0 +1,255 @@
1
+ 'use client';
2
+
3
+ import React, { useMemo } from 'react';
4
+ import type { EChartsOption } from 'echarts';
5
+ import { Box, Flex, Heading, Typo } from '../../../atoms';
6
+ import {
7
+ ExpandTable,
8
+ createColumnHelper,
9
+ type ColumnDef,
10
+ type Row,
11
+ } from '../../../molecules';
12
+ import { Chart, useTokenColors } from '../chart/session-chart';
13
+ import type { RpSessionHistoryItem } from '../types';
14
+ import { formatDate } from '../utils/session-utils';
15
+
16
+ /* ───────────────────────── Section ───────────────────────── */
17
+
18
+ export function Section({
19
+ title,
20
+ desc,
21
+ children,
22
+ }: {
23
+ title: string;
24
+ desc?: string;
25
+ children: React.ReactNode;
26
+ }): React.ReactElement {
27
+ return (
28
+ <section>
29
+ <Box mb="3">
30
+ <Heading
31
+ size={{ initial: '4', md: '5' }}
32
+ style={{ textWrap: 'balance' }}
33
+ weight="bold"
34
+ >
35
+ {title}
36
+ </Heading>
37
+ {desc ? (
38
+ <Box mt="1" style={{ maxWidth: '65ch' }}>
39
+ <Typo as="p" color="gray" variant="body">
40
+ {desc}
41
+ </Typo>
42
+ </Box>
43
+ ) : null}
44
+ </Box>
45
+ {children}
46
+ </section>
47
+ );
48
+ }
49
+
50
+ /* ───────────────────────── 타입 ───────────────────────── */
51
+
52
+ type TrendRow = RpSessionHistoryItem & {
53
+ delta: number | null;
54
+ isCurrent: boolean;
55
+ };
56
+
57
+ /* ───────────────────────── 테이블 셀 컴포넌트 ───────────────────────── */
58
+
59
+ function AttemptCell({ row }: { row: Row<TrendRow> }): React.ReactElement {
60
+ const d = row.original;
61
+ return (
62
+ <Typo
63
+ as="span"
64
+ style={{ color: d.isCurrent ? 'var(--gray-12)' : 'var(--gray-11)' }}
65
+ variant="body"
66
+ weight={d.isCurrent ? 'bold' : 'regular'}
67
+ >
68
+ {d.trainingSessionNum}회차{d.isCurrent ? ' (현재)' : null}
69
+ </Typo>
70
+ );
71
+ }
72
+
73
+ function DateCell({ row }: { row: Row<TrendRow> }): React.ReactElement {
74
+ return (
75
+ <Typo as="span" mono style={{ color: 'var(--gray-9)' }} variant="body">
76
+ {formatDate(row.original.createdAt)}
77
+ </Typo>
78
+ );
79
+ }
80
+
81
+ function ScoreCell({ row }: { row: Row<TrendRow> }): React.ReactElement {
82
+ return (
83
+ <Typo
84
+ as="span"
85
+ mono
86
+ variant="body"
87
+ weight={row.original.isCurrent ? 'bold' : 'regular'}
88
+ >
89
+ {row.original.totalScore ?? '—'}
90
+ </Typo>
91
+ );
92
+ }
93
+
94
+ function DeltaCell({ row }: { row: Row<TrendRow> }): React.ReactElement {
95
+ const { delta } = row.original;
96
+ if (delta === null) {
97
+ return (
98
+ <Typo as="span" mono style={{ color: 'var(--gray-9)' }} variant="body">
99
+
100
+ </Typo>
101
+ );
102
+ }
103
+ return (
104
+ <Typo as="span" mono style={{ color: 'var(--gray-9)' }} variant="body">
105
+ {delta > 0 ? `+${delta}` : `${delta}`}
106
+ </Typo>
107
+ );
108
+ }
109
+
110
+ /* ───────────────────────── ScoreTrendSection ───────────────────────── */
111
+
112
+ export interface ScoreTrendSectionProps {
113
+ history: RpSessionHistoryItem[];
114
+ }
115
+
116
+ export function ScoreTrendSection({
117
+ history,
118
+ }: ScoreTrendSectionProps): React.ReactElement {
119
+ const { colors, probe } = useTokenColors({
120
+ accent: ['--accent-9', '#5b5bd6'],
121
+ accentText: ['--accent-11', '#5753c6'],
122
+ grid: ['--gray-5', '#e0e0e0'],
123
+ gridSoft: ['--gray-4', '#e8e8e8'],
124
+ muted: ['--gray-9', '#8d8d8d'],
125
+ });
126
+
127
+ // 총점 없는 회차 제외, 회차 오름차순 정렬, 최근 5회차로 한정
128
+ const ordered = useMemo(
129
+ () =>
130
+ [...history]
131
+ .filter((e) => e.totalScore !== null)
132
+ .sort((a, b) => a.trainingSessionNum - b.trainingSessionNum)
133
+ .slice(-5),
134
+ [history],
135
+ );
136
+
137
+ const scores = ordered.map((e) => e.totalScore ?? 0);
138
+ const minScore = Math.max(0, Math.min(...scores) - 10);
139
+ const maxScore = Math.min(100, Math.max(...scores) + 10);
140
+ const labels = ordered.map((e) => `${e.trainingSessionNum}회차`);
141
+ const lastIndex = ordered.length - 1;
142
+
143
+ const option: EChartsOption = {
144
+ tooltip: { trigger: 'axis', valueFormatter: (v) => `${String(v)}점` },
145
+ grid: { left: 36, right: 16, top: 20, bottom: 24, containLabel: false },
146
+ xAxis: {
147
+ type: 'category',
148
+ data: labels,
149
+ boundaryGap: true,
150
+ axisLine: { lineStyle: { color: colors.grid } },
151
+ axisTick: { show: false },
152
+ axisLabel: { color: colors.muted, fontSize: 10 }, // ds-allow: chart-lib
153
+ },
154
+ yAxis: {
155
+ type: 'value',
156
+ min: minScore,
157
+ max: maxScore,
158
+ splitLine: { lineStyle: { color: colors.gridSoft } },
159
+ axisLabel: { color: colors.muted, fontSize: 10 }, // ds-allow: chart-lib
160
+ },
161
+ series: [
162
+ {
163
+ type: 'line',
164
+ data: scores,
165
+ smooth: false,
166
+ showSymbol: true,
167
+ symbol: 'circle',
168
+ symbolSize: 6,
169
+ lineStyle: { color: colors.accent, width: 2 },
170
+ itemStyle: { color: colors.accent },
171
+ label: {
172
+ show: true,
173
+ position: 'top',
174
+ color: colors.accentText,
175
+ fontSize: 10, // ds-allow: chart-lib
176
+ fontWeight: 'bold', // ds-allow: chart-lib
177
+ formatter: (p) => (p.dataIndex === lastIndex ? String(p.value) : ''),
178
+ },
179
+ },
180
+ ],
181
+ };
182
+
183
+ const rows: TrendRow[] = ordered.map((e, i) => {
184
+ const prev = i > 0 ? (ordered[i - 1].totalScore ?? null) : null;
185
+ return {
186
+ ...e,
187
+ delta:
188
+ prev !== null && e.totalScore !== null ? e.totalScore - prev : null,
189
+ isCurrent: i === ordered.length - 1,
190
+ };
191
+ });
192
+
193
+ const columns = useMemo<ColumnDef<TrendRow>[]>(() => {
194
+ const h = createColumnHelper<TrendRow>();
195
+ return [
196
+ h.display({
197
+ id: 'attempt',
198
+ header: '회차',
199
+ size: 120,
200
+ meta: { autoSize: true, align: 'left' },
201
+ cell: AttemptCell,
202
+ }),
203
+ h.display({
204
+ id: 'date',
205
+ header: '날짜',
206
+ size: 130,
207
+ meta: { align: 'left' },
208
+ cell: DateCell,
209
+ }),
210
+ h.display({
211
+ id: 'score',
212
+ header: '총점',
213
+ size: 80,
214
+ meta: { align: 'right' },
215
+ cell: ScoreCell,
216
+ }),
217
+ h.display({
218
+ id: 'delta',
219
+ header: '변화',
220
+ size: 80,
221
+ meta: { align: 'right' },
222
+ cell: DeltaCell,
223
+ }),
224
+ ];
225
+ }, []);
226
+
227
+ return (
228
+ <Section
229
+ desc={`같은 시나리오 최근 ${ordered.length}회차 총점 변화`}
230
+ title="총점 추이"
231
+ >
232
+ <Flex direction="column" gap="4">
233
+ <div
234
+ aria-label="총점 추이 라인 차트"
235
+ role="img"
236
+ style={{ position: 'relative', width: '100%', height: 200 }}
237
+ >
238
+ {probe}
239
+ <Chart option={option} />
240
+ </div>
241
+ <Box className="rp-expand-table">
242
+ <ExpandTable
243
+ columns={columns}
244
+ data={rows}
245
+ getRowStyle={(d: TrendRow) =>
246
+ d.isCurrent ? { background: 'var(--accent-a2)' } : {}
247
+ }
248
+ showPagination={false}
249
+ variant="sprite"
250
+ />
251
+ </Box>
252
+ </Flex>
253
+ </Section>
254
+ );
255
+ }
@@ -0,0 +1,190 @@
1
+ 'use client';
2
+
3
+ import { Box, Flex, Typo } from '../../../atoms';
4
+ import {
5
+ formatTimestamp,
6
+ SPEECH_OVERTALK_THRESHOLD,
7
+ type SpeechShare,
8
+ } from '../utils/session-utils';
9
+ import type { RpSessionScriptLine } from '../types';
10
+
11
+ export interface ScriptTranscriptProps {
12
+ lines: RpSessionScriptLine[];
13
+ partnerName: string | null;
14
+ speechShare: SpeechShare | null;
15
+ canSync: boolean;
16
+ currentMs: number;
17
+ onSeek: (startMs: number) => void;
18
+ }
19
+
20
+ function SpeechShareBar({ share }: { share: SpeechShare }): React.ReactElement {
21
+ const isOvertalking = share.userPct > SPEECH_OVERTALK_THRESHOLD;
22
+ return (
23
+ <Box>
24
+ <Flex align="center" justify="between" mb="1">
25
+ <Typo
26
+ as="span"
27
+ style={{
28
+ fontSize: '0.75rem',
29
+ color: isOvertalking ? 'var(--red-11)' : 'var(--accent-11)',
30
+ }}
31
+ variant="body"
32
+ weight="medium"
33
+ >
34
+ 내 발화량 {share.userPct}%
35
+ </Typo>
36
+ <Typo as="span" color="gray" style={{ fontSize: '0.75rem' }} variant="body">
37
+ 상대 {share.partnerPct}%
38
+ </Typo>
39
+ </Flex>
40
+ <Flex
41
+ style={{
42
+ height: 6,
43
+ borderRadius: 'var(--radius-1)',
44
+ overflow: 'hidden',
45
+ background: 'var(--gray-4)',
46
+ }}
47
+ >
48
+ <Box
49
+ style={{
50
+ width: `${share.userPct}%`,
51
+ background: isOvertalking ? 'var(--red-9)' : 'var(--accent-9)',
52
+ }}
53
+ />
54
+ </Flex>
55
+ {isOvertalking ? (
56
+ <Typo
57
+ as="p"
58
+ mt="1"
59
+ style={{ fontSize: '0.75rem', color: 'var(--red-11)' }}
60
+ variant="body"
61
+ >
62
+ 상대의 말을 더 듣고 질문·경청의 비중을 높여 보세요
63
+ </Typo>
64
+ ) : null}
65
+ </Box>
66
+ );
67
+ }
68
+
69
+ export function ScriptTranscript({
70
+ lines,
71
+ partnerName,
72
+ speechShare,
73
+ canSync,
74
+ currentMs,
75
+ onSeek,
76
+ }: ScriptTranscriptProps): React.ReactElement {
77
+ const activeIndex = canSync
78
+ ? lines.reduce((acc, line, i) => (line.startMs <= currentMs ? i : acc), -1)
79
+ : -1;
80
+
81
+ return (
82
+ <Flex direction="column" gap="2" style={{ minWidth: 0 }}>
83
+ <Box>
84
+ <Flex align="center" gap="2" justify="between">
85
+ <Typo as="span" variant="body" weight="bold">
86
+ 대화 스크립트
87
+ </Typo>
88
+ <Typo
89
+ as="span"
90
+ color="gray"
91
+ style={{ fontSize: '0.75rem', flexShrink: 0 }}
92
+ variant="body"
93
+ >
94
+ {canSync ? '재생 위치에 맞춰 표시' : '타임라인 비동기'}
95
+ </Typo>
96
+ </Flex>
97
+ {partnerName ? (
98
+ <Typo as="p" color="gray" mt="1" style={{ fontSize: '0.75rem' }} variant="body">
99
+ 상대 · {partnerName}
100
+ </Typo>
101
+ ) : null}
102
+ {speechShare ? (
103
+ <Box mt="2">
104
+ <SpeechShareBar share={speechShare} />
105
+ </Box>
106
+ ) : null}
107
+ </Box>
108
+
109
+ {lines.length === 0 ? (
110
+ <Typo as="p" color="gray" variant="body">
111
+ 대화 스크립트가 없습니다.
112
+ </Typo>
113
+ ) : (
114
+ <Box style={{ maxHeight: 440, overflowY: 'auto' }}>
115
+ <Flex direction="column" gap="1">
116
+ {lines.map((line, i) => {
117
+ const isUser = line.speaker === 'user';
118
+ const isActive = i === activeIndex;
119
+ return (
120
+ <Flex
121
+ align="start"
122
+ gap="3"
123
+ key={`${line.startMs}-${line.speaker}`}
124
+ onClick={
125
+ canSync
126
+ ? () => {
127
+ onSeek(line.startMs);
128
+ }
129
+ : undefined
130
+ }
131
+ px="2"
132
+ py="2"
133
+ style={{
134
+ cursor: canSync ? 'pointer' : 'default',
135
+ borderRadius: 'var(--radius-2)',
136
+ background: isActive ? 'var(--accent-a3)' : 'transparent',
137
+ }}
138
+ >
139
+ {canSync ? (
140
+ <Typo
141
+ as="span"
142
+ mono
143
+ style={{
144
+ color: isActive ? 'var(--accent-11)' : 'var(--gray-9)',
145
+ fontSize: '0.75rem',
146
+ flexShrink: 0,
147
+ minWidth: 32,
148
+ lineHeight: '1.7',
149
+ }}
150
+ variant="body"
151
+ >
152
+ {formatTimestamp(line.startMs)}
153
+ </Typo>
154
+ ) : null}
155
+ <Typo
156
+ as="span"
157
+ style={{
158
+ color: isUser ? 'var(--accent-11)' : 'var(--gray-11)',
159
+ fontSize: '0.8125rem',
160
+ flexShrink: 0,
161
+ minWidth: 28,
162
+ lineHeight: '1.7',
163
+ fontWeight: 700,
164
+ whiteSpace: 'nowrap',
165
+ }}
166
+ variant="body"
167
+ >
168
+ {isUser ? '나' : '상대'}
169
+ </Typo>
170
+ <Typo
171
+ as="span"
172
+ style={{
173
+ fontSize: '0.875rem',
174
+ lineHeight: '1.7',
175
+ color: isActive ? 'var(--gray-12)' : 'var(--gray-11)',
176
+ }}
177
+ variant="body"
178
+ weight={isActive ? 'medium' : undefined}
179
+ >
180
+ {line.text}
181
+ </Typo>
182
+ </Flex>
183
+ );
184
+ })}
185
+ </Flex>
186
+ </Box>
187
+ )}
188
+ </Flex>
189
+ );
190
+ }