텍스트 분할 복사
어떤 긴 텍스트가 입력되었을 때 그걸 특정 character 수 단위로 쪼개서 복사할 수 있게 해줄 수 있어?
좋죠! “원하는 글자 수로 잘라서 복사”할 수 있는 초간단 웹페이지를 바로 드릴게요. 아래 코드를 splitter.html로 저장해서 브라우저로 열면 됩니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>텍스트 분할 & 복사</title>
<style>
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans KR", Arial, sans-serif; margin: 24px; }
h1 { font-size: 20px; margin: 0 0 16px; }
textarea { width: 100%; min-height: 180px; box-sizing: border-box; padding: 12px; }
.row { display: flex; gap: 12px; align-items: center; margin: 12px 0; flex-wrap: wrap; }
input[type="number"] { width: 120px; padding: 8px; }
button { padding: 8px 12px; cursor: pointer; }
.stats { margin: 8px 0 16px; color: #333; }
.chunk { border: 1px solid #ddd; padding: 10px; margin: 12px 0; border-radius: 8px; background: #fafafa; }
.chunk header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.chunk pre { white-space: pre-wrap; word-break: break-word; margin: 0; }
.muted { color: #666; font-size: 12px; }
</style>
</head>
<body>
<h1>텍스트 분할 & 복사</h1>
<label for="input">원본 텍스트</label>
<textarea id="input" placeholder="여기에 긴 텍스트를 붙여넣으세요"></textarea>
<div class="row">
<label for="size">조각 글자 수</label>
<input id="size" type="number" min="1" step="1" value="500" />
<button id="splitBtn">분할하기</button>
<button id="copyAllBtn" title="모든 조각을 이어붙여서 복사">모두 복사</button>
</div>
<div class="muted">
* 한국어/이모지 등 묶음 문자가 깨지지 않도록 <code>Intl.Segmenter</code>를 이용해 “문자(그래프림)” 단위로 자릅니다.
(매우 오래된 브라우저에서는 일반 문자 길이로 대체)
</div>
<div id="stats" class="stats"></div>
<div id="chunks"></div>
<script>
(function(){
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => Array.from(document.querySelectorAll(sel));
const input = $('#input');
const sizeEl = $('#size');
const splitBtn = $('#splitBtn');
const copyAllBtn = $('#copyAllBtn');
const stats = $('#stats');
const container = $('#chunks');
// 안전한 "문자(그래프림)" 단위 분할을 위한 세그멘터
function getGraphemes(str) {
try {
if ('Segmenter' in Intl) {
const seg = new Intl.Segmenter('ko', { granularity: 'grapheme' });
return Array.from(seg.segment(str), s => s.segment);
}
} catch (e) { /* fallback below */ }
// Fallback: code unit 기준 (이모지 결합문자에 완벽하진 않지만 동작은 함)
return Array.from(str);
}
function chunkArray(arr, n) {
const out = [];
for (let i = 0; i < arr.length; i += n) out.push(arr.slice(i, i + n).join(''));
return out;
}
async function copyText(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (e) {
// Fallback: 임시 텍스트영역 이용
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); document.body.removeChild(ta); return true; }
catch (e2) { document.body.removeChild(ta); return false; }
}
}
function renderChunks(chunks) {
container.innerHTML = '';
chunks.forEach((text, idx) => {
const div = document.createElement('div');
div.className = 'chunk';
const header = document.createElement('header');
const title = document.createElement('strong');
title.textContent = `조각 ${idx + 1} / ${chunks.length} (${text.length}자)`;
const btn = document.createElement('button');
btn.textContent = '이 조각 복사';
btn.addEventListener('click', async () => {
const ok = await copyText(text);
btn.textContent = ok ? '복사됨!' : '복사 실패';
setTimeout(() => btn.textContent = '이 조각 복사', 1200);
});
header.appendChild(title);
header.appendChild(btn);
const pre = document.createElement('pre');
pre.textContent = text;
div.appendChild(header);
div.appendChild(pre);
container.appendChild(div);
});
}
function splitNow() {
const raw = input.value ?? '';
const size = Math.max(1, parseInt(sizeEl.value || '0', 10));
const grams = getGraphemes(raw);
const chunks = chunkArray(grams, size);
renderChunks(chunks);
stats.textContent = `전체 길이: ${grams.length}자 · 조각 개수: ${chunks.length} · 조각당 최대 ${size}자`;
}
splitBtn.addEventListener('click', splitNow);
copyAllBtn.addEventListener('click', async () => {
const pieces = $$('.chunk pre').map(p => p.textContent);
const joined = pieces.join('');
const ok = await copyText(joined);
copyAllBtn.textContent = ok ? '모두 복사됨!' : '복사 실패';
setTimeout(() => copyAllBtn.textContent = '모두 복사', 1200);
});
})();
</script>
</body>
</html>
사용 방법 (step-by-step)
-
위 코드를
splitter.html로 저장하고 더블클릭해 브라우저에서 엽니다. -
긴 텍스트를 상단 입력창에 붙여넣습니다.
-
“조각 글자 수”에 원하는 숫자(예: 500)를 입력합니다.
-
“분할하기”를 누르면 아래에 조각들이 생성됩니다.
-
각 조각의 “이 조각 복사” 버튼으로 개별 복사하거나, “모두 복사”로 다시 이어붙인 전체를 복사할 수 있어요.
참고
-
기본적으로 한국어, 이모지 등 결합 문자를 안전하게 처리하도록 “문자(그래프림)” 단위로 자릅니다. 아주 오래된 브라우저에선 일반 문자 길이(
.length)로 동작할 수 있습니다. -
필요하면 “조각당 N자” 외에도 “겹침(오버랩)”, “문단 단위로 자르되 최대 N자” 같은 옵션도 붙여드릴 수 있어요. 원하는 규칙이 있으면 알려주세요!
좋아 잘 작동해. 전체적인 구조를 유지하되 두 가지 개선해야할 사항이 있어. 1. 버튼이 “복사됨!”으로 바뀐 뒤 다시 일정 시간 후 “이 조각 복사” 버튼으로 돌아가는데, 내가 어디까지 복사했는지 헷갈려. 내가 1번이라도 누른 것은 버튼 색을 바꾸거나 해서 내가 어디까지 했는지 알 수 있으면 좋겠어. 2. 각 조각을 8000자 정도로 하고 있는데 스크롤 내려야할 게 너무 길어. 각 미리보기 블럭을 접어두고 내가 원할때만 펴도 좋을 거 같아. 두 가지는 내가 생각하는 방향인데 너한테 더 좋은 아이디어가 있다면 반영해줘.
좋아요! 말씀해주신 두 가지를 기준으로, 구조는 유지하면서 이렇게 개선했어요.
-
#1 “내가 복사한 조각을 한눈에”: 어떤 조각이든 한 번이라도 복사에 성공하면 그 카드가 영구 표시(세션 내) 됩니다.
-
상단 요약줄에 ✅ 배지가 붙고, 카드 테두리가 강조됩니다(초록색).
-
버튼 문구는 예전처럼 잠깐 “복사됨!”으로 바뀌지만, 배지는 그대로 남아 “어디까지 했는지” 헷갈리지 않게 해요.
-
-
#2 “스크롤 지옥 해방”: 각 조각을 접을 수 있는 미리보기(accordion) 로 바꿨습니다.
-
기본은 접힘 상태로 렌더링되고, 요약줄엔 조각 번호/총개수, 길이, 앞부분 미리보기 가 보여요.
-
상단에 “모두 펼치기 / 모두 접기 ” 토글도 추가했습니다.
-
거기에 제안 하나 더!
- “다음 조각 열기” : 어떤 조각에서 복사 성공하면, 다음 조각을 자동으로 펼쳐 작업 흐름이 매끄럽습니다(선택 가능 토글).
아래 전체 코드만 splitter.html로 저장해 열면 바로 됩니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>텍스트 분할 & 복사 (개선판)</title>
<style>
:root {
--accent: #2e7d32; /* 복사 완료 강조색 */
--muted: #666;
--border: #ddd;
--bg: #fafafa;
}
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans KR", Arial, sans-serif; margin: 24px; }
h1 { font-size: 20px; margin: 0 0 16px; }
textarea { width: 100%; min-height: 180px; box-sizing: border-box; padding: 12px; }
.row { display: flex; gap: 12px; align-items: center; margin: 12px 0; flex-wrap: wrap; }
input[type="number"] { width: 120px; padding: 8px; }
button { padding: 8px 12px; cursor: pointer; }
.stats { margin: 8px 0 16px; color: #333; }
.muted { color: var(--muted); font-size: 12px; }
/* 조각(접힘 카드) */
details.chunk {
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg);
margin: 12px 0;
transition: border-color .2s, box-shadow .2s;
}
details.chunk[open] {
box-shadow: 0 1px 10px rgba(0,0,0,.04);
}
details.chunk.copied-once {
border-color: var(--accent);
}
/* 요약줄 */
summary {
list-style: none;
display: flex;
gap: 8px;
align-items: center;
padding: 10px 12px;
cursor: pointer;
user-select: none;
}
summary::-webkit-details-marker { display: none; }
.summary-left { display: flex; align-items: center; gap: 8px; flex: 1; }
.badge {
font-size: 11px;
padding: 2px 6px;
border-radius: 999px;
border: 1px solid var(--border);
color: #333;
background: #fff;
}
.badge.ok {
border-color: var(--accent);
color: var(--accent);
font-weight: 600;
}
.snippet {
color: var(--muted);
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 40ch;
}
/* 본문 */
.chunk-body { padding: 0 12px 12px 12px; }
.actions { display: flex; gap: 8px; align-items: center; margin: 8px 0; flex-wrap: wrap; }
.chunk pre { white-space: pre-wrap; word-break: break-word; margin: 0; padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: #fff; max-height: 40vh; overflow: auto; }
/* 상단 컨트롤 */
.toolbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
.toolbar .spacer { flex: 1; }
/* 작은 토글 스위치 */
.toggle {
display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: #333;
}
.toggle input { width: 1.1rem; height: 1.1rem; }
</style>
</head>
<body>
<h1>텍스트 분할 & 복사</h1>
<label for="input">원본 텍스트</label>
<textarea id="input" placeholder="여기에 긴 텍스트를 붙여넣으세요"></textarea>
<div class="row">
<label for="size">조각 글자 수</label>
<input id="size" type="number" min="1" step="1" value="8000" />
<button id="splitBtn">분할하기</button>
<button id="copyAllBtn" title="모든 조각을 이어붙여서 복사">모두 복사</button>
</div>
<div class="toolbar">
<button id="expandAllBtn">모두 펼치기</button>
<button id="collapseAllBtn">모두 접기</button>
<span class="spacer"></span>
<label class="toggle" title="복사 성공 시 자동으로 다음 조각을 열어 작업 흐름을 빠르게 합니다.">
<input id="autoOpenNext" type="checkbox" checked />
복사 후 다음 조각 자동 열기
</label>
</div>
<div class="muted">
* 한국어/이모지 등 결합 문자가 깨지지 않도록 <code>Intl.Segmenter</code>로 “문자(그래프림)” 단위로 자릅니다. (오래된 브라우저는 일반 길이로 대체)
</div>
<div id="stats" class="stats"></div>
<div id="chunks"></div>
<script>
(function(){
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => Array.from(document.querySelectorAll(sel));
const input = $('#input');
const sizeEl = $('#size');
const splitBtn = $('#splitBtn');
const copyAllBtn = $('#copyAllBtn');
const stats = $('#stats');
const container = $('#chunks');
const expandAllBtn = $('#expandAllBtn');
const collapseAllBtn = $('#collapseAllBtn');
const autoOpenNextEl = $('#autoOpenNext');
function getGraphemes(str) {
try {
if ('Segmenter' in Intl) {
const seg = new Intl.Segmenter('ko', { granularity: 'grapheme' });
return Array.from(seg.segment(str), s => s.segment);
}
} catch (e) { /* fallback below */ }
return Array.from(str);
}
function chunkArray(arr, n) {
const out = [];
for (let i = 0; i < arr.length; i += n) out.push(arr.slice(i, i + n).join(''));
return out;
}
async function copyText(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (e) {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
document.body.removeChild(ta);
return true;
} catch (e2) {
document.body.removeChild(ta);
return false;
}
}
}
function makeSummaryLine(idx, total, text) {
const len = text.length;
const snippet = text.replace(/\s+/g, ' ').slice(0, 60);
return {
title: `조각 ${idx + 1} / ${total}`,
meta: `(${len}자)`,
snippet
};
}
function renderChunks(chunks) {
container.innerHTML = '';
chunks.forEach((text, idx) => {
const details = document.createElement('details');
details.className = 'chunk';
// 기본: 접힘
details.open = false;
const summary = document.createElement('summary');
const left = document.createElement('div');
left.className = 'summary-left';
const b1 = document.createElement('span'); b1.className = 'badge';
const b2 = document.createElement('span'); b2.className = 'badge';
const snip = document.createElement('span'); snip.className = 'snippet';
const meta = makeSummaryLine(idx, chunks.length, text);
b1.textContent = meta.title;
b2.textContent = meta.meta;
snip.textContent = meta.snippet;
left.appendChild(b1); left.appendChild(b2); left.appendChild(snip);
const copiedBadge = document.createElement('span');
copiedBadge.className = 'badge';
copiedBadge.textContent = '미복사';
summary.appendChild(left);
summary.appendChild(copiedBadge);
details.appendChild(summary);
const body = document.createElement('div');
body.className = 'chunk-body';
const actions = document.createElement('div');
actions.className = 'actions';
const copyBtn = document.createElement('button');
copyBtn.textContent = '이 조각 복사';
const copyAndNextBtn = document.createElement('button');
copyAndNextBtn.textContent = '복사하고 다음 펼치기';
const markBtn = document.createElement('button');
markBtn.textContent = '표시 해제';
actions.appendChild(copyBtn);
actions.appendChild(copyAndNextBtn);
actions.appendChild(markBtn);
const pre = document.createElement('pre');
pre.textContent = text;
body.appendChild(actions);
body.appendChild(pre);
details.appendChild(body);
container.appendChild(details);
// 상태 마킹 함수
function markCopiedOnce() {
details.classList.add('copied-once');
copiedBadge.classList.add('ok');
copiedBadge.textContent = '✅ 복사함';
markBtn.textContent = '표시 해제';
}
function unmarkCopied() {
details.classList.remove('copied-once');
copiedBadge.classList.remove('ok');
copiedBadge.textContent = '미복사';
markBtn.textContent = '표시하기';
}
// 수동 토글 (원하실 때 직접 표시/해제)
markBtn.addEventListener('click', (e) => {
e.preventDefault(); // summary 열림 방지
if (details.classList.contains('copied-once')) unmarkCopied();
else markCopiedOnce();
});
// 복사 버튼
copyBtn.addEventListener('click', async (e) => {
e.preventDefault(); // summary 토글 방지
const ok = await copyText(text);
copyBtn.textContent = ok ? '복사됨!' : '복사 실패';
if (ok) markCopiedOnce();
setTimeout(() => copyBtn.textContent = '이 조각 복사', 1200);
});
// 복사 후 다음 펼치기(옵션)
copyAndNextBtn.addEventListener('click', async (e) => {
e.preventDefault();
const ok = await copyText(text);
copyAndNextBtn.textContent = ok ? '복사됨!' : '복사 실패';
if (ok) {
markCopiedOnce();
if (autoOpenNextEl.checked) {
const next = details.nextElementSibling;
if (next && next.tagName === 'DETAILS') {
next.open = true;
next.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
}
setTimeout(() => copyAndNextBtn.textContent = '복사하고 다음 펼치기', 1200);
});
});
}
function splitNow() {
const raw = input.value ?? '';
const size = Math.max(1, parseInt(sizeEl.value || '0', 10));
const grams = getGraphemes(raw);
const chunks = chunkArray(grams, size);
renderChunks(chunks);
stats.textContent = `전체 길이: ${grams.length}자 · 조각 개수: ${chunks.length} · 조각당 최대 ${size}자`;
}
splitBtn.addEventListener('click', splitNow);
copyAllBtn.addEventListener('click', async () => {
const pieces = $$('.chunk pre').map(p => p.textContent);
const joined = pieces.join('');
const ok = await copyText(joined);
copyAllBtn.textContent = ok ? '모두 복사됨!' : '복사 실패';
setTimeout(() => copyAllBtn.textContent = '모두 복사', 1200);
});
expandAllBtn.addEventListener('click', () => {
$$('#chunks details.chunk').forEach(d => d.open = true);
});
collapseAllBtn.addEventListener('click', () => {
$$('#chunks details.chunk').forEach(d => d.open = false);
});
})();
</script>
</body>
</html>
바뀐 점 요약 (step-by-step)
-
복사 진행 상황 표시 : 한 번이라도 복사 성공하면 해당 카드가 초록 테두리 +
✅ 복사함배지로 영구 표시(세션 내) 됩니다. -
접힘 UI : 각 조각은
details/summary기반 접힘 카드 로 렌더링됩니다. 상단의 “모두 펼치기/모두 접기”로 일괄 제어 가능. -
미리보기 : 요약줄에 앞부분 60자 미리보기와 길이 표기가 들어가 스크롤 없이 훑어보기 가 쉬워졌습니다.
-
작업 흐름 가속 : “복사하고 다음 펼치기” + (옵션) “복사 후 다음 조각 자동 열기” 토글로 연속 작업 이 부드럽습니다.
-
수동 표시/해제 버튼: 필요하면 복사 없이도 “표시하기/표시 해제”로 체크리스트처럼 쓸 수 있어요.
원하시면
-
오버랩 분할(예: 8000자, 오버랩 200자) ,
-
문단/문장 단위 최대 N자(자연어 경계 유지),
-
세션 간 진행상황 저장(localStorage)
같은 옵션도 바로 붙여드릴게요.