6.7 KiB
Design
Context
См. proposal.md для мотивации. Текущее состояние, важное для подхода:
- Авторизация — Bearer-токен (не cookies), поэтому
<img>не может обращаться к защищённым эндпоинтам напрямую. Все фото уже отдаются фронтенду как presigned URL (internal/photos/service.go,attachURLs). albums.cover_photo_id— колонкаtextбез внешнего ключа (миграция000001_init.up.sql:15), поэтому удаление фото-обложки оставляет висячий id.albums.Serviceуже держит*storage.Clientи умеет presign; отдельной зависимости от пакетаphotosнет.- Сборка фото идёт асинхронно (
processVariants):thumb_keyпоявляется не сразу после создания записи фото. - В
backend/staticзакоммичен собранныйIcon.svelte(inline-SVG спрайт Material Symbols), при этом вfrontend/srcкомпонента нет — UI сейчас на эмодзи. Берём inline-SVG как ориентир.
Goals / Non-Goals
Goals:
- Список альбомов отдаёт
cover_url(presigned миниатюра) и карточки его рисуют. - Выбор и очистка обложки из фото альбома в настройках, применяется немедленно.
- Автоматическая обложка не затирает ручной выбор.
- Кнопка создания — иконка с хинтом, открывающая диалог ввода имени.
Non-Goals:
- Обложка в шапке страницы альбома и в публичных share-страницах.
- Смена обложки из лайтбокса/контекстного меню фото (может быть позже).
- Переработка всей иконочной системы проекта — вводим минимальный inline-SVG.
Decisions
1. cover_url в ответе списка (вариант A)
GET /albums отдаёт каждому альбому cover_url — presigned URL миниатюры
(thumb) или null. Источник ключа: albums.Store расширяет выборку
подзапросом к photos за thumb_key по cover_photo_id; albums.Service
делает presign через уже имеющийся storage.Client.
Альтернативы: (B) фронт дёргает GET /photos/:id на каждую обложку — N+1 и
тянет лишние поля; (C) эндпоинт GET /albums/:id/cover с редиректом — не
работает, <img> не приложит Bearer-токен. Оба отпадают.
2. Presign в albums, а не в photos
Не вводим зависимость albums -> photos; достаточно подзапроса за ключом и
существующего storage в albums.Service. Дублирование кода presign
минимально и оправдано сохранением слоёв.
3. Авто-обложка только при пустой
photos.Store.SetCover меняет SQL на условный:
UPDATE albums SET cover_photo_id = $2 WHERE id = $1 AND cover_photo_id IS NULL.
Вызов остаётся в processVariants после SetVariants, чтобы ключ миниатюры уже
существовал. Это же сохраняет ручной выбор при последующих загрузках.
4. Валидация обложки при PATCH
albums.Service.Update при заданном cover_photo_id SHALL проверять, что фото
принадлежит этому альбому (иначе обложкой станет произвольный чужой id). Ошибка —
BadRequest/NotFound. clear_cover по-прежнему очищает.
5. Иконка — inline-SVG, диалог — нативный <dialog>
Минимальный компонент иконки (один SVG на кнопку создания) с title +
aria-label для хинта. Диалог имени — нативный <dialog>: ESC, backdrop и
focus-trap бесплатно, не тащим оверлей как в Lightbox. Поле — только
«Название»; описание остаётся в настройках.
6. Очистка висячей обложки при удалении фото
Так как FK нет, при удалении фото, которое является обложкой, бэкенд SHALL
очищать cover_photo_id (в photos.Service.Delete или через SetCover-подобный
метод). Иначе получим обложку на несуществующее фото (визуально — заглушка).
Risks / Trade-offs
- [Presigned
cover_urlживёт ~1 час, а список может висеть открытым дольше] → Приемлемо: список перезагружается при навигации; при желании позже добавим refetch. Для MVP не блокирует. - [Подзапрос за
thumb_keyна каждый альбом в списке] → Дёшево при индексеphotosпо PK; presign — локальная подпись без сетевого вызова. - [
cover_photo_idзадан, но варианты ещё не собраны] →thumb_keyпуст →cover_url = null→ карточка показывает заглушку; авто-обложка ставится только после сборки вариантов, ручной выбор сразу после загрузки может временно не иметь миниатюры. - [Пользователь уберёт обложку во время загрузки нового фото] → авто-обложка снова назначит свежее фото, если обложка пуста — ожидаемо по спеке.
Migration Plan
Миграции не нужны. Изменения аддитивны: новое поле cover_url в JSON; старые
клиенты его игнорируют. Порядок выката: бэкенд, затем фронтенд. Откат —
возврат предыдущего бинарника и статики; данные не меняются.
Open Questions
Нет.