From 4b25556f245eb813123d59db0211afa07937b222 Mon Sep 17 00:00:00 2001 From: jstar_00 Date: Sun, 15 Mar 2026 20:40:51 +0900 Subject: [PATCH 01/46] =?UTF-8?q?feat:=20=EB=8F=84=EB=A9=B4=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../imageSetup/v2/constants/floorPlanDummy.ts | 190 ++++++++++++++++++ src/pages/imageSetup/v2/types/floorPlan.ts | 45 +++++ 2 files changed, 235 insertions(+) create mode 100644 src/pages/imageSetup/v2/constants/floorPlanDummy.ts create mode 100644 src/pages/imageSetup/v2/types/floorPlan.ts diff --git a/src/pages/imageSetup/v2/constants/floorPlanDummy.ts b/src/pages/imageSetup/v2/constants/floorPlanDummy.ts new file mode 100644 index 00000000..3d27a17f --- /dev/null +++ b/src/pages/imageSetup/v2/constants/floorPlanDummy.ts @@ -0,0 +1,190 @@ +import type { + FilterCategory, + FloorPlanData, + RecentSpaceData, +} from '../types/floorPlan'; + +export const DUMMY_FILTER_CATEGORIES: FilterCategory[] = [ + { + id: 'houseType', + label: '주거 형태', + options: [ + { id: 'ALL', label: '전체' }, + { id: 'OFFICETEL', label: '오피스텔' }, + { id: 'VILLA', label: '빌라/다세대' }, + { id: 'APARTMENT', label: '아파트' }, + { id: 'ETC', label: '그 외' }, + ], + }, + { + id: 'structure', + label: '구조', + options: [ + { id: 'ALL', label: '전체' }, + { id: 'OPEN_ONE_ROOM', label: '오픈형 원룸' }, + { id: 'SEPARATE_ONE_ROOM', label: '분리형 원룸' }, + { id: 'DUPLEX', label: '복층형' }, + { id: 'TWO_ROOM', label: '투룸' }, + { id: 'THREE_ROOM_PLUS', label: '쓰리룸 이상' }, + { id: 'ETC', label: '그 외' }, + ], + }, + { + id: 'areaType', + label: '평형', + options: [ + { id: 'ALL', label: '전체' }, + { id: 'UNDER_4', label: '4평 이하' }, + { id: 'FROM_5_TO_10', label: '5-10평' }, + { id: 'TENS', label: '10평대' }, + { id: 'TWENTIES', label: '20평대' }, + { id: 'OVER_30', label: '30평 이상' }, + ], + }, +]; + +export const DUMMY_FLOOR_PLANS: FloorPlanData[] = [ + { + id: 1, + spaceName: 'A타입 · 8평', + houseType: { id: 'OFFICETEL', label: '오피스텔' }, + structure: { id: 'OPEN_ONE_ROOM', label: '오픈형 원룸' }, + areaType: { id: 'FROM_5_TO_10', label: '5-10평' }, + thumbnailUrl: 'https://placehold.co/164x164?text=A-8', + views: [ + { + viewId: 1, + viewLabel: '정면', + imageUrl: 'https://placehold.co/343x343?text=A-8-front', + }, + ], + }, + { + id: 2, + spaceName: 'B타입 · 12평', + houseType: { id: 'OFFICETEL', label: '오피스텔' }, + structure: { id: 'SEPARATE_ONE_ROOM', label: '분리형 원룸' }, + areaType: { id: 'TENS', label: '10평대' }, + thumbnailUrl: 'https://placehold.co/164x164?text=B-12', + views: [ + { + viewId: 2, + viewLabel: '정면', + imageUrl: 'https://placehold.co/343x343?text=B-12-front', + }, + { + viewId: 3, + viewLabel: '왼쪽', + imageUrl: 'https://placehold.co/343x343?text=B-12-left', + }, + ], + }, + { + id: 3, + spaceName: 'C타입 · 15평', + houseType: { id: 'VILLA', label: '빌라/다세대' }, + structure: { id: 'TWO_ROOM', label: '투룸' }, + areaType: { id: 'TENS', label: '10평대' }, + thumbnailUrl: 'https://placehold.co/164x164?text=C-15', + views: [ + { + viewId: 4, + viewLabel: '정면', + imageUrl: 'https://placehold.co/343x343?text=C-15-front', + }, + ], + }, + { + id: 4, + spaceName: 'D타입 · 22평', + houseType: { id: 'APARTMENT', label: '아파트' }, + structure: { id: 'THREE_ROOM_PLUS', label: '쓰리룸 이상' }, + areaType: { id: 'TWENTIES', label: '20평대' }, + thumbnailUrl: 'https://placehold.co/164x164?text=D-22', + views: [ + { + viewId: 5, + viewLabel: '정면', + imageUrl: 'https://placehold.co/343x343?text=D-22-front', + }, + { + viewId: 6, + viewLabel: '왼쪽', + imageUrl: 'https://placehold.co/343x343?text=D-22-left', + }, + { + viewId: 7, + viewLabel: '오른쪽', + imageUrl: 'https://placehold.co/343x343?text=D-22-right', + }, + ], + }, + { + id: 5, + spaceName: 'E타입 · 6평', + houseType: { id: 'OFFICETEL', label: '오피스텔' }, + structure: { id: 'OPEN_ONE_ROOM', label: '오픈형 원룸' }, + areaType: { id: 'FROM_5_TO_10', label: '5-10평' }, + thumbnailUrl: 'https://placehold.co/164x164?text=E-6', + views: [ + { + viewId: 8, + viewLabel: '정면', + imageUrl: 'https://placehold.co/343x343?text=E-6-front', + }, + ], + }, + { + id: 6, + spaceName: 'F타입 · 3평', + houseType: { id: 'ETC', label: '그 외' }, + structure: { id: 'OPEN_ONE_ROOM', label: '오픈형 원룸' }, + areaType: { id: 'UNDER_4', label: '4평 이하' }, + thumbnailUrl: 'https://placehold.co/164x164?text=F-3', + views: [ + { + viewId: 9, + viewLabel: '정면', + imageUrl: 'https://placehold.co/343x343?text=F-3-front', + }, + ], + }, + { + id: 7, + spaceName: 'G타입 · 18평', + houseType: { id: 'VILLA', label: '빌라/다세대' }, + structure: { id: 'DUPLEX', label: '복층형' }, + areaType: { id: 'TENS', label: '10평대' }, + thumbnailUrl: 'https://placehold.co/164x164?text=G-18', + views: [ + { + viewId: 10, + viewLabel: '정면', + imageUrl: 'https://placehold.co/343x343?text=G-18-front', + }, + { + viewId: 11, + viewLabel: '오른쪽', + imageUrl: 'https://placehold.co/343x343?text=G-18-right', + }, + ], + }, + { + id: 8, + spaceName: 'H타입 · 35평', + houseType: { id: 'APARTMENT', label: '아파트' }, + structure: { id: 'THREE_ROOM_PLUS', label: '쓰리룸 이상' }, + areaType: { id: 'OVER_30', label: '30평 이상' }, + thumbnailUrl: 'https://placehold.co/164x164?text=H-35', + views: [ + { + viewId: 12, + viewLabel: '정면', + imageUrl: 'https://placehold.co/343x343?text=H-35-front', + }, + ], + }, +]; + +// null로 시작 — 테스트 시 값 넣어서 확인 +export const DUMMY_RECENT_SPACE: RecentSpaceData | null = null; diff --git a/src/pages/imageSetup/v2/types/floorPlan.ts b/src/pages/imageSetup/v2/types/floorPlan.ts new file mode 100644 index 00000000..a8f316c8 --- /dev/null +++ b/src/pages/imageSetup/v2/types/floorPlan.ts @@ -0,0 +1,45 @@ +export interface FloorPlanView { + viewId: number; + viewLabel: string; + imageUrl: string; +} + +export interface FilterOption { + id: string; + label: string; +} + +export interface FilterCategory { + id: string; + label: string; + options: FilterOption[]; +} + +export interface FloorPlanData { + id: number; + spaceName: string; + houseType: FilterOption; + structure: FilterOption; + areaType: FilterOption; + thumbnailUrl: string; + views: FloorPlanView[]; +} + +export interface RecentSpaceData { + spaceId: number; + spaceName: string; + thumbnailUrl: string; + views: FloorPlanView[]; +} + +export interface FloorPlanFilters { + houseType: string; + structure: string; + areaType: string; +} + +export const DEFAULT_FILTERS: FloorPlanFilters = { + houseType: 'ALL', + structure: 'ALL', + areaType: 'ALL', +}; From 83a0d15027b634b7737b6c9e64964f4a8b4fc89a Mon Sep 17 00:00:00 2001 From: jstar_00 Date: Wed, 18 Mar 2026 16:14:23 +0900 Subject: [PATCH 02/46] =?UTF-8?q?feat:=20FloorPlan=20=EC=8A=A4=ED=85=9D=20?= =?UTF-8?q?=ED=83=80=EC=9E=85,=20=EB=8D=94=EB=AF=B8=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0,=20=EC=8A=A4=ED=86=A0=EC=96=B4,=20=ED=9B=85=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useFloorPlanStore: 공간 선택 페이지의 모든 상태를 한 곳에 정리 - useFloorPlanSelect: 공간 선택 페이지 훅 - useFloorPlanSheet: 도면 바텀시트 관리 훅 - floorPlan: 도면선택 페이지/API에 필요한 타입(추후 수정 필요) --- src/pages/imageSetup/stores/useFunnelStore.ts | 2 + .../imageSetup/v2/constants/floorPlanDummy.ts | 20 +-- .../imageSetup/v2/hooks/useFloorPlanSelect.ts | 143 ++++++++++++++++++ .../imageSetup/v2/hooks/useFloorPlanSheet.ts | 42 +++++ .../imageSetup/v2/stores/useFloorPlanStore.ts | 114 ++++++++++++++ src/pages/imageSetup/v2/types/floorPlan.ts | 17 ++- 6 files changed, 323 insertions(+), 15 deletions(-) create mode 100644 src/pages/imageSetup/v2/hooks/useFloorPlanSelect.ts create mode 100644 src/pages/imageSetup/v2/hooks/useFloorPlanSheet.ts create mode 100644 src/pages/imageSetup/v2/stores/useFloorPlanStore.ts diff --git a/src/pages/imageSetup/stores/useFunnelStore.ts b/src/pages/imageSetup/stores/useFunnelStore.ts index d664f2a9..540afdc3 100644 --- a/src/pages/imageSetup/stores/useFunnelStore.ts +++ b/src/pages/imageSetup/stores/useFunnelStore.ts @@ -2,6 +2,8 @@ import { create } from 'zustand'; import type { CompletedHouseInfo } from '../types/funnel/houseInfo'; +// TODO: FunnelStore 수정 +// moodBoard, activityInfo 스텝은 이미지 생성에 필요할 수도 있고 아닐 수도 있음 interface FunnelStore { // 각 스텝 데이터(각 스텝 별 요구되는 데이터만 저장) houseInfo: CompletedHouseInfo | null; diff --git a/src/pages/imageSetup/v2/constants/floorPlanDummy.ts b/src/pages/imageSetup/v2/constants/floorPlanDummy.ts index 3d27a17f..00247252 100644 --- a/src/pages/imageSetup/v2/constants/floorPlanDummy.ts +++ b/src/pages/imageSetup/v2/constants/floorPlanDummy.ts @@ -1,7 +1,7 @@ import type { FilterCategory, FloorPlanData, - RecentSpaceData, + RecentFloorPlanData, } from '../types/floorPlan'; export const DUMMY_FILTER_CATEGORIES: FilterCategory[] = [ @@ -46,7 +46,7 @@ export const DUMMY_FILTER_CATEGORIES: FilterCategory[] = [ export const DUMMY_FLOOR_PLANS: FloorPlanData[] = [ { id: 1, - spaceName: 'A타입 · 8평', + name: 'A타입 · 8평', houseType: { id: 'OFFICETEL', label: '오피스텔' }, structure: { id: 'OPEN_ONE_ROOM', label: '오픈형 원룸' }, areaType: { id: 'FROM_5_TO_10', label: '5-10평' }, @@ -61,7 +61,7 @@ export const DUMMY_FLOOR_PLANS: FloorPlanData[] = [ }, { id: 2, - spaceName: 'B타입 · 12평', + name: 'B타입 · 12평', houseType: { id: 'OFFICETEL', label: '오피스텔' }, structure: { id: 'SEPARATE_ONE_ROOM', label: '분리형 원룸' }, areaType: { id: 'TENS', label: '10평대' }, @@ -81,7 +81,7 @@ export const DUMMY_FLOOR_PLANS: FloorPlanData[] = [ }, { id: 3, - spaceName: 'C타입 · 15평', + name: 'C타입 · 15평', houseType: { id: 'VILLA', label: '빌라/다세대' }, structure: { id: 'TWO_ROOM', label: '투룸' }, areaType: { id: 'TENS', label: '10평대' }, @@ -96,7 +96,7 @@ export const DUMMY_FLOOR_PLANS: FloorPlanData[] = [ }, { id: 4, - spaceName: 'D타입 · 22평', + name: 'D타입 · 22평', houseType: { id: 'APARTMENT', label: '아파트' }, structure: { id: 'THREE_ROOM_PLUS', label: '쓰리룸 이상' }, areaType: { id: 'TWENTIES', label: '20평대' }, @@ -121,7 +121,7 @@ export const DUMMY_FLOOR_PLANS: FloorPlanData[] = [ }, { id: 5, - spaceName: 'E타입 · 6평', + name: 'E타입 · 6평', houseType: { id: 'OFFICETEL', label: '오피스텔' }, structure: { id: 'OPEN_ONE_ROOM', label: '오픈형 원룸' }, areaType: { id: 'FROM_5_TO_10', label: '5-10평' }, @@ -136,7 +136,7 @@ export const DUMMY_FLOOR_PLANS: FloorPlanData[] = [ }, { id: 6, - spaceName: 'F타입 · 3평', + name: 'F타입 · 3평', houseType: { id: 'ETC', label: '그 외' }, structure: { id: 'OPEN_ONE_ROOM', label: '오픈형 원룸' }, areaType: { id: 'UNDER_4', label: '4평 이하' }, @@ -151,7 +151,7 @@ export const DUMMY_FLOOR_PLANS: FloorPlanData[] = [ }, { id: 7, - spaceName: 'G타입 · 18평', + name: 'G타입 · 18평', houseType: { id: 'VILLA', label: '빌라/다세대' }, structure: { id: 'DUPLEX', label: '복층형' }, areaType: { id: 'TENS', label: '10평대' }, @@ -171,7 +171,7 @@ export const DUMMY_FLOOR_PLANS: FloorPlanData[] = [ }, { id: 8, - spaceName: 'H타입 · 35평', + name: 'H타입 · 35평', houseType: { id: 'APARTMENT', label: '아파트' }, structure: { id: 'THREE_ROOM_PLUS', label: '쓰리룸 이상' }, areaType: { id: 'OVER_30', label: '30평 이상' }, @@ -187,4 +187,4 @@ export const DUMMY_FLOOR_PLANS: FloorPlanData[] = [ ]; // null로 시작 — 테스트 시 값 넣어서 확인 -export const DUMMY_RECENT_SPACE: RecentSpaceData | null = null; +export const DUMMY_RECENT_FLOOR_PLAN: RecentFloorPlanData | null = null; diff --git a/src/pages/imageSetup/v2/hooks/useFloorPlanSelect.ts b/src/pages/imageSetup/v2/hooks/useFloorPlanSelect.ts new file mode 100644 index 00000000..e2c46b3d --- /dev/null +++ b/src/pages/imageSetup/v2/hooks/useFloorPlanSelect.ts @@ -0,0 +1,143 @@ +import { useCallback, useEffect, useMemo } from 'react'; + +import { useFunnelStore } from '../../stores/useFunnelStore'; +import { + DUMMY_FILTER_CATEGORIES, + DUMMY_FLOOR_PLANS, + DUMMY_RECENT_FLOOR_PLAN, +} from '../constants/floorPlanDummy'; +import { useFloorPlanStore } from '../stores/useFloorPlanStore'; + +import type { + CompletedFloorPlan, + ImageSetupSteps, +} from '../../types/funnel/steps'; +import type { FloorPlanData } from '../types/floorPlan'; + +export const useFloorPlanSelect = ( + context: ImageSetupSteps['FloorPlan'], + onNext: (data: CompletedFloorPlan) => void +) => { + const store = useFloorPlanStore(); + const savedHouseInfo = useFunnelStore((state) => state.houseInfo); + + // 더미 데이터 (추후 useFloorPlanQuery로 교체) + const filterCategories = DUMMY_FILTER_CATEGORIES; + const allFloorPlans = DUMMY_FLOOR_PLANS; + const recentFloorPlan = DUMMY_RECENT_FLOOR_PLAN; + + // 최근 생성 공간이 있으면 초기 시트 표시 + useEffect(() => { + if (recentFloorPlan) { + store.openRecentSheet(); + } + // 마운트 시 1회만 실행 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // 필터링된 도면 리스트 + // appliedFilters 기준으로 filteredFloorPlans 반환 + const filteredFloorPlans = useMemo(() => { + const { houseType, structure, areaType } = store.appliedFilters; + + return allFloorPlans.filter((plan) => { + if (houseType !== 'ALL' && plan.houseType.id !== houseType) return false; + if (structure !== 'ALL' && plan.structure.id !== structure) return false; + if (areaType !== 'ALL' && plan.areaType.id !== areaType) return false; + return true; + }); + }, [allFloorPlans, store.appliedFilters]); + + // 선택된 공간 데이터 + // selectedFloorPlanId로 해당 객체 찾아서 반환 + const selectedFloorPlan: FloorPlanData | null = useMemo( + () => + store.selectedFloorPlanId + ? (allFloorPlans.find((p) => p.id === store.selectedFloorPlanId) ?? + null) + : null, + [allFloorPlans, store.selectedFloorPlanId] + ); + + // 카드 클릭 → 도면 바텀시트 오픈 + const handleCardClick = useCallback( + (floorPlanId: number) => { + store.selectFloorPlan(floorPlanId); + store.openFloorPlanSheet(); + }, + [store] + ); + + // TODO: 바텀시트 use-overlay 적용 + // TODO: 바텀시트 dismiss 시 처리할 로직 확인(상태 초기화 등) + + // 도면 선택 후 "공간 선택하기" CTA + const handleConfirmFloorPlan = useCallback(() => { + if (!selectedFloorPlan) return; + + const currentView = selectedFloorPlan.views[store.selectedViewIndex]; + + const floorPlanData = { + floorPlanId: selectedFloorPlan.id, + isMirror: store.isMirror, + viewId: currentView?.viewId ?? null, + }; + + // TODO: useFunnelStore 수정(각 스텝 별 필요한 데이터 달라짐) + useFunnelStore.getState().setFloorPlan(floorPlanData); + + const payload: CompletedFloorPlan = { + houseType: savedHouseInfo?.houseType ?? context.houseType, + roomType: savedHouseInfo?.roomType ?? context.roomType, + areaType: savedHouseInfo?.areaType ?? context.areaType, + houseId: savedHouseInfo?.houseId ?? context.houseId, + floorPlan: floorPlanData, + }; + + onNext(payload); + }, [ + selectedFloorPlan, + store.selectedViewIndex, + store.isMirror, + savedHouseInfo, + context, + onNext, + ]); + + // 저장된 내 공간 바텀시트 "선택 완료하기" CTA + const handleConfirmRecentFloorPlan = useCallback(() => { + if (!recentFloorPlan) return; + + const firstView = recentFloorPlan.views[0]; + + const floorPlanData = { + floorPlanId: recentFloorPlan.floorPlanId, + isMirror: false, + viewId: firstView?.viewId ?? null, + }; + + useFunnelStore.getState().setFloorPlan(floorPlanData); + + const payload: CompletedFloorPlan = { + houseType: savedHouseInfo?.houseType ?? context.houseType, + roomType: savedHouseInfo?.roomType ?? context.roomType, + areaType: savedHouseInfo?.areaType ?? context.areaType, + houseId: savedHouseInfo?.houseId ?? context.houseId, + floorPlan: floorPlanData, + }; + + store.closeRecentSheet(); + onNext(payload); + }, [recentFloorPlan, savedHouseInfo, context, store, onNext]); + + return { + filterCategories, + filteredFloorPlans, + selectedFloorPlan, + recentFloorPlan, + hasRecentFloorPlan: recentFloorPlan !== null, + handleCardClick, + handleConfirmFloorPlan, + handleConfirmRecentFloorPlan, + }; +}; diff --git a/src/pages/imageSetup/v2/hooks/useFloorPlanSheet.ts b/src/pages/imageSetup/v2/hooks/useFloorPlanSheet.ts new file mode 100644 index 00000000..75f839d9 --- /dev/null +++ b/src/pages/imageSetup/v2/hooks/useFloorPlanSheet.ts @@ -0,0 +1,42 @@ +// 도면 바텀시트 전용 로직 +// 도면 상세 바텀시트 안에서의 도면 이미지 전환 + 좌우반전 로직 담당 + +import { useCallback, useMemo } from 'react'; + +import { useFloorPlanStore } from '../stores/useFloorPlanStore'; + +import type { FloorPlanData } from '../types/floorPlan'; + +export const useFloorPlanSheet = (floorPlan: FloorPlanData | null) => { + const { selectedViewIndex, setViewIndex, isMirror, toggleMirror } = + useFloorPlanStore(); + + const views = floorPlan?.views ?? []; + const isSingleView = views.length === 1; + const isMultiView = views.length > 1; + + const currentView = useMemo( + () => views[selectedViewIndex] ?? null, + [views, selectedViewIndex] + ); + + const handlePrev = useCallback(() => { + if (views.length === 0) return; + setViewIndex((selectedViewIndex - 1 + views.length) % views.length); + }, [views.length, selectedViewIndex, setViewIndex]); + + const handleNext = useCallback(() => { + if (views.length === 0) return; + setViewIndex((selectedViewIndex + 1) % views.length); + }, [views.length, selectedViewIndex, setViewIndex]); + + return { + currentView, // 지금 보고 있는 view (이미지 URL + 라벨) + isSingleView, // view가 1개면 true, prev/next 버튼 안보여줌 + isMultiView, // view가 2개 이상, prev/next 버튼 보여줌 + isMirror, // 좌우반전 상태 + toggleMirror, // 좌우반전 토글 + handlePrev, // 이전 뷰 (순환: 처음에서 prev 시 마지막으로) + handleNext, // 다음 뷰 (순환: 마지막에서 next 시 첫번째로) + }; +}; diff --git a/src/pages/imageSetup/v2/stores/useFloorPlanStore.ts b/src/pages/imageSetup/v2/stores/useFloorPlanStore.ts new file mode 100644 index 00000000..877ff222 --- /dev/null +++ b/src/pages/imageSetup/v2/stores/useFloorPlanStore.ts @@ -0,0 +1,114 @@ +/** + * 공간 선택 페이지의 모든 상태를 한 곳에서 관리 + * 1. 공간 선택 상태 + * - selectedFloorPlanId: 어떤 도면 카드를 선택했는지 + * - selectedViewIndex: 어떤 도면 뷰를 선택했는지 + * - isMirror: 좌우반전 여부 + * 2. 필터 상태 + * - appliedFilters: 실제 적용된 필터 (도면 카드 그리드에 반영됨) + * - pendingFilters: FilterSheet에서 선택했지만 아직 적용하지 않은 값 저장용 임시 필터 + * 3. 시트 열림/닫힘 상태 + * - isFilterSheetOpen: 필터 sheet 상태 + * - isFloorPlanSheetOpen: 도면 sheet 상태 + * - isRecentSheetOpen: 최근 선택 도면 sheet 상태 + */ + +// 4개 컴포넌트(도면 그리드, 필터 3개)가 동일 상태를 읽고 써야함 + prop drilling 방지 +import { create } from 'zustand'; + +import { DEFAULT_FILTERS, type FloorPlanFilters } from '../types/floorPlan'; + +interface FloorPlanStoreState { + selectedFloorPlanId: number | null; + selectedViewIndex: number; + isMirror: boolean; + + appliedFilters: FloorPlanFilters; + pendingFilters: FloorPlanFilters; + + isFilterSheetOpen: boolean; + isFloorPlanSheetOpen: boolean; + isRecentSheetOpen: boolean; + + // 도면 카드 클릭 시 호출, viewIndex/mirror 초기화 + selectFloorPlan: (id: number) => void; + // 도면 카드 선택 해제 + clearFloorPlan: () => void; + setViewIndex: (idx: number) => void; + toggleMirror: () => void; + // FilterSheet 안에서 칩 클릭 시 클릭한 필터값 저장 (그리드에 실제 반영 X) + setPendingFilter: (key: keyof FloorPlanFilters, value: string) => void; + // '필터 적용하기' 클릭 시 pendingFilter를 appliedFilter로 복사 + applyFilters: () => void; + resetFilters: () => void; + // 필터 sheet 열 때 pendingFilter를 appliedFilter로 동기화 + openFilterSheet: () => void; + closeFilterSheet: () => void; + openFloorPlanSheet: () => void; + // 도면선택 sheet 닫을 때 선택 상태도 함께 초기화 + closeFloorPlanSheet: () => void; + openRecentSheet: () => void; + closeRecentSheet: () => void; + // 페이지 이탈 시 모든 상태 초기화 + reset: () => void; +} + +export const useFloorPlanStore = create((set) => ({ + selectedFloorPlanId: null, + selectedViewIndex: 0, + isMirror: false, + + appliedFilters: { ...DEFAULT_FILTERS }, + pendingFilters: { ...DEFAULT_FILTERS }, + + isFilterSheetOpen: false, + isFloorPlanSheetOpen: false, + isRecentSheetOpen: false, + + selectFloorPlan: (id) => + set({ selectedFloorPlanId: id, selectedViewIndex: 0, isMirror: false }), + clearFloorPlan: () => + set({ selectedFloorPlanId: null, selectedViewIndex: 0, isMirror: false }), + setViewIndex: (idx) => set({ selectedViewIndex: idx }), + toggleMirror: () => set((state) => ({ isMirror: !state.isMirror })), + + setPendingFilter: (key, value) => + set((state) => ({ + pendingFilters: { ...state.pendingFilters, [key]: value }, + })), + applyFilters: () => + set((state) => ({ appliedFilters: { ...state.pendingFilters } })), + resetFilters: () => + set({ + pendingFilters: { ...DEFAULT_FILTERS }, + }), + + openFilterSheet: () => + set((state) => ({ + isFilterSheetOpen: true, + pendingFilters: { ...state.appliedFilters }, + })), + closeFilterSheet: () => set({ isFilterSheetOpen: false }), + openFloorPlanSheet: () => set({ isFloorPlanSheetOpen: true }), + closeFloorPlanSheet: () => + set({ + isFloorPlanSheetOpen: false, + selectedFloorPlanId: null, + selectedViewIndex: 0, + isMirror: false, + }), + openRecentSheet: () => set({ isRecentSheetOpen: true }), + closeRecentSheet: () => set({ isRecentSheetOpen: false }), + + reset: () => + set({ + selectedFloorPlanId: null, + selectedViewIndex: 0, + isMirror: false, + appliedFilters: { ...DEFAULT_FILTERS }, + pendingFilters: { ...DEFAULT_FILTERS }, + isFilterSheetOpen: false, + isFloorPlanSheetOpen: false, + isRecentSheetOpen: false, + }), +})); diff --git a/src/pages/imageSetup/v2/types/floorPlan.ts b/src/pages/imageSetup/v2/types/floorPlan.ts index a8f316c8..3c9e94a1 100644 --- a/src/pages/imageSetup/v2/types/floorPlan.ts +++ b/src/pages/imageSetup/v2/types/floorPlan.ts @@ -1,23 +1,27 @@ +// 도면 뷰 export interface FloorPlanView { viewId: number; - viewLabel: string; + viewLabel: string; // 다용도실이 있는 원룸 / 10평대 imageUrl: string; } +// 각 FilterCategory의 하위 선택지 export interface FilterOption { id: string; label: string; } +// ex: 주거 형태, 구조, 평형 export interface FilterCategory { id: string; label: string; options: FilterOption[]; } +// 도면 카드 하나의 전체 데이터 export interface FloorPlanData { id: number; - spaceName: string; + name: string; houseType: FilterOption; structure: FilterOption; areaType: FilterOption; @@ -25,19 +29,22 @@ export interface FloorPlanData { views: FloorPlanView[]; } -export interface RecentSpaceData { - spaceId: number; - spaceName: string; +// '최근 생성한 공간' 데이터 +export interface RecentFloorPlanData { + floorPlanId: number; + name: string; thumbnailUrl: string; views: FloorPlanView[]; } +// 현재 적용된 필터 상태 (houseType, structure, areaType 각각의 선택값) export interface FloorPlanFilters { houseType: string; structure: string; areaType: string; } +// 필터 초기값 export const DEFAULT_FILTERS: FloorPlanFilters = { houseType: 'ALL', structure: 'ALL', From ee3ca1afbcd56e7971685c7daa97475f8bc4804b Mon Sep 17 00:00:00 2001 From: jstar_00 Date: Wed, 18 Mar 2026 17:17:42 +0900 Subject: [PATCH 03/46] =?UTF-8?q?fix:=20=EC=97=AC=EB=9F=AC=20=EC=8B=9C?= =?UTF-8?q?=EC=A0=90=20=EB=A1=9C=EC=A7=81=20=EC=9E=84=EC=8B=9C=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../imageSetup/v2/constants/floorPlanDummy.ts | 153 +++++------------- .../imageSetup/v2/hooks/useFloorPlanSelect.ts | 51 ++---- .../imageSetup/v2/hooks/useFloorPlanSheet.ts | 38 +++-- .../imageSetup/v2/stores/useFloorPlanStore.ts | 7 +- src/pages/imageSetup/v2/types/floorPlan.ts | 70 ++++---- 5 files changed, 111 insertions(+), 208 deletions(-) diff --git a/src/pages/imageSetup/v2/constants/floorPlanDummy.ts b/src/pages/imageSetup/v2/constants/floorPlanDummy.ts index 00247252..05acd781 100644 --- a/src/pages/imageSetup/v2/constants/floorPlanDummy.ts +++ b/src/pages/imageSetup/v2/constants/floorPlanDummy.ts @@ -6,7 +6,7 @@ import type { export const DUMMY_FILTER_CATEGORIES: FilterCategory[] = [ { - id: 'houseType', + id: 'residenceType', label: '주거 형태', options: [ { id: 'ALL', label: '전체' }, @@ -17,7 +17,7 @@ export const DUMMY_FILTER_CATEGORIES: FilterCategory[] = [ ], }, { - id: 'structure', + id: 'layoutType', label: '구조', options: [ { id: 'ALL', label: '전체' }, @@ -30,7 +30,7 @@ export const DUMMY_FILTER_CATEGORIES: FilterCategory[] = [ ], }, { - id: 'areaType', + id: 'areaSize', label: '평형', options: [ { id: 'ALL', label: '전체' }, @@ -46,145 +46,66 @@ export const DUMMY_FILTER_CATEGORIES: FilterCategory[] = [ export const DUMMY_FLOOR_PLANS: FloorPlanData[] = [ { id: 1, - name: 'A타입 · 8평', - houseType: { id: 'OFFICETEL', label: '오피스텔' }, - structure: { id: 'OPEN_ONE_ROOM', label: '오픈형 원룸' }, - areaType: { id: 'FROM_5_TO_10', label: '5-10평' }, - thumbnailUrl: 'https://placehold.co/164x164?text=A-8', - views: [ - { - viewId: 1, - viewLabel: '정면', - imageUrl: 'https://placehold.co/343x343?text=A-8-front', - }, + name: '일자형 원룸', + imageUrls: [ + 'https://placehold.co/343x343?text=1-A', + 'https://placehold.co/343x343?text=1-B', ], + isLatest: true, }, { id: 2, - name: 'B타입 · 12평', - houseType: { id: 'OFFICETEL', label: '오피스텔' }, - structure: { id: 'SEPARATE_ONE_ROOM', label: '분리형 원룸' }, - areaType: { id: 'TENS', label: '10평대' }, - thumbnailUrl: 'https://placehold.co/164x164?text=B-12', - views: [ - { - viewId: 2, - viewLabel: '정면', - imageUrl: 'https://placehold.co/343x343?text=B-12-front', - }, - { - viewId: 3, - viewLabel: '왼쪽', - imageUrl: 'https://placehold.co/343x343?text=B-12-left', - }, - ], + name: '분리형 원룸', + imageUrls: ['https://placehold.co/343x343?text=2-A'], + isLatest: false, }, { id: 3, - name: 'C타입 · 15평', - houseType: { id: 'VILLA', label: '빌라/다세대' }, - structure: { id: 'TWO_ROOM', label: '투룸' }, - areaType: { id: 'TENS', label: '10평대' }, - thumbnailUrl: 'https://placehold.co/164x164?text=C-15', - views: [ - { - viewId: 4, - viewLabel: '정면', - imageUrl: 'https://placehold.co/343x343?text=C-15-front', - }, + name: 'ㄱ자형 투룸', + imageUrls: [ + 'https://placehold.co/343x343?text=3-A', + 'https://placehold.co/343x343?text=3-B', + 'https://placehold.co/343x343?text=3-C', ], + isLatest: false, }, { id: 4, - name: 'D타입 · 22평', - houseType: { id: 'APARTMENT', label: '아파트' }, - structure: { id: 'THREE_ROOM_PLUS', label: '쓰리룸 이상' }, - areaType: { id: 'TWENTIES', label: '20평대' }, - thumbnailUrl: 'https://placehold.co/164x164?text=D-22', - views: [ - { - viewId: 5, - viewLabel: '정면', - imageUrl: 'https://placehold.co/343x343?text=D-22-front', - }, - { - viewId: 6, - viewLabel: '왼쪽', - imageUrl: 'https://placehold.co/343x343?text=D-22-left', - }, - { - viewId: 7, - viewLabel: '오른쪽', - imageUrl: 'https://placehold.co/343x343?text=D-22-right', - }, - ], + name: '복층형 쓰리룸', + imageUrls: ['https://placehold.co/343x343?text=4-A'], + isLatest: false, }, { id: 5, - name: 'E타입 · 6평', - houseType: { id: 'OFFICETEL', label: '오피스텔' }, - structure: { id: 'OPEN_ONE_ROOM', label: '오픈형 원룸' }, - areaType: { id: 'FROM_5_TO_10', label: '5-10평' }, - thumbnailUrl: 'https://placehold.co/164x164?text=E-6', - views: [ - { - viewId: 8, - viewLabel: '정면', - imageUrl: 'https://placehold.co/343x343?text=E-6-front', - }, + name: '오픈형 원룸', + imageUrls: [ + 'https://placehold.co/343x343?text=5-A', + 'https://placehold.co/343x343?text=5-B', ], + isLatest: false, }, { id: 6, - name: 'F타입 · 3평', - houseType: { id: 'ETC', label: '그 외' }, - structure: { id: 'OPEN_ONE_ROOM', label: '오픈형 원룸' }, - areaType: { id: 'UNDER_4', label: '4평 이하' }, - thumbnailUrl: 'https://placehold.co/164x164?text=F-3', - views: [ - { - viewId: 9, - viewLabel: '정면', - imageUrl: 'https://placehold.co/343x343?text=F-3-front', - }, - ], + name: '소형 원룸', + imageUrls: ['https://placehold.co/343x343?text=6-A'], + isLatest: false, }, { id: 7, - name: 'G타입 · 18평', - houseType: { id: 'VILLA', label: '빌라/다세대' }, - structure: { id: 'DUPLEX', label: '복층형' }, - areaType: { id: 'TENS', label: '10평대' }, - thumbnailUrl: 'https://placehold.co/164x164?text=G-18', - views: [ - { - viewId: 10, - viewLabel: '정면', - imageUrl: 'https://placehold.co/343x343?text=G-18-front', - }, - { - viewId: 11, - viewLabel: '오른쪽', - imageUrl: 'https://placehold.co/343x343?text=G-18-right', - }, - ], + name: '복층형 투룸', + imageUrls: ['https://placehold.co/343x343?text=7-A'], + isLatest: false, }, { id: 8, - name: 'H타입 · 35평', - houseType: { id: 'APARTMENT', label: '아파트' }, - structure: { id: 'THREE_ROOM_PLUS', label: '쓰리룸 이상' }, - areaType: { id: 'OVER_30', label: '30평 이상' }, - thumbnailUrl: 'https://placehold.co/164x164?text=H-35', - views: [ - { - viewId: 12, - viewLabel: '정면', - imageUrl: 'https://placehold.co/343x343?text=H-35-front', - }, + name: '대형 아파트', + imageUrls: [ + 'https://placehold.co/343x343?text=8-A', + 'https://placehold.co/343x343?text=8-B', ], + isLatest: false, }, ]; -// null로 시작 — 테스트 시 값 넣어서 확인 +// 최근 생성 도면 (별도 API 응답) — null로 시작, 테스트 시 값 넣어서 확인 export const DUMMY_RECENT_FLOOR_PLAN: RecentFloorPlanData | null = null; diff --git a/src/pages/imageSetup/v2/hooks/useFloorPlanSelect.ts b/src/pages/imageSetup/v2/hooks/useFloorPlanSelect.ts index e2c46b3d..49098136 100644 --- a/src/pages/imageSetup/v2/hooks/useFloorPlanSelect.ts +++ b/src/pages/imageSetup/v2/hooks/useFloorPlanSelect.ts @@ -12,7 +12,7 @@ import type { CompletedFloorPlan, ImageSetupSteps, } from '../../types/funnel/steps'; -import type { FloorPlanData } from '../types/floorPlan'; +import type { FloorPlanData, RecentFloorPlanData } from '../types/floorPlan'; export const useFloorPlanSelect = ( context: ImageSetupSteps['FloorPlan'], @@ -21,10 +21,12 @@ export const useFloorPlanSelect = ( const store = useFloorPlanStore(); const savedHouseInfo = useFunnelStore((state) => state.houseInfo); - // 더미 데이터 (추후 useFloorPlanQuery로 교체) + // 더미 데이터 (추후 useFloorPlanQuery / useRecentFloorPlanQuery로 교체) const filterCategories = DUMMY_FILTER_CATEGORIES; const allFloorPlans = DUMMY_FLOOR_PLANS; - const recentFloorPlan = DUMMY_RECENT_FLOOR_PLAN; + + // 최근 생성 도면 (별도 API 응답) + const recentFloorPlan: RecentFloorPlanData | null = DUMMY_RECENT_FLOOR_PLAN; // 최근 생성 공간이 있으면 초기 시트 표시 useEffect(() => { @@ -36,20 +38,10 @@ export const useFloorPlanSelect = ( }, []); // 필터링된 도면 리스트 - // appliedFilters 기준으로 filteredFloorPlans 반환 - const filteredFloorPlans = useMemo(() => { - const { houseType, structure, areaType } = store.appliedFilters; - - return allFloorPlans.filter((plan) => { - if (houseType !== 'ALL' && plan.houseType.id !== houseType) return false; - if (structure !== 'ALL' && plan.structure.id !== structure) return false; - if (areaType !== 'ALL' && plan.areaType.id !== areaType) return false; - return true; - }); - }, [allFloorPlans, store.appliedFilters]); - - // 선택된 공간 데이터 - // selectedFloorPlanId로 해당 객체 찾아서 반환 + // TODO: API 연동 시 서버사이드 필터링으로 교체 (query param: residenceType, layoutType, areaSize) + const filteredFloorPlans = useMemo(() => allFloorPlans, [allFloorPlans]); + + // 선택된 도면 데이터 const selectedFloorPlan: FloorPlanData | null = useMemo( () => store.selectedFloorPlanId @@ -68,22 +60,15 @@ export const useFloorPlanSelect = ( [store] ); - // TODO: 바텀시트 use-overlay 적용 - // TODO: 바텀시트 dismiss 시 처리할 로직 확인(상태 초기화 등) - // 도면 선택 후 "공간 선택하기" CTA const handleConfirmFloorPlan = useCallback(() => { if (!selectedFloorPlan) return; - const currentView = selectedFloorPlan.views[store.selectedViewIndex]; - const floorPlanData = { floorPlanId: selectedFloorPlan.id, isMirror: store.isMirror, - viewId: currentView?.viewId ?? null, }; - // TODO: useFunnelStore 수정(각 스텝 별 필요한 데이터 달라짐) useFunnelStore.getState().setFloorPlan(floorPlanData); const payload: CompletedFloorPlan = { @@ -95,25 +80,15 @@ export const useFloorPlanSelect = ( }; onNext(payload); - }, [ - selectedFloorPlan, - store.selectedViewIndex, - store.isMirror, - savedHouseInfo, - context, - onNext, - ]); - - // 저장된 내 공간 바텀시트 "선택 완료하기" CTA + }, [selectedFloorPlan, store.isMirror, savedHouseInfo, context, onNext]); + + // 최근 생성 공간 바텀시트 "선택 완료" CTA const handleConfirmRecentFloorPlan = useCallback(() => { if (!recentFloorPlan) return; - const firstView = recentFloorPlan.views[0]; - const floorPlanData = { - floorPlanId: recentFloorPlan.floorPlanId, + floorPlanId: recentFloorPlan.id, isMirror: false, - viewId: firstView?.viewId ?? null, }; useFunnelStore.getState().setFloorPlan(floorPlanData); diff --git a/src/pages/imageSetup/v2/hooks/useFloorPlanSheet.ts b/src/pages/imageSetup/v2/hooks/useFloorPlanSheet.ts index 75f839d9..ec9cf16d 100644 --- a/src/pages/imageSetup/v2/hooks/useFloorPlanSheet.ts +++ b/src/pages/imageSetup/v2/hooks/useFloorPlanSheet.ts @@ -5,38 +5,36 @@ import { useCallback, useMemo } from 'react'; import { useFloorPlanStore } from '../stores/useFloorPlanStore'; -import type { FloorPlanData } from '../types/floorPlan'; - -export const useFloorPlanSheet = (floorPlan: FloorPlanData | null) => { +export const useFloorPlanSheet = (imageUrls: string[]) => { const { selectedViewIndex, setViewIndex, isMirror, toggleMirror } = useFloorPlanStore(); - const views = floorPlan?.views ?? []; - const isSingleView = views.length === 1; - const isMultiView = views.length > 1; + const isSingleView = imageUrls.length <= 1; + const isMultiView = imageUrls.length > 1; - const currentView = useMemo( - () => views[selectedViewIndex] ?? null, - [views, selectedViewIndex] + const currentImageUrl = useMemo( + () => imageUrls[selectedViewIndex] ?? imageUrls[0] ?? null, + [imageUrls, selectedViewIndex] ); const handlePrev = useCallback(() => { - if (views.length === 0) return; - setViewIndex((selectedViewIndex - 1 + views.length) % views.length); - }, [views.length, selectedViewIndex, setViewIndex]); + if (imageUrls.length === 0) return; + setViewIndex((selectedViewIndex - 1 + imageUrls.length) % imageUrls.length); + }, [imageUrls.length, selectedViewIndex, setViewIndex]); const handleNext = useCallback(() => { - if (views.length === 0) return; - setViewIndex((selectedViewIndex + 1) % views.length); - }, [views.length, selectedViewIndex, setViewIndex]); + if (imageUrls.length === 0) return; + setViewIndex((selectedViewIndex + 1) % imageUrls.length); + }, [imageUrls.length, selectedViewIndex, setViewIndex]); return { - currentView, // 지금 보고 있는 view (이미지 URL + 라벨) - isSingleView, // view가 1개면 true, prev/next 버튼 안보여줌 - isMultiView, // view가 2개 이상, prev/next 버튼 보여줌 + currentImageUrl, // 현재 보고 있는 이미지 URL + selectedViewIndex, // 현재 뷰 인덱스 + isSingleView, // 이미지 1개 → prev/next 버튼 숨김 + isMultiView, // 이미지 2개 이상 → prev/next 버튼 표시 isMirror, // 좌우반전 상태 toggleMirror, // 좌우반전 토글 - handlePrev, // 이전 뷰 (순환: 처음에서 prev 시 마지막으로) - handleNext, // 다음 뷰 (순환: 마지막에서 next 시 첫번째로) + handlePrev, // 이전 뷰 (순환) + handleNext, // 다음 뷰 (순환) }; }; diff --git a/src/pages/imageSetup/v2/stores/useFloorPlanStore.ts b/src/pages/imageSetup/v2/stores/useFloorPlanStore.ts index 877ff222..db8ebe88 100644 --- a/src/pages/imageSetup/v2/stores/useFloorPlanStore.ts +++ b/src/pages/imageSetup/v2/stores/useFloorPlanStore.ts @@ -1,10 +1,9 @@ /** - * 공간 선택 페이지의 모든 상태를 한 곳에서 관리 - * 1. 공간 선택 상태 + * 도면 선택 페이지의 모든 상태를 한 곳에서 관리 + * 1. 도면 선택 상태 * - selectedFloorPlanId: 어떤 도면 카드를 선택했는지 - * - selectedViewIndex: 어떤 도면 뷰를 선택했는지 * - isMirror: 좌우반전 여부 - * 2. 필터 상태 + * 2. 필터 상태 (API query param 이름과 통일: residenceType, layoutType, areaSize) * - appliedFilters: 실제 적용된 필터 (도면 카드 그리드에 반영됨) * - pendingFilters: FilterSheet에서 선택했지만 아직 적용하지 않은 값 저장용 임시 필터 * 3. 시트 열림/닫힘 상태 diff --git a/src/pages/imageSetup/v2/types/floorPlan.ts b/src/pages/imageSetup/v2/types/floorPlan.ts index 3c9e94a1..15fa728b 100644 --- a/src/pages/imageSetup/v2/types/floorPlan.ts +++ b/src/pages/imageSetup/v2/types/floorPlan.ts @@ -1,10 +1,39 @@ -// 도면 뷰 -export interface FloorPlanView { - viewId: number; - viewLabel: string; // 다용도실이 있는 원룸 / 10평대 +// --- API 응답 타입 --- + +// 도면 카드 하나의 데이터 +// imageUrls: 도면 뷰 이미지 배열. [0]이 대표 이미지(그리드 썸네일), 나머지는 상세 시트에서 전환 +export interface FloorPlanData { + id: number; + name: string; + imageUrls: string[]; // 임시구현 + isLatest: boolean; +} + +// API 응답 래퍼 +export interface FloorPlanListResponse { + isExact: boolean; + floorPlans: FloorPlanData[]; +} + +// --- 최근 생성 도면 API 응답 타입 --- + +// 최근 생성에 사용된 도면 데이터 +export interface RecentFloorPlanData { + id: number; + name: string; imageUrl: string; + equilibrium: string; // 아직 Enum값 미정 + view: string; // 아직 Enum값 미정 } +// API 응답 래퍼 +export interface RecentFloorPlanResponse { + hasRecentImage: boolean; + floorPlan: RecentFloorPlanData | null; +} + +// --- 필터 관련 타입 --- + // 각 FilterCategory의 하위 선택지 export interface FilterOption { id: string; @@ -18,35 +47,16 @@ export interface FilterCategory { options: FilterOption[]; } -// 도면 카드 하나의 전체 데이터 -export interface FloorPlanData { - id: number; - name: string; - houseType: FilterOption; - structure: FilterOption; - areaType: FilterOption; - thumbnailUrl: string; - views: FloorPlanView[]; -} - -// '최근 생성한 공간' 데이터 -export interface RecentFloorPlanData { - floorPlanId: number; - name: string; - thumbnailUrl: string; - views: FloorPlanView[]; -} - -// 현재 적용된 필터 상태 (houseType, structure, areaType 각각의 선택값) +// 현재 적용된 필터 상태 (API query param 이름과 통일) export interface FloorPlanFilters { - houseType: string; - structure: string; - areaType: string; + residenceType: string; + layoutType: string; + areaSize: string; } // 필터 초기값 export const DEFAULT_FILTERS: FloorPlanFilters = { - houseType: 'ALL', - structure: 'ALL', - areaType: 'ALL', + residenceType: 'ALL', + layoutType: 'ALL', + areaSize: 'ALL', }; From 7b05776aa530efdd1703462070c58cbd47c0c851 Mon Sep 17 00:00:00 2001 From: jstar_00 Date: Wed, 18 Mar 2026 20:58:59 +0900 Subject: [PATCH 04/46] =?UTF-8?q?fix:=20PageLayout=EC=97=90=20v2=20navbar?= =?UTF-8?q?=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/pageLayout/PageLayout.tsx | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/shared/components/pageLayout/PageLayout.tsx b/src/shared/components/pageLayout/PageLayout.tsx index 130f267a..77cb8a1c 100644 --- a/src/shared/components/pageLayout/PageLayout.tsx +++ b/src/shared/components/pageLayout/PageLayout.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from 'react'; -import LogoNavBar from '@components/navBar/LogoNavBar'; -import TitleNavBar from '@components/navBar/TitleNavBar'; +import LogoNavBar from '@components/v2/navBar/LogoNavBar'; +import TitleNavBar from '@components/v2/navBar/TitleNavBar'; import * as styles from './PageLayout.css'; @@ -9,14 +9,16 @@ type HeaderConfig = | { type: 'title'; title: string; - showBackButton?: boolean; + backLabel?: string; onBackClick?: () => void; - showLoginButton?: boolean; - showSettingButton?: boolean; } | { type: 'logo'; - buttonType?: 'login' | 'profile' | null; + page?: 'landing' | 'home'; + showGenerateButton?: boolean; + authSlot?: 'none' | 'login' | 'profile'; + onGenerateClick?: () => void; + onLoginClick?: () => void; onProfileClick?: () => void; } | { type: 'none' }; @@ -34,16 +36,18 @@ const PageLayout = ({ header, children, className }: PageLayoutProps) => { return ( ); case 'logo': return ( ); @@ -53,7 +57,7 @@ const PageLayout = ({ header, children, className }: PageLayoutProps) => { }; return ( -
+
{renderHeader()}
{children}
From 8c792c670a83642f02fc9ea14141c8a8950baf21 Mon Sep 17 00:00:00 2001 From: jstar_00 Date: Wed, 18 Mar 2026 21:00:41 +0900 Subject: [PATCH 05/46] =?UTF-8?q?fix:=20RoomTypeCard=20=EB=B0=98=EC=9D=91?= =?UTF-8?q?=ED=98=95=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v2/roomTypeCard/RoomTypeCard.css.ts | 21 ++++++++++--------- .../v2/roomTypeCard/RoomTypeCard.tsx | 1 + 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/shared/components/v2/roomTypeCard/RoomTypeCard.css.ts b/src/shared/components/v2/roomTypeCard/RoomTypeCard.css.ts index 57d3e205..5bac1465 100644 --- a/src/shared/components/v2/roomTypeCard/RoomTypeCard.css.ts +++ b/src/shared/components/v2/roomTypeCard/RoomTypeCard.css.ts @@ -25,6 +25,7 @@ export const optionCard = recipe({ variants: { kind: { default: { + flexDirection: 'column', backgroundColor: colorVars.color.fill.strong, }, more: { @@ -35,20 +36,23 @@ export const optionCard = recipe({ }, size: { s: { + aspectRatio: '1 / 1', padding: unitVars.unit.gapPadding['300'], - width: '16rem', - height: '16rem', + width: '100%', + minWidth: '16rem', }, m: { + aspectRatio: '1 / 1', padding: unitVars.unit.gapPadding['300'], - width: '16.4rem', - height: '16.4rem', + width: '100%', + minWidth: '16.4rem', }, }, }, }); export const previewCard = style({ + aspectRatio: '1 / 1', position: 'relative', display: 'flex', flexShrink: 0, @@ -57,8 +61,8 @@ export const previewCard = style({ borderRadius: unitVars.unit.radius['600'], backgroundColor: colorVars.color.fill.tertiary, padding: unitVars.unit.gapPadding['100'], - width: '33.9rem', - height: '33.9rem', + width: '100%', + minWidth: '33.9rem', overflow: 'hidden', }); @@ -108,10 +112,7 @@ export const optionTitleIcon = recipe({ variants: { size: { s: {}, - m: { - paddingTop: unitVars.unit.gapPadding['050'], - paddingBottom: unitVars.unit.gapPadding['050'], - }, + m: {}, }, }, }); diff --git a/src/shared/components/v2/roomTypeCard/RoomTypeCard.tsx b/src/shared/components/v2/roomTypeCard/RoomTypeCard.tsx index 80609a97..bed2e95c 100644 --- a/src/shared/components/v2/roomTypeCard/RoomTypeCard.tsx +++ b/src/shared/components/v2/roomTypeCard/RoomTypeCard.tsx @@ -92,6 +92,7 @@ const RoomTypeOptionCard = ({ /> } primaryButton={ - + } secondaryButton={ - + 초기화 + } /> ); From 9195853c4afb2024bc0f576775606dd660741011 Mon Sep 17 00:00:00 2001 From: jstar_00 Date: Tue, 24 Mar 2026 17:40:50 +0900 Subject: [PATCH 25/46] =?UTF-8?q?fix:=20FloorPlanSheet=20ActionButton=20?= =?UTF-8?q?=EA=B3=B5=EC=BB=B4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../steps/floorPlanSelect/FloorPlanSheet.tsx | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/pages/imageSetup/v2/steps/floorPlanSelect/FloorPlanSheet.tsx b/src/pages/imageSetup/v2/steps/floorPlanSelect/FloorPlanSheet.tsx index 96419c94..eb3a2c36 100644 --- a/src/pages/imageSetup/v2/steps/floorPlanSelect/FloorPlanSheet.tsx +++ b/src/pages/imageSetup/v2/steps/floorPlanSelect/FloorPlanSheet.tsx @@ -1,11 +1,10 @@ import clsx from 'clsx'; -import IcnDoubleStar from '@assets/v2/svg/IcnDoubleStar.svg?react'; - import CloseBottomSheet from '@components/v2/bottomSheet/CloseBottomSheet'; +import ActionButton from '@components/v2/button/actionButton/ActionButton'; +import Icon from '@components/v2/icon/Icon'; import RoomTypeCard from '@components/v2/roomTypeCard/RoomTypeCard'; -import * as buttonStyles from './ActionButton.css'; import * as styles from './FloorPlanSheet.css'; import { useFloorPlanSheet } from '../../hooks/useFloorPlanSheet'; @@ -43,7 +42,7 @@ const FloorPlanSheet = ({ onClose={onClose} titleSlot={
-
} primaryButton={ - + } secondaryButton={ - + } /> ); From 46ad1a154013f01ebfd26c9ed8f3c3199bd921bc Mon Sep 17 00:00:00 2001 From: jstar_00 Date: Tue, 24 Mar 2026 17:48:58 +0900 Subject: [PATCH 26/46] =?UTF-8?q?fix:=20RecentFloorPlanSheet=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20(FloorPlanSheet=EB=A1=9C=20=ED=86=B5=EC=9D=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../floorPlanSelect/FloorPlanSelectStep.tsx | 7 +- .../floorPlanSelect/RecentFloorPlanSheet.tsx | 73 ------------------- 2 files changed, 3 insertions(+), 77 deletions(-) delete mode 100644 src/pages/imageSetup/v2/steps/floorPlanSelect/RecentFloorPlanSheet.tsx diff --git a/src/pages/imageSetup/v2/steps/floorPlanSelect/FloorPlanSelectStep.tsx b/src/pages/imageSetup/v2/steps/floorPlanSelect/FloorPlanSelectStep.tsx index e512c95f..3b723254 100644 --- a/src/pages/imageSetup/v2/steps/floorPlanSelect/FloorPlanSelectStep.tsx +++ b/src/pages/imageSetup/v2/steps/floorPlanSelect/FloorPlanSelectStep.tsx @@ -3,7 +3,6 @@ import PageLayout from '@components/pageLayout/PageLayout'; import FilterSheet from './FilterSheet'; import FloorPlanSelectGrid from './FloorPlanSelectGrid'; import FloorPlanSheet from './FloorPlanSheet'; -import RecentFloorPlanSheet from './RecentFloorPlanSheet'; import { useFloorPlanSelect } from '../../hooks/useFloorPlanSelect'; import { useFloorPlanStore } from '../../stores/useFloorPlanStore'; @@ -72,12 +71,12 @@ const FloorPlanSelectStep = ({ context, onNext }: FloorPlanSelectStepProps) => { /> {hasRecentFloorPlan && recentFloorPlan && ( - )} diff --git a/src/pages/imageSetup/v2/steps/floorPlanSelect/RecentFloorPlanSheet.tsx b/src/pages/imageSetup/v2/steps/floorPlanSelect/RecentFloorPlanSheet.tsx deleted file mode 100644 index 26368fbf..00000000 --- a/src/pages/imageSetup/v2/steps/floorPlanSelect/RecentFloorPlanSheet.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import IcnDoubleStar from '@assets/v2/svg/IcnDoubleStar.svg?react'; - -import CloseBottomSheet from '@components/v2/bottomSheet/CloseBottomSheet'; -import RoomTypeCard from '@components/v2/roomTypeCard/RoomTypeCard'; - -import * as buttonStyles from './ActionButton.css'; -import * as styles from './FloorPlanSheet.css'; - -import type { RecentFloorPlanData } from '../../types/floorPlan'; - -interface RecentFloorPlanSheetProps { - open: boolean; - onClose: () => void; - recentFloorPlan: RecentFloorPlanData; - onConfirm: () => void; - onSelectOther: () => void; -} - -const RecentFloorPlanSheet = ({ - open, - onClose, - recentFloorPlan, - onConfirm, - onSelectOther, -}: RecentFloorPlanSheetProps) => { - return ( - -
- } - contentSlot={ -
- -

{recentFloorPlan.view}

-
- } - primaryButton={ - - } - secondaryButton={ - - } - /> - ); -}; - -export default RecentFloorPlanSheet; From 4fcd5fe6cd9383df0879110245538c82979f7dd7 Mon Sep 17 00:00:00 2001 From: jstar_00 Date: Tue, 24 Mar 2026 17:52:37 +0900 Subject: [PATCH 27/46] =?UTF-8?q?feat:=20=EC=B5=9C=EA=B7=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EA=B3=B5=EA=B0=84=20sheet=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20=EC=8B=9C=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - '저장된 내 공간을 불러왔어요' --- src/pages/imageSetup/v2/hooks/useFloorPlanSelect.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/imageSetup/v2/hooks/useFloorPlanSelect.ts b/src/pages/imageSetup/v2/hooks/useFloorPlanSelect.ts index 7701baf7..85048b35 100644 --- a/src/pages/imageSetup/v2/hooks/useFloorPlanSelect.ts +++ b/src/pages/imageSetup/v2/hooks/useFloorPlanSelect.ts @@ -1,5 +1,7 @@ import { useCallback, useEffect, useMemo } from 'react'; +import { useToast } from '@components/toast/useToast'; + import { useFunnelStore } from '../../stores/useFunnelStore'; import { DUMMY_FILTER_CATEGORIES, @@ -26,6 +28,7 @@ export const useFloorPlanSelect = ( ) => { const store = useFloorPlanStore(); const savedHouseInfo = useFunnelStore((state) => state.houseInfo); + const { notify } = useToast(); // 더미 데이터 (추후 useFloorPlanQuery / useRecentFloorPlanQuery로 교체) const filterCategories = DUMMY_FILTER_CATEGORIES; @@ -34,10 +37,11 @@ export const useFloorPlanSelect = ( // 최근 생성 도면 (별도 API 응답) const recentFloorPlan: RecentFloorPlanData | null = DUMMY_RECENT_FLOOR_PLAN; - // 최근 생성 공간이 있으면 초기 시트 표시 + // 최근 생성 공간이 있으면 초기 시트 표시 + 토스트 알림 useEffect(() => { if (recentFloorPlan) { store.openRecentSheet(); + notify({ text: '저장된 내 공간을 불러왔어요.' }); } // 마운트 시 1회만 실행 // eslint-disable-next-line react-hooks/exhaustive-deps From 41706fba7768fc434f6fe4ae1ee26ba8bc3de4e0 Mon Sep 17 00:00:00 2001 From: jstar_00 Date: Tue, 24 Mar 2026 17:52:53 +0900 Subject: [PATCH 28/46] =?UTF-8?q?feat:=20=EC=9E=84=EC=8B=9C=20ActionButton?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../steps/floorPlanSelect/ActionButton.css.ts | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 src/pages/imageSetup/v2/steps/floorPlanSelect/ActionButton.css.ts diff --git a/src/pages/imageSetup/v2/steps/floorPlanSelect/ActionButton.css.ts b/src/pages/imageSetup/v2/steps/floorPlanSelect/ActionButton.css.ts deleted file mode 100644 index 6f066a1a..00000000 --- a/src/pages/imageSetup/v2/steps/floorPlanSelect/ActionButton.css.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { style } from '@vanilla-extract/css'; - -import { colorVars } from '@styles/tokensV2/color.css'; -import { fontVars } from '@styles/tokensV2/font.css'; -import { unitVars } from '@styles/tokensV2/unit.css'; - -const base = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - transition: 'transform 100ms ease', - border: 0, - borderRadius: unitVars.unit.radius['600'], - width: '100%', - height: '4.8rem', - ...fontVars.font.title_m_16, - selectors: { - '&:active': { - transform: 'scale(0.98)', - }, - }, -}); - -export const primary = style([ - base, - { - backgroundColor: colorVars.color.fill.strong, - color: colorVars.color.text.inverse, - }, -]); - -export const secondary = style([ - base, - { - backgroundColor: colorVars.color.fill.tertiary, - color: colorVars.color.text.primary, - }, -]); From 8b0e8d4fcf2208f33b2714f658b56d8e9b604e29 Mon Sep 17 00:00:00 2001 From: jstar_00 Date: Tue, 24 Mar 2026 17:53:34 +0900 Subject: [PATCH 29/46] =?UTF-8?q?fix:=20FloorPlanSelectGrid,=20BottomSheet?= =?UTF-8?q?,=20TitleNavBar=EC=97=90=20v2=20=EC=95=84=EC=9D=B4=EC=BD=98=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v2/steps/floorPlanSelect/FloorPlanSelectGrid.tsx | 10 ++++++---- .../components/v2/bottomSheet/BottomSheetBase.tsx | 4 ++-- src/shared/components/v2/navBar/TitleNavBar.tsx | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/pages/imageSetup/v2/steps/floorPlanSelect/FloorPlanSelectGrid.tsx b/src/pages/imageSetup/v2/steps/floorPlanSelect/FloorPlanSelectGrid.tsx index e677152e..57a88ccf 100644 --- a/src/pages/imageSetup/v2/steps/floorPlanSelect/FloorPlanSelectGrid.tsx +++ b/src/pages/imageSetup/v2/steps/floorPlanSelect/FloorPlanSelectGrid.tsx @@ -1,4 +1,5 @@ import Chip from '@components/v2/chip/Chip'; +import Icon from '@components/v2/icon/Icon'; import RoomTypeCard from '@components/v2/roomTypeCard/RoomTypeCard'; import * as styles from './FloorPlanSelectGrid.css'; @@ -54,11 +55,12 @@ const FloorPlanSelectGrid = ({ - {isFiltered ? '✕' : '▾'} - + isFiltered ? ( + + ) : ( + + ) } suffixAriaLabel={ isFiltered ? `${category.label} 필터 초기화` : undefined diff --git a/src/shared/components/v2/bottomSheet/BottomSheetBase.tsx b/src/shared/components/v2/bottomSheet/BottomSheetBase.tsx index 8dfe8557..9086b9ca 100644 --- a/src/shared/components/v2/bottomSheet/BottomSheetBase.tsx +++ b/src/shared/components/v2/bottomSheet/BottomSheetBase.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react'; import { Drawer } from 'vaul'; -import IcnX from '@assets/v2/svg/IcnX.svg?react'; +import Icon from '@components/v2/icon/Icon'; import * as styles from './BottomSheetBase.css'; @@ -102,7 +102,7 @@ const BottomSheetBase = ({ className={styles.closeButton} onClick={onCloseClick} > -