From 408744ad071c019ebe512b08596d8b872c86dc2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B1=EA=B2=BD?= Date: Wed, 6 May 2026 14:01:17 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=97=AD?= =?UTF-8?q?=ED=95=A0=20=EB=B3=84=20=EA=B6=8C=ED=95=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin_manager.py | 19 ++ app/backoffice/admin/admin_view.py | 2 +- app/backoffice/admin/auth.py | 1 + app/backoffice/credit_view.py | 2 + .../frontend/templates/sqladmin/details.html | 106 +++++++ .../frontend/templates/sqladmin/layout.html | 65 ++++ .../frontend/templates/sqladmin/list.html | 299 ++++++++++++++++++ app/user/api/user_admin.py | 2 + 8 files changed, 495 insertions(+), 1 deletion(-) create mode 100644 app/backoffice/frontend/templates/sqladmin/details.html create mode 100644 app/backoffice/frontend/templates/sqladmin/layout.html create mode 100644 app/backoffice/frontend/templates/sqladmin/list.html diff --git a/app/admin_manager.py b/app/admin_manager.py index 670eb13..87979a5 100644 --- a/app/admin_manager.py +++ b/app/admin_manager.py @@ -3,6 +3,7 @@ from pathlib import Path from fastapi import FastAPI from sqladmin import Admin from sqladmin.authentication import login_required +from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.responses import Response from sqlalchemy.ext.asyncio import AsyncEngine @@ -28,6 +29,24 @@ class DashboardAdmin(Admin): {"title": "대시보드", "subtitle": "", "admin_role": admin_role, **ctx}, ) + @login_required + async def edit(self, request: Request) -> Response: + if request.session.get("admin_role") == "viewer": + raise HTTPException(status_code=403) + return await super().edit(request) + + @login_required + async def create(self, request: Request) -> Response: + if request.session.get("admin_role") == "viewer": + raise HTTPException(status_code=403) + return await super().create(request) + + @login_required + async def delete(self, request: Request) -> Response: + if request.session.get("admin_role") == "viewer": + raise HTTPException(status_code=403) + return await super().delete(request) + def init_admin( app: FastAPI, diff --git a/app/backoffice/admin/admin_view.py b/app/backoffice/admin/admin_view.py index 7ab56b0..cf7ceac 100644 --- a/app/backoffice/admin/admin_view.py +++ b/app/backoffice/admin/admin_view.py @@ -43,7 +43,7 @@ class AdminAdmin(SuperAdminOnly, ModelView, model=Admin): form_args = { "role": { "label": "권한", - "choices": [("superadmin", "전체 관리자"), ("viewer", "조회 전용")], + "choices": [("superadmin", "전체 관리자"), ("viewer", "일반 관리자")], "default": "viewer", } } diff --git a/app/backoffice/admin/auth.py b/app/backoffice/admin/auth.py index 7c55642..6296b36 100644 --- a/app/backoffice/admin/auth.py +++ b/app/backoffice/admin/auth.py @@ -36,6 +36,7 @@ class AdminAuthBackend(AuthenticationBackend): request.session["admin_id"] = admin.id request.session["admin_role"] = admin.role + request.session["admin_name"] = admin.name or admin.username logger.info(f"[ADMIN-AUTH] login success admin_id={admin.id} username={username} role={admin.role}") # 마지막 로그인 시간 갱신 diff --git a/app/backoffice/credit_view.py b/app/backoffice/credit_view.py index 026426c..295d048 100644 --- a/app/backoffice/credit_view.py +++ b/app/backoffice/credit_view.py @@ -21,6 +21,8 @@ class CreditChargeRequestAdmin(SuperAdminEditable, ModelView, model=CreditCharge icon = "fa-solid fa-coins" category = "크레딧 관리" page_size = 30 + can_edit = True + can_delete = False column_list = [ "id", diff --git a/app/backoffice/frontend/templates/sqladmin/details.html b/app/backoffice/frontend/templates/sqladmin/details.html new file mode 100644 index 0000000..612b7ae --- /dev/null +++ b/app/backoffice/frontend/templates/sqladmin/details.html @@ -0,0 +1,106 @@ +{% extends "sqladmin/layout.html" %} +{% block content %} +
+
+
+

+ {% for pk in model_view.pk_columns -%} + {{ pk.name }} + {%- if not loop.last %};{% endif -%} + {% endfor %}: {{ get_object_identifier(model) }}

+
+
+
+ + + + + + + + + {% for name in model_view._details_prop_names %} + {% set label = model_view._column_labels.get(name, name) %} + + + {% set value, formatted_value = model_view.get_detail_value(model, name) %} + {% if name in model_view._relation_names %} + {% if is_list( value ) %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + + {% endfor %} + +
ColumnValue
{{ label }} + {% for elem, formatted_elem in zip(value, formatted_value) %} + {% if model_view.show_compact_lists %} + ({{ formatted_elem }}) + {% else %} + {{ formatted_elem }}
+ {% endif %} + {% endfor %} +
{{ formatted_value }} + {{ formatted_value }}
+
+ +
+
+
+{% if model_view.can_delete %} +{% include 'sqladmin/modals/delete.html' %} +{% endif %} + +{% for custom_action in model_view._custom_actions_in_detail %} +{% if custom_action in model_view._custom_actions_confirmation %} +{% with confirmation_message = model_view._custom_actions_confirmation[custom_action], custom_action=custom_action, +url=model_view._url_for_action(request, custom_action) + '?pks=' + (get_object_identifier(model) | string) %} +{% include 'sqladmin/modals/details_action_confirmation.html' %} +{% endwith %} +{% endif %} +{% endfor %} + +{% endblock %} \ No newline at end of file diff --git a/app/backoffice/frontend/templates/sqladmin/layout.html b/app/backoffice/frontend/templates/sqladmin/layout.html new file mode 100644 index 0000000..eb52700 --- /dev/null +++ b/app/backoffice/frontend/templates/sqladmin/layout.html @@ -0,0 +1,65 @@ +{% extends "sqladmin/base.html" %} +{% from 'sqladmin/_macros.html' import display_menu %} +{% block body %} +
+ +
+
+ +
+
+
+
+ {% block content %} {% endblock %} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/backoffice/frontend/templates/sqladmin/list.html b/app/backoffice/frontend/templates/sqladmin/list.html new file mode 100644 index 0000000..d1e9430 --- /dev/null +++ b/app/backoffice/frontend/templates/sqladmin/list.html @@ -0,0 +1,299 @@ +{% extends "sqladmin/layout.html" %} +{% block content %} +
+
+
+
+
+
+
+

{{ model_view.name_plural }}

+
+ {% if model_view.can_export %} + {% if model_view.export_types | length > 1 %} + + {% elif model_view.export_types | length == 1 %} + + {% endif %} + {% endif %} + {% if model_view.can_create %} + + {% endif %} +
+
+
+
+ + {% if model_view.column_searchable_list %} +
+
+ + + +
+
+ {% endif %} +
+
+
+ + + + + + {% for name in model_view._list_prop_names %} + {% set label = model_view._column_labels.get(name, name) %} + + {% endfor %} + + + + {% for row in pagination.rows %} + + + + {% for name in model_view._list_prop_names %} + {% set value, formatted_value = model_view.get_list_value(row, name) %} + {% if name in model_view._relation_names %} + {% if is_list( value ) %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + {% endfor %} + + {% endfor %} + +
+ {% if name in model_view._sort_fields %} + {% if request.query_params.get("sortBy") == name and request.query_params.get("sort") == "asc" %} + {{ + label }} + {% elif request.query_params.get("sortBy") == name and request.query_params.get("sort") == "desc" %} + {{ label + }} + {% else %} + {{ label }} + {% endif %} + {% else %} + {{ label }} + {% endif %} +
+ + + + {% if model_view.can_view_details %} + + + + {% endif %} + {% if model_view.can_edit and request.session.get('admin_role') == 'superadmin' %} + + + + {% endif %} + {% if model_view.can_delete and request.session.get('admin_role') == 'superadmin' %} + + + + {% endif %} + + {% for elem, formatted_elem in zip(value, formatted_value) %} + {% if model_view.show_compact_lists %} + ({{ formatted_elem }}) + {% else %} + {{ formatted_elem }}
+ {% endif %} + {% endfor %} +
{{ formatted_value }}{{ formatted_value }}
+
+ +
+
+ {% if model_view.get_filters() %} +
+
+
+

Filters

+
+
+ {% for filter in model_view.get_filters() %} + {% if filter.has_operator %} +
+
{{ filter.title }}
+
+ + {% set current_filter = request.query_params.get(filter.parameter_name, '') %} + {% set current_op = request.query_params.get(filter.parameter_name + '_op', '') %} + {% if current_filter %} +
+ Current: {{ current_op }} {{ current_filter }} + [Clear] +
+ {% endif %} + +
+ + {% for key, value in request.query_params.items() %} + {% if key != filter.parameter_name and key != filter.parameter_name + '_op' %} + + {% endif %} + {% endfor %} + + + + + +
+
+
+ {% else %} + +
+
{{ filter.title }}
+
+ {% for lookup in filter.lookups(request, model_view.model, model_view._run_arbitrary_query) %} + + {{ lookup[1] }} + + {% endfor %} +
+
+ {% endif %} + {% endfor %} +
+
+
+ {% endif %} +
+
+
+ {% if model_view.can_delete %} + {% include 'sqladmin/modals/delete.html' %} + {% endif %} + + {% for custom_action in model_view._custom_actions_in_list %} + {% if custom_action in model_view._custom_actions_confirmation %} + {% with confirmation_message = model_view._custom_actions_confirmation[custom_action], custom_action=custom_action, + url=model_view._url_for_action(request, custom_action) %} + {% include 'sqladmin/modals/list_action_confirmation.html' %} + {% endwith %} + {% endif %} + {% endfor %} +
+{% endblock %} diff --git a/app/user/api/user_admin.py b/app/user/api/user_admin.py index 8cc700e..9c20972 100644 --- a/app/user/api/user_admin.py +++ b/app/user/api/user_admin.py @@ -21,6 +21,8 @@ class UserAdmin(SuperAdminEditable, ModelView, model=User): icon = "fa-solid fa-user" category = "사용자 관리" page_size = 30 + can_edit = True + can_delete = True column_list = [ "id",