Skip to content
Open
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
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<!-- 네이버 지도 API v3 -->
<script
type="text/javascript"
src="https://oapi.map.naver.com/openapi/v3/maps.js?ncpKeyId=%VITE_NAVER_MAP_CLIENT_ID%"
src="https://oapi.map.naver.com/openapi/v3/maps.js?ncpKeyId=%VITE_NAVER_MAP_CLIENT_ID%&amp;submodules=geocoder"
></script>
</head>
<body>
Expand Down
69 changes: 42 additions & 27 deletions src/features/store-register/hooks/useStoreRegisterWizard.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
isValidWorkspaceLocation,
type WorkspaceLocation,
} from '@/features/store-register/lib/geocodeWorkspaceAddress'
import { useCallback, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useQueryClient } from '@tanstack/react-query'
Expand Down Expand Up @@ -36,10 +40,8 @@ export function useStoreRegisterWizard() {
const [step, setStep] = useState<Step>('info')
const [bizName, setBizName] = useState('')
const [brn, setBrn] = useState('')
const [province, setProvince] = useState('')
const [district, setDistrict] = useState('')
const [town, setTown] = useState('')
const [address, setAddress] = useState('')
const [location, setLocation] = useState<WorkspaceLocation | null>(null)
const [detailAddress, setDetailAddress] = useState('')
const [businessTypeId, setBusinessTypeId] = useState<number | null>(null)
const [businessTypeDetail, setBusinessTypeDetail] = useState('')
const [contact, setContact] = useState('')
Expand All @@ -63,10 +65,7 @@ export function useStoreRegisterWizard() {
const infoValid =
bizName.trim().length > 0 &&
brn.replace(/\D/g, '').length === 10 &&
province.trim().length > 0 &&
district.trim().length > 0 &&
town.trim().length > 0 &&
address.trim().length > 0 &&
isValidWorkspaceLocation(location) &&
businessTypeValid &&
contact.trim().length > 0

Expand All @@ -77,7 +76,13 @@ export function useStoreRegisterWizard() {
const goCertificate = () => setStep('certificate')

const submit = useCallback(async () => {
if (!certFile.file || !identityFile.file || !infoValid || !businessTypeId) {
if (
!certFile.file ||
!identityFile.file ||
!infoValid ||
!businessTypeId ||
!isValidWorkspaceLocation(location)
) {
return
}

Expand Down Expand Up @@ -117,10 +122,26 @@ export function useStoreRegisterWizard() {
await createWorkspaceRegistrationRequest(scope, {
bizName,
brn,
province,
district,
town,
address,
province: location.province,
district: location.district,
town: location.town,
// 기존 서버의 지역 + address 조합과 중복되지 않도록 지번의 나머지만 유지합니다.
address: location.jibunAddress.startsWith(
[location.province, location.district, location.town]
.filter(Boolean)
.join(' ') + ' '
)
? location.jibunAddress
.slice(
[location.province, location.district, location.town]
.filter(Boolean)
.join(' ').length
)
.trim()
: location.jibunAddress,
roadAddress: location.roadAddress,
jibunAddress: location.jibunAddress,
detailAddress: detailAddress.trim(),
businessTypeId,
businessTypeDetail: businessTypeDetail.trim() || null,
contact,
Expand All @@ -131,8 +152,8 @@ export function useStoreRegisterWizard() {
representativeImages: representativeImages.images.map(
(image, index) => ({ fileId: image.fileId, sortOrder: index })
),
latitude: 37.5665,
longitude: 126.978,
latitude: location.latitude,
longitude: location.longitude,
})
} catch (e) {
setSubmitError(
Expand All @@ -155,17 +176,15 @@ export function useStoreRegisterWizard() {
setIsSubmitting(false)
}
}, [
address,
location,
detailAddress,
brn,
certFile.file,
contact,
district,
identityFile.file,
infoValid,
province,
representativeImages.images,
scope,
town,
businessTypeId,
businessTypeDetail,
warrantFile.file,
Expand All @@ -180,10 +199,6 @@ export function useStoreRegisterWizard() {
step,
bizName,
brn,
province,
district,
town,
address,
businessTypeId,
businessTypeDetail,
businessTypes,
Expand All @@ -195,10 +210,10 @@ export function useStoreRegisterWizard() {
contact,
setBizName,
setBrn: setBrnMasked,
setProvince,
setDistrict,
setTown,
setAddress,
location,
setLocation,
detailAddress,
setDetailAddress,
setBusinessTypeId,
setBusinessTypeDetail,
setContact: setContactMasked,
Expand Down
176 changes: 176 additions & 0 deletions src/features/store-register/lib/geocodeWorkspaceAddress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
export type WorkspaceLocation = {
roadAddress: string | null
jibunAddress: string
latitude: number
longitude: number
province: string
district: string
town: string
}
/** 기존 등록 계약에서 사용하는 지역값과 좌표를 제출 직전에도 검사합니다. */
export function isValidWorkspaceLocation(
location: WorkspaceLocation | null
): location is WorkspaceLocation {
return (
!!location &&
!!location.province.trim() &&
!!location.district.trim() &&
!!location.town.trim() &&
!!(location.roadAddress?.trim() || location.jibunAddress.trim()) &&
Number.isFinite(location.latitude) &&
Math.abs(location.latitude) <= 90 &&
Number.isFinite(location.longitude) &&
Math.abs(location.longitude) <= 180
)
}

type Address = {
x: string
y: string
roadAddress: string
jibunAddress: string
addressElements: { types: string[]; longName: string }[]
}
type Region = {
area1: { name: string }
area2: { name: string }
area3: { name: string }
}
type Service = {
Status: { OK: number }
geocode(
options: { query: string; count: number },
callback: (
status: number,
response: { v2: { addresses: Address[] } }
) => void
): void
reverseGeocode(
options: { coords: { lat: number; lng: number }; orders: string },
callback: (
status: number,
response: {
v2: {
address: { roadAddress: string; jibunAddress: string }
results: { region: Region }[]
}
}
) => void
): void
}
function service(): Service {
const api = (window as Window & { naver?: { maps?: { Service?: Service } } })
.naver?.maps?.Service
if (!api)
throw new Error(
'주소 검색을 불러오지 못했습니다. 새로고침 후 다시 시도해 주세요.'
)
return api
}
function request<T>(
run: (done: (value: T) => void, fail: (error: Error) => void) => void
): Promise<T> {
return new Promise((resolve, reject) => {
const timer = window.setTimeout(
() =>
reject(new Error('주소 검색이 지연되고 있습니다. 다시 시도해 주세요.')),
10000
)
const done = (value: T) => {
window.clearTimeout(timer)
resolve(value)
}
const fail = (error: Error) => {
window.clearTimeout(timer)
reject(error)
}
try {
run(done, fail)
} catch (e) {
fail(e instanceof Error ? e : new Error('주소 검색에 실패했습니다.'))
}
})
}
export function searchWorkspaceAddresses(
query: string
): Promise<WorkspaceLocation[]> {
return request((done, fail) => {
const api = service()
api.geocode({ query: query.trim(), count: 20 }, (status, response) => {
if (status !== api.Status.OK) {
fail(new Error('주소 검색에 실패했습니다. 잠시 후 다시 시도해 주세요.'))
return
}
const items = (response?.v2?.addresses ?? []).filter(
a =>
a.x?.trim() &&
a.y?.trim() &&
Number.isFinite(Number(a.x)) &&
Number.isFinite(Number(a.y)) &&
Math.abs(Number(a.x)) <= 180 &&
Math.abs(Number(a.y)) <= 90 &&
a.jibunAddress
)
done(
items
.map(a => {
const region = (type: string) =>
a.addressElements
?.find(e => e.types?.includes(type))
?.longName?.trim() ?? ''
return {
roadAddress: a.roadAddress || null,
jibunAddress: a.jibunAddress,
latitude: Number(a.y),
longitude: Number(a.x),
province: region('SIDO'),
district: region('SIGUGUN'),
town: region('DONGMYUN'),
}
})
.filter(isValidWorkspaceLocation)
)
})
})
}
export function reverseWorkspaceAddress(
latitude: number,
longitude: number
): Promise<WorkspaceLocation> {
return request((done, fail) => {
const api = service()
api.reverseGeocode(
{ coords: { lat: latitude, lng: longitude }, orders: 'addr,roadaddr' },
(status, response) => {
const address = response?.v2?.address
const region = response?.v2?.results?.[0]?.region
if (status !== api.Status.OK || !address?.jibunAddress || !region) {
fail(
new Error(
'선택한 위치의 주소를 찾지 못했습니다. 건물 위치를 다시 선택하거나 주소로 검색해 주세요.'
)
)
return
}
const location = {
latitude,
longitude,
roadAddress: address.roadAddress || null,
jibunAddress: address.jibunAddress,
province: region.area1?.name?.trim() ?? '',
district: region.area2?.name?.trim() ?? '',
town: region.area3?.name?.trim() ?? '',
}
if (!isValidWorkspaceLocation(location)) {
fail(
new Error(
'선택한 위치의 지역 정보가 부족해 등록할 수 없습니다. 다른 위치를 선택하거나 주소로 검색해 주세요.'
)
)
return
}
done(location)
}
)
})
}
9 changes: 9 additions & 0 deletions src/features/store-register/types/workspaceRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export type BusinessTypesApiResponse = CommonApiResponse<BusinessTypeDto[]>
export interface WorkspaceRequestListItemDto {
id: number
businessName: string
roadAddress?: string | null
jibunAddress?: string | null
detailAddress?: string | null
fullAddress: string
createdAt: string
status: WorkspaceRequestStatusDto
Expand All @@ -45,6 +48,9 @@ export interface WorkspaceRequestDetailDto {
businessType: string
contact: string
status: WorkspaceRequestStatusDto
roadAddress?: string | null
jibunAddress?: string | null
detailAddress?: string | null
fullAddress: string
latitude: number
longitude: number
Expand All @@ -70,6 +76,9 @@ export interface WorkspaceRepresentativeImageBody {
export type WorkspaceRegistrationCreateBody = {
bizName: string
brn: string
roadAddress: string | null
jibunAddress: string
detailAddress: string
address: string
province: string
district: string
Expand Down
13 changes: 12 additions & 1 deletion src/features/store-register/ui/RequestInfoSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,18 @@ export function RequestInfoSection({ detail }: Props) {
<div className="border-t border-line-1" />
<InfoRow label="연락처" value={detail.contact} />
<div className="border-t border-line-1" />
<InfoRow label="주소" value={detail.fullAddress} />
<InfoRow
label="주소"
value={
detail.roadAddress || detail.jibunAddress || detail.fullAddress
}
/>
{detail.jibunAddress && (
<InfoRow label="지번 주소" value={detail.jibunAddress} />
)}
{detail.detailAddress && (
<InfoRow label="상세주소" value={detail.detailAddress} />
)}
</div>
</section>
)
Expand Down
Loading
Loading