Files
waiting-system/출석조회_신규회원탭_개선완료.md
Jun-dev f699a29a85 Add waiting system application files
- 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>
2025-12-14 00:29:39 +09:00

12 KiB

출석 조회 화면 신규회원 탭 개선 완료

요청 사항

  1. 신규회원 탭 데이터 표시 개선

    • 신규회원 탭도 출석현황처럼 데이터를 보여주기
    • 신규회원 리스트를 출석순으로 나열
  2. 기본 탭 변경

    • 현재: 출석현황 탭이 기본 선택
    • 변경: 대기 현황 탭이 기본 선택되도록

수정 내용

1. 백엔드 API 수정 - routers/attendance.py

/api/attendance/new-members 엔드포인트 개선

Before (기존):

# 가입일 기준 정렬
new_members = db.query(Member).filter(...).order_by(desc(Member.created_at)).all()

# 최초 출석일만 조회
for member in new_members:
    first_attendance = db.query(WaitingList).filter(...).first()
    result.append({
        "name": member.name,
        "phone": member.phone,
        "joined_at": member.created_at.strftime("%Y-%m-%d"),
        "first_attendance": first_attendance.attended_at.strftime("%Y-%m-%d") if first_attendance else None
    })

return {
    "count": len(new_members),
    "members": result
}

After (개선):

# 가입일 필터링만 (정렬은 나중에)
new_members = db.query(Member).filter(...).all()

result = []
total_attendance = 0

for member in new_members:
    # ✅ 출석 횟수 조회
    attendance_count = db.query(func.count(WaitingList.id)).filter(
        WaitingList.member_id == member.id,
        WaitingList.status == 'attended'
    ).scalar() or 0

    # 최초 출석일 조회
    first_attendance = db.query(WaitingList).filter(...).first()

    # ✅ 최근 출석일 조회
    last_attendance = db.query(WaitingList).filter(
        WaitingList.member_id == member.id,
        WaitingList.status == 'attended'
    ).order_by(desc(WaitingList.attended_at)).first()

    total_attendance += attendance_count

    result.append({
        "name": member.name,
        "phone": member.phone,
        "joined_at": member.created_at.strftime("%Y-%m-%d"),
        "first_attendance": first_attendance.attended_at.strftime("%Y-%m-%d") if first_attendance else None,
        "last_attendance": last_attendance.attended_at.strftime("%Y-%m-%d") if last_attendance else None,
        "attendance_count": attendance_count  # ✅ 출석 횟수 추가
    })

# ✅ 출석순으로 정렬 (출석 횟수가 많은 순)
result.sort(key=lambda x: x['attendance_count'], reverse=True)

# ✅ 평균 출석 횟수 계산
avg_attendance = round(total_attendance / len(new_members), 1) if new_members else 0

return {
    "count": len(new_members),
    "total_attendance": total_attendance,      # ✅ 총 출석 횟수
    "avg_attendance": avg_attendance,          # ✅ 평균 출석 횟수
    "members": result
}

주요 개선 사항:

  • 각 회원의 출석 횟수 조회 및 반환
  • 각 회원의 최근 출석일 조회 및 반환
  • 출석순으로 정렬 (출석 횟수 내림차순)
  • 총 출석 횟수 계산 및 반환
  • 평균 출석 횟수 계산 및 반환

2. 프론트엔드 수정 - templates/attendance.html

2-1. 통계 카드 추가 (lines 331-344)

Before:

<div class="stats-grid">
    <div class="stat-card">
        <div class="stat-label">신규 가입 회원</div>
        <div class="stat-value" id="newMemberCount">0명</div>
    </div>
</div>

After:

<div class="stats-grid">
    <div class="stat-card">
        <div class="stat-label">신규 가입 회원</div>
        <div class="stat-value" id="newMemberCount">0명</div>
    </div>
    <!-- ✅ 총 출석 횟수 카드 추가 -->
    <div class="stat-card">
        <div class="stat-label">총 출석 횟수</div>
        <div class="stat-value" style="color: #3498db;" id="newMemberTotalAttendance">0회</div>
    </div>
    <!-- ✅ 평균 출석 횟수 카드 추가 -->
    <div class="stat-card">
        <div class="stat-label">평균 출석 횟수</div>
        <div class="stat-value" style="color: #9b59b6;" id="newMemberAvgAttendance">0회</div>
    </div>
