html code change
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>HTML Cleaner & Formatter</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.container { max-width: 1200px; margin: auto; }
textarea { width: 100%; min-height: 250px; margin-bottom: 10px; padding: 10px; box-sizing: border-box; font-family: monospace; }
.options { margin-bottom: 20px; padding: 15px; border: 1px solid #ccc; background-color: #f9f9f9; }
button { padding: 10px 20px; background-color: #007bff; color: white; border: none; cursor: pointer; }
h2 { border-bottom: 2px solid #eee; padding-bottom: 5px; margin-top: 25px; }
</style>
</head>
<body>
<div class="container">
<h1>HTML 코드 정리 도구</h1>
<div class="options">
<label><input type="checkbox" id="option1" checked> 옵션 1. 태그 내 줄바꿈 및 불필요한 공백 제거</label><br>
<label><input type="checkbox" id="option2" checked> 옵션 2. 대문자를 소문자로 변경 (태그 및 속성)</label><br>
<label><input type="checkbox" id="option3" checked> 옵션 3. 태그 내 속성 따옴표 통일/추가/정리</label><br>
<label><input type="checkbox" id="option4" checked> 옵션 4. ARRAY 태그 대문자화 따옴표 제거</label>
</div>
<h2>수정 전 코드</h2>
<textarea id="input" placeholder="수정할 HTML 코드를 입력하세요."></textarea>
<button onclick="modifyHtml()">코드 수정</button>
<h2>수정 후 코드</h2>
<textarea id="output" readonly placeholder="수정된 코드가 여기에 표시됩니다."></textarea>
</div>
<script>
// =========================================================================
// 1. DOMParser 기반 속성 정리 핵심 함수 (변동 없음)
// =========================================================================
/**
* DOMParser를 사용하여 속성 문자열을 파싱하고, 모든 속성에 쌍따옴표를 통일/추가합니다.
*/
function modifyAttributesWithDOMParser(attributesString) {
if (!attributesString || !attributesString.trim()) {
return '';
}
const tempHtml = `<wrapper ${attributesString}>`;
const parser = new DOMParser();
const doc = parser.parseFromString(tempHtml, 'text/html');
const wrapper = doc.querySelector('wrapper');
if (!wrapper) {
return ' ' + attributesString.trim();
}
const attributesToSet = [];
Array.from(wrapper.attributes).forEach(attr => {
const attrName = attr.name;
let attrValue = attr.value;
// 값 내부의 쌍따옴표를 홑따옴표로 변환 (중복 방지)
attrValue = attrValue.replace(/"/g, "'");
// 속성 이름 정규화 (소문자 통일)
const newAttrName = attrName.toLowerCase();
attributesToSet.push({ name: newAttrName, value: attrValue });
});
let finalAttrs = '';
attributesToSet.forEach(item => {
finalAttrs += ` ${item.name}="${item.value}"`;
});
return finalAttrs.trim();
}
// =========================================================================
// 2. 메인 처리 함수 (modifyHtml) - 기존 구조 유지
// =========================================================================
/**
* 사용자 입력 HTML 코드를 처리하는 메인 함수.
* (isOption3Checked 변수는 UI에서 가져온다고 가정합니다.)
*/
function modifyHtml(inputCode, isOption3Checked) {
let modifiedCode = inputCode;
// 1. HTML 태그를 찾는 큰 replace 루프
modifiedCode = modifiedCode.replace(/<(\/?[a-zA-Z0-9]+)([^>]*)>/g, (match, tagName, attributes) => {
let newTagName = tagName;
let finalAttributes = attributes;
// --- 다른 옵션 처리 로직이 있다면 여기에 위치합니다 ---
// (예: isOption1Checked, isOption2Checked 로직 등)
// -----------------------------------------------------------------
// [옵션 3]: 속성 따옴표 통일/추가/정리 (DOMParser 대체)
// -----------------------------------------------------------------
if (isOption3Checked) {
// DOMParser를 사용하여 속성 문자열을 정리합니다.
finalAttributes = modifyAttributesWithDOMParser(finalAttributes);
}
// -----------------------------------------------------------------
// 3. 최종 결과 조립
// -----------------------------------------------------------------
let finalResult = finalAttributes.trim();
finalResult = finalResult ? ' ' + finalResult : '';
// 닫는 태그가 아닌 경우에만 태그 이름과 속성을 결합합니다.
if (tagName.startsWith('/')) {
return `<${tagName}>`;
} else {
return `<${newTagName}${finalResult}>`;
}
});
return modifiedCode;
}
// =========================================================================
// 3. UI 이벤트 연결 함수 (기존 방식 재현)
// =========================================================================
/**
* '코드 수정' 버튼 클릭 시 실행되는 함수.
*/
function handleCodeModification() {
// 1. 입력값 및 옵션 상태 가져오기
const inputTextArea = document.getElementById('inputCode');
const outputTextArea = document.getElementById('outputCode');
if (!inputTextArea || !outputTextArea) {
console.error("입력/출력 텍스트 에어리어 ID를 확인해 주세요 (inputCode, outputCode).");
return;
}
const inputCode = inputTextArea.value;
// 옵션 3 상태를 UI에서 가져오는 로직 (예시: 체크박스 ID가 option3인 경우)
// 실제 HTML ID에 맞게 수정이 필요합니다.
const option3Checkbox = document.getElementById('option3');
const isOption3Checked = option3Checkbox ? option3Checkbox.checked : true; // 기본값 true로 설정 (테스트 용이)
// 2. HTML 코드 수정 로직 실행
const modifiedCode = modifyHtml(inputCode, isOption3Checked);
// 3. 결과 텍스트에어리어에 출력
outputTextArea.value = modifiedCode;
}
// 4. 버튼 클릭 이벤트 연결
document.addEventListener('DOMContentLoaded', () => {
const modifyButton = document.getElementById('modifyButton');
if (modifyButton) {
modifyButton.addEventListener('click', handleCodeModification);
} else {
console.warn("버튼 ID를 확인해 주세요 (modifyButton).");
}
});
// ------------------------------------------------------------------
// [추가 기능]: 결과창 클릭 시 전체 선택
// ------------------------------------------------------------------
document.addEventListener('DOMContentLoaded', () => {
const outputTextarea = document.getElementById('output');
outputTextarea.addEventListener('click', () => {
if (outputTextarea.value.length > 0) {
outputTextarea.select();
}
});
});
</script>
</body>
</html>
regexp
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 정규 표현식(RegExp) 상세 가이드 및 테스트</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
max-width: 1000px;
margin: 0 auto;
padding: 20px;
background-color: #f4f4f9;
}
h1, h2, h3 {
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 5px;
margin-top: 30px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background-color: #fff;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
font-size: 0.95em;
}
th, td {
border: 1px solid #ddd;
padding: 10px;
text-align: left;
}
th {
background-color: #3498db;
color: white;
font-weight: bold;
}
code {
background-color: #ecf0f1;
padding: 2px 4px;
border-radius: 4px;
font-family: Consolas, monospace;
color: #c0392b;
}
pre {
background-color: #2c3e50;
color: #ecf0f1;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
}
/* 테스트 환경 스타일 */
.tester-container {
padding: 25px;
border: 2px solid #2c3e50;
border-radius: 10px;
background-color: #ecf0f1;
margin-top: 40px;
}
.input-group label {
font-weight: bold;
display: block;
margin-top: 15px;
color: #2c3e50;
}
.input-group input[type="text"], .input-group textarea {
width: 100%;
padding: 10px;
margin-top: 5px;
box-sizing: border-box;
border: 1px solid #bdc3c7;
border-radius: 5px;
font-size: 16px;
}
button {
background-color: #2ecc71;
color: white;
padding: 12px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 18px;
margin-top: 20px;
transition: background-color 0.3s;
}
button:hover {
background-color: #27ae60;
}
#output {
min-height: 150px;
white-space: pre-wrap;
background-color: #fff;
border: 1px solid #3498db;
padding: 15px;
margin-top: 15px;
border-radius: 5px;
font-family: Consolas, monospace;
}
</style>
</head>
<body>
<h1>자바스크립트 정규 표현식 상세 가이드</h1>
<p><strong>정규 표현식(Regular Expression, RegExp)</strong>은 문자열에서 특정 패턴을 검색, 추출, 또는 치환하기 위해 사용되는 강력한 문자열 처리 도구입니다.</p>
<hr>
<h2>2. 정규 표현식 생성 방법</h2>
<p>정규 표현식은 두 가지 방법으로 생성할 수 있습니다.</p>
<table>
<thead>
<tr>
<th>방법</th>
<th>문법</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>리터럴</strong></td>
<td><code>/패턴/플래그</code></td>
<td>스크립트 로드 시 컴파일되어, **정적인 패턴**에 사용하면 성능이 좋습니다.</td>
<td><code>const re = /abc/i;</code></td>
</tr>
<tr>
<td><strong>생성자</strong></td>
<td><code>new RegExp("패턴", "플래그")</code></td>
<td>런타임에 컴파일되며, **동적인 패턴** (예: 변수나 사용자 입력)을 사용할 때 유용합니다.</td>
<td><code>const re = new RegExp("abc", "i");</code></td>
</tr>
</tbody>
</table>
<h2>3. 정규 표현식 플래그 (Flags)</h2>
<p>정규식의 동작 방식을 설정하는 옵션입니다. <code>/패턴/</code> 뒤에 붙여 사용합니다.</p>
<table>
<thead>
<tr>
<th>플래그</th>
<th>이름</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>g</code></td>
<td>Global</td>
<td>**모든 일치 항목**을 찾습니다.</td>
<td><code>"apple banana apple".match(/apple/g); // ["apple", "apple"]</code></td>
</tr>
<tr>
<td><code>i</code></td>
<td>Ignore Case</td>
<td>**대소문자를 구별하지 않고** 일치시킵니다.</td>
<td><code>"Hello WORLD".match(/world/i); // ["WORLD"]</code></td>
</tr>
<tr>
<td><code>m</code></td>
<td>Multiline</td>
<td>입력 문자열의 시작(<code>^</code>)과 끝(<code>$</code>)을 **각 행의 시작과 끝**으로 인식합니다.</td>
<td><code>"a\nb".match(/^b/m); // ["b"]</code></td>
</tr>
<tr>
<td><code>s</code></td>
<td>DotAll</td>
<td><code>.</code> 메타 문자가 **줄바꿈 문자(\n)까지 포함**한 모든 문자와 일치하도록 합니다.</td>
<td><code>"a\nb".match(/a.b/s); // ["a\nb"]</code></td>
</tr>
</tbody>
</table>
<h2>4. 핵심 메타 문자 및 이스케이프</h2>
<p>패턴을 정의하는 데 사용되는 특별한 의미를 가진 문자들입니다.</p>
<table>
<thead>
<tr>
<th>문자</th>
<th>설명</th>
<th>예시 (패턴)</th>
<th>결과 설명</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>.</code></td>
<td>줄바꿈을 제외한 모든 단일 문자</td>
<td><code>/a.c/</code></td>
<td>"abc", "a9c" 에 일치 (문자 하나)</td>
</tr>
<tr>
<td><code>*</code>, <code>+</code>, <code>?</code></td>
<td>반복 (0회 이상, 1회 이상, 0 또는 1회)</td>
<td><code>/a+b/</code></td>
<td>"ab", "aaab" 에 일치</td>
</tr>
<tr>
<td><code>{n,m}</code></td>
<td>반복 횟수 지정</td>
<td><code>/\d{3,4}/</code></td>
<td>"123", "1234" 에 일치</td>
</tr>
<tr>
<td><code>^</code>, <code>$</code></td>
<td>시작, 끝</td>
<td><code>/^\d+$/</code></td>
<td>숫자로만 구성된 문자열에 일치</td>
</tr>
<tr>
<td><code>|</code></td>
<td>OR (논리적 합)</td>
<td><code>/html|css/</code></td>
<td>"html" 또는 "css" 에 일치</td>
</tr>
<tr>
<td><code>()</code>, <code>[]</code>, <code>[^]</code></td>
<td>그룹, 문자 집합, 부정 집합</td>
<td><code>/([a-z])\1/</code></td>
<td>연속된 동일 소문자("aa", "bb")에 일치 (\1은 첫 번째 그룹 참조)</td>
</tr>
<tr>
<td><code>\</code></td>
<td>이스케이프</td>
<td><code>/a\.b/</code></td>
<td>"a.b" 문자열에 일치 (<code>.</code>을 문자 그대로 인식)</td>
</tr>
</tbody>
</table>
<h2>5. 자주 사용되는 특수 시퀀스 (Shorthand Character Classes)</h2>
<p>미리 정의된 문자 집합의 단축 표기법입니다.</p>
<table>
<thead>
<tr>
<th>시퀀스</th>
<th>설명</th>
<th>동등 표현</th>
<th>예시</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>\d</code>, <code>\D</code></td>
<td>숫자, 숫자가 아닌 문자</td>
<td><code>[0-9]</code>, <code>[^0-9]</code></td>
<td><code>/\d{3}/</code> 는 우편번호 세 자리에 일치</td>
</tr>
<tr>
<td><code>\w</code>, <code>\W</code></td>
<td>단어 문자(영문/숫자/\_), 단어 문자가 아닌 문자</td>
<td><code>[a-zA-Z0-9_]</code>, <code>[^a-zA-Z0-9_]</code></td>
<td><code>/\w+/</code> 는 변수 이름(e.g., "my_var1") 전체에 일치</td>
</tr>
<tr>
<td><code>\s</code>, <code>\S</code></td>
<td>공백 문자, 공백 문자가 아닌 문자</td>
<td><code>[ \f\n\r\t\v]</code>, <code>[^ \f\n\r\t\v]</code></td>
<td><code>/단어\s+단어/</code> 는 단어 사이의 여러 공백에 일치</td>
</tr>
<tr>
<td><code>\b</code>, <code>\B</code></td>
<td>단어 경계, 단어 경계가 아닌 위치</td>
<td></td>
<td><code>/\bcat\b/</code> 는 "cat"에 일치 ("catalogue"에는 불일치)</td>
</tr>
</tbody>
</table>
<h2>6. 자바스크립트 정규식 메서드 (6가지 주요 메서드)</h2>
<p>정규식을 활용하여 문자열을 처리하는 주요 메서드입니다. 괄호 안의 대상 객체에 따라 메서드 실행 주체가 달라집니다.</p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>적용 대상</th>
<th>설명</th>
<th>예시 (결과)</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>test()</code></td>
<td><code>RegExp</code></td>
<td>문자열이 패턴과 일치하는지 확인 (일치 여부만 **boolean**으로 반환)</td>
<td><code>/\d+/.test("a123"); // true</code></td>
</tr>
<tr>
<td><code>exec()</code></td>
<td><code>RegExp</code></td>
<td>일치 항목을 검색하고, 일치 정보(그룹 캡처, 인덱스 등)가 담긴 **배열 또는 null** 반환</td>
<td><code>/(\d+)/.exec("num123"); // ["123", "123", index: 3, ...]</code></td>
</tr>
<tr>
<td><code>match()</code></td>
<td><code>String</code></td>
<td>패턴과 일치하는 항목을 검색. <code>g</code> 플래그 유무에 따라 **모든 일치 항목 배열** 또는 상세 정보 배열 반환</td>
<td><code>"a1b2".match(/\d/g); // ["1", "2"]</code></td>
</tr>
<tr>
<td><code>matchAll()</code></td>
<td><code>String</code></td>
<td>전체 일치 항목에 대한 반복자(Iterator)를 **반환** (반드시 `g` 플래그 사용)</td>
<td><code>[..."a1b2".matchAll(/\d/g)]; // [["1", index: 1, ...], ["2", index: 3, ...]]</code></td>
</tr>
<tr>
<td><code>search()</code></td>
<td><code>String</code></td>
<td>일치하는 첫 번째 항목의 **인덱스** 검색</td>
<td><code>"hello world".search(/world/); // 6 (일치하지 않으면 -1)</code></td>
</tr>
<tr>
<td><code>replace()</code></td>
<td><code>String</code></td>
<td>일치하는 패턴을 다른 문자열로 **치환** (원본 변경 X, 새 문자열 반환)</td>
<td><code>"a.b.c".replace(/\./g, "-"); // "a-b-c"</code></td>
</tr>
<tr>
<td><code>split()</code></td>
<td><code>String</code></td>
<td>정규식을 구분자로 사용하여 문자열을 **배열로 분할**</td>
<td><code>"1, 2, 3".split(/,\s*/); // ["1", "2", "3"]</code></td>
</tr>
</tbody>
</table>
<hr>
<div class="tester-container">
<h2>📝 정규식 테스트 환경 (match() 기반)</h2>
<p>패턴과 문자열을 입력하고 버튼을 눌러 결과를 확인해 보세요. (기본적으로 `match()`의 결과를 보여줍니다.)</p>
<div class="input-group">
<label for="regexPattern">정규식 패턴 (슬래시 제외):</label>
<input type="text" id="regexPattern" value="a(b+)c" placeholder="예: \d{3}-\d{4}">
</div>
<div class="input-group">
<label for="regexFlags">플래그 (e.g., g, i, m):</label>
<input type="text" id="regexFlags" value="g" placeholder="예: gi">
</div>
<div class="input-group">
<label for="testString">테스트할 문자열:</label>
<textarea id="testString" rows="4" placeholder="예: 010-1234-5678, test@email.com">xyzabccabccc</textarea>
</div>
<button onclick="runRegexTest()">정규식 테스트 실행</button>
<h3 style="border-bottom: none;">결과 (Output)</h3>
<div id="output">여기에 결과가 표시됩니다.</div>
</div>
<script>
function runRegexTest() {
const pattern = document.getElementById('regexPattern').value;
const flags = document.getElementById('regexFlags').value;
const testString = document.getElementById('testString').value;
const outputDiv = document.getElementById('output');
outputDiv.innerHTML = '테스트 중...';
try {
// 1. RegExp 객체 생성
const regex = new RegExp(pattern, flags);
// 2. String.prototype.match() 메서드 사용
const matchResult = testString.match(regex);
let resultText = '--- String.prototype.match() 결과 ---\n';
if (matchResult) {
resultText += `일치 성공! (총 ${matchResult.length}개)\n\n`;
if (flags.includes('g')) {
// g 플래그 사용 시: 전체 일치 목록 출력
resultText += '일치 항목 (전체):\n';
matchResult.forEach((match, index) => {
resultText += `[${index + 1}]: ${match}\n`;
});
} else {
// g 플래그 미사용 시: 첫 번째 일치 및 그룹 캡처 정보 출력
resultText += `전체 일치: ${matchResult[0]}\n`;
resultText += `시작 인덱스: ${matchResult.index}\n`;
resultText += `원본 문자열: ${matchResult.input}\n`;
if (matchResult.length > 1) {
resultText += '\n캡처 그룹 목록:\n';
for (let i = 1; i < matchResult.length; i++) {
resultText += `그룹 ${i}: ${matchResult[i] || '(캡처 없음)'}\n`;
}
}
}
} else {
resultText += '일치하는 항목이 없습니다.';
}
// 3. RegExp.prototype.test() 결과 추가
const regexForTest = new RegExp(pattern, flags);
const testResult = regexForTest.test(testString);
resultText += `\n\n--- RegExp.prototype.test() 결과 ---\n`;
resultText += `일치 여부: ${testResult}`;
// 4. String.prototype.search() 결과 추가
const regexForSearch = new RegExp(pattern);
const searchResult = testString.search(regexForSearch);
resultText += `\n\n--- String.prototype.search() 결과 ---\n`;
resultText += `첫 번째 일치 인덱스: ${searchResult} (${searchResult === -1 ? '일치 없음' : '성공'})`;
outputDiv.innerText = resultText;
} catch (e) {
outputDiv.innerText = `[정규식 패턴 오류 발생]\n${e.message}\n\n*특수문자(예: *, +, ?, ( 등)을 일반 문자로 쓰려면 \\를 앞에 붙여야 합니다. (예: \\*)`;
}
}
// 페이지 로드 시 기본 테스트 실행
window.onload = runRegexTest;
</script>
</body>
</html>
video
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>스크롤 동영상 자동 재생</title>
<style>
/* 기본 스타일 및 레이아웃 */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
margin: 0;
background-color: #f0f2f5;
color: #333;
}
.container {
max-width: 100%;
padding: 0 10px;
box-sizing: border-box;
}
.header,
.footer {
text-align: center;
padding: 80vh 20px; /* 스크롤 공간 확보 */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 200px;
}
.header h1 {
margin-bottom: 8px;
}
.header p {
margin-top: 0;
color: #666;
}
/* 비디오 행 스타일 */
.video-row {
display: flex;
gap: 8px; /* 비디오 사이의 간격 */
margin-bottom: 10vh; /* 각 행 사이의 스크롤 공간 확보 */
background-color: #fff;
padding: 8px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
opacity: 0.5; /* 비활성 상태일 때 투명도 */
transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out;
transform: scale(0.95);
}
/* 활성화된 행 스타일 */
.video-row.is-playing {
opacity: 1;
transform: scale(1);
}
/* 마지막 비디오 행의 아래쪽 마진 제거 */
.video-row:last-of-type {
margin-bottom: 0;
}
/* 비디오 아이템을 감싸는 래퍼 */
.video-wrapper {
position: relative;
width: calc(50% - 4px); /* 2열 그리드 */
cursor: pointer;
}
/* 비디오 아이템 스타일 */
.video-item {
width: 100%;
height: 250px; /* 모든 비디오의 높이를 고정 */
object-fit: cover; /* 비율을 유지하면서 래퍼를 꽉 채움 (잘리는 부분 발생 가능) */
border-radius: 8px;
background-color: #000;
display: block;
}
/* 자세히보기 링크 */
.details-link {
display: none; /* 평소에는 숨김 */
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 2147483647; /* 비디오 컨트롤러보다 위에 오도록 */
color: #fff;
background-color: rgba(0, 0, 0, 0.6);
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
text-decoration: none;
border: 1px solid rgba(255, 255, 255, 0.5);
}
/* 전체화면 이전 버튼 */
.fullscreen-back-btn {
display: none; /* 평소에는 숨김 */
position: absolute;
top: 20px;
left: 20px;
z-index: 2147483647; /* 비디오 컨트롤러보다 위에 오도록 */
padding: 8px 16px;
background-color: rgba(0, 0, 0, 0.6);
color: white;
border: 1px solid white;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
}
/* 래퍼가 전체화면일 때 '이전' 버튼과 '자세히보기' 링크 표시 */
.video-wrapper:fullscreen .fullscreen-back-btn,
.video-wrapper:fullscreen .details-link {
display: block;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>스크롤하여 동영상 재생</h1>
<p>화면 중앙에 오는 행의 동영상이 재생됩니다.</p>
</div>
<!-- 첫번째 행 -->
<div class="video-row">
<div class="video-wrapper">
<video class="video-item" muted playsinline poster="https://via.placeholder.com/400x300.png?text=Video+1-1" src="https://www.w3schools.com/html/mov_bbb.mp4"></video>
<button class="fullscreen-back-btn">< 이전</button>
<a href="#" class="details-link">자세히보기</a>
</div>
<div class="video-wrapper">
<video class="video-item" muted playsinline poster="https://via.placeholder.com/400x300.png?text=Video+1-2" src="https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4"></video>
<button class="fullscreen-back-btn">< 이전</button>
<a href="#" class="details-link">자세히보기</a>
</div>
</div>
<!-- 두번째 행 -->
<div class="video-row">
<div class="video-wrapper">
<video class="video-item" muted playsinline poster="https://via.placeholder.com/400x300.png?text=Video+2-1" src="https://www.w3schools.com/html/mov_bbb.mp4"></video>
<button class="fullscreen-back-btn">< 이전</button>
<a href="#" class="details-link">자세히보기</a>
</div>
<div class="video-wrapper">
<video class="video-item" muted playsinline poster="https://via.placeholder.com/400x300.png?text=Video+2-2" src="https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4"></video>
<button class="fullscreen-back-btn">< 이전</button>
<a href="#" class="details-link">자세히보기</a>
</div>
</div>
<!-- 세번째 행 -->
<div class="video-row">
<div class="video-wrapper">
<video class="video-item" muted playsinline poster="https://via.placeholder.com/400x300.png?text=Video+3-1" src="https://www.w3schools.com/html/mov_bbb.mp4"></video>
<button class="fullscreen-back-btn">< 이전</button>
<a href="#" class="details-link">자세히보기</a>
</div>
<div class="video-wrapper">
<video class="video-item" muted playsinline poster="https://via.placeholder.com/400x300.png?text=Video+3-2" src="https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4"></video>
<button class="fullscreen-back-btn">< 이전</button>
<a href="#" class="details-link">자세히보기</a>
</div>
</div>
<div class="footer">
<p>페이지 끝</p>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const videoRows = document.querySelectorAll('.video-row');
const videoWrappers = document.querySelectorAll('.video-wrapper');
// 첫 번째 비디오 재생이 끝나면 두 번째 비디오를 재생하는 핸들러
const playNextVideo = (e) => {
// e.target은 첫 번째 비디오
const firstVideo = e.target;
const firstVideoWrapper = firstVideo.closest('.video-wrapper');
// 두 번째 비디오는 nextElementSibling으로 찾음
const secondVideoWrapper = firstVideoWrapper.nextElementSibling;
if (secondVideoWrapper && secondVideoWrapper.classList.contains('video-wrapper')) {
const secondVideo = secondVideoWrapper.querySelector('.video-item');
secondVideo.play().catch(error => console.error("두 번째 비디오 재생 실패:", error));
}
};
const observerOptions = {
root: null, // 뷰포트를 root로 사용
rootMargin: '-50% 0px -50% 0px', // 뷰포트의 수직 중앙 지점을 기준으로 교차 감지
threshold: 0 // 요소가 1px이라도 중앙 영역에 들어오면 콜백 실행
};
const intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const row = entry.target;
const videos = row.querySelectorAll('.video-item');
const firstVideo = videos[0];
if (entry.isIntersecting) {
// 행이 화면 중앙에 들어왔을 때
row.classList.add('is-playing'); // 활성 스타일 적용
// 첫 번째 비디오 재생
firstVideo.play().catch(error => console.error("자동 재생 실패:", error));
// 첫 번째 비디오가 끝나면 두 번째 비디오를 재생하도록 이벤트 리스너 추가
// 'ended' 이벤트는 한 번만 실행되도록 { once: true } 옵션 추가
firstVideo.addEventListener('ended', playNextVideo, { once: true });
} else {
// 행이 화면 중앙에서 벗어났을 때
row.classList.remove('is-playing'); // 활성 스타일 제거
// 이 행의 모든 비디오 정지 및 시간 리셋
videos.forEach(video => {
video.pause();
video.currentTime = 0;
});
// 다른 곳에서 트리거되지 않도록 이벤트 리스너 제거
firstVideo.removeEventListener('ended', playNextVideo);
}
});
}, observerOptions);
// 각 비디오 행을 observer에 등록
videoRows.forEach(row => {
intersectionObserver.observe(row);
});
// 각 비디오 래퍼에 이벤트 리스너 설정
videoWrappers.forEach(wrapper => {
const video = wrapper.querySelector('.video-item');
const backBtn = wrapper.querySelector('.fullscreen-back-btn');
const detailsLink = wrapper.querySelector('.details-link');
// 자세히보기 링크 클릭
detailsLink.addEventListener('click', (e) => {
e.stopPropagation(); // 부모 요소로의 이벤트 전파를 막습니다.
console.log('자세히보기 링크 클릭:', video.src);
// 실제 링크 이동 로직: location.href = '...';
});
// 비디오 래퍼 클릭 시 전체화면
wrapper.addEventListener('click', () => {
// 이미 다른 요소가 전체화면인 경우 무시
if (document.fullscreenElement || document.webkitFullscreenElement) {
return;
}
// iOS Safari는 video 요소에 대해서만 webkitEnterFullscreen을 지원합니다.
if (typeof video.webkitEnterFullscreen === 'function') {
// iOS에서는 네이티브 플레이어로 전환되므로, 소리만 켜줍니다.
video.muted = false;
video.webkitEnterFullscreen();
} else if (typeof wrapper.requestFullscreen === 'function') {
// 다른 브라우저에서는 래퍼를 전체화면으로 만듭니다.
wrapper.requestFullscreen().catch(err => {
console.error(`전체화면 전환 오류: ${err.message} (${err.name})`);
});
}
});
// 전체화면 '이전' 버튼 클릭
backBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 부모 요소로의 이벤트 전파를 막습니다.
if (document.fullscreenElement) {
document.exitFullscreen();
}
});
});
// 전체화면 상태 변경 핸들러
const handleFullscreenChange = () => {
const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement;
if (!fullscreenElement) {
// 전체화면이 해제될 때
const previouslyFullscreenVideo = document.querySelector('.video-item[data-fullscreened="true"]');
if (previouslyFullscreenVideo) {
previouslyFullscreenVideo.muted = true;
previouslyFullscreenVideo.controls = false;
delete previouslyFullscreenVideo.dataset.fullscreened; // 속성 제거
}
} else {
// 전체화면이 될 때
let videoInFullscreen;
if (fullscreenElement.classList.contains('video-wrapper')) {
videoInFullscreen = fullscreenElement.querySelector('.video-item');
} else if (fullscreenElement.tagName === 'VIDEO') {
videoInFullscreen = fullscreenElement;
}
if (videoInFullscreen) {
videoInFullscreen.dataset.fullscreened = "true"; // 전체화면이 된 비디오에 표시
// 표준 API를 사용하는 브라우저를 위해 속성 설정
if (!videoInFullscreen.webkitEnterFullscreen) {
videoInFullscreen.muted = false;
videoInFullscreen.controls = true;
}
}
}
};
// 전체화면 변경 이벤트를 표준과 웹킷 접두사 모두에 대해 등록
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
});
</script>
</body>
</html>
html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>전체 동의 체크박스</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="agreement-plugin.js"></script>
<style>
ul { list-style-type: none; }
ul ul { margin-left: 20px; }
</style>
</head>
<body>
<h2>전체 동의 체크박스 테스트</h2>
<div class="terms">
<div class="check_wrap">
<div class="box_chk">
<label>
<input type="checkbox" name="" class="agree_all">
전체 동의
</label>
</div>
</div>
<div class="box_terms_wrap">
<div class="agree_wrap">
<div class="box_check">
<input type="checkbox" name="" id="agree01">
<label for="agree01">동의 1</label>
</div>
</div>
</div>
<div class="box_terms_wrap">
<div class="agree_wrap">
<div class="box_check">
<input type="checkbox" name="" id="agree02">
<label for="agree02">동의 2</label>
</div>
<ul class="agree_list">
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree02_1">
<label for="agree02_1">자식 동의 1</label>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree02_2">
<label for="agree02_2">자식 동의 2</label>
</p>
<!-- 2-depth 자식 그룹 (손자) -->
<ul class="agree_list">
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree02_2_1">
<label for="agree02_2_1">손자 동의 1 (2-depth)</label>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree02_2_2">
<label for="agree02_2_2">손자 동의 2 (2-depth)</label>
</p>
</li>
</ul>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree02_3">
<label for="agree02_3">자식 동의 3</label>
</p>
</li>
</ul>
</div>
</div>
<div class="box_terms_wrap">
<div class="agree_wrap">
<div class="box_check">
<input type="checkbox" name="" id="agree03">
<label for="agree03">동의 3</label>
</div>
</div>
</div>
<div class="box_terms_wrap">
<div class="agree_wrap">
<div class="box_check">
<input type="checkbox" name="" id="agree04">
<label for="agree04">동의 2</label>
</div>
<ul class="agree_list">
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree04_1">
<label for="agree04_1">자식 동의 1</label>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree04_2">
<label for="agree04_2">자식 동의 2</label>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree04_3">
<label for="agree04_3">자식 동의 3</label>
</p>
</li>
</ul>
</div>
</div>
</div>
<hr>
<div class="form_items">
<div class="agree_list01">
<div class="box_terms_wrap">
<div class="agree_wrap">
<div class="box_check">
<input type="checkbox" name="" id="agree1">
<label for="agree1">동의 1</label>
</div>
</div>
</div>
<div class="box_terms_wrap">
<div class="agree_wrap">
<div class="box_check">
<input type="checkbox" name="" id="agree2">
<label for="agree2">동의 2</label>
</div>
<ul class="agree_list">
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree2_1">
<label for="agree2_1">자식 동의 1</label>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree2_2">
<label for="agree2_2">자식 동의 2</label>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree2_3">
<label for="agree2_3">자식 동의 3</label>
</p>
</li>
</ul>
</div>
</div>
<div class="box_terms_wrap">
<div class="agree_wrap">
<div class="box_check">
<input type="checkbox" name="" id="agree3">
<label for="agree3">동의 3</label>
</div>
</div>
</div>
<div class="box_terms_wrap">
<div class="agree_wrap">
<div class="box_check">
<input type="checkbox" name="" id="agree4">
<label for="agree4">동의 2</label>
</div>
<ul class="agree_list">
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree4_1">
<label for="agree4_1">자식 동의 1</label>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree4_2">
<label for="agree4_2">자식 동의 2</label>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree4_3">
<label for="agree4_3">자식 동의 3</label>
</p>
</li>
</ul>
</div>
</div>
</div>
</div>
<script>
$(function() {
// 첫 번째 약관 그룹(.terms)에 플러그인을 기본 설정으로 적용합니다.
$('.terms').agreementCheckboxHandler();
// 두 번째 약관 그룹(.form_items)에도 플러그인을 적용합니다.
// 이 그룹은 '전체 동의' 체크박스의 클래스가 다르므로, 옵션을 통해 지정해줍니다.
$('.form_items').agreementCheckboxHandler({
allCheckbox: '.agree_group_all' // '전체 동의' 체크박스 선택자 변경
});
});
</script>
</body>
</html>
(function($) {
/**
* @name agreementCheckboxHandler
* @description 다양한 구조의 약관 동의 체크박스 상호작용을 처리하는 유연한 jQuery 플러그인입니다.
* @version 2.1.0
*
* @param {Object} options - 플러그인 동작을 제어하는 설정 객체입니다.
* @param {string} [options.allCheckbox='.agree_all'] - '전체 동의' 체크박스를 식별하는 CSS 선택자입니다. 해당하는 요소가 없으면 '전체 동의' 기능은 비활성화됩니다.
* @param {string} [options.mainCheckbox='.agree_wrap > .box_check > input[type="checkbox"]'] - '전체 동의' 상태를 결정하는 최상위 레벨의 동의 항목들을 식별하는 선택자입니다.
* @param {string} [options.checkboxWrapper='p, .box_check'] - 개별 체크박스와 라벨을 감싸는 컨테이너의 선택자입니다. 부모-자식 관계를 파악하는 데 사용됩니다.
* @param {string} [options.childList='.agree_list'] - 자식 동의 항목들을 담고 있는 리스트 컨테이너의 선택자입니다. (예: 'ul.agree_list', 'div.child-group')
* @param {string} [options.childListItem='li'] - 자식 리스트 컨테이너 내부의 각 항목을 감싸는 요소의 선택자입니다. (예: 'li', 'div.item')
*
* @example
* // 기본 설정으로 플러그인 실행 (ul > li 구조)
* $('.terms').agreementCheckboxHandler();
*
* // 자식 리스트가 div > div 구조일 경우
* $('.terms-div').agreementCheckboxHandler({
* childList: 'div.agree_list',
* childListItem: 'div.agree_item'
* });
*/
$.fn.agreementCheckboxHandler = function(options) {
// 1. 기본 설정과 사용자 정의 옵션을 병합합니다.
const settings = $.extend({
allCheckbox: '.agree_all',
mainCheckbox: '.agree_wrap > .box_check > input[type="checkbox"]',
checkboxWrapper: 'p, .box_check',
childList: '.agree_list',
childListItem: 'li',
}, options);
// 2. 선택된 각 컨테이너(예: '.terms', '.form_items')에 대해 플러그인을 개별적으로 실행합니다.
return this.each(function() {
const $container = $(this);
const $agreeAll = $container.find(settings.allCheckbox);
// --- 헬퍼 함수 ---
/**
* 상위 부모 체크박스들의 상태를 재귀적으로 업데이트합니다.
* @param {jQuery} $checkbox - 상태 변경을 시작한 체크박스
*/
function updateParents($checkbox) {
// 1. 현재 체크박스를 포함하는 가장 가까운 자식 리스트 컨테이너를 찾습니다.
const $list = $checkbox.closest(settings.childList);
if (!$list.length) return; // 자식 리스트에 속하지 않으면 부모가 없으므로 종료합니다.
// 2. 해당 리스트($list)의 직계 자식 항목에 포함된 모든 체크박스들을 찾습니다.
// 이 체크박스들은 서로 '형제' 관계에 있습니다.
// (예: ul.agree_list > li > p > input)
const $siblings = $list.find('> ' + settings.childListItem + ' > ' + settings.checkboxWrapper).find('> input[type="checkbox"]');
// 3. 모든 형제들이 체크되었는지 확인합니다.
const allChecked = $siblings.length > 0 && $siblings.length === $siblings.filter(':checked').length;
// 4. 이 자식 리스트($list)를 소유한 '부모 체크박스'를 찾습니다.
// - 시나리오 1 (1-depth 자식): 부모 체크박스의 래퍼(.box_check)와 자식 리스트(.agree_list)가 형제 관계일 때.
// - 시나리오 2 (Multi-depth 자식): 자식 리스트(.agree_list)가 부모 체크박스를 포함하는 리스트 아이템(li) 내부에 중첩되어 있을 때.
let $parentCheckbox = $list.siblings(settings.checkboxWrapper).find('input[type="checkbox"]');
if (!$parentCheckbox.length) {
// 시나리오 2: 가장 가까운 상위 리스트 아이템(li)을 찾아, 그 바로 아래에 있는 체크박스를 부모로 간주합니다.
$parentCheckbox = $list.closest(settings.childListItem).find('> ' + settings.checkboxWrapper).find('input[type="checkbox"]');
}
if (!$parentCheckbox.length) return; // 부모를 찾지 못하면 종료합니다.
// 5. 부모의 상태가 현재 자식들의 집계 상태와 다를 경우에만 업데이트하고, 다시 상위 부모의 업데이트를 요청합니다.
if ($parentCheckbox.prop('checked') !== allChecked) {
$parentCheckbox.prop('checked', allChecked);
updateParents($parentCheckbox); // 재귀 호출로 연쇄적으로 부모를 업데이트합니다.
}
}
/**
* '전체 동의' 체크박스의 상태를 업데이트합니다.
*/
function updateAgreeAllState() {
if ($agreeAll.length) {
const $mainCheckboxes = $container.find(settings.mainCheckbox);
const allMainChecked = $mainCheckboxes.length > 0 && $mainCheckboxes.length === $mainCheckboxes.filter(':checked').length;
$agreeAll.prop('checked', allMainChecked);
}
}
// --- 이벤트 핸들러 ---
// 3. '전체 동의' 체크박스에 대한 이벤트 핸들러 (존재하는 경우에만 바인딩)
if ($agreeAll.length) {
$agreeAll.on('change', function() {
// 컨테이너 내의 모든 체크박스 상태를 '전체 동의' 상태와 동기화합니다.
$container.find('input[type="checkbox"]').prop('checked', $(this).prop('checked'));
});
}
// 4. 개별 체크박스들에 대한 이벤트 핸들러 (이벤트 위임 사용)
$container.on('change', 'input[type="checkbox"]', function(e) {
const $this = $(this);
// '전체 동의' 체크박스 자체의 변경 이벤트는 위에서 이미 처리했으므로 무시합니다.
if ($agreeAll.length && $this.is($agreeAll)) {
return;
}
// A. 자식(하위) 요소들 상태 업데이트, 하향식 전파 (부모 체크 → 모든 자손 체크)
// 현재 체크박스에 속한 자식 리스트를 찾아 그 안의 모든 체크박스 상태를 변경합니다.
// 이 체크박스가 자식을 가지고 있지 않다면(손자가 없는 형태), $childList.length는 0이 되므로 아무 작업도 수행하지 않습니다.
const $childList = $this.closest(settings.checkboxWrapper).siblings(settings.childList);
if ($childList.length) {
$childList.find('input[type="checkbox"]').prop('checked', $this.prop('checked'));
}
// B. 부모(상위) 요소들 상태 업데이트, 상향식 전파 (자식 체크 → 모든 상위 부모 상태 업데이트)
updateParents($this);
// C. '전체 동의' 체크박스 상태 업데이트
updateAgreeAllState();
});
});
};
}(jQuery));
필수동의 경고창
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>전체 동의 체크박스</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="agreement-plugin.js"></script>
<style>
ul { list-style-type: none; }
ul ul { margin-left: 20px; }
</style>
</head>
<body>
<h2>전체 동의 체크박스 테스트</h2>
<div class="terms">
<div class="check_wrap">
<div class="box_chk">
<label>
<input type="checkbox" name="" class="agree_all">
전체 동의
</label>
</div>
</div>
<div class="box_terms_wrap">
<div class="agree_wrap">
<div class="box_check">
<input type="checkbox" name="" id="agree01" required>
<label for="agree01">동의 1</label>
</div>
</div>
</div>
<div class="box_terms_wrap">
<div class="agree_wrap">
<div class="box_check">
<input type="checkbox" name="" id="agree02">
<label for="agree02">동의 2</label>
</div>
<ul class="agree_list">
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree02_1">
<label for="agree02_1">자식 동의 1</label>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree02_2">
<label for="agree02_2">자식 동의 2</label>
</p>
<!-- 2-depth 자식 그룹 (손자) -->
<ul class="agree_list">
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree02_2_1">
<label for="agree02_2_1">손자 동의 1 (2-depth)</label>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree02_2_2">
<label for="agree02_2_2">손자 동의 2 (2-depth)</label>
</p>
</li>
</ul>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree02_3">
<label for="agree02_3">자식 동의 3</label>
</p>
</li>
</ul>
</div>
</div>
<div class="box_terms_wrap">
<div class="agree_wrap">
<div class="box_check">
<input type="checkbox" name="" id="agree03" required>
<label for="agree03">동의 3</label>
</div>
</div>
</div>
<div class="box_terms_wrap">
<div class="agree_wrap">
<div class="box_check">
<input type="checkbox" name="" id="agree04">
<label for="agree04">동의 2</label>
</div>
<ul class="agree_list">
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree04_1">
<label for="agree04_1">자식 동의 1</label>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree04_2">
<label for="agree04_2">자식 동의 2</label>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree04_3">
<label for="agree04_3">자식 동의 3</label>
</p>
</li>
</ul>
</div>
</div>
<div style="text-align: center; margin-top: 20px;">
<button type="button" id="btn-terms-next">다음</button>
</div>
</div>
<hr>
<div class="form_items">
<div class="agree_list01">
<div class="box_terms_wrap">
<div class="agree_wrap">
<div class="box_check">
<input type="checkbox" name="" id="agree1" required>
<label for="agree1">동의 1</label>
</div>
</div>
</div>
<div class="box_terms_wrap">
<div class="agree_wrap">
<div class="box_check">
<input type="checkbox" name="" id="agree2">
<label for="agree2">동의 2</label>
</div>
<ul class="agree_list">
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree2_1">
<label for="agree2_1">자식 동의 1</label>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree2_2">
<label for="agree2_2">자식 동의 2</label>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree2_3">
<label for="agree2_3">자식 동의 3</label>
</p>
</li>
</ul>
</div>
</div>
<div class="box_terms_wrap">
<div class="agree_wrap">
<div class="box_check">
<input type="checkbox" name="" id="agree3">
<label for="agree3">동의 3</label>
</div>
</div>
</div>
<div class="box_terms_wrap">
<div class="agree_wrap">
<div class="box_check">
<input type="checkbox" name="" id="agree4">
<label for="agree4">동의 2</label>
</div>
<ul class="agree_list">
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree4_1">
<label for="agree4_1">자식 동의 1</label>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree4_2">
<label for="agree4_2">자식 동의 2</label>
</p>
</li>
<li>
<p class="box_check02">
<input type="checkbox" name="" id="agree4_3">
<label for="agree4_3">자식 동의 3</label>
</p>
</li>
</ul>
</div>
</div>
</div>
<div style="text-align: center; margin-top: 20px;">
<button type="button" id="btn-form-items-next">다음</button>
</div>
</div>
<script>
$(function() {
// 첫 번째 약관 그룹(.terms)에 플러그인을 기본 설정으로 적용합니다.
$('.terms').agreementCheckboxHandler();
// 두 번째 약관 그룹(.form_items)에도 플러그인을 적용합니다.
// 이 그룹은 '전체 동의' 체크박스의 클래스가 다르므로, 옵션을 통해 지정해줍니다.
$('.form_items').agreementCheckboxHandler({
allCheckbox: '.agree_group_all' // '전체 동의' 체크박스 선택자 변경
});
/**
* 필수 약관 동의 유효성을 검사하는 함수
* @param {string} containerSelector - 검사를 수행할 최상위 컨테이너 선택자
* @returns {boolean} - 유효성 통과 여부
*/
function validateRequiredAgreements(containerSelector) {
const $container = $(containerSelector);
// 컨테이너 내에서 'required' 속성이 있지만 체크되지 않은 첫 번째 체크박스를 찾습니다.
const $firstUnchecked = $container.find('input[type="checkbox"][required]:not(:checked)').first();
if ($firstUnchecked.length > 0) {
// 미동의 항목이 있는 경우
const checkboxId = $firstUnchecked.attr('id');
const labelText = $(`label[for="${checkboxId}"]`).text();
alert(`'${labelText}' 항목은 필수 동의 항목입니다.`);
$firstUnchecked.focus(); // 사용자 편의를 위해 해당 항목으로 포커스 이동
return false; // 유효성 검사 실패
}
return true; // 유효성 검사 성공
}
// 첫 번째 그룹의 '다음' 버튼 클릭 이벤트
$('#btn-terms-next').on('click', function() {
if (validateRequiredAgreements('.terms')) {
alert('[첫 번째 그룹] 모든 필수 항목에 동의하셨습니다. 다음 단계로 진행합니다.');
}
});
// 두 번째 그룹의 '다음' 버튼 클릭 이벤트
$('#btn-form-items-next').on('click', function() {
if (validateRequiredAgreements('.form_items')) {
alert('[두 번째 그룹] 모든 필수 항목에 동의하셨습니다.');
}
});
});
</script>
</body>
</html>
<template id="ongoing-tasks-template">
<tr class="worker-1">
<td class="task-name">기존 진행중인 업무2</td>
<td class="task-worker">김삼성</td>
<td class="task-planner">기획자2</td>
<td class="task-duedate">2025-09-05</td>
<td class="task-assigneddate">2025-08-14</td>
<td class="task-actions">
<button class="edit-btn">수정</button>
<button class="complete-btn">완료</button>
<button class="delete-btn">삭제</button>
</td>
</tr><tr class="worker-2">
<td class="task-name">기존 진행중인 업무1</td>
<td class="task-worker">김현대</td>
<td class="task-planner">기획자1</td>
<td class="task-duedate">2025-08-20</td>
<td class="task-assigneddate">2025-08-14</td>
<td class="task-actions">
<button class="edit-btn">수정</button>
<button class="complete-btn">완료</button>
<button class="delete-btn">삭제</button>
</td>
</tr><tr class="worker-2">
<td class="task-name">제미나이 사용법</td>
<td class="task-worker">김현대</td>
<td class="task-planner">이기획</td>
<td class="task-duedate">2025-08-19</td>
<td class="task-assigneddate">2025-08-16</td>
<td class="task-actions">
<button class="edit-btn">수정</button>
<button class="complete-btn">완료</button>
<button class="delete-btn">삭제</button>
</td>
</tr><tr class="worker-3">
<td class="task-name">또 다른 업무 하나 추가</td>
<td class="task-worker">박삼성</td>
<td class="task-planner">삼기획</td>
<td class="task-duedate">2025-08-20</td>
<td class="task-assigneddate">2025-08-16</td>
<td class="task-actions">
<button class="edit-btn">수정</button>
<button class="complete-btn">완료</button>
<button class="delete-btn">삭제</button>
</td>
</tr><tr class="worker-4">
<td class="task-name">업무 1번 진행하는거</td>
<td class="task-worker">이신한</td>
<td class="task-planner">삼기획</td>
<td class="task-duedate">2025-08-19</td>
<td class="task-assigneddate">2025-08-16</td>
<td class="task-actions">
<button class="edit-btn">수정</button>
<button class="complete-btn">완료</button>
<button class="delete-btn">삭제</button>
</td>
</tr>
</template>
<template id="completed-tasks-template">
<tr>
<td>기존 완료된 업무1</td>
<td>현대</td>
<td>기획자3</td>
<td>2025-08-20</td>
<td>2025-08-10</td>
<td>2025-08-12</td>
</tr><tr>
<td>업무 리스트 추가하기....</td>
<td>이신한</td>
<td>삼기획</td>
<td>2025-08-21</td>
<td>2025-08-16</td>
<td>2025-08-16</td>
</tr>
</template>
버전2
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>업무 Todo List</title>
<link rel="stylesheet" href="style.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div class="container">
<div class="main-header">
<h1>업무 Todo List</h1>
<div class="header-buttons">
<button id="generateCodeBtn">업무 코드 생성</button>
<button id="generateBackupCodeBtn">완료된 업무 백업 코드 생성</button>
<button id="clearStorageBtn">업무 스토리지 비우기</button>
</div>
</div>
<div class="todo-input-area">
<label for="taskNameInput">업무명:</label>
<input type="text" id="taskNameInput" placeholder="업무명을 입력하세요">
<label for="workerSelect">작업자:</label>
<select id="workerSelect" name="worker">
<option value="김현대">김현대</option>
<option value="이신한">이신한</option>
<option value="박삼성">박삼성</option>
<option value="최국민">최국민</option>
</select>
<label for="plannerSelect">기획자:</label>
<select id="plannerSelect" name="planner">
<option value="일기획">일기획</option>
<option value="이기획">이기획</option>
<option value="삼기획">삼기획</option>
<option value="사기획">사기획</option>
</select>
<label for="dueDateInput">완료예정일:</label>
<input type="date" id="dueDateInput" name="duedate">
<button id="saveBtn">저장</button>
</div>
<div class="task-section">
<div id="ongoingTasksTitle"></div>
<table id="ongoingTasksTable">
<thead>
<tr>
<th>업무명</th>
<th>작업자</th>
<th>기획자</th>
<th>완료일</th>
<th>배정일</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr><td colspan="6">진행중인 업무를 불러오는 중...</td></tr>
</tbody>
</table>
</div>
<div class="task-section">
<h2>완료된 업무</h2>
<table id="completedTasksTable">
<thead>
<tr>
<th>업무명</th>
<th>작업자</th>
<th>기획자</th>
<th>완료일</th>
<th>배정일</th>
<th>실제 완료일</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr><td colspan="7">완료된 업무를 불러오는 중...</td></tr>
</tbody>
</table>
</div>
<div class="code-output-section">
<h3>진행중인 업무 HTML 코드</h3>
<pre id="ongoingCodeOutput" class="code-block"></pre>
<h3>완료된 업무 HTML 코드</h3>
<pre id="completedCodeOutput" class="code-block"></pre>
<h3>완료된 업무 백업 HTML 코드</h3>
<pre id="backupCodeOutput" class="code-block"></pre>
</div>
<div id="backup-section"><!-- 백업 섹션은 스크립트로 동적 로드됩니다. --></div>
</div>
<script src="script.js"></script>
</body>
</html>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f9;
margin: 0;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.main-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eee;
padding-bottom: 15px;
margin-bottom: 20px;
}
.main-header h1 {
margin: 0;
}
h1, h2, h3 {
color: #333;
}
.todo-input-area {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.header-buttons {
display: flex;
gap: 5px;
}
.header-buttons button,
.todo-input-area input,
.todo-input-area select, .todo-input-area button {
padding: 8px;
font-size: 14px;
border: 1px solid #ddd;
border-radius: 4px;
}
.todo-input-area button {
background-color: #5cb85c;
color: white;
cursor: pointer;
border: none;
}
#generateCodeBtn, #generateBackupCodeBtn { background-color: #0275d8; }
#clearStorageBtn { background-color: #f0ad4e; }
.code-output-section {
margin-top: 30px;
}
.task-section {
margin-bottom: 30px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
table th, table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
table th {
background-color: #f2f2f2;
font-weight: bold;
}
table tr:hover {
background-color: #f5f5f5;
}
.worker-1 { background-color: #e6f7ff; }
.worker-2 { background-color: #eafff0; }
.worker-3 { background-color: #fffbe6; }
.worker-4 { background-color: #f0e6ff; }
.worker-5 { background-color: #ffe6e6; }
.worker-6 { background-color: #e6fff7; }
.worker-7 { background-color: #fff0f5; }
.worker-8 { background-color: #f5ffef; }
/* ... 기존 CSS 코드 ... */
.worker-stats {
display: flex;
flex-wrap: wrap;
gap: 10px;
font-size: 12px;
margin-top: 5px;
}
.worker-stat {
padding: 3px 8px;
border-radius: 3px;
color: #333;
}
/* 아래는 worker-stat의 배경색을 작업자별로 지정하는 예시입니다. */
.worker-stat.worker-1 { background-color: #c9e9ff; }
.worker-stat.worker-2 { background-color: #d8ffdf; }
.worker-stat.worker-3 { background-color: #fff9d7; }
.worker-stat.worker-4 { background-color: #e3d7ff; }
.worker-stat.worker-5 { background-color: #ffdde6; }
.worker-stat.worker-6 { background-color: #d7fff2; }
.worker-stat.worker-7 { background-color: #ffd8f0; }
.worker-stat.worker-8 { background-color: #e9ffdf; }
/* ... 기존 CSS 코드 ... */
.task-actions button {
margin-right: 5px;
padding: 5px 10px;
cursor: pointer;
border: none;
border-radius: 3px;
color: white;
}
.edit-btn { background-color: #f0ad4e; }
.complete-btn { background-color: #5cb85c; }
.delete-btn { background-color: #d9534f; }
.save-edit-btn { background-color: #0275d8; }
.cancel-complete-btn {
padding: 5px 10px;
cursor: pointer;
border: none;
border-radius: 3px;
color: white;
background-color: #777; /* 회색 계열로 취소 버튼 표현 */
margin-right: 5px;
}
.delete-complete-btn {
padding: 5px 10px;
cursor: pointer;
border: none;
border-radius: 3px;
color: white;
background-color: #d9534f;
}
.backup-section {
margin-top: 40px;
}
.tab-container {
margin-top: 10px;
}
.tab-nav {
display: flex;
flex-wrap: wrap;
border-bottom: 1px solid #ddd;
}
.tab-nav button {
padding: 12px 18px;
border: none;
border-bottom: 3px solid transparent; /* 활성 상태 표시줄 공간 확보 */
background-color: transparent;
cursor: pointer;
font-size: 15px;
color: #555;
margin-bottom: -1px; /* 활성 테두리가 컨테이너 테두리와 겹치도록 설정 */
transition: color 0.2s ease, border-color 0.2s ease, background-color 0.2s ease;
}
.tab-nav button:hover {
color: #000;
background-color: #f5f5f5;
}
.tab-nav button.active {
font-weight: 600;
color: #333;
border-bottom-color: #5cb85c; /* 앱의 기본 색상 사용 */
}
.tab-content {
border: 1px solid #ddd;
border-top: none;
padding: 20px;
border-radius: 0 0 4px 4px;
}
.tab-content .tab-panel {
display: none;
}
.tab-content .tab-panel.active {
display: block;
}
/**
* @file Todo List 애플리케이션의 모든 클라이언트 사이드 로직을 관리합니다.
*/
$(document).ready(function() {
// --- 1. 전역 변수 및 DOM 요소 캐싱 ---
const $taskNameInput = $('#taskNameInput');
const $workerSelect = $('#workerSelect');
const $plannerSelect = $('#plannerSelect');
const $dueDateInput = $('#dueDateInput');
const $saveBtn = $('#saveBtn');
const $ongoingTasksTableBody = $('#ongoingTasksTable tbody');
const $completedTasksTableBody = $('#completedTasksTable tbody');
const $ongoingTasksTitle = $('#ongoingTasksTitle');
const $generateCodeBtn = $('#generateCodeBtn');
const $clearStorageBtn = $('#clearStorageBtn');
const $ongoingCodeOutput = $('#ongoingCodeOutput');
const $completedCodeOutput = $('#completedCodeOutput');
const $generateBackupCodeBtn = $('#generateBackupCodeBtn');
const $backupCodeOutput = $('#backupCodeOutput');
// 애플리케이션 상태 변수
let ongoingTasks = [];
let completedTasks = [];
let isAscending = true; // 작업자 정렬 방향
let workerIdMap = {};
let nextWorkerId = 1;
/**
* 작업자 이름에 고유한 ID를 할당하고 반환합니다. (UI 색상 지정을 위해 사용)
* @param {string} workerName - 작업자 이름.
* @returns {number} 작업자의 고유 ID.
*/
const getWorkerId = (workerName) => {
if (!workerIdMap[workerName]) {
workerIdMap[workerName] = nextWorkerId++;
}
return workerIdMap[workerName];
};
// --- 2. 헬퍼(Helper) 함수 ---
/**
* 'YYYY-MM-DD' 형식의 오늘 날짜 문자열을 반환합니다.
* @returns {string} 오늘 날짜.
*/
const getTodayDate = () => {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
/**
* 현재 업무 목록(진행중, 완료)을 브라우저의 localStorage에 저장합니다.
*/
const saveToLocalStorage = () => {
localStorage.setItem('ongoingTasks', JSON.stringify(ongoingTasks));
localStorage.setItem('completedTasks', JSON.stringify(completedTasks));
};
// --- 3. 렌더링(Rendering) 함수 ---
/**
* '진행중인 업무' 목록과 통계를 화면에 렌더링합니다.
* [최적화] 배열(map)을 사용해 HTML 문자열을 만든 후 한 번에 DOM에 삽입하여 성능을 향상시킵니다.
*/
const renderOngoingTasks = () => {
const workerCounts = {};
// 업무 목록 HTML 생성
const tasksHtml = ongoingTasks.map((task, index) => {
const workerId = getWorkerId(task.worker);
// 작업자별 업무 수 계산
workerCounts[task.worker] = (workerCounts[task.worker] || 0) + 1;
return `
<tr class="worker-${workerId}" data-index="${index}">
<td class="task-name">${task.taskName}</td>
<td class="task-worker">${task.worker}</td>
<td class="task-planner">${task.planner}</td>
<td class="task-duedate">${task.dueDate}</td>
<td class="task-assigneddate">${task.assignedDate}</td>
<td class="task-actions">
<button class="edit-btn">수정</button>
<button class="complete-btn">완료</button>
<button class="delete-btn">삭제</button>
</td>
</tr>
`;
}).join('');
$ongoingTasksTableBody.html(tasksHtml || '<tr><td colspan="6">진행중인 업무가 없습니다.</td></tr>');
// 작업자별 통계 HTML 생성
const workerStatsHtml = Object.keys(workerCounts).map(worker => {
const workerId = getWorkerId(worker);
return `<span class="worker-stat worker-${workerId}">${worker}:${workerCounts[worker]}</span>`;
}).join('');
// 제목 및 통계 업데이트
$ongoingTasksTitle.html(`
<h2>진행중인 업무 ${ongoingTasks.length}건</h2>
<div class="worker-stats">${workerStatsHtml}</div>
`);
};
/**
* '완료된 업무' 목록을 화면에 렌더링합니다.
* [최적화] 배열(map)을 사용해 HTML 문자열을 만든 후 한 번에 DOM에 삽입하여 성능을 향상시킵니다.
*/
const renderCompletedTasks = () => {
const tasksHtml = completedTasks.map((task, index) => `
<tr data-index="${index}">
<td>${task.taskName}</td>
<td>${task.worker}</td>
<td>${task.planner}</td>
<td>${task.dueDate}</td>
<td>${task.assignedDate}</td>
<td>${task.completedDate}</td>
<td>
<button class="cancel-complete-btn">취소</button>
<button class="delete-complete-btn">삭제</button>
</td>
</tr>
`).join('');
// [수정] colspan을 7로 변경
$completedTasksTableBody.html(tasksHtml || '<tr><td colspan="7">완료된 업무가 없습니다.</td></tr>');
};
// --- 4. 데이터 로딩 및 초기화 ---
/**
* task.html 파일에서 <template> 태그를 읽어 초기 업무 데이터를 배열로 로드합니다.
* @returns {Promise<void>} 데이터 로드가 완료되면 resolve되는 Promise 객체.
*/
const loadTasksFromHtml = () => {
return new Promise((resolve, reject) => {
// task.html 파일을 한 번만 요청하여 효율성을 높입니다.
$.get('task.html').done(data => {
try {
const $html = $(data);
// 진행중인 업무 템플릿 파싱
const ongoingTemplateContent = $html.filter('#ongoing-tasks-template').prop('content');
ongoingTasks = []; // 배열 초기화
$(ongoingTemplateContent).children('tr').each(function() {
const $tr = $(this);
const task = {
taskName: $tr.find('.task-name').text().trim(),
worker: $tr.find('.task-worker').text().trim(),
planner: $tr.find('.task-planner').text().trim(),
dueDate: $tr.find('.task-duedate').text().trim(),
assignedDate: $tr.find('.task-assigneddate').text().trim()
};
getWorkerId(task.worker);
ongoingTasks.push(task);
});
// 완료된 업무 템플릿 파싱
const completedTemplateContent = $html.filter('#completed-tasks-template').prop('content');
completedTasks = []; // 배열 초기화
$(completedTemplateContent).children('tr').each(function() {
const $tr = $(this);
const task = {
taskName: $tr.children().eq(0).text().trim(),
worker: $tr.children().eq(1).text().trim(),
planner: $tr.children().eq(2).text().trim(),
dueDate: $tr.children().eq(3).text().trim(),
assignedDate: $tr.children().eq(4).text().trim(),
completedDate: $tr.children().eq(5).text().trim()
};
getWorkerId(task.worker);
completedTasks.push(task);
});
resolve();
} catch (e) {
reject(e);
}
}).fail((jqXHR, textStatus, error) => reject(error));
});
};
/**
* 애플리케이션을 시작하는 메인 함수입니다.
* 애플리케이션을 초기화합니다.
* LocalStorage에 데이터가 있으면 로드하고, 없으면 HTML 템플릿에서 기본값을 로드합니다.
*/
const initializeApp = () => {
// [개선] 백업 섹션 UI를 비동기적으로 로드합니다.
$('#backup-section').load('task_backup.html');
const storedOngoingTasks = localStorage.getItem('ongoingTasks');
const storedCompletedTasks = localStorage.getItem('completedTasks');
if (storedOngoingTasks && storedCompletedTasks) {
ongoingTasks = JSON.parse(storedOngoingTasks);
completedTasks = JSON.parse(storedCompletedTasks);
// 페이지 로드 시 workerIdMap을 다시 채웁니다.
ongoingTasks.forEach(task => getWorkerId(task.worker));
completedTasks.forEach(task => getWorkerId(task.worker));
renderOngoingTasks();
renderCompletedTasks();
} else {
loadTasksFromHtml().then(() => {
saveToLocalStorage();
renderOngoingTasks();
renderCompletedTasks();
}).catch(error => {
console.error("초기 데이터 로딩에 실패했습니다:", error);
$ongoingTasksTableBody.html('<tr><td colspan="6">초기 업무 로딩에 실패했습니다.</td></tr>');
$completedTasksTableBody.html('<tr><td colspan="6">초기 업무 로딩에 실패했습니다.</td></tr>');
});
}
setupEventListeners();
};
// --- 5. 이벤트 핸들러(Event Handlers) 등록 ---
const setupEventListeners = () => {
/**
* 입력 폼의 데이터를 기반으로 새로운 업무를 추가합니다.
* 입력값 유효성 검사를 수행하고, 성공 시 데이터를 저장하고 화면을 다시 렌더링합니다.
*/
const addTask = () => {
const taskName = $taskNameInput.val();
const worker = $workerSelect.val();
const planner = $plannerSelect.val();
const dueDate = $dueDateInput.val();
const assignedDate = getTodayDate();
if (taskName && worker && planner && dueDate) {
const newTask = { taskName, worker, planner, dueDate, assignedDate };
ongoingTasks.push(newTask);
saveToLocalStorage();
renderOngoingTasks();
// [UX 개선] 업무명만 초기화하여 연속적인 입력 편의성 향상
$taskNameInput.val('').focus();
} else {
alert('모든 필드를 입력해주세요.');
}
};
// 저장 버튼 클릭 시 업무 추가
$saveBtn.on('click', addTask);
// [UX 개선] 업무명 입력 필드에서 Enter 키를 눌러도 저장되도록 기능 추가
$taskNameInput.on('keypress', function(e) {
if (e.which === 13) { // Enter 키 코드
e.preventDefault(); // 기본 동작(폼 제출 등) 방지
addTask();
}
});
// [성능 최적화] 이벤트 위임(Event Delegation)을 사용하여 동적으로 추가된 버튼에도 이벤트가 동작하도록 합니다.
// '삭제' 버튼 클릭 이벤트
$ongoingTasksTableBody.on('click', '.delete-btn', function() {
if (confirm('정말로 삭제하시겠습니까?')) {
const $tr = $(this).closest('tr');
const index = $tr.data('index');
ongoingTasks.splice(index, 1);
saveToLocalStorage();
renderOngoingTasks();
}
});
// '수정' 버튼 클릭 이벤트: 테이블 행을 편집 가능한 폼으로 변경
$ongoingTasksTableBody.on('click', '.edit-btn', function() {
const $tr = $(this).closest('tr');
const index = $tr.data('index');
const task = ongoingTasks[index];
$tr.addClass('editing');
$tr.find('.task-name').html(`<input type="text" value="${task.taskName}">`);
$tr.find('.task-worker').html($workerSelect.clone().val(task.worker));
$tr.find('.task-duedate').html(`<input type="date" value="${task.dueDate}">`);
$tr.find('.task-actions').html('<button class="save-edit-btn">수정완료</button>');
});
// '수정완료' 버튼 클릭 이벤트: 변경된 내용을 저장
$ongoingTasksTableBody.on('click', '.save-edit-btn', function() {
const $tr = $(this).closest('tr');
const index = $tr.data('index');
const newTaskName = $tr.find('.task-name input').val();
const newWorker = $tr.find('.task-worker select').val();
const newDueDate = $tr.find('.task-duedate input').val();
if (newTaskName && newWorker && newDueDate) {
ongoingTasks[index].taskName = newTaskName;
ongoingTasks[index].worker = newWorker;
ongoingTasks[index].dueDate = newDueDate;
saveToLocalStorage();
renderOngoingTasks();
} else {
alert('수정할 내용을 모두 입력해주세요.');
}
});
// '완료' 버튼 클릭 이벤트: 진행중인 업무를 완료 목록으로 이동
// [수정] 확인(confirm) 창 없이 바로 완료 처리
$ongoingTasksTableBody.on('click', '.complete-btn', function() {
const $tr = $(this).closest('tr');
const index = $tr.data('index');
const completedTask = ongoingTasks.splice(index, 1)[0];
completedTask.completedDate = getTodayDate();
completedTasks.push(completedTask);
saveToLocalStorage();
renderOngoingTasks();
renderCompletedTasks();
});
// [신규] '취소' 버튼 클릭 이벤트: 완료된 업무를 진행중 목록으로 복원
$completedTasksTableBody.on('click', '.cancel-complete-btn', function() {
const $tr = $(this).closest('tr');
const index = $tr.data('index');
const taskToRestore = completedTasks.splice(index, 1)[0];
delete taskToRestore.completedDate; // 완료일 속성 제거
ongoingTasks.push(taskToRestore);
saveToLocalStorage();
renderOngoingTasks();
renderCompletedTasks();
});
// [신규] 완료된 업무 '삭제' 버튼 클릭 이벤트
$completedTasksTableBody.on('click', '.delete-complete-btn', function() {
if (confirm('이 완료된 업무를 영구적으로 삭제하시겠습니까?')) {
const $tr = $(this).closest('tr');
const index = $tr.data('index');
completedTasks.splice(index, 1);
saveToLocalStorage();
renderCompletedTasks();
}
});
// '업무 코드 생성' 버튼 클릭 이벤트
$generateCodeBtn.on('click', () => {
const ongoingTableHtml = $ongoingTasksTableBody.html();
const completedTableHtml = $completedTasksTableBody.html();
$ongoingCodeOutput.text(ongoingTableHtml);
$completedCodeOutput.text(completedTableHtml);
});
// '완료된 업무 백업 코드 생성' 버튼 클릭 이벤트
$generateBackupCodeBtn.on('click', () => {
if (completedTasks.length === 0) {
$backupCodeOutput.text('코드 생성할 완료된 업무 데이터가 없습니다.');
return;
}
// '완료된 업무' 데이터(completedTasks)를 기반으로 '비고' 열이 없는 HTML 코드를 생성합니다.
const backupBodyHtml = completedTasks.map(task => {
// 각 줄을 들여쓰기하여 가독성을 높입니다.
return ` <tr>
<td>${task.taskName}</td>
<td>${task.worker}</td>
<td>${task.planner}</td>
<td>${task.dueDate}</td>
<td>${task.assignedDate}</td>
<td>${task.completedDate}</td>
</tr>`;
}).join('\n');
$backupCodeOutput.text(backupBodyHtml);
});
// '업무 스토리지 비우기' 버튼 클릭 이벤트
$clearStorageBtn.on('click', () => {
if (confirm('모든 업무 데이터를 삭제하고 초기값으로 되돌리시겠습니까?')) {
localStorage.removeItem('ongoingTasks');
localStorage.removeItem('completedTasks');
// HTML 파일에서 초기 데이터를 다시 로드
loadTasksFromHtml().then(() => {
saveToLocalStorage();
renderOngoingTasks();
renderCompletedTasks();
}).catch(error => {
alert('초기화에 실패했습니다. task.html 파일이 있는지 확인해주세요.');
console.error(error);
});
$ongoingCodeOutput.empty();
$completedCodeOutput.empty();
$backupCodeOutput.empty();
}
});
// '작업자' 테이블 헤더 클릭 이벤트: 작업자 이름으로 정렬
$('#ongoingTasksTable th:contains("작업자")').on('click', () => {
isAscending = !isAscending;
ongoingTasks.sort((a, b) => {
const workerA = a.worker.toLowerCase();
const workerB = b.worker.toLowerCase();
if (isAscending) {
return workerA.localeCompare(workerB);
} else {
return workerB.localeCompare(workerA);
}
});
saveToLocalStorage();
renderOngoingTasks();
});
/**
* 주어진 DOM 요소의 텍스트 전체를 선택(블록 지정)합니다.
* @param {HTMLElement} element - 텍스트를 선택할 DOM 요소.
*/
const selectText = (element) => {
const range = document.createRange();
range.selectNodeContents(element);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
};
// 생성된 HTML 코드를 클릭하면 전체 선택되도록 하는 이벤트
$ongoingCodeOutput.on('click', function() {
selectText(this);
});
$completedCodeOutput.on('click', function() {
selectText(this);
});
$backupCodeOutput.on('click', function() {
selectText(this);
});
// [수정] 백업 탭 클릭 이벤트: 동적으로 로드된 콘텐츠에 이벤트 핸들러를 바인딩하기 위해 이벤트 위임 사용
// 정적 부모 요소인 #backup-section에 이벤트를 위임합니다.
$('#backup-section').on('click', '#backupTabNav button', function() {
const $this = $(this);
if ($this.hasClass('active')) {
return; // 이미 활성화된 탭이면 아무것도 안 함
}
// 모든 탭과 패널에서 active 클래스 제거
$('#backupTabNav .active').removeClass('active');
$('#backupTabContent .active').removeClass('active');
// 클릭된 탭과 해당 패널에 active 클래스 추가
$this.addClass('active');
$('#' + $this.data('tab')).addClass('active');
});
};
// --- 6. 애플리케이션 시작 ---
initializeApp();
});
<div class="task-section backup-section">
<h2>완료된 업무 백업</h2>
<div class="tab-container">
<div class="tab-nav" id="backupTabNav">
<button class="active" data-tab="panel-1">1월</button>
<button data-tab="panel-2">2월</button>
<button data-tab="panel-3">3월</button>
<button data-tab="panel-4">4월</button>
<button data-tab="panel-5">5월</button>
<button data-tab="panel-6">6월</button>
<button data-tab="panel-7">7월</button>
<button data-tab="panel-8">8월</button>
<button data-tab="panel-9">9월</button>
<button data-tab="panel-10">10월</button>
<button data-tab="panel-11">11월</button>
<button data-tab="panel-12">12월</button>
</div>
<div class="tab-content" id="backupTabContent">
<div class="tab-panel active" id="panel-1">
<h3>1월 완료된 업무</h3>
<table>
<thead>
<tr>
<th>업무명</th>
<th>작업자</th>
<th>기획자</th>
<th>완료일</th>
<th>배정일</th>
<th>실제 완료일</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr data-index="0">
<td>기존 완료된 업무1</td>
<td>현대</td>
<td>기획자3</td>
<td>2025-08-20</td>
<td>2025-08-10</td>
<td>2025-08-12</td>
<td><button class="cancel-complete-btn">취소</button></td>
</tr>
<tr data-index="1">
<td>업무 리스트 추가하기....</td>
<td>이신한</td>
<td>삼기획</td>
<td>2025-08-21</td>
<td>2025-08-16</td>
<td>2025-08-16</td>
<td><button class="cancel-complete-btn">취소</button></td>
</tr>
<tr data-index="2">
<td>업무명 자동저장 4</td>
<td>이신한</td>
<td>일기획</td>
<td>2025-08-19</td>
<td>2025-08-16</td>
<td>2025-08-16</td>
<td><button class="cancel-complete-btn">취소</button></td>
</tr>
</tbody>
</table>
</div>
<div class="tab-panel" id="panel-2">
<h3>2월 완료된 업무</h3>
<table>
<thead>
<tr>
<th>업무명</th>
<th>작업자</th>
<th>기획자</th>
<th>완료일</th>
<th>배정일</th>
<th>실제 완료일</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr><td colspan="7">2월 완료된 업무를 불러오는 중...</td></tr>
</tbody>
</table>
</div>
<div class="tab-panel" id="panel-3">
<h3>3월 완료된 업무</h3>
<table>
<thead>
<tr>
<th>업무명</th>
<th>작업자</th>
<th>기획자</th>
<th>완료일</th>
<th>배정일</th>
<th>실제 완료일</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr><td colspan="7">3월 완료된 업무를 불러오는 중...</td></tr>
</tbody>
</table>
</div>
<div class="tab-panel" id="panel-4">
<h3>4월 완료된 업무</h3>
<table>
<thead>
<tr>
<th>업무명</th>
<th>작업자</th>
<th>기획자</th>
<th>완료일</th>
<th>배정일</th>
<th>실제 완료일</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr><td colspan="7">4월 완료된 업무를 불러오는 중...</td></tr>
</tbody>
</table>
</div>
<div class="tab-panel" id="panel-5">
<h3>5월 완료된 업무</h3>
<table>
<thead>
<tr>
<th>업무명</th>
<th>작업자</th>
<th>기획자</th>
<th>완료일</th>
<th>배정일</th>
<th>실제 완료일</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr><td colspan="7">5월 완료된 업무를 불러오는 중...</td></tr>
</tbody>
</table>
</div>
<div class="tab-panel" id="panel-6">
<h3>6월 완료된 업무</h3>
<table>
<thead>
<tr>
<th>업무명</th>
<th>작업자</th>
<th>기획자</th>
<th>완료일</th>
<th>배정일</th>
<th>실제 완료일</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr><td colspan="7">6월 완료된 업무를 불러오는 중...</td></tr>
</tbody>
</table>
</div>
<div class="tab-panel" id="panel-7">
<h3>7월 완료된 업무</h3>
<table>
<thead>
<tr>
<th>업무명</th>
<th>작업자</th>
<th>기획자</th>
<th>완료일</th>
<th>배정일</th>
<th>실제 완료일</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr><td colspan="7">7월 완료된 업무를 불러오는 중...</td></tr>
</tbody>
</table>
</div>
<div class="tab-panel" id="panel-8">
<h3>8월 완료된 업무</h3>
<table>
<thead>
<tr>
<th>업무명</th>
<th>작업자</th>
<th>기획자</th>
<th>완료일</th>
<th>배정일</th>
<th>실제 완료일</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr><td colspan="7">8월 완료된 업무를 불러오는 중...</td></tr>
</tbody>
</table>
</div>
<div class="tab-panel" id="panel-9">
<h3>9월 완료된 업무</h3>
<table>
<thead>
<tr>
<th>업무명</th>
<th>작업자</th>
<th>기획자</th>
<th>완료일</th>
<th>배정일</th>
<th>실제 완료일</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr><td colspan="7">9월 완료된 업무를 불러오는 중...</td></tr>
</tbody>
</table>
</div>
<div class="tab-panel" id="panel-10">
<h3>10월 완료된 업무</h3>
<table>
<thead>
<tr>
<th>업무명</th>
<th>작업자</th>
<th>기획자</th>
<th>완료일</th>
<th>배정일</th>
<th>실제 완료일</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr><td colspan="7">10월 완료된 업무를 불러오는 중...</td></tr>
</tbody>
</table>
</div>
<div class="tab-panel" id="panel-11">
<h3>11월 완료된 업무</h3>
<table>
<thead>
<tr>
<th>업무명</th>
<th>작업자</th>
<th>기획자</th>
<th>완료일</th>
<th>배정일</th>
<th>실제 완료일</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr><td colspan="7">11월 완료된 업무를 불러오는 중...</td></tr>
</tbody>
</table>
</div>
<div class="tab-panel" id="panel-12">
<h3>12월 완료된 업무</h3>
<table>
<thead>
<tr>
<th>업무명</th>
<th>작업자</th>
<th>기획자</th>
<th>완료일</th>
<th>배정일</th>
<th>실제 완료일</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr><td colspan="7">12월 완료된 업무를 불러오는 중...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>