Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@
## 2026-07-13 - 단일 패스 문자열 치환 최적화 (O(N) 단일 스캔 및 지연 할당)
**Learning:** `String.replace()`를 여러 번 체이닝하여 호출하면, 문자열 치환이 발생하지 않는 경우에도 내부적으로 불필요한 스캔이 중복 발생하고, 치환 시마다 새로운 문자열 객체와 char 배열이 할당되어 메모리 낭비와 성능 저하(GC 압박)가 발생한다.
**Action:** 여러 문자를 한 번에 치환해야 하는 경우, O(N) 단일 스캔을 통해 `charAt()`으로 문자를 확인하고, 치환이 실제로 필요한 경우에만 `StringBuilder`를 지연 할당(Lazy allocation)하여 성능을 최적화하고 불필요한 메모리 할당을 방지한다.
## 2026-07-14 - 뷰어 쉘 HTML 렌더링 성능 최적화
**Learning:** 크기가 큰 텍스트 블록에 대해 여러 번의 `.replace()` 메서드 체이닝을 사용할 경우, 호출할 때마다 새로운 문자열 객체가 생성되어 불필요한 메모리 할당이 발생합니다.
**Action:** 긴 문자열 템플릿에 동적 변수를 삽입해야 할 때는 정적인 템플릿 상수(static final)들로 분리한 뒤 문자열 연결(concatenation)을 사용하면, 컴파일 시 `StringConcatFactory`를 통해 최적화되어 요청당 객체 할당량을 크게 줄일 수 있습니다.
162 changes: 88 additions & 74 deletions src/main/java/com/clearfolio/viewer/controller/ViewerUiController.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,81 +59,95 @@ private static String escapeHtmlAttribute(String value) {
return HtmlUtils.htmlEscape(value);
}

private static String viewerShellHtml(String docId, String initialState) {
// ⚡ Bolt: Use static template parts to avoid chained replace()
// allocations on every request.

/** Part 1 of the viewer shell template. */
private static final String VIEWER_SHELL_PART_1 = """
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="referrer" content="no-referrer" />
<meta name="clearfolio-doc-id" content=\"""";

/** Part 2 of the viewer shell template. */
private static final String VIEWER_SHELL_PART_2 = """
" />
<meta name="clearfolio-initial-state" content=\"""";

/** Part 3 of the viewer shell template. */
private static final String VIEWER_SHELL_PART_3 = """
" />
<meta name="clearfolio-pdfjs-viewer-path" content=\"""";

/** Part 4 of the viewer shell template. */
private static final String VIEWER_SHELL_PART_4 = """
" />
<title>Clearfolio Viewer</title>
<link rel="stylesheet" href="/assets/viewer/viewer.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>

<header class="app-header" role="banner">
<div class="app-header__inner">
<div class="brand" aria-label="Clearfolio Viewer">
<span class="brand__name">Clearfolio Viewer</span>
</div>

<nav class="header-nav" aria-label="Viewer utilities">
<a class="header-nav__link" href="/healthz">Service status</a>
</nav>
</div>
</header>

<main id="main" class="app-main" tabindex="-1">
<h1 class="page-title">Document preview</h1>
<p class="page-subtitle" id="doc-meta">Preparing preview shell...</p>

<section class="panel" aria-labelledby="state-title">
<h2 id="state-title" class="panel__title">Preview status</h2>

<div id="live-status" class="status" role="status" aria-live="polite" aria-atomic="true">Loading...</div>

<div id="error" class="error" role="alert" hidden>
<h3 class="error__title" id="error-title" tabindex="-1">Unable to load preview</h3>
<p class="error__message" id="error-message"></p>
</div>

<div class="actions" aria-label="Actions">
<button type="button" class="btn btn-primary" id="retry-btn">Refresh</button>
<a class="btn btn-secondary" id="open-json-link" href="#" target="_blank" rel="noopener noreferrer" aria-label="Open JSON bootstrap in a new tab" hidden>Open JSON bootstrap</a>
</div>
</section>

<section class="panel" aria-labelledby="preview-title">
<h2 id="preview-title" class="panel__title">Preview</h2>

<div id="preview" class="preview" aria-busy="true">
<div class="skeleton" aria-hidden="true"></div>
<p class="help" id="preview-help">When ready, the converted artifact will appear here.</p>
</div>
</section>
</main>

<footer class="app-footer" role="contentinfo">
<div class="app-footer__inner">
<small>Copyright (c) 2026 by HYOSUNG. All rights reserved.</small>
</div>
</footer>

<script type="module" src="/assets/viewer/viewer.js"></script>
</body>
</html>
""";

private static String viewerShellHtml(final String docId, final String initialState) {
String docIdString = escapeHtmlAttribute(docId);
String template = """
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="referrer" content="no-referrer" />
<meta name="clearfolio-doc-id" content="{{DOC_ID}}" />
<meta name="clearfolio-initial-state" content="{{INITIAL_STATE}}" />
<meta name="clearfolio-pdfjs-viewer-path" content="{{PDFJS_VIEWER_PATH}}" />
<title>Clearfolio Viewer</title>
<link rel="stylesheet" href="/assets/viewer/viewer.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>

<header class="app-header" role="banner">
<div class="app-header__inner">
<div class="brand" aria-label="Clearfolio Viewer">
<span class="brand__name">Clearfolio Viewer</span>
</div>

<nav class="header-nav" aria-label="Viewer utilities">
<a class="header-nav__link" href="/healthz">Service status</a>
</nav>
</div>
</header>

<main id="main" class="app-main" tabindex="-1">
<h1 class="page-title">Document preview</h1>
<p class="page-subtitle" id="doc-meta">Preparing preview shell...</p>

<section class="panel" aria-labelledby="state-title">
<h2 id="state-title" class="panel__title">Preview status</h2>

<div id="live-status" class="status" role="status" aria-live="polite" aria-atomic="true">Loading...</div>

<div id="error" class="error" role="alert" hidden>
<h3 class="error__title" id="error-title" tabindex="-1">Unable to load preview</h3>
<p class="error__message" id="error-message"></p>
</div>

<div class="actions" aria-label="Actions">
<button type="button" class="btn btn-primary" id="retry-btn">Refresh</button>
<a class="btn btn-secondary" id="open-json-link" href="#" target="_blank" rel="noopener noreferrer" aria-label="Open JSON bootstrap in a new tab" hidden>Open JSON bootstrap</a>
</div>
</section>

<section class="panel" aria-labelledby="preview-title">
<h2 id="preview-title" class="panel__title">Preview</h2>

<div id="preview" class="preview" aria-busy="true">
<div class="skeleton" aria-hidden="true"></div>
<p class="help" id="preview-help">When ready, the converted artifact will appear here.</p>
</div>
</section>
</main>

<footer class="app-footer" role="contentinfo">
<div class="app-footer__inner">
<small>Copyright (c) 2026 by HYOSUNG. All rights reserved.</small>
</div>
</footer>

<script type="module" src="/assets/viewer/viewer.js"></script>
</body>
</html>
""";

return template
.replace("{{DOC_ID}}", docIdString)
.replace("{{INITIAL_STATE}}", initialState)
.replace("{{PDFJS_VIEWER_PATH}}", PDF_JS_VIEWER_PATH);
return VIEWER_SHELL_PART_1 + docIdString + VIEWER_SHELL_PART_2 + initialState
+ VIEWER_SHELL_PART_3 + PDF_JS_VIEWER_PATH + VIEWER_SHELL_PART_4;
}

private static String demoShellHtml() {
Expand Down
Loading