Files
sam-kd/README.md

474 lines
18 KiB
Markdown
Raw Normal View History

# 동적행 생성 시스템 (Dynamic Row Generation System)
## 개요
이 문서는 웹 애플리케이션에서 동적으로 테이블 행을 생성, 삭제, 복사할 수 있는 시스템의 구현 방법을 설명합니다.
## 주요 기능
- **행 추가**: 기존 행 아래에 새로운 행 삽입
- **행 삭제**: 선택된 행 제거
- **행 복사**: 기존 행의 내용을 복사하여 새로운 행 생성
- **동적 ID 관리**: 자동으로 증가하는 행 인덱스 관리
## HTML 구조
### 기본 테이블 구조
```html
<table class="table table-bordered align-middle text-center table-sm" id="taskTable">
<thead class="table-light">
<tr>
<th scope="col" style="width: 10%;">관리</th>
<th scope="col" style="width: 60%;">할일</th>
<th scope="col" style="width: 8%;">완료</th>
<th scope="col" style="width: 8%;">완료일</th>
<th scope="col" style="width: 8%;">경과</th>
</tr>
</thead>
<tbody id="taskTableBody">
<!-- 동적으로 생성되는 행들이 여기에 들어갑니다 -->
</tbody>
</table>
```
### 버튼 그룹 구조
```html
<div class="btn-group btn-group-sm" role="group" style="gap: 1px;">
<button type="button" class="btn btn-outline-primary btn-sm p-0"
style="width: 20px; height: 20px; font-size: 12px;"
onclick="addRowAfter(<?= $i ?>)" title="아래에 행 추가">
<i class="bi bi-plus"></i>
</button>
<button type="button" class="btn btn-outline-success btn-sm p-0"
style="width: 20px; height: 20px; font-size: 12px;"
onclick="copyRow(<?= $i ?>)" title="행 복사">
<i class="bi bi-files"></i>
</button>
<button type="button" class="btn btn-outline-danger btn-sm p-0"
style="width: 20px; height: 20px; font-size: 12px;"
onclick="deleteRow(<?= $i ?>)" title="행 삭제">
<i class="bi bi-dash"></i>
</button>
</div>
```
### 개별 행 구조
```html
<tr class="task-row align-middle" data-row="<?= $i ?>" data-unique-id="<?= $unique_id ?>">
<td class="text-center align-middle">
<!-- 버튼 그룹이 들어가는 셀 -->
</td>
<td class="text-center align-middle">
<div class="d-flex justify-content-start align-items-center w-100">
<input type="text" name="tasks[<?= $i ?>][task_content]"
class="form-control form-control-sm task-content-input flex-grow-1"
placeholder="할일을 입력하세요"
value="<?= htmlspecialchars($task['task_content'] ?? '') ?>">
<input type="hidden" name="tasks[<?= $i ?>][unique_id]" value="<?= $unique_id ?>">
<input type="hidden" name="tasks[<?= $i ?>][is_pending]" value="<?= $is_pending ? '1' : '0' ?>">
</div>
</td>
<td class="text-center align-middle">
<div class="form-check d-flex justify-content-center align-middle align-items-center">
<input class="form-check-input task-checkbox" type="checkbox"
name="tasks[<?= $i ?>][is_completed]" value="1"
<?= ($task['is_completed'] ?? false) ? 'checked' : '' ?>
onchange="updateCompletionDate(this)">
</div>
</td>
<td class="text-center align-middle">
<input type="date" name="tasks[<?= $i ?>][completion_date]"
class="form-control form-control-sm completion-date-input"
value="<?= $task['completion_date'] ?? '' ?>">
</td>
<td class="text-center align-middle">
<!-- 경과일 표시 영역 -->
</td>
</tr>
```
## JavaScript 함수들
### 1. 행 추가 함수 (addRowAfter)
```javascript
function addRowAfter(rowIndex) {
const tbody = document.getElementById('taskTableBody');
const rows = tbody.querySelectorAll('.task-row');
const newRowIndex = getNextRowIndex();
// 새로운 행 HTML 생성
const newRow = createTaskRow(newRowIndex);
// 지정된 행 다음에 삽입
if (rowIndex < rows.length - 1) {
rows[rowIndex].insertAdjacentHTML('afterend', newRow);
} else {
tbody.insertAdjacentHTML('beforeend', newRow);
}
// 행 인덱스 재정렬
reorderRowIndexes();
// 통계 업데이트
updateTaskStatistics();
}
function createTaskRow(rowIndex) {
return `
<tr class="task-row align-middle" data-row="${rowIndex}" data-unique-id="">
<td class="text-center align-middle">
<div class="d-flex align-items-center justify-content-center">
<div class="btn-group btn-group-sm" role="group" style="gap: 1px;">
<button type="button" class="btn btn-outline-primary btn-sm p-0"
style="width: 20px; height: 20px; font-size: 12px;"
onclick="addRowAfter(${rowIndex})" title="아래에 행 추가">
<i class="bi bi-plus"></i>
</button>
<button type="button" class="btn btn-outline-success btn-sm p-0"
style="width: 20px; height: 20px; font-size: 12px;"
onclick="copyRow(${rowIndex})" title="행 복사">
<i class="bi bi-files"></i>
</button>
<button type="button" class="btn btn-outline-danger btn-sm p-0"
style="width: 20px; height: 20px; font-size: 12px;"
onclick="deleteRow(${rowIndex})" title="행 삭제">
<i class="bi bi-dash"></i>
</button>
</div>
</div>
</td>
<td class="text-center align-middle">
<div class="d-flex justify-content-start align-items-center w-100">
<input type="text" name="tasks[${rowIndex}][task_content]"
class="form-control form-control-sm task-content-input flex-grow-1"
placeholder="할일을 입력하세요" value="">
<input type="hidden" name="tasks[${rowIndex}][unique_id]" value="">
<input type="hidden" name="tasks[${rowIndex}][is_pending]" value="0">
</div>
</td>
<td class="text-center align-middle">
<div class="form-check d-flex justify-content-center align-middle align-items-center">
<input class="form-check-input task-checkbox" type="checkbox"
name="tasks[${rowIndex}][is_completed]" value="1"
onchange="updateCompletionDate(this)">
</div>
</td>
<td class="text-center align-middle">
<input type="date" name="tasks[${rowIndex}][completion_date]"
class="form-control form-control-sm completion-date-input"
value="" readonly>
</td>
<td class="text-center align-middle">
<span class="elapsed-days-display" data-original-date="">-</span>
</td>
</tr>
`;
}
```
### 2. 행 삭제 함수 (deleteRow)
```javascript
function deleteRow(rowIndex) {
const tbody = document.getElementById('taskTableBody');
const rows = tbody.querySelectorAll('.task-row');
if (rows.length <= 1) {
alert('최소 하나의 행은 유지해야 합니다.');
return;
}
// 행 삭제 확인
if (!confirm('이 행을 삭제하시겠습니까?')) {
return;
}
// 해당 행 삭제
const targetRow = tbody.querySelector(`tr[data-row="${rowIndex}"]`);
if (targetRow) {
targetRow.remove();
}
// 행 인덱스 재정렬
reorderRowIndexes();
// 통계 업데이트
updateTaskStatistics();
}
```
### 3. 행 복사 함수 (copyRow)
```javascript
function copyRow(rowIndex) {
const tbody = document.getElementById('taskTableBody');
const sourceRow = tbody.querySelector(`tr[data-row="${rowIndex}"]`);
const newRowIndex = getNextRowIndex();
if (!sourceRow) return;
// 소스 행의 데이터 수집
const taskContent = sourceRow.querySelector('input[name*="[task_content]"]').value;
const isCompleted = sourceRow.querySelector('input[name*="[is_completed]"]').checked;
const completionDate = sourceRow.querySelector('input[name*="[completion_date]"]').value;
// 새로운 행 생성 (복사된 데이터 포함)
const newRow = createTaskRowWithData(newRowIndex, taskContent, isCompleted, completionDate);
// 소스 행 다음에 삽입
sourceRow.insertAdjacentHTML('afterend', newRow);
// 행 인덱스 재정렬
reorderRowIndexes();
// 통계 업데이트
updateTaskStatistics();
}
function createTaskRowWithData(rowIndex, taskContent, isCompleted, completionDate) {
const checkedAttr = isCompleted ? 'checked' : '';
const readonlyAttr = isCompleted ? '' : 'readonly';
return `
<tr class="task-row align-middle" data-row="${rowIndex}" data-unique-id="">
<td class="text-center align-middle">
<div class="d-flex align-items-center justify-content-center">
<div class="btn-group btn-group-sm" role="group" style="gap: 1px;">
<button type="button" class="btn btn-outline-primary btn-sm p-0"
style="width: 20px; height: 20px; font-size: 12px;"
onclick="addRowAfter(${rowIndex})" title="아래에 행 추가">
<i class="bi bi-plus"></i>
</button>
<button type="button" class="btn btn-outline-success btn-sm p-0"
style="width: 20px; height: 20px; font-size: 12px;"
onclick="copyRow(${rowIndex})" title="행 복사">
<i class="bi bi-files"></i>
</button>
<button type="button" class="btn btn-outline-danger btn-sm p-0"
style="width: 20px; height: 20px; font-size: 12px;"
onclick="deleteRow(${rowIndex})" title="행 삭제">
<i class="bi bi-dash"></i>
</button>
</div>
</div>
</td>
<td class="text-center align-middle">
<div class="d-flex justify-content-start align-items-center w-100">
<input type="text" name="tasks[${rowIndex}][task_content]"
class="form-control form-control-sm task-content-input flex-grow-1"
placeholder="할일을 입력하세요" value="${taskContent}">
<input type="hidden" name="tasks[${rowIndex}][unique_id]" value="">
<input type="hidden" name="tasks[${rowIndex}][is_pending]" value="0">
</div>
</td>
<td class="text-center align-middle">
<div class="form-check d-flex justify-content-center align-middle align-items-center">
<input class="form-check-input task-checkbox" type="checkbox"
name="tasks[${rowIndex}][is_completed]" value="1"
${checkedAttr} onchange="updateCompletionDate(this)">
</div>
</td>
<td class="text-center align-middle">
<input type="date" name="tasks[${rowIndex}][completion_date]"
class="form-control form-control-sm completion-date-input"
value="${completionDate}" ${readonlyAttr}>
</td>
<td class="text-center align-middle">
<span class="elapsed-days-display" data-original-date="">-</span>
</td>
</tr>
`;
}
```
### 4. 유틸리티 함수들
```javascript
// 다음 행 인덱스 가져오기
function getNextRowIndex() {
const tbody = document.getElementById('taskTableBody');
const rows = tbody.querySelectorAll('.task-row');
return rows.length;
}
// 행 인덱스 재정렬
function reorderRowIndexes() {
const tbody = document.getElementById('taskTableBody');
const rows = tbody.querySelectorAll('.task-row');
rows.forEach((row, index) => {
// data-row 속성 업데이트
row.setAttribute('data-row', index);
// 모든 input의 name 속성 업데이트
const inputs = row.querySelectorAll('input');
inputs.forEach(input => {
const name = input.getAttribute('name');
if (name && name.includes('[')) {
const newName = name.replace(/\[\d+\]/, `[${index}]`);
input.setAttribute('name', newName);
}
});
// 버튼의 onclick 속성 업데이트
const buttons = row.querySelectorAll('button');
buttons.forEach(button => {
const onclick = button.getAttribute('onclick');
if (onclick) {
const newOnclick = onclick.replace(/\(\d+\)/g, `(${index})`);
button.setAttribute('onclick', newOnclick);
}
});
});
}
// 완료일 업데이트 함수
function updateCompletionDate(checkbox) {
const row = checkbox.closest('tr');
const completionDateInput = row.querySelector('input[name*="[completion_date]"]');
if (checkbox.checked) {
completionDateInput.value = new Date().toISOString().split('T')[0];
completionDateInput.removeAttribute('readonly');
} else {
completionDateInput.value = '';
completionDateInput.setAttribute('readonly', 'readonly');
}
// 통계 업데이트
updateTaskStatistics();
}
// 통계 업데이트 함수
function updateTaskStatistics() {
const tbody = document.getElementById('taskTableBody');
const rows = tbody.querySelectorAll('.task-row');
let totalTasks = 0;
let completedTasks = 0;
rows.forEach(row => {
const taskContent = row.querySelector('input[name*="[task_content]"]').value.trim();
const isCompleted = row.querySelector('input[name*="[is_completed]"]').checked;
if (taskContent !== '') {
totalTasks++;
if (isCompleted) {
completedTasks++;
}
}
});
const pendingTasks = totalTasks - completedTasks;
const completionRate = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
// 통계 표시 업데이트
document.getElementById('totalTasks').textContent = totalTasks;
document.getElementById('completedTasks').textContent = completedTasks;
document.getElementById('pendingTasks').textContent = pendingTasks;
document.getElementById('completionRate').textContent = completionRate + '%';
}
```
## CSS 스타일
### 버튼 그룹 스타일
```css
.btn-group-sm .btn {
border-radius: 0.2rem;
font-size: 0.875rem;
line-height: 1.5;
padding: 0.25rem 0.5rem;
}
.btn-group .btn:not(:last-child):not(.dropdown-toggle) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.btn-group .btn:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
/* 작은 버튼 스타일 */
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
border-radius: 0.2rem;
}
/* 아이콘 버튼 스타일 */
.btn i {
font-size: 12px;
}
```
### 테이블 스타일
```css
.table-sm td, .table-sm th {
padding: 0.3rem;
}
.task-row {
transition: background-color 0.2s ease;
}
.task-row:hover {
background-color: rgba(0, 123, 255, 0.05);
}
.form-control-sm {
height: calc(1.5em + 0.5rem + 2px);
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
line-height: 1.5;
border-radius: 0.2rem;
}
```
## 사용 방법
### 1. HTML에 테이블 구조 추가
```html
<!-- 위의 HTML 구조를 복사하여 사용 -->
```
### 2. JavaScript 함수들 추가
```html
<script>
// 위의 모든 JavaScript 함수들을 복사하여 사용
</script>
```
### 3. CSS 스타일 추가
```html
<style>
/* 위의 CSS 스타일들을 복사하여 사용 */
</style>
```
### 4. Bootstrap 및 아이콘 라이브러리 포함
```html
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
```
## 주의사항
1. **최소 행 유지**: 최소 하나의 행은 항상 유지되어야 합니다.
2. **인덱스 관리**: 행이 삭제되거나 추가될 때 인덱스가 자동으로 재정렬됩니다.
3. **데이터 무결성**: 폼 제출 시 모든 행의 데이터가 올바르게 전송되도록 name 속성이 관리됩니다.
4. **브라우저 호환성**: ES6 문법을 사용하므로 최신 브라우저에서 동작합니다.
## 확장 가능한 기능
1. **드래그 앤 드롭**: 행 순서 변경
2. **일괄 작업**: 여러 행 선택 및 일괄 처리
3. **검색 및 필터링**: 특정 조건에 맞는 행만 표시
4. **데이터 유효성 검사**: 입력 데이터 검증
5. **자동 저장**: 변경사항 자동 저장
이 시스템을 사용하면 동적으로 테이블 행을 관리할 수 있는 완전한 기능을 갖춘 인터페이스를 구축할 수 있습니다.