Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions docs/features/curation-program-schema-form/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Program Schema-Driven Curation Form Design

## UI Summary

기존 큐레이션 작성 화면의 `DetailPageHeader`, 기본 정보 카드, 이미지 업로드,
섹션 카드를 유지한다. `PROGRAM` payload는 행사 정보 카드와 프로그램 목록 카드로
나누며, 프로그램 배열의 각 항목은 번호가 있는 중첩 카드로 표시한다.

requestSpec이 제공하는 필드 순서, 표시 이름, 타입, 필수 여부, 개수 제한을 화면에
반영한다. 스펙에 앱 렌더링 정보가 없으므로 오른쪽 앱 미리보기 패널은 만들지 않고
입력 영역을 전체 너비로 사용한다.

## Navigation

- Route:
- 모든 스펙의 공통 생성 라우트:
`/dashboard/curations/specs/:specCode/new`
- `PROGRAM` 진입:
`/dashboard/curations/specs/PROGRAM/new`
- 수정은 기존 `/dashboard/curations/:id`를 유지하고 상세 응답의
`spec.code === PROGRAM`일 때 프로그램 수정 폼을 렌더링한다.
- 기존 `/dashboard/curations/tasting-events/new`,
`/dashboard/curations/general/new`, `/dashboard/curations/pairings/new`는
동일한 공통 생성 라우트로 redirect하는 호환 경로만 유지한다.
- Sidebar/menu placement:
- 기존 `큐레이션` 메뉴와 `/dashboard/curations` 목록을 유지한다.
- 별도 사이드바 항목은 추가하지 않는다.
- Entry actions:
- `/dashboard/curations/new`의 모든 활성 스펙 카드 전체와 `작성하기` 버튼이
각 spec code를 포함한 공통 생성 라우트로 이동한다.
- 프로그램 카드는 행사형 큐레이션임을 보여주는 기존 lucide 아이콘과 디자인
시스템 색상을 사용한다.
- 앱 미리보기 계약이 없으므로 `PROGRAM` 카드의 미리보기 액션은 표시하지 않는다.

## List View

- Columns:
- 기존 큐레이션 목록 컬럼을 변경하지 않는다.
- 스펙 표시는 기존 스펙 이름/코드 formatter를 통해 `프로그램`으로 표시한다.
- Filters/search:
- 기존 스펙 필터는 API 스펙 목록 기반이므로 `PROGRAM`을 자동 표시한다.
- Row actions:
- 기존 상세 이동을 유지한다.
- Empty/loading/error states:
- 기존 큐레이션 목록 상태를 유지한다.

## Detail/Create/Edit View

- Sections/cards:
- Header:
- 생성: `프로그램 작성`
- 수정: `프로그램 수정`
- 보조 문구는 생성 시 스펙 이름, 수정 시 큐레이션 ID를 표시한다.
- `기본 정보`:
- 기존 `CurationBasicInfoSection`을 사용한다.
- 큐레이션명, 설명, 이미지, 노출 기간, 노출 순서, 활성 상태를 유지한다.
- `행사 정보`:
- `programs`를 제외한 requestSpec 루트 필드를 스펙 순서로 렌더링한다.
- 날짜/숫자/짧은 문자열은 2열, 주소·URL·enum 다중 선택은 전체 너비를 사용한다.
- `프로그램`:
- 섹션 제목에 현재 개수와 스펙의 1~20개 제한을 표시한다.
- 빈 상태에 `프로그램을 추가해주세요.`와 `프로그램 추가` 버튼을 표시한다.
- 각 항목은 `프로그램 1`, `프로그램 2` 형태의 카드이며 우측 상단에 `삭제`가 있다.
- 프로그램 필드는 requestSpec의 item properties 순서로 표시한다.
- `시음 위스키`는 해당 프로그램 카드 안의 하위 영역에 기존 위스키 카드 목록
UI를 재사용한다.
- Fields:
- string/date/time/number는 기존 `Input`, `Textarea`를 사용한다.
- 단일 enum은 `Select`로 표시한다.
- enum 배열은 체크박스 목록으로 표시하며 선택 개수 제한을 안내한다.
- `alcohol-card-list`는 DB 검색/직접 입력/테이스팅 태그/이미지 입력을 포함한
기존 위스키 카드 UI를 사용한다.
- optional 필드는 required marker를 표시하지 않는다.
- Validation messages:
- 기존 필드 메시지 형식인 `<필드명>은/는 필수입니다.`를 유지한다.
- 배열 최소/최대:
`<필드명>을 최소 N개 이상 추가해주세요.`,
`<필드명>은 최대 N개까지 추가할 수 있습니다.`
- 행사 날짜:
`행사 종료일은 행사 시작일보다 빠를 수 없습니다.`
- 프로그램 시간:
`종료 시간은 시작 시간보다 빠를 수 없습니다.`
- enum:
`<필드명>을 선택해주세요.`
- Primary/secondary actions:
- Primary: `저장` 또는 `수정`
- Secondary: `목록`
- 저장 중 또는 이미지 업로드 중에는 primary action을 비활성화한다.

