diff --git a/solution/frontend/src/api/generated/model/index.ts b/solution/frontend/src/api/generated/model/index.ts index 87864fa..d616f6f 100644 --- a/solution/frontend/src/api/generated/model/index.ts +++ b/solution/frontend/src/api/generated/model/index.ts @@ -56,6 +56,7 @@ export * from './listLinksParams'; export * from './listMediaParams'; export * from './listMySitesParams'; export * from './listPlacesParams'; +export * from './listShowcaseParams'; export * from './localContentData'; export * from './localContentDataBody'; export * from './localContentDataCollectedAt'; @@ -110,6 +111,10 @@ export * from './placeDataPhone'; export * from './placeDataRegionCode'; export * from './placeDataRoadAddress'; export * from './placeDataVerifiedAt'; +export * from './placeSearchItem'; +export * from './placeSearchItemCategory'; +export * from './placeSearchItemCategoryName'; +export * from './placeSearchItemRoadAddress'; export * from './placeStatus'; export * from './publishAction'; export * from './publishLogData'; @@ -231,6 +236,9 @@ export * from './resPlaceList'; export * from './resPlaceListMsg'; export * from './resPlaceMsg'; export * from './resPlacePlace'; +export * from './resPlaceSearch'; +export * from './resPlaceSearchMsg'; +export * from './resPlaceSearchSource'; export * from './resPublishLogs'; export * from './resPublishLogsMsg'; export * from './resRefreshToken'; @@ -239,6 +247,8 @@ export * from './resSeoAudit'; export * from './resSeoAuditMsg'; export * from './resSeoAuditSummary'; export * from './resSeoAuditVisibility'; +export * from './resShowcase'; +export * from './resShowcaseMsg'; export * from './resSite'; export * from './resSiteCurrentVersion'; export * from './resSiteMsg'; @@ -284,6 +294,10 @@ export * from './resVerifyCandidatesSource'; export * from './resWeather'; export * from './resWeatherMsg'; export * from './resWeatherWeather'; +export * from './searchPlacesPublicParams'; +export * from './showcaseItem'; +export * from './showcaseItemRegion'; +export * from './showcaseItemThumbnailUrl'; export * from './siteData'; export * from './siteDataCurrentVersionId'; export * from './siteDataDomain'; @@ -291,6 +305,7 @@ export * from './siteDataPublishedAt'; export * from './siteDataTemplateId'; export * from './siteDataTheme'; export * from './siteDataThemeAnyOf'; +export * from './siteDataThumbnailUrl'; export * from './siteStatus'; export * from './siteVersionData'; export * from './siteVersionDataBuildError'; diff --git a/solution/frontend/src/api/generated/model/listShowcaseParams.ts b/solution/frontend/src/api/generated/model/listShowcaseParams.ts new file mode 100644 index 0000000..303f4de --- /dev/null +++ b/solution/frontend/src/api/generated/model/listShowcaseParams.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v7.21.0 ๐Ÿบ + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ + +export type ListShowcaseParams = { +/** + * ๊ฐ€์ ธ์˜ฌ ๊ฐœ์ˆ˜ + * @minimum 1 + * @maximum 48 + */ +limit?: number; +}; diff --git a/solution/frontend/src/api/generated/model/placeSearchItem.ts b/solution/frontend/src/api/generated/model/placeSearchItem.ts new file mode 100644 index 0000000..1aacf27 --- /dev/null +++ b/solution/frontend/src/api/generated/model/placeSearchItem.ts @@ -0,0 +1,24 @@ +/** + * Generated by orval v7.21.0 ๐Ÿบ + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ +import type { PlaceSearchItemRoadAddress } from './placeSearchItemRoadAddress'; +import type { PlaceSearchItemCategoryName } from './placeSearchItemCategoryName'; +import type { PlaceSearchItemCategory } from './placeSearchItemCategory'; + +/** + * ๊ณต๊ฐœ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ 1๊ฑด. + +โ˜… ์™ธ๋ถ€ ์žฅ์†Œ DB ๊ฐ€ ๊ณต๊ฐœ์ ์œผ๋กœ ์ฃผ๋Š” ๊ฐ’๋งŒ ๋‹ด๋Š”๋‹ค. ์šฐ๋ฆฌ DB ๊ฐ’(place_idยทcompany_idยท์†Œ์œ ์ž)์€ + ํ•˜๋‚˜๋„ ๋‚˜๊ฐ€์ง€ ์•Š๋Š”๋‹ค โ€” ๋กœ๊ทธ์ธ ์—†์ด ์—ด๋ ค ์žˆ๋Š” ์‘๋‹ต์ด๋ผ ์—ฌ๊ธฐ์— ์šฐ๋ฆฌ ๊ฒƒ์„ ์‹ค์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์ƒŒ๋‹ค. +โ˜… ์ขŒํ‘œยท์ „ํ™”๋ฒˆํ˜ธ๋„ ๋บ๋‹ค. ๋žœ๋”ฉ์ด ํ•˜๋Š” ์ผ์€ '์–ด๋А ๊ฐ€๊ฒŒ์ธ์ง€ ๊ณ ๋ฅด๊ฒŒ ํ•˜๋Š” ๊ฒƒ'๋ฟ์ด๊ณ , + ํ™•์ •๊ณผ ์ˆ˜์ง‘์€ ๋กœ๊ทธ์ธ ๋’ค ๊ธฐ์กด ๊ฒฝ๋กœ(POST /place โ†’ verify)๊ฐ€ ๊ทธ๋Œ€๋กœ ํ•œ๋‹ค. + */ +export interface PlaceSearchItem { + name?: string; + road_address?: PlaceSearchItemRoadAddress; + category_name?: PlaceSearchItemCategoryName; + category?: PlaceSearchItemCategory; +} diff --git a/solution/frontend/src/api/generated/model/placeSearchItemCategory.ts b/solution/frontend/src/api/generated/model/placeSearchItemCategory.ts new file mode 100644 index 0000000..4c4e071 --- /dev/null +++ b/solution/frontend/src/api/generated/model/placeSearchItemCategory.ts @@ -0,0 +1,9 @@ +/** + * Generated by orval v7.21.0 ๐Ÿบ + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ +import type { PlaceCategory } from './placeCategory'; + +export type PlaceSearchItemCategory = PlaceCategory | null; diff --git a/solution/frontend/src/api/generated/model/placeSearchItemCategoryName.ts b/solution/frontend/src/api/generated/model/placeSearchItemCategoryName.ts new file mode 100644 index 0000000..feb298e --- /dev/null +++ b/solution/frontend/src/api/generated/model/placeSearchItemCategoryName.ts @@ -0,0 +1,8 @@ +/** + * Generated by orval v7.21.0 ๐Ÿบ + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ + +export type PlaceSearchItemCategoryName = string | null; diff --git a/solution/frontend/src/api/generated/model/placeSearchItemRoadAddress.ts b/solution/frontend/src/api/generated/model/placeSearchItemRoadAddress.ts new file mode 100644 index 0000000..e156c8f --- /dev/null +++ b/solution/frontend/src/api/generated/model/placeSearchItemRoadAddress.ts @@ -0,0 +1,8 @@ +/** + * Generated by orval v7.21.0 ๐Ÿบ + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ + +export type PlaceSearchItemRoadAddress = string | null; diff --git a/solution/frontend/src/api/generated/model/resPlaceSearch.ts b/solution/frontend/src/api/generated/model/resPlaceSearch.ts new file mode 100644 index 0000000..beb75cc --- /dev/null +++ b/solution/frontend/src/api/generated/model/resPlaceSearch.ts @@ -0,0 +1,23 @@ +/** + * Generated by orval v7.21.0 ๐Ÿบ + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ +import type { ErrorInfo } from './errorInfo'; +import type { ResPlaceSearchMsg } from './resPlaceSearchMsg'; +import type { ResPlaceSearchSource } from './resPlaceSearchSource'; +import type { PlaceSearchItem } from './placeSearchItem'; + +/** + * ์ƒํ˜ธ๋ช… ๊ณต๊ฐœ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ. + +โ˜… ์ธ์ฆ์ด ์—†๋‹ค. ๋žœ๋”ฉ ์ฒซ ํ™”๋ฉด์—์„œ ์ƒํ˜ธ๋ช…์„ ์น˜๋ฉด ๋ฐ”๋กœ ๋ถ€๋ฅธ๋‹ค โ€” + ๋งŒ๋“ค์–ด ๋ณด๊ธฐ๋„ ์ „์— ๋กœ๊ทธ์ธ์„ ์š”๊ตฌํ•˜์ง€ ์•Š๊ธฐ๋กœ ํ•œ ๊ฒฐ์ •(๋กœ๊ทธ์ธ ๊ด€๋ฌธ์€ ์—๋””ํ„ฐ ์ง„์ž… ํ•˜๋‚˜)์˜ ์—ฐ์žฅ์ด๋‹ค. + */ +export interface ResPlaceSearch { + result?: ErrorInfo; + msg?: ResPlaceSearchMsg; + source?: ResPlaceSearchSource; + items?: PlaceSearchItem[]; +} diff --git a/solution/frontend/src/api/generated/model/resPlaceSearchMsg.ts b/solution/frontend/src/api/generated/model/resPlaceSearchMsg.ts new file mode 100644 index 0000000..ab2bd13 --- /dev/null +++ b/solution/frontend/src/api/generated/model/resPlaceSearchMsg.ts @@ -0,0 +1,8 @@ +/** + * Generated by orval v7.21.0 ๐Ÿบ + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ + +export type ResPlaceSearchMsg = string | null; diff --git a/solution/frontend/src/api/generated/model/resPlaceSearchSource.ts b/solution/frontend/src/api/generated/model/resPlaceSearchSource.ts new file mode 100644 index 0000000..c1ea375 --- /dev/null +++ b/solution/frontend/src/api/generated/model/resPlaceSearchSource.ts @@ -0,0 +1,9 @@ +/** + * Generated by orval v7.21.0 ๐Ÿบ + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ +import type { ExternalPlaceSource } from './externalPlaceSource'; + +export type ResPlaceSearchSource = ExternalPlaceSource | null; diff --git a/solution/frontend/src/api/generated/model/resShowcase.ts b/solution/frontend/src/api/generated/model/resShowcase.ts new file mode 100644 index 0000000..7c01a7f --- /dev/null +++ b/solution/frontend/src/api/generated/model/resShowcase.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v7.21.0 ๐Ÿบ + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ +import type { ErrorInfo } from './errorInfo'; +import type { ResShowcaseMsg } from './resShowcaseMsg'; +import type { ShowcaseItem } from './showcaseItem'; + +export interface ResShowcase { + result?: ErrorInfo; + msg?: ResShowcaseMsg; + items?: ShowcaseItem[]; +} diff --git a/solution/frontend/src/api/generated/model/resShowcaseMsg.ts b/solution/frontend/src/api/generated/model/resShowcaseMsg.ts new file mode 100644 index 0000000..b71fd15 --- /dev/null +++ b/solution/frontend/src/api/generated/model/resShowcaseMsg.ts @@ -0,0 +1,8 @@ +/** + * Generated by orval v7.21.0 ๐Ÿบ + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ + +export type ResShowcaseMsg = string | null; diff --git a/solution/frontend/src/api/generated/model/searchPlacesPublicParams.ts b/solution/frontend/src/api/generated/model/searchPlacesPublicParams.ts new file mode 100644 index 0000000..8d965fd --- /dev/null +++ b/solution/frontend/src/api/generated/model/searchPlacesPublicParams.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v7.21.0 ๐Ÿบ + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ + +export type SearchPlacesPublicParams = { +/** + * ์ƒํ˜ธ๋ช… + * @minLength 2 + * @maxLength 100 + */ +q: string; +}; diff --git a/solution/frontend/src/api/generated/model/showcaseItem.ts b/solution/frontend/src/api/generated/model/showcaseItem.ts new file mode 100644 index 0000000..5d3d0a7 --- /dev/null +++ b/solution/frontend/src/api/generated/model/showcaseItem.ts @@ -0,0 +1,24 @@ +/** + * Generated by orval v7.21.0 ๐Ÿบ + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ +import type { PlaceCategory } from './placeCategory'; +import type { ShowcaseItemRegion } from './showcaseItemRegion'; +import type { ShowcaseItemThumbnailUrl } from './showcaseItemThumbnailUrl'; + +/** + * ๋žœ๋”ฉ ์‡ผ์ผ€์ด์Šค ์นด๋“œ ํ•œ ์žฅ. **๋กœ๊ทธ์ธ ์—†์ด ๋‚˜๊ฐ€๋Š” ๊ฐ’์ด๋‹ค.** + +โ˜… ์—ฌ๊ธฐ ์žˆ๋Š” ๊ฒƒ์€ ์ „๋ถ€ ์ด๋ฏธ ๋ฐœํ–‰๋œ ํŽ˜์ด์ง€์— ์ ํ˜€ ์žˆ๋Š” ๊ฒƒ๋ฟ์ด๋‹ค. + place_idยทcompany_idยท์ „ํ™”๋ฒˆํ˜ธยท์ƒ์„ธ ์ฃผ์†Œ๋Š” ์ ˆ๋Œ€ ์‹ฃ์ง€ ์•Š๋Š”๋‹ค โ€” ์‚ฌ์ดํŠธ ํ•œ ๊ณณ์„ ์—ฌ๋Š” ๊ฒƒ๊ณผ + ๋ฐœํ–‰ ์—…์†Œ ๋ช…๋‹จ์„ ํ†ต์งธ๋กœ ๊ธ๋Š” ๊ฒƒ์€ ๋‹ค๋ฅธ ์ผ์ด๋‹ค. ์ง€์—ญ๋„ ์‹œยท๊ตฐยท๊ตฌ๊นŒ์ง€๋งŒ ์ค€๋‹ค. + */ +export interface ShowcaseItem { + name: string; + category: PlaceCategory; + region?: ShowcaseItemRegion; + url: string; + thumbnail_url?: ShowcaseItemThumbnailUrl; +} diff --git a/solution/frontend/src/api/generated/model/showcaseItemRegion.ts b/solution/frontend/src/api/generated/model/showcaseItemRegion.ts new file mode 100644 index 0000000..d5090ae --- /dev/null +++ b/solution/frontend/src/api/generated/model/showcaseItemRegion.ts @@ -0,0 +1,8 @@ +/** + * Generated by orval v7.21.0 ๐Ÿบ + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ + +export type ShowcaseItemRegion = string | null; diff --git a/solution/frontend/src/api/generated/model/showcaseItemThumbnailUrl.ts b/solution/frontend/src/api/generated/model/showcaseItemThumbnailUrl.ts new file mode 100644 index 0000000..ffdcc72 --- /dev/null +++ b/solution/frontend/src/api/generated/model/showcaseItemThumbnailUrl.ts @@ -0,0 +1,8 @@ +/** + * Generated by orval v7.21.0 ๐Ÿบ + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ + +export type ShowcaseItemThumbnailUrl = string | null; diff --git a/solution/frontend/src/api/generated/model/siteData.ts b/solution/frontend/src/api/generated/model/siteData.ts index f3a3279..f48299d 100644 --- a/solution/frontend/src/api/generated/model/siteData.ts +++ b/solution/frontend/src/api/generated/model/siteData.ts @@ -10,6 +10,7 @@ import type { SiteDataTemplateId } from './siteDataTemplateId'; import type { SiteDataTheme } from './siteDataTheme'; import type { SiteDataCurrentVersionId } from './siteDataCurrentVersionId'; import type { SiteDataPublishedAt } from './siteDataPublishedAt'; +import type { SiteDataThumbnailUrl } from './siteDataThumbnailUrl'; export interface SiteData { site_id: string; @@ -20,4 +21,5 @@ export interface SiteData { theme?: SiteDataTheme; current_version_id?: SiteDataCurrentVersionId; published_at?: SiteDataPublishedAt; + thumbnail_url?: SiteDataThumbnailUrl; } diff --git a/solution/frontend/src/api/generated/model/siteDataThumbnailUrl.ts b/solution/frontend/src/api/generated/model/siteDataThumbnailUrl.ts new file mode 100644 index 0000000..2a3cb5c --- /dev/null +++ b/solution/frontend/src/api/generated/model/siteDataThumbnailUrl.ts @@ -0,0 +1,8 @@ +/** + * Generated by orval v7.21.0 ๐Ÿบ + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ + +export type SiteDataThumbnailUrl = string | null; diff --git a/solution/frontend/src/api/generated/place/place.ts b/solution/frontend/src/api/generated/place/place.ts index ee2d808..b0e67d7 100644 --- a/solution/frontend/src/api/generated/place/place.ts +++ b/solution/frontend/src/api/generated/place/place.ts @@ -41,12 +41,14 @@ import type { ResLinkList, ResPlace, ResPlaceList, + ResPlaceSearch, ResStartCollect, ResStartCopy, ResStartVision, ResUnit, ResUnitList, ResVerifyCandidates, + SearchPlacesPublicParams, VerifyCandidatesParams } from '.././model'; @@ -150,6 +152,100 @@ export function useListPlaces>, TE +/** + * ์ƒํ˜ธ๋ช…์œผ๋กœ ์™ธ๋ถ€ ์žฅ์†Œ DB(์นด์นด์˜ค โ†’ ์—†์œผ๋ฉด ๋„ค์ด๋ฒ„)๋ฅผ ์ฐพ์•„ ํ›„๋ณด๋ฅผ ๊ทธ๋Œ€๋กœ ๋Œ๋ ค์ค€๋‹ค. โ˜… ์ธ์ฆ์ด ์—†๋‹ค โ€” ๋žœ๋”ฉ ์ฒซ ํ™”๋ฉด์ด ๋ถ€๋ฅธ๋‹ค. ์‚ฌ์—…์žฅ์„ ๋งŒ๋“ค์ง€๋„, ์šฐ๋ฆฌ DB ๋ฅผ ์ฝ์ง€๋„ ์•Š๋Š”๋‹ค. โ˜… ์‘๋‹ต์˜ category ๋Š” ์™ธ๋ถ€ ๋ถ„๋ฅ˜์—์„œ **์ถ”์ •ํ•œ ๊ธฐ๋ณธ๊ฐ’**์ด๋‹ค. None ์ด๋ฉด ๋ชป ์ •ํ•œ ๊ฒƒ์ด๊ณ , ๊ฐ’์ด ์žˆ์–ด๋„ ํ™•์ •์ด ์•„๋‹ˆ๋‹ค โ€” ํ™”๋ฉด์€ ์–ธ์ œ๋‚˜ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๊ฒŒ ๋‘”๋‹ค. + * @summary ์ƒํ˜ธ๋ช… ๊ณต๊ฐœ ๊ฒ€์ƒ‰ + */ +export const searchPlacesPublic = ( + params: SearchPlacesPublicParams, + options?: SecondParameter,signal?: AbortSignal +) => { + + + return customFetch( + {url: `/v1/place/search`, method: 'GET', + params, signal + }, + options); + } + + + + +export const getSearchPlacesPublicQueryKey = (params?: SearchPlacesPublicParams,) => { + return [ + `/v1/place/search`, ...(params ? [params]: []) + ] as const; + } + + +export const getSearchPlacesPublicQueryOptions = >, TError = void | HTTPValidationError>(params: SearchPlacesPublicParams, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} +) => { + +const {query: queryOptions, request: requestOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getSearchPlacesPublicQueryKey(params); + + + + const queryFn: QueryFunction>> = ({ signal }) => searchPlacesPublic(params, requestOptions, signal); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type SearchPlacesPublicQueryResult = NonNullable>> +export type SearchPlacesPublicQueryError = void | HTTPValidationError + + +export function useSearchPlacesPublic>, TError = void | HTTPValidationError>( + params: SearchPlacesPublicParams, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, request?: SecondParameter} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useSearchPlacesPublic>, TError = void | HTTPValidationError>( + params: SearchPlacesPublicParams, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, request?: SecondParameter} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useSearchPlacesPublic>, TError = void | HTTPValidationError>( + params: SearchPlacesPublicParams, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary ์ƒํ˜ธ๋ช… ๊ณต๊ฐœ ๊ฒ€์ƒ‰ + */ + +export function useSearchPlacesPublic>, TError = void | HTTPValidationError>( + params: SearchPlacesPublicParams, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getSearchPlacesPublicQueryOptions(params,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + /** * ์ƒํ˜ธ๋ช… ํ•˜๋‚˜๋กœ ์‹œ์ž‘ํ•œ๋‹ค. ์ฃผ์†Œยท์ขŒํ‘œ๋Š” ๋™์ผ ์—…์†Œ ๊ฒ€์ฆ(verify)์ด ์ฑ„์šด๋‹ค. * @summary ์‚ฌ์—…์žฅ ๋“ฑ๋ก diff --git a/solution/frontend/src/api/generated/showcase/showcase.ts b/solution/frontend/src/api/generated/showcase/showcase.ts new file mode 100644 index 0000000..2ed057a --- /dev/null +++ b/solution/frontend/src/api/generated/showcase/showcase.ts @@ -0,0 +1,128 @@ +/** + * Generated by orval v7.21.0 ๐Ÿบ + * Do not edit manually. + * Web4Ai API + * OpenAPI spec version: 0.1.0 + */ +import { + useQuery +} from '@tanstack/react-query'; +import type { + DataTag, + DefinedInitialDataOptions, + DefinedUseQueryResult, + QueryClient, + QueryFunction, + QueryKey, + UndefinedInitialDataOptions, + UseQueryOptions, + UseQueryResult +} from '@tanstack/react-query'; + +import type { + HTTPValidationError, + ListShowcaseParams, + ResShowcase +} from '.././model'; + +import { customFetch } from '../../mutator/custom-fetch'; + + +type SecondParameter unknown> = Parameters[1]; + + + +/** + * ์‹ค์ œ๋กœ ๋ฐœํ–‰๋œ ์‚ฌ์ดํŠธ๋ฅผ ์ตœ์‹ ์ˆœ์œผ๋กœ ์ค€๋‹ค. ์ƒํ˜ธ๋ช…ยท์—…์ข…ยท์ง€์—ญ(์‹œยท๊ตฐยท๊ตฌ๊นŒ์ง€)ยท๋ฐœํ–‰ ์ฃผ์†Œยท์ธ๋„ค์ผ๋ฟ์ด๋‹ค โ€” ๋กœ๊ทธ์ธ ์—†์ด ๋‚˜๊ฐ€๋ฏ€๋กœ ์‚ฌ์—…์žฅ ์‹๋ณ„์žยท์ „ํ™”๋ฒˆํ˜ธยท์ƒ์„ธ ์ฃผ์†Œ๋Š” ์‹ฃ์ง€ ์•Š๋Š”๋‹ค. ์ธ๋„ค์ผ์€ ๊ทธ ์‚ฌ์ดํŠธ์˜ ๋Œ€ํ‘œ ์‚ฌ์ง„์ด๊ณ , ๋งŒ๋“ค์ง€ ๋ชปํ•œ ์‚ฌ์ดํŠธ๋Š” ํ‚ค๊ฐ€ ์—†๋‹ค. + * @summary ๋ฐœํ–‰ ์‚ฌ์ดํŠธ ์‡ผ์ผ€์ด์Šค(๊ณต๊ฐœ) + */ +export const listShowcase = ( + params?: ListShowcaseParams, + options?: SecondParameter,signal?: AbortSignal +) => { + + + return customFetch( + {url: `/v1/showcase`, method: 'GET', + params, signal + }, + options); + } + + + + +export const getListShowcaseQueryKey = (params?: ListShowcaseParams,) => { + return [ + `/v1/showcase`, ...(params ? [params]: []) + ] as const; + } + + +export const getListShowcaseQueryOptions = >, TError = void | HTTPValidationError>(params?: ListShowcaseParams, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} +) => { + +const {query: queryOptions, request: requestOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getListShowcaseQueryKey(params); + + + + const queryFn: QueryFunction>> = ({ signal }) => listShowcase(params, requestOptions, signal); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type ListShowcaseQueryResult = NonNullable>> +export type ListShowcaseQueryError = void | HTTPValidationError + + +export function useListShowcase>, TError = void | HTTPValidationError>( + params: undefined | ListShowcaseParams, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, request?: SecondParameter} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useListShowcase>, TError = void | HTTPValidationError>( + params?: ListShowcaseParams, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, request?: SecondParameter} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useListShowcase>, TError = void | HTTPValidationError>( + params?: ListShowcaseParams, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary ๋ฐœํ–‰ ์‚ฌ์ดํŠธ ์‡ผ์ผ€์ด์Šค(๊ณต๊ฐœ) + */ + +export function useListShowcase>, TError = void | HTTPValidationError>( + params?: ListShowcaseParams, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getListShowcaseQueryOptions(params,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + diff --git a/solution/frontend/src/features/builder/EditorHeader.tsx b/solution/frontend/src/features/builder/EditorHeader.tsx index a84c1fd..16f9a82 100644 --- a/solution/frontend/src/features/builder/EditorHeader.tsx +++ b/solution/frontend/src/features/builder/EditorHeader.tsx @@ -13,7 +13,6 @@ export function EditorHeader() { const isPreviewMode = useBuilderStore((s) => s.isPreviewMode); const togglePreview = useBuilderStore((s) => s.togglePreview); const openPublishModal = useBuilderStore((s) => s.openPublishModal); - const reset = useBuilderStore((s) => s.reset); const placeId = useBuilderStore((s) => s.placeId); const unverifiedCount = useUnverifiedFields().length; @@ -37,20 +36,19 @@ export function EditorHeader() { ์—ฌ๊ธฐ ๋งํฌ๋ฅผ ๋‚จ๊ธฐ๋ฉด ์‚ฌ์žฅ๋‹˜ ์•ฑ์—์„œ 404 ์ด๊ณ , ๋‚ด๋ถ€ ๊ฒฝ๋กœ ์ด๋ฆ„์ด ์‚ฌ์žฅ๋‹˜ ๋ฒˆ๋“ค์—๋„ ๋‚จ๋Š”๋‹ค. reset() ์„ ๋ถ€๋ฅด์ง€ ์•Š๋Š” ์ด์œ ๋Š” ๊ทธ๋Œ€๋กœ๋‹ค: ํŽธ์ง‘ํ•˜๋˜ ๊ฐ€๊ฒŒ๋ฅผ ๋– ๋‚˜ ๋ฐ๋ชจ ์œ„์ €๋“œ๋กœ ๋–จ์–ด์ง€๋ฉด ์‚ฌ์žฅ๋‹˜ ๋ˆˆ์—๋Š” ์ž๊ธฐ ๊ฐ€๊ฒŒ๊ฐ€ ์‚ฌ๋ผ์ง„ ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค. - (์‚ฌ์žฅ๋‹˜์šฉ "๋‚ด ์‚ฌ์ดํŠธ ๊ด€๋ฆฌ"๊ฐ€ ์ƒ๊ธฐ๋ฉด ๊ทธ๋•Œ ๊ทธ๋ฆฌ๋กœ ์ž‡๋Š”๋‹ค โ€” ARCHITECTURE.md 4์ ˆ) */} + (์‚ฌ์žฅ๋‹˜์šฉ "๋‚ด ์‚ฌ์ดํŠธ ๊ด€๋ฆฌ"๊ฐ€ ์ƒ๊ธฐ๋ฉด ๊ทธ๋•Œ ๊ทธ๋ฆฌ๋กœ ์ž‡๋Š”๋‹ค โ€” ARCHITECTURE.md 4์ ˆ) + โ˜… ๋ฐ๋ชจ์—์„œ ๋Œ์•„๊ฐ€๋Š” ๊ธธ์€ `?new=1` ์ด๋‹ค โ€” ์Šคํ† ์–ด๋ฅผ ์ง์ ‘ ๋น„์šฐ๋ฉด ํ™”๋ฉด๋งŒ ์œ„์ €๋“œ๋กœ ๋ฐ”๋€Œ๊ณ  + ์ฃผ์†Œ๋Š” ๊ทธ๋Œ€๋กœ๋ผ, ๋’ค๋กœ๊ฐ€๊ธฐ๊ฐ€ ํŽธ์ง‘๊ธฐ๋กœ ๋Œ์•„์˜ค์ง€ ์•Š๋Š”๋‹ค(๋‹จ๊ณ„๋Š” ์ฃผ์†Œ์ฐฝ์ด ์†Œ์œ ํ•œ๋‹ค). */} {placeId ? ( - + // ํŽธ์ง‘ ์ค‘์ด์–ด๋„ ๋กœ๊ณ ๋Š” ํ™ˆ์œผ๋กœ ๊ฐ„๋‹ค โ€” ๋ˆŒ๋ฆฌ์ง€ ์•Š๋Š” ๋กœ๊ณ ๋Š” ๊ณ ์žฅ์œผ๋กœ ์ฝํžŒ๋‹ค. + // ์ž…๋ ฅ๊ฐ’์€ ์„œ๋ฒ„์— ์ €์žฅ๋˜๋ฏ€๋กœ ๋‚˜๊ฐ”๋‹ค ๋“ค์–ด์™€๋„ ๊ทธ๋Œ€๋กœ๋‹ค. + - + ) : ( - + )} diff --git a/solution/frontend/src/features/onboarding/Step1Industry.tsx b/solution/frontend/src/features/onboarding/Step1Industry.tsx index ee866d9..e42eeaf 100644 --- a/solution/frontend/src/features/onboarding/Step1Industry.tsx +++ b/solution/frontend/src/features/onboarding/Step1Industry.tsx @@ -1,18 +1,54 @@ +import {useState} from 'react'; import {Check} from 'lucide-react'; import type {IndustryType} from '@o2o/shared'; import {INDUSTRY_CONFIGS} from '@/data/industryData'; import {cn} from '@/lib/utils'; import {useBuilderStore} from '@/stores/builder'; import {INDUSTRY_ICONS} from './industryIcons'; +import {useWizardStep} from './wizardUrl'; import {WizardFooter} from './WizardFooter'; import {WizardSteps} from './WizardSteps'; const INDUSTRY_ORDER: IndustryType[] = ['stay', 'cafe', 'restaurant', 'clinic']; +/** + * ์—…์ข… ์ง์ ‘ ๊ณ ๋ฅด๊ธฐ โ€” `?step=industry`. + * + * โ˜… ์ด ํ™”๋ฉด์€ ๋” ์ด์ƒ ์œ„์ €๋“œ์˜ ์ฒซ ํ™”๋ฉด์ด ์•„๋‹ˆ๋‹ค. ์—…์ข…์€ ์ƒํ˜ธ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์˜ ๋ถ„๋ฅ˜๊ฐ€ ์ •ํ•˜๊ณ  + * (services/place_category.py), ์—ฌ๊ธฐ๋Š” **๋ชป ์ •ํ–ˆ์„ ๋•Œ์™€ ๋ฐ”๊ฟ€ ๋•Œ๋งŒ** ๋“ค๋ฅธ๋‹ค. + * ์ง„ํ–‰ ํ‘œ์‹œ์—์„œ 1๋ฒˆ์„ ์œ ์ง€ํ•˜๋Š” ์ด์œ ๋„ ๊ฐ™๋‹ค โ€” ์ด๊ฑด '๋‚ด ๊ฐ€๊ฒŒ ํ™•์ธ' ์•ˆ์˜ ๊ฐˆ๋ž˜๋‹ค. + * + * โ˜… ๋ชป ์ •ํ•ด์„œ ๋“ค์–ด์˜จ ๊ฒฝ์šฐ์—๋Š” ์•„๋ฌด ์นด๋“œ๋„ ์„ ํƒ๋œ ๊ฒƒ์œผ๋กœ ๋ณด์ด์ง€ ์•Š๊ฒŒ ํ•œ๋‹ค. ์Šคํ† ์–ด์—๋Š” ์–ธ์ œ๋‚˜ + * ๊ธฐ๋ณธ ์—…์ข…์ด ๋“ค์–ด ์žˆ์–ด์„œ, ๊ทธ๊ฑธ ๊ทธ๋Œ€๋กœ ์น ํ•˜๋ฉด ์‚ฌ์žฅ๋‹˜์€ ์ž๊ธฐ๊ฐ€ ๊ณ ๋ฅด์ง€๋„ ์•Š์€ ์—…์ข…์ด + * ์ด๋ฏธ ์ •ํ•ด์ง„ ๊ฒƒ์œผ๋กœ ์ฝ๋Š”๋‹ค. + */ export function Step1Industry() { const industry = useBuilderStore((s) => s.industry); + const pendingPick = useBuilderStore((s) => s.pendingPick); const selectIndustry = useBuilderStore((s) => s.selectIndustry); - const goToStep = useBuilderStore((s) => s.goToStep); + const setPendingPick = useBuilderStore((s) => s.setPendingPick); + const [, goToStep] = useWizardStep(); + + /** ๊ฒ€์ƒ‰์ด ์—…์ข…์„ ๋ชป ์ •ํ•ด ๋„˜์–ด์˜จ ๊ฒฝ์šฐ โ€” ๊ณ ๋ฅด๊ธฐ ์ „์—๋Š” ์ง„ํ–‰์‹œํ‚ค์ง€ ์•Š๋Š”๋‹ค. */ + const mustChoose = Boolean(pendingPick && !pendingPick.industry); + const [touched, setTouched] = useState(false); + const showSelection = touched || !mustChoose; + + const pick = (id: IndustryType) => { + setTouched(true); + selectIndustry(id); + }; + + /** + * ๊ณ ๋ฅธ ์—…์ข…์„ ๋“ค๊ณ  ๊ฒ€์ƒ‰ ํ™”๋ฉด์œผ๋กœ ๋Œ์•„๊ฐ„๋‹ค. + * + * โ˜… ํ›„๋ณด๋ฅผ ๋“ค๊ณ  ์™”์œผ๋ฉด ๊ทธ ํ›„๋ณด์— ์—…์ข…์„ ์ฐ์–ด ๋Œ๋ ค๋ณด๋‚ธ๋‹ค โ€” ํ™•์ •(์‚ฌ์—…์žฅ ์ƒ์„ฑ)์€ ๊ฒ€์ƒ‰ ํ™”๋ฉด + * ํ•œ ๊ณณ์—์„œ๋งŒ ํ•œ๋‹ค. ๋‘ ํ™”๋ฉด์ด ๊ฐ์ž ํ™•์ •ํ•˜๋ฉด ๊ฐ™์€ ๊ฐ€๊ฒŒ๊ฐ€ ๋‘ ๋ฒˆ ๋งŒ๋“ค์–ด์ง€๋Š” ๊ธธ์ด ์ƒ๊ธด๋‹ค. + */ + const goBack = () => { + if (pendingPick) setPendingPick({...pendingPick, industry}); + goToStep('search'); + }; return (
@@ -22,10 +58,12 @@ export function Step1Industry() {

- ์–ด๋–ค ์—…์ข…์˜ ํ™ˆํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค๊นŒ์š”? + {mustChoose ? '์ด ๊ฐ€๊ฒŒ๋Š” ์–ด๋–ค ์—…์ข…์ธ๊ฐ€์š”?' : '์—…์ข…์„ ๋ฐ”๊ฟ€๊นŒ์š”?'}

- ์—…์ข…์„ ๊ณ ๋ฅด๋ฉด ๊ทธ ์—…์ข…์— ๋งž๋Š” ํ•ญ๋ชฉ๋งŒ ์ˆ˜์ง‘ํ•˜๊ณ , ๊ทธ ํ•ญ๋ชฉ๋งŒ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + {mustChoose + ? '์ง€๋„ ๋ถ„๋ฅ˜๋งŒ์œผ๋กœ๋Š” ์—…์ข…์„ ์ •ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์—…์ข…์— ๋งž๋Š” ํ•ญ๋ชฉ๋งŒ ์ˆ˜์ง‘ํ•˜๊ณ , ๊ทธ ํ•ญ๋ชฉ๋งŒ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.' + : '์—…์ข…์„ ๊ณ ๋ฅด๋ฉด ๊ทธ ์—…์ข…์— ๋งž๋Š” ํ•ญ๋ชฉ๋งŒ ์ˆ˜์ง‘ํ•˜๊ณ , ๊ทธ ํ•ญ๋ชฉ๋งŒ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.'}

@@ -33,13 +71,13 @@ export function Step1Industry() { {INDUSTRY_ORDER.map((id) => { const config = INDUSTRY_CONFIGS[id]; const Icon = INDUSTRY_ICONS[id]; - const isSelected = industry === id; + const isSelected = showSelection && industry === id; return (
); diff --git a/solution/frontend/src/features/onboarding/Step2PlaceSearch.tsx b/solution/frontend/src/features/onboarding/Step2PlaceSearch.tsx index 00f42d4..16b1947 100644 --- a/solution/frontend/src/features/onboarding/Step2PlaceSearch.tsx +++ b/solution/frontend/src/features/onboarding/Step2PlaceSearch.tsx @@ -1,19 +1,23 @@ -import {useState} from 'react'; -import {useSearchParams} from 'react-router'; -import {ArrowRight, Check, MapPin, Phone, Search, TriangleAlert} from 'lucide-react'; -import {getAccessToken, type PlaceCandidate} from '@/api'; +import {useEffect, useRef, useState} from 'react'; +import {ArrowRight, Check, MapPin, Search, TriangleAlert} from 'lucide-react'; +import type {IndustryType} from '@o2o/shared'; +import type {PlaceSearchItem} from '@/api'; import {Badge} from '@/components/ui/badge'; import {Button} from '@/components/ui/button'; import {Input} from '@/components/ui/input'; +import {INDUSTRY_CONFIGS} from '@/data/industryData'; +import {CATEGORY_TO_INDUSTRY} from '@/features/builder/placeAdapter'; import {cn} from '@/lib/utils'; +import type {PendingPick} from '@/stores/builder'; import {useBuilderStore} from '@/stores/builder'; import {PlaceUrlBox} from './PlaceUrlBox'; import {usePlaceSearch} from './usePlaceSearch'; +import {useWizardStep} from './wizardUrl'; import {WizardFooter} from './WizardFooter'; import {WizardStage, WizardWaiting} from './WizardStage'; const STAGE_TITLE: Record = { - input: '์ƒํ˜ธ๋ช…์„ ๊ฒ€์ƒ‰ํ•ด ๋‚ด ๊ฐ€๊ฒŒ๋ฅผ ์ฐพ์•„์ฃผ์„ธ์š”', + input: '๊ฐ€๊ฒŒ ์ด๋ฆ„์„ ์•Œ๋ ค์ฃผ์„ธ์š”', searching: '๋‚ด ๊ฐ€๊ฒŒ๋ฅผ ์ฐพ๋Š” ์ค‘์ž…๋‹ˆ๋‹ค', picking: '์ด ์ค‘์— ์‚ฌ์žฅ๋‹˜ ๊ฐ€๊ฒŒ๊ฐ€ ์žˆ๋‚˜์š”?', unavailable: '์ง€๋„ ๊ฒ€์ƒ‰์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค', @@ -21,22 +25,20 @@ const STAGE_TITLE: Record = { }; const STAGE_DESCRIPTION: Record = { - input: '์ง€๋„ยทํ”Œ๋ ˆ์ด์Šค์— ๋“ฑ๋ก๋œ ๊ฐ€๊ฒŒ ์ค‘์—์„œ ์‚ฌ์žฅ๋‹˜์ด ์ง์ ‘ ๊ณ ๋ฅธ ํ•œ ๊ณณ๋งŒ ๊ธฐ์ค€์ด ๋ฉ๋‹ˆ๋‹ค.', + input: '์—…์ข…์€ ์•ˆ ๊ณ ๋ฅด์…”๋„ ๋ฉ๋‹ˆ๋‹ค โ€” ์ฐพ์€ ๊ฐ€๊ฒŒ์˜ ๋ถ„๋ฅ˜์—์„œ ์ž๋™์œผ๋กœ ์ •ํ•ด์ง‘๋‹ˆ๋‹ค.', searching: '', - picking: '๊ณ ๋ฅธ ๊ฐ€๊ฒŒ์˜ ์ƒํ˜ธยท์ฃผ์†Œยท์ „ํ™”๊ฐ€ ์ด ์‚ฌ์ดํŠธ์˜ ๊ธฐ์ค€ ์ •๋ณด๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.', + picking: '๊ณ ๋ฅธ ๊ฐ€๊ฒŒ์˜ ์ƒํ˜ธยท์ฃผ์†Œ๊ฐ€ ์ด ์‚ฌ์ดํŠธ์˜ ๊ธฐ์ค€ ์ •๋ณด๊ฐ€ ๋˜๊ณ , ์—…์ข…๋„ ๊ทธ ๋ถ„๋ฅ˜์—์„œ ์ •ํ•ด์ง‘๋‹ˆ๋‹ค.', unavailable: '', confirmed: '์ด์ œ ์ด ๊ฐ€๊ฒŒ์˜ ๊ณต๊ฐœ ์ฑ„๋„์—์„œ ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•ฉ๋‹ˆ๋‹ค.', }; -/** outcome ์ฝ”๋“œ โ†’ ์‚ฌ์žฅ๋‹˜์ด ์ฝ์„ ํ•œ ์ค„. ์„œ๋ฒ„ ํŒ์ •์€ ๋ฌธ๊ตฌ๋ฅผ ๊ณ ๋ฅด๋Š” ๋ฐ๋งŒ ์“ด๋‹ค. */ -const OUTCOME_TEXT: Record = { - matched: '์ด ๊ฐ€๊ฒŒ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ๋งž๋Š”์ง€ ํ•œ ๋ฒˆ๋งŒ ํ™•์ธํ•ด ์ฃผ์„ธ์š”.', - ambiguous: '๋น„์Šทํ•œ ์ด๋ฆ„์˜ ๊ฐ€๊ฒŒ๊ฐ€ ์—ฌ๋Ÿฟ์ž…๋‹ˆ๋‹ค. ์–ด๋А ์ชฝ์ด ์‚ฌ์žฅ๋‹˜ ๊ฐ€๊ฒŒ์ธ๊ฐ€์š”?', - no_candidate: '์ด ์ด๋ฆ„์œผ๋กœ๋Š” ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.', -}; - /** - * 2๋‹จ๊ณ„ โ€” ๋‚ด ๊ฐ€๊ฒŒ ํ™•์ธ. + * 1๋‹จ๊ณ„ โ€” ๋‚ด ๊ฐ€๊ฒŒ ํ™•์ธ. **์œ„์ €๋“œ์˜ ์‹œ์ž‘์ ์ด๋‹ค.** + * + * โ˜… ์˜ˆ์ „์—๋Š” ์—…์ข… ์„ ํƒ์ด ์•ž์— ์žˆ์—ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์‚ฌ์žฅ๋‹˜์ด 100% ์•„๋Š” ๊ฑด ์ž๊ธฐ ๊ฐ€๊ฒŒ **์ด๋ฆ„**์ด๊ณ , + * ์—…์ข…์€ ๊ฒฝ๊ณ„์—์„œ ๋ฉˆ์ถ˜๋‹ค("์šฐ๋ฆฌ๋Š” ์นดํŽ˜์ธ๊ฐ€ ์Œ์‹์ ์ธ๊ฐ€"). ๊ทธ๋ž˜์„œ ์ˆœ์„œ๋ฅผ ๋’ค์ง‘์—ˆ๋‹ค โ€” + * ์ƒํ˜ธ๋ช…์„ ๋จผ์ € ๋ฐ›๊ณ , ์—…์ข…์€ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์˜ ๋ถ„๋ฅ˜๊ฐ€ ์ •ํ•œ๋‹ค(services/place_category.py). + * ๋ชป ์ •ํ–ˆ์„ ๋•Œ๋งŒ ์—…์ข… ํ™”๋ฉด(`?step=industry`)์œผ๋กœ ๋„˜๊ธด๋‹ค. * * ํ•œ ํ™”๋ฉด์— ํ•œ ๊ฐ€์ง€๋งŒ ๋ฌป๋Š”๋‹ค: ์ƒํ˜ธ๋ฅผ ๋ฌป๊ฑฐ๋‚˜ ยท ๊ธฐ๋‹ค๋ฆฌ๊ฑฐ๋‚˜ ยท ํ›„๋ณด๋ฅผ ๊ณ ๋ฅด๊ฒŒ ํ•˜๊ฑฐ๋‚˜ ยท ํ™•์ •์„ ๋ณด์—ฌ์ค€๋‹ค. */ @@ -45,117 +47,144 @@ export function Step2PlaceSearch() { const storeName = useBuilderStore((s) => s.storeName); const location = useBuilderStore((s) => s.location); const confirmedIdentity = useBuilderStore((s) => s.confirmedIdentity); + const pendingPick = useBuilderStore((s) => s.pendingPick); const setStoreName = useBuilderStore((s) => s.setStoreName); const setLocation = useBuilderStore((s) => s.setLocation); + const selectIndustry = useBuilderStore((s) => s.selectIndustry); + const setPendingPick = useBuilderStore((s) => s.setPendingPick); const confirmIdentity = useBuilderStore((s) => s.confirmIdentity); const clearIdentity = useBuilderStore((s) => s.clearIdentity); - const goToStep = useBuilderStore((s) => s.goToStep); // ์ƒˆ๋กœ๊ณ ์นจ์„ ๋„˜์–ด์˜จ ๊ฒฝ์šฐ ์ด๋ฏธ ๋งŒ๋“ค์–ด์ง„ ์‚ฌ์—…์žฅ์„ ๊ทธ๋Œ€๋กœ ์“ด๋‹ค(๊ฐ™์€ ์œ„์ €๋“œ์—์„œ ๋‘ ๋ฒˆ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค). - const search = usePlaceSearch(industry, confirmedIdentity?.placeId ?? null); - const [searchParams, setSearchParams] = useSearchParams(); - - /** - * ํ™•์ •๋œ ์‚ฌ์—…์žฅ์„ ์ฃผ์†Œ์ฐฝ์— ๋‚จ๊ธด๋‹ค โ€” `?placeId=...`. - * - * โ˜… ์ด๊ฒŒ ์ƒˆ๋กœ๊ณ ์นจ์„ ๋„˜๊ธฐ๋Š” ์œ ์ผํ•œ ์ˆ˜๋‹จ์ด๋‹ค. ์œ„์ €๋“œ ์ƒํƒœ๋Š” ๋ธŒ๋ผ์šฐ์ €์— ์ €์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค - * (stores/builder ์ฃผ์„): ์ €์žฅํ•˜๋ฉด ์ƒˆ ๊ฐ€๊ฒŒ๋ฅผ ๋งŒ๋“ค๋Ÿฌ ๋“ค์–ด์™€๋„ ์ง€๋‚œ ๊ฐ€๊ฒŒ๊ฐ€ ํ™•์ •๋œ ๊ฒƒ์œผ๋กœ - * ๋–  ๋ฒ„๋ฆฐ๋‹ค. ์ฃผ์†Œ์ฐฝ์— ๋‘๋ฉด ์ƒˆ๋กœ๊ณ ์นจ์€ ์„œ๋ฒ„์—์„œ ๋ณต์›๋˜๊ณ (usePlaceSync), ์ฃผ์†Œ๋ฅผ ์ƒˆ๋กœ - * ์—ด๋ฉด ๊นจ๋—ํ•˜๋‹ค โ€” ๋‘ ์š”๊ตฌ๋ฅผ ๋™์‹œ์— ๋งŒ์กฑํ•˜๋Š” ์ž๋ฆฌ๋Š” ์—ฌ๊ธฐ๋ฟ์ด๋‹ค. - */ - const rememberPlace = (placeId: string | null) => { - const next = new URLSearchParams(searchParams); - if (placeId) { - next.set('placeId', placeId); - // ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์žฌ์‹œ์ž‘/HMR๋กœ BuilderPage๊ฐ€ ๋‹ค์‹œ ๋งˆ์šดํŠธ๋ผ๋„ ์‹ ๊ทœ ๋“ฑ๋ก ํ๋ฆ„์ž„์„ ๋ณด์กดํ•œ๋‹ค. - next.set('flow', 'onboarding'); - } else { - next.delete('placeId'); - next.delete('flow'); - } - setSearchParams(next, {replace: true}); - }; - const [pickedIndex, setPickedIndex] = useState(null); + const search = usePlaceSearch(confirmedIdentity?.placeId ?? null); + const [, goToStep] = useWizardStep(); const [placeUrl, setPlaceUrl] = useState(''); const canSearch = storeName.trim().length > 0 && search.phase !== 'searching'; - const runSearch = () => { - clearIdentity(); // ์ƒํ˜ธ๋ฅผ ๊ณ ์ณ ๋‹ค์‹œ ์ฐพ๋Š” ๊ฒƒ์ด๋ฏ€๋กœ ์•ž์„œ ํ™•์ •ํ•œ ์‹ ์›์€ ๋ฌผ๋ฆฐ๋‹ค - setPickedIndex(null); - // โ˜… ๋กœ๊ทธ์ธ ์ „์—๋Š” ์„œ๋ฒ„๋ฅผ ๋ถ€๋ฅด์ง€ ์•Š๋Š”๋‹ค. ๋กœ๊ทธ์ธ์€ ์—๋””ํ„ฐ ์ง„์ž…์—์„œ ํ•œ ๋ฒˆ ๋ฐ›๋Š” ๊ฒƒ์ด ์ด ์•ฑ์˜ ํ๋ฆ„์ธ๋ฐ, - // ์žฅ์†Œ API ๋Š” ์ „๋ถ€ ํ† ํฐ์„ ์š”๊ตฌํ•ด์„œ(place.py) ์—ฌ๊ธฐ์„œ ๋ถ€๋ฅด๋ฉด 2๋‹จ๊ณ„๊ฐ€ ๋กœ๊ทธ์ธ ๋ฒฝ์ด ๋œ๋‹ค. - // ์ž…๋ ฅํ•œ ๊ฐ’์œผ๋กœ ์‹ ์›์„ ์„ธ์šฐ๊ณ  ๋„˜์–ด๊ฐ„๋‹ค โ€” ๊ฒ€์ฆ์€ ๋กœ๊ทธ์ธ ๋’ค์— ๋‹ค์‹œ ํ•  ์ˆ˜ ์žˆ๋‹ค. - if (!getAccessToken()) { - confirmIdentity(search.confirmManual(storeName, location)); - goToStep(3); + /** + * ํ™•์ •๋œ ์‚ฌ์—…์žฅ์„ ์ฃผ์†Œ์ฐฝ์— ๋‚จ๊ธฐ๋ฉฐ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ. + * + * โ˜… `?placeId=` ๊ฐ€ ์ƒˆ๋กœ๊ณ ์นจ์„ ๋„˜๊ธฐ๋Š” ์œ ์ผํ•œ ์ˆ˜๋‹จ์ด๋‹ค. ์œ„์ €๋“œ ์ƒํƒœ๋Š” ๋ธŒ๋ผ์šฐ์ €์— ์ €์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค + * (stores/builder ์ฃผ์„): ์ €์žฅํ•˜๋ฉด ์ƒˆ ๊ฐ€๊ฒŒ๋ฅผ ๋งŒ๋“ค๋Ÿฌ ๋“ค์–ด์™€๋„ ์ง€๋‚œ ๊ฐ€๊ฒŒ๊ฐ€ ํ™•์ •๋œ ๊ฒƒ์œผ๋กœ ๋–  ๋ฒ„๋ฆฐ๋‹ค. + * ์ฃผ์†Œ์ฐฝ์— ๋‘๋ฉด ์ƒˆ๋กœ๊ณ ์นจ์€ ์„œ๋ฒ„์—์„œ ๋ณต์›๋˜๊ณ (usePlaceSync), ์ฃผ์†Œ๋ฅผ ์ƒˆ๋กœ ์—ด๋ฉด ๊นจ๋—ํ•˜๋‹ค. + * โ˜… ๋‹จ๊ณ„์™€ ์‚ฌ์—…์žฅ์„ **ํ•œ ๋ฒˆ์—** ๋ฐ”๊พผ๋‹ค. ๋‚˜๋ˆ  ์“ฐ๋ฉด ๊ทธ ์‚ฌ์ด์— "placeId ๋Š” ๋ถ™์—ˆ๋Š”๋ฐ ์•„์ง 1๋‹จ๊ณ„"์ธ + * ์ฃผ์†Œ๊ฐ€ ํžˆ์Šคํ† ๋ฆฌ์— ํ•œ ์นธ ์ƒ๊ธฐ๊ณ , ๋’ค๋กœ๊ฐ€๊ธฐ๊ฐ€ ๊ทธ ์นธ์— ๊ฑธ๋ฆฐ๋‹ค. + */ + const advance = (placeId: string | null) => { + goToStep('collect', { + params: {placeId, flow: placeId ? 'onboarding' : null}, + }); + }; + + const finishPick = async (pick: PendingPick, nextIndustry: IndustryType) => { + const identity = await search.confirmPick(pick, nextIndustry); + setPendingPick(null); + if (!identity) return; // ์‹คํŒจ ์‚ฌ์œ ๋Š” search.pickError ๊ฐ€ ํ™”๋ฉด์— ๋‚จ๊ธด๋‹ค + confirmIdentity(identity); + advance(identity.placeId); + }; + + /** + * ์—…์ข… ํ™”๋ฉด์„ ๋‹ค๋…€์˜จ ํ›„๋ณด๋ฅผ ์ด์–ด์„œ ํ™•์ •ํ•œ๋‹ค. + * + * โ˜… ์—…์ข…์ด **์ฐํ˜€์„œ** ๋Œ์•„์˜จ ๊ฒƒ๋งŒ ์ด์–ด๊ฐ„๋‹ค. ๋’ค๋กœ๊ฐ€๊ธฐ๋กœ ๋Œ์•„์˜จ ๊ฒฝ์šฐ(์—…์ข…์„ ์•ˆ ๊ณ ๋ฅธ ๊ฒฝ์šฐ)๊นŒ์ง€ + * ํ™•์ •ํ•˜๋ฉด ์‚ฌ์žฅ๋‹˜์ด ๊ณ ๋ฅด์ง€๋„ ์•Š์€ ๊ธฐ๋ณธ ์—…์ข…์œผ๋กœ ์‚ฌ์—…์žฅ์ด ๋งŒ๋“ค์–ด์ง„๋‹ค. + */ + const resumed = useRef(false); + useEffect(() => { + const industryFromPicker = pendingPick?.industry; + if (!pendingPick || !industryFromPicker || resumed.current) return; + resumed.current = true; + void finishPick(pendingPick, industryFromPicker); + // eslint-disable-next-line react-hooks/exhaustive-deps -- ํ•œ ๋ฒˆ๋งŒ ์ด์–ด๋ถ™์ด๋Š” ์ผ์ด๋ผ pendingPick ๋งŒ ๋ณธ๋‹ค + }, [pendingPick]); + + /** + * ํ›„๋ณด ํ•˜๋‚˜๋ฅผ ๊ณจ๋ž๋‹ค โ€” ์—ฌ๊ธฐ์„œ ์—…์ข…์ด ์ •ํ•ด์ง„๋‹ค. + * + * โ˜… `category` ๋Š” ์„œ๋ฒ„๊ฐ€ ์™ธ๋ถ€ ๋ถ„๋ฅ˜์—์„œ **์ถ”์ •ํ•œ** ๊ฐ’์ด๊ณ , ๋ชป ์ •ํ•˜๋ฉด ํ‚ค ์ž์ฒด๊ฐ€ ์—†๋‹ค + * (RemoveNoneResponse ๊ฐ€ null ํ•„๋“œ๋ฅผ ์ง€์šด๋‹ค). ์–ต์ง€๋กœ ํ•˜๋‚˜๋ฅผ ๊ณ ๋ฅด์ง€ ์•Š๊ณ  ์‚ฌ์žฅ๋‹˜์—๊ฒŒ ๋ฌป๋Š”๋‹ค โ€” + * ์—…์ข…์€ ์ˆ˜์ง‘ ์Šคํ‚ค๋งˆ์™€ JSON-LD ํƒ€์ž…์„ ํ†ต์งธ๋กœ ์ •ํ•˜๋Š” ๊ฐ’์ด๋ผ ํ‹€๋ฆฌ๋ฉด ๋˜๋Œ๋ฆฌ๋Š” ๊ฐ’์ด ๋น„์‹ธ๋‹ค. + */ + const choose = (item: PlaceSearchItem) => { + const pick: PendingPick = { + name: item.name ?? '', + address: item.road_address ?? '', + }; + const guessed = item.category != null ? (CATEGORY_TO_INDUSTRY[item.category] ?? null) : null; + if (!guessed) { + setPendingPick(pick); + goToStep('industry'); return; } - void search.search(storeName, location); + selectIndustry(guessed); + void finishPick(pick, guessed); + }; + + const runSearch = () => { + clearIdentity(); // ์ƒํ˜ธ๋ฅผ ๊ณ ์ณ ๋‹ค์‹œ ์ฐพ๋Š” ๊ฒƒ์ด๋ฏ€๋กœ ์•ž์„œ ํ™•์ •ํ•œ ์‹ ์›์€ ๋ฌผ๋ฆฐ๋‹ค + void search.searchPublic(storeName, location); }; const backToInput = () => { clearIdentity(); // ๋‹ค๋ฅธ ๊ฐ€๊ฒŒ๋ฅผ ์ฐพ์œผ๋Ÿฌ ๊ฐ„๋‹ค โ€” ์ฃผ์†Œ์ฐฝ์— ๋‚จ์€ ์‚ฌ์—…์žฅ๋„ ๊ฐ™์ด ๋†“์•„์ค€๋‹ค. - rememberPlace(null); - setPickedIndex(null); + goToStep('search', {params: {placeId: null, flow: null}, replace: true}); search.reset(); }; - const pick = async (candidate: PlaceCandidate, index: number) => { - // ์ง€์—ญ๊ฒ€์ƒ‰ ํ›„๋ณด๋งŒ ์žˆ๊ณ  ๋„ค์ด๋ฒ„ ํ”Œ๋ ˆ์ด์Šค๋ฅผ ๋ชป ์ฐพ์•˜์œผ๋ฉด ํ™•์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค. - // URL ํ™•์ • ๊ฒฝ๋กœ๊ฐ€ ์ƒํ˜ธยท์ฃผ์†Œยท์ขŒํ‘œ์™€ ์ˆ˜์ง‘ ์ฑ„๋„์„ ํ•œ ๋ฒˆ์— ๋ณด์žฅํ•œ๋‹ค. - if (!candidate.naver_place_url) return; - setPickedIndex(index); - const identity = await search.confirmByUrl(candidate.naver_place_url); - if (!identity) { - setPickedIndex(null); - return; - } - confirmIdentity(identity); - rememberPlace(identity.placeId); - goToStep(3); - }; - /** ๋„ค์ด๋ฒ„ ํ”Œ๋ ˆ์ด์Šค URL ๋กœ ํ™•์ • โ€” ๊ฐ€์žฅ ํ™•์‹คํ•œ ๊ฒฝ๋กœ๋‹ค(usePlaceSearch.confirmByUrl ์ฃผ์„ ์ฐธ๊ณ ). */ const pickByUrl = async () => { const url = placeUrl.trim(); if (!url) return; - // URL ํ™•์ธ๋„ ์„œ๋ฒ„๊ฐ€ ํ† ํฐ์„ ์š”๊ตฌํ•œ๋‹ค โ€” ๋กœ๊ทธ์ธ ์ „์—๋Š” ์ž…๋ ฅ๊ฐ’์œผ๋กœ ๋„˜์–ด๊ฐ„๋‹ค. - if (!getAccessToken()) { - confirmIdentity(search.confirmManual(storeName, location)); - goToStep(3); - return; - } - const identity = await search.confirmByUrl(url); + const identity = await search.confirmByUrl(url, industry); if (!identity) return; confirmIdentity(identity); - rememberPlace(identity.placeId); - goToStep(3); + advance(identity.placeId); + }; + + /** + * ์—…์ข…์„ ๋ฐ”๊พธ๋Ÿฌ ๊ฐ„๋‹ค. + * + * โ˜… **์„œ๋ฒ„์— ์‚ฌ์—…์žฅ์ด ์ด๋ฏธ ๋งŒ๋“ค์–ด์กŒ์œผ๋ฉด ์‹ ์› ํ™•์ธ๋ถ€ํ„ฐ ๋‹ค์‹œ ๋ฐ›๋Š”๋‹ค.** places.category ๋Š” ๋งŒ๋“ค ๋•Œ + * ์ •ํ•ด์ง€๊ณ  PATCH ๋กœ ๋ชป ๊ณ ์นœ๋‹ค(Req_UpdatePlace ์— category ๊ฐ€ ์—†๋‹ค) โ€” ํ™”๋ฉด์—์„œ๋งŒ ๋ฐ”๊พธ๋ฉด + * ๋ฆฌํŽ˜์น˜ ํ•œ ๋ฒˆ์— ์„œ๋ฒ„ ๊ฐ’์œผ๋กœ ๋˜๋Œ์•„๊ฐ€์„œ, ์‚ฌ์žฅ๋‹˜ ๋ˆˆ์—๋Š” ๋ฐ”๊พผ ๊ฒŒ ์”นํžŒ ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค. + * ํ™•์ • ์ „(๋กœ๊ทธ์ธ ์ „ ํฌํ•จ)์—๋Š” ์„œ๋ฒ„์— ์•„๋ฌด๊ฒƒ๋„ ์—†์œผ๋‹ˆ ์—…์ข…๋งŒ ๊ฐˆ์•„๋„ ๋œ๋‹ค. + */ + const identityIsOnServer = Boolean(confirmedIdentity?.placeId); + const changeIndustry = () => { + if (identityIsOnServer) { + clearIdentity(); + goToStep('industry', {params: {placeId: null, flow: null}}); + return; + } + goToStep('industry'); }; // โ”€โ”€ ํ™”๋ฉด ๊ณ ๋ฅด๊ธฐ. ์œ„์—์„œ๋ถ€ํ„ฐ ๋จผ์ € ๋งž๋Š” ๊ฒƒ ํ•˜๋‚˜๋งŒ ๊ทธ๋ฆฐ๋‹ค โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const stage = confirmedIdentity ? 'confirmed' : search.phase === 'searching' - ? 'searching' - : search.phase === 'unavailable' - ? 'unavailable' - : search.phase === 'done' - ? 'picking' - : 'input'; + ? 'searching' + : search.phase === 'unavailable' + ? 'unavailable' + : search.phase === 'done' + ? 'picking' + : 'input'; + + const searchQuery = [storeName, location].filter(Boolean).join(' '); return (
{stage === 'input' && ( -
-
+
)} @@ -216,72 +244,51 @@ export function Step2PlaceSearch() {

0 + search.items.length > 0 ? 'border border-border bg-card text-muted-foreground' : 'border border-warning/30 bg-warning/10 text-warning', )} > - {OUTCOME_TEXT[search.outcome] ?? '์•„๋ž˜ ๋ชฉ๋ก์—์„œ ์‚ฌ์žฅ๋‹˜ ๊ฐ€๊ฒŒ๋ฅผ ๊ณจ๋ผ ์ฃผ์„ธ์š”.'} - {search.sourceLabel && ( - - {search.sourceLabel} - - )} + {search.items.length > 0 + ? '์•„๋ž˜ ๋ชฉ๋ก์—์„œ ์‚ฌ์žฅ๋‹˜ ๊ฐ€๊ฒŒ๋ฅผ ๊ณจ๋ผ ์ฃผ์„ธ์š”.' + : '์ด ์ด๋ฆ„์œผ๋กœ๋Š” ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜์—์„œ ๋„ค์ด๋ฒ„ ์ง€๋„ ์ฃผ์†Œ๋กœ ์ฐพ์•„ ์ฃผ์„ธ์š”.'}

- {search.candidates.length > 0 && - !search.candidates.some((candidate) => candidate.naver_place_url) && ( -

- - ์ƒํ˜ธ ํ›„๋ณด๋Š” ์ฐพ์•˜์ง€๋งŒ ๋„ค์ด๋ฒ„ ํ”Œ๋ ˆ์ด์Šค๋Š” ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜ ๋ฒ„ํŠผ์œผ๋กœ ๋„ค์ด๋ฒ„ - ์ง€๋„์—์„œ ๊ฐ€๊ฒŒ๋ฅผ ๊ฒ€์ƒ‰ํ•œ ๋’ค ํ”Œ๋ ˆ์ด์Šค URL์„ ๋ถ™์—ฌ๋„ฃ์–ด ์ฃผ์„ธ์š”. -

- )} + {search.pickError && ( +

+ + {search.pickError} +

+ )}
    - {search.candidates.map((candidate, index) => ( -
  • + {search.items.map((item, index) => ( +
  • @@ -289,25 +296,26 @@ export function Step2PlaceSearch() { ))}
- {!search.candidates.some((candidate) => candidate.naver_place_url) ? ( - void pickByUrl()} - isBusy={search.isConfirming} - searchQuery={[storeName, location].filter(Boolean).join(' ')} - /> - ) : ( - void pickByUrl()} - isBusy={search.isConfirming} - compact - searchQuery={[storeName, location].filter(Boolean).join(' ')} - /> - )} + {/* ๋ชฉ๋ก์—์„œ ๊ณ ๋ฅด๋ฉด ์—…์ข…์€ ๊ทธ ํ›„๋ณด๊ฐ€ ์ •ํ•œ๋‹ค โ€” ์ด ์ค„์€ ์•„๋ž˜ '์ฃผ์†Œ๋กœ ํ™•์ •' ๊ฒฝ๋กœ์šฉ์ด๋‹ค. */} + + void pickByUrl()} + isBusy={search.isConfirming} + compact={search.items.length > 0 && !search.pickError} + searchQuery={searchQuery} + /> + +
)} @@ -317,12 +325,19 @@ export function Step2PlaceSearch() { {search.unavailableReason}

+ + + void pickByUrl()} isBusy={search.isConfirming} - searchQuery={[storeName, location].filter(Boolean).join(' ')} + searchQuery={searchQuery} />
+ + + + ); +} diff --git a/solution/frontend/src/features/onboarding/Step3DataReview.tsx b/solution/frontend/src/features/onboarding/Step3DataReview.tsx index 0a896c7..cd2c699 100644 --- a/solution/frontend/src/features/onboarding/Step3DataReview.tsx +++ b/solution/frontend/src/features/onboarding/Step3DataReview.tsx @@ -10,6 +10,7 @@ import {useBuilderStore, useUnverifiedFields} from '@/stores/builder'; import {ChannelConfirmPanel} from './ChannelConfirmPanel'; import {ChannelUrlInput} from './ChannelUrlInput'; import {useCollectFlow} from './useCollectFlow'; +import {useWizardStep} from './wizardUrl'; import {WizardFooter} from './WizardFooter'; import {WizardStage, WizardWaiting} from './WizardStage'; @@ -29,7 +30,7 @@ const STAGE_DESCRIPTION: Record = { review: '์ฐพ์€ ๊ฐ’์€ ์ „๋ถ€ ์ถœ์ฒ˜์™€ ํ•จ๊ป˜ ๋ณด์—ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ์‚ฌ์žฅ๋‹˜์ด [๋งž์•„์š”]๋ฅผ ๋ˆ„๋ฅธ ๊ฐ’๋งŒ ์‚ฌ์ดํŠธ์™€ AI ๊ฒ€์ƒ‰ ๋‹ต๋ณ€์— ๋‚˜๊ฐ‘๋‹ˆ๋‹ค.', }; -/** ์ฑ„๋„ URL ํ™•์ธ๊ณผ ์ˆ˜์ง‘ ๊ฒฐ๊ณผ ๊ฒ€ํ†  ๋‹จ๊ณ„. */ +/** ์ฑ„๋„ URL ํ™•์ธ๊ณผ ์ˆ˜์ง‘ ๊ฒฐ๊ณผ ๊ฒ€ํ†  ๋‹จ๊ณ„(์ง„ํ–‰ ํ‘œ์‹œ 2๋ฒˆ). */ export function Step3DataReview() { const industry = useBuilderStore((s) => s.industry); const storeName = useBuilderStore((s) => s.storeName); @@ -37,7 +38,7 @@ export function Step3DataReview() { const gatherCompleted = useBuilderStore((s) => s.gatherCompleted); const infoFields = useBuilderStore((s) => s.infoFields); const photos = useBuilderStore((s) => s.photos); - const goToStep = useBuilderStore((s) => s.goToStep); + const [, goToStep] = useWizardStep(); const unverified = useUnverifiedFields(); const placeId = confirmedIdentity?.placeId ?? null; @@ -91,7 +92,7 @@ export function Step3DataReview() { return (
- )} -
@@ -302,7 +303,7 @@ export function Step3DataReview() { )} - @@ -354,8 +355,8 @@ export function Step3DataReview() { '์ˆ˜์ง‘์„ ๋Œ๋ฆฌ๋ฉด ์ฐพ์€ ๊ฐ’์ด ์ถœ์ฒ˜์™€ ํ•จ๊ป˜ ์—ฌ๊ธฐ์— ๋‚˜์˜ต๋‹ˆ๋‹ค.' ) } - onPrev={() => goToStep(2)} - onNext={() => goToStep(4)} + onPrev={() => goToStep('search')} + onNext={() => goToStep('template')} nextLabel="๋‹ค์Œ: ํ…œํ”Œ๋ฆฟ ์„ ํƒ" /** * โ˜… ์ˆ˜์ง‘ ์ „์—๋Š” ์ž ๊ทผ๋‹ค. diff --git a/solution/frontend/src/features/onboarding/Step4Template.tsx b/solution/frontend/src/features/onboarding/Step4Template.tsx index 089e3c6..fa7dfa9 100644 --- a/solution/frontend/src/features/onboarding/Step4Template.tsx +++ b/solution/frontend/src/features/onboarding/Step4Template.tsx @@ -4,6 +4,7 @@ import {INDUSTRY_CONFIGS} from '@/data/industryData'; import {queueSiteTemplateSave} from '@/features/publish/siteTemplate'; import {cn} from '@/lib/utils'; import {useBuilderStore} from '@/stores/builder'; +import {useWizardStep} from './wizardUrl'; import {WizardFooter} from './WizardFooter'; import {WizardSteps} from './WizardSteps'; @@ -85,7 +86,7 @@ export function Step4Template() { const templateId = useBuilderStore((s) => s.templateId); const placeId = useBuilderStore((s) => s.placeId); const selectTemplate = useBuilderStore((s) => s.selectTemplate); - const goToStep = useBuilderStore((s) => s.goToStep); + const [, goToStep] = useWizardStep(); const startGenerating = useBuilderStore((s) => s.startGenerating); const config = INDUSTRY_CONFIGS[industry]; @@ -105,7 +106,7 @@ export function Step4Template() {
- +

@@ -166,12 +167,13 @@ export function Step4Template() {

goToStep(3)} + onPrev={() => goToStep('collect')} onNext={() => { // โ˜… ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ๋ˆ„๋ฅด๊ณ  ๋„˜์–ด๊ฐ€๋Š” ๊ฒฝ์šฐ(์ฒซ ํ…œํ”Œ๋ฆฟ์ด ์ด๋ฏธ ์„ ํƒ๋ผ ์žˆ๋‹ค)๋„ ์„œ๋ฒ„์— ๋‚จ๊ธด๋‹ค โ€” // ํ™”๋ฉด์ด ๋ณด์—ฌ์ค€ ๊ทธ ํ…œํ”Œ๋ฆฟ์ด ๋ฐœํ–‰๋ณธ์ด ๋˜์–ด์•ผ ํ•œ๋‹ค. ๊ฐ™์€ ๊ฐ’์ด๋ฉด ์„œ๋ฒ„๊ฐ€ ์žฌ๋นŒ๋“œ ํ‘œ์‹œ๋„ ์ฐ์ง€ ์•Š๋Š”๋‹ค. queueSiteTemplateSave(placeId, templateId); startGenerating(); + goToStep('generating'); }} nextLabel="์ด ํ…œํ”Œ๋ฆฟ์œผ๋กœ ์‚ฌ์ดํŠธ ์ƒ์„ฑํ•˜๊ธฐ" /> diff --git a/solution/frontend/src/features/onboarding/Step5Generating.tsx b/solution/frontend/src/features/onboarding/Step5Generating.tsx index be94552..930c7fa 100644 --- a/solution/frontend/src/features/onboarding/Step5Generating.tsx +++ b/solution/frontend/src/features/onboarding/Step5Generating.tsx @@ -1,12 +1,12 @@ import {useCallback, useEffect} from 'react'; -import {useSearchParams} from 'react-router'; import {Check, Loader2} from 'lucide-react'; import {JobStatus} from '@o2o/shared'; import {delay, getAccessToken, pollJob, startCopy} from '@/api'; import {Progress} from '@/components/ui/progress'; import {notify, notifyApiError} from '@/lib/notify'; import {cn} from '@/lib/utils'; -import {EDITOR_STEP, useBuilderStore} from '@/stores/builder'; +import {useBuilderStore} from '@/stores/builder'; +import {EDITOR_STEP, useWizardStep} from './wizardUrl'; const BUILD_STEPS = [ '์ˆ˜์ง‘๋œ ์‚ฌ์ง„ ๋ถ„๋ฅ˜ ๋ฐ ๋Œ€์ฒด ํ…์ŠคํŠธ ์ƒ์„ฑ', @@ -30,9 +30,8 @@ export function Step5Generating() { const storeName = useBuilderStore((s) => s.storeName); const stage = useBuilderStore((s) => s.generateStage); const setGenerateStage = useBuilderStore((s) => s.setGenerateStage); - const goToStep = useBuilderStore((s) => s.goToStep); const placeId = useBuilderStore((s) => s.placeId); - const [searchParams, setSearchParams] = useSearchParams(); + const [, goToStep] = useWizardStep(); /** * ์ƒ์„ฑ์ด ๋๋‚˜๋ฉด ๊ณง๋ฐ”๋กœ ์—๋””ํ„ฐ๋‹ค. @@ -43,11 +42,10 @@ export function Step5Generating() { * '๋ชฉ๋ก์œผ๋กœ ๋ณด๋‚ด๋Š” ๊ฒƒ'์€ ๋‹ค๋ฅธ ์–˜๊ธฐ๋‹ค. */ const finishOnboarding = useCallback(() => { - const next = new URLSearchParams(searchParams); - next.delete('flow'); - setSearchParams(next, {replace: true}); - goToStep(EDITOR_STEP); - }, [goToStep, searchParams, setSearchParams]); + // โ˜… `flow=onboarding` ๋„ ๊ฐ™์ด ๋—€๋‹ค. ๋‚จ๊ฒจ ๋‘๋ฉด ์ด ์ฃผ์†Œ๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ–ˆ์„ ๋•Œ ์œ„์ €๋“œ ์ค‘์ธ + // ์‚ฌ์—…์žฅ์œผ๋กœ ์ฝํ˜€ ์‹ ์›์ด ๋‹ค์‹œ ์„ธ์›Œ์ง„๋‹ค โ€” ํŽธ์ง‘ ์ค‘์ธ ์‚ฌ๋žŒ์—๊ฒŒ๋Š” ์•„๋ฌด ์˜๋ฏธ๊ฐ€ ์—†๋Š” ์ผ์ด๋‹ค. + goToStep(EDITOR_STEP, {params: {flow: null}, replace: true}); + }, [goToStep]); /** * ์†Œ๊ฐœ๋ฌธยทFAQ ์ƒ์„ฑ(JobType.COPY). `POST /v1/place/{id}/copy` ๋กœ ์žก์„ ๋„ฃ๊ณ  ํด๋งํ•œ๋‹ค. diff --git a/solution/frontend/src/features/onboarding/WizardSteps.tsx b/solution/frontend/src/features/onboarding/WizardSteps.tsx index a209a0f..6fe4b1c 100644 --- a/solution/frontend/src/features/onboarding/WizardSteps.tsx +++ b/solution/frontend/src/features/onboarding/WizardSteps.tsx @@ -4,14 +4,17 @@ import {cn} from '@/lib/utils'; /** * ์œ„์ €๋“œ ์ง„ํ–‰ ํ‘œ์‹œ. * - * 2(์‹ ์› ํ™•์ธ)์™€ 3(์‚ฌ์‹ค ํ™•์ธ)์ด ์™œ ๋”ฐ๋กœ์ธ์ง€ ์‚ฌ์žฅ๋‹˜์ด ํ•œ๋ˆˆ์— ์•Œ๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด ์ด ์ค„์˜ ๋ชฉ์ ์ด๋‹ค โ€” + * 1(์‹ ์› ํ™•์ธ)๊ณผ 2(์‚ฌ์‹ค ํ™•์ธ)๊ฐ€ ์™œ ๋”ฐ๋กœ์ธ์ง€ ์‚ฌ์žฅ๋‹˜์ด ํ•œ๋ˆˆ์— ์•Œ๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด ์ด ์ค„์˜ ๋ชฉ์ ์ด๋‹ค โ€” * "๊ฐ€๊ฒŒ๊ฐ€ ๋งž๋‚˜" ๋ฅผ ๋จผ์ € ๋๋‚ด๊ณ  "๊ฐ’์ด ๋งž๋‚˜" ๋กœ ๋„˜์–ด๊ฐ„๋‹ค. + * + * โ˜… ์—…์ข…์€ ๋” ์ด์ƒ ๋‹จ๊ณ„๊ฐ€ ์•„๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์˜ ๋ถ„๋ฅ˜๊ฐ€ ์ •ํ•˜๊ณ (๋ชป ์ •ํ•  ๋•Œ๋งŒ 1๋‹จ๊ณ„ ์•ˆ์—์„œ ๋ฌป๋Š”๋‹ค), + * ๊ทธ๋ž˜์„œ ๋„ค ์นธ์ด๋˜ ์ค„์ด ์„ธ ์นธ์ด ๋๋‹ค โ€” ์‚ฌ์žฅ๋‹˜์ด ์ฒ˜์Œ ๋ณด๋Š” ํ™”๋ฉด์ด "์šฐ๋ฆฌ๋Š” ์นดํŽ˜์ธ๊ฐ€ ์Œ์‹์ ์ธ๊ฐ€" + * ๊ฐ€ ์•„๋‹ˆ๋ผ "๊ฐ€๊ฒŒ ์ด๋ฆ„"์ด ๋˜๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด ์ด ๋ณ€๊ฒฝ์˜ ๋ชฉ์ ์ด๋‹ค. */ const STEPS = [ - {step: 1, label: '์—…์ข…'}, - {step: 2, label: '๋‚ด ๊ฐ€๊ฒŒ ํ™•์ธ'}, - {step: 3, label: '์ˆ˜์ง‘ ์ •๋ณด ํ™•์ธ'}, - {step: 4, label: 'ํ…œํ”Œ๋ฆฟ'}, + {step: 1, label: '๋‚ด ๊ฐ€๊ฒŒ ํ™•์ธ'}, + {step: 2, label: '์ˆ˜์ง‘ ์ •๋ณด ํ™•์ธ'}, + {step: 3, label: 'ํ…œํ”Œ๋ฆฟ'}, ] as const; export function WizardSteps({current}: {current: number}) { diff --git a/solution/frontend/src/features/onboarding/index.ts b/solution/frontend/src/features/onboarding/index.ts index af239dd..c71fcd8 100644 --- a/solution/frontend/src/features/onboarding/index.ts +++ b/solution/frontend/src/features/onboarding/index.ts @@ -1,3 +1,5 @@ +// ์œ„์ €๋“œ ๋‹จ๊ณ„๋Š” ์ฃผ์†Œ์ฐฝ์ด ์†Œ์œ ํ•œ๋‹ค โ€” ํ™”๋ฉด์„ ๊ณ ๋ฅด๋Š” ์ชฝ(BuilderPage)์ด ์ด ๊ณ„์•ฝ์„ ์ฝ๋Š”๋‹ค. +export {defaultStep, EDITOR_STEP, useWizardStep, type WizardStep} from './wizardUrl'; export {Step1Industry} from './Step1Industry'; export {Step2PlaceSearch} from './Step2PlaceSearch'; export {Step3DataReview} from './Step3DataReview'; diff --git a/solution/frontend/src/features/onboarding/usePlaceSearch.ts b/solution/frontend/src/features/onboarding/usePlaceSearch.ts index 9716eab..805180b 100644 --- a/solution/frontend/src/features/onboarding/usePlaceSearch.ts +++ b/solution/frontend/src/features/onboarding/usePlaceSearch.ts @@ -1,21 +1,21 @@ import {useCallback, useRef, useState} from 'react'; import type {IndustryType} from '@o2o/shared'; -import type {ExternalPlaceSource as ExternalPlaceSourceType, PlaceCandidate, PlaceCategory} from '@/api'; +import type {PlaceCategory, PlaceSearchItem} from '@/api'; import { createPlace, - ExternalPlaceSource, getAccessToken, PlaceCategory as PlaceCategoryEnum, + searchPlacesPublic, updatePlace, verifyCandidates, - verifyPlace, verifyPlaceByUrl, } from '@/api'; import type {ConfirmedIdentity} from '@/stores/builder'; import {ensureAutoSession} from '@/lib/autoSession'; +import {describeError} from '@/lib/errorMessages'; import {notify, notifyApiError} from '@/lib/notify'; -/** ๋นŒ๋” ์—…์ข… โ†’ places.category. ๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ์€ stores/builder ์˜ CATEGORY_TO_INDUSTRY ๋‹ค. */ +/** ๋นŒ๋” ์—…์ข… โ†’ places.category. ๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ์€ placeAdapter ์˜ CATEGORY_TO_INDUSTRY ๋‹ค. */ const INDUSTRY_TO_CATEGORY: Record = { stay: PlaceCategoryEnum.LODGING, cafe: PlaceCategoryEnum.CAFE, @@ -23,21 +23,15 @@ const INDUSTRY_TO_CATEGORY: Record = { clinic: PlaceCategoryEnum.CLINIC, }; -/** ํ›„๋ณด๋ฅผ ์–ด๋А ์žฅ์†Œ DB ์—์„œ ์ฐพ์•˜๋Š”์ง€ โ€” ์‚ฌ์žฅ๋‹˜์ด ํŒ๋‹จํ•  ๊ทผ๊ฑฐ๋กœ ์นด๋“œ์— ๊ทธ๋Œ€๋กœ ๋ถ™์ธ๋‹ค. */ -const SOURCE_LABEL: Record = { - [ExternalPlaceSource.KAKAO]: '์นด์นด์˜ค๋งต', - [ExternalPlaceSource.NAVER]: '๋„ค์ด๋ฒ„ ์ง€๋„', -}; - export type SearchPhase = /** ์•„์ง ๊ฒ€์ƒ‰ ์ „. */ | 'idle' - /** ์‚ฌ์—…์žฅ ์ƒ์„ฑ + ํ›„๋ณด ์กฐํšŒ ์ค‘. */ + /** ๊ณต๊ฐœ ๊ฒ€์ƒ‰ ์ค‘. */ | 'searching' - /** ํ›„๋ณด๋ฅผ ๋ฐ›์•˜๋‹ค(0๊ฑด์ผ ์ˆ˜๋„ ์žˆ๋‹ค โ€” outcome ์ด no_candidate). */ + /** ํ›„๋ณด๋ฅผ ๋ฐ›์•˜๋‹ค(0๊ฑด์ผ ์ˆ˜๋„ ์žˆ๋‹ค). */ | 'done' /** - * ์žฅ์†Œ DB ๋ฅผ ๋ถ€๋ฅผ ์ˆ˜ ์—†๋‹ค โ€” ๋กœ๊ทธ์ธ์ด ์—†๊ฑฐ๋‚˜ ๋ฐฑ์—”๋“œ๊ฐ€ ์‘๋‹ตํ•˜์ง€ ์•Š๋Š”๋‹ค. + * ์žฅ์†Œ DB ๋ฅผ ๋ถ€๋ฅผ ์ˆ˜ ์—†๋‹ค โ€” ํ‚ค๊ฐ€ ์—†๊ฑฐ๋‚˜, ๋ถ„๋‹น ํ˜ธ์ถœ์„ ๋„˜๊ฒผ๊ฑฐ๋‚˜, ๋ฐฑ์—”๋“œ๊ฐ€ ์‘๋‹ตํ•˜์ง€ ์•Š๋Š”๋‹ค. * โ˜… ์ด๋•Œ ๊ฐ€์งœ ํ›„๋ณด๋ฅผ ์ง€์–ด๋‚ด์ง€ ์•Š๋Š”๋‹ค. ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์ธ ์ฒ™ํ•˜๋Š” ์˜ˆ์‹œ ์นด๋“œ๋Š” * ์‚ฌ์žฅ๋‹˜์ด ๋‚จ์˜ ๊ฐ€๊ฒŒ๋ฅผ ์ž๊ธฐ ๊ฐ€๊ฒŒ๋กœ ํ™•์ •ํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฐ€์žฅ ๋น ๋ฅธ ๊ธธ์ด๋‹ค. */ @@ -45,41 +39,47 @@ export type SearchPhase = export interface PlaceSearchState { phase: SearchPhase; - /** ์™ธ๋ถ€ ์žฅ์†Œ DB ํ›„๋ณด. ์„œ๋ฒ„๊ฐ€ ์ž๋™ ํ™•์ •ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ํ•ญ์ƒ ์‚ฌ๋žŒ์ด ๊ณ ๋ฅธ๋‹ค. */ - candidates: PlaceCandidate[]; - /** matched | ambiguous | no_candidate โ€” ์„œ๋ฒ„ ํŒ์ •. ๋ฌธ๊ตฌ๋ฅผ ๊ณ ๋ฅด๋Š” ๋ฐ๋งŒ ์“ด๋‹ค. */ - outcome: string; - /** true ๋ฉด ํŒ์ •์ด ๋ช…ํ™•ํ•ด '์ด๊ฑฐ ๋งž๋‚˜์š”?' ํ•œ ๋ฒˆ๋งŒ ๋ฌผ์–ด๋„ ๋œ๋‹ค. */ - autoSelectable: boolean; - /** ํ›„๋ณด๋ฅผ ์ค€ ์žฅ์†Œ DB ์ด๋ฆ„. ์—†์œผ๋ฉด ๋นˆ ๋ฌธ์ž์—ด. */ - sourceLabel: string; - source: ExternalPlaceSourceType | null; - /** ๊ฒ€์ƒ‰์„ ๋ชป ํ•œ ์ด์œ (๋กœ๊ทธ์ธ ํ•„์š” ๋“ฑ). phase === 'unavailable' ์ผ ๋•Œ๋งŒ ์ฑ„์›Œ์ง„๋‹ค. */ + /** ๊ณต๊ฐœ ๊ฒ€์ƒ‰ ํ›„๋ณด. ์„œ๋ฒ„๊ฐ€ ์ž๋™ ํ™•์ •ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ํ•ญ์ƒ ์‚ฌ๋žŒ์ด ๊ณ ๋ฅธ๋‹ค. */ + items: PlaceSearchItem[]; + /** ๊ฒ€์ƒ‰์„ ๋ชป ํ•œ ์ด์œ . phase === 'unavailable' ์ผ ๋•Œ๋งŒ ์ฑ„์›Œ์ง„๋‹ค. */ unavailableReason: string; + /** + * ๊ณ ๋ฅธ ํ›„๋ณด๋ฅผ ์ž๋™์œผ๋กœ ํ™•์ •ํ•˜์ง€ ๋ชปํ•œ ์ด์œ . + * โ˜… ์‹คํŒจ๋ฅผ ์กฐ์šฉํžˆ ์‚ผํ‚ค๋ฉด ์‚ฌ์žฅ๋‹˜์€ "๋ˆŒ๋ €๋Š”๋ฐ ์•„๋ฌด ์ผ๋„ ์•ˆ ์ผ์–ด๋‚œ๋‹ค"๋งŒ ๊ฒช๋Š”๋‹ค โ€” + * ๋„ค์ด๋ฒ„ ํ”Œ๋ ˆ์ด์Šค URL ๋ถ™์—ฌ๋„ฃ๊ธฐ๋กœ ๋„˜์–ด๊ฐ€์•ผ ํ•œ๋‹ค๋Š” ๊ฑธ ์—ฌ๊ธฐ์„œ ๋งํ•œ๋‹ค. + */ + pickError: string; } const INITIAL: PlaceSearchState = { phase: 'idle', - candidates: [], - outcome: '', - autoSelectable: false, - sourceLabel: '', - source: null, + items: [], unavailableReason: '', + pickError: '', }; +/** ์ƒํ˜ธ๋ช… ๋น„๊ต์šฉ. ๊ณต๋ฐฑยท๊ด„ํ˜ธ ํ‘œ๊ธฐ๊ฐ€ ํ›„๋ณด๋งˆ๋‹ค ๋‹ฌ๋ผ ๊ทธ๋Œ€๋กœ ๋น„๊ตํ•˜๋ฉด ๊ฐ™์€ ๊ฐ€๊ฒŒ๋„ ์•ˆ ๋งž๋Š”๋‹ค. */ +function normalizeName(name: string | undefined | null): string { + return (name ?? '').replace(/\s+/g, '').toLowerCase(); +} + /** - * ์ƒํ˜ธ ๊ฒ€์ƒ‰ โ†’ ๋™์ผ ์—…์†Œ ํ›„๋ณด ์กฐํšŒ โ†’ ์‚ฌ๋žŒ์ด ๊ณ ๋ฅธ ํ›„๋ณด๋กœ ํ™•์ •. + * ์ƒํ˜ธ๋ช… ๊ณต๊ฐœ ๊ฒ€์ƒ‰ โ†’ ์‚ฌ๋žŒ์ด ๊ณ ๋ฅธ ํ›„๋ณด๋กœ ํ™•์ •. * - * ๋ฐฑ์—”๋“œ ์ˆœ์„œ๊ฐ€ ๊ทธ๋Œ€๋กœ ํ™”๋ฉด ์ˆœ์„œ๋‹ค: - * 1) POST /v1/place ์‚ฌ์—…์žฅ(๊ป๋ฐ๊ธฐ)์„ ๋งŒ๋“ ๋‹ค โ€” ํ›„๋ณด ์กฐํšŒ๊ฐ€ place_id ๋ฅผ ์š”๊ตฌํ•œ๋‹ค - * 2) GET /v1/place/{id}/verify/candidates?query= ์™ธ๋ถ€ ์žฅ์†Œ DB ํ›„๋ณด - * 3) POST /v1/place/{id}/verify ์‚ฌ๋žŒ์ด ๊ณ ๋ฅธ ํ›„๋ณด๋กœ ๋™์ผ ์—…์†Œ ํ™•์ • + * โ˜… ๋ชฉ๋ก์„ ์ฃผ๋Š” ๊ฑด **๊ณต๊ฐœ ๊ฒ€์ƒ‰**(`GET /v1/place/search`)์ด๋‹ค. ์ธ์ฆ์ด ์—†์–ด ๋กœ๊ทธ์ธ ์ „์—๋„ ๋Œ๊ณ , + * ํ›„๋ณด๋งˆ๋‹ค ์ถ”์ • ์—…์ข…(category)์ด ํ•จ๊ป˜ ์˜จ๋‹ค โ€” ์—…์ข…์„ ์‚ฌ์žฅ๋‹˜์—๊ฒŒ ๋จผ์ € ๋ฌป์ง€ ์•Š๊ธฐ๋กœ ํ•œ ๊ฒฐ์ •์ด + * API ๊นŒ์ง€ ๋‚ด๋ ค์˜จ ์ž๋ฆฌ๋‹ค. ์ธ์ฆ์ด ํ•„์š”ํ•œ ํ›„๋ณด ์กฐํšŒ(verify/candidates)๋Š” ๋ชฉ๋ก์„ ๊ทธ๋ฆฌ๋Š” ๋ฐ + * ์“ฐ์ง€ ์•Š๊ณ , ๊ณ ๋ฅธ ๋’ค **๋„ค์ด๋ฒ„ ํ”Œ๋ ˆ์ด์Šค URL ์„ ์ฐพ๋Š” ๋ฐ๋งŒ** ์“ด๋‹ค. + * + * ํ™•์ •์€ ๋ฐฑ์—”๋“œ ์ˆœ์„œ ๊ทธ๋Œ€๋กœ๋‹ค: + * 1) POST /v1/place ์‚ฌ์—…์žฅ(๊ป๋ฐ๊ธฐ) โ€” ์—…์ข…์ด ์—ฌ๊ธฐ์„œ ์ •ํ•ด์ง„๋‹ค + * 2) GET /v1/place/{id}/verify/candidates?query= ๋„ค์ด๋ฒ„ ํ”Œ๋ ˆ์ด์Šค URL ์ฐพ๊ธฐ + * 3) POST /v1/place/{id}/verify/by-url ๊ทธ URL ๋กœ ๋™์ผ ์—…์†Œ ํ™•์ • * * โ˜… 3 ์„ ํ†ต๊ณผํ•ด์•ผ ์ˆ˜์ง‘์ด ์—ด๋ฆฐ๋‹ค(place.py: "๋™์ผ ์—…์†Œ ๊ฒ€์ฆ๊ณผ ์ฑ„๋„ URL ํ™•์ •์ด ๋๋‚˜์•ผ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๋‹ค"). - * ๊ทธ๋ž˜์„œ ์ง์ ‘ ์ž…๋ ฅ ๊ฒฝ๋กœ๋Š” ์ˆ˜์ง‘ ์—†์ด ์‚ฌ์žฅ๋‹˜ ์ž…๋ ฅ๋งŒ์œผ๋กœ ์‚ฌ์ดํŠธ๋ฅผ ๋งŒ๋“ ๋‹ค โ€” ๊ฒ€์ฆ์„ ์šฐํšŒํ•˜์ง€ ์•Š๋Š”๋‹ค. + * ๊ทธ๋ž˜์„œ ๋กœ๊ทธ์ธ ์ „ ๊ฒฝ๋กœ๋Š” ์ˆ˜์ง‘ ์—†์ด ์‚ฌ์žฅ๋‹˜์ด ๊ณ ๋ฅธ ์ƒํ˜ธยท์ฃผ์†Œ๋งŒ์œผ๋กœ ์‚ฌ์ดํŠธ๋ฅผ ๋งŒ๋“ ๋‹ค โ€” ๊ฒ€์ฆ์„ ์šฐํšŒํ•˜์ง€ ์•Š๋Š”๋‹ค. */ -export function usePlaceSearch(industry: IndustryType, existingPlaceId: string | null = null) { +export function usePlaceSearch(existingPlaceId: string | null = null) { const [state, setState] = useState(INITIAL); const [isConfirming, setIsConfirming] = useState(false); /** @@ -88,8 +88,10 @@ export function usePlaceSearch(industry: IndustryType, existingPlaceId: string | * ๋˜์‚ด์•„๋‚œ ํ™”๋ฉด์ด ๊ฐ™์€ ๊ฐ€๊ฒŒ๋ฅผ ํ•œ ๋ฒˆ ๋” ๋งŒ๋“ ๋‹ค. */ const placeIdRef = useRef(existingPlaceId); + /** ๊ทธ ์‚ฌ์—…์žฅ์„ ๋งŒ๋“ค ๋•Œ ์“ด ์—…์ข…. ์—…์ข…์ด ๋ฐ”๋€Œ๋ฉด ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค(์•„๋ž˜ ensurePlace). */ + const placeCategoryRef = useRef(null); const inflight = useRef(null); - /** ๋งˆ์ง€๋ง‰์œผ๋กœ ๊ฒ€์ƒ‰ํ•œ ์ƒํ˜ธ. URL ํ™•์ • ๋•Œ ์‚ฌ์—…์žฅ ๊ป๋ฐ๊ธฐ ์ด๋ฆ„์œผ๋กœ ์“ด๋‹ค. */ + /** ๋งˆ์ง€๋ง‰์œผ๋กœ ํ™•์ • ์‹œ๋„ํ•œ ์ƒํ˜ธ. URL ํ™•์ • ๋•Œ ์‚ฌ์—…์žฅ ๊ป๋ฐ๊ธฐ ์ด๋ฆ„์œผ๋กœ ์“ด๋‹ค. */ const nameRef = useRef(''); const reset = useCallback(() => { @@ -98,128 +100,76 @@ export function usePlaceSearch(industry: IndustryType, existingPlaceId: string | setState(INITIAL); }, []); - /** ์‚ฌ์—…์žฅ ๊ป๋ฐ๊ธฐ ํ™•๋ณด. ์ด๋ฏธ ๋งŒ๋“ค์—ˆ์œผ๋ฉด ์ƒํ˜ธ๋งŒ ๋งž์ถฐ ๋‘”๋‹ค(๊ฐ™์€ ์œ„์ €๋“œ์—์„œ ๋‘ ๋ฒˆ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค). */ + /** + * ์‚ฌ์—…์žฅ ๊ป๋ฐ๊ธฐ ํ™•๋ณด. + * + * โ˜… ์—…์ข…์ด ๋‹ฌ๋ผ์กŒ์œผ๋ฉด **์ƒˆ๋กœ ๋งŒ๋“ ๋‹ค.** places.category ๋Š” ์ƒ์„ฑํ•  ๋•Œ๋งŒ ์ •ํ•ด์ง€๊ณ  PATCH ๋กœ ๊ณ ์น  + * ์ˆ˜ ์—†๋‹ค(Req_UpdatePlace ์— category ๊ฐ€ ์—†๋‹ค). ๊ทธ๋Œ€๋กœ ์žฌ์‚ฌ์šฉํ•˜๋ฉด ์นดํŽ˜๋กœ ๋งŒ๋“  ์‚ฌ์—…์žฅ์— + * ์ˆ™๋ฐ• ์Šคํ‚ค๋งˆ๋กœ ์ˆ˜์ง‘์ด ๋Œ๊ณ  JSON-LD ํƒ€์ž…๋„ ์–ด๊ธ‹๋‚œ๋‹ค โ€” ํ™”๋ฉด๋„ ๋นŒ๋“œ๋„ ๋ฉ€์ฉกํ•œ ์ฑ„๋กœ ํ‹€๋ฆฐ๋‹ค. + */ const ensurePlace = useCallback( - async (name: string, signal: AbortSignal): Promise => { - if (placeIdRef.current) { + async (name: string, category: PlaceCategory, signal: AbortSignal): Promise => { + const reusable = + placeIdRef.current !== null && + (placeCategoryRef.current === null || placeCategoryRef.current === category); + if (reusable && placeIdRef.current) { // PATCH ๋Š” ์ทจ์†Œ ์‹ ํ˜ธ๋ฅผ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค(์ƒ์„ฑ๋ฌผ ์‹œ๊ทธ๋‹ˆ์ฒ˜) โ€” ๋’ค์—์„œ signal ๋กœ ๊ฒฐ๊ณผ๋ฅผ ๋ฒ„๋ฆฐ๋‹ค. await updatePlace(placeIdRef.current, {name}); if (signal.aborted) return null; return placeIdRef.current; } - const created = await createPlace( - {name, category: INDUSTRY_TO_CATEGORY[industry]}, - undefined, - signal, - ); + const created = await createPlace({name, category}, undefined, signal); placeIdRef.current = created.place?.place_id ?? null; + placeCategoryRef.current = category; return placeIdRef.current; }, - [industry], + [], ); - const search = useCallback( - async (name: string, location: string) => { - nameRef.current = name.trim(); - const query = [name.trim(), location.trim()].filter(Boolean).join(' '); - if (!name.trim()) return; + /** + * ์ƒํ˜ธ๋ช…์œผ๋กœ ํ›„๋ณด๋ฅผ ์ฐพ๋Š”๋‹ค โ€” **๋กœ๊ทธ์ธ ์—†์ด**. + * + * โ˜… ์‚ฌ์—…์žฅ์„ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค. ์•„์ง "๋‚ด ๊ฐ€๊ฒŒ๊ฐ€ ์—ฌ๊ธฐ ์žˆ๋‚˜"๋ฅผ ๋ณด๋Š” ์ค‘์ด๊ณ , ์—ฌ๊ธฐ์„œ ํ–‰์„ ๋งŒ๋“ค๋ฉด + * ๊ฒ€์ƒ‰๋งŒ ํ•ด๋ณด๊ณ  ๋– ๋‚œ ์‚ฌ๋žŒ๋งŒํผ ๋นˆ ์‚ฌ์—…์žฅ์ด ์Œ“์ธ๋‹ค. + */ + const searchPublic = useCallback(async (name: string, location: string) => { + const query = [name.trim(), location.trim()].filter(Boolean).join(' '); + if (query.length < 2) return; - inflight.current?.abort(); - const controller = new AbortController(); - inflight.current = controller; + inflight.current?.abort(); + const controller = new AbortController(); + inflight.current = controller; - // โ˜… ์ž๋™ ๋กœ๊ทธ์ธ์ด ์ผœ์ ธ ์žˆ์œผ๋ฉด ๋๋‚  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ๋‹ค. ์ด๊ฑธ ์•ˆ ๊ธฐ๋‹ค๋ฆฌ๋ฉด ํŽ˜์ด์ง€๋ฅผ ์—ด์ž๋งˆ์ž - // ๋ˆ„๋ฅธ ๊ฒ€์ƒ‰์ด ํ† ํฐ ์—†์ด ๋‚˜๊ฐ€ '๋กœ๊ทธ์ธ ๋งŒ๋ฃŒ'๋กœ ๋–จ์–ด์ง„๋‹ค โ€” ๋งŒ๋ฃŒ๊ฐ€ ์•„๋‹ˆ๋ผ ๊ฒฝํ•ฉ์ด๋‹ค. - await ensureAutoSession(); + setState({...INITIAL, phase: 'searching'}); - // โ˜… ํ† ํฐ์ด ์—†์œผ๋ฉด ํ™”๋ฉด(Step2)์ด ๊ฒ€์ƒ‰์„ ๋ถ€๋ฅด์ง€ ์•Š๊ณ  ์ž…๋ ฅ๊ฐ’์œผ๋กœ ๋„˜์–ด๊ฐ„๋‹ค โ€” 2๋‹จ๊ณ„๋Š” ๋กœ๊ทธ์ธ ๋ฒฝ์ด ์•„๋‹ˆ๋‹ค. - // ๊ทธ๋ž˜๋„ ์—ฌ๊ธฐ ๋„๋‹ฌํ–ˆ๋‹ค๋ฉด ์„ธ์…˜์ด ๋„์ค‘์— ๋Š๊ธด ๊ฒƒ์ด๋ฏ€๋กœ, ๋กœ๊ทธ์ธ์„ ์š”๊ตฌํ•˜์ง€ ๋ง๊ณ  ์กฐ์šฉํžˆ ๋ฌผ๋Ÿฌ๋‚œ๋‹ค. - if (!getAccessToken()) { - setState({...INITIAL}); - return; - } + try { + const res = await searchPlacesPublic({q: query}, undefined, controller.signal); + if (controller.signal.aborted) return; - setState({...INITIAL, phase: 'searching'}); - - try { - const placeId = await ensurePlace(name.trim(), controller.signal); - if (!placeId) throw new Error('์‚ฌ์—…์žฅ์„ ๋งŒ๋“ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.'); - - const res = await verifyCandidates(placeId, {query}, undefined, controller.signal); - if (controller.signal.aborted) return; - - const source = res.source ?? null; - setState({ - phase: 'done', - candidates: res.candidates ?? [], - outcome: res.outcome ?? '', - autoSelectable: res.auto_selectable ?? false, - source, - sourceLabel: source ? (SOURCE_LABEL[source] ?? '์™ธ๋ถ€ ์žฅ์†Œ DB') : '์™ธ๋ถ€ ์žฅ์†Œ DB', - unavailableReason: '', - }); - } catch (error) { - if (controller.signal.aborted) return; - notifyApiError(error, '์ƒํ˜ธ๋ฅผ ๊ฒ€์ƒ‰ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.'); + // โ˜… ์ด ์—”๋“œํฌ์ธํŠธ์˜ ๊ฑฐ์ ˆ์€ HTTP 200 + result.success=false ๋กœ ์˜จ๋‹ค(RemoveNoneResponse). + // ์ƒํƒœ์ฝ”๋“œ๋งŒ ๋ณด๋ฉด "ํ›„๋ณด 0๊ฑด"๊ณผ "๋ถ„๋‹น 20ํšŒ ์ดˆ๊ณผ"๊ฐ€ ๊ฐ™์€ ํ™”๋ฉด์ด ๋œ๋‹ค. + if (res.result?.success === false) { setState({ ...INITIAL, phase: 'unavailable', unavailableReason: - '์žฅ์†Œ DB ๋ฅผ ๋ถ€๋ฅด์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฐฑ์—”๋“œ๊ฐ€ ๋–  ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์‹œ๊ฑฐ๋‚˜, ์ง์ ‘ ์ž…๋ ฅ์œผ๋กœ ์ง„ํ–‰ํ•ด ์ฃผ์„ธ์š”.', + describeError(res.result.desc) ?? '์ง€๋„ ๊ฒ€์ƒ‰์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.', }); + return; } - }, - [ensurePlace], - ); - /** - * ์‚ฌ๋žŒ์ด ๊ณ ๋ฅธ ํ›„๋ณด๋ฅผ ์ด ์‚ฌ์—…์žฅ์˜ ์‹ ์›์œผ๋กœ ํ™•์ •ํ•œ๋‹ค(POST /verify). - * ์„ฑ๊ณตํ•˜๋ฉด ํ™”๋ฉด์— ์–น์„ ์‹ ์›์„ ๋Œ๋ ค์ค€๋‹ค โ€” ์‹คํŒจํ•˜๋ฉด null. - */ - const confirm = useCallback( - async (candidate: PlaceCandidate): Promise => { - const placeId = placeIdRef.current; - const address = candidate.road_address ?? candidate.address ?? ''; - if (!placeId) return null; - - setIsConfirming(true); - try { - await verifyPlace(placeId, { - source: state.source ?? ExternalPlaceSource.KAKAO, - // ๋„ค์ด๋ฒ„๋Š” ๊ณ ์œ  id ๋ฅผ ์ฃผ์ง€ ์•Š๋Š”๋‹ค โ€” ๊ทธ ๊ฒฝ์šฐ ์ƒํ˜ธ๋ช… + ๋„๋กœ๋ช…์ฃผ์†Œ๊ฐ€ ์ค‘๋ณต ํŒ์ • ํ‚ค๋‹ค. - external_place_id: candidate.external_place_id ?? '', - road_address: candidate.road_address ?? null, - address: candidate.address ?? null, - phone: candidate.phone ?? null, - latitude: candidate.latitude ?? null, - longitude: candidate.longitude ?? null, - place_url: candidate.place_url ?? null, - }); - - // โ˜… ์ƒํ˜ธ๋„ ํ›„๋ณด ์ชฝ์œผ๋กœ ๋งž์ถ˜๋‹ค. ์‚ฌ์žฅ๋‹˜์ด ๊ทธ ์ด๋ฆ„์ด ์ ํžŒ ์นด๋“œ๋ฅผ ๋ณด๊ณ  "์ด ๊ฐ€๊ฒŒ์˜ˆ์š”"๋ฅผ - // ๋ˆŒ๋ €๋Š”๋ฐ DB ์—๋Š” ๊ฒ€์ƒ‰์–ด๋กœ ์นœ ์ด๋ฆ„์ด ๋‚จ์œผ๋ฉด, ํ™”๋ฉด๊ณผ ์„œ๋ฒ„๊ฐ€ ๋‹ค๋ฅธ ์ƒํ˜ธ๋ฅผ ๋“ค๊ณ  ์žˆ๊ฒŒ ๋œ๋‹ค. - const officialName = candidate.name?.trim(); - if (officialName) await updatePlace(placeId, {name: officialName}); - - return { - placeId, - name: candidate.name ?? '', - address, - phone: candidate.phone ?? undefined, - origin: 'external', - sourceLabel: state.sourceLabel || '์™ธ๋ถ€ ์žฅ์†Œ DB', - externalPlaceId: candidate.external_place_id ?? undefined, - placeUrl: candidate.place_url ?? undefined, - }; - } catch (error) { - notifyApiError(error, '์ด ๊ฐ€๊ฒŒ๋กœ ํ™•์ •ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.'); - return null; - } finally { - setIsConfirming(false); - } - }, - [state.source, state.sourceLabel], - ); + setState({...INITIAL, phase: 'done', items: res.items ?? []}); + } catch (error) { + if (controller.signal.aborted) return; + notifyApiError(error, '์ƒํ˜ธ๋ฅผ ๊ฒ€์ƒ‰ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.'); + setState({ + ...INITIAL, + phase: 'unavailable', + unavailableReason: + '์žฅ์†Œ DB ๋ฅผ ๋ถ€๋ฅด์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฐฑ์—”๋“œ๊ฐ€ ๋–  ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์‹œ๊ฑฐ๋‚˜, ์ง์ ‘ ์ž…๋ ฅ์œผ๋กœ ์ง„ํ–‰ํ•ด ์ฃผ์„ธ์š”.', + }); + } + }, []); /** * ๋„ค์ด๋ฒ„ ํ”Œ๋ ˆ์ด์Šค URL ๋กœ ํ™•์ •ํ•œ๋‹ค. @@ -230,10 +180,14 @@ export function usePlaceSearch(industry: IndustryType, existingPlaceId: string | * ์ƒํ˜ธยท์ฃผ์†Œ๋Š” ์„œ๋ฒ„๊ฐ€ ๊ทธ URL ์—์„œ ์ฝ์–ด ์˜จ๋‹ค(์†์œผ๋กœ ์˜ฎ๊ฒจ ์ ๊ฒŒ ํ•˜๋ฉด ์˜คํƒ€๊ฐ€ ๋‚จ์˜ ๊ฐ€๊ฒŒ๊ฐ€ ๋œ๋‹ค). */ const confirmByUrl = useCallback( - async (url: string): Promise => { + async (url: string, industry: IndustryType): Promise => { setIsConfirming(true); try { - const placeId = await ensurePlace(nameRef.current || '๋‚ด ๊ฐ€๊ฒŒ', new AbortController().signal); + const placeId = await ensurePlace( + nameRef.current || '๋‚ด ๊ฐ€๊ฒŒ', + INDUSTRY_TO_CATEGORY[industry], + new AbortController().signal, + ); if (!placeId) return null; const res = await verifyPlaceByUrl(placeId, {url}); const place = res.place; @@ -261,20 +215,81 @@ export function usePlaceSearch(industry: IndustryType, existingPlaceId: string | ); /** - * ๋ชฉ๋ก์— ์—†๊ฑฐ๋‚˜ ์žฅ์†Œ DB ๋ฅผ ๋ชป ๋ถ€๋ฅผ ๋•Œ โ€” ์‚ฌ์žฅ๋‹˜์ด ์ง์ ‘ ๋Œ„ ๊ฐ’์œผ๋กœ ์‹ ์›์„ ์„ธ์šด๋‹ค. + * ๋ชฉ๋ก์— ์—†๊ฑฐ๋‚˜ ๋กœ๊ทธ์ธ ์ „์ด๋ผ ๊ฒ€์ฆ์„ ๋ชป ๊ฑธ ๋•Œ โ€” ์‚ฌ์žฅ๋‹˜์ด ๊ณ ๋ฅธ ๊ฐ’์œผ๋กœ ์‹ ์›๋งŒ ์„ธ์šด๋‹ค. * * โ˜… ๋™์ผ ์—…์†Œ ๊ฒ€์ฆ(POST /verify)์„ ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๊ฒ€์ฆ ์—†์ด ์ˆ˜์ง‘์„ ์—ด๋ฉด ๋‚จ์˜ ๊ฐ€๊ฒŒ URL ์„ - * ๊ธ์„ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ด ๊ฒฝ๋กœ๋Š” ์ˆ˜์ง‘ ์—†์ด ์ง์ ‘ ์ž…๋ ฅํ•œ ์ •๋ณด๋กœ๋งŒ ์‚ฌ์ดํŠธ๋ฅผ ๋งŒ๋“ ๋‹ค. + * ๊ธ์„ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ด ๊ฒฝ๋กœ๋Š” ์ˆ˜์ง‘ ์—†์ด ์ด ๊ฐ’๋“ค๋กœ๋งŒ ์‚ฌ์ดํŠธ๋ฅผ ๋งŒ๋“ ๋‹ค. */ - const confirmManual = useCallback((name: string, address: string): ConfirmedIdentity => { - return { + const confirmManual = useCallback( + (name: string, address: string, sourceLabel = '์ง์ ‘ ์ž…๋ ฅ'): ConfirmedIdentity => ({ placeId: placeIdRef.current, name: name.trim(), address: address.trim(), origin: 'owner', - sourceLabel: '์ง์ ‘ ์ž…๋ ฅ', - }; - }, []); + sourceLabel, + }), + [], + ); - return {...state, isConfirming, search, confirm, confirmByUrl, confirmManual, reset}; + /** + * ๊ณต๊ฐœ ๊ฒ€์ƒ‰์—์„œ ๊ณ ๋ฅธ ํ•œ ๊ฑด์„ ์ด ์œ„์ €๋“œ์˜ ์‚ฌ์—…์žฅ์œผ๋กœ ํ™•์ •ํ•œ๋‹ค. + * + * โ˜… ๋กœ๊ทธ์ธ ์ „์—๋Š” ์„œ๋ฒ„๋ฅผ ๋ถ€๋ฅด์ง€ ์•Š๋Š”๋‹ค. ๋กœ๊ทธ์ธ ๊ด€๋ฌธ์€ ์—๋””ํ„ฐ ์ง„์ž… ํ•˜๋‚˜์ด๊ณ (b94daa9), + * ์‚ฌ์—…์žฅ ์ƒ์„ฑยท๊ฒ€์ฆ์€ ์ „๋ถ€ ํ† ํฐ์„ ์š”๊ตฌํ•œ๋‹ค โ€” ์—ฌ๊ธฐ์„œ ๋ถ€๋ฅด๋ฉด ์ฒซ ํ™”๋ฉด์ด ๋กœ๊ทธ์ธ ๋ฒฝ์ด ๋œ๋‹ค. + * ๊ณ ๋ฅธ ํ›„๋ณด์˜ **๊ณต์‹ ์ƒํ˜ธยท์ฃผ์†Œ**๋กœ ์‹ ์›์„ ์„ธ์šฐ๊ณ  ๋„˜์–ด๊ฐ„๋‹ค(๊ฒ€์ฆ์€ ๋กœ๊ทธ์ธ ๋’ค์— ๋‹ค์‹œ ๊ฑธ ์ˆ˜ ์žˆ๋‹ค). + * โ˜… ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฉด ๊ณต๊ฐœ ํ›„๋ณด์— ์—†๋Š” ๊ฒƒ(๋„ค์ด๋ฒ„ ํ”Œ๋ ˆ์ด์Šค URL)์„ ์ธ์ฆ ๊ฒฝ๋กœ์—์„œ ํ•œ ๋ฒˆ ๋” ์ฐพ์•„ + * URL ํ™•์ •๊นŒ์ง€ ๊ฐ„๋‹ค โ€” ๊ทธ๋ž˜์•ผ ์ˆ˜์ง‘์ด ์—ด๋ฆฐ๋‹ค. + */ + const confirmPick = useCallback( + async ( + pick: {name: string; address: string}, + industry: IndustryType, + ): Promise => { + nameRef.current = pick.name.trim(); + setState((prev) => ({...prev, pickError: ''})); + + // โ˜… ์ž๋™ ๋กœ๊ทธ์ธ์ด ์ผœ์ ธ ์žˆ์œผ๋ฉด ๋๋‚  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ๋‹ค. ์ด๊ฑธ ์•ˆ ๊ธฐ๋‹ค๋ฆฌ๋ฉด ํŽ˜์ด์ง€๋ฅผ ์—ด์ž๋งˆ์ž + // ๋ˆ„๋ฅธ ํ™•์ •์ด ํ† ํฐ ์—†์ด ๋‚˜๊ฐ€ '๋กœ๊ทธ์ธ ๋งŒ๋ฃŒ'๋กœ ๋–จ์–ด์ง„๋‹ค โ€” ๋งŒ๋ฃŒ๊ฐ€ ์•„๋‹ˆ๋ผ ๊ฒฝํ•ฉ์ด๋‹ค. + await ensureAutoSession(); + if (!getAccessToken()) return confirmManual(pick.name, pick.address, '์ง€๋„ ๊ฒ€์ƒ‰'); + + setIsConfirming(true); + try { + const controller = new AbortController(); + const placeId = await ensurePlace( + pick.name.trim(), + INDUSTRY_TO_CATEGORY[industry], + controller.signal, + ); + if (!placeId) throw new Error('์‚ฌ์—…์žฅ์„ ๋งŒ๋“ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.'); + + // ์ƒํ˜ธ๋งŒ์œผ๋กœ๋Š” ๋™๋ช… ์—…์†Œ๊ฐ€ ์„ž์ธ๋‹ค โ€” ์‚ฌ์žฅ๋‹˜์ด ๊ณ ๋ฅธ ๊ทธ ์ฃผ์†Œ๊นŒ์ง€ ๋ถ™์—ฌ ์ขํžŒ๋‹ค. + const query = [pick.name.trim(), pick.address.trim()].filter(Boolean).join(' '); + const res = await verifyCandidates(placeId, {query}, undefined, controller.signal); + const candidates = res.candidates ?? []; + const hit = + candidates.find( + (c) => c.naver_place_url && normalizeName(c.name) === normalizeName(pick.name), + ) ?? candidates.find((c) => c.naver_place_url); + + if (!hit?.naver_place_url) { + setState((prev) => ({ + ...prev, + pickError: + '์ด ๊ฐ€๊ฒŒ์˜ ๋„ค์ด๋ฒ„ ํ”Œ๋ ˆ์ด์Šค๋ฅผ ์ž๋™์œผ๋กœ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜์—์„œ ์ง€๋„ ์ฃผ์†Œ๋ฅผ ๋ถ™์—ฌ๋„ฃ์–ด ์ฃผ์„ธ์š”.', + })); + return null; + } + return await confirmByUrl(hit.naver_place_url, industry); + } catch (error) { + notifyApiError(error, '์ด ๊ฐ€๊ฒŒ๋กœ ํ™•์ •ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.'); + return null; + } finally { + setIsConfirming(false); + } + }, + [confirmByUrl, confirmManual, ensurePlace], + ); + + return {...state, isConfirming, searchPublic, confirmPick, confirmByUrl, confirmManual, reset}; } diff --git a/solution/frontend/src/features/onboarding/wizardUrl.ts b/solution/frontend/src/features/onboarding/wizardUrl.ts new file mode 100644 index 0000000..5190181 --- /dev/null +++ b/solution/frontend/src/features/onboarding/wizardUrl.ts @@ -0,0 +1,96 @@ +import {useCallback} from 'react'; +import {useSearchParams} from 'react-router'; + +/** + * ์œ„์ €๋“œ ๋‹จ๊ณ„๋Š” **์ฃผ์†Œ์ฐฝ์ด ์†Œ์œ ํ•œ๋‹ค** โ€” `/builder?step=<์ด๋ฆ„>`. + * + * โ˜… ์˜ˆ์ „์—” ์Šคํ† ์–ด(zustand)์˜ `step` ์ด ํ™”๋ฉด์„ ๊ณจ๋ž๋‹ค. ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ณด๊ธฐ์—” ์ฃผ์†Œ๊ฐ€ ํ•œ ๋ฒˆ๋„ ์•ˆ ๋ฐ”๋€Œ๋‹ˆ + * ๋’ค๋กœ๊ฐ€๊ธฐ๊ฐ€ ์ด์ „ ๋‹จ๊ณ„๊ฐ€ ์•„๋‹ˆ๋ผ ์œ„์ €๋“œ **๋ฐ–์œผ๋กœ** ๋‚˜๊ฐ”๊ณ , ๋ถ๋งˆํฌํ•œ ์ฃผ์†Œ๋Š” ์–ธ์ œ๋‚˜ ์ฒซ ํ™”๋ฉด์œผ๋กœ ์—ด๋ ธ๋‹ค. + * โ˜… ๋ฒˆํ˜ธ๊ฐ€ ์•„๋‹ˆ๋ผ ์ด๋ฆ„์„ ์“ด๋‹ค. ๋‹จ๊ณ„๋Š” ๋Š˜๊ณ  ์ค€๋‹ค(์—…์ข… ์„ ํƒ์ด ์ฒซ ํ™”๋ฉด์—์„œ ๋น ์ง€๋ฉด์„œ ๋’ค๊ฐ€ ์ „๋ถ€ ํ•œ ์นธ + * ๋‹น๊ฒจ์กŒ๋‹ค) โ€” ๋ฒˆํ˜ธ๋ฅผ ์ฃผ์†Œ์— ๋ฐ•์•„ ๋‘๋ฉด ๊ทธ๋‚  ์ดํ›„ ์˜› ๋ถ๋งˆํฌยท๋žœ๋”ฉ ๋งํฌ๊ฐ€ ์กฐ์šฉํžˆ ๋‹ค๋ฅธ ํ™”๋ฉด์„ ์—ฐ๋‹ค. + */ +export const WIZARD_STEPS = [ + /** ์ƒํ˜ธ๋ช…์œผ๋กœ ๋‚ด ๊ฐ€๊ฒŒ ์ฐพ๊ธฐ โ€” ์œ„์ €๋“œ์˜ ์‹œ์ž‘์ ์ด๋‹ค. */ + 'search', + /** ์—…์ข… ์ง์ ‘ ๊ณ ๋ฅด๊ธฐ. ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—…์ข…์„ ๋ชป ์ •ํ–ˆ์„ ๋•Œ, ๋˜๋Š” [๋ฐ”๊พธ๊ธฐ] ๋กœ ๋“ค์–ด์˜จ๋‹ค. */ + 'industry', + 'collect', + 'template', + 'generating', + 'editor', +] as const; + +export type WizardStep = (typeof WIZARD_STEPS)[number]; + +/** ์œ„์ €๋“œ๊ฐ€ ๋๋‚˜๊ณ  ํŽธ์ง‘๊ธฐ๋กœ ๋„˜์–ด๊ฐ€๋Š” ๋‹จ๊ณ„. ์ˆซ์ž๋ฅผ ํ™”๋ฉด๋งˆ๋‹ค ์™ธ์šฐ์ง€ ์•Š๊ฒŒ ํ•œ ๊ณณ์— ๋‘”๋‹ค. */ +export const EDITOR_STEP: WizardStep = 'editor'; + +/** + * ์ง„ํ–‰ ํ‘œ์‹œ(WizardSteps)์— ์ฐํžˆ๋Š” ๋ฒˆํ˜ธ. + * + * โ˜… ์—…์ข… ํ™”๋ฉด์€ '๋‚ด ๊ฐ€๊ฒŒ ํ™•์ธ' ์•ˆ์˜ **๊ฐˆ๋ž˜**๋ผ ๊ฐ™์€ 1๋ฒˆ์ด๋‹ค. ์—…์ข…์€ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์ •ํ•˜๊ณ  + * ๋ชป ์ •ํ–ˆ์„ ๋•Œ๋งŒ ์‚ฌ๋žŒ์—๊ฒŒ ๋ฌป๋Š” ๊ฒƒ์ด๋ผ, ๋ณ„๋„ ๋‹จ๊ณ„๋กœ ์„ธ๋ฉด ์‚ฌ์žฅ๋‹˜์€ ์•ˆ ๊ฒช์„ ์ˆ˜๋„ ์žˆ๋Š” ๋‹จ๊ณ„๋ฅผ + * ์ง„ํ–‰ ํ‘œ์‹œ์—์„œ ๊ณ„์† ๋ณด๊ฒŒ ๋œ๋‹ค. + * โ˜… ์ƒ์„ฑ ํ™”๋ฉด์€ ์ง„ํ–‰ ํ‘œ์‹œ๋ฅผ ๊ทธ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค(0 ์€ "ํ‘œ์‹œํ•  ๋ฒˆํ˜ธ๊ฐ€ ์—†๋‹ค"๋Š” ๋œป์ด๋‹ค). + */ +export const STEP_NUMBER: Record = { + search: 1, + industry: 1, + collect: 2, + template: 3, + generating: 0, + editor: 0, +}; + +function parseStep(value: string | null): WizardStep | null { + return WIZARD_STEPS.includes(value as WizardStep) ? (value as WizardStep) : null; +} + +/** + * ์ฃผ์†Œ์ฐฝ์— `step` ์ด ์—†์„ ๋•Œ ์—ด๋ฆฌ๋Š” ํ™”๋ฉด. + * + * โ˜… `?placeId=` ๋”ฅ๋งํฌ๋Š” **ํŽธ์ง‘ํ•˜๋Ÿฌ ์˜จ ๊ฒƒ**์ด๋‹ค(๋‚ด ์‚ฌ์ดํŠธ ๋ชฉ๋ก์˜ [์‚ฌ์ดํŠธ ํŽธ์ง‘]) โ€” ์œ„์ €๋“œ๋ฅผ + * ๋‹ค์‹œ ๊ฑท๊ฒŒ ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์œ„์ €๋“œ๊ฐ€ ๋ฐฉ๊ธˆ ๋งŒ๋“  ์‚ฌ์—…์žฅ(`flow=onboarding`)๋งŒ ์˜ˆ์™ธ๋‹ค: + * ๊ทธ๊ฑด ์•„์ง ์ˆ˜์ง‘ยทํ…œํ”Œ๋ฆฟ์„ ์ง€๋‚˜๋Š” ์ค‘์ด๊ณ  ๊ทธ๋•Œ๋Š” step ์ด ํ•ญ์ƒ ํ•จ๊ป˜ ๋ถ™์–ด ์žˆ๋‹ค. + */ +export function defaultStep(params: URLSearchParams): WizardStep { + return params.get('placeId') && params.get('flow') !== 'onboarding' ? EDITOR_STEP : 'search'; +} + +interface GoOptions { + /** ํžˆ์Šคํ† ๋ฆฌ์— ์Œ“์ง€ ์•Š๋Š”๋‹ค โ€” ํ™”๋ฉด์ด ๋ฐ”๋€Œ์ง€ ์•Š๋Š” ์ •์ •(์ฃผ์†Œ์ฐฝ ์ •๋ฆฌ)์ผ ๋•Œ๋งŒ ์“ด๋‹ค. */ + replace?: boolean; + /** ๊ฐ™์€ ์ด๋™์—์„œ ํ•จ๊ป˜ ๋ฐ”๊ฟ€ ์ฟผ๋ฆฌ. null ์ด๋ฉด ์ง€์šด๋‹ค. ๋‹จ๊ณ„์™€ ํ•จ๊ป˜ ํ•œ ๋ฒˆ์— ๋ฐ”๊ฟ”์•ผ ์ค‘๊ฐ„ ์ฃผ์†Œ๊ฐ€ ์•ˆ ์ƒ๊ธด๋‹ค. */ + params?: Record; +} + +/** + * ์ง€๊ธˆ ๋‹จ๊ณ„์™€, ๋‹จ๊ณ„๋ฅผ ์˜ฎ๊ธฐ๋Š” ํ•จ์ˆ˜. + * + * โ˜… ๋‹ค๋ฅธ ์ฟผ๋ฆฌ(`placeId`ยท`flow`ยท`q`)๋Š” ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ณ  `step` ๋งŒ ๊ฐˆ์•„ ๋ผ์šด๋‹ค โ€” ๋‹จ๊ณ„๋ฅผ ์˜ฎ๊ธธ ๋•Œ๋งˆ๋‹ค + * ์‚ฌ์—…์žฅ ๋”ฅ๋งํฌ๊ฐ€ ๋–จ์–ด์ ธ ๋‚˜๊ฐ€๋ฉด ์ƒˆ๋กœ๊ณ ์นจ ํ•œ ๋ฒˆ์— ์–ด๋А ๊ฐ€๊ฒŒ์˜€๋Š”์ง€๊ฐ€ ์‚ฌ๋ผ์ง„๋‹ค. + */ +export function useWizardStep(): [WizardStep, (next: WizardStep, options?: GoOptions) => void] { + const [searchParams, setSearchParams] = useSearchParams(); + const step = parseStep(searchParams.get('step')) ?? defaultStep(searchParams); + + const go = useCallback( + (next: WizardStep, options?: GoOptions) => { + setSearchParams( + (prev) => { + const params = new URLSearchParams(prev); + params.set('step', next); + for (const [key, value] of Object.entries(options?.params ?? {})) { + if (value === null) params.delete(key); + else params.set(key, value); + } + return params; + }, + {replace: options?.replace}, + ); + }, + [setSearchParams], + ); + + return [step, go]; +} diff --git a/solution/frontend/src/hooks/usePlaceSync.ts b/solution/frontend/src/hooks/usePlaceSync.ts index 78abb68..5f0be43 100644 --- a/solution/frontend/src/hooks/usePlaceSync.ts +++ b/solution/frontend/src/hooks/usePlaceSync.ts @@ -17,7 +17,7 @@ import {useBuilderStore} from '@/stores/builder'; import type {ApplyPlaceOptions} from '@/stores/builderTypes'; export function usePlaceSync(placeId: string | null, options?: ApplyPlaceOptions) { - const enterEditor = options?.enterEditor ?? true; + const isOnboarding = options?.isOnboarding ?? false; const enabled = Boolean(placeId); const id = placeId ?? ''; @@ -42,8 +42,8 @@ export function usePlaceSync(placeId: string | null, options?: ApplyPlaceOptions /** * โ˜… useEffect ๊ฐ€ ์•„๋‹ˆ๋ผ useLayoutEffect ๋‹ค. - * ์ผ๋ฐ˜ effect ๋Š” ํŽ˜์ธํŠธ ๋’ค์— ๋Œ์•„์„œ, ์‘๋‹ต์ด ๋„์ฐฉํ•œ ํ”„๋ ˆ์ž„์— ์œ„์ €๋“œ 1๋‹จ๊ณ„๊ฐ€ ํ•œ ๋ฒˆ - * ๊ทธ๋ ค์กŒ๋‹ค๊ฐ€ ํŽธ์ง‘๊ธฐ๋กœ ๋ฐ”๋€๋‹ค(๊นœ๋นก์ž„). ์Šคํ† ์–ด์— ์–น๋Š” ์ผ์€ ํŽ˜์ธํŠธ ์ „์— ๋๋‚˜์•ผ ํ•œ๋‹ค. + * ์ผ๋ฐ˜ effect ๋Š” ํŽ˜์ธํŠธ ๋’ค์— ๋Œ์•„์„œ, ์‘๋‹ต์ด ๋„์ฐฉํ•œ ํ”„๋ ˆ์ž„์— ๋นˆ ํŽธ์ง‘๊ธฐ๊ฐ€ ํ•œ ๋ฒˆ ๊ทธ๋ ค์กŒ๋‹ค๊ฐ€ + * ๊ฐ’์ด ์ฑ„์›Œ์ง„๋‹ค(๊นœ๋นก์ž„). ์Šคํ† ์–ด์— ์–น๋Š” ์ผ์€ ํŽ˜์ธํŠธ ์ „์— ๋๋‚˜์•ผ ํ•œ๋‹ค. */ useLayoutEffect(() => { if (!placeId) { @@ -53,8 +53,10 @@ export function usePlaceSync(placeId: string | null, options?: ApplyPlaceOptions if (!place) return; // fact ๊ฐ€ ์•„์ง ์•ˆ ์™”์–ด๋„ ๋จผ์ € ์˜จ place ๋กœ ์ƒํ˜ธยท์ฃผ์†Œ๋ถ€ํ„ฐ ์–น๋Š”๋‹ค. // fact/์Šคํ‚ค๋งˆ๊ฐ€ ๋„์ฐฉํ•˜๋ฉด ์ด effect ๊ฐ€ ํ•œ ๋ฒˆ ๋” ๋Œ์•„ ์ •๋ณด ํ‘œ๋ฅผ ์ฑ„์šด๋‹ค. - applyPlace(toLivePlaceInput(placeId, place, facts ?? [], specs ?? [], media ?? []), {enterEditor}); - }, [placeId, place, facts, specs, media, applyPlace, clearPlace, enterEditor]); + applyPlace(toLivePlaceInput(placeId, place, facts ?? [], specs ?? [], media ?? []), { + isOnboarding, + }); + }, [placeId, place, facts, specs, media, applyPlace, clearPlace, isOnboarding]); /** * ์„œ๋ฒ„์— ์ €์žฅ๋œ ๋””์ž์ธ์„ ์–น๋Š”๋‹ค. diff --git a/solution/frontend/src/lib/errorMessages.ts b/solution/frontend/src/lib/errorMessages.ts index 701c703..b5661be 100644 --- a/solution/frontend/src/lib/errorMessages.ts +++ b/solution/frontend/src/lib/errorMessages.ts @@ -26,6 +26,10 @@ export const ERROR_MESSAGE: Record = { PLACE_NOT_VERIFIED: '๋™์ผ ์—…์†Œ ๊ฒ€์ฆ์„ ๋จผ์ € ๋๋‚ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ฆ ์ „์—๋Š” ์ˆ˜์ง‘ยท๋ฐœํ–‰์ด ์—ด๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.', PLACE_VERIFY_NO_CANDIDATE: '์™ธ๋ถ€ ์žฅ์†Œ ์ •๋ณด์—์„œ ์ด ์ƒํ˜ธ๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.', PLACE_VERIFY_AMBIGUOUS: '๊ฐ™์€ ์ด๋ฆ„์˜ ์—…์†Œ๊ฐ€ ์—ฌ๋Ÿฟ์ž…๋‹ˆ๋‹ค. ์–ด๋А ๊ณณ์ธ์ง€ ๊ณจ๋ผ ์ฃผ์„ธ์š”.', + // ์ƒํ˜ธ๋ช… ๊ณต๊ฐœ ๊ฒ€์ƒ‰(๋กœ๊ทธ์ธ ์ „ ์ฒซ ํ™”๋ฉด)์ด ๋งŒ๋‚˜๋Š” ์„ธ ๊ฐ€์ง€. ์ธ์ฆ์ด ์—†๋Š” ๊ฒฝ๋กœ๋ผ ๋ถ„๋‹น ์ƒํ•œ์ด ๊ฑธ๋ ค ์žˆ๋‹ค. + HTTP_TO_MANY_REQUEST: '๊ฒ€์ƒ‰์„ ๋„ˆ๋ฌด ์ž์ฃผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ๋’ค ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.', + LOCAL_NOT_CONFIGURED: '์ง€๋„ ๊ฒ€์ƒ‰์ด ์•„์ง ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋„ค์ด๋ฒ„ ์ง€๋„ ์ฃผ์†Œ๋ฅผ ๋ถ™์—ฌ๋„ฃ์–ด ์ง„ํ–‰ํ•ด ์ฃผ์„ธ์š”.', + LOCAL_FETCH_FAILED: '์ง€๋„์—์„œ ๊ฐ€๊ฒŒ๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ๋’ค ๋‹ค์‹œ ์‹œ๋„ํ•˜๊ฑฐ๋‚˜ ์ง€๋„ ์ฃผ์†Œ๋ฅผ ๋ถ™์—ฌ๋„ฃ์–ด ์ฃผ์„ธ์š”.', LINK_NOT_CONFIRMED: 'ํ™•์ •ํ•œ ์ฑ„๋„ URL ๋งŒ ์ˆ˜์ง‘ ๋Œ€์ƒ์ด ๋ฉ๋‹ˆ๋‹ค.', // fact diff --git a/solution/frontend/src/pages/BuilderPage.tsx b/solution/frontend/src/pages/BuilderPage.tsx index 621b04b..bedb166 100644 --- a/solution/frontend/src/pages/BuilderPage.tsx +++ b/solution/frontend/src/pages/BuilderPage.tsx @@ -1,25 +1,34 @@ -import {useEffect, useRef} from 'react'; +import {useEffect} from 'react'; import {ArrowLeft, ExternalLink, Loader2, LogOut, TriangleAlert} from 'lucide-react'; import {Link, useSearchParams} from 'react-router'; -import {SiteStatus} from '@o2o/shared'; +import {SiteStatus, type IndustryType} from '@o2o/shared'; import {getAccessToken} from '@/api'; import {EditorSignInGate} from '@/features/auth/EditorSignInGate'; import { + EDITOR_STEP, Step1Industry, Step2PlaceSearch, Step3DataReview, Step4Template, Step5Generating, + useWizardStep, } from '@/features/onboarding'; import {EditorLayout} from '@/features/builder'; import {useAutoLogin} from '@/hooks/useAutoLogin'; import {usePlaceSync} from '@/hooks/usePlaceSync'; import {userLabel, useAuthStore} from '@/stores/auth'; -import {EDITOR_STEP, useBuilderStore} from '@/stores/builder'; +import {useBuilderStore} from '@/stores/builder'; /** ๋ฐœํ–‰ ์‚ฌ์ดํŠธ ๋ Œ๋”๋Ÿฌ์˜ ๊ฐœ๋ฐœ ์„œ๋ฒ„. ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์‹ค์ œ ๋ฐœํ–‰ ์ฃผ์†Œ๋กœ ๋ฐ”๋€๋‹ค. */ const SITE_PREVIEW_URL = import.meta.env.VITE_SITE_PREVIEW_URL ?? window.location.origin; +/** ๋žœ๋”ฉ์ด `?industry=` ๋กœ ๋„˜๊ธธ ์ˆ˜ ์žˆ๋Š” ๊ฐ’. ์ฃผ์†Œ์ฐฝ ๊ฐ’์ด๋ผ ์•„๋ฌด ๋ฌธ์ž์—ด์ด๋‚˜ ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ๋‹ค. */ +const INDUSTRY_VALUES: IndustryType[] = ['stay', 'cafe', 'restaurant', 'clinic']; + +function parseIndustry(value: string | null): IndustryType | null { + return INDUSTRY_VALUES.includes(value as IndustryType) ? (value as IndustryType) : null; +} + /** * "๋ฐœํ–‰๋ณธ ์‚ฌ์ดํŠธ ์—ด๊ธฐ" ๊ฐ€ ํ–ฅํ•  ์ฃผ์†Œ. * @@ -36,32 +45,55 @@ function siteUrl(domain: string | null | undefined): string | null { export function BuilderPage() { useAutoLogin(); /** - * ์–ด๋–ค ์‚ฌ์—…์žฅ์„ ํŽธ์ง‘ํ• ์ง€๋Š” ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์œผ๋กœ ๋ฐ›๋Š”๋‹ค โ€” `/builder?placeId=`. + * โ˜… ์ด ํ™”๋ฉด์ด ๋ฌด์—‡์„ ๊ทธ๋ฆด์ง€๋Š” **์ „๋ถ€ ์ฃผ์†Œ์ฐฝ์ด ์ •ํ•œ๋‹ค.** * - * โ˜… ๋ผ์šฐํŠธ(`/builder/:placeId`)๋กœ ๋ฐ›์ง€ ์•Š๋Š” ์ด์œ : ๋นŒ๋”๋Š” ๋กœ๊ทธ์ธ ์—†์ด ๋„๋Š” ๋ฐ๋ชจ ๊ฒฝ๋กœ์ด๊ณ  - * (router.tsx ์ฃผ์„), placeId ๋Š” ์žˆ์„ ์ˆ˜๋„ ์—†์„ ์ˆ˜๋„ ์žˆ๋Š” ์„ ํƒ๊ฐ’์ด๋‹ค. ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์ด๋ฉด - * ๋ผ์šฐํŠธ๋ฅผ ํ•˜๋‚˜๋„ ์•ˆ ๊ฑด๋“œ๋ฆฌ๊ณ  ๋‘ ๊ฒฝ์šฐ๋ฅผ ๊ฐ™์€ ํ™”๋ฉด์ด ๋ฐ›๋Š”๋‹ค. - * placeId ๊ฐ€ ์—†์œผ๋ฉด ์•„๋ž˜ ํ›…์€ ๋„คํŠธ์›Œํฌ๋ฅผ ํ•œ ๋ฒˆ๋„ ํƒ€์ง€ ์•Š๋Š”๋‹ค โ€” ๋ฐ๋ชจ๋Š” ์ง€๊ธˆ ๊ทธ๋Œ€๋กœ๋‹ค. + * ?step= ์–ด๋А ๋‹จ๊ณ„์ธ๊ฐ€(์—†์œผ๋ฉด wizardUrl.defaultStep ์ด ์ •ํ•œ๋‹ค) + * ?placeId= ์–ด๋–ค ์‚ฌ์—…์žฅ์ธ๊ฐ€ โ€” ๋ผ์šฐํŠธ(`/builder/:placeId`)๋กœ ๋ฐ›์ง€ ์•Š๋Š” ์ด์œ ๋Š” + * ๋นŒ๋”๊ฐ€ ๋กœ๊ทธ์ธ ์—†์ด ๋„๋Š” ๊ฒฝ๋กœ์ด๊ณ  placeId ๋Š” ์žˆ์„ ์ˆ˜๋„ ์—†์„ ์ˆ˜๋„ ์žˆ์–ด์„œ๋‹ค. + * ?flow=onboarding ์œ„์ €๋“œ๊ฐ€ ๋ฐฉ๊ธˆ ๋งŒ๋“  ์‚ฌ์—…์žฅ์ด๋‹ค(๋”ฅ๋งํฌ๋กœ ํŽธ์ง‘ํ•˜๋Ÿฌ ์˜จ ๊ฒƒ๊ณผ ๊ตฌ๋ถ„ํ•œ๋‹ค) + * ?new=1 ?q= ?industry= ๋žœ๋”ฉ์—์„œ ๋„˜์–ด์˜จ ์ž…๊ตฌ. ํ•œ ๋ฒˆ ์ฝ๊ณ  ์ฃผ์†Œ์ฐฝ์—์„œ ์ง€์šด๋‹ค. */ const [searchParams, setSearchParams] = useSearchParams(); + const [step, goToStep] = useWizardStep(); const urlPlaceId = searchParams.get('placeId'); + const isOnboarding = searchParams.get('flow') === 'onboarding'; /** - * `?new=1` ๋กœ ๋“ค์–ด์˜ค๋ฉด ์œ„์ €๋“œ๋ฅผ 1๋‹จ๊ณ„(์—…์ข… ์„ ํƒ)๋ถ€ํ„ฐ ์‹œ์ž‘ํ•œ๋‹ค. + * ๋žœ๋”ฉ์—์„œ ๋„˜์–ด์˜จ ์ž…๊ตฌ๋ฅผ ํ•œ ๋ฒˆ๋งŒ ์ฝ๋Š”๋‹ค โ€” `?new=1` ยท `?q=` ยท `?industry=`. * - * โ˜… ์ €์žฅ๋œ ์ƒํƒœ๋ฅผ ๊ทธ๋Œ€๋กœ ๋‘๋ฉด ์ง€๋‚œ๋ฒˆ ์—๋””ํ„ฐ๊ฐ€ ๋ณต์›๋œ๋‹ค โ€” ์ƒˆ ๊ฐ€๊ฒŒ๋ฅผ ๋งŒ๋“ค๋Ÿฌ ์˜จ ์‚ฌ๋žŒ์—๊ฒŒ๋Š” - * ์ž๊ธฐ๊ฐ€ ๋งŒ๋“  ์  ์—†๋Š” ํ™”๋ฉด์ด ๋œจ๋Š” ์…ˆ์ด๋‹ค. ๋น„์šด ๋’ค์—๋Š” ์ฃผ์†Œ์ฐฝ์—์„œ ํ”Œ๋ž˜๊ทธ๋ฅผ ์ง€์›Œ, - * ์ƒˆ๋กœ๊ณ ์นจํ•  ๋•Œ๋งˆ๋‹ค ์ž‘์—…ํ•˜๋˜ ๋‚ด์šฉ์ด ๋‚ ์•„๊ฐ€์ง€ ์•Š๊ฒŒ ํ•œ๋‹ค. + * โ˜… `?new=1` ์€ ์ €์žฅ๋œ ์ƒํƒœ๋ฅผ ๋น„์šด๋‹ค. ์•ˆ ๋น„์šฐ๋ฉด ์ƒˆ ๊ฐ€๊ฒŒ๋ฅผ ๋งŒ๋“ค๋Ÿฌ ์˜จ ์‚ฌ๋žŒ์—๊ฒŒ ์ง€๋‚œ๋ฒˆ ์—๋””ํ„ฐ๊ฐ€ + * ๋ณต์›๋ผ ๋œฌ๋‹ค. ์ฝ์€ ๋’ค ์ฃผ์†Œ์ฐฝ์—์„œ ์ง€์šฐ๋Š” ์ด์œ ๋Š” ๋‘ ๊ฐ€์ง€๋‹ค โ€” ์ƒˆ๋กœ๊ณ ์นจ๋งˆ๋‹ค ์ž‘์—…ํ•˜๋˜ ๋‚ด์šฉ์ด + * ๋‚ ์•„๊ฐ€์ง€ ์•Š๊ฒŒ, ๊ทธ๋ฆฌ๊ณ  ์‚ฌ์žฅ๋‹˜์ด ํ™”๋ฉด์—์„œ ๊ณ ์นœ ์ƒํ˜ธยท์—…์ข…์„ ๋งํฌ ๊ฐ’์ด ๋„๋กœ ๋ฎ์ง€ ์•Š๊ฒŒ. + * โ˜… **๋‹ค๋ฅธ ์ฟผ๋ฆฌ๋Š” ๋‚จ๊ธด๋‹ค.** ์˜ˆ์ „์—” `setSearchParams({})` ๋กœ ํ†ต์งธ๋กœ ๋น„์› ๋Š”๋ฐ, ๊ทธ๋Ÿฌ๋ฉด + * `?new=1&q=...` ๋กœ ๋“ค์–ด์˜จ ์ƒํ˜ธ๊ฐ€ ์ฝํžˆ๊ธฐ๋„ ์ „์— ์‚ฌ๋ผ์ง„๋‹ค. */ const reset = useBuilderStore((s) => s.reset); + const selectIndustry = useBuilderStore((s) => s.selectIndustry); + const setStoreName = useBuilderStore((s) => s.setStoreName); const isNew = searchParams.get('new') === '1'; + const seedQuery = searchParams.get('q'); + const seedIndustry = searchParams.get('industry'); useEffect(() => { - if (!isNew) return; - reset(); - setSearchParams({}, {replace: true}); - }, [isNew, reset, setSearchParams]); + if (!isNew && seedQuery === null && seedIndustry === null) return; + if (isNew) reset(); + // ์ˆœ์„œ๊ฐ€ ๋’ค์ง‘ํžˆ๋ฉด ์•ˆ ๋œ๋‹ค โ€” selectIndustry ๋Š” ์‹œ๋“œ๋ฅผ ํ†ต์งธ๋กœ ๊ฐˆ์•„ ์ƒํ˜ธ๋ฅผ ๋น„์šด๋‹ค. + const industry = parseIndustry(seedIndustry); + if (industry) selectIndustry(industry); + if (seedQuery) setStoreName(seedQuery); + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev); + next.delete('new'); + next.delete('q'); + next.delete('industry'); + return next; + }, + {replace: true}, + ); + }, [isNew, seedQuery, seedIndustry, reset, selectIndustry, setStoreName, setSearchParams]); + /** - * ์œ„์ €๋“œ 2๋‹จ๊ณ„์—์„œ ํ™•์ •ํ•œ ์‚ฌ์—…์žฅ. ์ฃผ์†Œ์ฐฝ์— placeId ๊ฐ€ ์—†์–ด๋„ ์ด๊ฑธ๋กœ ๋ฐฐ์„ ํ•œ๋‹ค. + * ์œ„์ €๋“œ 1๋‹จ๊ณ„์—์„œ ํ™•์ •ํ•œ ์‚ฌ์—…์žฅ. ์ฃผ์†Œ์ฐฝ์— placeId ๊ฐ€ ์—†์–ด๋„ ์ด๊ฑธ๋กœ ๋ฐฐ์„ ํ•œ๋‹ค. * * โ˜… ์ด๊ฒŒ ์—†์œผ๋ฉด ์œ„์ €๋“œ๋ฅผ ๋๊นŒ์ง€ ๊ฑธ์–ด์˜จ ์‚ฌ์žฅ๋‹˜์ด ์—๋””ํ„ฐ์—์„œ **์—…์ข… ์˜ˆ์‹œ๊ฐ’**์„ ๋ณธ๋‹ค โ€” * ๋ฐฉ๊ธˆ 27๊ฑด์„ ํ™•์ธํ•ด ๋†“๊ณ  '๋…์ฑ„ 3๊ฐœ ๋™' ๊ฐ™์€ ๋‚จ์˜ ๊ฐ€๊ฒŒ ๊ฐ’์ด ๋œฌ๋‹ค. ์‹ค์ œ๋กœ ๊ทธ๋žฌ๋‹ค. @@ -69,21 +101,8 @@ export function BuilderPage() { */ const wizardPlaceId = useBuilderStore((s) => s.confirmedIdentity?.placeId ?? null); const placeId = urlPlaceId ?? wizardPlaceId; - /** - * ์—๋””ํ„ฐ๋กœ ๋ฐ”๋กœ ๋“ค์–ด๊ฐˆ์ง€. - * - * โ˜… **์ฒ˜์Œ ๋“ค์–ด์˜จ ์ˆœ๊ฐ„**์˜ ์ฃผ์†Œ์ฐฝ๋งŒ ๋ณธ๋‹ค. ์œ„์ €๋“œ 2๋‹จ๊ณ„๊ฐ€ ํ™•์ • ๋’ค์— `?placeId=` ๋ฅผ ๋ถ™์ด๋Š”๋ฐ - * (์ƒˆ๋กœ๊ณ ์นจ ๋ณต์›์šฉ), ๊ทธ๊ฑธ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณด๋ฉด "URL ๋ถ™์—ฌ๋„ฃ๊ณ  ํ™•์ธ โ†’ ๊ณง์žฅ ์—๋””ํ„ฐ" ๊ฐ€ ๋œ๋‹ค โ€” - * ์ˆ˜์ง‘ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” 3ยท4ยท5๋‹จ๊ณ„๊ฐ€ ํ†ต์งธ๋กœ ๊ฑด๋„ˆ๋›ฐ์–ด์ง„๋‹ค. ์‹ค์ œ๋กœ ๊ทธ๋ ‡๊ฒŒ ๋๋‹ค. - * ์‚ฌ์—…์žฅ ๋ชฉ๋ก์—์„œ ๋”ฅ๋งํฌ๋กœ ๋“ค์–ด์˜จ ๊ฒฝ์šฐ(์ด๋ฏธ ๋งŒ๋“  ๊ฐ€๊ฒŒ๋ฅผ ์—ฌ๋Š” ๊ฒƒ)๋งŒ ์—๋””ํ„ฐ๋กœ ๋ณด๋‚ธ๋‹ค. - */ - const enteredWithPlace = useRef( - Boolean(urlPlaceId) && searchParams.get('flow') !== 'onboarding', - ).current; - const sync = usePlaceSync(placeId, {enterEditor: enteredWithPlace}); + const sync = usePlaceSync(placeId, {isOnboarding}); - const step = useBuilderStore((s) => s.step); - const goToStep = useBuilderStore((s) => s.goToStep); const storeName = useBuilderStore((s) => s.storeName); // ์—๋””ํ„ฐ๋Š” AppShell(์‚ฌ์ด๋“œ๋ฐ”)์„ ์•ˆ ์“ด๋‹ค โ€” ๋ˆ„๊ตฌ๋กœ ๋กœ๊ทธ์ธํ–ˆ๋Š”์ง€ยท๋‚˜๊ฐ€๋Š” ๊ธธ์ด ์—ฌ๊ธฐ ์—†์œผ๋ฉด ์•„์˜ˆ ์—†๋‹ค. const user = useAuthStore((s) => s.user); @@ -128,7 +147,7 @@ export function BuilderPage() { // ์—๋””ํ„ฐ์— ๋“ค์–ด๊ฐˆ ๋•Œ ๋กœ๊ทธ์ธ์„ ๋ฐ›๋Š”๋‹ค. ์œ„์ €๋“œ(1~5๋‹จ๊ณ„)๋Š” ์š”๊ตฌํ•˜์ง€ ์•Š๋Š”๋‹ค. if (step === EDITOR_STEP && !isSignedIn) { - return goToStep(4)} />; + return goToStep('template')} />; } if (step === EDITOR_STEP) { @@ -225,7 +244,9 @@ export function BuilderPage() { return (
- Web4Ai + + Web4Ai + {/* ๋น„๋กœ๊ทธ์ธ์€ ๋Œ์•„๊ฐˆ ๋ชฉ๋ก์ด ์—†๋‹ค โ€” ๊ทธ ์ž๋ฆฌ์—๋Š” ๋กœ๊ทธ์ธ์„ ๋‘”๋‹ค(๋นˆ ๋ฒ„ํŠผ์„ ๋‘์ง€ ์•Š๋Š”๋‹ค). */} {isSignedIn ? (
- {step === 1 && } - {step === 2 && } - {step === 3 && } - {step === 4 && } - {step === 5 && } + {step === 'search' && } + {step === 'industry' && } + {step === 'collect' && } + {step === 'template' && } + {step === 'generating' && }
); diff --git a/solution/frontend/src/stores/builder.ts b/solution/frontend/src/stores/builder.ts index 6d34f1a..5af0ab7 100644 --- a/solution/frontend/src/stores/builder.ts +++ b/solution/frontend/src/stores/builder.ts @@ -23,9 +23,9 @@ import type { ConfirmedIdentity, FactRef, LivePlaceInput, + PendingPick, RightTab, WeatherLocation, - WizardStep, } from '@/stores/builderTypes'; /** @@ -38,16 +38,14 @@ export type { ConfirmedIdentity, FactRef, LivePlaceInput, + PendingPick, RightTab, WeatherLocation, - WizardStep, }; -export {EDITOR_STEP} from '@/stores/builderTypes'; -import {EDITOR_STEP} from '@/stores/builderTypes'; interface BuilderState { // โ”€โ”€ ์œ„์ €๋“œ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - step: WizardStep; + // โ˜… ๋‹จ๊ณ„(step)๋Š” ์—ฌ๊ธฐ ์—†๋‹ค โ€” ์ฃผ์†Œ์ฐฝ์ด ์†Œ์œ ํ•œ๋‹ค(features/onboarding/wizardUrl). industry: IndustryType; /** * ์ง€๊ธˆ ํŽธ์ง‘ ์ค‘์ธ ์‹ค์ œ ์‚ฌ์—…์žฅ. null ์ด๋ฉด ์•„์ง ๊ฐ€๊ฒŒ๊ฐ€ ์ •ํ•ด์ง€์ง€ ์•Š์€ ์ƒํƒœ๋‹ค. @@ -65,13 +63,15 @@ interface BuilderState { * ํ™”๋ฉด์˜ ์ƒํ˜ธยท์ฃผ์†Œ๋Š” ์—…์ข… ์˜ˆ์‹œ์ผ ๋ฟ์ด๋ผ๋Š” ๋œป์ด๊ณ , 3๋‹จ๊ณ„๋Š” ๊ทธ ์‚ฌ์‹ค์„ ๊ทธ๋Œ€๋กœ ํ‘œ์‹œํ•œ๋‹ค. */ confirmedIdentity: ConfirmedIdentity | null; + /** ์—…์ข…์„ ๋ชป ์ •ํ•ด ์—…์ข… ํ™”๋ฉด์œผ๋กœ ๋„˜๊ธด ํ›„๋ณด. ๋Œ์•„์™€์„œ ํ™•์ •์„ ์ด์–ด๊ฐˆ ๋•Œ๋งŒ ์“ด๋‹ค. */ + pendingPick: PendingPick | null; - // ์ˆ˜์ง‘(Step 2) + // ์ˆ˜์ง‘ isGathering: boolean; gatherStage: number; gatherCompleted: boolean; - // ํ…œํ”Œ๋ฆฟ(Step 3) ยท ์ƒ์„ฑ(Step 4) + // ํ…œํ”Œ๋ฆฟ ยท ์ƒ์„ฑ templateId: string; colorPaletteId: string | null; generateStage: number; @@ -97,7 +97,6 @@ interface BuilderState { publishedUrl: string | null; // โ”€โ”€ ์•ก์…˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - goToStep: (step: WizardStep) => void; selectIndustry: (industry: IndustryType) => void; /** ์‹ค์‚ฌ์—…์žฅ ๋ฐ์ดํ„ฐ๋ฅผ ์บ”๋ฒ„์Šค์— ์–น๋Š”๋‹ค(๊ฐ™์€ ์‚ฌ์—…์žฅ์„ ๋‹ค์‹œ ์ฝ์–ด๋„ ํŽธ์ง‘์„ ์ง€์šฐ์ง€ ์•Š๋Š”๋‹ค). */ @@ -114,12 +113,16 @@ interface BuilderState { /** ํ™•์ •์„ ๋ฌผ๋ฆฐ๋‹ค โ€” ์ƒํ˜ธ๋ฅผ ๋‹ค์‹œ ๊ฒ€์ƒ‰ํ•˜๋Ÿฌ ๊ฐˆ ๋•Œ. */ clearIdentity: () => void; + /** ์—…์ข… ํ™”๋ฉด์œผ๋กœ ๋„˜๊ธธ ํ›„๋ณด๋ฅผ ๋“ค๋ ค ๋ณด๋‚ธ๋‹ค(null ์ด๋ฉด ๋น„์šด๋‹ค). */ + setPendingPick: (pick: PendingPick | null) => void; + startGather: () => void; setGatherStage: (stage: number) => void; finishGather: () => void; selectTemplate: (templateId: string) => void; selectColorPalette: (paletteId: string | null) => void; + /** ์ƒ์„ฑ ํ™”๋ฉด์— ๋“ค์–ด๊ฐ€๊ธฐ ์ง์ „, ์ง„ํ–‰ ํ‘œ์‹œ๋ฅผ ์ฒ˜์Œ์œผ๋กœ ๋˜๋Œ๋ฆฐ๋‹ค. ํ™”๋ฉด ์ด๋™์€ ์ฃผ์†Œ์ฐฝ์ด ํ•œ๋‹ค. */ startGenerating: () => void; setGenerateStage: (stage: number) => void; @@ -288,10 +291,10 @@ for (const key of ['o2osite.builder.wizard.v1', 'o2osite.builder.wizard.v2']) { let factSaver: FactSaver; export const useBuilderStore = create((set, get) => ({ - step: 1, ...seedFor(FALLBACK_INDUSTRY), placeId: null, confirmedIdentity: null, + pendingPick: null, savingFieldIds: [], isGathering: false, @@ -306,8 +309,6 @@ export const useBuilderStore = create((set, get) => ({ isPublishModalOpen: false, publishedUrl: null, - goToStep: (step) => set({step}), - // ์—…์ข…์„ ๋ฐ”๊พธ๋ฉด ๊ทธ ์—…์ข…์˜ ์‹œ๋“œ๋กœ ํ†ต์งธ๋กœ ๊ฐˆ์•„ํƒ„๋‹ค โ€” ์•ž ์—…์ข…์˜ ์„น์…˜ยทํ•„๋“œ๊ฐ€ ๋‚จ์œผ๋ฉด // ์นดํŽ˜ ์‚ฌ์ดํŠธ์— '๊ฐ์‹ค ์•ˆ๋‚ด'๊ฐ€ ๋ถ™๋Š” ์‹์œผ๋กœ ์„ž์ธ๋‹ค. selectIndustry: (industry) => { @@ -327,7 +328,16 @@ export const useBuilderStore = create((set, get) => ({ location: identity.address, infoFields: withConfirmedIdentity(seed.infoFields, identity), } - : null), + : { + /** + * โ˜… ์‚ฌ์žฅ๋‹˜์ด ์นœ ์ƒํ˜ธยท์œ„์น˜๋Š” ์‹œ๋“œ๊ฐ€ ์•„๋‹ˆ๋‹ค โ€” ์—…์ข…์„ ๋ฐ”๊ฟจ๋‹ค๊ณ  ์ง€์šฐ๋ฉด ์•ˆ ๋œ๋‹ค. + * ์—…์ข…์ด ์ฒซ ํ™”๋ฉด์ด๋˜ ์‹œ์ ˆ์—” ์—ฌ๊ธฐ๊ฐ€ ๋Š˜ ๋นˆ ๊ฐ’์ด๋ผ ํ‹ฐ๊ฐ€ ์•ˆ ๋‚ฌ๋Š”๋ฐ, ์ง€๊ธˆ์€ ์ƒํ˜ธ๋ฅผ + * ๋จผ์ € ๋ฐ›๋Š”๋‹ค: ์ง€์šฐ๋ฉด ๊ฒ€์ƒ‰์–ด๋ฅผ ์นœ ๋’ค ์—…์ข…๋งŒ ๊ณจ๋ผ๋„ ๊ทธ ์ด๋ฆ„์ด ์‚ฌ๋ผ์ง„๋‹ค. + */ + storeName: state.storeName, + location: state.location, + infoFields: withOwnerIdentity(seed.infoFields, state.storeName, state.location), + }), // ์—…์ข…์„ ์†์œผ๋กœ ๊ณ ๋ฅด๋ฉด ํ™”๋ฉด์˜ ๊ฐ’์€ ๋‹ค์‹œ ์‹œ๋“œ๋‹ค โ€” ์‹ค์‚ฌ์—…์žฅ ๋ฐฐ์„ ์„ ๋‚จ๊ฒจ๋‘๋ฉด // "placeId ๊ฐ€ ์žˆ๋‹ค = ํ™”๋ฉด ๊ฐ’์ด ์„œ๋ฒ„์—์„œ ์™”๋‹ค"๋Š” ์•ฝ์†์ด ๊นจ์ง„๋‹ค. placeId: null, @@ -350,7 +360,7 @@ export const useBuilderStore = create((set, get) => ({ * 'ํ™•์ธ ํ•„์š”'๋กœ ํŠ€์–ด, ์‚ฌ์žฅ๋‹˜ ๋ˆˆ์—๋Š” ํด๋ฆญ์ด ์”นํžŒ ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค(withPendingEdits). */ applyPlace: (input, options) => { - const enterEditor = options?.enterEditor ?? true; + const isOnboarding = options?.isOnboarding ?? false; // ์ฒ˜์Œ ์—ฌ๋Š” ์‚ฌ์—…์žฅ์ธ๊ฐ€. ๋ฆฌํŽ˜์น˜(๊ฐ™์€ placeId)๋ฉด ์„น์…˜ยท๋‹จ๊ณ„๋ฅผ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค. const isNewPlace = get().placeId !== input.placeId; // ์‚ฌ์—…์žฅ์„ ๊ฐˆ์•„ํƒ€๋ฉด ์•ž ์‚ฌ์—…์žฅ์˜ ์ €์žฅ ๋Œ€๊ธฐ๋ถ„์€ ๋ฒ„๋ฆฐ๋‹ค โ€” ๋“ค๊ณ  ๊ฐ€๋ฉด ๋‚จ์˜ ๊ฐ’์ด ์ด ํ™”๋ฉด์— ์–นํžŒ๋‹ค. @@ -366,11 +376,14 @@ export const useBuilderStore = create((set, get) => ({ return { // ์—…์ข…์ด ๋ฐ”๋€” ๋•Œ๋งŒ ์‹œ๋“œ๋ฅผ ๊ฐˆ์•„๋ผ์šด๋‹ค(์นดํŽ˜ ์‚ฌ์—…์žฅ์— '๊ฐ์‹ค ์•ˆ๋‚ด'๊ฐ€ ๋‚จ์ง€ ์•Š๊ฒŒ). ...(needsReseed ? seedFor(input.industry) : null), - // ์ด๋ฏธ ๋“ฑ๋ก๋œ ์‚ฌ์—…์žฅ์„ ์—ฌ๋Š” ๊ฒƒ์ด๋ฏ€๋กœ ์ˆ˜์ง‘ ์œ„์ €๋“œ๋ฅผ ๋‹ค์‹œ ๊ฑท๊ฒŒ ํ•˜์ง€ ์•Š๋Š”๋‹ค. ...(isNewPlace ? { - step: enterEditor ? EDITOR_STEP : 3, - ...(!enterEditor + /** + * โ˜… ์œ„์ €๋“œ ๋„์ค‘์— ์ƒˆ๋กœ๊ณ ์นจํ•˜๋ฉด ์Šคํ† ์–ด์˜ ํ™•์ • ์‹ ์›์ด ๋น„์–ด ์žˆ๋‹ค(๋ธŒ๋ผ์šฐ์ €์— ์ €์žฅํ•˜์ง€ + * ์•Š์œผ๋ฏ€๋กœ). ๊ทธ๋Œ€๋กœ ๋‘๋ฉด 3๋‹จ๊ณ„๊ฐ€ "์•„์ง ๊ฐ€๊ฒŒ๋ฅผ ์•ˆ ๊ณจ๋ž๋‹ค"๊ณ  ํŒ๋‹จํ•ด ์ˆ˜์ง‘์„ ์—ด์ง€ + * ์•Š๋Š”๋‹ค โ€” ์„œ๋ฒ„์—์„œ ์ฝ์–ด์˜จ ์‚ฌ์—…์žฅ์œผ๋กœ ์—ฌ๊ธฐ์„œ ๋‹ค์‹œ ์„ธ์šด๋‹ค. + */ + ...(isOnboarding ? { confirmedIdentity: { placeId: input.placeId, @@ -384,18 +397,7 @@ export const useBuilderStore = create((set, get) => ({ selectedSectionId: null, isGathering: false, } - : /** - * ๊ฐ™์€ ์‚ฌ์—…์žฅ์„ **๋”ฅ๋งํฌ๋กœ ๋‹ค์‹œ ์—ฐ** ๊ฒฝ์šฐ๋‹ค(`/builder?placeId=...`). - * - * โ˜… ๋‹จ๊ณ„๋ฅผ ๋‹ค์‹œ ๋ชป ๋ฐ•์ง€ ์•Š์œผ๋ฉด, ์Šคํ† ์–ด์— ๋‚จ์•„ ์žˆ๋˜ ์œ„์ €๋“œ ๋‹จ๊ณ„๊ฐ€ ๊ทธ๋Œ€๋กœ ๋œฌ๋‹ค โ€” - * ์‚ฌ์—…์žฅ ๋ชฉ๋ก์—์„œ [์‚ฌ์ดํŠธ ํŽธ์ง‘] ์„ ๋ˆŒ๋ €๋Š”๋ฐ ์ƒ์„ฑ ํ™”๋ฉด(5๋‹จ๊ณ„)์ด ๋˜์‚ด์•„๋‚˜ - * ์ƒ์„ฑ ์žก์ด ํ•œ ๋ฒˆ ๋” ๋“ค์–ด๊ฐ€๋Š” ์ผ์ด ์‹ค์ œ๋กœ ์ƒ๊ธด๋‹ค. ํŽธ์ง‘ํ•˜๋Ÿฌ ์˜จ ์‚ฌ๋žŒ์€ - * ์–ธ์ œ๋‚˜ ํŽธ์ง‘๊ธฐ๋ฅผ ๋ด์•ผ ํ•œ๋‹ค. (๋ฆฌํŽ˜์น˜๋Š” enterEditor ์—ฌ๋ถ€์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ์•ˆ์ „ํ•˜๋‹ค โ€” - * ์ด๋ฏธ EDITOR_STEP ์ด๋ฉด ๊ฐ™์€ ๊ฐ’์„ ๋‹ค์‹œ ์“ฐ๋Š” ๊ฒƒ๋ฟ์ด๋‹ค.) - */ - enterEditor - ? {step: EDITOR_STEP} - : null), + : null), /** * โ˜… "์ˆ˜์ง‘ ์™„๋ฃŒ"๋Š” fact ๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ์ด๋‹ค. * @@ -454,6 +456,8 @@ export const useBuilderStore = create((set, get) => ({ clearIdentity: () => set({confirmedIdentity: null}), + setPendingPick: (pendingPick) => set({pendingPick}), + toggleChannel: (channelId) => set((state) => ({ selectedChannels: state.selectedChannels.includes(channelId) @@ -496,9 +500,8 @@ export const useBuilderStore = create((set, get) => ({ set({colorPaletteId}); persistTheme(); }, - // โ˜… ์ƒ์„ฑ ํ™”๋ฉด(5)์œผ๋กœ ๋ณด๋‚ธ๋‹ค. ๋‹จ๊ณ„ ๋ฒˆํ˜ธ๋Š” EDITOR_STEP ๊ณผ ํ•จ๊ป˜ WizardStep ์ฃผ์„์ด ๊ธฐ์ค€์ด๋‹ค โ€” - // ์—ฌ๊ธฐ ์ˆซ์ž๊ฐ€ ์ž๊ธฐ ํ™”๋ฉด(4)์ด๋ฉด [์‚ฌ์ดํŠธ ์ƒ์„ฑํ•˜๊ธฐ]๊ฐ€ ์•„๋ฌด ์ผ๋„ ์•ˆ ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ธ๋‹ค. - startGenerating: () => set({step: 5, generateStage: 1}), + // ํ™”๋ฉด ์ด๋™์€ ๋ถ€๋ฅด๋Š” ์ชฝ์ด ์ฃผ์†Œ์ฐฝ์œผ๋กœ ํ•œ๋‹ค(`?step=generating`) โ€” ์—ฌ๊ธฐ์„œ๋Š” ์ง„ํ–‰ ํ‘œ์‹œ๋งŒ ๋˜๊ฐ๋Š”๋‹ค. + startGenerating: () => set({generateStage: 1}), setGenerateStage: (generateStage) => set({generateStage}), selectSection: (selectedSectionId) => set({selectedSectionId}), @@ -747,11 +750,11 @@ export const useBuilderStore = create((set, get) => ({ factSaver.clear(); clearThemeSaves(); set({ - step: 1, ...seedFor(get().industry), placeId: null, // [์ฒ˜์Œ๋ถ€ํ„ฐ]๋Š” "์ด ๊ฐ€๊ฒŒ๊ฐ€ ๋งž๋‹ค"๊นŒ์ง€ ๋ฌผ๋ฆฐ๋‹ค โ€” ์ƒํ˜ธ๋ถ€ํ„ฐ ๋‹ค์‹œ ํ™•์ธ๋ฐ›๋Š”๋‹ค. confirmedIdentity: null, + pendingPick: null, isGathering: false, gatherStage: 1, gatherCompleted: false, diff --git a/solution/frontend/src/stores/builderTypes.ts b/solution/frontend/src/stores/builderTypes.ts index c3dfbb2..225b0d8 100644 --- a/solution/frontend/src/stores/builderTypes.ts +++ b/solution/frontend/src/stores/builderTypes.ts @@ -8,23 +8,9 @@ import type {FactStatus, IndustryType, InfoField, PhotoItem} from '@o2o/shared'; /** - * ์œ„์ €๋“œ ๋‹จ๊ณ„. - * - * 1 ์—…์ข… ์„ ํƒ - * 2 ์ƒํ˜ธ ๊ฒ€์ƒ‰ โ†’ **ํ›„๋ณด ๋ชฉ๋ก์—์„œ ๋‚ด ๊ฐ€๊ฒŒ ํ™•์ธ** โ† ๋‚จ์˜ ๊ฐ€๊ฒŒ๊ฐ€ ์„ž์ด๋ฉด ์ œ์ผ ๋น„์‹ผ ์‹ค์ˆ˜๋‹ค - * 3 ์ˆ˜์ง‘๋œ ๋ฐ์ดํ„ฐ ํ™•์ธ โ† [๋งž์•„์š”]/[์•„๋‹ˆ์—์š”] ๊ฐ€ ์—ฌ๊ธฐ์„œ ๋๋‚œ๋‹ค - * 4 ํ…œํ”Œ๋ฆฟ ์„ ํƒ - * 5 ์ƒ์„ฑ ์ค‘ - * 6 ์—๋””ํ„ฐ - * - * โ˜… 2 ์™€ 3 ์€ ์„ฑ๊ฒฉ์ด ๋‹ค๋ฅธ ํ™•์ธ์ด๋ผ ํ•œ ํ™”๋ฉด์— ๋‘์ง€ ์•Š๋Š”๋‹ค. - * 2 ๋Š” "์ด ๊ฐ€๊ฒŒ๊ฐ€ ๋งž๋‚˜" (์‹ ์›), 3 ์€ "์ด ๊ฐ’์ด ๋งž๋‚˜" (์‚ฌ์‹ค). ์‹ ์›์ด ํ‹€๋ฆฐ ์ฑ„๋กœ - * ์‚ฌ์‹ค์„ ํ™•์ธ์‹œํ‚ค๋ฉด ์‚ฌ์žฅ๋‹˜์ด ๋‚จ์˜ ๊ฐ€๊ฒŒ ์ •๋ณด๋ฅผ ์ž๊ธฐ ๊ฒƒ์ด๋ผ๊ณ  ์Šน์ธํ•˜๊ฒŒ ๋œ๋‹ค. + * โ˜… ์œ„์ €๋“œ ๋‹จ๊ณ„๋Š” ์—ฌ๊ธฐ ์—†๋‹ค โ€” ์ฃผ์†Œ์ฐฝ์ด ์†Œ์œ ํ•œ๋‹ค(`features/onboarding/wizardUrl`). + * ์Šคํ† ์–ด์— ๋‘๋ฉด ๋’ค๋กœ๊ฐ€๊ธฐยท๋ถ๋งˆํฌ๊ฐ€ ๋‹จ๊ณ„๋ฅผ ๋ชป ๋”ฐ๋ผ์˜จ๋‹ค. */ -export type WizardStep = 1 | 2 | 3 | 4 | 5 | 6; - -/** ์œ„์ €๋“œ๊ฐ€ ๋๋‚˜๊ณ  ์—๋””ํ„ฐ๋กœ ๋„˜์–ด๊ฐ€๋Š” ๋‹จ๊ณ„. ์ˆซ์ž๋ฅผ ํ™”๋ฉด๋งˆ๋‹ค ์™ธ์šฐ์ง€ ์•Š๊ฒŒ ํ•œ ๊ณณ์— ๋‘”๋‹ค. */ -export const EDITOR_STEP = 6 satisfies WizardStep; export type RightTab = 'content' | 'design' | 'photos' | 'info' | 'faq' | 'verify'; /** @@ -97,11 +83,26 @@ export interface ConfirmedIdentity { export interface ApplyPlaceOptions { /** - * ์ฒ˜์Œ ์–น๋Š” ์‚ฌ์—…์žฅ์ผ ๋•Œ ๊ณง์žฅ ์—๋””ํ„ฐ๋กœ ๋ณด๋‚ผ์ง€. + * ์œ„์ €๋“œ ์•ˆ์—์„œ ์—ด๋ฆฐ ์‚ฌ์—…์žฅ์ธ๊ฐ€(`?flow=onboarding`). * - * โ˜… ๊ธฐ๋ณธ์€ true โ€” ์‚ฌ์—…์žฅ ๋ชฉ๋ก์—์„œ ๋”ฅ๋งํฌ๋กœ ๋“ค์–ด์˜จ ๊ฒฝ์šฐ๋‹ค(์ด๋ฏธ ๋“ฑ๋ก๋œ ๊ฐ€๊ฒŒ๋ฅผ ์—ฌ๋Š” ๊ฒƒ์ด๋ฏ€๋กœ - * ์œ„์ €๋“œ๋ฅผ ๋‹ค์‹œ ๊ฑท๊ฒŒ ํ•˜์ง€ ์•Š๋Š”๋‹ค). ๋ฐ˜๋Œ€๋กœ ์œ„์ €๋“œ 3๋‹จ๊ณ„๋Š” **์ˆ˜์ง‘ ๊ฒฐ๊ณผ๋ฅผ ์œ„์ €๋“œ ์•ˆ์—์„œ** - * ๋ณด์—ฌ์ค˜์•ผ ํ•˜๋ฏ€๋กœ false ๋กœ ๋ถ€๋ฅธ๋‹ค. ์•ˆ ๊ทธ๋Ÿฌ๋ฉด ์ˆ˜์ง‘์ด ๋๋‚˜๋Š” ์ˆœ๊ฐ„ ํ™”๋ฉด์ด ์—๋””ํ„ฐ๋กœ ํŠ„๋‹ค. + * โ˜… ํ™”๋ฉด์„ ์˜ฎ๊ธฐ์ง€๋Š” ์•Š๋Š”๋‹ค โ€” ๊ทธ๊ฑด ์ฃผ์†Œ์ฐฝ(`?step=`)์˜ ์ผ์ด๋‹ค. ์ด ๊ฐ’์ด ํ•˜๋Š” ์ผ์€ ํ•˜๋‚˜๋ฟ: + * ์œ„์ €๋“œ ๋„์ค‘์— ์ƒˆ๋กœ๊ณ ์นจํ•˜๋ฉด ์Šคํ† ์–ด์˜ ํ™•์ • ์‹ ์›์ด ๋น„์–ด ์žˆ์œผ๋ฏ€๋กœ, ์„œ๋ฒ„์—์„œ ์ฝ์€ ์‚ฌ์—…์žฅ์œผ๋กœ + * ๊ทธ๊ฑธ ๋‹ค์‹œ ์„ธ์šด๋‹ค. ์—†์œผ๋ฉด 3๋‹จ๊ณ„๊ฐ€ "๊ฐ€๊ฒŒ๋ฅผ ์•„์ง ์•ˆ ๊ณจ๋ž๋‹ค"๊ณ  ํŒ๋‹จํ•ด ์ˆ˜์ง‘์„ ์—ด์ง€ ์•Š๋Š”๋‹ค. */ - enterEditor?: boolean; + isOnboarding?: boolean; +} + +/** + * ๊ณต๊ฐœ ๊ฒ€์ƒ‰์—์„œ ๊ณ ๋ฅธ, **์•„์ง ์—…์ข…์ด ์•ˆ ์ •ํ•ด์ง„** ํ›„๋ณด. + * + * โ˜… ์—…์ข… ํ™”๋ฉด(`?step=industry`)์„ ๋‹ค๋…€์˜ค๋Š” ๋™์•ˆ๋งŒ ์‚ฐ๋‹ค. ์‚ฌ์žฅ๋‹˜์ด ๊ณ ๋ฅธ ์—…์ข…์„ ๋“ค๊ณ  ๋Œ์•„์™€์•ผ + * ๊ทธ ์—…์ข…์œผ๋กœ ์‚ฌ์—…์žฅ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด์„œ๋‹ค(์‚ฌ์—…์žฅ์˜ category ๋Š” ๋งŒ๋“ค ๋•Œ ์ •ํ•ด์ง„๋‹ค). + * โ˜… ๋ธŒ๋ผ์šฐ์ €์— ์ €์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค(์œ„์ €๋“œ ์ƒํƒœ ๊ทœ์น™). ์ƒˆ๋กœ๊ณ ์นจํ•˜๋ฉด ์‚ฌ๋ผ์ง€๊ณ  ๊ฒ€์ƒ‰๋ถ€ํ„ฐ ๋‹ค์‹œ ํ•œ๋‹ค โ€” + * ํ™•์ • ์ „์ด๋ผ ์„œ๋ฒ„์—๋„ ์•„์ง ์•„๋ฌด๊ฒƒ๋„ ์—†๋‹ค. + */ +export interface PendingPick { + name: string; + address: string; + /** ์—…์ข… ํ™”๋ฉด์—์„œ ๊ณ ๋ฅธ ๊ฐ’. ์ด๊ฒŒ ์ฑ„์›Œ์ ธ์•ผ ํ™•์ •์ด ์ด์–ด์ง„๋‹ค. */ + industry?: IndustryType; }