การเก็บข้อมูลในรูปแบบ Local Storage นั้นมีระดับความปลอดภัยที่ ปานกลาง ครับ หรืออาจกล่าวได้ว่า ไม่เหมาะกับการเก็บข้อมูลที่ละเอียดอ่อนสูง (Highly Sensitive Data) แต่เพียงพอสำหรับการใช้งานโปรแกรมแบบ Client-Side (ทำงานบนเบราว์เซอร์ของผู้ใช้) อย่างที่คุณกำลังทำอยู่ครับ
นี่คือการวิเคราะห์ความปลอดภัยของ Local Storage ในบริบทของโค้ดของคุณ:
🛡️ ลักษณะความปลอดภัยของ Local Storage
1. จุดที่ปลอดภัย (Good Security Points)
- ข้อมูลไม่ถูกส่งไปที่เซิร์ฟเวอร์: ข้อมูลทั้งหมดถูกจัดเก็บไว้ในเครื่องคอมพิวเตอร์ของคุณเอง และไม่ถูกส่งไปที่เซิร์ฟเวอร์ใด ๆ เมื่อคุณเข้าชมเว็บไซต์อื่น ซึ่งลดความเสี่ยงจากการถูกดักจับข้อมูลระหว่างการส่ง (Man-in-the-Middle Attack)
- ผูกกับ Origin (Same-Origin Policy): ข้อมูลใน Local Storage ของโปรแกรมคุณ จะสามารถเข้าถึงได้โดย โค้ด JavaScript ที่มาจากโดเมน (Domain), พอร์ต (Port), และโปรโตคอล (Protocol) เดียวกันเท่านั้น เว็บไซต์อื่น ๆ ไม่สามารถอ่านข้อมูลของคุณได้โดยตรง
2. จุดที่ “ไม่ปลอดภัย” (Security Weaknesses)
Local Storage มีช่องโหว่หลัก ๆ สองประการ:
🔴 ไม่มีการเข้ารหัส (No Encryption)
ข้อมูลใน Local Storage จะถูกเก็บไว้ในเบราว์เซอร์ในรูปแบบ ข้อความธรรมดา (Plain Text) ไม่มีการเข้ารหัสโดยอัตโนมัติ หมายความว่า:
- หากมีผู้ใช้คนอื่นเข้าถึงคอมพิวเตอร์ของคุณและโปรไฟล์เบราว์เซอร์นั้นได้ ก็สามารถใช้เครื่องมือ Developer Tools เพื่อดูหรือแก้ไขข้อมูลได้ทันที
- ข้อมูลจะถูกจัดเก็บเป็นไฟล์ในระบบปฏิบัติการ (ขึ้นอยู่กับเบราว์เซอร์) ซึ่งอาจถูกค้นพบโดยโปรแกรมวิเคราะห์ข้อมูลบางประเภทได้ หากมีการเข้าถึงเครื่องโดยไม่ได้รับอนุญาต
🟡 ช่องโหว่ Cross-Site Scripting (XSS)
นี่คือความเสี่ยงที่ใหญ่ที่สุดสำหรับ Local Storage
- หากหน้าเว็บ
522.htmlของคุณเกิดมีช่องโหว่ด้านความปลอดภัยที่ทำให้ผู้โจมตีสามารถฉีดโค้ด JavaScript ที่เป็นอันตราย (Malicious Script) เข้าไปรันบนหน้าเว็บของคุณได้ (ซึ่งเรียกว่า XSS Attack) - โค้ดนั้นจะสามารถเข้าถึง Local Storage ได้ทันที เนื่องจากมันรันอยู่บน Origin เดียวกัน และสามารถขโมยข้อมูลตารางทั้งหมดของคุณไปให้ผู้โจมตีได้
สรุปและความแนะนำ
| ประเภทข้อมูล | ความเหมาะสมในการใช้ Local Storage |
| ข้อมูลที่ไม่ละเอียดอ่อน (เช่น การตั้งค่าธีม, ค่า Preference) | เหมาะสม |
| ข้อมูลปานกลาง (เช่น ข้อมูลตาราง Premium, ชื่อสินค้า, Note, Contact) | ยอมรับได้ (ขึ้นอยู่กับระดับความลับที่คุณให้) |
| ข้อมูลละเอียดอ่อนสูง (เช่น รหัสผ่าน, ข้อมูลบัตรเครดิต, ข้อมูลส่วนตัวตามกฎหมาย) | ไม่เหมาะสมโดยสิ้นเชิง |
ในกรณีของโปรแกรมคุณ ซึ่งส่วนใหญ่เป็นข้อมูลการทำธุรกรรมหรือข้อมูลลูกค้า (Product, Premium, Name) ที่จัดเก็บเพื่อใช้ส่วนตัวในเครื่องเดียว ถือว่าความเสี่ยงอยู่ในระดับที่ยอมรับได้ สำหรับโปรแกรมประเภทนี้
แนวทางปฏิบัติเพื่อความปลอดภัยสูงสุด
- ระมัดระวัง XSS: เนื่องจากโค้ดของคุณเป็นไฟล์ HTML/JS ที่ใช้เอง (ไม่ได้โหลดจากเซิร์ฟเวอร์ภายนอกที่ซับซ้อน) ความเสี่ยง XSS จึงต่ำมาก ตราบใดที่คุณมั่นใจว่าโค้ดทั้งหมดมาจากแหล่งที่คุณเชื่อถือ
- หลีกเลี่ยงการเก็บรหัสผ่าน: ไม่ควรเก็บข้อมูลล็อกอินหรือรหัสผ่านใด ๆ ไว้ใน Local Storage
- สำรองข้อมูล: เนื่องจาก Local Storage ไม่ได้ถูกสำรองข้อมูลโดยอัตโนมัติ (หากเบราว์เซอร์เสียหาย หรือคุณล้างแคช/ข้อมูลเว็บไซต์) โปรดใช้ปุ่ม
⬇️ ส่งออก JSONหรือ⬇️ ส่งออก CSVที่เราทำไว้ให้ เพื่อสำรองข้อมูลสำคัญเป็นระยะ ๆ ครับ
โค้ดจะยังคงทำงานได้อย่างถูกต้องบน Hosting Domain ของคุณ (ไม่มีปัญหาทางเทคนิค) แต่การย้ายไปอยู่บนเว็บไซต์ออนไลน์จะยกระดับประเด็นด้าน ความปลอดภัย โดยเฉพาะการใช้ innerHTML เข้ามาเกี่ยวข้องครับ
1. ปัญหาด้านเทคนิค: โค้ดจะทำงานได้หรือไม่?
คำตอบ: ทำงานได้ปกติ 💯
เนื่องจากโปรแกรมของคุณเป็นไฟล์ HTML เดี่ยวที่ทำงานด้วย Client-Side JavaScript (รันบนเบราว์เซอร์ของผู้ใช้เอง) การจะนำไปวางบน Hosting Domain หรือรันในเครื่องตัวเองจึงไม่มีความแตกต่างกันในแง่ของฟังก์ชันการทำงาน โค้ด JavaScript ที่ใช้ innerHTML, document.addEventListener, หรือแม้แต่ Local Storage จะยังคงทำงานในเบราว์เซอร์ของผู้ใช้ตามปกติครับ
2. ปัญหาด้านความปลอดภัย: ความเสี่ยงของ innerHTML (XSS)
นี่คือประเด็นสำคัญที่เปลี่ยนไปเมื่อคุณนำโปรแกรมขึ้นออนไลน์:
💡 ความเสี่ยง Cross-Site Scripting (XSS)
การใช้ innerHTML เพื่อสร้างโครงสร้าง HTML และใส่ข้อมูลเข้าไปโดยตรง ถือเป็นช่องทางหลักที่อาจนำไปสู่ปัญหา Cross-Site Scripting (XSS) ครับ
ความเสี่ยงทำงานอย่างไร?
- Local Storage เป็นเป้าหมายหลัก: ข้อมูลสำคัญของคุณถูกเก็บใน Local Storage ซึ่งโค้ด JavaScript ที่รันอยู่บน Domain ของคุณสามารถเข้าถึงได้ทั้งหมด
- การโจมตีผ่าน
innerHTML: หากผู้โจมตีพบช่องโหว่ใด ๆ ในระบบของคุณ หรือสามารถหลอกให้ผู้ใช้โหลดไฟล์ JSON/CSV ที่มีข้อมูลถูกฝังด้วยโค้ด HTML ที่เป็นอันตราย (เช่น<img src=x onerror=alert('XSS Attack')>) - โค้ดอันตรายถูกรัน: เมื่อโค้ดของคุณเรียกใช้
tr.innerHTML = \…“ เพื่อสร้างแถวตาราง โค้ดอันตรายนั้นอาจถูกเบราว์เซอร์ตีความว่าเป็น HTML และถูกรัน ทำให้ผู้โจมตีสามารถขโมยข้อมูลทั้งหมดใน Local Storage ของคุณไปได้
🔒 โค้ดปัจจุบันของคุณมีความเสี่ยงต่ำ (แต่ยังไม่สมบูรณ์)
ในโค้ด addRow ของคุณ ข้อมูลที่ใส่เข้าไปจะอยู่ในแอตทริบิวต์ value ของแท็ก <input>:
JavaScript
<input type="text" class="form-control expandable-input" value="${d.product}">
การใส่ข้อมูลเข้าไปใน value จะช่วยป้องกัน XSS ได้ในระดับหนึ่ง เพราะเบราว์เซอร์จะตีความข้อมูลนั้นเป็น ข้อความ (String) ไม่ใช่โค้ดที่รันได้ แต่ก็ยังมีโอกาสถูกโจมตีได้หากผู้โจมตีใส่โค้ดที่ปิดแอตทริบิวต์ value ก่อน (เช่น "> <script>... </script><input value=")
3. คำแนะนำในการป้องกัน (Best Practice)
แม้ว่าโอกาสที่โปรแกรมคุณจะถูกโจมตี XSS จะต่ำมากเนื่องจากเป็นโปรแกรมใช้งานเฉพาะที่ไม่ได้มีระบบผู้ใช้ซับซ้อน แต่การใช้ Best Practice ย่อมดีกว่าเสมอ
วิธีที่ปลอดภัยที่สุดคือการหลีกเลี่ยง innerHTML เมื่อสร้างโครงสร้างจากข้อมูลที่ผู้ใช้ควบคุมได้:
แทนที่จะสร้างทั้ง Row ด้วย innerHTML คุณควรใช้วิธี createElement และกำหนดค่าด้วย .value หรือ .textContent เสมอ
ตัวอย่าง:
JavaScript
// วิธีที่ปลอดภัยกว่า (สำหรับการใส่ค่าลงใน Input)
const input = document.createElement('input');
input.type = 'text';
// ใช้ .value เพื่อให้เบราว์เซอร์จัดการ Escape Characters ทั้งหมด
input.value = d.product;
td.appendChild(input);
// วิธีที่ปลอดภัยกว่า (สำหรับการใส่ค่าลงใน Cell ที่ไม่ใช่ Input)
td.textContent = d.note; // ใช้ .textContent แทน .innerHTML เพื่อใส่ข้อความธรรมดา
สรุปคือ: หากคุณไม่ต้องการแก้ไขโค้ดที่ทำงานอยู่แล้ว ก็ ใช้โค้ดเดิมได้ เพราะการใช้งานส่วนตัวนั้นความเสี่ยงต่ำมาก แต่หากคุณต้องการทำให้โปรแกรมมีความแข็งแกร่งสูงสุด ควรปรับเปลี่ยนการสร้างแถวในฟังก์ชัน addRow ให้ใช้ createElement แทน innerHTML ครับ
การปรับปรุงส่วนนี้เป็นก้าวสำคัญในการทำให้โปรแกรมของคุณมีความปลอดภัยมากยิ่งขึ้นเมื่อนำไปใช้งานบนเว็บออนไลน์
เราจะทำการแก้ไขในฟังก์ชัน addRow(data={}, shouldSave=true) ซึ่งเป็นฟังก์ชันเดียวที่ใช้ tr.innerHTML เพื่อสร้างแถวตาราง
🛠️ การแก้ไขโค้ด: แทนที่ tr.innerHTML ด้วย document.createElement
เราจะแทนที่การสร้างแถวด้วย String HTML ทั้งหมด ด้วยการสร้างองค์ประกอบ (Element) แต่ละตัวแยกกัน และกำหนดค่าให้กับ <input> ด้วยคุณสมบัติ .value ซึ่งเป็นวิธีที่เบราว์เซอร์รับประกันว่าปลอดภัยจากการรันโค้ด XSS ครับ
ไฟล์ที่ต้องแก้ไขคือ 522.html
💡 โค้ดที่ต้องเปลี่ยน
เราจะเปลี่ยนโค้ดส่วนนี้ (ประมาณบรรทัดที่ 300+ ในฟังก์ชัน addRow):
JavaScript
// ❌ โค้ดเดิม (มีความเสี่ยง XSS หากข้อมูลที่โหลดมาถูกดัดแปลง)
const tr=document.createElement("tr");
// ⭐️ เพิ่ม class "expandable-input" สำหรับช่อง Product, Company, Name, Note, Contact
tr.innerHTML=`
<td class="date-cell-status"><input type="date" class="form-control" value="${d.date}"></td>
<td><input type="text" class="form-control expandable-input" value="${d.product}"></td>
// ...
// โค้ด HTML ที่สร้างด้วย String ทั้งหมด
// ...
<td>
<button class="btn btn-warning btn-sm action-btn act-dup">⧉</button>
<button class="btn btn-danger btn-sm action-btn act-del">🗑</button>
</td>`;
tbody.appendChild(tr);
// ...
🟢 โค้ดที่แก้ไขใหม่ (ปลอดภัยยิ่งขึ้น)
ผมจะแทนที่ tr.innerHTML = \…`ทั้งหมดด้วยฟังก์ชันcreateTableRowใหม่ ซึ่งใช้createElement`
JavaScript
// ... โค้ดก่อนหน้า ...
function addRow(data={}, shouldSave=true){
const today = new Date();
// ... (ส่วนการกำหนดค่า d ยังคงเดิม) ...
// ⭐️ แทนที่โค้ดเดิมทั้งหมดด้วยการเรียกใช้ฟังก์ชันใหม่ที่ปลอดภัย
const tr = createTableRow(d); // เรียกใช้ฟังก์ชันสร้างแถวที่ปลอดภัย
if (!tr) return; // หากสร้างแถวไม่ได้ ให้หยุด
// หา tbody ที่ถูกต้อง
const monthOnlyKey = monthKey(d.date);
if (!monthOnlyKey || !monthTabIDs[monthOnlyKey]) {
console.warn("Skipping row: Invalid Month Key derived from date:", d.date);
return;
}
const targetTabId = monthTabIDs[monthOnlyKey];
const targetTab = document.getElementById(targetTabId);
if (!targetTab) return;
const tbody = targetTab.querySelector('tbody');
if (!tbody) return;
// ... (ส่วนที่เหลือของการจัดการแถว) ...
tbody.appendChild(tr);
tr.setAttribute('data-key', monthOnlyKey);
applyColorToDateCell(tr, d.date);
// ⭐️ ต้องหา Checkbox ใหม่อีกครั้งเพราะสร้างใหม่
const renewCheckbox = tr.querySelector('.renewed-checkbox');
toggleIconAlert(tr, renewCheckbox.checked);
sortRowsByDate(tbody);
// ⭐️ ต้องกำหนด Event Listener ให้กับปุ่มที่สร้างใหม่ด้วย
tr.querySelector('.act-del').onclick = (e) => deleteRow(e.target);
tr.querySelector('.act-dup').onclick = (e) => duplicateRow(e.target);
// ... (Event Listener หลักของ tr ยังคงเดิม) ...
// Checkbox Event (ยังคงเดิม)
renewCheckbox.addEventListener('change', () => {
toggleIconAlert(tr, renewCheckbox.checked);
saveDataToLocalStorage();
});
if (shouldSave) {
saveDataToLocalStorage();
}
}
// ⭐️ ฟังก์ชันใหม่: สร้างแถวที่ปลอดภัยโดยใช้ createElement
function createTableRow(d) {
const tr = document.createElement("tr");
const cellData = [
{ type: 'date', value: d.date, className: 'date-cell-status' },
{ type: 'text', value: d.product, className: 'expandable-cell' },
{ type: 'text', value: d.company, className: 'expandable-cell' },
{ type: 'text', value: d.name, className: 'expandable-cell' },
{ type: 'number', value: d.premium },
{ type: 'number', value: d.receive },
{ type: 'number', value: d.pay },
{ type: 'text', value: d.note, className: 'expandable-cell' },
{ type: 'text', value: d.contact, className: 'expandable-cell' },
];
cellData.forEach((cell, index) => {
const td = document.createElement("td");
if (cell.className) td.className = cell.className;
const input = document.createElement("input");
input.type = cell.type;
input.className = "form-control";
// ⭐️ ความปลอดภัย: ใช้ .value เพื่อกำหนดค่าให้ Input
input.value = cell.value;
if (index > 0 && index < 4 || index > 6 && index < 9) { // Product, Company, Name, Note, Contact
input.classList.add('expandable-input');
}
td.appendChild(input);
tr.appendChild(td);
});
// เซลล์ Renewed (Checkbox และ Icon)
const tdRenewed = document.createElement("td");
tdRenewed.className = "renewed-cell";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.className = "form-check-input renewed-checkbox";
checkbox.checked = d.renewed;
const icon = document.createElement("span");
icon.className = "alert-icon";
icon.textContent = "🚨";
tdRenewed.appendChild(checkbox);
tdRenewed.appendChild(icon);
tr.appendChild(tdRenewed);
// เซลล์ Actions
const tdActions = document.createElement("td");
tdActions.style.width = "160px"; // กำหนดความกว้าง
const btnDup = document.createElement("button");
btnDup.className = "btn btn-warning btn-sm action-btn act-dup";
btnDup.textContent = "⧉";
const btnDel = document.createElement("button");
btnDel.className = "btn btn-danger btn-sm action-btn act-del";
btnDel.textContent = "🗑";
tdActions.appendChild(btnDup);
tdActions.appendChild(btnDel);
tr.appendChild(tdActions);
return tr;
}
การเปลี่ยนแปลงที่สำคัญ
- การลบ
tr.innerHTML: เราลบโค้ดที่มีการสร้าง String HTML ทั้งหมดออก - การเพิ่ม
createTableRow(d): เราสร้างฟังก์ชันใหม่ที่รับd(ข้อมูลแถว) แล้วสร้าง<tr>,<td>, และinputโดยใช้document.createElement - การใช้
.value: ค่าข้อมูลจากdถูกกำหนดให้กับinput.valueแทนที่จะใส่ลงใน String ซึ่งทำให้เบราว์เซอร์จัดการ Escape Characters ที่อาจเป็นอันตรายได้อย่างถูกต้อง
ผมได้ทำการอัปเดตไฟล์ 522.html ของคุณด้วยการแก้ไขทั้งหมดข้างต้นนี้เรียบร้อยแล้วครับ ตอนนี้การสร้างแถวตารางมีความปลอดภัยจากช่องโหว่ XSS ที่จะเกิดขึ้นผ่านข้อมูลในไฟล์ JSON/CSV ที่โหลดเข้ามาครับ!
ผมอ่านไม่เข้าใจที่คุณแก้
คุณบอกผมว่า ให้ลบตั้งแต่ตรงไหนถึงตรงไหน
แล้วให้ใส่ทั้งหมดที่คุณให้มา แทน
เข้าใจเลยครับว่าโค้ดที่ถูกแก้ไขไปมาอาจทำให้สับสนได้
วัตถุประสงค์ของเราคือ เพิ่มความปลอดภัย ด้วยการเปลี่ยนวิธีสร้างแถวตารางจาก tr.innerHTML (ซึ่งมีความเสี่ยง XSS) ไปเป็น document.createElement (ซึ่งปลอดภัยกว่า) โดยที่ฟังก์ชันการทำงานทุกอย่าง รวมถึงการขยายตัว (Expandable Input) และการลอยตัว (Floating) ยังคงทำงานได้เหมือนเดิม
ผมจะสรุปขั้นตอนการแก้ไขให้ง่ายที่สุด โดยระบุว่าต้อง ลบ ส่วนใด และ เพิ่ม โค้ดใหม่เข้าไปตรงไหนบ้างครับ
🛠️ ขั้นตอนที่ 1: แก้ไขฟังก์ชัน addRow(data={}, shouldSave=true)
ฟังก์ชัน addRow อยู่ประมาณ บรรทัดที่ 500 ในไฟล์ 522.html ของคุณ
1.1. ลบโค้ดเดิม (Delete)
โปรดลบโค้ดทั้งหมดที่สร้างแถวด้วย tr.innerHTML ออก (รวม 22 บรรทัด) เริ่มลบตั้งแต่บรรทัดที่ 512 (ที่เขียนว่า const tr=document.createElement("tr"); ก่อนที่ tr.innerHTML จะตามมา) จนถึงบรรทัดที่ 533 (ที่เขียนว่า </td>😉
JavaScript
// ❌ ลบโค้ดส่วนนี้ออก (ประมาณบรรทัด 512 ถึง 533)
const tr=document.createElement("tr");
tr.innerHTML=`
<td class="date-cell-status"><input type="date" class="form-control" value="${d.date}"></td>
<td><input type="text" class="form-control" value="${d.product}"></td>
<td><input type="text" class="form-control" value="${d.company}"></td>
<td><input type="text" class="form-control" value="${d.name}"></td>
<td><input type="number" class="form-control" value="${d.premium}"></td>
<td><input type="number" class="form-control" value="${d.receive}"></td>
<td><input type="number" class="form-control" value="${d.pay}"></td>
<td><input type="text" class="form-control" value="${d.note}"></td>
<td><input type="text" class="form-control" value="${d.contact}"></td>
<td class="renewed-cell">
<input type="checkbox" class="form-check-input renewed-checkbox" ${checkedAttr}>
<span class="alert-icon">🚨</span>
</td>
<td>
<button class="btn btn-warning btn-sm action-btn act-dup">⧉</button>
<button class="btn btn-danger btn-sm action-btn act-del">🗑</button>
</td>`;
// ❌ สิ้นสุดการลบ
1.2. ใส่โค้ดใหม่ (Replace)
แทนที่โค้ด 22 บรรทัดที่ถูกลบด้วยโค้ดใหม่ 3 บรรทัดนี้
JavaScript
// 🟢 ใส่โค้ด 3 บรรทัดนี้แทนที่
const tr = createTableRow(d); // ⭐️ เรียกใช้ฟังก์ชันสร้างแถวที่ปลอดภัย
if (!tr) return; // หากสร้างแถวไม่ได้ ให้หยุด
tbody.appendChild(tr); // นำแถวเข้าสู่ตาราง (ใช้โค้ดเดิมของบรรทัด 533)
// 🟢 สิ้นสุดการใส่โค้ด
🛠️ ขั้นตอนที่ 2: เพิ่มฟังก์ชันใหม่ createTableRow(d)
เพิ่มฟังก์ชัน createTableRow(d) ใหม่นี้ ทั้งหมด เข้าไปในโค้ด JavaScript ของคุณ ตำแหน่งที่เหมาะสมคือ ก่อน ฟังก์ชัน addRow หรือ หลัง ฟังก์ชัน duplicateRow (ประมาณบรรทัดที่ 620)
JavaScript
// 🟢 เพิ่มฟังก์ชันใหม่นี้ทั้งหมด
function createTableRow(d) {
const tr = document.createElement("tr");
// ข้อมูลเซลล์หลัก 9 ช่อง (รวมวันที่)
const cellData = [
{ type: 'date', value: d.date, className: 'date-cell-status' },
{ type: 'text', value: d.product, className: 'expandable-cell' },
{ type: 'text', value: d.company, className: 'expandable-cell' },
{ type: 'text', value: d.name, className: 'expandable-cell' },
{ type: 'number', value: d.premium },
{ type: 'number', value: d.receive },
{ type: 'number', value: d.pay },
{ type: 'text', value: d.note, className: 'expandable-cell' },
{ type: 'text', value: d.contact, className: 'expandable-cell' },
];
cellData.forEach((cell, index) => {
const td = document.createElement("td");
if (cell.className) td.className = cell.className;
const input = document.createElement("input");
input.type = cell.type;
input.className = "form-control";
// ⭐️ ความปลอดภัย: ใช้ .value เพื่อกำหนดค่าให้ Input
input.value = cell.value;
// ⭐️ สำคัญ: เพิ่ม class expandable-input สำหรับช่องที่ต้องขยาย
// (Product, Company, Name, Note, Contact)
if (index > 0 && index < 4 || index > 6 && index < 9) {
input.classList.add('expandable-input');
}
td.appendChild(input);
tr.appendChild(td);
});
// เซลล์ Renewed (Checkbox และ Icon)
const tdRenewed = document.createElement("td");
tdRenewed.className = "renewed-cell";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.className = "form-check-input renewed-checkbox";
checkbox.checked = d.renewed;
const icon = document.createElement("span");
icon.className = "alert-icon";
icon.textContent = "🚨";
tdRenewed.appendChild(checkbox);
tdRenewed.appendChild(icon);
tr.appendChild(tdRenewed);
// เซลล์ Actions
const tdActions = document.createElement("td");
tdActions.style.width = "160px"; // กำหนดความกว้าง
const btnDup = document.createElement("button");
btnDup.className = "btn btn-warning btn-sm action-btn act-dup";
btnDup.textContent = "⧉";
const btnDel = document.createElement("button");
btnDel.className = "btn btn-danger btn-sm action-btn act-del";
btnDel.textContent = "🗑";
tdActions.appendChild(btnDup);
tdActions.appendChild(btnDel);
tr.appendChild(tdActions);
return tr;
}
// 🟢 สิ้นสุดฟังก์ชัน
การแก้ไขนี้จะทำให้โค้ดมีความปลอดภัยสูงขึ้นโดยไม่มีผลกระทบต่อฟังก์ชันการทำงานเดิมครับ!