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
8 changes: 8 additions & 0 deletions src/features/post/api/post-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ describe('postApi', () => {
photoUrls: ['https://cdn.example.com/offer.jpg'],
pointCost: 25000,
location: '여의도',
lat: 37.5283,
lng: 126.9326,
deadline: '2026-06-30',
detailDescription: '그늘막 포함',
desiredPrice: 50000,
Expand All @@ -71,6 +73,8 @@ describe('postApi', () => {
expect(url).toBe('/api/backend/v1/feeds/offer');
expect(body).toMatchObject({
categories: ['음식_요리'],
lat: 37.5283,
lng: 126.9326,
desiredPrice: 50000,
maxPartnerCount: 4,
plan,
Expand Down Expand Up @@ -102,6 +106,8 @@ describe('postApi', () => {
photoUrls: [],
pointCost: 15000,
location: '합정동',
lat: 37.5496,
lng: 126.9136,
deadline: '2026-06-30',
detailDescription: '야간 이용 가능',
serviceStylePhotoUrl: 'https://cdn.example.com/style.jpg',
Expand All @@ -118,6 +124,8 @@ describe('postApi', () => {
expect(url).toBe('/api/backend/v1/feeds/request');
expect(body).toMatchObject({
categories: ['BBQ_조개'],
lat: 37.5496,
lng: 126.9136,
priceCapPerPerson: 30000,
maxPartnerCount: 3,
plan,
Expand Down
4 changes: 2 additions & 2 deletions src/features/post/api/post-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ type BasePostPayload = {
deadline: string;
detailDescription: string;
maxPartnerCount?: number;
lat?: number;
lng?: number;
lat: number;
lng: number;
plan?: PlanV3;
preparation?: Preparation;
priceBreakdown?: PriceBreakdown;
Expand Down
19 changes: 15 additions & 4 deletions src/features/post/client/OfferFormClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,13 @@ export function OfferFormClient() {
categories,
photoPreviews,
location,
selectedLocation,
deadline,
setSpotName,
setTitle,
setContent,
setCategories,
setLocation,
setSelectedLocation,
setDeadline,
handleAddPhoto,
handleRemovePhoto,
Expand All @@ -87,7 +88,8 @@ export function OfferFormClient() {
const isStep0Valid =
spotName.trim() !== '' &&
title.trim() !== '' &&
location.trim() !== '' &&
content.trim() !== '' &&
selectedLocation !== null &&
deadline !== '';
const isStep1Valid = detailDescription.trim() !== '';
const isStep2Valid =
Expand Down Expand Up @@ -122,6 +124,13 @@ export function OfferFormClient() {
setStep((s) => s + 1);
window.scrollTo({ top: 0, behavior: 'smooth' });
} else {
const locationForSubmit = selectedLocation;

if (!locationForSubmit) {
setSubmitError('지도에서 활동 위치를 선택해주세요.');
return;
}

setIsSubmitting(true);
setSubmitError(null);

Expand All @@ -135,6 +144,8 @@ export function OfferFormClient() {
photoUrls: photoPreviews,
pointCost: POINT_COST,
location,
lat: locationForSubmit.lat,
lng: locationForSubmit.lng,
deadline,
detailDescription,
supporterPhotoUrl: supporterPhotoPreview ?? undefined,
Expand Down Expand Up @@ -210,15 +221,15 @@ export function OfferFormClient() {
content={content}
categories={categories}
photoPreviews={photoPreviews}
location={location}
selectedLocation={selectedLocation}
deadline={deadline}
onSpotNameChange={setSpotName}
onTitleChange={setTitle}
onContentChange={setContent}
onCategoriesChange={setCategories}
onAddPhoto={handleAddPhoto}
onRemovePhoto={handleRemovePhoto}
onLocationChange={setLocation}
onLocationChange={setSelectedLocation}
onDeadlineChange={setDeadline}
/>
)}
Expand Down
19 changes: 15 additions & 4 deletions src/features/post/client/RequestFormClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,13 @@ export function RequestFormClient() {
categories,
photoPreviews,
location,
selectedLocation,
deadline,
setSpotName,
setTitle,
setContent,
setCategories,
setLocation,
setSelectedLocation,
setDeadline,
handleAddPhoto,
handleRemovePhoto,
Expand All @@ -87,7 +88,8 @@ export function RequestFormClient() {
const isStep0Valid =
spotName.trim() !== '' &&
title.trim() !== '' &&
location.trim() !== '' &&
content.trim() !== '' &&
selectedLocation !== null &&
deadline !== '';
const isStep1Valid = detailDescription.trim() !== '';
// step 2 (플랜·준비물) 는 선택. 항상 다음 진행 가능.
Expand All @@ -111,6 +113,13 @@ export function RequestFormClient() {
setStep((s) => s + 1);
window.scrollTo({ top: 0, behavior: 'smooth' });
} else {
const locationForSubmit = selectedLocation;

if (!locationForSubmit) {
setSubmitError('지도에서 활동 위치를 선택해주세요.');
return;
}

setIsSubmitting(true);
setSubmitError(null);

Expand All @@ -124,6 +133,8 @@ export function RequestFormClient() {
photoUrls: photoPreviews,
pointCost: POINT_COST,
location,
lat: locationForSubmit.lat,
lng: locationForSubmit.lng,
deadline,
detailDescription,
serviceStylePhotoUrl: stylePhotoPreview ?? undefined,
Expand Down Expand Up @@ -199,15 +210,15 @@ export function RequestFormClient() {
content={content}
categories={categories}
photoPreviews={photoPreviews}
location={location}
selectedLocation={selectedLocation}
deadline={deadline}
onSpotNameChange={setSpotName}
onTitleChange={setTitle}
onContentChange={setContent}
onCategoriesChange={setCategories}
onAddPhoto={handleAddPhoto}
onRemovePhoto={handleRemovePhoto}
onLocationChange={setLocation}
onLocationChange={setSelectedLocation}
onDeadlineChange={setDeadline}
/>
)}
Expand Down
2 changes: 2 additions & 0 deletions src/features/post/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface PostBaseFormData {
photoPreviews: string[];
pointCost: number;
location: string;
lat: number;
lng: number;
deadline: string; // ISO date YYYY-MM-DD
}

Expand Down
37 changes: 36 additions & 1 deletion src/features/post/model/use-post-base-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useEffect, useState, useSyncExternalStore } from 'react';
import type { PostSpotCategory } from './types';
import type { SelectedPostLocation } from '../ui/post-form/MapLocationPicker';

const DRAFT_KEY = 'post-base-form-draft';
const EMPTY_DRAFT: BaseFormDraft = {
Expand All @@ -10,6 +11,8 @@ const EMPTY_DRAFT: BaseFormDraft = {
content: '',
categories: [],
location: '',
locationLat: undefined,
locationLng: undefined,
deadline: '',
};

Expand All @@ -19,6 +22,8 @@ interface BaseFormDraft {
content: string;
categories: PostSpotCategory[];
location: string;
locationLat?: number;
locationLng?: number;
deadline: string;
}

Expand Down Expand Up @@ -56,6 +61,8 @@ function getInitialDraft(): BaseFormDraft {
content: draft.content ?? '',
categories: draft.categories ?? [],
location: draft.location ?? '',
locationLat: draft.locationLat,
locationLng: draft.locationLng,
deadline: draft.deadline ?? '',
};
} catch {
Expand All @@ -81,7 +88,27 @@ export function usePostBaseForm(prefill?: PostBaseFormPrefill) {
);
const [photoPreviews, setPhotoPreviews] = useState<string[]>([]);
const draft = draftOverride ?? initialDraft;
const { spotName, title, content, categories, location, deadline } = draft;
const {
spotName,
title,
content,
categories,
location,
locationLat,
locationLng,
deadline,
} = draft;
const selectedLocation: SelectedPostLocation | null =
typeof locationLat === 'number' &&
Number.isFinite(locationLat) &&
typeof locationLng === 'number' &&
Number.isFinite(locationLng)
? {
lat: locationLat,
lng: locationLng,
label: location,
}
: null;

// prefill 이 시뮬→포스트 전환에서 변경되면 draft 를 한번 덮어씀 (localStorage 는 그대로).
// prefill 의 내용 변화를 단순 JSON 직렬화로 감지.
Expand Down Expand Up @@ -121,6 +148,7 @@ export function usePostBaseForm(prefill?: PostBaseFormPrefill) {
categories,
photoPreviews,
location,
selectedLocation,
deadline,
setSpotName: (value: string) =>
setDraftOverride((prev) => ({
Expand All @@ -147,6 +175,13 @@ export function usePostBaseForm(prefill?: PostBaseFormPrefill) {
...(prev ?? initialDraft),
location: value,
})),
setSelectedLocation: (value: SelectedPostLocation) =>
setDraftOverride((prev) => ({
...(prev ?? initialDraft),
location: value.label,
locationLat: value.lat,
locationLng: value.lng,
})),
setDeadline: (value: string) =>
setDraftOverride((prev) => ({
...(prev ?? initialDraft),
Expand Down
61 changes: 61 additions & 0 deletions src/features/post/ui/post-form/MapLocationPicker.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { MapLocationPicker } from './MapLocationPicker';

vi.mock('@/features/map/ui/MapV3Canvas', () => ({
MapV3Canvas: ({
onMapClickAction,
}: {
onMapClickAction?: (lat: number, lng: number) => void;
}) => (
<button
type="button"
onClick={() => onMapClickAction?.(37.2636, 127.0286)}
>
테스트 지도 좌표 선택
</button>
),
}));

afterEach(() => {
cleanup();
});

describe('MapLocationPicker', () => {
it('selects a required feed location as lat/lng from a map click', () => {
const handleChange = vi.fn();

render(<MapLocationPicker value={null} onChange={handleChange} />);

fireEvent.click(
screen.getByRole('button', { name: '테스트 지도 좌표 선택' }),
);

expect(handleChange).toHaveBeenCalledWith({
lat: 37.2636,
lng: 127.0286,
label: '지도 선택 위치 (37.26360, 127.02860)',
});
});

it('renders the selected coordinate summary', () => {
render(
<MapLocationPicker
value={{
lat: 37.2636,
lng: 127.0286,
label: '지도 선택 위치 (37.26360, 127.02860)',
}}
onChange={vi.fn()}
/>,
);

expect(
screen.getByText('지도 선택 위치 (37.26360, 127.02860)'),
).toBeTruthy();
expect(
screen.getByText(/lat\s+37\.263600\s+· lng\s+127\.028600/),
).toBeTruthy();
});
});
Loading
Loading