docs: add sprint planning — dependency diagram + specs for all 8 stories
Analyzes all Taiga stories, maps hard/soft dependencies, and writes full specs (Goal, Data Model, Components, Verification, Open Questions) for every story before any implementation begins. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
# Spec: 04 · Fruit Publications
|
||||
|
||||
**Taiga Story:** #4 · `04-fruit-publications`
|
||||
**Status:** Ready
|
||||
**Branch:** `feature/04-fruit-publications`
|
||||
**Depends on:** `02-manage-fruits` (fruits table)
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Maintainers can manage publications (books, catalogues) that document fruits. Each publication has a cover image, linked fruits, linked PDFs per fruit, and linked fruit images from the publication. Fruit detail page shows descriptions and publication images.
|
||||
|
||||
---
|
||||
|
||||
## 2. Background / Scope / Context / Constraints
|
||||
|
||||
- `osdb_pub_id` is the unique external identifier (label: "OSDB Kürzel").
|
||||
- Cover image stored as BYTEA in `publications.image_data`.
|
||||
- PDF stored as BYTEA in `publication_descriptions.pdf_data` (one PDF per fruit per publication).
|
||||
- Fruit images from publications stored in `publication_fruit_images` (BYTEA); `image_type` hardcoded to `'fruit'`, cannot be changed.
|
||||
- On fruit detail page: show description list only if at least one description exists; link text = publication title; link opens PDF.
|
||||
- On fruit detail page: show publication fruit images labeled "Publication title · Author".
|
||||
- Mouseclick on cover image → overlay/drawer with full-size image.
|
||||
|
||||
---
|
||||
|
||||
## 3. Necessary Refactorings
|
||||
|
||||
- `fruit_images.image_type` already defined in story #2 — no changes.
|
||||
- `thumbnail_data` column on `publication_fruit_images` deferred to story #7 migration.
|
||||
|
||||
---
|
||||
|
||||
## 4. Related Data Model
|
||||
|
||||
```sql
|
||||
-- Migration: 000003_create_publications.up.sql
|
||||
|
||||
CREATE TABLE publications (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
author VARCHAR(255),
|
||||
osdb_pub_id VARCHAR(50) NOT NULL UNIQUE,
|
||||
image_data BYTEA,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE publication_fruits (
|
||||
publication_id INTEGER NOT NULL REFERENCES publications(id) ON DELETE CASCADE,
|
||||
fruit_id INTEGER NOT NULL REFERENCES fruits(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (publication_id, fruit_id)
|
||||
);
|
||||
|
||||
CREATE TABLE publication_descriptions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
publication_id INTEGER NOT NULL REFERENCES publications(id) ON DELETE CASCADE,
|
||||
fruit_id INTEGER NOT NULL REFERENCES fruits(id) ON DELETE CASCADE,
|
||||
pdf_data BYTEA NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (publication_id, fruit_id)
|
||||
);
|
||||
|
||||
CREATE TABLE publication_fruit_images (
|
||||
id SERIAL PRIMARY KEY,
|
||||
publication_id INTEGER NOT NULL REFERENCES publications(id) ON DELETE CASCADE,
|
||||
fruit_id INTEGER NOT NULL REFERENCES fruits(id) ON DELETE CASCADE,
|
||||
filename VARCHAR(255),
|
||||
data BYTEA NOT NULL,
|
||||
image_type VARCHAR(20) NOT NULL DEFAULT 'fruit', -- hardcoded
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
-- Note: thumbnail_data column added in story #07 migration
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Affected Places / Related Components
|
||||
|
||||
### Backend
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `backend/internal/domain/publication.go` | `Publication`, `PublicationDescription`, `PublicationFruitImage` structs |
|
||||
| `backend/internal/repository/publication_repo.go` | DB queries |
|
||||
| `backend/internal/handler/publication_handler.go` | Echo handlers |
|
||||
| `backend/cmd/server/router.go` | Register `/api/v1/publications` routes |
|
||||
| `migrations/000003_create_publications.up.sql` | Schema above |
|
||||
| `migrations/000003_create_publications.down.sql` | DROP TABLEs |
|
||||
|
||||
### API Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/publications` | List |
|
||||
| POST | `/api/v1/publications` | Create |
|
||||
| GET | `/api/v1/publications/:id` | Get (with linked fruits list) |
|
||||
| PUT | `/api/v1/publications/:id` | Update |
|
||||
| DELETE | `/api/v1/publications/:id` | Delete |
|
||||
| POST | `/api/v1/publications/:id/image` | Upload cover image |
|
||||
| GET | `/api/v1/publications/:id/image` | Serve cover image |
|
||||
| DELETE | `/api/v1/publications/:id/image` | Delete cover image |
|
||||
| GET | `/api/v1/publications/:id/fruits` | List linked fruits |
|
||||
| POST | `/api/v1/publications/:id/fruits` | Link a fruit |
|
||||
| DELETE | `/api/v1/publications/:id/fruits/:fruitId` | Unlink a fruit |
|
||||
| GET | `/api/v1/publications/:id/descriptions` | List descriptions (fruit + link metadata) |
|
||||
| POST | `/api/v1/publications/:id/descriptions` | Upload PDF description (body: fruitId + PDF) |
|
||||
| GET | `/api/v1/publications/:id/descriptions/:descId` | Serve PDF |
|
||||
| DELETE | `/api/v1/publications/:id/descriptions/:descId` | Delete |
|
||||
| GET | `/api/v1/publications/:id/fruit-images` | List pub fruit images |
|
||||
| POST | `/api/v1/publications/:id/fruit-images` | Upload fruit image (fruitId + image) |
|
||||
| GET | `/api/v1/publications/:id/fruit-images/:imgId` | Serve image |
|
||||
| DELETE | `/api/v1/publications/:id/fruit-images/:imgId` | Delete |
|
||||
| GET | `/api/v1/fruits/:id/descriptions` | All descriptions for a fruit (across publications) |
|
||||
| GET | `/api/v1/fruits/:id/publication-images` | All pub images for a fruit |
|
||||
|
||||
### Frontend
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `frontend/src/api/publications.ts` | API wrapper |
|
||||
| `frontend/src/stores/publicationStore.ts` | Pinia store |
|
||||
| `frontend/src/views/PublicationList.vue` | List + link to detail |
|
||||
| `frontend/src/views/PublicationDetail.vue` | Full management UI |
|
||||
| `frontend/src/components/ImageOverlayDrawer.vue` | Full-size image overlay on click |
|
||||
| `frontend/src/views/FruitDetail.vue` | Extended: descriptions section + pub images section |
|
||||
| `frontend/src/router/index.ts` | Add `/publications`, `/publications/:id` |
|
||||
|
||||
---
|
||||
|
||||
## 6. Out of Scope
|
||||
|
||||
- Bulk import from XML (story #5)
|
||||
- Thumbnail generation (story #7)
|
||||
- Authentication guards (story #8)
|
||||
|
||||
---
|
||||
|
||||
## 7. Verification
|
||||
|
||||
| Check | Expected |
|
||||
|-------|---------|
|
||||
| Create publication with duplicate osdb_pub_id | 409 Conflict |
|
||||
| Upload cover image → serve back → correct bytes | Pass |
|
||||
| Link fruit to publication → appears in GET /publications/:id/fruits | Pass |
|
||||
| Upload PDF description → GET description → Content-Type: application/pdf | Pass |
|
||||
| Fruit detail page: no descriptions → section hidden | Pass |
|
||||
| Fruit detail page: 1+ description → links visible | Pass |
|
||||
| Click cover image thumbnail → overlay opens full-size | Pass |
|
||||
| Pub fruit images on fruit detail labeled correctly | Publication title · Author shown |
|
||||
| `go test ./...` | Green |
|
||||
| `npm run test` | Green |
|
||||
|
||||
---
|
||||
|
||||
## 8. Open Questions
|
||||
|
||||
| # | Question | Impact |
|
||||
|---|----------|--------|
|
||||
| 1 | Is one PDF per (publication, fruit) pair enforced (UNIQUE) or allow multiples? | Schema + UI |
|
||||
| 2 | Can a fruit image exist in a publication without the fruit being linked via publication_fruits? | Data integrity |
|
||||
| 3 | Listing publications: include fruit count or just metadata? | API response shape |
|
||||
| 4 | PDF viewer: open in new tab or inline browser PDF viewer? | UX |
|
||||
Reference in New Issue
Block a user