코드

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">&lt; 이전</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">&lt; 이전</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">&lt; 이전</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">&lt; 이전</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">&lt; 이전</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">&lt; 이전</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>