Last active
May 22, 2025 13:25
-
-
Save lunamoth/ff236983a98c06b9b1e5a7e23759198b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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