## State and Feedback

- Loading:
- 모든 생성 유형의 스펙 목록/상세 로딩은 단일 작성 페이지의 로딩 카드를 사용한다.
- Empty:
- 스펙이 없거나 비활성이면 기존 `큐레이션 스펙을 찾을 수 없습니다.` 상태를
표시한다.
- 프로그램 배열이 비어 있으면 카드 안에 dashed empty state를 표시한다.
- Error:
- 스펙 목록/상세 조회 실패는 기존 retry/back blocking state를 사용한다.
- 지원하지 않는 requestSpec 구조는 `이 스펙은 아직 자동 폼에서 지원하지
않습니다.` 오류 카드와 목록 이동을 표시한다.
- Success:
- 생성: `<스펙 이름> 큐레이션이 등록되었습니다.`
- 수정: 기존 `큐레이션이 수정되었습니다.` 토스트를 유지한다.
- 성공 후 큐레이션 목록으로 이동한다.
- Destructive confirmation:
- 아직 저장되지 않은 프로그램/위스키 항목 삭제는 즉시 반영한다.
- 서버 레코드 삭제는 이 기능에 포함하지 않는다.

## Design System Usage

- Components:
- `DetailPageHeader`, `CurationBasicInfoSection`, `CurationSectionCard`,
`CurationFormFieldRenderer`, `CurationWhiskyCardListField`, `FormField`
- shadcn `Card`, `Button`, `Input`, `Textarea`, `Select`, `Checkbox`, `Badge`
- Tokens/classes:
- `border-border`, `bg-card`, `bg-muted/20`, `text-muted-foreground`,
`text-destructive`, `ring-ring` 등 기존 토큰을 사용한다.
- 페이지 주요 영역은 `grid`, `minmax(0,1fr)`, `md:grid-cols-2`를 사용하고
고정 픽셀 열 너비를 추가하지 않는다.
- Existing pages to mirror:
- 공통 기본 정보와 저장: `WhiskyTastingEventForm.tsx`
- 공통 스펙 조회 상태: `CurationCreate.tsx`
- 반복 카드와 위스키 입력: `CurationWhiskyCardListField.tsx`
- 일반 payload 상세 fallback: `CurationDetail.tsx`

## Manual UI Review Points

- [ ] 생성 유형 화면에서 프로그램 카드가 다른 활성 스펙과 자연스럽게 정렬되는지 확인한다.
- [ ] `/dashboard/curations/specs/PROGRAM/new` 직접 진입 시 스펙 로딩 후 폼이 표시되는지 확인한다.
- [ ] 모바일/태블릿/데스크톱에서 행사 정보의 1열/2열 전환과 중첩 프로그램 카드가
가로 스크롤 없이 표시되는지 확인한다.
- [ ] 프로그램 추가/삭제 후 번호와 개수 badge가 즉시 갱신되는지 확인한다.
- [ ] 두 프로그램 각각의 시음 위스키가 서로 섞이지 않고 독립적으로 추가/삭제되는지 확인한다.
- [ ] enum 단일/다중 선택의 키보드 접근성과 required/error 표시를 확인한다.
- [ ] 저장 요청 payload의 프로그램 및 위스키 배열 순서가 화면 순서와 같은지 확인한다.
- [ ] 기존 시음회/추천/페어링 생성·수정 화면에 시각적 회귀가 없는지 확인한다.
186 changes: 186 additions & 0 deletions docs/features/curation-program-schema-form/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# Program Schema-Driven Curation Form Implementation Plan

