Skip to content

Instantly share code, notes, and snippets.

@lunamoth
Last active May 22, 2025 13:25
Show Gist options
  • Save lunamoth/ff236983a98c06b9b1e5a7e23759198b to your computer and use it in GitHub Desktop.
Save lunamoth/ff236983a98c06b9b1e5a7e23759198b to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hard Return Remover</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@400&display=swap" rel="stylesheet">
<style>
:root {
--beos-bg-primary: #D4D0C8;
--beos-bg-window: #EEEEEE;
--beos-bg-widget: #BFBFBF;
--beos-bg-widget-active: #A0A0A0;
--beos-bg-text-area: #FFFFFF;
--beos-border-light: #FFFFFF;
--beos-border-shadow: #808080;
--beos-text-primary: #000000;
--beos-text-placeholder: #999999;
--beos-tab-bg: #FFD700;
--beos-tab-text: #000000;
--beos-scrollbar-track: #CCCCCC;
--beos-scrollbar-thumb: #A0A0A0;
--beos-scrollbar-thumb-hover: #B0B0B0;
--font-primary: Lato, '나눔바른고딕', 'NanumBarunGothic', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
--font-editor: Lato, '나눔바른고딕', 'NanumBarunGothic', var(--font-primary);
--padding-xs: 4px;
--padding-s: 5px;
--padding-m: 8px;
--padding-l: 10px;
--font-size-s: 13px;
--font-size-m: 14px;
--font-size-l: 18px;
--line-height-editor: 2;
--line-height-default: 1.5;
--min-button-width: 80px;
--scrollbar-width: 16px;
--status-bar-height: 22px;
}
html,
body {
height: 100%;
margin: 0;
overflow: hidden;
font-family: var(--font-primary);
color: var(--beos-text-primary);
background-color: var(--beos-bg-primary);
}
body {
display: flex;
flex-direction: column;
padding: var(--padding-m);
box-sizing: border-box;
align-items: center;
}
.app-window {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
max-width: 100%;
background-color: var(--beos-bg-window);
border-top: 1px solid var(--beos-border-light);
border-left: 1px solid var(--beos-border-light);
border-right: 1px solid var(--beos-border-shadow);
border-bottom: 1px solid var(--beos-border-shadow);
box-shadow: 2px 2px 0px 0px rgba(0, 0, 0, 0.15);
}
.title-bar {
padding: var(--padding-xs) var(--padding-m);
background-color: var(--beos-tab-bg);
color: var(--beos-tab-text);
font-size: var(--font-size-m);
font-weight: bold;
text-align: center;
border-bottom: 1px solid var(--beos-border-shadow);
user-select: none;
flex-shrink: 0;
}
.content-area {
display: flex;
flex-grow: 1;
gap: var(--padding-m);
padding: var(--padding-m);
overflow: hidden;
}
.column {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0; /* Prevenir que el contenido interno cause overflow en el contenedor flex */
}
.column-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--padding-s);
flex-shrink: 0;
}
.editor-controls {
display: flex;
gap: var(--padding-xs);
flex-shrink: 0;
}
.editor-label {
padding-left: 2px;
font-size: var(--font-size-s);
font-weight: normal;
flex-shrink: 0;
margin-right: var(--padding-s);
}
textarea {
flex-grow: 1;
width: 100%;
padding: var(--padding-s);
box-sizing: border-box;
resize: none;
color: var(--beos-text-primary);
font-family: var(--font-editor);
font-size: var(--font-size-l);
font-weight: 400;
letter-spacing: 0;
line-height: var(--line-height-editor);
text-align: left;
word-break: keep-all;
background-color: var(--beos-bg-text-area);
border-top: 1px solid var(--beos-border-shadow);
border-left: 1px solid var(--beos-border-shadow);
border-right: 1px solid var(--beos-border-light);
border-bottom: 1px solid var(--beos-border-light);
box-shadow: inset 1px 1px 0px 0px rgba(0, 0, 0, 0.1);
min-height: 100px; /* 컨텐츠가 적을 때도 최소 높이 유지 */
}
textarea:focus {
outline: 1px solid var(--beos-tab-bg);
}
textarea::placeholder {
color: var(--beos-text-placeholder);
font-style: normal;
}
.beos-button {
padding: var(--padding-xs) var(--padding-l);
min-width: var(--min-button-width);
font-family: var(--font-primary);
font-size: var(--font-size-s);
color: var(--beos-text-primary);
background-color: var(--beos-bg-widget);
border-top: 1px solid var(--beos-border-light);
border-left: 1px solid var(--beos-border-light);
border-right: 1px solid var(--beos-border-shadow);
border-bottom: 1px solid var(--beos-border-shadow);
box-shadow: 1px 1px 0px 0px rgba(0, 0, 0, 0.1);
text-align: center;
cursor: default;
user-select: none;
}
.beos-button:disabled {
color: #808080; /* 비활성화 시 텍스트 색상 */
background-color: #D4D0C8; /* 비활성화 시 배경 색상 */
box-shadow: none;
border-color: #A0A0A0; /* 비활성화 시 테두리 색상 */
}
.beos-button:not(:disabled):active {
background-color: var(--beos-bg-widget-active);
border-top: 1px solid var(--beos-border-shadow);
border-left: 1px solid var(--beos-border-shadow);
border-right: 1px solid var(--beos-border-light);
border-bottom: 1px solid var(--beos-border-light);
box-shadow: inset 1px 1px 0px 0px rgba(0, 0, 0, 0.1);
padding-top: calc(var(--padding-xs) + 1px);
padding-bottom: calc(var(--padding-xs) - 1px);
}
.column-status-bar {
height: var(--status-bar-height);
line-height: var(--status-bar-height);
padding: 0 var(--padding-m);
font-size: calc(var(--font-size-s) - 1px);
background-color: var(--beos-bg-widget);
border-top: 1px solid var(--beos-border-light);
color: var(--beos-text-primary);
flex-shrink: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-top: var(--padding-s); /* textarea와의 간격 */
}
textarea::-webkit-scrollbar {
width: var(--scrollbar-width);
height: var(--scrollbar-width);
}
textarea::-webkit-scrollbar-track {
background-color: var(--beos-scrollbar-track);
border-left: 1px solid var(--beos-border-shadow);
}
textarea::-webkit-scrollbar-thumb {
background-color: var(--beos-bg-widget);
border-top: 1px solid var(--beos-border-light);
border-left: 1px solid var(--beos-border-light);
border-right: 1px solid var(--beos-border-shadow);
border-bottom: 1px solid var(--beos-border-shadow);
}
textarea::-webkit-scrollbar-thumb:hover {
background-color: var(--beos-scrollbar-thumb-hover);
}
textarea::-webkit-scrollbar-button {
display: block;
width: var(--scrollbar-width);
height: var(--scrollbar-width);
background-color: var(--beos-bg-widget);
border-top: 1px solid var(--beos-border-light);
border-left: 1px solid var(--beos-border-light);
border-right: 1px solid var(--beos-border-shadow);
border-bottom: 1px solid var(--beos-border-shadow);
background-repeat: no-repeat;
background-position: center center;
}
textarea::-webkit-scrollbar-button:active {
background-color: var(--beos-bg-widget-active);
border-top: 1px solid var(--beos-border-shadow);
border-left: 1px solid var(--beos-border-shadow);
}
textarea::-webkit-scrollbar-button:vertical:decrement {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' fill='%23000000'><polygon points='5,2 2,8 8,8'/></svg>");
}
textarea::-webkit-scrollbar-button:vertical:increment {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' fill='%23000000'><polygon points='5,8 2,2 8,2'/></svg>");
}
@media screen and (min-width: 3800px) {
.app-window {
width: 50vw;
max-width: 2000px;
height: 100%;
}
}
</style>
</head>
<body>
<div class="app-window">
<header class="title-bar">Hard Return Remover</header>
<main class="content-area">
<section class="column">
<div class="column-header">
<label for="input-text-area" class="editor-label">Input:</label>
<div class="editor-controls">
<button id="clear-input-button" class="beos-button">Clear</button>
</div>
</div>
<textarea id="input-text-area" placeholder="Enter text here..."></textarea>
<div id="input-status-bar" class="column-status-bar">Chars: 0 | Words: 0 | Lines: 0</div>
</section>
<section class="column">
<div class="column-header">
<label for="output-text-area" class="editor-label">Output:</label>
<div class="editor-controls">
<button id="copy-button" class="beos-button">Copy</button>
<button id="save-button" class="beos-button">Save</button>
</div>
</div>
<textarea id="output-text-area" readonly placeholder="Processed text will appear here..."></textarea>
<div id="output-status-bar" class="column-status-bar">Chars: 0 | Words: 0 | Lines: 0</div>
</section>
</main>
<!-- 기존 footer는 제거되었습니다. -->
</div>
<script>
const UI_ELEMENTS = {
inputTextArea: document.getElementById('input-text-area'),
outputTextArea: document.getElementById('output-text-area'),
copyButton: document.getElementById('copy-button'),
saveButton: document.getElementById('save-button'), // Save 버튼 추가
clearInputButton: document.getElementById('clear-input-button'),
inputStatusBar: document.getElementById('input-status-bar'), // 입력 상태 바 추가
outputStatusBar: document.getElementById('output-status-bar'), // 출력 상태 바 추가
};
const COPY_BUTTON_FEEDBACK_DURATION_MS = 1500;
function normalizeNewlines(text) {
return text.replace(/\r\n|\r/g, '\n');
}
function removeSingleInternalNewlines(text) {
const normalizedText = normalizeNewlines(text);
return normalizedText.replace(/(?<!\n)\n(?!\n)/g, '');
}
function updateTextStats(text, statusBarElement) {
if (!statusBarElement) return;
const charCount = text.length;
const words = text.trim().split(/\s+/).filter(word => word.length > 0);
const wordCount = (words.length === 1 && words[0] === '') ? 0 : words.length;
const lines = text.split('\n');
const lineCount = text ? lines.length : (text === '' ? 0 : 1); // 빈 텍스트는 0줄, 내용이 있으면 최소 1줄
statusBarElement.textContent = `Chars: ${charCount} | Words: ${wordCount} | Lines: ${lineCount}`;
}
function updateOutputText() {
const inputText = UI_ELEMENTS.inputTextArea.value;
const processedText = removeSingleInternalNewlines(inputText);
UI_ELEMENTS.outputTextArea.value = processedText;
const hasProcessedText = processedText.length > 0;
UI_ELEMENTS.copyButton.disabled = !hasProcessedText;
UI_ELEMENTS.saveButton.disabled = !hasProcessedText; // Save 버튼 상태 업데이트
updateTextStats(inputText, UI_ELEMENTS.inputStatusBar);
updateTextStats(processedText, UI_ELEMENTS.outputStatusBar);
}
function updateCopyButtonState(button, text, isDisabled) {
button.textContent = text;
button.disabled = isDisabled;
}
function handleCopyAction() {
const textToCopy = UI_ELEMENTS.outputTextArea.value;
if (!textToCopy) {
return;
}
navigator.clipboard.writeText(textToCopy)
.then(() => {
const originalButtonText = UI_ELEMENTS.copyButton.textContent;
updateCopyButtonState(UI_ELEMENTS.copyButton, 'Copied!', true);
setTimeout(() => {
updateCopyButtonState(UI_ELEMENTS.copyButton, originalButtonText, false);
// 복사 후 버튼 상태는 updateOutputText()에서 이미 관리되므로,
// outputTextArea.value 기준으로 다시 설정할 필요는 없음
// 만약 다른 로직으로 비활성화 되었다면, 여기서 다시 활성화 시켜야 함.
// 현재는 processedText가 있으면 활성화되도록 되어 있음.
UI_ELEMENTS.copyButton.disabled = !UI_ELEMENTS.outputTextArea.value;
}, COPY_BUTTON_FEEDBACK_DURATION_MS);
})
.catch(err => {
console.error('Failed to copy text: ', err);
alert('Copy failed. Please try again or copy manually.');
});
}
function handleClearInputAction() {
UI_ELEMENTS.inputTextArea.value = '';
updateOutputText();
UI_ELEMENTS.inputTextArea.focus();
}
function handleSaveAction() {
const textToSave = UI_ELEMENTS.outputTextArea.value;
if (!textToSave) {
return;
}
const blob = new Blob([textToSave], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'processed_text.txt'; // 다운로드될 파일 이름
document.body.appendChild(link); // Firefox에서 필요할 수 있음
link.click();
document.body.removeChild(link); // 생성된 링크 제거
URL.revokeObjectURL(url); // 생성된 URL 해제
}
function initializeApp() {
if (UI_ELEMENTS.inputTextArea) {
UI_ELEMENTS.inputTextArea.addEventListener('input', updateOutputText);
}
if (UI_ELEMENTS.copyButton && UI_ELEMENTS.outputTextArea) {
UI_ELEMENTS.copyButton.addEventListener('click', handleCopyAction);
}
if (UI_ELEMENTS.clearInputButton) {
UI_ELEMENTS.clearInputButton.addEventListener('click', handleClearInputAction);
}
if (UI_ELEMENTS.saveButton && UI_ELEMENTS.outputTextArea) { // Save 버튼 이벤트 리스너 추가
UI_ELEMENTS.saveButton.addEventListener('click', handleSaveAction);
}
// 초기 상태 업데이트
updateOutputText();
}
document.addEventListener('DOMContentLoaded', initializeApp);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment