Files
juliaandClaude Sonnet 4.6 1070ba3ac1 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>
2026-06-16 11:53:24 +02:00

165 lines
6.7 KiB
Markdown

# 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 |