</div>

2-2. 테이블 구조 변경 (lines 346-361)

Before:

<table class="data-table">
    <thead>
        <tr>
            <th>가입일시</th>
            <th>이름</th>
            <th>전화번호</th>
            <th>최초 출석일</th>
        </tr>
    </thead>
    <tbody id="newMemberList"></tbody>
</table>

After:

<table class="data-table">
    <thead>
        <tr>
            <th width="80">순위</th>           <!-- ✅ 순위 추가 -->
            <th>이름</th>
            <th>전화번호</th>
            <th>출석 횟수</th>                 <!-- ✅ 출석 횟수 추가 -->
            <th>가입일</th>
            <th>최초 출석일</th>
            <th>최근 출석일</th>                <!-- ✅ 최근 출석일 추가 -->
        </tr>
    </thead>
    <tbody id="newMemberList"></tbody>
</table>

2-3. loadNewMembers() 함수 개선 (lines 658-693)

Before:

async function loadNewMembers() {
    const period = document.getElementById('newMemberPeriod').value;
    const date = document.getElementById('newMemberDate').value;

    const response = await fetch(`/api/attendance/new-members?period=${period}&date=${date}`, { headers: getHeaders() });
    const data = await response.json();

    document.getElementById('newMemberCount').textContent = `${data.count}명`;

    const tbody = document.getElementById('newMemberList');
    tbody.innerHTML = data.members.map(m => `
        <tr>
            <td>${m.joined_at}</td>
            <td>${m.name}</td>
            <td>${m.phone}</td>
            <td>${m.first_attendance || '-'}</td>
        </tr>
    `).join('');
}

After:

async function loadNewMembers() {
    const period = document.getElementById('newMemberPeriod').value;
    const date = document.getElementById('newMemberDate').value;

    const response = await fetch(`/api/attendance/new-members?period=${period}&date=${date}`, { headers: getHeaders() });
    const data = await response.json();

    // ✅ 통계 데이터 표시
    document.getElementById('newMemberCount').textContent = `${data.count}명`;
    document.getElementById('newMemberTotalAttendance').textContent = `${data.total_attendance}회`;
    document.getElementById('newMemberAvgAttendance').textContent = `${data.avg_attendance}회`;

    const tbody = document.getElementById('newMemberList');
    tbody.innerHTML = data.members.map((m, index) => {
        // ✅ 순위 배지 스타일 적용
        let rankClass = 'rank-other';
        if (index === 0) rankClass = 'rank-1';
        if (index === 1) rankClass = 'rank-2';
        if (index === 2) rankClass = 'rank-3';

        return `
        <tr>
            <td><span class="rank-badge ${rankClass}">${index + 1}</span></td>
            <td>${m.name}</td>
            <td>${m.phone}</td>
            <td><strong>${m.attendance_count}회</strong></td>  <!-- ✅ 출석 횟수 -->
            <td>${m.joined_at}</td>
            <td>${m.first_attendance || '-'}</td>
            <td>${m.last_attendance || '-'}</td>                 <!-- ✅ 최근 출석일 -->
        </tr>
        `;
    }).join('');
}

2-4. 기본 탭 변경 (lines 169, 177, 221)

탭 버튼 (line 169):

<!-- Before -->
<button class="tab-btn" onclick="switchTab('waiting_status')">대기현황</button>
<button class="tab-btn active" onclick="switchTab('status')">출석현황</button>

<!-- After -->
<button class="tab-btn active" onclick="switchTab('waiting_status')">대기현황</button>  <!-- ✅ active 추가 -->
<button class="tab-btn" onclick="switchTab('status')">출석현황</button>               <!-- ✅ active 제거 -->

탭 컨텐츠 (lines 177, 221):

