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,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>