This commit is contained in:
iu
2026-10-10 14:21:43 +03:00
parent 84ce4d1930
commit 9329ebc023
17 changed files with 591 additions and 34 deletions
+102
View File
@@ -0,0 +1,102 @@
# 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
Нет.