diff --git a/package.json b/package.json index 0aa46f2..38e3efb 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@hookform/resolvers": "^5.2.1", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-select": "^2.2.6", @@ -37,6 +38,7 @@ "next-themes": "^0.4.6", "prettier": "^3.6.2", "react": "^19.1.1", + "react-daum-postcode": "^3.2.0", "react-dom": "^19.1.1", "react-hook-form": "^7.62.0", "react-kakao-maps-sdk": "^1.2.0", @@ -46,7 +48,8 @@ "tailwind-merge": "^3.3.1", "tailwind-scrollbar-hide": "^4.0.0", "tailwindcss": "^4.1.12", - "zod": "^4.0.17" + "zod": "^4.0.17", + "zustand": "^5.0.8" }, "devDependencies": { "@eslint/js": "^9.33.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e67a427..835eaaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@radix-ui/react-checkbox': specifier: ^1.3.3 version: 1.3.3(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@radix-ui/react-label': specifier: ^2.1.7 version: 2.1.7(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -56,6 +59,9 @@ importers: react: specifier: ^19.1.1 version: 19.1.1 + react-daum-postcode: + specifier: ^3.2.0 + version: 3.2.0(react@19.1.1) react-dom: specifier: ^19.1.1 version: 19.1.1(react@19.1.1) @@ -86,6 +92,9 @@ importers: zod: specifier: ^4.0.17 version: 4.0.17 + zustand: + specifier: ^5.0.8 + version: 5.0.8(@types/react@19.1.10)(immer@10.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) devDependencies: '@eslint/js': specifier: ^9.33.0 @@ -567,6 +576,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-direction@1.1.1': resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} peerDependencies: @@ -2588,6 +2610,11 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-daum-postcode@3.2.0: + resolution: {integrity: sha512-NHY8TUicZXMqykbKYT8kUo2PEU7xu1DFsdRmyWJrLEUY93Xhd3rEdoJ7vFqrvs+Grl9wIm9Byxh3bI+eZxepMQ==} + peerDependencies: + react: '>=16.8.0' + react-dom@19.1.1: resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} peerDependencies: @@ -3084,6 +3111,24 @@ packages: zod@4.0.17: resolution: {integrity: sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==} + zustand@5.0.8: + resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@babel/code-frame@7.27.1': @@ -3418,6 +3463,28 @@ snapshots: optionalDependencies: '@types/react': 19.1.10 + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.10)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.10)(react@19.1.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.10)(react@19.1.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.10)(react@19.1.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.10)(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.10)(react@19.1.1) + aria-hidden: 1.2.6 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-remove-scroll: 2.7.1(@types/react@19.1.10)(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.10 + '@types/react-dom': 19.1.7(@types/react@19.1.10) + '@radix-ui/react-direction@1.1.1(@types/react@19.1.10)(react@19.1.1)': dependencies: react: 19.1.1 @@ -5377,6 +5444,10 @@ snapshots: queue-microtask@1.2.3: {} + react-daum-postcode@3.2.0(react@19.1.1): + dependencies: + react: 19.1.1 + react-dom@19.1.1(react@19.1.1): dependencies: react: 19.1.1 @@ -5978,3 +6049,10 @@ snapshots: yocto-queue@0.1.0: {} zod@4.0.17: {} + + zustand@5.0.8(@types/react@19.1.10)(immer@10.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)): + optionalDependencies: + '@types/react': 19.1.10 + immer: 10.1.1 + react: 19.1.1 + use-sync-external-store: 1.5.0(react@19.1.1) diff --git a/src/entities/auth/hooks/useKakaoLogin.ts b/src/entities/auth/hooks/useKakaoLogin.ts index b8d5ab0..252b62d 100644 --- a/src/entities/auth/hooks/useKakaoLogin.ts +++ b/src/entities/auth/hooks/useKakaoLogin.ts @@ -5,12 +5,9 @@ import { BASE_URL } from '@/shared'; import { getTicketApi } from '../apis'; -// --- 1. "로그인 시작"을 위한 useMutation --- -// 이 훅은 API를 호출하지 않아. 오직 카카오 로그인 페이지로 '이동'시키는 액션만 책임져. export const useKakaoLogin = () => { return useMutation({ mutationFn: () => { - // 실제로는 Promise를 반환할 필요는 없지만, mutationFn은 Promise를 기대해. window.location.href = `${BASE_URL}/oauth2/authorization/kakao`; return Promise.resolve(); }, @@ -20,7 +17,6 @@ export const useKakaoLogin = () => { }); }; -// --- 2. "콜백 처리"를 위한 useQuery --- export const KakaoCallbackQueryKey = { callback: (ticket: string) => ['kakaoCallback', ticket], }; @@ -28,9 +24,7 @@ export const KakaoCallbackQueryKey = { export const useKakaoCallback = (ticket: string) => { return useQuery({ queryKey: KakaoCallbackQueryKey.callback(ticket), - // code가 있을 때만 이 쿼리를 실행해. queryFn: () => getTicketApi(ticket), - // 이 쿼리는 한 번만 실행되면 되므로, 재시도나 캐시 관련 옵션을 조정할 수 있어. retry: 0, staleTime: Infinity, }); diff --git a/src/entities/index.ts b/src/entities/index.ts index 6bf3065..d57fe8e 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -1,2 +1,3 @@ export * from './auth'; export * from './chart'; +export * from './map'; diff --git a/src/entities/map/index.ts b/src/entities/map/index.ts new file mode 100644 index 0000000..e12841b --- /dev/null +++ b/src/entities/map/index.ts @@ -0,0 +1,3 @@ +export * from './types'; +export * from './utils'; +export * from './ui'; diff --git a/src/entities/map/types/index.ts b/src/entities/map/types/index.ts new file mode 100644 index 0000000..4b3f498 --- /dev/null +++ b/src/entities/map/types/index.ts @@ -0,0 +1 @@ +export * from './map.type'; diff --git a/src/entities/map/types/map.type.ts b/src/entities/map/types/map.type.ts new file mode 100644 index 0000000..2510452 --- /dev/null +++ b/src/entities/map/types/map.type.ts @@ -0,0 +1,5 @@ +export type MapAddress = { + address: string; + latitude: number; + longitude: number; +}; diff --git a/src/shared/components/features/kakao-map/KakaoMap.tsx b/src/entities/map/ui/KakaoMap.tsx similarity index 63% rename from src/shared/components/features/kakao-map/KakaoMap.tsx rename to src/entities/map/ui/KakaoMap.tsx index 1fb5eac..5fdc493 100644 --- a/src/shared/components/features/kakao-map/KakaoMap.tsx +++ b/src/entities/map/ui/KakaoMap.tsx @@ -1,5 +1,7 @@ import { Map, MapMarker, useKakaoLoader } from 'react-kakao-maps-sdk'; +import { Spinner } from '@/shared'; + type Props = { lat: number; lng: number; @@ -16,16 +18,12 @@ export const KakaoMap = ({ lat, lng }: Props) => { lng: lng, }; - if (loading) return
Loading...
; - if (error) return
Error: {error.message}
; + if (loading) return ; + + if (error) return
오류 : {error.message}
; return ( - + ); diff --git a/src/shared/components/features/kakao-map/index.ts b/src/entities/map/ui/index.ts similarity index 100% rename from src/shared/components/features/kakao-map/index.ts rename to src/entities/map/ui/index.ts diff --git a/src/entities/map/utils/index.ts b/src/entities/map/utils/index.ts new file mode 100644 index 0000000..d0ce68e --- /dev/null +++ b/src/entities/map/utils/index.ts @@ -0,0 +1 @@ +export * from './search-coordinates'; diff --git a/src/entities/map/utils/search-coordinates.ts b/src/entities/map/utils/search-coordinates.ts new file mode 100644 index 0000000..b8348c6 --- /dev/null +++ b/src/entities/map/utils/search-coordinates.ts @@ -0,0 +1,114 @@ +// 카카오맵 API 타입 정의 +declare global { + interface Window { + kakao: { + maps: { + services: { + Geocoder: new () => { + addressSearch: ( + address: string, + callback: (result: KakaoGeocoderResult[], status: string) => void, + ) => void; + }; + }; + }; + }; + } +} + +export interface KakaoGeocoderResult { + address: { + address_name: string; + b_code: string; + h_code: string; + main_address_no: string; + mountain_yn: string; + region_1depth_name: string; + region_2depth_name: string; + region_3depth_h_name: string; + region_3depth_name: string; + sub_address_no: string; + x: string; + y: string; + }; + road_address: { + address_name: string; + building_name: string; + main_building_no: string; + region_1depth_name: string; + region_2depth_name: string; + region_3depth_name: string; + road_name: string; + sub_building_no: string; + underground_yn: string; + x: string; + y: string; + zone_no: string; + } | null; + x: string; // 경도 + y: string; // 위도 +} + +export interface AddressSearchResult { + success: boolean; + data?: { + address: string; + latitude: number; + longitude: number; + }; + error?: string; +} + +/** + * 카카오맵 API를 사용하여 주소를 좌표로 변환하는 함수 + * @param address 검색할 주소 + * @returns Promise + */ +export const searchAddressToCoordinates = (address: string): Promise => { + return new Promise((resolve) => { + // 카카오맵 API가 로드되지 않은 경우 + if (!window.kakao?.maps?.services?.Geocoder) { + resolve({ + success: false, + error: '카카오맵 API가 로드되지 않았습니다.', + }); + return; + } + + const geocoder = new window.kakao.maps.services.Geocoder(); + + geocoder.addressSearch(address, (result: KakaoGeocoderResult[], status: string) => { + if (status === 'OK' && result.length > 0) { + const firstResult = result[0]; + const addressInfo = firstResult.road_address || firstResult.address; + + resolve({ + success: true, + data: { + address: addressInfo.address_name, + latitude: parseFloat(firstResult.y), + longitude: parseFloat(firstResult.x), + }, + }); + } else if (status === 'ZERO_RESULT') { + resolve({ + success: false, + error: '검색 결과가 없습니다.', + }); + } else { + resolve({ + success: false, + error: `검색 중 오류가 발생했습니다: ${status}`, + }); + } + }); + }); +}; + +/** + * 카카오맵 API 스크립트가 로드되었는지 확인하는 함수 + * @returns boolean + */ +export const isKakaoMapLoaded = (): boolean => { + return !!window.kakao?.maps?.services?.Geocoder; +}; diff --git a/src/features/main/components/common/AddressField.tsx b/src/features/main/components/common/AddressField.tsx index 9dfc80e..0d229e5 100644 --- a/src/features/main/components/common/AddressField.tsx +++ b/src/features/main/components/common/AddressField.tsx @@ -1,12 +1,36 @@ +import DaumPostcode from 'react-daum-postcode'; import { useFormContext } from 'react-hook-form'; -import { FormField, FormItem, FormLabel, FormMessage, Input } from '@/shared'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/shared/components/ui/dialog'; +import { MapPin } from 'lucide-react'; + +import { Button, FormField, FormItem, FormLabel, FormMessage, Input } from '@/shared'; import { FORM_FIELDS, LABEL_TEXTS, PLACEHOLDER_TEXTS } from '../../constants'; +import { useAddressSearch, useKakaoAddressSearch } from '../../hooks'; import { type SearchAddressType } from '../../model'; export const AddressField = () => { + const { isOpen, completeAddress, openSearch, closeSearch } = useAddressSearch(); + const { searchAddress } = useKakaoAddressSearch(); + const form = useFormContext(); + + const searchMap = () => { + const address = form.getValues('address'); + + if (address && address.trim().length > 0) { + // 카카오맵 API로 주소 검색 + searchAddress(); + } + }; + return ( { render={({ field }) => ( {LABEL_TEXTS.ADDRESS} - +
+ + +
+ + + + + 주소 검색 + 주소를 검색하여 선택해주세요. + + + +
)} /> diff --git a/src/features/main/hooks/index.ts b/src/features/main/hooks/index.ts index 6eb5c2e..db3e054 100644 --- a/src/features/main/hooks/index.ts +++ b/src/features/main/hooks/index.ts @@ -1 +1,3 @@ export * from './useChartConfig'; +export * from './useAddressSearch'; +export * from './useKakaoAddressSearch'; diff --git a/src/features/main/hooks/useAddressSearch.ts b/src/features/main/hooks/useAddressSearch.ts new file mode 100644 index 0000000..83acf1f --- /dev/null +++ b/src/features/main/hooks/useAddressSearch.ts @@ -0,0 +1,62 @@ +import { useEffect, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; + +import type { SearchAddressType } from '../model'; +import type { AddressType } from '../types'; + +export const useAddressSearch = () => { + const [isOpen, setIsOpen] = useState(false); + + const form = useFormContext(); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape' && isOpen) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.body.style.overflow = 'hidden'; + document.addEventListener('keydown', handleKeyDown); + } + + return () => { + document.body.style.overflow = 'unset'; + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isOpen]); + + const completeAddress = (data: AddressType) => { + let fullAddress = data.address; + let extraAddress = ''; + + if (data.addressType === 'R') { + if (data.bname !== '') { + extraAddress += data.bname; + } + if (data.buildingName !== '') { + extraAddress += extraAddress !== '' ? `, ${data.buildingName}` : data.buildingName; + } + fullAddress += extraAddress !== '' ? ` (${extraAddress})` : ''; + } + + form.setValue('address', fullAddress); + setIsOpen(false); + }; + + const openSearch = () => { + setIsOpen(true); + }; + + const closeSearch = () => { + setIsOpen(false); + }; + + return { + isOpen, + completeAddress, + openSearch, + closeSearch, + }; +}; diff --git a/src/features/main/hooks/useKakaoAddressSearch.ts b/src/features/main/hooks/useKakaoAddressSearch.ts new file mode 100644 index 0000000..ca1b4f1 --- /dev/null +++ b/src/features/main/hooks/useKakaoAddressSearch.ts @@ -0,0 +1,59 @@ +import { useState } from 'react'; +import { useFormContext } from 'react-hook-form'; + +import { type AddressSearchResult, searchAddressToCoordinates } from '@/entities'; + +import { type SearchAddressType } from '../model'; +import { useMapAddress } from '../store'; + +export const useKakaoAddressSearch = () => { + const [isSearching, setIsSearching] = useState(false); + const [searchResult, setSearchResult] = useState(null); + + const { setMapAddress } = useMapAddress(); + + const form = useFormContext(); + const address = form.watch('address'); + + const searchAddress = async () => { + if (!address || address.trim().length === 0) { + setSearchResult({ + success: false, + error: '주소를 입력해주세요.', + }); + return; + } + + setIsSearching(true); + setSearchResult(null); + + try { + const result = await searchAddressToCoordinates(address); + setSearchResult(result); + + if (result.success && result.data) { + setMapAddress({ + address: result.data.address, + latitude: result.data.latitude, + longitude: result.data.longitude, + }); + } + } catch (error) { + console.error('주소 검색 중 예기치 않은 오류 발생:', error); + + setSearchResult({ + success: false, + error: '주소 검색 중 오류가 발생했습니다.', + }); + } finally { + setIsSearching(false); + } + }; + + return { + searchAddress, + isSearching, + searchResult, + address, + }; +}; diff --git a/src/features/main/index.ts b/src/features/main/index.ts index 5ecdd1f..2da1779 100644 --- a/src/features/main/index.ts +++ b/src/features/main/index.ts @@ -1 +1,2 @@ export * from './ui'; +export * from './hooks'; diff --git a/src/features/main/store/index.ts b/src/features/main/store/index.ts new file mode 100644 index 0000000..c88c0b6 --- /dev/null +++ b/src/features/main/store/index.ts @@ -0,0 +1 @@ +export * from './useMapAddress'; diff --git a/src/features/main/store/useMapAddress.ts b/src/features/main/store/useMapAddress.ts new file mode 100644 index 0000000..1907946 --- /dev/null +++ b/src/features/main/store/useMapAddress.ts @@ -0,0 +1,50 @@ +import { create } from 'zustand'; + +import type { MapAddress } from '@/entities'; + +type MapAddressState = { + mapAddress: MapAddress | null; + + setMapAddress: (address: MapAddress) => void; + clearMapAddress: () => void; + updateCoordinates: (latitude: number, longitude: number) => void; + updateAddress: (address: string) => void; +}; + +export const useMapAddress = create()((set, get) => ({ + mapAddress: null, + + setMapAddress: (address: MapAddress) => { + set({ mapAddress: address }); + }, + + clearMapAddress: () => { + set({ mapAddress: null }); + }, + + updateCoordinates: (latitude: number, longitude: number) => { + const currentAddress = get().mapAddress; + if (currentAddress) { + set({ + mapAddress: { + ...currentAddress, + latitude, + longitude, + }, + }); + } + }, + + updateAddress: (address: string) => { + const currentAddress = get().mapAddress; + + if (currentAddress) { + set({ + mapAddress: { + ...currentAddress, + address, + }, + }); + } + }, +})); diff --git a/src/features/main/types/address.type.ts b/src/features/main/types/address.type.ts new file mode 100644 index 0000000..66f761b --- /dev/null +++ b/src/features/main/types/address.type.ts @@ -0,0 +1,6 @@ +export type AddressType = { + address: string; + addressType: string; + bname: string; + buildingName: string; +}; diff --git a/src/features/main/types/index.ts b/src/features/main/types/index.ts index 8aefa50..40f5843 100644 --- a/src/features/main/types/index.ts +++ b/src/features/main/types/index.ts @@ -1,2 +1,4 @@ +export * from './address.type'; export * from './house-types.type'; export * from './properties-type.type'; +export * from './search-place.type'; diff --git a/src/features/main/types/search-place.type.ts b/src/features/main/types/search-place.type.ts new file mode 100644 index 0000000..1aace4f --- /dev/null +++ b/src/features/main/types/search-place.type.ts @@ -0,0 +1,12 @@ +export type SearchPlaceType = { + placeId: number; + landlordId: number; + address: string; + addressDetail: string; + lat: number; + lng: number; + createdAt: string; + updatedAt: string; + externalProvider: string; + externalPlaceId: string; +}; diff --git a/src/features/main/ui/MapSection.tsx b/src/features/main/ui/MapSection.tsx index 96f0445..ba0e2d5 100644 --- a/src/features/main/ui/MapSection.tsx +++ b/src/features/main/ui/MapSection.tsx @@ -1,8 +1,15 @@ -import { Button, KakaoMap } from '@/shared'; +import { KakaoMap } from '@/entities'; +import { BEXCO_ADDRESS, Button } from '@/shared'; import { HOUSE_TYPES } from '../constants'; +import { useMapAddress } from '../store'; export const MapSection = () => { + const { mapAddress } = useMapAddress(); + + const lat = mapAddress?.latitude || BEXCO_ADDRESS.latitude; + const lng = mapAddress?.longitude || BEXCO_ADDRESS.longitude; + return (
@@ -13,9 +20,8 @@ export const MapSection = () => { ))}
- {/* TODO: 추후 지도 (카카오맵, 네이버 지도, 구글 맵 등) 컴포넌트 추가 */}
- +
); diff --git a/src/shared/components/features/index.ts b/src/shared/components/features/index.ts index 96856a9..37d77ea 100644 --- a/src/shared/components/features/index.ts +++ b/src/shared/components/features/index.ts @@ -1,4 +1,3 @@ export * from './layout'; export * from './header'; export * from './optimized-image'; -export * from './kakao-map'; diff --git a/src/shared/components/ui/dialog.tsx b/src/shared/components/ui/dialog.tsx new file mode 100644 index 0000000..ef9a23a --- /dev/null +++ b/src/shared/components/ui/dialog.tsx @@ -0,0 +1,128 @@ +import * as React from 'react'; + +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { XIcon } from 'lucide-react'; + +import { cn } from '../../utils'; + +function Dialog({ ...props }: React.ComponentProps) { + return ; +} + +function DialogTrigger({ ...props }: React.ComponentProps) { + return ; +} + +function DialogPortal({ ...props }: React.ComponentProps) { + return ; +} + +function DialogClose({ ...props }: React.ComponentProps) { + return ; +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean; +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ); +} + +function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function DialogTitle({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/src/shared/components/ui/index.ts b/src/shared/components/ui/index.ts index f25fb2f..c4ae336 100644 --- a/src/shared/components/ui/index.ts +++ b/src/shared/components/ui/index.ts @@ -7,3 +7,5 @@ export * from './label'; export * from './avatar'; export * from './radio-group'; export * from './select'; +export * from './spinner'; +export * from './dialog'; diff --git a/src/shared/components/ui/spinner.tsx b/src/shared/components/ui/spinner.tsx new file mode 100644 index 0000000..7e2fe83 --- /dev/null +++ b/src/shared/components/ui/spinner.tsx @@ -0,0 +1,13 @@ +export const Spinner = () => { + return ( +
+
+ Loading... +
+
+ ); +}; diff --git a/src/shared/constants/default-map-value.ts b/src/shared/constants/default-map-value.ts new file mode 100644 index 0000000..3126f4d --- /dev/null +++ b/src/shared/constants/default-map-value.ts @@ -0,0 +1,5 @@ +export const BEXCO_ADDRESS = { + address: '부산 해운대구 APEC로 55', + latitude: 35.1690637154991, + longitude: 129.136018268316, +}; diff --git a/src/shared/constants/index.ts b/src/shared/constants/index.ts index 0899ce9..798e507 100644 --- a/src/shared/constants/index.ts +++ b/src/shared/constants/index.ts @@ -1,3 +1,4 @@ export * from './router-path'; export * from './risk-chart-segments'; export * from './chart-color'; +export * from './default-map-value';