Skip to content

Commit 1c81c4f

Browse files
committed
feat: 릴리즈 노트 포맷팅 개선
- 릴리즈 노트를 HTML 태그에서 마크다운 스타일로 변환하는 로직을 개선하여 가독성을 향상 - 다양한 마크다운 요소(헤더, 리스트, 볼드체, 이탤릭체 등)를 지원하는 새로운 포맷팅 방식 구현 - 텍스트 요소의 스타일을 조정하여 일관된 UI 제공
1 parent a87d26c commit 1c81c4f

File tree

1 file changed

+131
-28
lines changed

1 file changed

+131
-28
lines changed

src/renderer/components/UpdateNotification.tsx

Lines changed: 131 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -84,36 +84,139 @@ const UpdateNotification: React.FC = () => {
8484

8585
const formatReleaseNotes = (releaseNotes: string): React.ReactNode => {
8686
try {
87-
// HTML 태그가 있는지 확인
88-
const hasHtmlTags = /<[^>]*>/g.test(releaseNotes);
89-
90-
if (hasHtmlTags) {
91-
// HTML 태그 제거하고 마크다운 스타일로 변환
92-
let formatted = releaseNotes
93-
.replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, '## $1\n')
94-
.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**')
95-
.replace(/<b[^>]*>(.*?)<\/b>/gi, '**$1**')
96-
.replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*')
97-
.replace(/<i[^>]*>(.*?)<\/i>/gi, '*$1*')
98-
.replace(/<ul[^>]*>/gi, '')
99-
.replace(/<\/ul>/gi, '')
100-
.replace(/<li[^>]*>/gi, '• ')
101-
.replace(/<\/li>/gi, '\n')
102-
.replace(/<br\s*\/?>/gi, '\n')
103-
.replace(/<p[^>]*>/gi, '')
104-
.replace(/<\/p>/gi, '\n\n')
105-
.replace(/<[^>]*>/g, '') // 남은 모든 HTML 태그 제거
106-
.replace(/&nbsp;/gi, ' ')
107-
.replace(/&lt;/gi, '<')
108-
.replace(/&gt;/gi, '>')
109-
.replace(/&amp;/gi, '&')
110-
.trim();
87+
const lines = releaseNotes.split('\n');
88+
const elements: React.ReactNode[] = [];
89+
let listItems: string[] = [];
90+
let key = 0;
91+
92+
const flushList = () => {
93+
if (listItems.length > 0) {
94+
elements.push(
95+
<ul key={key++} style={{ margin: '8px 0', paddingLeft: '16px' }}>
96+
{listItems.map((item, index) => (
97+
<li key={index} style={{ marginBottom: '4px' }}>
98+
<Text style={{ fontSize: 13 }}>{parseInlineMarkdown(item)}</Text>
99+
</li>
100+
))}
101+
</ul>
102+
);
103+
listItems = [];
104+
}
105+
};
106+
107+
const parseInlineMarkdown = (text: string): React.ReactNode => {
108+
const parts: React.ReactNode[] = [];
109+
let currentText = text;
110+
let partKey = 0;
111+
112+
// Bold (**text**)
113+
currentText = currentText.replace(/\*\*(.+?)\*\*/g, (_, content) => {
114+
const placeholder = `__BOLD_${partKey}__`;
115+
parts[partKey] = <Text key={partKey} strong style={{ fontSize: 13 }}>{content}</Text>;
116+
partKey++;
117+
return placeholder;
118+
});
119+
120+
// Italic (*text* or _text_)
121+
currentText = currentText.replace(/(?<!\*)\*([^*]+?)\*(?!\*)/g, (_, content) => {
122+
const placeholder = `__ITALIC_${partKey}__`;
123+
parts[partKey] = <Text key={partKey} italic style={{ fontSize: 13 }}>{content}</Text>;
124+
partKey++;
125+
return placeholder;
126+
});
127+
128+
currentText = currentText.replace(/_([^_]+?)_/g, (_, content) => {
129+
const placeholder = `__ITALIC_${partKey}__`;
130+
parts[partKey] = <Text key={partKey} italic style={{ fontSize: 13 }}>{content}</Text>;
131+
partKey++;
132+
return placeholder;
133+
});
134+
135+
// Split by placeholders and combine
136+
const finalParts: React.ReactNode[] = [];
137+
const segments = currentText.split(/(__(?:BOLD|ITALIC)_\d+__)/);
138+
139+
segments.forEach((segment, index) => {
140+
const match = segment.match(/^__(?:BOLD|ITALIC)_(\d+)__$/);
141+
if (match) {
142+
const partIndex = parseInt(match[1]);
143+
finalParts.push(parts[partIndex]);
144+
} else if (segment) {
145+
finalParts.push(<span key={`text_${index}`}>{segment}</span>);
146+
}
147+
});
148+
149+
return finalParts.length > 1 ? <>{finalParts}</> : finalParts[0] || text;
150+
};
151+
152+
for (const line of lines) {
153+
const trimmedLine = line.trim();
111154

112-
return <Text style={{ fontSize: 13, fontFamily: 'inherit' }}>{formatted}</Text>;
155+
if (!trimmedLine) {
156+
flushList();
157+
continue;
158+
}
159+
160+
// Headers
161+
if (trimmedLine.startsWith('# ')) {
162+
flushList();
163+
elements.push(
164+
<Title key={key++} level={1} style={{ fontSize: 18, margin: '16px 0 8px 0', color: '#1890ff' }}>
165+
{trimmedLine.slice(2)}
166+
</Title>
167+
);
168+
} else if (trimmedLine.startsWith('## ')) {
169+
flushList();
170+
elements.push(
171+
<Title key={key++} level={2} style={{ fontSize: 16, margin: '12px 0 6px 0', color: '#1890ff' }}>
172+
{trimmedLine.slice(3)}
173+
</Title>
174+
);
175+
} else if (trimmedLine.startsWith('### ')) {
176+
flushList();
177+
elements.push(
178+
<Title key={key++} level={3} style={{ fontSize: 14, margin: '10px 0 4px 0', color: '#1890ff' }}>
179+
{trimmedLine.slice(4)}
180+
</Title>
181+
);
182+
} else if (trimmedLine.startsWith('#### ')) {
183+
flushList();
184+
elements.push(
185+
<Title key={key++} level={4} style={{ fontSize: 13, margin: '8px 0 4px 0', color: '#1890ff' }}>
186+
{trimmedLine.slice(5)}
187+
</Title>
188+
);
189+
}
190+
// Horizontal rule
191+
else if (trimmedLine === '---' || trimmedLine === '***') {
192+
flushList();
193+
elements.push(
194+
<hr key={key++} style={{
195+
border: 'none',
196+
borderTop: '1px solid #f0f0f0',
197+
margin: '16px 0'
198+
}} />
199+
);
200+
}
201+
// List items
202+
else if (trimmedLine.startsWith('- ') || trimmedLine.startsWith('* ')) {
203+
listItems.push(trimmedLine.slice(2));
204+
}
205+
// Regular text
206+
else {
207+
flushList();
208+
elements.push(
209+
<Text key={key++} style={{ fontSize: 13, display: 'block', marginBottom: '8px' }}>
210+
{parseInlineMarkdown(trimmedLine)}
211+
</Text>
212+
);
213+
}
113214
}
114-
115-
// 일반 텍스트인 경우 그대로 표시
116-
return <Text style={{ fontSize: 13, fontFamily: 'inherit' }}>{releaseNotes}</Text>;
215+
216+
// Flush any remaining list items
217+
flushList();
218+
219+
return <div style={{ lineHeight: 1.6 }}>{elements}</div>;
117220
} catch (error) {
118221
console.error('릴리즈 노트 포맷팅 오류:', error);
119222
return <Text style={{ fontSize: 13, fontFamily: 'inherit' }}>{releaseNotes}</Text>;

0 commit comments

Comments
 (0)