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
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Curation V2 Component Structure

큐레이션 v2 화면은 spec code별 page component가 presentation 조립을 맡고, 저장 폼과 API 계층은 공유한다.

```mermaid
flowchart TD
Routes["src/routes/index.tsx"] --> Entry["CurationEntryPage"]
Routes --> TastingCreate["CurationWhiskyTastingEventCreatePage"]
Routes --> RecommendedCreate["CurationRecommendedWhiskyCreatePage"]
Routes --> PairingCreate["CurationWhiskyPairingCreatePage"]
Routes --> Detail["CurationDetailPage"]

Detail -->|"WHISKY_TASTING_EVENT"| TastingEdit["CurationWhiskyTastingEventEditPage"]
Detail -->|"RECOMMENDED_WHISKY"| RecommendedEdit["CurationRecommendedWhiskyEditPage"]
Detail -->|"WHISKY_PAIRING"| PairingEdit["CurationWhiskyPairingEditPage"]
Detail -->|"unknown spec"| ReadOnlyDetail["CurationDetailContent"]

TastingCreate --> TastingGate["WhiskyTastingEventCreateGate"]
TastingGate --> TastingForm["WhiskyTastingEventForm"]
TastingEdit --> TastingForm

RecommendedCreate --> WhiskyGate["WhiskyCurationCreateGate"]
PairingCreate --> WhiskyGate
WhiskyGate --> WhiskyForm["WhiskyCurationForm"]
RecommendedEdit --> WhiskyForm
PairingEdit --> WhiskyForm

RecommendedCreate --> RecommendedPresentation["showCommentField: true"]
RecommendedEdit --> RecommendedPresentation
RecommendedPresentation --> WhiskyForm

PairingCreate --> PairingPresentation["showCommentField: false<br/>renderItemExtra: WhiskyPairingFields"]
PairingEdit --> PairingPresentation
PairingPresentation --> WhiskyForm

TastingForm --> BasicInfo["CurationBasicInfoSection<br/>label: 광고노출 시작일/종료일"]
WhiskyForm --> BasicInfoCuration["CurationBasicInfoSection<br/>label: 노출 시작일/종료일"]
WhiskyForm --> ImageSection["CurationImageSection"]

TastingForm --> FormSection["CurationFormSection"]
WhiskyForm --> FormSection
FormSection --> FieldRenderer["CurationFormFieldRenderer"]
FieldRenderer --> WhiskyCardList["CurationWhiskyCardListField"]
PairingPresentation --> PairingFields["WhiskyPairingFields"]

TastingForm --> TastingPreview["WhiskyTastingEventPreviewPanel"]
WhiskyForm --> WhiskyPreview["WhiskyCurationPreviewPanel"]
WhiskyPreview --> AppPreview["WhiskyCurationPreview"]

TastingForm --> Mutations["useCurationCreate / useCurationUpdate"]
WhiskyForm --> Mutations
Mutations --> Service["curationService"]
Service --> ApiTypes["src/types/api/curation.api.ts"]
```

## Entry Points

- 시음회 생성: `src/pages/curation/whisky-tasting-event/CurationWhiskyTastingEventCreate.tsx`
- 시음회 수정: `src/pages/curation/whisky-tasting-event/CurationWhiskyTastingEventEdit.tsx`
- 추천 위스키 생성: `src/pages/curation/whisky-curation/CurationRecommendedWhiskyCreate.tsx`
- 추천 위스키 수정: `src/pages/curation/whisky-curation/CurationRecommendedWhiskyEdit.tsx`
- 위스키 페어링 생성: `src/pages/curation/whisky-curation/CurationWhiskyPairingCreate.tsx`
- 위스키 페어링 수정: `src/pages/curation/whisky-curation/CurationWhiskyPairingEdit.tsx`

## Shared Layers