<!-- Before -->
<div id="waiting_statusTab" class="tab-content">      <!-- 대기현황 탭 -->
<div id="statusTab" class="tab-content active">       <!-- 출석현황 탭 -->

<!-- After -->
<div id="waiting_statusTab" class="tab-content active">  <!-- ✅ active 추가 -->
<div id="statusTab" class="tab-content">                 <!-- ✅ active 제거 -->

변경 사항 요약

신규회원 탭 개선

항목 Before After
통계 카드 신규 가입 회원 수만 표시 신규 가입 회원, 총 출석 횟수, 평균 출석 횟수 표시
테이블 컬럼 가입일시, 이름, 전화번호, 최초 출석일 순위, 이름, 전화번호, 출석 횟수, 가입일, 최초 출석일, 최근 출석일
정렬 기준 가입일 기준 (최신순) 출석 횟수 기준 (많은 순)
순위 표시 없음 1-3위는 금/은/동 배지, 나머지는 일반 배지

기본 탭 변경

항목 Before After
기본 선택 탭 출석현황 대기 현황

결과

신규회원 탭 화면

통계 표시:

┌────────────────┐  ┌────────────────┐  ┌────────────────┐
│ 신규 가입 회원 │  │ 총 출석 횟수   │  │ 평균 출석 횟수 │
│     15명       │  │     45회       │  │     3.0회      │
└────────────────┘  └────────────────┘  └────────────────┘

리스트 표시 (출석순 정렬):

┌────┬────────┬──────────────┬──────────┬────────────┬──────────────┬──────────────┐
│순위│  이름  │   전화번호   │출석 횟수 │   가입일   │ 최초 출석일  │ 최근 출석일  │
├────┼────────┼──────────────┼──────────┼────────────┼──────────────┼──────────────┤
│ 🥇 │ 홍길동 │ 010-1234-5678│   10회   │ 2025-11-01 │ 2025-11-02   │ 2025-12-03   │
│ 🥈 │ 김철수 │ 010-2345-6789│    8회   │ 2025-11-05 │ 2025-11-06   │ 2025-12-01   │
│ 🥉 │ 이영희 │ 010-3456-7890│    7회   │ 2025-11-10 │ 2025-11-11   │ 2025-11-30   │
│  4 │ 박민수 │ 010-4567-8901│    5회   │ 2025-11-15 │ 2025-11-16   │ 2025-11-28   │
│  5 │ 최지은 │ 010-5678-9012│    3회   │ 2025-11-20 │ 2025-11-21   │ 2025-11-25   │
└────┴────────┴──────────────┴──────────┴────────────┴──────────────┴──────────────┘

기본 탭

화면 진입 시 대기 현황 탭이 자동으로 선택됩니다.

┌────────────────────────────────────────────────────┐
│  [대기현황*]  [출석현황]  [개인별 출석]  [신규회원]  [출석순위]  │
└────────────────────────────────────────────────────┘

영향 범위

수정된 파일

  1. routers/attendance.py - /api/attendance/new-members 엔드포인트
  2. templates/attendance.html - 신규회원 탭 UI 및 기본 탭 설정

영향받는 기능

  • 출석 조회 > 신규회원 탭
    • 통계 데이터 표시
    • 출석순 정렬
    • 순위 배지 표시
  • 출석 조회 화면 진입 시 기본 탭

영향받지 않는 기능

  • 대기 현황 탭
  • 출석현황 탭
  • 개인별 출석 탭
  • 출석순위 탭

결론

신규회원 탭이 출석현황 탭처럼 풍부한 데이터를 보여줍니다:

  1. 통계 카드 3개 표시 (신규 회원 수, 총 출석, 평균 출석)
  2. 출석순으로 정렬 (출석 횟수가 많은 순)
  3. 순위 표시 (1-3위는 금/은/동 배지)
  4. 상세 정보 표시 (출석 횟수, 가입일, 최초/최근 출석일)
  5. 기본 탭이 "대기 현황"으로 변경

신규회원의 출석 활동을 한눈에 파악할 수 있습니다!