Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions apps/web/app/_utils/device/device.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* 모바일 디바이스 감지 정규식
*/
const MOBILE_REGEX = /iPhone|iPad|iPod|Android/i
const IOS_REGEX = /iPhone|iPad|iPod/i

/**
* 현재 디바이스가 모바일인지 확인
*/
export const isMobileDevice = (): boolean =>
MOBILE_REGEX.test(navigator.userAgent)

/**
* 현재 디바이스가 iOS인지 확인
*/
export const isIOSDevice = (): boolean => IOS_REGEX.test(navigator.userAgent)
1 change: 1 addition & 0 deletions apps/web/app/_utils/device/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { isMobileDevice, isIOSDevice } from './device'
1 change: 1 addition & 0 deletions apps/web/app/_utils/openDeepLink/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { openDeepLink } from './openDeepLink'
43 changes: 43 additions & 0 deletions apps/web/app/_utils/openDeepLink/openDeepLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
interface OpenDeepLinkParams {
appScheme: string
fallbackUrl: string
timeout?: number
}

const DEEPLINK_TIMEOUT = 2500

/**
* 딥링크 실행 유틸 함수
* - 앱이 설치되어 있으면 앱 실행
* - 앱이 없으면 timeout 이후 fallbackUrl로 이동
* - 앱 실행 시 페이지가 백그라운드로 가면 fallback 취소
*/
export const openDeepLink = ({
appScheme,
fallbackUrl,
timeout = DEEPLINK_TIMEOUT,
}: OpenDeepLinkParams): void => {
const startTime = Date.now()

// 페이지가 백그라운드로 가면 앱이 실행된 것으로 간주
const handleVisibilityChange = () => {
if (document.hidden) {
// 앱이 실행되어 페이지가 숨겨짐
clearTimeout(fallbackTimeout)
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}

document.addEventListener('visibilitychange', handleVisibilityChange)

// 앱이 설치되지 않은 경우를 대비한 타임아웃
const fallbackTimeout = setTimeout(() => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
// 페이지가 포그라운드 상태로 유지되었다면 앱이 없는 것으로 간주
if (!document.hidden && Date.now() - startTime >= timeout) {
window.location.href = fallbackUrl
}
}, timeout)

window.location.href = appScheme
}
1 change: 1 addition & 0 deletions apps/web/app/_utils/openKakaoTaxi/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { openKakaoTaxi } from './openKakaoTaxi'
35 changes: 35 additions & 0 deletions apps/web/app/_utils/openKakaoTaxi/openKakaoTaxi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { addToast } from '@heroui/react'
import { openDeepLink } from '../openDeepLink'
import { isMobileDevice, isIOSDevice } from '../device'
import { Coord } from '@/map/_utils/toLatLng'

interface OpenKakaoTaxiParams extends Coord {
placeName?: string
}

/**
* 카카오택시 앱으로 특정 목적지를 설정하는 딥링크 함수
* - 모바일: 카카오택시 앱이 설치되어 있으면 앱 실행, 없으면 앱스토어로 이동
* - 데스크톱: 카카오택시 앱은 모바일 전용이므로 토스트 메시지로 안내
*/
export const openKakaoTaxi = ({
latitude,
longitude,
placeName = '목적지',
}: OpenKakaoTaxiParams): void => {
if (isMobileDevice()) {
const urls = buildKakaoTaxiUrls({ latitude, longitude }, placeName)
openDeepLink({ appScheme: urls.app, fallbackUrl: urls.store })
} else {
addToast({
title: '카카오택시 앱은 모바일에서만 이용 가능합니다.',
})
}
}

const buildKakaoTaxiUrls = (coords: Coord, placeName: string) => ({
app: `kakaot://taxi?dest_lat=${coords.latitude}&dest_lng=${coords.longitude}&end_name=${encodeURIComponent(placeName)}`,
store: isIOSDevice()
? 'https://apps.apple.com/kr/app/kakaotaxi/id981110422'
: 'https://play.google.com/store/apps/details?id=com.kakao.taxi',
})
1 change: 1 addition & 0 deletions apps/web/app/_utils/openNaverMap/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { openNaverMap } from './openNaverMap'
32 changes: 32 additions & 0 deletions apps/web/app/_utils/openNaverMap/openNaverMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { openDeepLink } from '../openDeepLink'
import { isMobileDevice } from '../device'
import { Coord } from '@/map/_utils/toLatLng'