- `WhiskyTastingEventCreateGate`, `WhiskyCurationCreateGate`: spec list/detail loading and blocking states.
- `WhiskyTastingEventForm`, `WhiskyCurationForm`: create/update mutation and common form layout.
- `CurationBasicInfoSection`: shared basic fields. Tasting event keeps ad exposure copy; recommendation and pairing override to plain exposure copy.
- `CurationFormSection` and `CurationFormFieldRenderer`: JSON schema-derived payload fields.
- `curation.api.ts` -> `curation.service.ts` -> `useCurations.ts`: API type, service, TanStack Query hook ownership.
20 changes: 10 additions & 10 deletions src/pages/curation/CurationDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useCurationDetail } from '@/hooks/useCurations';
import { CurationSpecCode, type CurationV2Detail } from '@/types/api';

import { CurationTastingEventEditPage } from './tasting-event/CurationTastingEventCreate';
import { CurationWhiskyCardEditPage } from './whisky-card/CurationWhiskyCardCreate';
import { CurationWhiskyTastingEventEditPage } from './whisky-tasting-event/CurationWhiskyTastingEventEdit';
import { CurationRecommendedWhiskyEditPage } from './whisky-curation/CurationRecommendedWhiskyEdit';
import { CurationWhiskyPairingEditPage } from './whisky-curation/CurationWhiskyPairingEdit';
import {
formatCurationDateTime,
formatCurationExposurePeriod,
Expand All @@ -25,16 +26,15 @@ export function CurationDetailPage() {
const { data: curation, isLoading, isError, refetch } = useCurationDetail(curationId);

if (!isLoading && !isError && curation?.spec.code === CurationSpecCode.WHISKY_TASTING_EVENT) {
return <CurationTastingEventEditPage curation={curation} />;
return <CurationWhiskyTastingEventEditPage curation={curation} />;
}

if (
!isLoading &&
!isError &&
(curation?.spec.code === CurationSpecCode.RECOMMENDED_WHISKY ||
curation?.spec.code === CurationSpecCode.WHISKY_PAIRING)
) {
return <CurationWhiskyCardEditPage curation={curation} />;
if (!isLoading && !isError && curation?.spec.code === CurationSpecCode.RECOMMENDED_WHISKY) {
return <CurationRecommendedWhiskyEditPage curation={curation} />;
}

if (!isLoading && !isError && curation?.spec.code === CurationSpecCode.WHISKY_PAIRING) {
return <CurationWhiskyPairingEditPage curation={curation} />;
}

return (
Expand Down
10 changes: 5 additions & 5 deletions src/pages/curation/CurationEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import {
import {
CurationPreviewFrame,
TastingEventPreview,
WhiskyCardCurationPreview,
WhiskyCurationPreview,
type TastingEventPreviewData,
type WhiskyCardCurationPreviewData,
type WhiskyCurationPreviewData,
} from './_preview';

interface CurationSpecUiConfig {
Expand Down Expand Up @@ -299,7 +299,7 @@ function CurationEntryPreview({ specCode }: { specCode: KnownCurationV2SpecCode
{specCode === CurationSpecCode.WHISKY_TASTING_EVENT ? (
<TastingEventPreview event={TASTING_EVENT_PREVIEW_SAMPLE} />
) : (
<WhiskyCardCurationPreview
<WhiskyCurationPreview
curation={
specCode === CurationSpecCode.WHISKY_PAIRING
? WHISKY_PAIRING_PREVIEW_SAMPLE
Expand Down Expand Up @@ -355,7 +355,7 @@ const TASTING_EVENT_PREVIEW_SAMPLE: TastingEventPreviewData = {
},
};

const GENERAL_CURATION_PREVIEW_SAMPLE: WhiskyCardCurationPreviewData = {
const GENERAL_CURATION_PREVIEW_SAMPLE: WhiskyCurationPreviewData = {
specName: '일반 큐레이션',
name: '입문자를 위한 스카치 베스트 6',
description: '처음 위스키를 시작하는 사용자에게 권하기 좋은 부드러운 스카치 위스키입니다.',
Expand All @@ -368,7 +368,7 @@ const GENERAL_CURATION_PREVIEW_SAMPLE: WhiskyCardCurationPreviewData = {
comment: '밸런스가 좋아 니트와 하이볼 모두 부담 없이 즐길 수 있습니다.',
};

const WHISKY_PAIRING_PREVIEW_SAMPLE: WhiskyCardCurationPreviewData = {
const WHISKY_PAIRING_PREVIEW_SAMPLE: WhiskyCurationPreviewData = {
specName: '페어링 · 위스키 → 음식',
name: '글렌피딕 12년과 어울리는 디저트',
description: '부드러운 과일 향과 꿀 향을 살려주는 달콤한 페어링을 추천합니다.',
Expand Down
18 changes: 14 additions & 4 deletions src/pages/curation/__tests__/CurationDetail.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';

Expand Down Expand Up @@ -216,10 +216,18 @@ describe('CurationDetailPage', () => {
expect(screen.getByLabelText('큐레이션명')).toHaveValue('6월 싱글몰트 시음회');
expect(screen.getByLabelText('설명')).toHaveValue('');
expect(screen.getByLabelText('광고노출 시작일')).toHaveValue('');
expect(screen.getByLabelText('광고노출 시작일')).toBeDisabled();
expect(screen.getByText('광고노출 시작일은 등록 후 변경할 수 없습니다.')).toBeInTheDocument();
expect(screen.getByLabelText('광고노출 시작일')).not.toBeDisabled();
expect(
screen.getAllByText((_, element) => element?.textContent === '광고노출 시작일*').length
).toBeGreaterThan(0);
expect(
screen.getByText('기존 노출 시작일이 없어 이번 수정에서만 입력할 수 있습니다.')
).toBeInTheDocument();
expect(screen.getByLabelText('광고노출 종료일')).toHaveValue('');
expect(screen.getByLabelText('광고노출 종료일')).not.toBeDisabled();
expect(
screen.getAllByText((_, element) => element?.textContent === '광고노출 종료일*').length
).toBeGreaterThan(0);
expect(screen.getByText('날짜 및 장소')).toBeInTheDocument();
expect(screen.getByText('참가 정보')).toBeInTheDocument();
expect(screen.getAllByText('시음 위스키').length).toBeGreaterThan(0);
Expand All @@ -239,6 +247,7 @@ describe('CurationDetailPage', () => {
'첫 잔으로 가볍게 시작하는 위스키'
);

fireEvent.change(screen.getByLabelText('광고노출 시작일'), { target: { value: '2026-06-01' } });
await user.clear(screen.getByLabelText('안내사항'));
await user.type(screen.getByLabelText('안내사항'), '수정된 안내사항');
await user.click(screen.getByRole('button', { name: /수정/ }));
Expand All @@ -249,7 +258,7 @@ describe('CurationDetailPage', () => {
name: '6월 싱글몰트 시음회',
description: null,
imageUrls: ['https://cdn.example.com/cover.jpg'],
exposureStartDate: null,
exposureStartDate: '2026-06-01',
exposureEndDate: null,
displayOrder: 1,
isActive: true,
Expand Down Expand Up @@ -356,4 +365,5 @@ describe('CurationDetailPage', () => {
expect(screen.getByText('channel')).toBeInTheDocument();
expect(screen.getByText('home')).toBeInTheDocument();
});

});
2 changes: 1 addition & 1 deletion src/pages/curation/_preview/TastingEventPreviewExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const exampleEvent: TastingEventPreviewData = {
barAddress: '서울 강남구 테헤란로 123',
detailAddress: '4층',
isRecruiting: true,
applicationLink: 'https://example.com/tasting-event/apply',
applicationLink: 'https://example.com/whisky-tasting-event/apply',
alcohols: [
{
source: 'admin-preview-1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,35 @@ import type {
import { CurationPreviewWhiskyCard } from './CurationPreviewWhiskyCard';
import { tastingEventPreviewThemeStyle } from './previewTheme';

export interface WhiskyCardCurationPreviewPairing {
export interface WhiskyCurationPreviewPairing {
itemName: string;
pairingNote: string;
itemImageUrl?: string;
}

export interface WhiskyCardCurationPreviewData {
export interface WhiskyCurationPreviewData {
specName: string;
name: string;
description?: string | null;
imageUrls: string[];
alcohol?: CurationWhiskyMirror;
stats?: CurationWhiskyCardValue['stats'];
comment?: string | null;
pairings?: WhiskyCardCurationPreviewPairing[];
items?: Array<CurationWhiskyCardValue & { pairings?: WhiskyCardCurationPreviewPairing[] }>;
pairings?: WhiskyCurationPreviewPairing[];
items?: Array<CurationWhiskyCardValue & { pairings?: WhiskyCurationPreviewPairing[] }>;
}

interface WhiskyCardCurationPreviewProps {
curation: WhiskyCardCurationPreviewData;
interface WhiskyCurationPreviewProps {
curation: WhiskyCurationPreviewData;
pairingTitle?: string;
className?: string;
}

export function WhiskyCardCurationPreview({
export function WhiskyCurationPreview({
curation,
pairingTitle = '페어링 음식',
className,
}: WhiskyCardCurationPreviewProps) {
}: WhiskyCurationPreviewProps) {
const visibleItems = getVisibleItems(curation);
const coverImageUrl = curation.imageUrls[0] ?? visibleItems[0]?.alcohol.imageUrl ?? '';
const galleryImageUrls = curation.imageUrls.filter((imageUrl) => imageUrl !== coverImageUrl);
Expand Down Expand Up @@ -84,8 +84,8 @@ export function WhiskyCardCurationPreview({
}

function getVisibleItems(
curation: WhiskyCardCurationPreviewData
): Array<CurationWhiskyCardValue & { pairings?: WhiskyCardCurationPreviewPairing[] }> {
curation: WhiskyCurationPreviewData
): Array<CurationWhiskyCardValue & { pairings?: WhiskyCurationPreviewPairing[] }> {
const visibleItems =
curation.items?.filter(
(item) =>
Expand Down Expand Up @@ -192,7 +192,7 @@ function WhiskyPreviewItem({
order,
pairingTitle,
}: {
item: CurationWhiskyCardValue & { pairings?: WhiskyCardCurationPreviewPairing[] };
item: CurationWhiskyCardValue & { pairings?: WhiskyCurationPreviewPairing[] };
order: number;
pairingTitle: string;
}) {
Expand Down Expand Up @@ -224,7 +224,7 @@ function WhiskyPreviewItem({
);
}

function PairingPreviewItem({ pairing }: { pairing: WhiskyCardCurationPreviewPairing }) {
function PairingPreviewItem({ pairing }: { pairing: WhiskyCurationPreviewPairing }) {
return (
<article className="border-t border-[var(--preview-bg-gray)] pt-4">
<div className="flex gap-3">
Expand Down
8 changes: 4 additions & 4 deletions src/pages/curation/_preview/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export { buildTastingEventPreviewModel } from './buildTastingEventPreviewModel';
export { CurationPreviewFrame } from './CurationPreviewFrame';
export { TastingEventPreview } from './TastingEventPreview';
export { TastingEventPreviewExample } from './TastingEventPreviewExample';
export { WhiskyCardCurationPreview } from './WhiskyCardCurationPreview';
export { WhiskyCurationPreview } from './WhiskyCurationPreview';
export type {
TastingEventPreviewAlcoholItem,
TastingEventPreviewCta,
Expand All @@ -11,6 +11,6 @@ export type {
TastingEventPreviewPayload,
} from './types';
export type {
WhiskyCardCurationPreviewData,
WhiskyCardCurationPreviewPairing,
} from './WhiskyCardCurationPreview';
WhiskyCurationPreviewData,
WhiskyCurationPreviewPairing,
} from './WhiskyCurationPreview';
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Info } from 'lucide-react';

import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';

export function ActiveExposureTooltip() {
export function CurationActiveExposureTooltip() {
return (
<TooltipProvider delayDuration={100}>
<Tooltip>
Expand Down
Loading