SOY NV AI commited on
Commit ·
673f124
1
Parent(s): 47fd3af
Update photo album and database logic for better Hugging Face integration
Browse files- app/database.py +4 -1
- app/i18n.py +8 -2
- app/routes.py +28 -0
- templates/admin_photo_album.html +19 -0
- templates/admin_photo_album_photos.html +49 -5
- templates/photo_album_list.html +4 -4
app/database.py
CHANGED
|
@@ -820,17 +820,20 @@ class PhotoAlbum(db.Model):
|
|
| 820 |
id = db.Column(db.Integer, primary_key=True)
|
| 821 |
title = db.Column(db.String(255), nullable=False)
|
| 822 |
is_public = db.Column(db.Boolean, default=False, nullable=False)
|
|
|
|
| 823 |
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
| 824 |
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
| 825 |
|
| 826 |
# 관계
|
| 827 |
-
photos = db.relationship('Photo', backref='album', lazy=True, cascade='all, delete-orphan')
|
|
|
|
| 828 |
|
| 829 |
def to_dict(self):
|
| 830 |
return {
|
| 831 |
'id': self.id,
|
| 832 |
'title': self.title,
|
| 833 |
'is_public': self.is_public,
|
|
|
|
| 834 |
'created_at': self.created_at.isoformat() if self.created_at else None,
|
| 835 |
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
| 836 |
'photo_count': len(self.photos) if self.photos else 0
|
|
|
|
| 820 |
id = db.Column(db.Integer, primary_key=True)
|
| 821 |
title = db.Column(db.String(255), nullable=False)
|
| 822 |
is_public = db.Column(db.Boolean, default=False, nullable=False)
|
| 823 |
+
representative_photo_id = db.Column(db.Integer, db.ForeignKey('photo.id'), nullable=True)
|
| 824 |
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
| 825 |
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
| 826 |
|
| 827 |
# 관계
|
| 828 |
+
photos = db.relationship('Photo', foreign_keys='Photo.album_id', backref='album', lazy=True, cascade='all, delete-orphan')
|
| 829 |
+
representative_photo = db.relationship('Photo', foreign_keys=[representative_photo_id], post_update=True)
|
| 830 |
|
| 831 |
def to_dict(self):
|
| 832 |
return {
|
| 833 |
'id': self.id,
|
| 834 |
'title': self.title,
|
| 835 |
'is_public': self.is_public,
|
| 836 |
+
'representative_photo_id': self.representative_photo_id,
|
| 837 |
'created_at': self.created_at.isoformat() if self.created_at else None,
|
| 838 |
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
| 839 |
'photo_count': len(self.photos) if self.photos else 0
|
app/i18n.py
CHANGED
|
@@ -39,7 +39,10 @@ TRANSLATIONS = {
|
|
| 39 |
'file_too_large': 'Vượt quá dung lượng tải lên tối đa (500MB). Vui lòng chia nhỏ tệp để tải lên.',
|
| 40 |
'db_error': 'Kết nối máy chủ DB đang chậm lại. Chúng tôi sẽ phản hồi nhanh chóng. Vui lòng liên hệ Lee Chang-woo / rarcissus@soymedia.kr',
|
| 41 |
'no_resource': 'Không tìm thấy tài nguyên.',
|
| 42 |
-
'upload_limit_exceeded': 'Đã vượt quá dung lượng tải lên tối đa.'
|
|
|
|
|
|
|
|
|
|
| 43 |
},
|
| 44 |
'ko': {
|
| 45 |
'photo_album': '사진첩',
|
|
@@ -81,7 +84,10 @@ TRANSLATIONS = {
|
|
| 81 |
'file_too_large': '업로드 가능한 최대 용량(500MB)을 초과했습니다. 파일을 나누어 업로드해주세요.',
|
| 82 |
'db_error': 'DB 서버 연결이 느려지고 있습니다. 빠르게 대응하겠습니다. 담당자 이창우 / rarcissus@soymedia.kr 로 연락 주시길 바랍니다',
|
| 83 |
'no_resource': '리소스를 찾을 수 없습니다.',
|
| 84 |
-
'upload_limit_exceeded': '업로드 가능한 최대 용량을 초과했습니다.'
|
|
|
|
|
|
|
|
|
|
| 85 |
}
|
| 86 |
}
|
| 87 |
|
|
|
|
| 39 |
'file_too_large': 'Vượt quá dung lượng tải lên tối đa (500MB). Vui lòng chia nhỏ tệp để tải lên.',
|
| 40 |
'db_error': 'Kết nối máy chủ DB đang chậm lại. Chúng tôi sẽ phản hồi nhanh chóng. Vui lòng liên hệ Lee Chang-woo / rarcissus@soymedia.kr',
|
| 41 |
'no_resource': 'Không tìm thấy tài nguyên.',
|
| 42 |
+
'upload_limit_exceeded': 'Đã vượt quá dung lượng tải lên tối đa.',
|
| 43 |
+
'representative': 'Đại diện',
|
| 44 |
+
'set_representative': 'Đặt làm đại diện',
|
| 45 |
+
'thumbnail': 'Ảnh thu nhỏ'
|
| 46 |
},
|
| 47 |
'ko': {
|
| 48 |
'photo_album': '사진첩',
|
|
|
|
| 84 |
'file_too_large': '업로드 가능한 최대 용량(500MB)을 초과했습니다. 파일을 나누어 업로드해주세요.',
|
| 85 |
'db_error': 'DB 서버 연결이 느려지고 있습니다. 빠르게 대응하겠습니다. 담당자 이창우 / rarcissus@soymedia.kr 로 연락 주시길 바랍니다',
|
| 86 |
'no_resource': '리소스를 찾을 수 없습니다.',
|
| 87 |
+
'upload_limit_exceeded': '업로드 가능한 최대 용량을 초과했습니다.',
|
| 88 |
+
'representative': '대표',
|
| 89 |
+
'set_representative': '대표 설정',
|
| 90 |
+
'thumbnail': '썸네일'
|
| 91 |
}
|
| 92 |
}
|
| 93 |
|
app/routes.py
CHANGED
|
@@ -184,12 +184,22 @@ def check_db_schema():
|
|
| 184 |
try:
|
| 185 |
from sqlalchemy import inspect, text
|
| 186 |
inspector = inspect(db.engine)
|
|
|
|
|
|
|
| 187 |
columns = [c['name'] for c in inspector.get_columns('user')]
|
| 188 |
if 'role' not in columns:
|
| 189 |
print("Adding 'role' column to user table...")
|
| 190 |
with db.engine.connect() as conn:
|
| 191 |
conn.execute(text("ALTER TABLE user ADD COLUMN role VARCHAR(20) DEFAULT 'user' NOT NULL"))
|
| 192 |
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
except Exception as e:
|
| 194 |
print(f"Schema check error: {e}")
|
| 195 |
|
|
@@ -6332,6 +6342,11 @@ def admin_photo_album_photo_delete(photo_id):
|
|
| 6332 |
photo = Photo.query.get_or_404(photo_id)
|
| 6333 |
album_id = photo.album_id
|
| 6334 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6335 |
# 실제 파일 삭제
|
| 6336 |
base_path = os.path.dirname(os.path.dirname(__file__))
|
| 6337 |
full_file_path = os.path.join(base_path, photo.file_path)
|
|
@@ -6363,6 +6378,19 @@ def admin_photo_album_photo_delete(photo_id):
|
|
| 6363 |
flash('사진이 삭제되었습니다.', 'success')
|
| 6364 |
return redirect(url_for('main.admin_photo_album_photos', album_id=album_id))
|
| 6365 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6366 |
# -----------------------------
|
| 6367 |
# 사진첩 (사용자용)
|
| 6368 |
# -----------------------------
|
|
|
|
| 184 |
try:
|
| 185 |
from sqlalchemy import inspect, text
|
| 186 |
inspector = inspect(db.engine)
|
| 187 |
+
|
| 188 |
+
# User 테이블
|
| 189 |
columns = [c['name'] for c in inspector.get_columns('user')]
|
| 190 |
if 'role' not in columns:
|
| 191 |
print("Adding 'role' column to user table...")
|
| 192 |
with db.engine.connect() as conn:
|
| 193 |
conn.execute(text("ALTER TABLE user ADD COLUMN role VARCHAR(20) DEFAULT 'user' NOT NULL"))
|
| 194 |
conn.commit()
|
| 195 |
+
|
| 196 |
+
# PhotoAlbum 테이블
|
| 197 |
+
columns = [c['name'] for c in inspector.get_columns('photo_album')]
|
| 198 |
+
if 'representative_photo_id' not in columns:
|
| 199 |
+
print("Adding 'representative_photo_id' column to photo_album table...")
|
| 200 |
+
with db.engine.connect() as conn:
|
| 201 |
+
conn.execute(text("ALTER TABLE photo_album ADD COLUMN representative_photo_id INTEGER REFERENCES photo(id)"))
|
| 202 |
+
conn.commit()
|
| 203 |
except Exception as e:
|
| 204 |
print(f"Schema check error: {e}")
|
| 205 |
|
|
|
|
| 6342 |
photo = Photo.query.get_or_404(photo_id)
|
| 6343 |
album_id = photo.album_id
|
| 6344 |
|
| 6345 |
+
# 만약 삭제하는 사진이 대표 사진이면 null 처리
|
| 6346 |
+
album = PhotoAlbum.query.get(album_id)
|
| 6347 |
+
if album and album.representative_photo_id == photo_id:
|
| 6348 |
+
album.representative_photo_id = None
|
| 6349 |
+
|
| 6350 |
# 실제 파일 삭제
|
| 6351 |
base_path = os.path.dirname(os.path.dirname(__file__))
|
| 6352 |
full_file_path = os.path.join(base_path, photo.file_path)
|
|
|
|
| 6378 |
flash('사진이 삭제되었습니다.', 'success')
|
| 6379 |
return redirect(url_for('main.admin_photo_album_photos', album_id=album_id))
|
| 6380 |
|
| 6381 |
+
@main_bp.route('/admin/photo-album/photo/<int:photo_id>/representative', methods=['POST'])
|
| 6382 |
+
@admin_required
|
| 6383 |
+
def admin_photo_album_photo_representative(photo_id):
|
| 6384 |
+
"""대표 사진 설정"""
|
| 6385 |
+
photo = Photo.query.get_or_404(photo_id)
|
| 6386 |
+
album = PhotoAlbum.query.get_or_404(photo.album_id)
|
| 6387 |
+
|
| 6388 |
+
album.representative_photo_id = photo.id
|
| 6389 |
+
db.session.commit()
|
| 6390 |
+
|
| 6391 |
+
flash('대표 사진이 설정되었습니다.', 'success')
|
| 6392 |
+
return redirect(url_for('main.admin_photo_album_photos', album_id=album.id))
|
| 6393 |
+
|
| 6394 |
# -----------------------------
|
| 6395 |
# 사진첩 (사용자용)
|
| 6396 |
# -----------------------------
|
templates/admin_photo_album.html
CHANGED
|
@@ -41,6 +41,14 @@
|
|
| 41 |
background-color: #fce8e6;
|
| 42 |
color: #d93025;
|
| 43 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
</style>
|
| 45 |
</head>
|
| 46 |
<body>
|
|
@@ -90,6 +98,7 @@
|
|
| 90 |
<thead>
|
| 91 |
<tr>
|
| 92 |
<th>ID</th>
|
|
|
|
| 93 |
<th>{{ gettext('album_title') }}</th>
|
| 94 |
<th>{{ gettext('photos_count', count='')|replace(' ', '')|replace('{}', '') }}</th>
|
| 95 |
<th>{{ gettext('public_status') }}</th>
|
|
@@ -101,6 +110,16 @@
|
|
| 101 |
{% for album in albums %}
|
| 102 |
<tr>
|
| 103 |
<td>{{ album.id }}</td>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
<td>
|
| 105 |
<a href="{{ url_for('main.admin_photo_album_photos', album_id=album.id) }}" class="text-decoration-none fw-medium text-dark">
|
| 106 |
{{ album.title }}
|
|
|
|
| 41 |
background-color: #fce8e6;
|
| 42 |
color: #d93025;
|
| 43 |
}
|
| 44 |
+
.admin-thumbnail {
|
| 45 |
+
width: 60px;
|
| 46 |
+
height: 60px;
|
| 47 |
+
object-fit: cover;
|
| 48 |
+
border-radius: 6px;
|
| 49 |
+
background-color: #f1f3f4;
|
| 50 |
+
border: 1px solid #dee2e6;
|
| 51 |
+
}
|
| 52 |
</style>
|
| 53 |
</head>
|
| 54 |
<body>
|
|
|
|
| 98 |
<thead>
|
| 99 |
<tr>
|
| 100 |
<th>ID</th>
|
| 101 |
+
<th>{{ gettext('thumbnail') }}</th>
|
| 102 |
<th>{{ gettext('album_title') }}</th>
|
| 103 |
<th>{{ gettext('photos_count', count='')|replace(' ', '')|replace('{}', '') }}</th>
|
| 104 |
<th>{{ gettext('public_status') }}</th>
|
|
|
|
| 110 |
{% for album in albums %}
|
| 111 |
<tr>
|
| 112 |
<td>{{ album.id }}</td>
|
| 113 |
+
<td>
|
| 114 |
+
{% set display_photo = album.representative_photo if album.representative_photo else (album.photos[0] if album.photos else None) %}
|
| 115 |
+
{% if display_photo %}
|
| 116 |
+
<img src="{{ url_for('static', filename=display_photo.thumbnail_path.replace('static/', '')) if display_photo.thumbnail_path.startswith('static/') else '/' + display_photo.thumbnail_path }}" class="admin-thumbnail">
|
| 117 |
+
{% else %}
|
| 118 |
+
<div class="admin-thumbnail d-flex align-items-center justify-content-center">
|
| 119 |
+
<span class="text-muted" style="font-size: 0.6rem;">N/A</span>
|
| 120 |
+
</div>
|
| 121 |
+
{% endif %}
|
| 122 |
+
</td>
|
| 123 |
<td>
|
| 124 |
<a href="{{ url_for('main.admin_photo_album_photos', album_id=album.id) }}" class="text-decoration-none fw-medium text-dark">
|
| 125 |
{{ album.title }}
|
templates/admin_photo_album_photos.html
CHANGED
|
@@ -31,6 +31,15 @@
|
|
| 31 |
overflow: hidden;
|
| 32 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 33 |
position: relative;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
| 35 |
.photo-card img {
|
| 36 |
width: 100%;
|
|
@@ -39,6 +48,11 @@
|
|
| 39 |
}
|
| 40 |
.photo-actions {
|
| 41 |
padding: 10px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
display: flex;
|
| 43 |
justify-content: space-between;
|
| 44 |
align-items: center;
|
|
@@ -49,10 +63,23 @@
|
|
| 49 |
background: none;
|
| 50 |
border: none;
|
| 51 |
font-size: 0.8rem;
|
|
|
|
| 52 |
}
|
| 53 |
.delete-btn:hover {
|
| 54 |
text-decoration: underline;
|
| 55 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
.drop-zone {
|
| 57 |
border: 2px dashed #dadce0;
|
| 58 |
border-radius: 8px;
|
|
@@ -143,17 +170,34 @@
|
|
| 143 |
|
| 144 |
<div class="photo-grid">
|
| 145 |
{% for photo in album.photos %}
|
| 146 |
-
<div class="photo-card">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
{% if photo.thumbnail_path %}
|
| 148 |
<img src="{{ url_for('static', filename=photo.thumbnail_path.replace('static/', '')) if photo.thumbnail_path.startswith('static/') else '/' + photo.thumbnail_path }}" alt="{{ photo.original_filename }}">
|
| 149 |
{% else %}
|
| 150 |
<img src="{{ url_for('static', filename=photo.file_path.replace('static/', '')) if photo.file_path.startswith('static/') else '/' + photo.file_path }}" alt="{{ photo.original_filename }}">
|
| 151 |
{% endif %}
|
|
|
|
| 152 |
<div class="photo-actions">
|
| 153 |
-
<
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
</div>
|
| 158 |
</div>
|
| 159 |
{% endfor %}
|
|
|
|
| 31 |
overflow: hidden;
|
| 32 |
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 33 |
position: relative;
|
| 34 |
+
transition: transform 0.2s;
|
| 35 |
+
}
|
| 36 |
+
.photo-card:hover {
|
| 37 |
+
transform: translateY(-2px);
|
| 38 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 39 |
+
}
|
| 40 |
+
.photo-card.representative {
|
| 41 |
+
outline: 3px solid #1a73e8;
|
| 42 |
+
outline-offset: -3px;
|
| 43 |
}
|
| 44 |
.photo-card img {
|
| 45 |
width: 100%;
|
|
|
|
| 48 |
}
|
| 49 |
.photo-actions {
|
| 50 |
padding: 10px;
|
| 51 |
+
display: flex;
|
| 52 |
+
flex-direction: column;
|
| 53 |
+
gap: 8px;
|
| 54 |
+
}
|
| 55 |
+
.photo-info {
|
| 56 |
display: flex;
|
| 57 |
justify-content: space-between;
|
| 58 |
align-items: center;
|
|
|
|
| 63 |
background: none;
|
| 64 |
border: none;
|
| 65 |
font-size: 0.8rem;
|
| 66 |
+
padding: 0;
|
| 67 |
}
|
| 68 |
.delete-btn:hover {
|
| 69 |
text-decoration: underline;
|
| 70 |
}
|
| 71 |
+
.representative-badge {
|
| 72 |
+
position: absolute;
|
| 73 |
+
top: 10px;
|
| 74 |
+
right: 10px;
|
| 75 |
+
background: #1a73e8;
|
| 76 |
+
color: white;
|
| 77 |
+
padding: 2px 8px;
|
| 78 |
+
border-radius: 4px;
|
| 79 |
+
font-size: 0.75rem;
|
| 80 |
+
font-weight: 500;
|
| 81 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
| 82 |
+
}
|
| 83 |
.drop-zone {
|
| 84 |
border: 2px dashed #dadce0;
|
| 85 |
border-radius: 8px;
|
|
|
|
| 170 |
|
| 171 |
<div class="photo-grid">
|
| 172 |
{% for photo in album.photos %}
|
| 173 |
+
<div class="photo-card {{ 'representative' if album.representative_photo_id == photo.id else '' }}">
|
| 174 |
+
{% if album.representative_photo_id == photo.id %}
|
| 175 |
+
<div class="representative-badge">{{ gettext('representative') }}</div>
|
| 176 |
+
{% endif %}
|
| 177 |
+
|
| 178 |
{% if photo.thumbnail_path %}
|
| 179 |
<img src="{{ url_for('static', filename=photo.thumbnail_path.replace('static/', '')) if photo.thumbnail_path.startswith('static/') else '/' + photo.thumbnail_path }}" alt="{{ photo.original_filename }}">
|
| 180 |
{% else %}
|
| 181 |
<img src="{{ url_for('static', filename=photo.file_path.replace('static/', '')) if photo.file_path.startswith('static/') else '/' + photo.file_path }}" alt="{{ photo.original_filename }}">
|
| 182 |
{% endif %}
|
| 183 |
+
|
| 184 |
<div class="photo-actions">
|
| 185 |
+
<div class="photo-info">
|
| 186 |
+
<span class="text-truncate small text-muted" style="max-width: 150px;" title="{{ photo.original_filename }}">{{ photo.original_filename }}</span>
|
| 187 |
+
</div>
|
| 188 |
+
<div class="d-flex justify-content-between align-items-center mt-1">
|
| 189 |
+
{% if album.representative_photo_id != photo.id %}
|
| 190 |
+
<form action="{{ url_for('main.admin_photo_album_photo_representative', photo_id=photo.id) }}" method="POST">
|
| 191 |
+
<button type="submit" class="btn btn-sm btn-outline-primary py-0 px-2" style="font-size: 0.75rem;">{{ gettext('set_representative') }}</button>
|
| 192 |
+
</form>
|
| 193 |
+
{% else %}
|
| 194 |
+
<span></span>
|
| 195 |
+
{% endif %}
|
| 196 |
+
|
| 197 |
+
<form action="{{ url_for('main.admin_photo_album_photo_delete', photo_id=photo.id) }}" method="POST" onsubmit="return confirm('{{ gettext('delete') }}?')">
|
| 198 |
+
<button type="submit" class="delete-btn">{{ gettext('delete') }}</button>
|
| 199 |
+
</form>
|
| 200 |
+
</div>
|
| 201 |
</div>
|
| 202 |
</div>
|
| 203 |
{% endfor %}
|
templates/photo_album_list.html
CHANGED
|
@@ -116,11 +116,11 @@
|
|
| 116 |
<a href="{{ url_for('main.photo_album_view', album_id=album.id) }}" class="album-card">
|
| 117 |
<div class="album-thumbnail">
|
| 118 |
{% if album.photos %}
|
| 119 |
-
{% set
|
| 120 |
-
{% if
|
| 121 |
-
<img src="
|
| 122 |
{% else %}
|
| 123 |
-
<img src="
|
| 124 |
{% endif %}
|
| 125 |
{% else %}
|
| 126 |
<div class="text-muted">{{ gettext('no_photos') }}</div>
|
|
|
|
| 116 |
<a href="{{ url_for('main.photo_album_view', album_id=album.id) }}" class="album-card">
|
| 117 |
<div class="album-thumbnail">
|
| 118 |
{% if album.photos %}
|
| 119 |
+
{% set display_photo = album.representative_photo if album.representative_photo else album.photos[0] %}
|
| 120 |
+
{% if display_photo.thumbnail_path %}
|
| 121 |
+
<img src="{{ url_for('static', filename=display_photo.thumbnail_path.replace('static/', '')) if display_photo.thumbnail_path.startswith('static/') else '/' + display_photo.thumbnail_path }}" alt="{{ album.title }}">
|
| 122 |
{% else %}
|
| 123 |
+
<img src="{{ url_for('static', filename=display_photo.file_path.replace('static/', '')) if display_photo.file_path.startswith('static/') else '/' + display_photo.file_path }}" alt="{{ album.title }}">
|
| 124 |
{% endif %}
|
| 125 |
{% else %}
|
| 126 |
<div class="text-muted">{{ gettext('no_photos') }}</div>
|