2222@ RequiredArgsConstructor
2323public class BadgeService {
2424
25- private static final String GITHUB_LOGO_PATH = "M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.137 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" ;
26- private static final String FONT_IMPORT_CSS = "@import url('https://fonts.googleapis.com/css2?&family=Noto+Sans+KR:wght@400;500;700&family=Outfit:wght@400;500;700;900&display=swap');" ;
27-
2825 private final UserRepository userRepository ;
2926 private final ActivityLogRepository activityLogRepository ;
27+ private final SvgBadgeRenderer svgBadgeRenderer ;
3028
3129 @ Transactional (readOnly = true )
3230 public String generateBadge (String nodeId ) {
@@ -41,266 +39,32 @@ public String generateBadge(String nodeId) {
4139 .with ("target_username" , user .getUsername ())
4240 .info ();
4341
44- return createSvgContent (user , user .getTier (), activityLog );
42+ return svgBadgeRenderer . render (user , user .getTier (), activityLog );
4543 }
4644
4745 @ Transactional (readOnly = true )
4846 public String generateBadgeByTier (Tier tier ) {
49- User user = User .builder ().nodeId ("preview" ).username (tier .toString ()).build ();
47+ User user = User .builder ()
48+ .nodeId ("preview" )
49+ .username (tier .toString ())
50+ .build ();
5051 user .updateScore (Score .of (12345 ));
5152 user .updateRankInfo (RankInfo .of (1 , 0.1 , 12345 ));
5253
5354 ActivityLog activityLog = ActivityLog .builder ()
54- .user (user ).commitCount (150 ).prCount (30 ).mergedPrCount (25 ).issueCount (10 ).reviewCount (45 )
55- .diffCommitCount (12 ).diffPrCount (0 ).diffMergedPrCount (0 ).diffIssueCount (2 ).diffReviewCount (8 ).build ();
56- return createSvgContent (user , tier , activityLog );
57- }
58-
59- private String createSvgContent (User user , Tier tier , ActivityLog activityLog ) {
60- String gradientDefs = getTierGradientDefs (tier );
61-
62- String displayTierName = tier .name ().charAt (0 ) + tier .name ().substring (1 ).toLowerCase ();
63-
64- int tierFontSize = 32 ;
65- if (displayTierName .length () > 9 ) {
66- tierFontSize = 26 ;
67- } else if (displayTierName .length () > 6 ) {
68- tierFontSize = 30 ;
69- }
70-
71- return String .format ("""
72- <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
73- <svg width="350" height="170" viewBox="0 0 350 170" fill="none" role="img" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
74- <defs>
75- %s
76- <clipPath id="card-clip">
77- <rect x="0" y="0" width="350" height="170" rx="12" ry="12"/>
78- </clipPath>
79-
80- <linearGradient id="static-gloss" x1="0%%" y1="0%%" x2="0%%" y2="100%%">
81- <stop offset="0%%" style="stop-color:#ffffff;stop-opacity:0.2" />
82- <stop offset="100%%" style="stop-color:#ffffff;stop-opacity:0" />
83- </linearGradient>
84-
85- <linearGradient id="soft-shine-gradient" x1="0%%" y1="0%%" x2="100%%" y2="0%%">
86- <stop offset="0%%" style="stop-color:#ffffff;stop-opacity:0" />
87- <stop offset="50%%" style="stop-color:#ffffff;stop-opacity:0.4" />
88- <stop offset="100%%" style="stop-color:#ffffff;stop-opacity:0" />
89- </linearGradient>
90- </defs>
91-
92- <style type="text/css">
93- <![CDATA[
94- %s
95-
96- @keyframes soft-pass {
97- 0%% { transform: translateX(-400px) skewX(-25deg); }
98- 50%% { transform: translateX(-400px) skewX(-35deg); }
99- 100%% { transform: translateX(500px) skewX(-35deg); }
100- }
101-
102- .shine-bar {
103- animation: soft-pass 5s infinite ease-in-out;
104- opacity: 0.20;
105- }
106-
107- text {
108- fill: #ffffff;
109- text-rendering: geometricPrecision;
110- -webkit-font-smoothing: antialiased;
111- }
112-
113- .text-shadow { filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.5)); }
114- .text-shadow-strong { filter: drop-shadow(0px 2px 4px rgba(0, 0, 0, 0.7)); }
115-
116- .header {
117- font-family: 'Noto Sans KR', sans-serif;
118- font-weight: 700;
119- font-size: 12px;
120- letter-spacing: 0px;
121- }
122-
123- .username {
124- font-family: 'Noto Sans KR', sans-serif;
125- font-weight: 500;
126- font-size: 13px;
127- opacity: 0.95;
128- }
129-
130- .stat-label {
131- font-family: 'Noto Sans KR', sans-serif;
132- font-size: 11px;
133- opacity: 0.85;
134- letter-spacing: 0.5px;
135- font-weight: 700;
136- }
137-
138- .stat-value {
139- font-family: 'Noto Sans KR', sans-serif;
140- font-weight: 700;
141- font-size: 13px;
142- }
143-
144- .tier-text {
145- font-family: 'Noto Sans KR', sans-serif;
146- font-weight: 500;
147- font-size: %dpx;
148- letter-spacing: 0.5px;
149- }
150-
151- .score-text {
152- font-family: 'Noto Sans KR', sans-serif;
153- font-weight: 700;
154- font-size: 22px;
155- letter-spacing: 0px;
156- }
157-
158- .rank-text {
159- font-family: 'Noto Sans KR', sans-serif;
160- font-size: 11px;
161- font-weight: 500;
162- opacity: 0.9;
163- }
164-
165- .diff-plus { fill: #4ADE80; font-weight: 700; font-size: 11px; font-family: 'Noto Sans KR', sans-serif; }
166- .diff-minus { fill: #FF6B6B; font-weight: 700; font-size: 11px; font-family: 'Noto Sans KR', sans-serif; }
167- ]]>
168- </style>
169-
170- <rect x="0" y="0" width="350" height="170" rx="12" ry="12" fill="url(#tierGradient)" shape-rendering="geometricPrecision" />
171-
172- <g clip-path="url(#card-clip)">
173- <path d="%s" fill="white" fill-opacity="0.08" transform="translate(200, -20) scale(9)" shape-rendering="geometricPrecision"/>
174-
175- <rect x="0" y="0" width="350" height="85" fill="url(#static-gloss)" />
176- <rect class="shine-bar" x="0" y="-30" width="200" height="230" fill="url(#soft-shine-gradient)" />
177- </g>
178-
179- <text x="20" y="28" class="base-text header text-shadow">Git Ranker</text>
180- <text x="330" y="28" text-anchor="end" class="base-text username text-shadow">@%s</text>
181- <line x1="20" y1="40" x2="330" y2="40" stroke="#ffffff" stroke-width="1" stroke-opacity="0.4" shape-rendering="crispEdges"/>
182-
183- <g transform="translate(20, 85)">
184- <text x="0" y="0" class="base-text tier-text text-shadow-strong">%s</text>
185- <text x="0" y="30" class="mono-text score-text text-shadow">%s pts</text>
186- <text x="0" y="52" class="base-text rank-text text-shadow">Top %.2f%% • Rank %s</text>
187- </g>
188-
189- <line x1="165" y1="55" x2="165" y2="155" stroke="#ffffff" stroke-width="1" stroke-opacity="0.3" shape-rendering="crispEdges"/>
190-
191- <g transform="translate(180, 60)">
192- <g transform="translate(0, 0)">
193- <text x="0" y="0" class="base-text stat-label">Commits</text>
194- <text x="0" y="18" class="mono-text stat-value text-shadow">%s %s</text>
195- </g>
196- <g transform="translate(85, 0)">
197- <text x="0" y="0" class="base-text stat-label">Issues</text>
198- <text x="0" y="18" class="mono-text stat-value text-shadow">%s %s</text>
199- </g>
200- <g transform="translate(0, 34)">
201- <text x="0" y="0" class="base-text stat-label">PR Open</text>
202- <text x="0" y="18" class="mono-text stat-value text-shadow">%s %s</text>
203- </g>
204- <g transform="translate(85, 34)">
205- <text x="0" y="0" class="base-text stat-label">PR Merged</text>
206- <text x="0" y="18" class="mono-text stat-value text-shadow">%s %s</text>
207- </g>
208- <g transform="translate(0, 68)">
209- <text x="0" y="0" class="base-text stat-label">Reviews</text>
210- <text x="0" y="18" class="mono-text stat-value text-shadow">%s %s</text>
211- </g>
212- </g>
213- </svg>
214- """ ,
215- gradientDefs ,
216- FONT_IMPORT_CSS ,
217- tierFontSize ,
218- GITHUB_LOGO_PATH ,
219- user .getUsername (),
220- displayTierName ,
221- formatNumber (user .getTotalScore ()),
222- user .getPercentile (),
223- formatNumber (user .getRanking ()),
224-
225- formatCount (activityLog .getCommitCount ()), formatDiff (activityLog .getDiffCommitCount ()),
226- formatCount (activityLog .getIssueCount ()), formatDiff (activityLog .getDiffIssueCount ()),
227- formatCount (activityLog .getPrCount ()), formatDiff (activityLog .getDiffPrCount ()),
228- formatCount (activityLog .getMergedPrCount ()), formatDiff (activityLog .getDiffMergedPrCount ()),
229- formatCount (activityLog .getReviewCount ()), formatDiff (activityLog .getDiffReviewCount ())
230- );
231- }
232-
233- private String formatNumber (long number ) {
234- return String .format ("%,d" , number );
235- }
236-
237- private String formatCount (int count ) {
238- return String .format ("%,d" , count );
239- }
240-
241- private String formatDiff (int diff ) {
242- if (diff > 0 ) return String .format ("<tspan class='diff-plus' dy='-1'>+%d</tspan>" , diff );
243- if (diff < 0 ) return String .format ("<tspan class='diff-minus' dy='-1'>-%d</tspan>" , Math .abs (diff ));
244- return "" ;
245- }
246-
247- private String getTierGradientDefs (Tier tier ) {
248- String color1 , color2 , color3 ;
249-
250- switch (tier ) {
251- case CHALLENGER -> {
252- color1 = "#09203F" ;
253- color2 = "#3B82F6" ;
254- color3 = "#D4AF37" ;
255- }
256- case MASTER -> {
257- color1 = "#2E1065" ;
258- color2 = "#7C3AED" ;
259- color3 = "#F472B6" ;
260- }
261- case DIAMOND -> {
262- color1 = "#0C4A6E" ;
263- color2 = "#0284C7" ;
264- color3 = "#7DD3FC" ;
265- }
266- case EMERALD -> {
267- color1 = "#064E3B" ;
268- color2 = "#059669" ;
269- color3 = "#34D399" ;
270- }
271- case PLATINUM -> {
272- color1 = "#1E293B" ;
273- color2 = "#0F766E" ;
274- color3 = "#2DD4BF" ;
275- }
276- case GOLD -> {
277- color1 = "#8E6310" ;
278- color2 = "#C2971F" ;
279- color3 = "#F4D03F" ;
280- }
281- case SILVER -> {
282- color1 = "#111827" ;
283- color2 = "#4B5563" ;
284- color3 = "#9CA3AF" ;
285- }
286- case BRONZE -> {
287- color1 = "#431407" ;
288- color2 = "#92400E" ;
289- color3 = "#D97706" ;
290- }
291- default -> {
292- color1 = "#0F172A" ;
293- color2 = "#334155" ;
294- color3 = "#64748B" ;
295- }
296- }
297-
298- return String .format ("""
299- <linearGradient id="tierGradient" x1="0%%" y1="0%%" x2="100%%" y2="100%%">
300- <stop offset="0%%" style="stop-color:%s;stop-opacity:1" />
301- <stop offset="50%%" style="stop-color:%s;stop-opacity:1" />
302- <stop offset="100%%" style="stop-color:%s;stop-opacity:1" />
303- </linearGradient>
304- """ , color1 , color2 , color3 );
55+ .user (user )
56+ .commitCount (150 )
57+ .prCount (30 )
58+ .mergedPrCount (25 )
59+ .issueCount (10 )
60+ .reviewCount (45 )
61+ .diffCommitCount (12 )
62+ .diffPrCount (0 )
63+ .diffMergedPrCount (0 )
64+ .diffIssueCount (2 )
65+ .diffReviewCount (8 )
66+ .build ();
67+
68+ return svgBadgeRenderer .render (user , tier , activityLog );
30569 }
306- }
70+ }
0 commit comments