interface OpenNaverMapParams extends Coord {
placeName?: string
}

/**
* 네이버 지도 앱으로 특정 위치를 여는 딥링크 함수
* - 모바일: 네이버 지도 앱이 설치되어 있으면 앱 실행, 없으면 웹으로 이동
* - 데스크톱: 네이버 지도 웹 페이지로 이동
*/
export const openNaverMap = ({
latitude,
longitude,
placeName = '공주대학교',
}: OpenNaverMapParams): void => {
const urls = buildNaverMapUrls({ latitude, longitude }, placeName)

if (isMobileDevice()) {
openDeepLink({ appScheme: urls.app, fallbackUrl: urls.web })
} else {
// 데스크톱: 웹 페이지로 바로 이동
window.open(urls.web, '_blank')
}
}

const buildNaverMapUrls = (coords: Coord, placeName: string) => ({
app: `nmap://place?lat=${coords.latitude}&lng=${coords.longitude}&name=${encodeURIComponent(placeName)}&appname=com.matzip`,
web: `https://map.naver.com/p/search/${encodeURIComponent(placeName)}?c=${coords.longitude},${coords.latitude},18,0,0,0,dh`,
})
2 changes: 1 addition & 1 deletion apps/web/app/places/[id]/PlaceDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const PlaceDetailPage = ({ id }: { id: string }) => {
</Carousel>
<Column className={'flex-1 justify-around gap-4 p-5'}>
<Section icon={'pin'} title={'위치'}>
<Location location={location} />
<Location location={location} placeName={placeName} />
</Section>
<Section icon={'note'} title={'메뉴'}>
<Menus menus={menus} />
Expand Down
93 changes: 82 additions & 11 deletions apps/web/app/places/[id]/_components/Location/Location.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,91 @@ import { useState } from 'react'
import { Container, NaverMap } from 'react-naver-maps'
import { type Coord, toLatLng } from '@/map/_utils/toLatLng'
import { PlaceMarker } from '@/map/_components/Marker'
import { Column } from '@repo/ui/components/Layout'
import { openNaverMap } from '@/_utils/openNaverMap'
import { openKakaoTaxi } from '@/_utils/openKakaoTaxi'
import Image from 'next/image'
import { cn } from '@repo/ui/utils/cn'

export const Location = ({ location }: { location: Coord }) => {
interface LocationProps {
location: Coord
placeName: string
}

export const Location = ({ location, placeName }: LocationProps) => {
const [, setMap] = useState<naver.maps.Map | null>(null)

const handleOpenNaverMap = () => {
openNaverMap({
latitude: location.latitude,
longitude: location.longitude,
placeName,
})
}

const handleOpenKakaoTaxi = () => {
openKakaoTaxi({
latitude: location.latitude,
longitude: location.longitude,
placeName,
})
}

return (
<Container className={'h-[150px] overflow-hidden rounded-xl'}>
<NaverMap
defaultZoom={18}
minZoom={15}
ref={setMap}
defaultCenter={toLatLng(location)}
>
<PlaceMarker position={location} icon={'logo'} />
</NaverMap>
</Container>
<Column className={'gap-3'}>
<Container className={'relative h-[150px] overflow-hidden rounded-xl'}>
<Column className={'absolute right-2 top-2 z-10 gap-2'}>
<MapButton
onClick={handleOpenKakaoTaxi}
imageSrc={'/images/kakao-taxi-logo.png'}
alt={'kakao-taxi-logo'}
/>
<MapButton
onClick={handleOpenNaverMap}
imageSrc={'/images/naver-map-logo.webp'}
alt={'naver-map-logo'}
/>
</Column>
<NaverMap
draggable={false}
defaultZoom={18}
minZoom={15}
ref={setMap}
defaultCenter={toLatLng(location)}
>
<PlaceMarker position={location} icon={'logo'} />
</NaverMap>
</Container>
</Column>
)
}

interface MapButtonProps {
onClick: VoidFunction
imageSrc: string
alt: string
}

const MapButton = ({ onClick, imageSrc, alt }: MapButtonProps) => (
<button
type={'button'}
className={cn(
'border-1 border-gray-100',
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

border-1은 표준 Tailwind 유틸리티 클래스가 아닐 수 있습니다

공식 Tailwind 문서의 border-width 예시는 border, border-2, border-4, border-8만 나열하며 border-1은 포함되지 않습니다. 이 클래스가 인식되지 않으면 버튼 테두리가 렌더링되지 않습니다. 1px 테두리에는 border를 사용해야 합니다.

🐛 제안 수정
-      'border-1 border-gray-100',
+      'border border-gray-100',

프로젝트의 Tailwind 버전에서 border-1이 유효한지 확인하려면 아래 스크립트를 실행하세요:

#!/bin/bash
# Description: Check Tailwind version in the project and verify if border-1 is used elsewhere

# Check Tailwind CSS version
cat package.json | grep -i tailwind 2>/dev/null
fd "package.json" --max-depth 3 --exec grep -l "tailwindcss" {} \; | xargs -I{} sh -c 'echo "=== {} ===" && grep "tailwindcss" {}'

# Check if border-1 is used elsewhere in the codebase
rg -n '\bborder-1\b' --type=tsx --type=ts --type=css
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/places/`[id]/_components/Location/Location.tsx at line 74, In
Location.tsx replace the nonstandard Tailwind class 'border-1' with the correct
1px utility 'border' where the class list includes "'border-1 border-gray-100'";
update the class string/array inside the Location component (the place that
builds the element classes) to use "border border-gray-100" so the 1px border
renders correctly across Tailwind versions.

'rounded-lg',
'bg-white',
'p-1',
'shadow',
'transition-transform',
'hover:scale-105',
)}
onClick={onClick}
>
<Image
src={imageSrc}
alt={alt}
width={20}
height={20}
className={'rounded-sm'}
/>
</button>
)
28 changes: 28 additions & 0 deletions apps/web/app/places/new/_components/Step/PlacePreview/Location.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useState } from 'react'
import { Container, NaverMap } from 'react-naver-maps'
import { type Coord, toLatLng } from '@/map/_utils/toLatLng'
import { PlaceMarker } from '@/map/_components/Marker'
import { Column } from '@repo/ui/components/Layout'

interface LocationProps {
location: Coord
}

export const Location = ({ location }: LocationProps) => {
const [, setMap] = useState<naver.maps.Map | null>(null)

return (
<Column className={'gap-3'}>
<Container className={'h-[150px] overflow-hidden rounded-xl'}>
<NaverMap
defaultZoom={18}
minZoom={15}
ref={setMap}
defaultCenter={toLatLng(location)}
>
<PlaceMarker position={location} icon={'logo'} />
</NaverMap>
</Container>
</Column>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { useFormContext } from 'react-hook-form'
import type { NewPlaceRequest } from '@/_apis/schemas/place'
import { usePlaceQueries } from '@/_apis/queries/place'
import { Carousel } from '@repo/ui/components/Carousel'
import { Location, Menus } from '@/places/[id]/_components'
import { Menus } from '@/places/[id]/_components'
import { Location } from './Location'
import { Text } from '@repo/ui/components/Text'
import { Button } from '@repo/ui/components/Button'
import { Column, Flex, VerticalScrollArea } from '@repo/ui/components/Layout'
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/requests/[id]/RequestDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const RequestDetailPage = ({ id }: { id: string }) => {
</Carousel>
<Column className={'flex-1 justify-around gap-4 p-5'}>
<Section icon={'pin'} title={'위치'}>
<Location location={location} />
<Location location={location} placeName={placeName} />
</Section>
<Section icon={'note'} title={'메뉴'}>
<Menus menus={menus} />
Expand Down
Binary file added apps/web/public/images/kakao-taxi-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/images/naver-map-logo.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.