## Summary

Add `PROGRAM` as a known curation spec and use one spec-code create route/page
for every curation type. The page resolves the list/detail spec once, then
selects a form-model/renderer strategy from the resolved spec. Extend the
shared curation schema vocabulary with enum select, enum multi-select, and
object array fields. Refactor the shared whisky card list to use its model key
as a React Hook Form path so each program can own an independent nested whisky
list. Preserve the specialized form renderers where their interaction model is
not expressible by the generic renderer, but remove page-level routing and
spec-query ownership from them.

## Inputs

- Spec: `docs/features/curation-program-schema-form/spec.md`
- Design: `docs/features/curation-program-schema-form/design.md`
- API source:
- Published admin API docs for curation spec list/detail and curation
create/update endpoints.
- Backend `main` resource
`bottlenote-mono/src/main/resources/openapi/curation/program.json`,
introduced by backend commit `d7944c51`.
- The development API could not be authenticated through direct probing, and
the published docs do not yet include a `PROGRAM` example.

## Data and API Mapping

- `CurationSpecCode.PROGRAM` -> known code used by entry routing and detail form
dispatch.
- `/dashboard/curations/specs/:specCode/new` ->
`useCurationSpecByCode(specCode)` -> `useCurationSpec(specId)` -> dynamic form
model/renderer strategy.
- Known create strategies:
- `WHISKY_TASTING_EVENT` -> tasting-event form model and form renderer.
- `RECOMMENDED_WHISKY`, `WHISKY_PAIRING` -> whisky-curation form model and
form renderer.
- object requestSpec, including `PROGRAM` -> generic schema-driven form model
and renderer.
- `requestSpec.properties` -> ordered root payload field models.
- `string enum` -> single select field with enum values as options.
- `array` with `items.enum` -> multi-select checkbox field.
- `array` with `items.type === object` -> object array field with recursively
parsed item property models.
- `x-field-style: alcohol-card-list` -> shared whisky card list field whose
`key` may be a nested path such as `programs.0.whiskies`.
- `required`, `nullable`, `minLength`, `maxLength`, `minimum`, `maximum`,
`minItems`, `maxItems` -> initial values and Zod validation.
- Common curation fields -> existing `CurationBasicInfoSection`.
- Create form -> `POST /admin/api/v2/curations` with `specId`, common fields,
and object `payload`.
- Edit form -> `PUT /admin/api/v2/curations/{curationId}` with the same shape.
- `CurationV2Detail.payload` -> recursively normalized edit form values.

## Implementation Steps

