convert to gitea

This commit is contained in:
2025-09-15 13:33:34 +09:00
commit 95882ac072
277 changed files with 46023 additions and 0 deletions

View File

@ -0,0 +1,193 @@
<!-- /data/gyber/apps/web/templates/base.html -->
{% load static %}
<!DOCTYPE html>
<html lang="ko" {% if is_dark_theme %}data-bs-theme="dark"{% endif %}>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Gyber 자산 관리{% endblock %}</title>
<!-- Bootstrap 5.3+ CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<!-- Select2 Core CSS -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<!-- Select2 Bootstrap 5 Theme CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" />
<!-- Font Awesome CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- 커스텀 CSS (Bootstrap 및 라이브러리 CSS 이후에 로드) -->
<link rel="stylesheet" href="{% static 'css/custom_styles.css' %}">
{% block extra_head %}{% endblock %}
<style>
/* base.html 에 직접 작성하는 스타일은 최소화하고 custom_styles.css을 활용하자! */
</style>
</head>
<body>
{# Navbar #}
<nav class="navbar navbar-expand-lg bg-body-tertiary mb-4 shadow-sm">
<div class="container-fluid">
<a class="navbar-brand fw-bold" href="{% url 'gyber:dashboard' %}"><i class="fas fa-cubes me-1"></i>Gyber</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
{# ... (메뉴 항목은 이전과 동일) ... #}
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.app_name == 'gyber' and request.resolver_match.url_name == 'dashboard' %}active{% endif %}" href="{% url 'gyber:dashboard' %}"><i class="fas fa-tachometer-alt me-1"></i>대시보드</a>
</li>
{% if user_is_viewer_group_member %}
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.app_name == 'gyber' and request.resolver_match.url_name == 'resource_list' %}active{% endif %}" href="{% url 'gyber:resource_list' %}"><i class="fas fa-box me-1"></i>자산 목록</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.app_name == 'gyber' and request.resolver_match.url_name == 'user_list' %}active{% endif %}" href="{% url 'gyber:user_list' %}"><i class="fas fa-users me-1"></i>사용자 목록</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.app_name == 'gyber' and request.resolver_match.url_name == 'group_list' %}active{% endif %}" href="{% url 'gyber:group_list' %}"><i class="fas fa-sitemap me-1"></i>그룹(부서) 관리</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.app_name == 'gyber' and request.resolver_match.url_name == 'category_list' %}active{% endif %}" href="{% url 'gyber:category_list' %}"><i class="fas fa-tags me-1"></i>카테고리 관리</a>
</li>
{% endif %}
{% if user_is_admin_group_member %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if 'log_list' in request.resolver_match.url_name %}active{% endif %}" href="#" id="navbarLogDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-clipboard-list me-1"></i>로그 조회
</a>
<ul class="dropdown-menu" aria-labelledby="navbarLogDropdown">
<li><a class="dropdown-item {% if request.resolver_match.url_name == 'resource_log_list' %}active{% endif %}" href="{% url 'gyber:resource_log_list' %}">자산 로그</a></li>
<li><a class="dropdown-item {% if request.resolver_match.url_name == 'user_log_list' %}active{% endif %}" href="{% url 'gyber:user_log_list' %}">사용자 로그</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item {% if request.resolver_match.url_name == 'group_log_list' %}active{% endif %}" href="{% url 'gyber:group_log_list' %}">그룹(부서) 로그</a></li>
<li><a class="dropdown-item {% if request.resolver_match.url_name == 'category_log_list' %}active{% endif %}" href="{% url 'gyber:category_log_list' %}">카테고리 로그</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/" target="_blank"><i class="fas fa-cog me-1"></i>Admin</a>
</li>
{% endif %}
{% endif %}
</ul>
<ul class="navbar-nav ms-auto align-items-center">
{# ... (사용자 정보, 로그아웃, 테마 버튼은 이전과 동일) ... #}
{% if user.is_authenticated %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarUserDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-user-circle me-1"></i>{{ user.first_name|default:user.username }}
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarUserDropdown">
<li>
<form action="{% url 'gyber:custom_logout' %}" method="post" class="d-inline">
{% csrf_token %}
<button type="submit" class="dropdown-item"><i class="fas fa-sign-out-alt me-1"></i>로그아웃</button>
</form>
</li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{% url 'oidc_authentication_init' %}">로그인</a>
</li>
{% endif %}
<li class="nav-item ms-2">
<button id="theme-toggle-button" class="btn btn-outline-secondary btn-sm" type="button" title="테마 전환">
<i id="theme-icon" class="fas"></i>
</button>
</li>
</ul>
</div>
</div>
</nav>
{# 메시지 표시 영역 (이전과 동일) #}
{% if messages %}
<div class="container mt-3 mb-3">
{% for message in messages %}
{% with message_icon_class=message.tags|lower %}
<div class="alert alert-{% if message_icon_class == 'error' %}danger{% elif message_icon_class == 'warning' %}warning{% elif message_icon_class == 'success' %}success{% else %}info{% endif %} alert-dismissible fade show" role="alert">
{% if message_icon_class == 'error' %}<i class="fas fa-times-circle me-2"></i>
{% elif message_icon_class == 'warning' %}<i class="fas fa-exclamation-triangle me-2"></i>
{% elif message_icon_class == 'success' %}<i class="fas fa-check-circle me-2"></i>
{% else %}<i class="fas fa-info-circle me-2"></i>
{% endif %}
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endwith %}
{% endfor %}
</div>
{% endif %}
<main class="container">
{% block content %}
{% endblock %}
</main>
<footer class="mt-5 py-3 text-center text-body-secondary border-top">
<p class="mb-0">© <span id="footer-year">2023</span> Gyber Corp. All rights reserved.</p>
</footer>
<!-- JavaScript 파일들 (body 닫기 직전으로 이동) -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
{# 테마 전환 JavaScript #}
<script>
(function() {
'use strict';
const htmlElement = document.documentElement; // html 태그 선택
const themeToggleButton = document.getElementById('theme-toggle-button');
const themeIcon = document.getElementById('theme-icon');
const storedTheme = localStorage.getItem('theme');
const getPreferredTheme = () => {
if (storedTheme) return storedTheme;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
const setTheme = (theme) => {
if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
htmlElement.setAttribute('data-bs-theme', 'dark');
} else {
htmlElement.setAttribute('data-bs-theme', theme);
}
if (themeIcon) {
themeIcon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
}
};
setTheme(getPreferredTheme()); // 초기 테마 설정
if (themeToggleButton) {
themeToggleButton.addEventListener('click', () => {
const currentTheme = htmlElement.getAttribute('data-bs-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
localStorage.setItem('theme', newTheme);
setTheme(newTheme);
});
}
// 시스템 테마 변경 감지 (localStorage에 저장된 테마가 없을 때만)
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (!localStorage.getItem('theme')) {
setTheme(e.matches ? 'dark' : 'light');
}
});
// Footer 연도 자동 업데이트
const footerYear = document.getElementById('footer-year');
if (footerYear) footerYear.textContent = new Date().getFullYear();
})();
</script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,28 @@
{# /data/gyber/apps/web/templates/includes/confirm_delete_modal.html #}
{# 삭제 확인 공통 모달 #}
{# 필요 변수: modal_id_prefix, item_id, item_name, item_type, delete_url #}
{# ★ 수정: ID를 내부에서 조합 (addstr 필터 불필요) #}
<div class="modal fade" id="{{ modal_id_prefix|default:'confirm-delete-modal-' }}{{ item_id }}" tabindex="-1" aria-labelledby="{{ modal_id_prefix|default:'confirm-delete-modal-' }}{{ item_id }}Label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="{{ modal_id_prefix|default:'confirm-delete-modal-' }}{{ item_id }}Label">{{ item_type|default:"항목" }} 삭제 확인</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p><strong>ID:</strong> {{ item_id|default:"-" }}</p>
<p><strong>{{ item_type|default:"항목" }}명:</strong> {{ item_name|default:"해당 항목" }}</p>
<p class="text-danger">이 {{ item_type|default:"항목" }}을(를) 정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.</p>
{% block modal_extra_warning %}{% endblock %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
<form action="{{ delete_url }}" method="post" class="d-inline">
{% csrf_token %}
<button type="submit" class="btn btn-danger">삭제 실행</button>
</form>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,94 @@
{# /data/gyber/apps/web/templates/includes/gyber/resource_controls.html #}
{# 자산 목록 페이지 상단 컨트롤 영역 #}
<table class="table table-borderless mb-3 controls-table">
<tbody>
<tr>
{# 카테고리 필터 #}
<td style="width: auto;">
<form method="get" action="{% url 'gyber:resource_list' %}" id="filterFormCategory" class="d-inline-flex align-items-center">
{% comment %} 다른 필터/정렬/검색값 유지 위한 hidden inputs {% endcomment %}
{% if search_query %}<input type="hidden" name="query" value="{{ search_query }}">{% endif %}
<input type="hidden" name="page_size" value="{{ page_size|default:'20' }}">
{% if sort_by != 'id' %}<input type="hidden" name="sort" value="{{ sort_by }}">{% endif %}
{% if sort_dir != 'desc' %}<input type="hidden" name="dir" value="{{ sort_dir }}">{% endif %}
{% if request.GET.group %}<input type="hidden" name="group" value="{{ request.GET.group }}">{% endif %}
{% if request.GET.user_id %}<input type="hidden" name="user_id" value="{{ request.GET.user_id }}">{% endif %}
<label for="filterCategory" class="form-label me-1 mb-0 text-nowrap">카테고리:</label>
<select name="category" id="filterCategory" class="form-select form-select-sm" style="width: auto;" onchange="this.form.submit()">
<option value="" {% if not current_category %}selected{% endif %}>-- 전체 --</option>
{% for cat in category_list %}
<option value="{{ cat.category_id }}" {% if current_category == cat.category_id %}selected{% endif %}>{{ cat.category_name }}</option>
{% endfor %}
</select>
</form>
</td>
{# 부서 필터 #}
<td style="width: auto;">
<form method="get" action="{% url 'gyber:resource_list' %}" id="filterFormGroup" class="d-inline-flex align-items-center">
{% comment %} 다른 필터/정렬/검색값 유지 위한 hidden inputs {% endcomment %}
{% if search_query %}<input type="hidden" name="query" value="{{ search_query }}">{% endif %}
<input type="hidden" name="page_size" value="{{ page_size|default:'20' }}">
{% if sort_by != 'id' %}<input type="hidden" name="sort" value="{{ sort_by }}">{% endif %}
{% if sort_dir != 'desc' %}<input type="hidden" name="dir" value="{{ sort_dir }}">{% endif %}
{% if request.GET.category %}<input type="hidden" name="category" value="{{ request.GET.category }}">{% endif %}
{% if request.GET.user_id %}<input type="hidden" name="user_id" value="{{ request.GET.user_id }}">{% endif %}
<label for="filterGroup" class="form-label me-1 mb-0 text-nowrap">부서:</label>
<select name="group" id="filterGroup" class="form-select form-select-sm" style="width: auto;" onchange="this.form.submit()">
<option value="" {% if not current_group %}selected{% endif %}>-- 전체 --</option>
{% for grp in group_list %}
<option value="{{ grp.group_id }}" {% if current_group == grp.group_id %}selected{% endif %}>{{ grp.group_name }}</option>
{% endfor %}
</select>
</form>
</td>
{# 페이지 크기 #}
<td style="width: auto;">
<form method="get" action="{% url 'gyber:resource_list' %}" id="pageSizeForm" class="d-inline-flex align-items-center">
{% comment %} 다른 필터/정렬/검색값 유지 위한 hidden inputs {% endcomment %}
{% if search_query %}<input type="hidden" name="query" value="{{ search_query }}">{% endif %}
{% if request.GET.category %}<input type="hidden" name="category" value="{{ request.GET.category }}">{% endif %}
{% if request.GET.group %}<input type="hidden" name="group" value="{{ request.GET.group }}">{% endif %}
{% if request.GET.user_id %}<input type="hidden" name="user_id" value="{{ request.GET.user_id }}">{% endif %}
{% if sort_by != 'id' %}<input type="hidden" name="sort" value="{{ sort_by }}">{% endif %}
{% if sort_dir != 'desc' %}<input type="hidden" name="dir" value="{{ sort_dir }}">{% endif %}
<label for="pageSizeSelect" class="form-label me-2 mb-0 text-nowrap">표시:</label>
<select name="page_size" id="pageSizeSelect" class="form-select form-select-sm" style="width: auto;" onchange="this.form.submit()">
{% for size in valid_page_sizes %}
<option value="{{ size }}" {% if size == page_size %}selected{% endif %}>{{ size }}개씩</option>
{% endfor %}
</select>
</form>
</td>
{# 검색 입력 그룹 #}
<td style="width: 100%;">
<form method="get" action="{% url 'gyber:resource_list' %}" id="searchForm" class="mb-0">
{% comment %} 다른 필터/정렬값 유지 위한 hidden inputs {% endcomment %}
<input type="hidden" name="page_size" value="{{ page_size|default:'20' }}">
{% if request.GET.category %}<input type="hidden" name="category" value="{{ request.GET.category }}">{% endif %}
{% if request.GET.group %}<input type="hidden" name="group" value="{{ request.GET.group }}">{% endif %}
{% if request.GET.user_id %}<input type="hidden" name="user_id" value="{{ request.GET.user_id }}">{% endif %}
{% if sort_by != 'id' %}<input type="hidden" name="sort" value="{{ sort_by }}">{% endif %}
{% if sort_dir != 'desc' %}<input type="hidden" name="dir" value="{{ sort_dir }}">{% endif %}
<div class="input-group input-group-sm">
<input type="text" name="query" class="form-control form-control-sm" placeholder="검색어 입력..." value="{{ search_query|default:'' }}" aria-label="Search query">
<button type="submit" class="btn btn-primary btn-sm"><i class="fas fa-search"></i> 검색</button>
{% if search_query %}
<a href="{% url 'gyber:resource_list' %}?{{ query_params_no_search }}" class="btn btn-secondary btn-sm">초기화</a>
{% endif %}
</div>
</form>
</td>
</tr>
<tr>
<td colspan="2">
{% if current_category or current_group %}
<a href="{% url 'gyber:resource_list' %}?{{ query_params_no_filter }}" class="btn btn-sm btn-outline-secondary text-nowrap">필터 초기화</a>
{% else %}   {% endif %}
</td>
<td> </td>
<td class="text-end">
<div class="form-text"><small>검색 대상: 제품명, 시리얼, 코드, 사용자, 제조사, 비고</small></div>
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,66 @@
{# /data/gyber/apps/web/templates/includes/gyber/resource_table.html #}
{# 자산 목록 테이블 - include 로 사용됨 #}
<div class="table-responsive">
<table class="table table-striped table-hover table-bordered table-sm">
<thead class="table-light">
<tr>
{# 테이블 헤더 (정렬 링크 포함) #}
<th><a href="?{{ query_params_all }}&sort=id&dir={% if sort_by == 'id' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}">ID {% if sort_by == 'id' %}<i class="fas fa-sort-{{ sort_dir|lower }}"></i>{% endif %}</a></th>
<th><a href="?{{ query_params_all }}&sort=name&dir={% if sort_by == 'name' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}">제품명 {% if sort_by == 'name' %}<i class="fas fa-sort-{{ sort_dir|lower }}"></i>{% endif %}</a></th>
<th class="hide-on-mobile"><a href="?{{ query_params_all }}&sort=category&dir={% if sort_by == 'category' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}">카테고리 {% if sort_by == 'category' %}<i class="fas fa-sort-{{ sort_dir|lower }}"></i>{% endif %}</a></th>
<th class="hide-on-mobile"><a href="?{{ query_params_all }}&sort=code&dir={% if sort_by == 'code' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}">관리 코드 {% if sort_by == 'code' %}<i class="fas fa-sort-{{ sort_dir|lower }}"></i>{% endif %}</a></th>
<th><a href="?{{ query_params_all }}&sort=user&dir={% if sort_by == 'user' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}">사용자 {% if sort_by == 'user' %}<i class="fas fa-sort-{{ sort_dir|lower }}"></i>{% endif %}</a></th>
<th><a href="?{{ query_params_all }}&sort=group&dir={% if sort_by == 'group' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}">부서 {% if sort_by == 'group' %}<i class="fas fa-sort-{{ sort_dir|lower }}"></i>{% endif %}</a></th>
<th class="ellipsis-cell"><a href="?{{ query_params_all }}&sort=serial&dir={% if sort_by == 'serial' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}">시리얼 번호 {% if sort_by == 'serial' %}<i class="fas fa-sort-{{ sort_dir|lower }}"></i>{% endif %}</a></th>
<th style="width: 5%; text-align: center;"><a href="?{{ query_params_all }}&sort=is_locked&dir={% if sort_by == 'is_locked' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" title="자동 동기화 제외 여부">잠금 {% if sort_by == 'is_locked' %}<i class="fas fa-sort-{{ sort_dir|lower }}"></i>{% endif %}</a></th>
<th class="hide-on-mobile"><a href="?{{ query_params_all }}&sort=purchased&dir={% if sort_by == 'purchased' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}">구매일 {% if sort_by == 'purchased' %}<i class="fas fa-sort-{{ sort_dir|lower }}"></i>{% endif %}</a></th>
<th class="hide-on-mobile"><a href="?{{ query_params_all }}&sort=registered&dir={% if sort_by == 'registered' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}">등록일 {% if sort_by == 'registered' %}<i class="fas fa-sort-{{ sort_dir|lower }}"></i>{% endif %}</a></th>
<th class="hide-on-mobile"><a href="?{{ query_params_all }}&sort=updated&dir={% if sort_by == 'updated' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}">수정일 {% if sort_by == 'updated' %}<i class="fas fa-sort-{{ sort_dir|lower }}"></i>{% endif %}</a></th>
<th>액션</th>
</tr>
</thead>
<tbody>
{% for resource in resource_list %}
<tr>
<td>{{ resource.resource_id }}</td>
<td><a href="{% url 'gyber:resource_detail' resource.resource_id %}">{{ resource.resource_name }}</a></td>
<td class="hide-on-mobile">{{ resource.category_name|default:"-" }}</td>
<td class="hide-on-mobile">{{ resource.resource_code|default:"-" }}</td>
<td>{{ resource.user_display_name|default:"<span class='text-muted'>미지정</span>"|safe }}</td>
<td>{{ resource.group_name|default:"-" }}</td>
<td class="ellipsis-cell" title="{{ resource.serial_num|default:'' }}">{{ resource.serial_num|default:"-" }}</td>
<td style="text-align: center;">
{% if resource.is_locked %}
<i class="fas fa-lock text-danger" title="잠김 (자동 동기화 제외)"></i>
{% else %}
<i class="fas fa-unlock-alt text-success" title="잠금 해제 (자동 동기화 대상)"></i>
{% endif %}
</td>
<td class="hide-on-mobile">{{ resource.purchase_date|date:"Y-m-d"|default:"-" }}</td>
<td class="hide-on-mobile">{{ resource.register_date|date:"Y-m-d H:i"|default:"-" }}</td>
<td class="hide-on-mobile">{{ resource.update_date|date:"Y-m-d H:i"|default:"-" }}</td>
<td class="text-nowrap">
{% if user_is_admin_group_member %}
<a href="{% url 'gyber:resource_detail' resource.resource_id %}" class="btn btn-sm btn-info" title="상세보기"><i class="fas fa-eye"></i></a>
<a href="{% url 'gyber:resource_edit' resource.resource_id %}" class="btn btn-sm btn-warning" title="수정하기"><i class="fas fa-edit"></i></a>
{# ★ 수정: data-bs-target ID 형식 변경 #}
<button type="button" class="btn btn-sm btn-danger" data-bs-toggle="modal" data-bs-target="#resource-delete-modal-{{ resource.resource_id }}" title="삭제하기">
<i class="fas fa-trash-alt"></i>
</button>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="12" class="text-center">
{% if search_query or current_category or current_group or current_user_id %}
조건에 맞는 자산이 없습니다.
{% else %}
등록된 자산이 없습니다.
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

View File

@ -0,0 +1,29 @@
{# /data/gyber/apps/web/templates/includes/pagination.html #}
{# 페이지네이션 UI 컴포넌트 #}
{% if total_pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
<li class="page-item {% if current_page == 1 %}disabled{% endif %}">
<a class="page-link" href="?page=1&{{ query_params_all }}" aria-label="First">««</a>
</li>
<li class="page-item {% if not has_previous %}disabled{% endif %}">
<a class="page-link" href="?page={{ previous_page_number }}&{{ query_params_all }}" aria-label="Previous">«</a>
</li>
{% for page_num in page_numbers %}
<li class="page-item {% if page_num == current_page %}active{% endif %}">
{% if page_num == current_page %}
<span class="page-link">{{ page_num }}</span>
{% else %}
<a class="page-link" href="?page={{ page_num }}&{{ query_params_all }}">{{ page_num }}</a>
{% endif %}
</li>
{% endfor %}
<li class="page-item {% if not has_next %}disabled{% endif %}">
<a class="page-link" href="?page={{ next_page_number }}&{{ query_params_all }}" aria-label="Next">»</a>
</li>
<li class="page-item {% if current_page == total_pages %}disabled{% endif %}">
<a class="page-link" href="?page={{ total_pages }}&{{ query_params_all }}" aria-label="Last">»»</a>
</li>
</ul>
</nav>
{% endif %}

View File

@ -0,0 +1,53 @@
{# /data/gyber/apps/web/templates/includes/user/user_controls.html #}
{# 사용자 목록 페이지 상단 컨트롤 영역 #}
<form method="get" action="{% url 'gyber:user_list' %}" class="row gy-2 gx-3 align-items-center mb-4">
{# 검색어 입력 #}
<div class="col-auto">
<label class="visually-hidden" for="query">검색어</label>
<input type="text" class="form-control form-control-sm" id="query" name="query" placeholder="이름, 계정, 부서 검색" value="{{ search_query|default:'' }}">
</div>
{# 부서 필터 드롭다운 #}
<div class="col-auto">
<label class="visually-hidden" for="group">부서</label>
{# ★ onchange 이벤트 제거하고 검색 버튼으로 통일? 또는 유지? 여기서는 유지 #}
<select class="form-select form-select-sm" id="group" name="group" onchange="this.form.submit()">
<option value="">-- 전체 부서 --</option>
{% for group_item in group_list %} {# 변수 이름 충돌 피하기 위해 group_item 사용 #}
<option value="{{ group_item.group_id }}" {% if group_item.group_id == current_group %}selected{% endif %}>
{{ group_item.group_name }}
</option>
{% endfor %}
</select>
</div>
{# 페이지 크기 선택 #}
<div class="col-auto">
<label class="visually-hidden" for="page_size">페이지 크기</label>
<select class="form-select form-select-sm" id="page_size" name="page_size" onchange="this.form.submit()">
{% for size in valid_page_sizes %}
<option value="{{ size }}" {% if size == page_size %}selected{% endif %}>{{ size }}개씩 보기</option>
{% endfor %}
</select>
</div>
{# 검색 버튼 #}
<div class="col-auto">
<button type="submit" class="btn btn-primary btn-sm">
<i class="fas fa-search"></i> 검색
</button>
</div>
{# 검색/필터 초기화 버튼 #}
{% if search_query or current_group %}
<div class="col-auto">
{# 초기화 링크 #}
<a href="{% url 'gyber:user_list' %}" class="btn btn-secondary btn-sm">
<i class="fas fa-times"></i> 초기화
</a>
</div>
{% endif %}
{# 다른 필터/정렬값 유지 위한 hidden inputs (필요시 추가) #}
{% if sort_by != 'name' %}<input type="hidden" name="sort" value="{{ sort_by }}">{% endif %}
{% if sort_dir != 'asc' %}<input type="hidden" name="dir" value="{{ sort_dir }}">{% endif %}
{# page_size 는 select 에서 전달됨 #}
</form>

View File

@ -0,0 +1,72 @@
{# /data/gyber/apps/web/templates/includes/user/user_table.html #}
{# 사용자 목록 테이블 #}
<div class="table-responsive">
<table class="table table-striped table-hover table-sm align-middle">
<thead class="table-light">
<tr>
{# 테이블 헤더 (정렬 링크 포함) #}
<th>
<a href="?{{ query_params_all }}&sort=name&dir={% if sort_by == 'name' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}">
사용자 (표시이름 [계정명])
{% if sort_by == 'name' %}<i class="fas fa-sort-{{ sort_dir|lower }}"></i>{% endif %}
</a>
</th>
<th>
<a href="?{{ query_params_all }}&sort=account&dir={% if sort_by == 'account' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}">
계정명
{% if sort_by == 'account' %}<i class="fas fa-sort-{{ sort_dir|lower }}"></i>{% endif %}
</a>
</th>
<th>
<a href="?{{ query_params_all }}&sort=group&dir={% if sort_by == 'group' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}">
부서
{% if sort_by == 'group' %}<i class="fas fa-sort-{{ sort_dir|lower }}"></i>{% endif %}
</a>
</th>
<th class="text-end">
<a href="?{{ query_params_all }}&sort=assets&dir={% if sort_by == 'assets' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}">
보유 자산 수
{% if sort_by == 'assets' %}<i class="fas fa-sort-{{ sort_dir|lower }}"></i>{% endif %}
</a>
</th>
<th style="width: 15%;">액션</th>
</tr>
</thead>
<tbody>
{% for user_item in user_list %} {# 변수 이름 변경 #}
<tr>
<td>{{ user_item.user_display_name|default:"-" }}</td>
<td>{{ user_item.account_name|default:"-" }}</td>
<td>{{ user_item.group_name|default:"-" }}</td>
<td class="text-end">{{ user_item.assigned_asset_count }}</td>
<td class="text-nowrap"> {# 액션 버튼 줄바꿈 방지 #}
{# 자산 보기 링크 #}
<a href="{% url 'gyber:resource_list' %}?user_id={{ user_item.user_id }}" class="btn btn-sm btn-outline-primary me-1" title="{{ user_item.user_display_name|default:'사용자' }}님의 자산 목록 보기">
<i class="fas fa-list"></i> <span class="d-none d-md-inline">자산</span>
</a>
{# 사용자 수정 버튼 #}
{% if user_is_admin_group_member %}
<a href="{% url 'gyber:user_edit' user_item.user_id %}" class="btn btn-sm btn-outline-secondary me-1" title="사용자 정보 수정">
<i class="fas fa-edit"></i> <span class="d-none d-md-inline">수정</span>
</a>
{# 삭제 버튼 (모달 트리거) #}
<button type="button" class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#user-delete-modal-{{ user_item.user_id }}" title="사용자 삭제">
<i class="fas fa-trash-alt"></i> <span class="d-none d-md-inline">삭제</span>
</button>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center">
{% if search_query or current_group %}
조건에 맞는 사용자가 없습니다.
{% else %}
등록된 사용자가 없습니다.
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>