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-16 - HTML 템플릿 렌더링 시 문자열 치환 체이닝 오버헤드 방지
**Learning:** 여러 번의 `.replace()` 호출을 체이닝하여 큰 HTML 템플릿에 동적 값을 주입하면 매 치환마다 중간 `String` 및 `char[]` 객체가 크게 할당되어 메모리 낭비와 성능 저하를 초래합니다.
**Action:** 큰 HTML 템플릿은 삽입될 위치를 기준으로 여러 개의 `private static final String` 조각으로 미리 분리하고, 단순히 `+` 연산자로 연결(StringBuilder로 자동 최적화됨)하여 불필요한 중간 객체 할당 없이 성능을 개선해야 합니다.
164 changes: 90 additions & 74 deletions src/main/java/com/clearfolio/viewer/controller/ViewerUiController.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,82 @@ public class ViewerUiController {
static final String PDF_JS_VIEWER_PATH = "/webjars/pdfjs-dist/4.10.38/web/viewer.html";
private static final String INVALID_DOC_ID_SENTINEL = "invalid";

// Pre-allocated template parts for viewer shell HTML
private static final String VIEWER_HTML_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=\"""";
private static final String VIEWER_HTML_PART_2 = """
" />
<meta name="clearfolio-initial-state" content=\"""";
private static final String VIEWER_HTML_PART_3 = """
" />
<meta name="clearfolio-pdfjs-viewer-path" content=\"""";
private static final String VIEWER_HTML_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>
""";

/**
* Returns the buyer-demo document intake shell.
*
Expand Down Expand Up @@ -55,85 +131,25 @@ public ResponseEntity<String> viewer(@PathVariable String docId) {
}
}

private static String escapeHtmlAttribute(String value) {
private static String escapeHtmlAttribute(final String value) {
return HtmlUtils.htmlEscape(value);
}

private static String viewerShellHtml(String docId, String initialState) {
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);
// Avoid chained String.replace() calls on large templates by using
// pre-allocated static string parts and simple concatenation, which is
// compiled into an efficient StringBuilder allocation.
return VIEWER_HTML_PART_1
+ docIdString
+ VIEWER_HTML_PART_2
+ initialState
+ VIEWER_HTML_PART_3
+ PDF_JS_VIEWER_PATH
+ VIEWER_HTML_PART_4;
}

private static String demoShellHtml() {
Expand Down
Loading