From cdd28513b0fa0b8945b16d87dc370382331e1526 Mon Sep 17 00:00:00 2001 From: JW_Ahn0 Date: Mon, 19 Jan 2026 17:56:45 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B4=80=EB=A0=A8=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MypageMenuItem 컴포넌트 생성 (개별 메뉴 아이템) - MypageSection 컴포넌트 생성 (섹션 컨테이너) - app/mypage/page.tsx에서 중복 코드 제거 및 재사용 컴포넌트 적용 - "내 정보"와 "내 활동" 섹션을 공통 컴포넌트로 통일 - 관련 아이콘 파일 추가 --- app/mypage/page.tsx | 122 ++++++++++++++++++ src/assets/icons/mypage/footPrintIcon.tsx | 17 +++ src/assets/icons/mypage/mapPinIcon.tsx | 17 +++ src/assets/icons/mypage/myPageArrow.tsx | 17 +++ src/assets/icons/mypage/pinsetIcon.tsx | 14 ++ src/assets/icons/mypage/recentIcon.tsx | 16 +++ src/assets/icons/mypage/settingsIcon.tsx | 18 +++ src/assets/icons/svgFile/mypage/footprint.svg | 10 ++ .../icons/svgFile/mypage/mapPinIcon.svg | 10 ++ .../icons/svgFile/mypage/myPageArrowIcon.svg | 10 ++ .../icons/svgFile/mypage/pinsetIcon.svg | 7 + src/assets/icons/svgFile/mypage/recent.svg | 10 ++ .../icons/svgFile/mypage/settingsIcon.svg | 11 ++ src/features/mypage/ui/index.ts | 2 + src/features/mypage/ui/mypageMenuItem.tsx | 30 +++++ src/features/mypage/ui/mypageSection.tsx | 25 ++++ 16 files changed, 336 insertions(+) create mode 100644 app/mypage/page.tsx create mode 100644 src/assets/icons/mypage/footPrintIcon.tsx create mode 100644 src/assets/icons/mypage/mapPinIcon.tsx create mode 100644 src/assets/icons/mypage/myPageArrow.tsx create mode 100644 src/assets/icons/mypage/pinsetIcon.tsx create mode 100644 src/assets/icons/mypage/recentIcon.tsx create mode 100644 src/assets/icons/mypage/settingsIcon.tsx create mode 100644 src/assets/icons/svgFile/mypage/footprint.svg create mode 100644 src/assets/icons/svgFile/mypage/mapPinIcon.svg create mode 100644 src/assets/icons/svgFile/mypage/myPageArrowIcon.svg create mode 100644 src/assets/icons/svgFile/mypage/pinsetIcon.svg create mode 100644 src/assets/icons/svgFile/mypage/recent.svg create mode 100644 src/assets/icons/svgFile/mypage/settingsIcon.svg create mode 100644 src/features/mypage/ui/mypageMenuItem.tsx create mode 100644 src/features/mypage/ui/mypageSection.tsx diff --git a/app/mypage/page.tsx b/app/mypage/page.tsx new file mode 100644 index 0000000..545b35e --- /dev/null +++ b/app/mypage/page.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { SearchLine } from "@/src/assets/icons/home"; +import { ProfileDefaultImg } from "@/src/assets/images/mypage/ProfileDefaultImg"; +import Image from "next/image"; +import SettingsIcon from "@/src/assets/icons/mypage/settingsIcon"; +import FootPrintIcon from "@/src/assets/icons/mypage/footPrintIcon"; +import MapPinIcon from "@/src/assets/icons/mypage/mapPinIcon"; +import RecentIcon from "@/src/assets/icons/mypage/recentIcon"; +import PinsetIcon from "@/src/assets/icons/mypage/pinsetIcon"; +import { MypageSection } from "@/src/features/mypage/ui"; + + +export default function MypagePage() { + const imageUrl = null; + return ( +
+ {/* 헤더 */} +
+

+ 마이 페이지 +

+ +
+ +
+ {/* 사용자 정보 카드 */} +
+
+
+ {imageUrl ? ( + 프로필 사진 + ) : ( +
+ +
+ )} +
+
+ + 유저명 + + + userid@naver.com + +
+
+ +
+ + {/* 핀 보고서 섹션 */} +
+

+ 핀 보고서 +

+

+ 자격진단으로 +
+ 임대주택 지원 가능 여부를 확인하고 +
+ 맞춤 보고서를 받아보세요 +

+ +
+ + {/* 내 정보 섹션 */} + , + label: "관심 주변 환경 설정", + onClick: () => { + // TODO: 네비게이션 구현 + }, + }, + { + icon: , + label: "핀포인트 설정", + onClick: () => { + // TODO: 네비게이션 구현 + }, + }, + ]} + /> + + {/* 내 활동 섹션 */} + , + label: "저장 목록", + onClick: () => { + // TODO: 네비게이션 구현 + }, + }, + { + icon: , + label: "최근 본 공고", + onClick: () => { + // TODO: 네비게이션 구현 + }, + }, + ]} + /> +
+
+ ); +} + diff --git a/src/assets/icons/mypage/footPrintIcon.tsx b/src/assets/icons/mypage/footPrintIcon.tsx new file mode 100644 index 0000000..875f1e1 --- /dev/null +++ b/src/assets/icons/mypage/footPrintIcon.tsx @@ -0,0 +1,17 @@ +const FootPrintIcon = () => { + return ( + + + + + + + + + + + + ); +}; + +export default FootPrintIcon; \ No newline at end of file diff --git a/src/assets/icons/mypage/mapPinIcon.tsx b/src/assets/icons/mypage/mapPinIcon.tsx new file mode 100644 index 0000000..40fb6a9 --- /dev/null +++ b/src/assets/icons/mypage/mapPinIcon.tsx @@ -0,0 +1,17 @@ +const MapPinIcon = () => { + return ( + + + + + + + + + + + + ); +}; + +export default MapPinIcon; \ No newline at end of file diff --git a/src/assets/icons/mypage/myPageArrow.tsx b/src/assets/icons/mypage/myPageArrow.tsx new file mode 100644 index 0000000..bac9ce9 --- /dev/null +++ b/src/assets/icons/mypage/myPageArrow.tsx @@ -0,0 +1,17 @@ +const MyPageArrow = () => { + return ( + + + + + + + + + + + + ); +}; + +export default MyPageArrow; \ No newline at end of file diff --git a/src/assets/icons/mypage/pinsetIcon.tsx b/src/assets/icons/mypage/pinsetIcon.tsx new file mode 100644 index 0000000..8aff999 --- /dev/null +++ b/src/assets/icons/mypage/pinsetIcon.tsx @@ -0,0 +1,14 @@ +const PinsetIcon = () => { + return ( + + + + + + + + + ); +}; + +export default PinsetIcon; \ No newline at end of file diff --git a/src/assets/icons/mypage/recentIcon.tsx b/src/assets/icons/mypage/recentIcon.tsx new file mode 100644 index 0000000..50f9836 --- /dev/null +++ b/src/assets/icons/mypage/recentIcon.tsx @@ -0,0 +1,16 @@ +const RecentIcon = () => { + return ( + + + + + + + + + + + ); +}; + +export default RecentIcon; \ No newline at end of file diff --git a/src/assets/icons/mypage/settingsIcon.tsx b/src/assets/icons/mypage/settingsIcon.tsx new file mode 100644 index 0000000..d81e5d5 --- /dev/null +++ b/src/assets/icons/mypage/settingsIcon.tsx @@ -0,0 +1,18 @@ +const SettingsIcon = () => { + return ( + + + + + + + + + + + + + ); +}; + +export default SettingsIcon; \ No newline at end of file diff --git a/src/assets/icons/svgFile/mypage/footprint.svg b/src/assets/icons/svgFile/mypage/footprint.svg new file mode 100644 index 0000000..490c130 --- /dev/null +++ b/src/assets/icons/svgFile/mypage/footprint.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/svgFile/mypage/mapPinIcon.svg b/src/assets/icons/svgFile/mypage/mapPinIcon.svg new file mode 100644 index 0000000..f96e536 --- /dev/null +++ b/src/assets/icons/svgFile/mypage/mapPinIcon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/svgFile/mypage/myPageArrowIcon.svg b/src/assets/icons/svgFile/mypage/myPageArrowIcon.svg new file mode 100644 index 0000000..294a063 --- /dev/null +++ b/src/assets/icons/svgFile/mypage/myPageArrowIcon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/svgFile/mypage/pinsetIcon.svg b/src/assets/icons/svgFile/mypage/pinsetIcon.svg new file mode 100644 index 0000000..8930d2b --- /dev/null +++ b/src/assets/icons/svgFile/mypage/pinsetIcon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/icons/svgFile/mypage/recent.svg b/src/assets/icons/svgFile/mypage/recent.svg new file mode 100644 index 0000000..8d258fa --- /dev/null +++ b/src/assets/icons/svgFile/mypage/recent.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/svgFile/mypage/settingsIcon.svg b/src/assets/icons/svgFile/mypage/settingsIcon.svg new file mode 100644 index 0000000..dc28109 --- /dev/null +++ b/src/assets/icons/svgFile/mypage/settingsIcon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/features/mypage/ui/index.ts b/src/features/mypage/ui/index.ts index 4db27ce..9e77daf 100644 --- a/src/features/mypage/ui/index.ts +++ b/src/features/mypage/ui/index.ts @@ -5,3 +5,5 @@ export * from "./profileAvatar"; export * from "./profileNicknameInput"; export * from "./profileLoginInfo"; export * from "./profilePhotoBottomSheet"; +export * from "./mypageSection"; +export * from "./mypageMenuItem"; diff --git a/src/features/mypage/ui/mypageMenuItem.tsx b/src/features/mypage/ui/mypageMenuItem.tsx new file mode 100644 index 0000000..1f6b7e5 --- /dev/null +++ b/src/features/mypage/ui/mypageMenuItem.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { ReactNode } from "react"; +import MyPageArrow from "@/src/assets/icons/mypage/myPageArrow"; + +export interface MypageMenuItemProps { + icon: ReactNode; + label: string; + onClick?: () => void; +} + +export const MypageMenuItem = ({ icon, label, onClick }: MypageMenuItemProps) => { + return ( +
+ +
+ ); +}; + diff --git a/src/features/mypage/ui/mypageSection.tsx b/src/features/mypage/ui/mypageSection.tsx new file mode 100644 index 0000000..afa3d01 --- /dev/null +++ b/src/features/mypage/ui/mypageSection.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { ReactNode } from "react"; +import { MypageMenuItem, MypageMenuItemProps } from "./mypageMenuItem"; + +export interface MypageSectionProps { + title: string; + items: MypageMenuItemProps[]; +} + +export const MypageSection = ({ title, items }: MypageSectionProps) => { + return ( +
+
+

+ {title} +

+
+ {items.map((item, index) => ( + + ))} +
+ ); +}; + From 2cfd8e282f752e65440c7a3611cef02a8f49ac59 Mon Sep 17 00:00:00 2001 From: JW_Ahn0 Date: Fri, 30 Jan 2026 17:30:37 +0900 Subject: [PATCH 2/6] =?UTF-8?q?chore:=20=EB=B0=B1=EC=97=94=EB=93=9C=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EB=AC=B8=EC=A0=9C=EB=A1=9C=20MSW=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/layout.tsx | 2 + package.json | 7 +- public/mockServiceWorker.js | 349 ++++++++++++++++++ src/features/login/api/loginApi.ts | 2 + src/features/onboarding/api/onBoardingApi.ts | 2 + .../onboarding/hooks/useOnboardingFlow.ts | 3 +- src/mocks/MockProvider.tsx | 16 + src/mocks/browser.ts | 9 + src/mocks/handlers.ts | 147 ++++++++ src/mocks/index.ts | 30 ++ src/mocks/utils.ts | 27 ++ 11 files changed, 592 insertions(+), 2 deletions(-) create mode 100644 public/mockServiceWorker.js create mode 100644 src/mocks/MockProvider.tsx create mode 100644 src/mocks/browser.ts create mode 100644 src/mocks/handlers.ts create mode 100644 src/mocks/index.ts create mode 100644 src/mocks/utils.ts diff --git a/app/layout.tsx b/app/layout.tsx index 42f226a..4d6326d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,7 @@ import "./globals.css"; import { QueryProvider } from "@/src/app/providers/queryProvider"; import { BottomNavigation } from "@/src/shared/ui/bottomNavigation/"; import { Toast } from "@/src/shared/ui/toast"; +import { MockProvider } from "@/src/mocks/MockProvider"; export const metadata: Metadata = { title: "Create Next App", @@ -18,6 +19,7 @@ export default function RootLayout({ return ( +
diff --git a/package.json b/package.json index b2bd401..e5a4d7d 100644 --- a/package.json +++ b/package.json @@ -77,5 +77,10 @@ "tailwindcss": "^3.4.15", "ts-jest": "^29.4.5", "typescript": "^5" + }, + "msw": { + "workerDirectory": [ + "public" + ] } -} +} \ No newline at end of file diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js new file mode 100644 index 0000000..6951ed1 --- /dev/null +++ b/public/mockServiceWorker.js @@ -0,0 +1,349 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.12.3' +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() + + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/src/features/login/api/loginApi.ts b/src/features/login/api/loginApi.ts index 30f7f88..3c67928 100644 --- a/src/features/login/api/loginApi.ts +++ b/src/features/login/api/loginApi.ts @@ -12,6 +12,8 @@ const authorizeUrl: Record = { /** * 소셜 로그인 요청 함수 * @param provider - 로그인할 소셜 플랫폼 + * + * Mock 모드일 경우 MSW가 OAuth 엔드포인트를 가로채서 처리합니다. */ export const requestOAuthLogin = (provider: OAuthProviderType) => { try { diff --git a/src/features/onboarding/api/onBoardingApi.ts b/src/features/onboarding/api/onBoardingApi.ts index b5609d2..41305d8 100644 --- a/src/features/onboarding/api/onBoardingApi.ts +++ b/src/features/onboarding/api/onBoardingApi.ts @@ -24,6 +24,8 @@ export const completeOnboarding = async (data: IOnboardingCompleteRequest) => { data ); console.log("response", response); + + // Mock 모드일 경우 쿠키는 MSW 핸들러에서 설정됩니다. return response.success; } catch (error) { console.error("온보딩 완료 요청 실패:", error); diff --git a/src/features/onboarding/hooks/useOnboardingFlow.ts b/src/features/onboarding/hooks/useOnboardingFlow.ts index 27bdb88..0fa27d0 100644 --- a/src/features/onboarding/hooks/useOnboardingFlow.ts +++ b/src/features/onboarding/hooks/useOnboardingFlow.ts @@ -29,7 +29,8 @@ export const useOnboardingFlow = ({ onSuccess: async success => { if (success) { console.log("온보딩 완료 성공"); - // 핀포인트 설정은 onSuccess에서 처리 (쿠키 설정 후) + + // 핀포인트 설정은 onSuccess에서 처리 if (pinpointData) { try { await requestSetPinpoint(pinpointData); diff --git a/src/mocks/MockProvider.tsx b/src/mocks/MockProvider.tsx new file mode 100644 index 0000000..e37ffed --- /dev/null +++ b/src/mocks/MockProvider.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { useEffect } from 'react'; +import { initMocks } from './index'; + +/** + * MSW 초기화를 위한 클라이언트 컴포넌트 + */ +export function MockProvider() { + useEffect(() => { + initMocks(); + }, []); + + return null; +} + diff --git a/src/mocks/browser.ts b/src/mocks/browser.ts new file mode 100644 index 0000000..221ea5d --- /dev/null +++ b/src/mocks/browser.ts @@ -0,0 +1,9 @@ +import { setupWorker } from 'msw/browser'; +import { handlers } from './handlers'; + +/** + * 브라우저 환경에서 MSW 워커 설정 + */ +export const worker = setupWorker(...handlers); + + diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts new file mode 100644 index 0000000..87973a4 --- /dev/null +++ b/src/mocks/handlers.ts @@ -0,0 +1,147 @@ +import { http, HttpResponse } from 'msw'; +import { IResponse } from '@/src/shared/types'; +import { setMockAuthCookie } from './utils'; + +const baseURL = process.env.NEXT_PUBLIC_API_URL || 'https://api.pinhouse.cloud/v1'; +const oAuthBaseURL = process.env.NEXT_PUBLIC_OAUTH2 || baseURL; + +/** + * MSW 핸들러 정의 + * 개발 환경에서 백엔드 API를 Mock 처리합니다. + */ +export const handlers = [ + // OAuth 카카오 로그인 + http.get(`${oAuthBaseURL}/kakao`, () => { + const mockTempUserId = `mock-kakao-${Date.now()}`; + const callbackUrl = `/signup?state=${mockTempUserId}`; + console.log('[Mock API] OAuth 카카오 로그인 요청 - 리다이렉트:', callbackUrl); + return HttpResponse.redirect(callbackUrl, 302); + }), + + // OAuth 네이버 로그인 + http.get(`${oAuthBaseURL}/naver`, () => { + const mockTempUserId = `mock-naver-${Date.now()}`; + const callbackUrl = `/signup?state=${mockTempUserId}`; + console.log('[Mock API] OAuth 네이버 로그인 요청 - 리다이렉트:', callbackUrl); + return HttpResponse.redirect(callbackUrl, 302); + }), + + // 온보딩 완료 (회원가입) API + // POST /users?tempKey=mock-kakao-1769671978332 + http.post(`${baseURL}/users`, async ({ request }) => { + const url = new URL(request.url); + const tempKey = url.searchParams.get('tempKey'); + + // 요청 body 파싱 + const body = await request.json().catch(() => ({})); + + console.log('[Mock API] 온보딩 완료 요청:', { tempKey, body }); + + // Mock 응답 생성 + const mockResponse: IResponse<{ + userId: string; + onboardingCompleted: boolean; + }> = { + success: true, + code: 200, + message: '회원가입이 완료되었습니다.', + data: { + userId: tempKey ? `user-${tempKey}` : 'mock-user-id', + onboardingCompleted: true, + }, + }; + + // Mock 모드에서 인증 쿠키 설정 + setMockAuthCookie(); + + return HttpResponse.json(mockResponse); + }), + + // 인증 토큰 갱신 API + http.put(`${baseURL}/auth`, () => { + return HttpResponse.json({ + success: true, + code: 200, + message: '토큰 갱신 성공', + data: {}, + }); + }), + + // 인증 상태 확인 API (JWT 토큰 검증) + // GET /auth + http.get(`${baseURL}/auth`, () => { + console.log('[Mock API] 인증 상태 확인 요청'); + return HttpResponse.json({ + success: true, + code: 200, + message: '인증 성공', + data: true, // useAuthHook에서 response.data를 확인하므로 true 반환 + }); + }), + + // 인증 상태 확인 API (필요한 경우) + http.get(`${baseURL}/auth/status`, () => { + return HttpResponse.json({ + success: true, + code: 200, + message: '성공', + data: { + authenticated: true, + user: { + id: 'mock-user-id', + name: 'Mock User', + }, + }, + }); + }), + + // 핀포인트 설정(생성) API + // POST /pinpoints + http.post(`${baseURL}/pinpoints`, async ({ request }) => { + const body = await request.json().catch(() => ({})); + + console.log('[Mock API] 핀포인트 설정 요청:', body); + console.log('[Mock API] 요청 URL:', request.url); + + // Mock 응답 생성 (인증 없이도 성공하도록 처리) + const mockResponse: IResponse<{ + address: string; + name: string; + first: boolean; + }> = { + success: true, + code: 200, + message: '핀포인트가 설정되었습니다.', + data: { + address: (body as any).address || '서울시 강남구', + name: (body as any).name || '내 집', + first: (body as any).first ?? false, + }, + }; + + console.log('[Mock API] 핀포인트 설정 응답:', mockResponse); + return HttpResponse.json(mockResponse); + }), + + // 핀포인트 목록 조회 API + http.get(`${baseURL}/pinpoints`, () => { + return HttpResponse.json({ + success: true, + code: 200, + message: '성공', + data: { + userName: 'Mock User', + pinPoints: [ + { + id: 'mock-pinpoint-1', + name: '내 집', + address: '서울시 강남구', + latitude: 37.5665, + longitude: 126.978, + }, + ], + }, + }); + }), +]; + diff --git a/src/mocks/index.ts b/src/mocks/index.ts new file mode 100644 index 0000000..a57f630 --- /dev/null +++ b/src/mocks/index.ts @@ -0,0 +1,30 @@ +/** + * MSW 초기화 함수 + * 개발 환경에서만 실행됩니다. + */ +export async function initMocks() { + // 서버 사이드에서는 실행하지 않음 + if (typeof window === 'undefined') { + return; + } + + // 개발 환경에서만 실행 + if (process.env.NODE_ENV !== 'development') { + return; + } + + // Mock 모드가 활성화되어 있는지 확인 + if (process.env.NEXT_PUBLIC_USE_MOCK_OAUTH !== 'true') { + return; + } + + const { worker } = await import('./browser'); + + await worker.start({ + onUnhandledRequest: 'bypass', // 핸들러가 없는 요청은 그대로 통과 + }); + + console.log('[Mock] MSW가 활성화되었습니다.'); + console.log('[Mock] Base URL:', process.env.NEXT_PUBLIC_API_URL || 'https://api.pinhouse.cloud/v1'); +} + diff --git a/src/mocks/utils.ts b/src/mocks/utils.ts new file mode 100644 index 0000000..cf3fb81 --- /dev/null +++ b/src/mocks/utils.ts @@ -0,0 +1,27 @@ +/** + * Mock 모드 활성화 여부 확인 + */ +export const isMockMode = (): boolean => { + return ( + process.env.NODE_ENV === 'development' && + process.env.NEXT_PUBLIC_USE_MOCK_OAUTH === 'true' + ); +}; + +/** + * Mock 모드에서 인증 쿠키 설정 + * 브라우저에서 직접 쿠키를 설정합니다. + */ +export const setMockAuthCookie = () => { + if (!isMockMode() || typeof document === 'undefined') { + return; + } + + // 기존 쿠키 제거 + document.cookie = 'is_auth=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'; + + // 새 쿠키 설정 + document.cookie = 'is_auth=true; path=/; max-age=86400; SameSite=Lax'; // 24시간 + console.log('[Mock] 인증 쿠키 설정 완료'); +}; + From 4a396915770fbd3467ed6972080ee683be5a0bd1 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 10 Feb 2026 09:02:34 +0900 Subject: [PATCH 3/6] =?UTF-8?q?2026/02/10=20FE=5Ffeat/#380=20=EC=B2=AD?= =?UTF-8?q?=EC=95=BD=20=EC=A7=84=EB=8B=A8=20=EA=B2=B0=EA=B3=BC=EB=A5=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EA=B3=B5=EA=B3=A0=EB=A5=BC=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/home/hooks/homeHooks.ts | 48 +++++++++++++++---- src/features/home/ui/homeActionCardList.tsx | 20 ++++---- .../home/ui/homeUseHooks/homeUseHooks.ts | 23 +++++++-- src/shared/api/endpoints.ts | 1 + 4 files changed, 69 insertions(+), 23 deletions(-) diff --git a/src/entities/home/hooks/homeHooks.ts b/src/entities/home/hooks/homeHooks.ts index e4a67bf..d792ca1 100644 --- a/src/entities/home/hooks/homeHooks.ts +++ b/src/entities/home/hooks/homeHooks.ts @@ -1,18 +1,18 @@ import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { getNoticeByPinPoint } from "../interface/homeInterface"; -import { - GlobalSearchItem, - NoticeContent, - NoticeCount, - PopularResponse, - SearchCategory, - SliceResponse, -} from "../model/type"; +import { NoticeContent, NoticeCount, SearchCategory, SliceResponse } from "../model/type"; import { useOAuthStore } from "@/src/features/login/model"; -import { HOME_NOTICE_ENDPOINT, HOME_SEARCH_POPULAR_ENDPOINT } from "@/src/shared/api"; +import { + HOME_NOTICE_ENDPOINT, + HOME_RECOMMENDED_ENDPOINT, + HOME_SEARCH_POPULAR_ENDPOINT, +} from "@/src/shared/api"; import { useHomeMaxTime } from "@/src/features/home/model/homeStore"; import { useDebounce } from "@/src/shared/hooks/useDebounce/useDebounce"; import { ApiCategory, CATEGORY_MAP } from "@/src/features/home/model/model"; +import { ListingItem } from "@/src/entities/listings/model/type"; +import axios from "axios"; +import { toast } from "sonner"; export const useNoticeInfinite = () => { const pinpointId = useOAuthStore(state => state.pinPointId); @@ -21,6 +21,7 @@ export const useNoticeInfinite = () => { queryKey: ["notice", pinpointId], enabled: !!pinpointId, initialPageParam: 1, + retry: false, queryFn: ({ pageParam }) => getNoticeByPinPoint>({ url: HOME_NOTICE_ENDPOINT, @@ -48,6 +49,7 @@ export const useNoticeCount = () => { return useQuery({ queryKey: ["noticeCount", pinPointId, debouncedMaxTime], enabled: !!pinPointId, + retry: false, placeholderData: previousData => previousData, queryFn: () => getNoticeByPinPoint({ url: url, params: param }), }); @@ -60,6 +62,7 @@ export const useGlobal = ({ params, q }: { params: string; q: string }) => { return useQuery({ queryKey: ["global-search", params, q], + retry: false, queryFn: () => getNoticeByPinPoint({ url, params: param }), enabled: params === "popular" || q?.length > 0, }); @@ -81,6 +84,7 @@ export const useGlobalPageNation = ({ queryKey: ["globalInfinity", apiCategory], enabled: Boolean(category), initialPageParam: 1, + retry: false, queryFn: ({ pageParam }) => getNoticeByPinPoint>({ url, @@ -95,3 +99,29 @@ export const useGlobalPageNation = ({ }, }); }; + +export const useRecommendedNotice = () => { + return useInfiniteQuery, Error>({ + queryKey: ["HOME_RECOMMENDED"], + initialPageParam: 1, + retry: false, + queryFn: async ({ pageParam }) => { + try { + return await getNoticeByPinPoint>({ + url: HOME_RECOMMENDED_ENDPOINT, + params: { page: Number(pageParam), offSet: 10 }, + }); + } catch (e) { + if (axios.isAxiosError(e)) { + const message = e.response?.data?.message ?? e.response?.data?.error ?? e.message; + toast.error(message); + throw new Error(message); + } + throw e instanceof Error ? e : new Error("Unknown error"); + } + }, + getNextPageParam: lastPage => { + return lastPage.hasNext ? lastPage.pages + 1 : undefined; + }, + }); +}; diff --git a/src/features/home/ui/homeActionCardList.tsx b/src/features/home/ui/homeActionCardList.tsx index ff1eb7c..510cbc6 100644 --- a/src/features/home/ui/homeActionCardList.tsx +++ b/src/features/home/ui/homeActionCardList.tsx @@ -1,20 +1,16 @@ "use client"; import { ArrowUpRight } from "@/src/assets/icons/button/arrowUpRight"; -import { useNoticeCount } from "@/src/entities/home/hooks/homeHooks"; +import { useNoticeCount, useRecommendedNotice } from "@/src/entities/home/hooks/homeHooks"; import { useRouter } from "next/navigation"; +import { useHomeActionCard } from "@/src/features/home/ui/homeUseHooks/homeUseHooks"; export const ActionCardList = () => { const { data } = useNoticeCount(); - const conut = data?.count; - const router = useRouter(); + const { data: recommend } = useRecommendedNotice(); - const onListingsPageMove = () => { - router.push("/listings"); - }; + const count = data?.count; + const { onListingsPageMove, onEligibilityPageMove } = useHomeActionCard(); - const onEligibilityPageMove = () => { - router.push("/eligibility"); - }; return (
{
-

{conut}건

+

{count}건

{
-

0건

+

+ {recommend?.pages?.length ? recommend?.pages?.length : "0"}건 +

{ return { line1, line2, - onSelectSection - } -} \ No newline at end of file + onSelectSection, + }; +}; + +export const useHomeActionCard = () => { + const router = useRouter(); + + const onListingsPageMove = () => { + router.push("/listings"); + }; + + const onEligibilityPageMove = () => { + router.push("/eligibility"); + }; + + return { + onListingsPageMove, + onEligibilityPageMove, + }; +}; diff --git a/src/shared/api/endpoints.ts b/src/shared/api/endpoints.ts index cb46965..e7ea4c4 100644 --- a/src/shared/api/endpoints.ts +++ b/src/shared/api/endpoints.ts @@ -13,6 +13,7 @@ export const HTTP_METHODS = { */ export const HOME_NOTICE_ENDPOINT = "/home/notice"; export const HOME_SEARCH_POPULAR_ENDPOINT = "/home/search"; +export const HOME_RECOMMENDED_ENDPOINT = "/home/recommended-notices"; /** * 공고 API 엔드포인트 From 273429d1dd75c39e218e3c53b561e6cde3fa8e36 Mon Sep 17 00:00:00 2001 From: JW_Ahn0 Date: Tue, 10 Feb 2026 14:36:20 +0900 Subject: [PATCH 4/6] =?UTF-8?q?chore:=20=20=EB=B0=B1=EC=97=94=EB=93=9C=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= =?UTF-8?q?=EB=A1=9C=20MSW=20=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=EB=A1=A4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 2cfd8e282f752e65440c7a3611cef02a8f49ac59. --- app/layout.tsx | 2 - package.json | 7 +- public/mockServiceWorker.js | 349 ------------------ src/features/login/api/loginApi.ts | 2 - src/features/onboarding/api/onBoardingApi.ts | 2 - .../onboarding/hooks/useOnboardingFlow.ts | 3 +- src/mocks/MockProvider.tsx | 16 - src/mocks/browser.ts | 9 - src/mocks/handlers.ts | 147 -------- src/mocks/index.ts | 30 -- src/mocks/utils.ts | 27 -- 11 files changed, 2 insertions(+), 592 deletions(-) delete mode 100644 public/mockServiceWorker.js delete mode 100644 src/mocks/MockProvider.tsx delete mode 100644 src/mocks/browser.ts delete mode 100644 src/mocks/handlers.ts delete mode 100644 src/mocks/index.ts delete mode 100644 src/mocks/utils.ts diff --git a/app/layout.tsx b/app/layout.tsx index 4d6326d..42f226a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,7 +4,6 @@ import "./globals.css"; import { QueryProvider } from "@/src/app/providers/queryProvider"; import { BottomNavigation } from "@/src/shared/ui/bottomNavigation/"; import { Toast } from "@/src/shared/ui/toast"; -import { MockProvider } from "@/src/mocks/MockProvider"; export const metadata: Metadata = { title: "Create Next App", @@ -19,7 +18,6 @@ export default function RootLayout({ return ( -
diff --git a/package.json b/package.json index e5a4d7d..b2bd401 100644 --- a/package.json +++ b/package.json @@ -77,10 +77,5 @@ "tailwindcss": "^3.4.15", "ts-jest": "^29.4.5", "typescript": "^5" - }, - "msw": { - "workerDirectory": [ - "public" - ] } -} \ No newline at end of file +} diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js deleted file mode 100644 index 6951ed1..0000000 --- a/public/mockServiceWorker.js +++ /dev/null @@ -1,349 +0,0 @@ -/* eslint-disable */ -/* tslint:disable */ - -/** - * Mock Service Worker. - * @see https://github.com/mswjs/msw - * - Please do NOT modify this file. - */ - -const PACKAGE_VERSION = '2.12.3' -const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' -const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') -const activeClientIds = new Set() - -addEventListener('install', function () { - self.skipWaiting() -}) - -addEventListener('activate', function (event) { - event.waitUntil(self.clients.claim()) -}) - -addEventListener('message', async function (event) { - const clientId = Reflect.get(event.source || {}, 'id') - - if (!clientId || !self.clients) { - return - } - - const client = await self.clients.get(clientId) - - if (!client) { - return - } - - const allClients = await self.clients.matchAll({ - type: 'window', - }) - - switch (event.data) { - case 'KEEPALIVE_REQUEST': { - sendToClient(client, { - type: 'KEEPALIVE_RESPONSE', - }) - break - } - - case 'INTEGRITY_CHECK_REQUEST': { - sendToClient(client, { - type: 'INTEGRITY_CHECK_RESPONSE', - payload: { - packageVersion: PACKAGE_VERSION, - checksum: INTEGRITY_CHECKSUM, - }, - }) - break - } - - case 'MOCK_ACTIVATE': { - activeClientIds.add(clientId) - - sendToClient(client, { - type: 'MOCKING_ENABLED', - payload: { - client: { - id: client.id, - frameType: client.frameType, - }, - }, - }) - break - } - - case 'CLIENT_CLOSED': { - activeClientIds.delete(clientId) - - const remainingClients = allClients.filter((client) => { - return client.id !== clientId - }) - - // Unregister itself when there are no more clients - if (remainingClients.length === 0) { - self.registration.unregister() - } - - break - } - } -}) - -addEventListener('fetch', function (event) { - const requestInterceptedAt = Date.now() - - // Bypass navigation requests. - if (event.request.mode === 'navigate') { - return - } - - // Opening the DevTools triggers the "only-if-cached" request - // that cannot be handled by the worker. Bypass such requests. - if ( - event.request.cache === 'only-if-cached' && - event.request.mode !== 'same-origin' - ) { - return - } - - // Bypass all requests when there are no active clients. - // Prevents the self-unregistered worked from handling requests - // after it's been terminated (still remains active until the next reload). - if (activeClientIds.size === 0) { - return - } - - const requestId = crypto.randomUUID() - event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) -}) - -/** - * @param {FetchEvent} event - * @param {string} requestId - * @param {number} requestInterceptedAt - */ -async function handleRequest(event, requestId, requestInterceptedAt) { - const client = await resolveMainClient(event) - const requestCloneForEvents = event.request.clone() - const response = await getResponse( - event, - client, - requestId, - requestInterceptedAt, - ) - - // Send back the response clone for the "response:*" life-cycle events. - // Ensure MSW is active and ready to handle the message, otherwise - // this message will pend indefinitely. - if (client && activeClientIds.has(client.id)) { - const serializedRequest = await serializeRequest(requestCloneForEvents) - - // Clone the response so both the client and the library could consume it. - const responseClone = response.clone() - - sendToClient( - client, - { - type: 'RESPONSE', - payload: { - isMockedResponse: IS_MOCKED_RESPONSE in response, - request: { - id: requestId, - ...serializedRequest, - }, - response: { - type: responseClone.type, - status: responseClone.status, - statusText: responseClone.statusText, - headers: Object.fromEntries(responseClone.headers.entries()), - body: responseClone.body, - }, - }, - }, - responseClone.body ? [serializedRequest.body, responseClone.body] : [], - ) - } - - return response -} - -/** - * Resolve the main client for the given event. - * Client that issues a request doesn't necessarily equal the client - * that registered the worker. It's with the latter the worker should - * communicate with during the response resolving phase. - * @param {FetchEvent} event - * @returns {Promise} - */ -async function resolveMainClient(event) { - const client = await self.clients.get(event.clientId) - - if (activeClientIds.has(event.clientId)) { - return client - } - - if (client?.frameType === 'top-level') { - return client - } - - const allClients = await self.clients.matchAll({ - type: 'window', - }) - - return allClients - .filter((client) => { - // Get only those clients that are currently visible. - return client.visibilityState === 'visible' - }) - .find((client) => { - // Find the client ID that's recorded in the - // set of clients that have registered the worker. - return activeClientIds.has(client.id) - }) -} - -/** - * @param {FetchEvent} event - * @param {Client | undefined} client - * @param {string} requestId - * @param {number} requestInterceptedAt - * @returns {Promise} - */ -async function getResponse(event, client, requestId, requestInterceptedAt) { - // Clone the request because it might've been already used - // (i.e. its body has been read and sent to the client). - const requestClone = event.request.clone() - - function passthrough() { - // Cast the request headers to a new Headers instance - // so the headers can be manipulated with. - const headers = new Headers(requestClone.headers) - - // Remove the "accept" header value that marked this request as passthrough. - // This prevents request alteration and also keeps it compliant with the - // user-defined CORS policies. - const acceptHeader = headers.get('accept') - if (acceptHeader) { - const values = acceptHeader.split(',').map((value) => value.trim()) - const filteredValues = values.filter( - (value) => value !== 'msw/passthrough', - ) - - if (filteredValues.length > 0) { - headers.set('accept', filteredValues.join(', ')) - } else { - headers.delete('accept') - } - } - - return fetch(requestClone, { headers }) - } - - // Bypass mocking when the client is not active. - if (!client) { - return passthrough() - } - - // Bypass initial page load requests (i.e. static assets). - // The absence of the immediate/parent client in the map of the active clients - // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet - // and is not ready to handle requests. - if (!activeClientIds.has(client.id)) { - return passthrough() - } - - // Notify the client that a request has been intercepted. - const serializedRequest = await serializeRequest(event.request) - const clientMessage = await sendToClient( - client, - { - type: 'REQUEST', - payload: { - id: requestId, - interceptedAt: requestInterceptedAt, - ...serializedRequest, - }, - }, - [serializedRequest.body], - ) - - switch (clientMessage.type) { - case 'MOCK_RESPONSE': { - return respondWithMock(clientMessage.data) - } - - case 'PASSTHROUGH': { - return passthrough() - } - } - - return passthrough() -} - -/** - * @param {Client} client - * @param {any} message - * @param {Array} transferrables - * @returns {Promise} - */ -function sendToClient(client, message, transferrables = []) { - return new Promise((resolve, reject) => { - const channel = new MessageChannel() - - channel.port1.onmessage = (event) => { - if (event.data && event.data.error) { - return reject(event.data.error) - } - - resolve(event.data) - } - - client.postMessage(message, [ - channel.port2, - ...transferrables.filter(Boolean), - ]) - }) -} - -/** - * @param {Response} response - * @returns {Response} - */ -function respondWithMock(response) { - // Setting response status code to 0 is a no-op. - // However, when responding with a "Response.error()", the produced Response - // instance will have status code set to 0. Since it's not possible to create - // a Response instance with status code 0, handle that use-case separately. - if (response.status === 0) { - return Response.error() - } - - const mockedResponse = new Response(response.body, response) - - Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { - value: true, - enumerable: true, - }) - - return mockedResponse -} - -/** - * @param {Request} request - */ -async function serializeRequest(request) { - return { - url: request.url, - mode: request.mode, - method: request.method, - headers: Object.fromEntries(request.headers.entries()), - cache: request.cache, - credentials: request.credentials, - destination: request.destination, - integrity: request.integrity, - redirect: request.redirect, - referrer: request.referrer, - referrerPolicy: request.referrerPolicy, - body: await request.arrayBuffer(), - keepalive: request.keepalive, - } -} diff --git a/src/features/login/api/loginApi.ts b/src/features/login/api/loginApi.ts index 3c67928..30f7f88 100644 --- a/src/features/login/api/loginApi.ts +++ b/src/features/login/api/loginApi.ts @@ -12,8 +12,6 @@ const authorizeUrl: Record = { /** * 소셜 로그인 요청 함수 * @param provider - 로그인할 소셜 플랫폼 - * - * Mock 모드일 경우 MSW가 OAuth 엔드포인트를 가로채서 처리합니다. */ export const requestOAuthLogin = (provider: OAuthProviderType) => { try { diff --git a/src/features/onboarding/api/onBoardingApi.ts b/src/features/onboarding/api/onBoardingApi.ts index 41305d8..b5609d2 100644 --- a/src/features/onboarding/api/onBoardingApi.ts +++ b/src/features/onboarding/api/onBoardingApi.ts @@ -24,8 +24,6 @@ export const completeOnboarding = async (data: IOnboardingCompleteRequest) => { data ); console.log("response", response); - - // Mock 모드일 경우 쿠키는 MSW 핸들러에서 설정됩니다. return response.success; } catch (error) { console.error("온보딩 완료 요청 실패:", error); diff --git a/src/features/onboarding/hooks/useOnboardingFlow.ts b/src/features/onboarding/hooks/useOnboardingFlow.ts index 0fa27d0..27bdb88 100644 --- a/src/features/onboarding/hooks/useOnboardingFlow.ts +++ b/src/features/onboarding/hooks/useOnboardingFlow.ts @@ -29,8 +29,7 @@ export const useOnboardingFlow = ({ onSuccess: async success => { if (success) { console.log("온보딩 완료 성공"); - - // 핀포인트 설정은 onSuccess에서 처리 + // 핀포인트 설정은 onSuccess에서 처리 (쿠키 설정 후) if (pinpointData) { try { await requestSetPinpoint(pinpointData); diff --git a/src/mocks/MockProvider.tsx b/src/mocks/MockProvider.tsx deleted file mode 100644 index e37ffed..0000000 --- a/src/mocks/MockProvider.tsx +++ /dev/null @@ -1,16 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; -import { initMocks } from './index'; - -/** - * MSW 초기화를 위한 클라이언트 컴포넌트 - */ -export function MockProvider() { - useEffect(() => { - initMocks(); - }, []); - - return null; -} - diff --git a/src/mocks/browser.ts b/src/mocks/browser.ts deleted file mode 100644 index 221ea5d..0000000 --- a/src/mocks/browser.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { setupWorker } from 'msw/browser'; -import { handlers } from './handlers'; - -/** - * 브라우저 환경에서 MSW 워커 설정 - */ -export const worker = setupWorker(...handlers); - - diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts deleted file mode 100644 index 87973a4..0000000 --- a/src/mocks/handlers.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { http, HttpResponse } from 'msw'; -import { IResponse } from '@/src/shared/types'; -import { setMockAuthCookie } from './utils'; - -const baseURL = process.env.NEXT_PUBLIC_API_URL || 'https://api.pinhouse.cloud/v1'; -const oAuthBaseURL = process.env.NEXT_PUBLIC_OAUTH2 || baseURL; - -/** - * MSW 핸들러 정의 - * 개발 환경에서 백엔드 API를 Mock 처리합니다. - */ -export const handlers = [ - // OAuth 카카오 로그인 - http.get(`${oAuthBaseURL}/kakao`, () => { - const mockTempUserId = `mock-kakao-${Date.now()}`; - const callbackUrl = `/signup?state=${mockTempUserId}`; - console.log('[Mock API] OAuth 카카오 로그인 요청 - 리다이렉트:', callbackUrl); - return HttpResponse.redirect(callbackUrl, 302); - }), - - // OAuth 네이버 로그인 - http.get(`${oAuthBaseURL}/naver`, () => { - const mockTempUserId = `mock-naver-${Date.now()}`; - const callbackUrl = `/signup?state=${mockTempUserId}`; - console.log('[Mock API] OAuth 네이버 로그인 요청 - 리다이렉트:', callbackUrl); - return HttpResponse.redirect(callbackUrl, 302); - }), - - // 온보딩 완료 (회원가입) API - // POST /users?tempKey=mock-kakao-1769671978332 - http.post(`${baseURL}/users`, async ({ request }) => { - const url = new URL(request.url); - const tempKey = url.searchParams.get('tempKey'); - - // 요청 body 파싱 - const body = await request.json().catch(() => ({})); - - console.log('[Mock API] 온보딩 완료 요청:', { tempKey, body }); - - // Mock 응답 생성 - const mockResponse: IResponse<{ - userId: string; - onboardingCompleted: boolean; - }> = { - success: true, - code: 200, - message: '회원가입이 완료되었습니다.', - data: { - userId: tempKey ? `user-${tempKey}` : 'mock-user-id', - onboardingCompleted: true, - }, - }; - - // Mock 모드에서 인증 쿠키 설정 - setMockAuthCookie(); - - return HttpResponse.json(mockResponse); - }), - - // 인증 토큰 갱신 API - http.put(`${baseURL}/auth`, () => { - return HttpResponse.json({ - success: true, - code: 200, - message: '토큰 갱신 성공', - data: {}, - }); - }), - - // 인증 상태 확인 API (JWT 토큰 검증) - // GET /auth - http.get(`${baseURL}/auth`, () => { - console.log('[Mock API] 인증 상태 확인 요청'); - return HttpResponse.json({ - success: true, - code: 200, - message: '인증 성공', - data: true, // useAuthHook에서 response.data를 확인하므로 true 반환 - }); - }), - - // 인증 상태 확인 API (필요한 경우) - http.get(`${baseURL}/auth/status`, () => { - return HttpResponse.json({ - success: true, - code: 200, - message: '성공', - data: { - authenticated: true, - user: { - id: 'mock-user-id', - name: 'Mock User', - }, - }, - }); - }), - - // 핀포인트 설정(생성) API - // POST /pinpoints - http.post(`${baseURL}/pinpoints`, async ({ request }) => { - const body = await request.json().catch(() => ({})); - - console.log('[Mock API] 핀포인트 설정 요청:', body); - console.log('[Mock API] 요청 URL:', request.url); - - // Mock 응답 생성 (인증 없이도 성공하도록 처리) - const mockResponse: IResponse<{ - address: string; - name: string; - first: boolean; - }> = { - success: true, - code: 200, - message: '핀포인트가 설정되었습니다.', - data: { - address: (body as any).address || '서울시 강남구', - name: (body as any).name || '내 집', - first: (body as any).first ?? false, - }, - }; - - console.log('[Mock API] 핀포인트 설정 응답:', mockResponse); - return HttpResponse.json(mockResponse); - }), - - // 핀포인트 목록 조회 API - http.get(`${baseURL}/pinpoints`, () => { - return HttpResponse.json({ - success: true, - code: 200, - message: '성공', - data: { - userName: 'Mock User', - pinPoints: [ - { - id: 'mock-pinpoint-1', - name: '내 집', - address: '서울시 강남구', - latitude: 37.5665, - longitude: 126.978, - }, - ], - }, - }); - }), -]; - diff --git a/src/mocks/index.ts b/src/mocks/index.ts deleted file mode 100644 index a57f630..0000000 --- a/src/mocks/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * MSW 초기화 함수 - * 개발 환경에서만 실행됩니다. - */ -export async function initMocks() { - // 서버 사이드에서는 실행하지 않음 - if (typeof window === 'undefined') { - return; - } - - // 개발 환경에서만 실행 - if (process.env.NODE_ENV !== 'development') { - return; - } - - // Mock 모드가 활성화되어 있는지 확인 - if (process.env.NEXT_PUBLIC_USE_MOCK_OAUTH !== 'true') { - return; - } - - const { worker } = await import('./browser'); - - await worker.start({ - onUnhandledRequest: 'bypass', // 핸들러가 없는 요청은 그대로 통과 - }); - - console.log('[Mock] MSW가 활성화되었습니다.'); - console.log('[Mock] Base URL:', process.env.NEXT_PUBLIC_API_URL || 'https://api.pinhouse.cloud/v1'); -} - diff --git a/src/mocks/utils.ts b/src/mocks/utils.ts deleted file mode 100644 index cf3fb81..0000000 --- a/src/mocks/utils.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Mock 모드 활성화 여부 확인 - */ -export const isMockMode = (): boolean => { - return ( - process.env.NODE_ENV === 'development' && - process.env.NEXT_PUBLIC_USE_MOCK_OAUTH === 'true' - ); -}; - -/** - * Mock 모드에서 인증 쿠키 설정 - * 브라우저에서 직접 쿠키를 설정합니다. - */ -export const setMockAuthCookie = () => { - if (!isMockMode() || typeof document === 'undefined') { - return; - } - - // 기존 쿠키 제거 - document.cookie = 'is_auth=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'; - - // 새 쿠키 설정 - document.cookie = 'is_auth=true; path=/; max-age=86400; SameSite=Lax'; // 24시간 - console.log('[Mock] 인증 쿠키 설정 완료'); -}; - From 601258849a6106dcd2779896da1f5ac018b7af6d Mon Sep 17 00:00:00 2001 From: JW_Ahn0 Date: Tue, 10 Feb 2026 14:38:38 +0900 Subject: [PATCH 5/6] =?UTF-8?q?file:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - kiro 관련 파일 삭제 --- .kiro/steering/korean-language.md | 12 ----- .kiro/steering/product.md | 18 ------- .kiro/steering/structure.md | 89 ------------------------------- .kiro/steering/tech.md | 54 ------------------- 4 files changed, 173 deletions(-) delete mode 100644 .kiro/steering/korean-language.md delete mode 100644 .kiro/steering/product.md delete mode 100644 .kiro/steering/structure.md delete mode 100644 .kiro/steering/tech.md diff --git a/.kiro/steering/korean-language.md b/.kiro/steering/korean-language.md deleted file mode 100644 index 7e5c478..0000000 --- a/.kiro/steering/korean-language.md +++ /dev/null @@ -1,12 +0,0 @@ -# 한국어 응답 규칙 - -## 언어 설정 -- 모든 응답은 한국어로 작성해야 합니다 -- 코드 주석도 가능한 한 한국어로 작성합니다 -- 기술 용어는 필요시 영어와 한국어를 병행 표기합니다 (예: "컨테이너(container)") -- 에러 메시지나 로그는 원본 언어를 유지하되, 설명은 한국어로 제공합니다 - -## 예외 상황 -- 코드 자체는 영어로 작성 (변수명, 함수명 등) -- 공식 문서나 명령어는 원본 언어 유지 -- 사용자가 명시적으로 다른 언어를 요청하는 경우에만 예외 적용 \ No newline at end of file diff --git a/.kiro/steering/product.md b/.kiro/steering/product.md deleted file mode 100644 index 28e14bf..0000000 --- a/.kiro/steering/product.md +++ /dev/null @@ -1,18 +0,0 @@ -# 제품 개요 - -한국 시장을 타겟으로 하는 부동산 임대 웹 애플리케이션입니다. - -## 타겟 플랫폼 -- 데스크톱 우선 반응형 웹앱 (모바일 지원) -- 브레이크포인트: sm (375px), md (640px), lg (768px) - -## 핵심 기능 -- 사용자 인증 (OAuth 기반 로그인/회원가입) -- 다단계 온보딩 플로우 -- 주소 검색 기능 -- 태그 기반 필터링 및 분류 - -## 사용자 플로우 -- 공개 라우트: 랜딩 페이지, 로그인, 회원가입, 온보딩 -- 보호된 라우트: 쿠키 기반 인증 필요 (`is_auth` 쿠키) -- 미들웨어가 라우트 보호 및 리다이렉트 처리 diff --git a/.kiro/steering/structure.md b/.kiro/steering/structure.md deleted file mode 100644 index ee59a5f..0000000 --- a/.kiro/steering/structure.md +++ /dev/null @@ -1,89 +0,0 @@ -# 프로젝트 구조 - -## 아키텍처 패턴 -**FSD Hybrid** (Feature-Sliced Design + Next.js App Router) - -Next.js App Router 규칙과 FSD 원칙을 결합한 하이브리드 접근 방식을 사용합니다. - -## 디렉토리 구조 - -``` -/app # Next.js App Router (라우팅 레이어) - /api # API 라우트 - /home # 홈 페이지 라우트 - /login # 로그인 페이지 라우트 - /signup # 회원가입 페이지 라우트 - /onboarding # 온보딩 플로우 라우트 - /test # 테스트 페이지 - layout.tsx # 루트 레이아웃 - page.tsx # 루트 페이지 - globals.css # 전역 스타일 - -/src # FSD 레이어 - /app # 앱 레이어 (설정, 프로바이더) - /config # 앱 설정 - /providers # Context 프로바이더 - - /entities # 비즈니스 엔티티 - /address # 주소 엔티티 - /auth # 인증 엔티티 - /tag # 태그 엔티티 - - /features # 사용자 대면 기능 - /addressSearch # 주소 검색 기능 - /login # 로그인 기능 - /onboarding # 온보딩 기능 - - /shared # 공유 리소스 - /api # API 클라이언트 및 유틸리티 - /hooks # 재사용 가능한 React 훅 - /lib # 유틸리티 함수 - /types # TypeScript 타입 정의 - /ui # 공유 UI 컴포넌트 - - /widgets # 복합 UI 블록 - /onboardingSection # 온보딩 섹션 위젯 - - /stories # Storybook 스토리 - /assets # 정적 자산 - /icons # 아이콘 파일 - /images # 이미지 파일 - -/components # 레거시/shadcn 컴포넌트 - /ui # UI 컴포넌트 라이브러리 - -/lib # 루트 레벨 유틸리티 - utils.ts # className 병합을 위한 cn() 유틸리티 - -/public # 루트에서 제공되는 정적 파일 - /fonts # 폰트 파일 - -/.storybook # Storybook 설정 - -middleware.ts # 인증/라우팅을 위한 Next.js 미들웨어 -``` - -## 레이어 책임 - -### App Router (`/app`) -- 라우팅 및 페이지 렌더링 처리 -- Next.js 전용 파일 포함 (layout, page, loading, error) -- `/app/api`에 API 라우트 위치 - -### FSD 레이어 (`/src`) -- **app**: 애플리케이션 초기화, 프로바이더, 전역 설정 -- **entities**: 비즈니스 도메인 모델 및 로직 -- **features**: UI와 로직을 포함한 완전한 사용자 대면 기능 -- **shared**: 비즈니스 로직이 없는 재사용 가능한 코드 -- **widgets**: features/entities를 결합한 복합 UI 섹션 - -## 임포트 규칙 -- 워크스페이스 루트에서 절대 임포트 시 `@/` 접두사 사용 -- FSD 레이어는 의존성 규칙 준수: shared ← entities ← features ← widgets ← app -- `/src/shared/ui`의 컴포넌트는 프레임워크에 독립적이고 재사용 가능해야 함 - -## 컴포넌트 패턴 -- 조건부 className 병합을 위해 `@/lib/utils`의 `cn()` 유틸리티 사용 -- 접근성 높은 컴포넌트를 위해 Radix UI 프리미티브 선호 -- SVG 파일은 @svgr/webpack을 통해 React 컴포넌트로 임포트 -- Storybook 스토리는 `/src/stories`에 위치 diff --git a/.kiro/steering/tech.md b/.kiro/steering/tech.md deleted file mode 100644 index bd27f49..0000000 --- a/.kiro/steering/tech.md +++ /dev/null @@ -1,54 +0,0 @@ -# 기술 스택 - -## 코어 프레임워크 -- **Next.js 15.5.4** - App Router 및 Turbopack 사용 -- **React 19.1.0** - strict mode 비활성화 -- **TypeScript 5** - strict mode 활성화 - -## 스타일링 -- **Tailwind CSS 3.x** - 커스텀 디자인 토큰 사용 -- **Framer Motion** - 애니메이션 처리 -- **next-themes** - 다크 모드 지원 -- 커스텀 애니메이션: logoBounce, logoPop, slideOutLeft, slideInRight - -## 상태 관리 -- **Zustand** - 전역 상태 관리 -- **TanStack React Query** - 서버 상태 및 데이터 페칭 - -## UI 컴포넌트 -- **Radix UI** - 접근성 높은 UI 프리미티브 (Dialog, Dropdown Menu, Slot) -- **shadcn/ui** 패턴 - `cn()` 유틸리티 사용 (clsx + tailwind-merge) -- **Lucide React** - 아이콘 라이브러리 -- **Sonner** - 토스트 알림 - -## 개발 도구 -- **Storybook 9.x** - 컴포넌트 개발 및 문서화 -- **Chromatic** - 비주얼 테스팅 -- **ESLint** - TypeScript, React, 접근성, Tailwind 플러그인 포함 -- **Prettier** - Tailwind 플러그인을 포함한 코드 포맷팅 - -## 빌드 시스템 -- **Turbopack** - 빠른 빌드 속도 -- **@svgr/webpack** - SVG를 React 컴포넌트로 임포트 -- **ESM 모듈** - package.json에 `"type": "module"` 설정 - -## 주요 명령어 - -```bash -# 개발 -npm run dev # Turbopack으로 개발 서버 시작 -npm run build # Turbopack으로 프로덕션 빌드 -npm run start # 프로덕션 서버 시작 - -# 코드 품질 -npm run lint # ESLint 실행 -npm run format # Prettier로 코드 포맷팅 - -# Storybook -npm run storybook # Storybook 개발 서버 시작 (포트 6006) -npm run build-storybook # Storybook 정적 파일 빌드 -npm run chromatic # Chromatic에 배포하여 비주얼 테스팅 -``` - -## 경로 별칭 (Path Aliases) -- `@/*` - 워크스페이스 루트를 가리킴 From 6e6d24e9f7e0159a23b2c5dae4db65166a36122b Mon Sep 17 00:00:00 2001 From: JW_Ahn0 Date: Tue, 10 Feb 2026 15:57:57 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20UI=20=EA=B0=9C=EB=B0=9C=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 바텀 네비게이션 바와 연결 작업 완료 - 미구현 사항의 경우 alert 으로 별도 표시 완료 - 바텀 네비게이션 바 fill 안되던 문제 해결 --- app/mypage/page.tsx | 159 +++++++----------- src/assets/icons/mypage/settingsIcon.tsx | 4 +- .../images/mypage/ProfileDefaultImg.tsx | 4 +- src/features/mypage/ui/index.ts | 2 + src/features/mypage/ui/mypageMenuItem.tsx | 10 +- src/features/mypage/ui/mypageSection.tsx | 6 +- src/features/mypage/ui/pinReportSection.tsx | 35 ++++ src/features/mypage/ui/userInfoCard.tsx | 49 ++++++ .../ui/bottomNavigation/bottomNavigation.tsx | 5 +- 9 files changed, 161 insertions(+), 113 deletions(-) create mode 100644 src/features/mypage/ui/pinReportSection.tsx create mode 100644 src/features/mypage/ui/userInfoCard.tsx diff --git a/app/mypage/page.tsx b/app/mypage/page.tsx index 545b35e..7e5c58d 100644 --- a/app/mypage/page.tsx +++ b/app/mypage/page.tsx @@ -1,121 +1,82 @@ "use client"; import { SearchLine } from "@/src/assets/icons/home"; -import { ProfileDefaultImg } from "@/src/assets/images/mypage/ProfileDefaultImg"; -import Image from "next/image"; -import SettingsIcon from "@/src/assets/icons/mypage/settingsIcon"; import FootPrintIcon from "@/src/assets/icons/mypage/footPrintIcon"; import MapPinIcon from "@/src/assets/icons/mypage/mapPinIcon"; import RecentIcon from "@/src/assets/icons/mypage/recentIcon"; import PinsetIcon from "@/src/assets/icons/mypage/pinsetIcon"; -import { MypageSection } from "@/src/features/mypage/ui"; +import { MypageSection, UserInfoCard, PinReportSection } from "@/src/features/mypage/ui"; +import { useRouter } from "next/navigation"; +import { useOAuthStore } from "@/src/features/login/model/authStore"; export default function MypagePage() { + const { userName } = useOAuthStore(); const imageUrl = null; + const router = useRouter(); return ( -
- {/* 헤더 */} -
-

- 마이 페이지 -

- -
+
+ {/* 사용자 정보 카드 */} + { + router.push("/mypage/settings"); + }} + /> -
- {/* 사용자 정보 카드 */} -
-
-
- {imageUrl ? ( - 프로필 사진 - ) : ( -
- -
- )} -
-
- - 유저명 - - - userid@naver.com - -
-
- -
+ {/* 핀 보고서 섹션 */} + { + router.push("/eligibility"); + }} + /> - {/* 핀 보고서 섹션 */} -
-

- 핀 보고서 -

-

- 자격진단으로 -
- 임대주택 지원 가능 여부를 확인하고 -
- 맞춤 보고서를 받아보세요 -

- -
- - {/* 내 정보 섹션 */} - , - label: "관심 주변 환경 설정", - onClick: () => { - // TODO: 네비게이션 구현 - }, + {/* 내 정보 섹션 */} + , + label: "관심 주변 환경 설정", + onClick: () => { + alert("관심 주변 환경 설정 미구현 상태"); + // TODO: 네비게이션 구현 }, - { - icon: , - label: "핀포인트 설정", - onClick: () => { - // TODO: 네비게이션 구현 - }, + }, + { + icon: , + label: "핀포인트 설정", + onClick: () => { + router.push("/mypage/pinpoints"); }, - ]} - /> + }, + ]} + /> - {/* 내 활동 섹션 */} - , - label: "저장 목록", - onClick: () => { - // TODO: 네비게이션 구현 - }, + {/* 내 활동 섹션 */} + , + label: "저장 목록", + onClick: () => { + alert("저장 목록 이동 미구현 상태"); + // TODO: 네비게이션 구현 }, - { - icon: , - label: "최근 본 공고", - onClick: () => { - // TODO: 네비게이션 구현 - }, + }, + { + icon: , + label: "최근 본 공고", + onClick: () => { + alert("최근 본 공고 이동 미구현 상태"); + // TODO: 네비게이션 구현 }, - ]} - /> -
+ }, + ]} + />
); } diff --git a/src/assets/icons/mypage/settingsIcon.tsx b/src/assets/icons/mypage/settingsIcon.tsx index d81e5d5..0980437 100644 --- a/src/assets/icons/mypage/settingsIcon.tsx +++ b/src/assets/icons/mypage/settingsIcon.tsx @@ -2,8 +2,8 @@ const SettingsIcon = () => { return ( - - + + diff --git a/src/assets/images/mypage/ProfileDefaultImg.tsx b/src/assets/images/mypage/ProfileDefaultImg.tsx index c4bd39c..9e55df4 100644 --- a/src/assets/images/mypage/ProfileDefaultImg.tsx +++ b/src/assets/images/mypage/ProfileDefaultImg.tsx @@ -1,6 +1,6 @@ -export const ProfileDefaultImg = () => { +export const ProfileDefaultImg = ({width = 43, height = 43}: {width?: number, height?: number}) => { return ( - + { return ( -
+
+
+ + +
+ ); +}; diff --git a/src/features/mypage/ui/userInfoCard.tsx b/src/features/mypage/ui/userInfoCard.tsx new file mode 100644 index 0000000..8d7bff6 --- /dev/null +++ b/src/features/mypage/ui/userInfoCard.tsx @@ -0,0 +1,49 @@ +"use client"; + +import Image from "next/image"; +import { ProfileDefaultImg } from "@/src/assets/images/mypage/ProfileDefaultImg"; +import SettingsIcon from "@/src/assets/icons/mypage/settingsIcon"; + +interface UserInfoCardProps { + imageUrl?: string | null; + userName?: string; + userEmail?: string; + onSettingsClick?: () => void; +} + +export const UserInfoCard = ({ + imageUrl, + userName = "유저명", + userEmail = "userid@naver.com", + onSettingsClick +}: UserInfoCardProps) => { + return ( +
+
+ {imageUrl ? ( + 프로필 사진 + ) : ( +
+ +
+ )} +
+
+ + {userName} + + + {userEmail} + +
+ +
+ ); +}; diff --git a/src/shared/ui/bottomNavigation/bottomNavigation.tsx b/src/shared/ui/bottomNavigation/bottomNavigation.tsx index 3c3dfe7..a551641 100644 --- a/src/shared/ui/bottomNavigation/bottomNavigation.tsx +++ b/src/shared/ui/bottomNavigation/bottomNavigation.tsx @@ -40,6 +40,7 @@ function BottomNavigationContent() { compareDetailPageRegex.test(pathname) || (pathname === "/home" && searchParams.has("mode")); + const isMypageActive = pathname === "/mypage" || pathname.startsWith("/mypage/"); if (shouldHide) return null; return (
@@ -65,8 +66,8 @@ function BottomNavigationContent() { router.push("/mypage/settings")} - fill={pathname === "/mypage/settings" ? "black" : "none"} + onClick={() => router.push("/mypage")} + fill={isMypageActive ? "black" : "none"} /> 마이