@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.
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 +5715 -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 +4687 -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 +3687 -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 +167 -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 +5712 -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-32WIS4L7.js +131 -0
  123. package/dist/chunk-32WIS4L7.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-TFJJKM3W.js +261 -0
  154. package/dist/chunk-TFJJKM3W.js.map +1 -0
  155. package/dist/{chunk-KQ3ME6XY.js → chunk-U2EAXN3Y.js} +4 -4
  156. package/dist/chunk-UXAEOCOH.js +150 -0
  157. package/dist/chunk-UXAEOCOH.js.map +1 -0
  158. package/dist/chunk-YHP75Y5K.js +242 -0
  159. package/dist/chunk-YHP75Y5K.js.map +1 -0
  160. package/dist/chunk-ZE36DFOC.js +142 -0
  161. package/dist/chunk-ZE36DFOC.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 +265 -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,265 @@
1
+ 'use client';
2
+
3
+ import React, { useMemo } from 'react';
4
+ import { format } from 'date-fns';
5
+ import type { EChartsOption } from 'echarts';
6
+ import { Box, Flex, Heading, Typo } from '../../../atoms';
7
+ import {
8
+ ExpandTable,
9
+ createColumnHelper,
10
+ type ColumnDef,
11
+ type Row,
12
+ } from '../../../molecules';
13
+ import { Chart, useTokenColors } from '../chart/session-chart';
14
+ import type { RpSessionHistoryItem } from '../types';
15
+
16
+ // 트렌드 테이블 날짜 셀은 시간 정보 없이 날짜만 표시.
17
+ function formatDateOnly(iso: string): string {
18
+ const d = new Date(iso);
19
+ if (Number.isNaN(d.getTime())) return iso;
20
+ return format(d, 'M/d');
21
+ }
22
+
23
+ /* ───────────────────────── Section ───────────────────────── */
24
+
25
+ export function Section({
26
+ title,
27
+ desc,
28
+ children,
29
+ }: {
30
+ title: string;
31
+ desc?: string;
32
+ children: React.ReactNode;
33
+ }): React.ReactElement {
34
+ return (
35
+ <section>
36
+ <Box mb="3">
37
+ <Heading
38
+ size={{ initial: '4', md: '5' }}
39
+ style={{ textWrap: 'balance' }}
40
+ weight="bold"
41
+ >
42
+ {title}
43
+ </Heading>
44
+ {desc ? (
45
+ <Box mt="1" style={{ maxWidth: '65ch' }}>
46
+ <Typo as="p" color="gray" variant="body">
47
+ {desc}
48
+ </Typo>
49
+ </Box>
50
+ ) : null}
51
+ </Box>
52
+ {children}
53
+ </section>
54
+ );
55
+ }
56
+
57
+ /* ───────────────────────── 타입 ───────────────────────── */
58
+
59
+ type TrendRow = RpSessionHistoryItem & {
60
+ delta: number | null;
61
+ isCurrent: boolean;
62
+ };
63
+
64
+ /* ───────────────────────── 테이블 셀 컴포넌트 ───────────────────────── */
65
+
66
+ function AttemptCell({ row }: { row: Row<TrendRow> }): React.ReactElement {
67
+ const d = row.original;
68
+ return (
69
+ <Typo
70
+ as="span"
71
+ style={{ color: d.isCurrent ? 'var(--gray-12)' : 'var(--gray-11)' }}
72
+ variant="body"
73
+ weight={d.isCurrent ? 'bold' : 'regular'}
74
+ >
75
+ {d.trainingSessionNum}회차{d.isCurrent ? ' (현재)' : null}
76
+ </Typo>
77
+ );
78
+ }
79
+
80
+ function DateCell({ row }: { row: Row<TrendRow> }): React.ReactElement {
81
+ return (
82
+ <Typo as="span" mono style={{ color: 'var(--gray-9)' }} variant="body">
83
+ {formatDateOnly(row.original.createdAt)}
84
+ </Typo>
85
+ );
86
+ }
87
+
88
+ function ScoreCell({ row }: { row: Row<TrendRow> }): React.ReactElement {
89
+ return (
90
+ <Typo
91
+ as="span"
92
+ mono
93
+ variant="body"
94
+ weight={row.original.isCurrent ? 'bold' : 'regular'}
95
+ >
96
+ {row.original.totalScore ?? '—'}
97
+ </Typo>
98
+ );
99
+ }
100
+
101
+ function DeltaCell({ row }: { row: Row<TrendRow> }): React.ReactElement {
102
+ const { delta } = row.original;
103
+ if (delta === null) {
104
+ return (
105
+ <Typo as="span" mono style={{ color: 'var(--gray-9)' }} variant="body">
106
+
107
+ </Typo>
108
+ );
109
+ }
110
+ return (
111
+ <Typo as="span" mono style={{ color: 'var(--gray-9)' }} variant="body">
112
+ {delta > 0 ? `+${delta}` : `${delta}`}
113
+ </Typo>
114
+ );
115
+ }
116
+
117
+ /* ───────────────────────── ScoreTrendSection ───────────────────────── */
118
+
119
+ export interface ScoreTrendSectionProps {
120
+ history: RpSessionHistoryItem[];
121
+ }
122
+
123
+ export function ScoreTrendSection({
124
+ history,
125
+ }: ScoreTrendSectionProps): React.ReactElement {
126
+ const { colors, probe } = useTokenColors({
127
+ accent: ['--accent-9', '#5b5bd6'],
128
+ accentText: ['--accent-11', '#5753c6'],
129
+ grid: ['--gray-5', '#e0e0e0'],
130
+ gridSoft: ['--gray-4', '#e8e8e8'],
131
+ muted: ['--gray-9', '#8d8d8d'],
132
+ });
133
+
134
+ // 총점 없는 회차 제외, 회차 오름차순 정렬, 최근 5회차로 한정
135
+ const ordered = useMemo(
136
+ () =>
137
+ [...history]
138
+ .filter((e) => e.totalScore !== null)
139
+ .sort((a, b) => a.trainingSessionNum - b.trainingSessionNum)
140
+ .slice(-5),
141
+ [history],
142
+ );
143
+
144
+ const scores = ordered.map((e) => e.totalScore ?? 0);
145
+ const minScore = Math.max(0, Math.min(...scores) - 10);
146
+ const maxScore = Math.min(100, Math.max(...scores) + 10);
147
+ const labels = ordered.map((e) => `${e.trainingSessionNum}회차`);
148
+ const lastIndex = ordered.length - 1;
149
+
150
+ const option: EChartsOption = {
151
+ tooltip: { trigger: 'axis', valueFormatter: (v) => `${String(v)}점` },
152
+ grid: { left: 36, right: 16, top: 20, bottom: 24, containLabel: false },
153
+ xAxis: {
154
+ type: 'category',
155
+ data: labels,
156
+ boundaryGap: true,
157
+ axisLine: { lineStyle: { color: colors.grid } },
158
+ axisTick: { show: false },
159
+ axisLabel: { color: colors.muted, fontSize: 10 }, // ds-allow: chart-lib
160
+ },
161
+ yAxis: {
162
+ type: 'value',
163
+ min: minScore,
164
+ max: maxScore,
165
+ splitLine: { lineStyle: { color: colors.gridSoft } },
166
+ axisLabel: { color: colors.muted, fontSize: 10 }, // ds-allow: chart-lib
167
+ },
168
+ series: [
169
+ {
170
+ type: 'line',
171
+ data: scores,
172
+ smooth: false,
173
+ showSymbol: true,
174
+ symbol: 'circle',
175
+ symbolSize: 6,
176
+ lineStyle: { color: colors.accent, width: 2 },
177
+ itemStyle: { color: colors.accent },
178
+ label: {
179
+ show: true,
180
+ position: 'top',
181
+ color: colors.accentText,
182
+ fontSize: 10, // ds-allow: chart-lib
183
+ fontWeight: 'bold', // ds-allow: chart-lib
184
+ formatter: (p) => (p.dataIndex === lastIndex ? String(p.value) : ''),
185
+ },
186
+ },
187
+ ],
188
+ };
189
+
190
+ const rows: TrendRow[] = ordered.map((e, i) => {
191
+ const prev = i > 0 ? (ordered[i - 1].totalScore ?? null) : null;
192
+ return {
193
+ ...e,
194
+ delta:
195
+ prev !== null && e.totalScore !== null ? e.totalScore - prev : null,
196
+ isCurrent: i === ordered.length - 1,
197
+ };
198
+ });
199
+
200
+ const columns = useMemo<ColumnDef<TrendRow>[]>(() => {
201
+ const h = createColumnHelper<TrendRow>();
202
+ return [
203
+ // autoSize 컬럼에선 size가 최소폭으로 동작(1fr로 늘어남). 모바일(iPhone SE 375px,
204
+ // 가용 ≈343px)에서 grid 최소폭 합이 화면을 넘지 않도록 작게 잡는다.
205
+ h.display({
206
+ id: 'attempt',
207
+ header: '회차',
208
+ size: 116,
209
+ meta: { align: 'left' },
210
+ cell: AttemptCell,
211
+ }),
212
+ h.display({
213
+ id: 'date',
214
+ header: '날짜',
215
+ size: 76,
216
+ meta: { align: 'left' },
217
+ cell: DateCell,
218
+ }),
219
+ h.display({
220
+ id: 'score',
221
+ header: '총점',
222
+ size: 64,
223
+ meta: { align: 'right' },
224
+ cell: ScoreCell,
225
+ }),
226
+ h.display({
227
+ id: 'delta',
228
+ header: '변화',
229
+ size: 64,
230
+ meta: { autoSize: true, align: 'left' },
231
+ cell: DeltaCell,
232
+ }),
233
+ ];
234
+ }, []);
235
+
236
+ return (
237
+ <Section
238
+ desc={`같은 시나리오 최근 ${ordered.length}회차 총점 변화`}
239
+ title="총점 추이"
240
+ >
241
+ <Flex direction="column" gap="4">
242
+ <div
243
+ aria-label="총점 추이 라인 차트"
244
+ role="img"
245
+ style={{ position: 'relative', width: '100%', height: 200 }}
246
+ >
247
+ {probe}
248
+ <Chart option={option} />
249
+ </div>
250
+ <Box className="rp-expand-table">
251
+ <ExpandTable
252
+ columns={columns}
253
+ data={rows}
254
+ getRowStyle={(d: TrendRow) =>
255
+ d.isCurrent ? { background: 'var(--accent-a2)' } : {}
256
+ }
257
+ showPagination={false}
258
+ slim
259
+ variant="sprite"
260
+ />
261
+ </Box>
262
+ </Flex>
263
+ </Section>
264
+ );
265
+ }
@@ -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
+ }