- Add main application files (main.py, models.py, schemas.py, etc.) - Add routers for all features (waiting, attendance, members, etc.) - Add HTML templates for admin and user interfaces - Add migration scripts and utility files - Add Docker configuration - Add documentation files - Add .gitignore to exclude database and cache files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
354 lines
12 KiB
Markdown
354 lines
12 KiB
Markdown
# 개발 일지 - 교시 마감 시스템 구현
|
|
|
|
**날짜**: 2025-11-28
|
|
**개발자**: Claude (AI Assistant)
|
|
**작업 시간**: 약 2시간
|
|
|
|
## 📋 작업 개요
|
|
|
|
일괄 출석 기능을 교시 마감 시스템으로 전환하여, 마감된 교시에는 더 이상 대기자를 등록할 수 없도록 구현했습니다.
|
|
|
|
## 🎯 요구사항
|
|
|
|
사용자가 요청한 기능:
|
|
|
|
1. **교시별 마감 기능**: 일괄 출석 대신 교시 마감으로 동작 변경
|
|
2. **대기자 등록 차단**: 마감된 교시에는 더 이상 대기자 등록 불가
|
|
3. **대기자 리스트 유지**: 마감 후에도 대기자 리스트는 그대로 표시 (비활성화 상태)
|
|
4. **시각적 표시**:
|
|
- 탭 색상을 빨강으로 변경
|
|
- 인원수 옆에 "마감" 텍스트 표시
|
|
5. **왼쪽 화살표 비활성화**: 마감된 교시 오른쪽의 대기자는 왼쪽 화살표 비활성화
|
|
|
|
## 🔨 구현 내용
|
|
|
|
### 1. 데이터베이스 변경
|
|
|
|
#### 새로운 테이블 추가: `class_closure`
|
|
```python
|
|
class ClassClosure(Base):
|
|
"""교시 마감 정보"""
|
|
__tablename__ = "class_closure"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
business_date = Column(Date, nullable=False, index=True)
|
|
class_id = Column(Integer, ForeignKey("class_info.id"), nullable=False)
|
|
closed_at = Column(DateTime, default=datetime.now)
|
|
created_at = Column(DateTime, default=datetime.now)
|
|
```
|
|
|
|
**파일**: `models.py` (98-106번 줄)
|
|
|
|
#### 마이그레이션 스크립트
|
|
- **파일**: `migrate_add_class_closure.py`
|
|
- **실행 결과**: ✅ 마이그레이션 성공
|
|
|
|
### 2. 백엔드 API 수정
|
|
|
|
#### 2.1 교시 마감 엔드포인트 (`/api/board/batch-attendance`)
|
|
**파일**: `routers/waiting_board.py` (394-450번 줄)
|
|
|
|
**변경 전**: 모든 대기자를 attended 상태로 변경
|
|
**변경 후**: ClassClosure 레코드 생성, 대기자 상태는 waiting 유지
|
|
|
|
```python
|
|
@router.post("/batch-attendance")
|
|
async def batch_attendance(batch: BatchAttendance, db: Session = Depends(get_db)):
|
|
"""교시 마감 처리"""
|
|
# 이미 마감된 교시인지 확인
|
|
existing_closure = db.query(ClassClosure).filter(...).first()
|
|
if existing_closure:
|
|
raise HTTPException(status_code=400, detail="이미 마감된 교시입니다.")
|
|
|
|
# 교시 마감 레코드 생성
|
|
closure = ClassClosure(
|
|
business_date=today,
|
|
class_id=batch.class_id,
|
|
closed_at=datetime.now()
|
|
)
|
|
db.add(closure)
|
|
db.commit()
|
|
|
|
# SSE 브로드캐스트
|
|
await sse_manager.broadcast(
|
|
store_id="default",
|
|
event_type="class_closed", # 이벤트 타입 변경
|
|
data={...}
|
|
)
|
|
```
|
|
|
|
#### 2.2 다음 마감 대상 조회 (`/api/board/next-batch-class`)
|
|
**파일**: `routers/waiting_board.py` (452-492번 줄)
|
|
|
|
마감된 교시를 제외한 첫 번째 대기자 있는 교시를 반환하도록 수정
|
|
|
|
```python
|
|
# 이미 마감된 교시 ID 목록
|
|
closed_class_ids = db.query(ClassClosure.class_id).filter(
|
|
ClassClosure.business_date == today
|
|
).all()
|
|
closed_class_ids = set(c.class_id for c in closed_class_ids)
|
|
|
|
for cls in classes:
|
|
if cls.id in closed_class_ids:
|
|
continue # 마감된 교시는 건너뜀
|
|
# ...
|
|
```
|
|
|
|
#### 2.3 마감된 교시 목록 조회 (`/api/board/closed-classes`)
|
|
**파일**: `routers/waiting_board.py` (494-507번 줄)
|
|
|
|
새로운 엔드포인트 추가 - 오늘 마감된 교시 ID 목록 반환
|
|
|
|
```python
|
|
@router.get("/closed-classes")
|
|
async def get_closed_classes(db: Session = Depends(get_db)):
|
|
"""오늘 마감된 교시 목록 조회"""
|
|
today = date.today()
|
|
closed_classes = db.query(ClassClosure).filter(
|
|
ClassClosure.business_date == today
|
|
).all()
|
|
return {"closed_class_ids": [c.class_id for c in closed_classes]}
|
|
```
|
|
|
|
#### 2.4 대기자 등록 차단
|
|
**파일**: `routers/waiting.py` (27-95번 줄)
|
|
|
|
`get_available_class()` 함수 수정 - 마감된 교시를 배치 대상에서 제외
|
|
|
|
```python
|
|
def get_available_class(db: Session, business_date: date):
|
|
"""배치 가능한 클래스 찾기 - 마감된 교시 제외"""
|
|
# 마감된 교시 ID 목록 조회
|
|
closed_class_ids = db.query(ClassClosure.class_id).filter(
|
|
ClassClosure.business_date == business_date
|
|
).all()
|
|
closed_class_ids = set(c.class_id for c in closed_class_ids)
|
|
|
|
# 마감되지 않은 교시만 필터링
|
|
available_classes = [c for c in classes if c.id not in closed_class_ids]
|
|
|
|
if not available_classes:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="모든 교시가 마감되었습니다. 대기 접수를 받을 수 없습니다."
|
|
)
|
|
# ...
|
|
```
|
|
|
|
### 3. 프론트엔드 UI 수정
|
|
|
|
#### 3.1 CSS 스타일 변경
|
|
**파일**: `templates/manage.html` (74-136번 줄)
|
|
|
|
**변경 전**: 완료된 교시는 회색 (`#95a5a6`)
|
|
**변경 후**: 마감된 교시는 빨강 (`#e74c3c`)
|
|
|
|
```css
|
|
/* 마감된 교시 탭 스타일 - 빨강 */
|
|
.class-tab.closed {
|
|
background: #e74c3c;
|
|
color: #fff;
|
|
border-color: #c0392b;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
/* 마감된 교시의 리스트 스타일 - 비활성화 상태로 표시 */
|
|
.waiting-table.closed {
|
|
background: #f5f5f5;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.waiting-table.closed::before {
|
|
content: '🔒 마감된 교시입니다';
|
|
display: block;
|
|
padding: 15px;
|
|
background: #e74c3c;
|
|
color: #fff;
|
|
text-align: center;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
}
|
|
```
|
|
|
|
#### 3.2 JavaScript 로직 수정
|
|
**파일**: `templates/manage.html`
|
|
|
|
**주요 변경 사항**:
|
|
|
|
1. **변수명 변경**: `completedClasses` → `closedClasses`
|
|
2. **초기 로드 시 마감된 교시 조회** (774-816번 줄):
|
|
```javascript
|
|
async function loadClasses() {
|
|
// 마감된 교시 목록 조회
|
|
const closedResponse = await fetch('/api/board/closed-classes');
|
|
const closedData = await closedResponse.json();
|
|
closedClasses = new Set(closedData.closed_class_ids);
|
|
// ...
|
|
}
|
|
```
|
|
|
|
3. **탭 렌더링 수정** (813-833번 줄):
|
|
```javascript
|
|
function renderClassTabs() {
|
|
classes.forEach(cls => {
|
|
const isClosed = closedClasses.has(cls.id);
|
|
tab.className = isClosed ? 'class-tab closed' : 'class-tab';
|
|
|
|
// 마감된 교시는 인원수와 "마감" 배지 표시
|
|
tab.innerHTML = `
|
|
${cls.class_name}
|
|
<span class="count">${cls.current_count || 0}명</span>
|
|
${isClosed ? '<span class="badge-closed">마감</span>' : ''}
|
|
`;
|
|
});
|
|
}
|
|
```
|
|
|
|
4. **SSE 이벤트 핸들러 수정** (506-515번 줄):
|
|
```javascript
|
|
case 'class_closed': // 이벤트 타입 변경
|
|
closedClasses.add(message.data.class_id);
|
|
updateClassCounts();
|
|
loadBatchInfo();
|
|
if (currentClassId === message.data.class_id) {
|
|
updateWaitingOrder();
|
|
}
|
|
break;
|
|
```
|
|
|
|
5. **왼쪽 화살표 비활성화** (967-1019번 줄):
|
|
```javascript
|
|
// 왼쪽에 마감된 교시가 있는지 확인
|
|
let hasClosedClassOnLeft = false;
|
|
if (hasPrevClass) {
|
|
const prevClass = classes[classIndex - 1];
|
|
hasClosedClassOnLeft = closedClasses.has(prevClass.id);
|
|
}
|
|
|
|
// 왼쪽 화살표 비활성화 조건
|
|
const leftArrowDisabled = !hasPrevClass || hasClosedClassOnLeft;
|
|
|
|
div.innerHTML = `
|
|
...
|
|
<button class="btn-icon btn-left"
|
|
${leftArrowDisabled ? 'disabled' : ''}
|
|
onclick="moveToClass(${item.id}, ${classIndex - 1})"
|
|
title="이전 교시로 이동">
|
|
←
|
|
</button>
|
|
...
|
|
`;
|
|
```
|
|
|
|
6. **마감 확인 다이얼로그** (1292-1319번 줄):
|
|
```javascript
|
|
async function batchAttendance() {
|
|
if (!confirm(`${batchClass.class_name}을(를) 마감하시겠습니까?\n마감 후 해당 교시에는 더 이상 대기자를 등록할 수 없습니다.`)) return;
|
|
// ...
|
|
}
|
|
```
|
|
|
|
7. **대기자 리스트 표시 로직** (688-722번 줄):
|
|
```javascript
|
|
async function updateWaitingOrder() {
|
|
// 마감된 교시도 대기 목록 표시 (비활성화 상태로)
|
|
const isClosed = closedClasses.has(currentClassId);
|
|
const status = 'waiting'; // 마감된 교시도 waiting 상태 유지
|
|
const response = await fetch(`/api/waiting/list?status=${status}&class_id=${currentClassId}`);
|
|
// ...
|
|
}
|
|
```
|
|
|
|
## ✅ 테스트 항목
|
|
|
|
### 기능 테스트
|
|
- [x] 교시 마감 버튼 동작
|
|
- [x] 마감된 교시 탭 빨강색 표시
|
|
- [x] "마감" 배지 표시
|
|
- [x] 마감된 교시의 대기자 리스트 비활성화 표시
|
|
- [x] 마감된 교시에 대기자 등록 차단
|
|
- [x] 왼쪽 화살표 비활성화 (마감된 교시 우측)
|
|
- [x] 이미 마감된 교시 중복 마감 방지
|
|
- [x] SSE 실시간 업데이트
|
|
- [x] 페이지 새로고침 시 마감 상태 유지
|
|
|
|
### UI/UX 테스트
|
|
- [x] 마감 확인 다이얼로그 표시
|
|
- [x] 대기자 리스트 그대로 유지 (비활성화 상태)
|
|
- [x] 드래그 앤 드롭 비활성화 (마감된 교시)
|
|
- [x] 모든 버튼 비활성화 (마감된 교시)
|
|
|
|
## 📊 변경 파일 목록
|
|
|
|
### 신규 파일
|
|
1. `migrate_add_class_closure.py` - 마이그레이션 스크립트
|
|
|
|
### 수정 파일
|
|
1. `models.py` - ClassClosure 모델 추가
|
|
2. `routers/waiting_board.py` - 교시 마감 API 구현
|
|
3. `routers/waiting.py` - 대기자 등록 차단 로직
|
|
4. `templates/manage.html` - UI 및 JavaScript 로직 전면 수정
|
|
|
|
### 영향받는 파일 (수정 없음)
|
|
- `schemas.py` - BatchAttendance 스키마 재사용
|
|
- `sse_manager.py` - SSE 이벤트 타입만 변경
|
|
- `database.py` - 변경 없음
|
|
|
|
## 🐛 발견된 문제 및 해결
|
|
|
|
### 문제 1: 모듈 import 오류
|
|
**증상**: `ModuleNotFoundError: No module named 'fastapi'`
|
|
**원인**: 가상환경 활성화 없이 서버 실행
|
|
**해결**: `source venv/bin/activate` 후 실행
|
|
|
|
### 문제 2: 없음
|
|
초기 설계가 명확했고, 요구사항이 구체적이어서 추가 문제 없이 구현 완료
|
|
|
|
## 📝 코드 품질
|
|
|
|
### 장점
|
|
- ✅ 기존 코드 구조 유지하며 최소한의 변경
|
|
- ✅ 명확한 변수명과 함수명 사용
|
|
- ✅ 일관된 코딩 스타일 유지
|
|
- ✅ 충분한 주석과 docstring 작성
|
|
- ✅ 에러 핸들링 적절히 구현
|
|
|
|
### 개선 가능한 부분
|
|
- ⚠️ 마감 취소 기능 미구현 (향후 필요시 추가)
|
|
- ⚠️ 마감 이력 조회 기능 미구현
|
|
- ⚠️ 마감 시간 설정 기능 미구현 (현재는 즉시 마감만 가능)
|
|
|
|
## 🎓 배운 점
|
|
|
|
1. **상태 관리의 중요성**:
|
|
- 프론트엔드에서 `closedClasses` Set을 사용하여 효율적으로 마감 상태 추적
|
|
- 초기 로드 시 서버에서 마감 상태 동기화
|
|
|
|
2. **점진적 기능 전환**:
|
|
- 기존 일괄 출석 기능을 완전히 대체하지 않고, 엔드포인트는 유지하며 동작만 변경
|
|
- UI 텍스트만 변경하여 사용자 혼란 최소화
|
|
|
|
3. **실시간 동기화**:
|
|
- SSE 이벤트 타입만 변경하여 실시간 업데이트 유지
|
|
- 여러 화면 간 상태 동기화 보장
|
|
|
|
## 🚀 향후 개선 방향
|
|
|
|
1. **마감 취소 기능**: 실수로 마감한 경우 취소 가능하도록
|
|
2. **마감 이력**: 언제 누가 마감했는지 이력 추적
|
|
3. **자동 마감**: 특정 시간에 자동으로 교시 마감
|
|
4. **마감 알림**: 마감 전 관리자에게 알림 발송
|
|
5. **마감 통계**: 일별/월별 마감 현황 통계
|
|
|
|
## 📌 참고사항
|
|
|
|
- **데이터베이스**: SQLite 사용
|
|
- **실시간 통신**: Server-Sent Events (SSE) 사용
|
|
- **프론트엔드**: Vanilla JavaScript (프레임워크 없음)
|
|
- **백엔드**: FastAPI + SQLAlchemy
|
|
|
|
## ✨ 결론
|
|
|
|
교시 마감 시스템이 성공적으로 구현되었습니다. 모든 요구사항이 충족되었으며, 기존 시스템과의 호환성을 유지하면서 새로운 기능이 추가되었습니다. 사용자는 이제 교시별로 마감 처리를 할 수 있으며, 마감된 교시에는 더 이상 대기자가 등록되지 않습니다.
|