diff --git a/src/components/common/PlaceSearchInput.tsx b/src/components/common/PlaceSearchInput.tsx index 0b437c3..19e34a8 100644 --- a/src/components/common/PlaceSearchInput.tsx +++ b/src/components/common/PlaceSearchInput.tsx @@ -96,7 +96,7 @@ interface PlaceSearchPaginationState { export interface PlaceSearchInputProps { registration: UseFormRegisterReturn; - onAddressSelect: (address: string) => void; + onAddressSelect?: (address: string) => void; onPlaceSelect?: (place: SelectedPlace) => void; placeholder?: string; maxLength?: number; @@ -189,7 +189,7 @@ export function PlaceSearchInput({ const handleSelectPlace = (place: KakaoPlaceDocument) => { const address = place.road_address_name || place.address_name; - onAddressSelect(address); + onAddressSelect?.(address); onPlaceSelect?.({ id: place.id, placeName: place.place_name, diff --git a/src/components/common/__tests__/PlaceSearchInput.test.tsx b/src/components/common/__tests__/PlaceSearchInput.test.tsx index 573fef2..de322d6 100644 --- a/src/components/common/__tests__/PlaceSearchInput.test.tsx +++ b/src/components/common/__tests__/PlaceSearchInput.test.tsx @@ -46,9 +46,11 @@ function setKakaoPlacesMock() { function Harness({ defaultValue = '', + onAddressSelect = true, onPlaceSelect, }: { defaultValue?: string; + onAddressSelect?: boolean; onPlaceSelect?: (place: SelectedPlace) => void; }) { const { register, setValue } = useForm({ defaultValues: { addr: defaultValue } }); @@ -57,7 +59,7 @@ function Harness({ setValue('addr', addr)} + onAddressSelect={onAddressSelect ? (addr) => setValue('addr', addr) : undefined} onPlaceSelect={onPlaceSelect} /> ); @@ -123,6 +125,45 @@ describe('PlaceSearchInput', () => { expect(screen.queryByText('도시남 바')).not.toBeInTheDocument(); }); + it('주소 변경 콜백 없이도 장소 선택 정보를 전달한다', async () => { + const handlePlaceSelect = vi.fn(); + setKakaoPlacesMock(); + keywordSearchMock.mockImplementation((_keyword, callback) => { + callback( + [ + { + id: '123', + place_name: '도시남 바', + category_name: '음식점 > 술집', + phone: '02-123-4567', + address_name: '서울 강남구 역삼동 123-45', + road_address_name: '서울 강남구 테헤란로 123', + x: '127.0276', + y: '37.4979', + place_url: 'https://place.map.kakao.com/123', + }, + ], + 'OK', + { ...paginationPage1, last: 1, totalCount: 1, hasNextPage: false } + ); + }); + + render(); + + fireEvent.click(screen.getByRole('button', { name: '장소 검색' })); + fireEvent.change(screen.getByLabelText('장소 검색어'), { target: { value: '도시남' } }); + fireEvent.click(screen.getByRole('button', { name: '검색' })); + fireEvent.click(await screen.findByText('도시남 바')); + + expect(handlePlaceSelect).toHaveBeenCalledWith( + expect.objectContaining({ + id: '123', + placeName: '도시남 바', + address: '서울 강남구 테헤란로 123', + }) + ); + }); + it('검색 결과 하단에서 다음 페이지를 조회한다', async () => { setKakaoPlacesMock(); keywordSearchMock.mockImplementation((keyword, callback, options) => { @@ -180,11 +221,10 @@ describe('PlaceSearchInput', () => { fireEvent.click(screen.getByRole('button', { name: '다음' })); expect(await screen.findByText('도시남 라운지')).toBeInTheDocument(); - expect(keywordSearchMock).toHaveBeenLastCalledWith( - '도시남', - expect.any(Function), - { size: 10, page: 2 } - ); + expect(keywordSearchMock).toHaveBeenLastCalledWith('도시남', expect.any(Function), { + size: 10, + page: 2, + }); expect(screen.getByText('총 12개 · 2 / 2 페이지')).toBeInTheDocument(); }); diff --git a/src/pages/curation/__tests__/CurationDetail.test.tsx b/src/pages/curation/__tests__/CurationDetail.test.tsx index 3e3cbfa..05410ae 100644 --- a/src/pages/curation/__tests__/CurationDetail.test.tsx +++ b/src/pages/curation/__tests__/CurationDetail.test.tsx @@ -56,11 +56,32 @@ const tastingEventSpec: CurationV2Spec = { 'x-field-style': 'time', 'x-display-name': '시음회 시간', }, + placeName: { + type: 'string', + maxLength: 100, + description: '시음회 장소명', + 'x-field-style': 'address-search', + 'x-display-name': '장소명', + 'x-place-search-targets': { + placeName: 'placeName', + kakaoPlaceId: 'id', + barAddress: 'address', + }, + }, + kakaoPlaceId: { + type: 'string', + minLength: 1, + maxLength: 20, + description: 'Kakao Places 장소 ID', + 'x-field-style': 'hidden', + 'x-display-name': 'Kakao 장소 ID', + }, barAddress: { type: 'string', maxLength: 200, description: '장소 및 바 주소', 'x-field-style': 'plain-text', + 'x-read-only': true, 'x-display-name': '장소 및 바(bar) 주소', }, isRecruiting: { @@ -210,6 +231,7 @@ describe('CurationDetailPage', () => { eventDate: '2026-06-15', eventTime: '19:30', placeName: '도시남 바', + kakaoPlaceId: '123', barAddress: '서울 강남구 테헤란로 123', isRecruiting: true, entryFee: 50000, @@ -312,6 +334,7 @@ describe('CurationDetailPage', () => { eventDate: '2026-06-15', eventTime: '19:30', placeName: '도시남 바', + kakaoPlaceId: '123', barAddress: '서울 강남구 테헤란로 123', capacity: 0, guideText: '수정된 안내사항', diff --git a/src/pages/curation/components/CurationFormFieldRenderer.tsx b/src/pages/curation/components/CurationFormFieldRenderer.tsx index a1650af..6e63f2c 100644 --- a/src/pages/curation/components/CurationFormFieldRenderer.tsx +++ b/src/pages/curation/components/CurationFormFieldRenderer.tsx @@ -200,6 +200,7 @@ function TextInputField({ type={field.kind === 'text' ? 'text' : field.kind} maxLength={field.maxLength} placeholder={field.placeholder} + readOnly={field.readOnly} {...form.register(name)} /> ); @@ -305,29 +306,29 @@ function AddressField({ placeholder={field.placeholder} maxLength={field.maxLength} registration={form.register(name)} - onAddressSelect={(next) => - form.setValue(name, next, { shouldDirty: true, shouldValidate: true }) - } - onPlaceSelect={(place) => syncPlaceSelection(form, place)} + onPlaceSelect={(place) => syncPlaceSelection(form, field.placeSearchTargets, place)} /> ); } function syncPlaceSelection( form: ReturnType>, + targets: CurationTextFieldModel['placeSearchTargets'], place: SelectedPlace ) { - const valuesByField = { + if (!targets) return; + + const valuesBySource = { placeName: place.placeName, - kakaoPlaceId: place.id, - barAddress: place.address, + id: place.id, address: place.address, }; - for (const [fieldName, value] of Object.entries(valuesByField)) { - if (typeof form.getValues(fieldName) !== 'string') continue; - - form.setValue(fieldName, value, { shouldDirty: true, shouldValidate: true }); + for (const [fieldName, source] of Object.entries(targets)) { + form.setValue(fieldName, valuesBySource[source], { + shouldDirty: true, + shouldValidate: true, + }); } } diff --git a/src/pages/curation/components/__tests__/CurationFormFieldRenderer.test.tsx b/src/pages/curation/components/__tests__/CurationFormFieldRenderer.test.tsx index 6d5ba55..6c6ff37 100644 --- a/src/pages/curation/components/__tests__/CurationFormFieldRenderer.test.tsx +++ b/src/pages/curation/components/__tests__/CurationFormFieldRenderer.test.tsx @@ -51,7 +51,7 @@ function FieldHarness({ } describe('CurationFormFieldRenderer', () => { - it('장소 검색 결과를 request spec에 있는 장소 필드로 함께 매핑한다', async () => { + it('시음회 스펙이 선언한 target만 장소 검색 결과로 매핑한다', async () => { const user = userEvent.setup(); render( @@ -61,12 +61,18 @@ describe('CurationFormFieldRenderer', () => { label: '장소명', required: false, kind: 'address', + placeSearchTargets: { + placeName: 'placeName', + kakaoPlaceId: 'id', + barAddress: 'address', + }, }} defaultValues={{ placeName: '', kakaoPlaceId: '', barAddress: '', - detailAddress: '', + address: '프로그램 주소는 변경하지 않음', + detailAddress: '2층 안쪽 입구', }} /> ); @@ -74,31 +80,38 @@ describe('CurationFormFieldRenderer', () => { await user.click(await screen.findByRole('button', { name: '장소 선택' })); await waitFor(() => { - expect(JSON.parse(screen.getByTestId('form-values').textContent ?? '{}')).toMatchObject({ + expect(JSON.parse(screen.getByTestId('form-values').textContent ?? '{}')).toEqual({ placeName: '보틀노트 테이스팅룸', kakaoPlaceId: '27288225', barAddress: '서울 강남구 테헤란로 123', - detailAddress: '', + address: '프로그램 주소는 변경하지 않음', + detailAddress: '2층 안쪽 입구', }); }); }); - it('프로그램 주소 검색도 같은 선택값을 address와 Kakao 장소 ID에 매핑한다', async () => { + it('프로그램 스펙은 address target만 장소 검색 결과로 매핑한다', async () => { const user = userEvent.setup(); render( ); @@ -106,15 +119,36 @@ describe('CurationFormFieldRenderer', () => { await user.click(await screen.findByRole('button', { name: '장소 선택' })); await waitFor(() => { - expect(JSON.parse(screen.getByTestId('form-values').textContent ?? '{}')).toMatchObject({ + expect(JSON.parse(screen.getByTestId('form-values').textContent ?? '{}')).toEqual({ placeName: '보틀노트 테이스팅룸', kakaoPlaceId: '27288225', address: '서울 강남구 테헤란로 123', - detailLocation: '', + barAddress: '시음회 주소는 변경하지 않음', + detailAddress: 'B홀 2층', }); }); }); + it('x-read-only plain text는 disabled가 아닌 readOnly input으로 렌더링한다', () => { + render( + + ); + + const input = screen.getByLabelText('장소 및 바 주소'); + expect(input).toHaveValue('서울 강남구 테헤란로 123'); + expect(input).toHaveProperty('readOnly', true); + expect(input).not.toBeDisabled(); + }); + it('hidden field는 라벨 없이 전송용 input으로만 렌더링한다', () => { const { container } = render( ; + export interface CurationBaseFieldModel { key: string; label: string; @@ -27,12 +30,14 @@ export interface CurationBaseFieldModel { defaultValue?: unknown; nullable?: boolean; ariaLabel?: string; + readOnly?: boolean; } export interface CurationTextFieldModel extends CurationBaseFieldModel { kind: 'text' | 'textarea' | 'date' | 'time' | 'address' | 'hidden'; minLength?: number; maxLength?: number; + placeSearchTargets?: CurationPlaceSearchTargets; } export interface CurationNumberFieldModel extends CurationBaseFieldModel { @@ -218,6 +223,28 @@ export function getSchemaXString(schema: JsonSchemaNode, key: `x-${string}`): st return typeof value === 'string' ? value : undefined; } +export function getSchemaXBoolean(schema: JsonSchemaNode, key: `x-${string}`): boolean | undefined { + const value = schema[key]; + return typeof value === 'boolean' ? value : undefined; +} + +export function getSchemaPlaceSearchTargets( + schema: JsonSchemaNode +): CurationPlaceSearchTargets | undefined { + const targets = schema['x-place-search-targets']; + if (!targets || typeof targets !== 'object' || Array.isArray(targets)) return undefined; + + const validSources = new Set(['placeName', 'id', 'address']); + const entries = Object.entries(targets).filter( + (entry): entry is [string, CurationPlaceSearchTargetSource] => + entry[0].length > 0 && + typeof entry[1] === 'string' && + validSources.has(entry[1] as CurationPlaceSearchTargetSource) + ); + + return entries.length > 0 ? Object.fromEntries(entries) : undefined; +} + export function getSchemaDisplayLabel(schema: JsonSchemaNode): string { return getSchemaXString(schema, 'x-display-name') ?? schema.description ?? schema.title ?? ''; } @@ -240,7 +267,6 @@ function resolveSchemaKind( schema: JsonSchemaNode, fieldStyle?: string ): CurationFieldKind { - if (fieldStyle === 'address-search' && key.toLowerCase().endsWith('placeid')) return 'hidden'; if (fieldStyle === 'long-text') return 'textarea'; if (fieldStyle === 'plain-text') return 'text'; if (fieldStyle === 'address-search') return 'address'; @@ -282,6 +308,7 @@ export function createCurationBasicFieldModel( placeholder: getSchemaPlaceholder(fieldSchema), defaultValue: fieldSchema.default, nullable: fieldSchema.nullable, + readOnly: getSchemaXBoolean(fieldSchema, 'x-read-only'), }; const kind = getSchemaFieldKind(key, fieldSchema); @@ -315,10 +342,17 @@ export function createCurationBasicFieldModel( minItems: fieldSchema.minItems ?? 0, maxItems: typeof fieldSchema.maxItems === 'number' ? fieldSchema.maxItems : undefined, }; + case 'address': + return { + ...base, + kind, + minLength: fieldSchema.minLength, + maxLength: fieldSchema.maxLength, + placeSearchTargets: getSchemaPlaceSearchTargets(fieldSchema), + }; case 'textarea': case 'date': case 'time': - case 'address': case 'hidden': case 'text': return { diff --git a/src/pages/curation/schema-driven/__tests__/program-spec.fixture.ts b/src/pages/curation/schema-driven/__tests__/program-spec.fixture.ts index ddd13c5..c74bd24 100644 --- a/src/pages/curation/schema-driven/__tests__/program-spec.fixture.ts +++ b/src/pages/curation/schema-driven/__tests__/program-spec.fixture.ts @@ -25,15 +25,35 @@ export const programSpec: CurationV2Spec = { placeName: { type: 'string', maxLength: 100, - 'x-field-style': 'plain-text', + 'x-field-style': 'address-search', 'x-display-name': '장소명', + 'x-place-search-targets': { + placeName: 'placeName', + kakaoPlaceId: 'id', + address: 'address', + }, + }, + kakaoPlaceId: { + type: 'string', + minLength: 1, + maxLength: 20, + 'x-field-style': 'hidden', + 'x-display-name': 'Kakao 장소 ID', }, address: { type: 'string', maxLength: 200, 'x-field-style': 'plain-text', + 'x-read-only': true, 'x-display-name': '장소 및 주소', }, + detailAddress: { + type: 'string', + maxLength: 200, + nullable: true, + 'x-field-style': 'plain-text', + 'x-display-name': '상세 주소', + }, entryFee: { type: 'integer', minimum: 0, diff --git a/src/pages/curation/schema-driven/__tests__/schema-driven-curation.form-model.test.ts b/src/pages/curation/schema-driven/__tests__/schema-driven-curation.form-model.test.ts index d549abc..895d5b1 100644 --- a/src/pages/curation/schema-driven/__tests__/schema-driven-curation.form-model.test.ts +++ b/src/pages/curation/schema-driven/__tests__/schema-driven-curation.form-model.test.ts @@ -12,13 +12,30 @@ describe('createSchemaDrivenCurationFormModel', () => { expect(model.payloadFields.map((field) => [field.key, field.kind])).toEqual([ ['eventStartDate', 'date'], ['eventEndDate', 'date'], - ['placeName', 'text'], + ['placeName', 'address'], + ['kakaoPlaceId', 'hidden'], ['address', 'text'], + ['detailAddress', 'text'], ['entryFee', 'number'], ['programTags', 'multi-select'], ['programs', 'object-array'], ]); + const placeName = model.payloadFields.find((field) => field.key === 'placeName'); + const address = model.payloadFields.find((field) => field.key === 'address'); + const detailAddress = model.payloadFields.find((field) => field.key === 'detailAddress'); + + expect(placeName).toMatchObject({ + kind: 'address', + placeSearchTargets: { + placeName: 'placeName', + kakaoPlaceId: 'id', + address: 'address', + }, + }); + expect(address).toMatchObject({ kind: 'text', readOnly: true }); + expect(detailAddress).toMatchObject({ kind: 'text', readOnly: undefined, nullable: true }); + const programTags = model.payloadFields.find((field) => field.key === 'programTags'); expect(programTags).toMatchObject({ kind: 'multi-select', diff --git a/src/pages/curation/whisky-tasting-event/__tests__/CurationWhiskyTastingEventCreate.test.tsx b/src/pages/curation/whisky-tasting-event/__tests__/CurationWhiskyTastingEventCreate.test.tsx index ed8b821..3ff59db 100644 --- a/src/pages/curation/whisky-tasting-event/__tests__/CurationWhiskyTastingEventCreate.test.tsx +++ b/src/pages/curation/whisky-tasting-event/__tests__/CurationWhiskyTastingEventCreate.test.tsx @@ -91,10 +91,32 @@ const tastingEventSpec: CurationV2Spec = { description: '시음회 시간', 'x-display-name': '시음회 시간', }, + placeName: { + type: 'string', + maxLength: 100, + description: '시음회 장소명', + 'x-field-style': 'address-search', + 'x-display-name': '장소명', + 'x-place-search-targets': { + placeName: 'placeName', + kakaoPlaceId: 'id', + barAddress: 'address', + }, + }, + kakaoPlaceId: { + type: 'string', + minLength: 1, + maxLength: 20, + description: 'Kakao Places 장소 ID', + 'x-field-style': 'hidden', + 'x-display-name': 'Kakao 장소 ID', + }, barAddress: { type: 'string', maxLength: 200, description: '장소 및 바 주소', + 'x-field-style': 'plain-text', + 'x-read-only': true, 'x-display-name': '장소 및 바(bar) 주소', }, detailAddress: { @@ -277,6 +299,37 @@ function setKakaoPlacesMock() { }; } +function mockDefaultKakaoPlaceSearch() { + setKakaoPlacesMock(); + keywordSearchMock.mockImplementation((_, callback, options) => { + expect(options).toEqual({ size: 10, page: 1 }); + callback( + [ + { + id: '123', + place_name: '도시남 바', + category_name: '음식점 > 술집', + phone: '02-123-4567', + address_name: '서울 강남구 역삼동 123-45', + road_address_name: '서울 강남구 테헤란로 123', + x: '127.0276', + y: '37.4979', + place_url: 'https://place.map.kakao.com/123', + }, + ], + 'OK', + placeSearchPagination + ); + }); +} + +async function selectDefaultPlace(user: ReturnType) { + await user.click(await screen.findByRole('button', { name: '장소 검색' })); + fireEvent.change(screen.getByLabelText('장소 검색어'), { target: { value: '도시남' } }); + await user.click(screen.getByRole('button', { name: '검색' })); + await user.click(await screen.findByText('도시남 바')); +} + async function typeTastingTagSearch( user: ReturnType, comboboxName: string, @@ -395,7 +448,7 @@ describe('CurationCreatePage whisky tasting event strategy', () => { expect(capacityInput).toBeEnabled(); }); - it('장소명 input 클릭으로 장소 검색을 열고 선택한 장소명을 빈 상세 주소에만 채운다', async () => { + it('장소 검색 선택값만 target 필드에 반영하고 상세 주소는 유지한다', async () => { const user = userEvent.setup(); mockSpecSuccess(); setKakaoPlacesMock(); @@ -436,12 +489,9 @@ describe('CurationCreatePage whisky tasting event strategy', () => { await user.click(await screen.findByText('도시남 바')); expect(placeNameInput).toHaveValue('도시남 바'); - expect(placeNameInput).not.toHaveAttribute('readonly'); + expect(placeNameInput).toHaveAttribute('readonly'); expect(screen.getByLabelText('장소 및 바(bar) 주소')).toHaveValue('서울 강남구 테헤란로 123'); - expect(screen.getByLabelText('상세 주소')).toHaveValue('도시남 바'); - - fireEvent.change(placeNameInput, { target: { value: '도시남 바 수정' } }); - expect(placeNameInput).toHaveValue('도시남 바 수정'); + expect(screen.getByLabelText('상세 주소')).toHaveValue(''); fireEvent.change(screen.getByLabelText('상세 주소'), { target: { value: '2층 별실' } }); await user.click(screen.getByRole('button', { name: '장소 검색' })); @@ -753,6 +803,7 @@ describe('CurationCreatePage whisky tasting event strategy', () => { it('시음 위스키를 추가하지 않고 저장하면 alcohols validation을 표시한다', async () => { const user = userEvent.setup(); mockSpecSuccess(); + mockDefaultKakaoPlaceSearch(); render(); @@ -768,10 +819,7 @@ describe('CurationCreatePage whisky tasting event strategy', () => { fireEvent.change(screen.getByLabelText('광고노출 종료일'), { target: { value: '2099-06-30' } }); fireEvent.change(screen.getByLabelText('시음회 날짜'), { target: { value: '2026-06-15' } }); fireEvent.change(screen.getByLabelText('시음회 시간'), { target: { value: '19:30' } }); - fireEvent.change(screen.getByLabelText('장소 및 바(bar) 주소'), { - target: { value: '서울 강남구 테헤란로 123' }, - }); - fireEvent.change(screen.getByLabelText('장소명'), { target: { value: '도시남 바' } }); + await selectDefaultPlace(user); fireEvent.change(screen.getByLabelText('상세 주소'), { target: { value: '2층 도시남 바' } }); fireEvent.change(screen.getByLabelText('참가비(1인당)'), { target: { value: '75000' } }); fireEvent.change(screen.getByLabelText('총 모집 인원수'), { target: { value: '20' } }); @@ -794,6 +842,7 @@ describe('CurationCreatePage whisky tasting event strategy', () => { const user = userEvent.setup(); let capturedBody: CurationV2CreateRequest | null = null; mockSpecSuccess(); + mockDefaultKakaoPlaceSearch(); server.use( http.post(CURATION_BASE, async ({ request }) => { capturedBody = (await request.json()) as CurationV2CreateRequest; @@ -822,10 +871,7 @@ describe('CurationCreatePage whisky tasting event strategy', () => { fireEvent.change(screen.getByLabelText('광고노출 종료일'), { target: { value: '2099-06-30' } }); fireEvent.change(screen.getByLabelText('시음회 날짜'), { target: { value: '2026-06-15' } }); fireEvent.change(screen.getByLabelText('시음회 시간'), { target: { value: '19:30' } }); - fireEvent.change(screen.getByLabelText('장소 및 바(bar) 주소'), { - target: { value: '서울 강남구 테헤란로 123' }, - }); - fireEvent.change(screen.getByLabelText('장소명'), { target: { value: '도시남 바' } }); + await selectDefaultPlace(user); fireEvent.change(screen.getByLabelText('상세 주소'), { target: { value: '2층 도시남 바' } }); fireEvent.change(screen.getByLabelText('참가비(1인당)'), { target: { value: '75000' } }); await user.click(screen.getByRole('checkbox', { name: '모집 인원 미정' })); @@ -881,6 +927,7 @@ describe('CurationCreatePage whisky tasting event strategy', () => { eventTime: '19:30', barAddress: '서울 강남구 테헤란로 123', placeName: '도시남 바', + kakaoPlaceId: '123', detailAddress: '2층 도시남 바', isRecruiting: true, entryFee: 75000, @@ -918,6 +965,7 @@ describe('CurationCreatePage whisky tasting event strategy', () => { const user = userEvent.setup(); let capturedBody: CurationV2CreateRequest | null = null; mockSpecSuccess(); + mockDefaultKakaoPlaceSearch(); server.use( http.post(CURATION_BASE, async ({ request }) => { capturedBody = (await request.json()) as CurationV2CreateRequest; @@ -946,10 +994,7 @@ describe('CurationCreatePage whisky tasting event strategy', () => { fireEvent.change(screen.getByLabelText('광고노출 종료일'), { target: { value: '2099-06-30' } }); fireEvent.change(screen.getByLabelText('시음회 날짜'), { target: { value: '2026-06-15' } }); fireEvent.change(screen.getByLabelText('시음회 시간'), { target: { value: '19:30' } }); - fireEvent.change(screen.getByLabelText('장소 및 바(bar) 주소'), { - target: { value: '서울 강남구 테헤란로 123' }, - }); - fireEvent.change(screen.getByLabelText('장소명'), { target: { value: '도시남 바' } }); + await selectDefaultPlace(user); fireEvent.change(screen.getByLabelText('상세 주소'), { target: { value: '2층 도시남 바' } }); fireEvent.change(screen.getByLabelText('참가비(1인당)'), { target: { value: '75000' } }); fireEvent.change(screen.getByLabelText('총 모집 인원수'), { target: { value: '20' } }); @@ -993,6 +1038,7 @@ describe('CurationCreatePage whisky tasting event strategy', () => { const user = userEvent.setup(); let capturedBody: CurationV2CreateRequest | null = null; mockSpecSuccess(); + mockDefaultKakaoPlaceSearch(); mockImageUpload(['https://cdn.example.com/curation/manual-whisky.jpg']); server.use( http.post(CURATION_BASE, async ({ request }) => { @@ -1022,10 +1068,7 @@ describe('CurationCreatePage whisky tasting event strategy', () => { fireEvent.change(screen.getByLabelText('광고노출 종료일'), { target: { value: '2099-06-30' } }); fireEvent.change(screen.getByLabelText('시음회 날짜'), { target: { value: '2026-06-15' } }); fireEvent.change(screen.getByLabelText('시음회 시간'), { target: { value: '19:30' } }); - fireEvent.change(screen.getByLabelText('장소 및 바(bar) 주소'), { - target: { value: '서울 강남구 테헤란로 123' }, - }); - fireEvent.change(screen.getByLabelText('장소명'), { target: { value: '도시남 바' } }); + await selectDefaultPlace(user); fireEvent.change(screen.getByLabelText('상세 주소'), { target: { value: '2층 도시남 바' } }); fireEvent.change(screen.getByLabelText('참가비(1인당)'), { target: { value: '75000' } }); fireEvent.change(screen.getByLabelText('총 모집 인원수'), { target: { value: '20' } }); diff --git a/src/pages/curation/whisky-tasting-event/__tests__/whisky-tasting-event.form-model.test.ts b/src/pages/curation/whisky-tasting-event/__tests__/whisky-tasting-event.form-model.test.ts index 7bea5ba..8790dbd 100644 --- a/src/pages/curation/whisky-tasting-event/__tests__/whisky-tasting-event.form-model.test.ts +++ b/src/pages/curation/whisky-tasting-event/__tests__/whisky-tasting-event.form-model.test.ts @@ -43,13 +43,18 @@ const tastingEventSpec: CurationV2Spec = { description: '시음회 장소명', 'x-field-style': 'address-search', 'x-display-name': '장소명', + 'x-place-search-targets': { + placeName: 'placeName', + kakaoPlaceId: 'id', + barAddress: 'address', + }, }, kakaoPlaceId: { type: 'string', minLength: 1, maxLength: 20, description: 'Kakao Places 장소 ID', - 'x-field-style': 'address-search', + 'x-field-style': 'hidden', 'x-display-name': 'Kakao 장소 ID', }, barAddress: { @@ -57,6 +62,7 @@ const tastingEventSpec: CurationV2Spec = { maxLength: 200, description: '장소 및 바 주소', 'x-field-style': 'plain-text', + 'x-read-only': true, 'x-display-name': '장소 및 바(bar) 주소', }, detailAddress: { @@ -152,12 +158,18 @@ describe('createWhiskyTastingEventFormModel', () => { expect(fieldsByKey.barAddress).toMatchObject({ label: '장소 및 바(bar) 주소', kind: 'text', + readOnly: true, }); expect(fieldsByKey.placeName).toMatchObject({ label: '장소명', kind: 'address', required: false, maxLength: 100, + placeSearchTargets: { + placeName: 'placeName', + kakaoPlaceId: 'id', + barAddress: 'address', + }, }); expect(fieldsByKey.kakaoPlaceId).toMatchObject({ label: 'Kakao 장소 ID',