Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0bed44d
feat: 퍼널 navBar 타이틀 분기처리 적용
jstar000 Apr 8, 2026
27ba0f4
feat: InteriorStyle 스텝 B-2 디자인 수정 적용 (레이아웃 수정)
jstar000 Apr 8, 2026
777985a
refactor: CardImage 컴포넌트 삭제 및 MoodboardCard 추가
jstar000 Apr 8, 2026
95e9cbe
feat: MoodboardCard 드래그 방지, circle CSS 수정
jstar000 Apr 8, 2026
12b7325
refactor: InteriorStyle LargeFilledButton을 ActionButton으로 변경
jstar000 Apr 8, 2026
40f5505
fix: MoodboardCard 접근성 개선
jstar000 Apr 8, 2026
5fccccf
refactor: ActivityInfo API를 activities/categories 두 엔드포인트로 분리(B-2 API…
jstar000 Apr 9, 2026
c0de482
refactor: useActivitySelection 훅을 B-2 주요활동/가구 API 명세에 맞게 수정
jstar000 Apr 9, 2026
5352c55
refactor: useActivitySelection 훅을 B-2 주요활동/가구 API 명세에 맞게 수정
jstar000 Apr 9, 2026
9c4f46e
feat: 아이콘 svg 추가
jstar000 Apr 9, 2026
1a32382
feat: SelectTrigger 컴포넌트, ActivityIcon.ts 추가
jstar000 Apr 9, 2026
190879b
feat: ActivityTypeSheet 구현(주요활동 선택 바텀시트)
jstar000 Apr 9, 2026
8d9f75d
feat: UI 테스트용 MSW 활성화
jstar000 Apr 9, 2026
01d464a
refactor: ActivityInfo 페이지 UI B-2 디자인으로 리뉴얼
jstar000 Apr 9, 2026
216fc39
fix: ActivityInfo 페이지 UI 피그마 디자인에 맞게 수정
jstar000 Apr 9, 2026
9b7ca78
feat: Lock 아이콘 추가
jstar000 Apr 9, 2026
7796ad8
feat: Chip disabled 상태 css 적용
jstar000 Apr 9, 2026
5f716c8
feat: Chip 수정 + 필수가구 Lock 아이콘 적용
jstar000 Apr 9, 2026
c61e9ae
refactor: 주요활동 바텀시트 overlay-kit 적용
jstar000 Apr 9, 2026
af7c65b
refactor: selectionByIndex 하드코딩 -> selectionByNameEng (API 카테고리 순서 변경…
jstar000 Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 26 additions & 15 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ import '@styles/global.css';

import App from './App';

// TODO: UI 테스트용 MSW 활성화 — API 배포 후 제거
async function enableMocking() {
if (!import.meta.env.DEV) return;
const { worker } = await import('./mocks/browser');
return worker.start({ onUnhandledRequest: 'bypass' });
}

initSentry();
initClarity();

Expand All @@ -43,18 +50,22 @@ if (!rootElement) {
throw new Error('Root element not found');
}

createRoot(rootElement, getSentryReactErrorHandlerOptions()).render(
// <StrictMode>
<ErrorBoundary FallbackComponent={AppErrorFallback}>
<HelmetProvider>
<QueryClientProvider client={queryClient}>
<OverlayProvider>
<App />
<ToastContainer {...toastConfig} />
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
</OverlayProvider>
</QueryClientProvider>
</HelmetProvider>
</ErrorBoundary>
// </StrictMode>
);
enableMocking().then(() => {
createRoot(rootElement, getSentryReactErrorHandlerOptions()).render(
// <StrictMode>
<ErrorBoundary FallbackComponent={AppErrorFallback}>
<HelmetProvider>
<QueryClientProvider client={queryClient}>
<OverlayProvider>
<App />
<ToastContainer {...toastConfig} />
{import.meta.env.DEV && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</OverlayProvider>
</QueryClientProvider>
</HelmetProvider>
</ErrorBoundary>
// </StrictMode>
);
});
Comment on lines +53 to +71
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

MSW 초기화 실패 시 앱이 렌더링되지 않는 경로가 있습니다.

현재는 enableMocking() 성공 시에만 render가 실행됩니다. 초기화 실패 시에도 앱은 뜨도록 fallback을 보장해야 합니다.

🛠️ 제안 수정안
-enableMocking().then(() => {
-  createRoot(rootElement, getSentryReactErrorHandlerOptions()).render(
+const renderApp = () => {
+  createRoot(rootElement, getSentryReactErrorHandlerOptions()).render(
     // <StrictMode>
     <ErrorBoundary FallbackComponent={AppErrorFallback}>
       <HelmetProvider>
         <QueryClientProvider client={queryClient}>
           <OverlayProvider>
@@
     </ErrorBoundary>
     // </StrictMode>
   );
-});
+};
+
+void enableMocking()
+  .catch((error) => {
+    console.error('MSW initialization failed', error);
+  })
+  .finally(() => {
+    renderApp();
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.tsx` around lines 53 - 71, The app currently only calls
createRoot(...).render(...) inside the enableMocking() promise success path, so
if enableMocking() rejects the app never mounts; change the flow to always call
createRoot(rootElement, getSentryReactErrorHandlerOptions()).render(...)
regardless of enableMocking() outcome by moving the render call out of the
then-only branch and handling enableMocking() errors separately (e.g., catch the
rejection, log the error, then proceed to render), referencing enableMocking(),
createRoot(...).render, rootElement, getSentryReactErrorHandlerOptions(), and
AppErrorFallback so the ErrorBoundary/HelmetProvider/QueryClientProvider tree is
mounted even when mocking initialization fails.

72 changes: 48 additions & 24 deletions src/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,60 +28,84 @@ export const handlers = [
});
}),

// GET /api/v1/dashboard-info활동 옵션 (prefetchStaticData에서 호출)
http.get('*/api/v1/dashboard-info', () => {
// GET /api/v2/dashboard/activities주요활동 + 활동별 필수 가구
http.get('*/api/v2/dashboard/activities', () => {
return HttpResponse.json({
code: 200,
message: 'OK',
data: {
activities: [
{ code: 'COOKING', label: '요리' },
{ code: 'READING', label: '독서' },
{
code: 'REMOTE_WORK',
label: '재택근무형',
furnitures: [{ id: 7, code: 'DESK', label: '업무용 책상' }],
},
{
code: 'READING',
label: '독서형',
furnitures: [{ id: 12, code: 'BOOKSHELF', label: '책 선반' }],
},
{
code: 'FLOOR_LIVING',
label: '좌식 생활형',
furnitures: [{ id: 9, code: 'LOW_TABLE', label: '좌식 테이블' }],
},
{
code: 'HOME_CAFE',
label: '홈카페형',
furnitures: [{ id: 8, code: 'DINING_TABLE', label: '식탁' }],
},
],
},
});
}),

// GET /api/v2/dashboard/categories — 가구 카테고리 + 카테고리별 가구
http.get('*/api/v2/dashboard/categories', () => {
return HttpResponse.json({
code: 200,
message: 'OK',
data: {
categories: [
{
categoryId: 1,
nameKr: '침대',
nameKr: '침대/프레임',
nameEng: 'BED',
furnitures: [
{ id: 1, code: 'BED_SINGLE', label: '싱글 침대' },
{ id: 2, code: 'BED_DOUBLE', label: '더블 침대' },
{ id: 1, code: 'BED_SINGLE', label: '싱글' },
{ id: 2, code: 'BED_SUPER_SINGLE', label: '슈퍼싱글' },
{ id: 3, code: 'BED_DOUBLE', label: '더블' },
{ id: 4, code: 'BED_QUEEN_OR_LARGER', label: '퀸 이상' },
],
},
{
categoryId: 2,
nameKr: '소파',
nameEng: 'SOFA',
furnitures: [
{ id: 3, code: 'SOFA_2', label: '2인 소파' },
{ id: 4, code: 'SOFA_3', label: '3인 소파' },
{ id: 5, code: 'SOFA_SINGLE', label: '1인용' },
{ id: 6, code: 'SOFA_DOUBLE', label: '2인용' },
],
},
{
categoryId: 3,
nameKr: '수납',
nameEng: 'STORAGE',
furnitures: [
{ id: 5, code: 'STORAGE_SHELF', label: '선반' },
{ id: 6, code: 'STORAGE_WARDROBE', label: '옷장' },
],
},
{
categoryId: 4,
nameKr: '테이블',
nameEng: 'TABLE',
furnitures: [
{ id: 7, code: 'TABLE_DINING', label: '식탁' },
{ id: 8, code: 'TABLE_COFFEE', label: '커피 테이블' },
{ id: 7, code: 'DESK', label: '업무용 책상' },
{ id: 8, code: 'DINING_TABLE', label: '식탁' },
{ id: 9, code: 'LOW_TABLE', label: '좌식 테이블' },
],
},
{
categoryId: 5,
nameKr: '선택 가구',
categoryId: 4,
nameKr: '그 외',
nameEng: 'SELECTIVE',
furnitures: [
{ id: 9, code: 'SELECTIVE_PLANT', label: '화분' },
{ id: 10, code: 'SELECTIVE_LAMP', label: '스탠드 조명' },
{ id: 10, code: 'MOVABLE_TV', label: '이동식 TV' },
{ id: 11, code: 'FULL_MIRROR', label: '전신 거울' },
{ id: 12, code: 'BOOKSHELF', label: '책 선반' },
{ id: 13, code: 'DISPLAY_CABINET', label: '장식장' },
],
},
],
Expand Down
24 changes: 24 additions & 0 deletions src/pages/imageSetup/apis/queries/useActivitiesQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useQuery } from '@tanstack/react-query';

import { HTTPMethod, request } from '@apis/config/request';

import { API_ENDPOINT } from '@constants/apiEndpoints';
import { queryKeys } from '@constants/queryKey';

import type { ActivitiesResponse } from '../../types/apis/activityInfo';

export const getActivities = async (): Promise<ActivitiesResponse> => {
return request<ActivitiesResponse>({
method: HTTPMethod.GET,
url: API_ENDPOINT.IMAGE_SETUP.ACTIVITIES,
});
};

export const useActivitiesQuery = () => {
return useQuery({
queryKey: queryKeys.imageSetup.activities(),
queryFn: getActivities,
staleTime: Infinity,
gcTime: 1000 * 60 * 60 * 24,
});
};
25 changes: 0 additions & 25 deletions src/pages/imageSetup/apis/queries/useActivityOptionsQuery.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useQuery } from '@tanstack/react-query';

import { HTTPMethod, request } from '@apis/config/request';

import { API_ENDPOINT } from '@constants/apiEndpoints';
import { queryKeys } from '@constants/queryKey';

import type { FurnitureCategoriesResponse } from '../../types/apis/activityInfo';

export const getFurnitureCategories =
async (): Promise<FurnitureCategoriesResponse> => {
return request<FurnitureCategoriesResponse>({
method: HTTPMethod.GET,
url: API_ENDPOINT.IMAGE_SETUP.FURNITURE_CATEGORIES,
});
};

export const useFurnitureCategoriesQuery = () => {
return useQuery({
queryKey: queryKeys.imageSetup.furnitureCategories(),
queryFn: getFurnitureCategories,
staleTime: Infinity,
gcTime: 1000 * 60 * 60 * 24,
});
};
21 changes: 15 additions & 6 deletions src/pages/imageSetup/components/layout/FunnelLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
import { overlay } from 'overlay-kit';
import { useNavigate } from 'react-router-dom';

import TitleNavBar from '@components/navBar/TitleNavBar';
import Popup from '@components/overlay/popup/Popup';

import TitleNavBar from '@/shared/components/v2/navBar/TitleNavBar';

import * as styles from './FunnelLayout.css';
import {
logSelectHouseInfoClickModalContinue,
logSelectHouseInfoClickModalExit,
logSelectHouseInfoViewModal,
} from '../../utils/analytics';

type FunnelStepKey = 'FloorPlanSelect' | 'InteriorStyle' | 'ActivityInfo';

// 퍼널 스텝 별 NavBar 타이틀 매핑
const NAVBAR_TITLE_BY_STEP: Record<FunnelStepKey, string> = {
FloorPlanSelect: '공간 선택하기',
InteriorStyle: '취향 선택하기',
ActivityInfo: '가구 선택하기',
};
Comment on lines +15 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

FunnelStepKey 타입 export 고려

FunnelStepKey 타입이 다른 퍼널 관련 컴포넌트에서도 사용될 수 있으므로, export 여부를 검토해주세요. 현재는 로컬 타입으로만 정의되어 있어요.

♻️ 타입 export 제안
-type FunnelStepKey = 'FloorPlanSelect' | 'InteriorStyle' | 'ActivityInfo';
+export type FunnelStepKey = 'FloorPlanSelect' | 'InteriorStyle' | 'ActivityInfo';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type FunnelStepKey = 'FloorPlanSelect' | 'InteriorStyle' | 'ActivityInfo';
// 퍼널 스텝 별 NavBar 타이틀 매핑
const NAVBAR_TITLE_BY_STEP: Record<FunnelStepKey, string> = {
FloorPlanSelect: '공간 선택하기',
InteriorStyle: '취향 선택하기',
ActivityInfo: '가구 선택하기',
};
export type FunnelStepKey = 'FloorPlanSelect' | 'InteriorStyle' | 'ActivityInfo';
// 퍼널 스텝 별 NavBar 타이틀 매핑
const NAVBAR_TITLE_BY_STEP: Record<FunnelStepKey, string> = {
FloorPlanSelect: '공간 선택하기',
InteriorStyle: '취향 선택하기',
ActivityInfo: '가구 선택하기',
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/imageSetup/components/layout/FunnelLayout.tsx` around lines 15 -
22, The FunnelStepKey type is currently local but should be exported so other
funnel-related components can reuse it; change the declaration to export the
type (export type FunnelStepKey = ...) and keep NAVBAR_TITLE_BY_STEP as
Record<FunnelStepKey, string> to preserve typings, then update any consumers to
import { FunnelStepKey } from this module where needed.


interface FunnelLayoutProps {
children: React.ReactNode;
currentStep: 'FloorPlanSelect' | 'InteriorStyle' | 'ActivityInfo';
currentStep: FunnelStepKey;
}

const FunnelLayout = ({ children, currentStep }: FunnelLayoutProps) => {
const navigate = useNavigate();

// TODO: 퍼널 전체 탈출 가드 useBlocker로 바꾸기
const handleBackClick = () => {
if (currentStep === 'FloorPlanSelect') {
logSelectHouseInfoViewModal();
Expand All @@ -43,10 +54,8 @@ const FunnelLayout = ({ children, currentStep }: FunnelLayoutProps) => {
return (
<div className={styles.wrapper}>
<TitleNavBar
// TODO: 각 스텝별 헤더 타이틀 설정하기
title="스타일링 이미지 생성"
isBackIcon={true}
isLoginBtn={false}
title={NAVBAR_TITLE_BY_STEP[currentStep]}
backLabel="이전"
onBackClick={
currentStep === 'FloorPlanSelect' ? handleBackClick : undefined
}
Comment on lines 59 to 61
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wire back navigation for non-floor funnel steps

The new header wiring passes undefined for onBackClick outside FloorPlanSelect, but the v2 TitleNavBar back button only invokes the provided callback and no longer falls back to navigate(-1). This makes the visible back button a no-op on InteriorStyle and ActivityInfo, so users cannot move back through the funnel from the header.

Useful? React with 👍 / 👎.

Expand Down
Loading
Loading