1. Extend the curation API/code vocabulary.
- Add `PROGRAM` to `CurationSpecCode` in
`src/types/api/curation.api.ts`.
- Update `src/pages/curation/curation-display-utils.ts` to show `프로그램`
for the known code.
2. Extend the shared schema-driven field model.
- In `src/pages/curation/curation-form-model.ts`, add:
- single enum select model,
- enum multi-select model,
- recursively parsed object array model.
- Resolve supported array/object shapes explicitly. Throw a descriptive
error for unsupported schema structures instead of falling back to text.
- Preserve property order from `requestSpec.properties`.
- Add a helper that prefixes a field model key recursively for nested React
Hook Form paths.
3. Extend shared rendering and validation.
- Add single enum and multi-enum inputs to
`src/pages/curation/components/CurationFormFieldRenderer.tsx`.
- Add
`src/pages/curation/components/CurationObjectArrayField.tsx` for
add/remove and nested item rendering.
- Extend `src/pages/curation/curation-form-schema.ts` with recursive Zod
generation for all supported field kinds and shared whisky-card item
validation.
4. Make the shared whisky list path-aware.
- Refactor
`src/pages/curation/components/CurationWhiskyCardListField.tsx` so
`useFieldArray`, `useWatch`, `getValues`, `setValue`, `clearErrors`,
error lookup, and nested registration paths derive from
`fieldModel.key`.
- Keep current root `alcohols` behavior unchanged for tasting events and
whisky curations.
5. Add the generic object-payload form pipeline under
`src/pages/curation/schema-driven/`.
- `schema-driven-curation.form-model.ts`: create root sections from
requestSpec; separate object arrays into their own sections.
- `schema-driven-curation.schema.ts`: combine common curation validation
with recursive payload validation and add the `PROGRAM` date/time
cross-field checks.
- `schema-driven-curation.mapper.ts`: create defaults, hydrate edit values,
and recursively serialize trimmed payload values while preserving array
order.
- `SchemaDrivenCurationForm.tsx`: render common info and generated sections,
submit create/update mutations, and handle upload/submission states.
- `CurationCreate.tsx`: read `specCode` from route params, resolve active
spec list/detail once, handle unsupported schema errors, and select/render
the correct form strategy from the resolved spec.
- `SchemaDrivenCurationEditPage.tsx`: build and render the same form from a
loaded `CurationV2Detail`.
6. Integrate navigation and detail dispatch.
- Render `CurationCreatePage` only at
`/dashboard/curations/specs/:specCode/new` with the existing curation
roles.
- Keep the three prior type-specific paths as redirects to the shared route;
they must not render or query a separate page.
- Add `PROGRAM` UI metadata to `CurationEntry.tsx`.
- Route every active card to the shared spec-code path without per-card
href configuration.
- Hide preview action for specs without a supported app preview, including
`PROGRAM`.
- Dispatch `PROGRAM` detail to `SchemaDrivenCurationEditPage` in
`CurationDetail.tsx`.
7. Add tests first for each behavior before its implementation.
- Extend `src/pages/curation/__tests__/CurationEntry.test.tsx` for the
program card, no preview action, and shared route navigation for all
known and unknown active specs.
- Add
`src/pages/curation/schema-driven/__tests__/schema-driven-curation.form-model.test.ts`
for primitive/enum/enum-array/object-array/nested whisky parsing and
unsupported structure errors.
- Add
`src/pages/curation/schema-driven/__tests__/schema-driven-curation.schema.test.ts`
for required/limits/date/time validation.
- Add
`src/pages/curation/schema-driven/__tests__/SchemaDrivenCurationPages.test.tsx`
for create rendering, nested program whisky isolation, create payload,
edit hydration, and update payload.
- Add a shared create-page dispatch test proving that the same page renders
the tasting-event, whisky-curation, and generic strategies after one spec
lookup.
- Retain and run existing curation form/page tests as regressions.

## Edge Cases

- An inactive or missing URL spec code shows the existing missing-spec state.
- A malformed or unsupported schema shows a blocking unsupported-spec state;
it does not render an incorrect generic text field.
- Optional enum arrays start empty and stop accepting values at `maxItems`.
- Programs start empty so the `minItems` error is observable on submit.
- Removing a program removes only its nested whisky list and React Hook Form
errors.
- Nested field paths remain stable after removing an earlier program.
- Optional empty strings/arrays are excluded from serialized payload objects;
required empty values remain available for validation.
- Nullable API values hydrate as empty UI values.
- Nested DB/manual whisky mapping follows the existing shared card contract and
removes UI-only stats only when they are absent.
- Existing root `alcohols` pages continue using the same component behavior.
- Program app preview is intentionally absent until a response/display
vocabulary is defined.

## Verification Checklist

- [x] `pnpm test:run`
- [ ] `pnpm lint`
- [x] Changed-file ESLint and Prettier checks
- [x] `pnpm build`
- [ ] Manual UI: open `/dashboard/curations/new` and confirm the `PROGRAM`
card enters `/dashboard/curations/specs/PROGRAM/new`.
- [ ] Manual UI: add two programs and different whisky lists, then confirm
independent state, responsive layout, and submitted array order.
- [ ] Manual UI: open an existing `PROGRAM` curation and confirm edit hydration.

## Implementation Notes

- Do not duplicate list/detail spec queries inside specialized create gates.
- Specialized form components may remain as renderer strategies, but all
runtime create navigation and page-level state must be owned by
`CurationCreatePage`.
- The dynamic engine supports the schema vocabulary listed in the feature spec,
not arbitrary JSON Schema.
- Do not add static `PROGRAM` payload TypeScript interfaces that duplicate
`requestSpec`; form state remains a recursive record/array shape.
- Do not hardcode `programs.0.whiskies` in the whisky component. All paths must
derive from field models.
- Do not add program-specific enum label maps. Use a generic readable enum
formatter until the backend publishes enum label metadata.
- Do not edit `src/components/ui`.
- New React components must each live in their own file.
Loading