fix: shorten update notes and contain entry content in cards
This commit is contained in:
+1
-7
@@ -6,13 +6,7 @@ updatedAt: 2026-09-07T13:44:23+09:00
|
|||||||
|
|
||||||
### 리더 레이팅 곡 추천
|
### 리더 레이팅 곡 추천
|
||||||
|
|
||||||
`NEW` 상위 메뉴에 리더 레이팅 탭을 추가했습니다. 입력한 멤버 5명과 리더·의상·성장 설정으로 가창 대상 곡의 최적 배치를 계산해, 가창곡의 Hard·Expert를 각각 예상 고점 레이팅 순으로 최대 20개, 5개씩 확인할 수 있습니다. 가창 보너스는 모든 곡에 10%를 적용합니다. Hard·Expert 비교, 상위 3곡 합계, 계산 취소와 추천 곡의 상세 배치 확인을 지원합니다.
|
`NEW` 입력한 편성에 맞는 리더 가창곡을 레이팅순으로 최대 20개 추천합니다. 5개씩 보기, 상위 3곡 합계, 최고점·99%·95% 달성 확률과 상세 배치를 제공하며 병렬 계산을 지원합니다. 가창 보너스는 10%로 고정합니다.
|
||||||
|
|
||||||
리더와 분석 조건, 합산 레이팅과 상위 3곡을 요약해 보여주며, 모바일에서는 추천곡을 카드 형태로 확인할 수 있습니다. 계산 기준은 펼쳐서 확인할 수 있습니다.
|
|
||||||
|
|
||||||
추천곡마다 최고점 및 최고점의 99%·95% 이상 달성 확률을 표시합니다. 상세 배치에서도 같은 확률을 확인할 수 있으며, 최고점은 정확 계산, 99%·95% 이상은 25,000회 반복 계산을 사용합니다.
|
|
||||||
|
|
||||||
‘최대 컴퓨팅 자원 활용’을 켜면 기기의 논리 코어 수에 맞춰 여러 채보를 동시에 계산합니다. 동시 계산 수와 전체 진행률을 표시하며, 취소하거나 입력 조건을 바꾸면 모든 계산을 중단합니다.
|
|
||||||
|
|
||||||
### 배치 분석의 인게임 1노트 점수 표시
|
### 배치 분석의 인게임 1노트 점수 표시
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { renderReleaseMarkdown } from '../release-notes/markdown.ts';
|
||||||
|
|
||||||
|
test('업데이트의 모든 문단과 목록을 하나의 카드에 묶는다', () => {
|
||||||
|
const html = renderReleaseMarkdown('## 날짜\n\n### 기능\n\n첫 문단\n\n추가 문단\n\n- 목록\n\n### 다음 기능\n\n설명');
|
||||||
|
const cards = [...html.matchAll(/<section class="update-entry">([\s\S]*?)<\/section>/g)];
|
||||||
|
assert.equal(cards.length, 2);
|
||||||
|
assert.match(cards[0][1], /첫 문단[\s\S]*추가 문단[\s\S]*<ul>/);
|
||||||
|
assert.doesNotMatch(cards[0][1], /다음 기능/);
|
||||||
|
assert.match(cards[1][1], /다음 기능[\s\S]*설명/);
|
||||||
|
assert.ok(html.indexOf('<h2>') < html.indexOf('<section'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('구분선과 날짜는 카드 밖에 두고 원시 HTML은 계속 이스케이프한다', () => {
|
||||||
|
const html = renderReleaseMarkdown('### 기능\n\n<script>alert(1)</script>\n\n---\n\n## 다음 날짜\n\n### 새 기능\n\n[문서](https://example.com)');
|
||||||
|
assert.match(html, /<\/section>\s*<hr>/);
|
||||||
|
assert.match(html, /<hr>\s*<h2>다음 날짜<\/h2>\s*<section/);
|
||||||
|
assert.doesNotMatch(html, /<script>/);
|
||||||
|
assert.match(html, /<script>/);
|
||||||
|
assert.match(html, /href="https:\/\/example.com\/"/);
|
||||||
|
});
|
||||||
@@ -39,10 +39,26 @@ renderer.image = ({ href, title, text }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function renderReleaseMarkdown(markdown: string): string {
|
export function renderReleaseMarkdown(markdown: string): string {
|
||||||
return marked.parse(markdown, {
|
const options = {
|
||||||
async: false,
|
async: false,
|
||||||
breaks: true,
|
breaks: true,
|
||||||
gfm: true,
|
gfm: true,
|
||||||
renderer
|
renderer
|
||||||
}) as string;
|
} as const;
|
||||||
|
const tokens = marked.lexer(markdown, options);
|
||||||
|
let html = '';
|
||||||
|
let entryOpen = false;
|
||||||
|
for (const token of tokens) {
|
||||||
|
const boundary = token.type === 'hr' || (token.type === 'heading' && token.depth <= 3);
|
||||||
|
if (boundary && entryOpen) {
|
||||||
|
html += '</section>\n';
|
||||||
|
entryOpen = false;
|
||||||
|
}
|
||||||
|
if (token.type === 'heading' && token.depth === 3) {
|
||||||
|
html += '<section class="update-entry">\n';
|
||||||
|
entryOpen = true;
|
||||||
|
}
|
||||||
|
html += marked.parser([token], options);
|
||||||
|
}
|
||||||
|
return html + (entryOpen ? '</section>\n' : '');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,8 +42,12 @@
|
|||||||
:global(.markdown-body h2) { position: relative; margin: 0 0 14px; padding-top: 2px; color: #cad0d9; font-size: 15px; font-weight: 850; }
|
:global(.markdown-body h2) { position: relative; margin: 0 0 14px; padding-top: 2px; color: #cad0d9; font-size: 15px; font-weight: 850; }
|
||||||
:global(.markdown-body h2:not(:first-child)) { margin-top: 38px; }
|
:global(.markdown-body h2:not(:first-child)) { margin-top: 38px; }
|
||||||
:global(.markdown-body h2::before) { position: absolute; top: 6px; left: -34px; width: 13px; height: 13px; border: 3px solid #101319; border-radius: 50%; background: #ed8bb5; box-shadow: 0 0 0 1px rgb(237 139 181 / 45%); content: ''; }
|
:global(.markdown-body h2::before) { position: absolute; top: 6px; left: -34px; width: 13px; height: 13px; border: 3px solid #101319; border-radius: 50%; background: #ed8bb5; box-shadow: 0 0 0 1px rgb(237 139 181 / 45%); content: ''; }
|
||||||
:global(.markdown-body h3) { margin: 10px 0 0; padding: 19px 20px 0; border: 1px solid #2b3441; border-bottom: 0; border-radius: 12px 12px 0 0; color: #edf0f4; background: #151a22; font-size: 17px; letter-spacing: -.02em; }
|
:global(.markdown-body .update-entry) { min-width: 0; margin: 10px 0; padding: 19px 20px; border: 1px solid #2b3441; border-radius: 12px; background: #151a22; overflow-wrap: anywhere; }
|
||||||
:global(.markdown-body h3 + p) { margin: 0 0 10px; padding: 8px 20px 19px; border: 1px solid #2b3441; border-top: 0; border-radius: 0 0 12px 12px; color: #9ca6b4; background: #151a22; font-size: 14px; line-height: 1.65; }
|
:global(.markdown-body h3) { margin: 0; color: #edf0f4; font-size: 17px; letter-spacing: -.02em; }
|
||||||
|
:global(.markdown-body .update-entry p) { margin: 8px 0 0; color: #9ca6b4; font-size: 14px; line-height: 1.65; }
|
||||||
|
:global(.markdown-body .update-entry > :last-child) { margin-bottom: 0; }
|
||||||
|
:global(.markdown-body img) { max-width: 100%; height: auto; }
|
||||||
|
:global(.markdown-body pre) { max-width: 100%; overflow-x: auto; }
|
||||||
:global(.markdown-body code) { margin-right: 7px; padding: 3px 7px; border: 1px solid rgb(237 139 181 / 32%); border-radius: 999px; color: #efa0c1; background: rgb(237 139 181 / 9%); font-family: inherit; font-size: 10px; font-weight: 900; }
|
:global(.markdown-body code) { margin-right: 7px; padding: 3px 7px; border: 1px solid rgb(237 139 181 / 32%); border-radius: 999px; color: #efa0c1; background: rgb(237 139 181 / 9%); font-family: inherit; font-size: 10px; font-weight: 900; }
|
||||||
:global(.markdown-body hr) { height: 1px; margin: 38px 0; border: 0; background: #2c3440; }
|
:global(.markdown-body hr) { height: 1px; margin: 38px 0; border: 0; background: #2c3440; }
|
||||||
:global(.markdown-body ul), :global(.markdown-body ol) { color: #9ca6b4; line-height: 1.7; }
|
:global(.markdown-body ul), :global(.markdown-body ol) { color: #9ca6b4; line-height: 1.7; }
|
||||||
@@ -54,7 +58,6 @@
|
|||||||
.delivery-note { margin-bottom: 30px; }
|
.delivery-note { margin-bottom: 30px; }
|
||||||
.markdown-body { padding-left: 27px; }
|
.markdown-body { padding-left: 27px; }
|
||||||
:global(.markdown-body h2::before) { left: -27px; }
|
:global(.markdown-body h2::before) { left: -27px; }
|
||||||
:global(.markdown-body h3) { padding: 17px 17px 0; }
|
:global(.markdown-body .update-entry) { padding: 17px; }
|
||||||
:global(.markdown-body h3 + p) { padding: 8px 17px 17px; }
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user