diff --git a/package.json b/package.json
index a0a615dc..a030571c 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
"path-to-regexp": "^8.2.0",
"react": "^19",
"react-datepicker": "^7.4.0",
+ "react-daum-postcode": "^3.2.0",
"react-dom": "^19",
"react-error-boundary": "^4.1.2",
"react-hook-form": "^7.53.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a1af55a5..d954e936 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -71,6 +71,9 @@ importers:
react-datepicker:
specifier: ^7.4.0
version: 7.4.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ react-daum-postcode:
+ specifier: ^3.2.0
+ version: 3.2.0(react@19.0.0)
react-dom:
specifier: ^19
version: 19.0.0(react@19.0.0)
@@ -5472,6 +5475,11 @@ packages:
react: ^16.9.0 || ^17 || ^18
react-dom: ^16.9.0 || ^17 || ^18
+ react-daum-postcode@3.2.0:
+ resolution: {integrity: sha512-NHY8TUicZXMqykbKYT8kUo2PEU7xu1DFsdRmyWJrLEUY93Xhd3rEdoJ7vFqrvs+Grl9wIm9Byxh3bI+eZxepMQ==}
+ peerDependencies:
+ react: '>=16.8.0'
+
react-docgen-typescript@2.2.2:
resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==}
peerDependencies:
@@ -12757,6 +12765,10 @@ snapshots:
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
+ react-daum-postcode@3.2.0(react@19.0.0):
+ dependencies:
+ react: 19.0.0
+
react-docgen-typescript@2.2.2(typescript@5.5.4):
dependencies:
typescript: 5.5.4
diff --git a/src/app/providers/google-map-provider.tsx b/src/app/providers/google-map-provider.tsx
new file mode 100644
index 00000000..5fa9deb9
--- /dev/null
+++ b/src/app/providers/google-map-provider.tsx
@@ -0,0 +1,10 @@
+'use client'
+
+import { APIProvider } from '@vis.gl/react-google-maps'
+import { ReactNode } from 'react'
+
+export const GoogleMapProvider = ({ children }: { children: ReactNode }) => {
+ const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || ''
+
+ return {children}
+}
diff --git a/src/app/providers/index.tsx b/src/app/providers/index.tsx
index 50ec8c2d..7f9eea6b 100644
--- a/src/app/providers/index.tsx
+++ b/src/app/providers/index.tsx
@@ -1,6 +1,7 @@
import { SessionProvider } from 'next-auth/react'
import { ReactNode } from 'react'
+import { GoogleMapProvider } from './google-map-provider'
import { MSWProvider } from './msw-provider'
import { QueryProvider } from './query-provider'
import { ToastProvider } from './toast-provider'
@@ -15,7 +16,9 @@ export const AppProviders = ({ basePath, children }: Props) => {
- {children}
+
+ {children}
+
diff --git a/src/entities/job-post/config/location.ts b/src/entities/job-post/config/location.ts
index 070a4a9b..c8d7a0b0 100644
--- a/src/entities/job-post/config/location.ts
+++ b/src/entities/job-post/config/location.ts
@@ -1,19 +1,19 @@
export enum Location {
- SEOUL = 'Seoul',
- BUSAN = 'Busan',
- DAEGU = 'Daegu',
- INCHEON = 'Incheon',
- GWANGJU = 'Gwangju',
- DAEJEON = 'Daejeon',
- ULSAN = 'Ulsan',
- SEJONG = 'Sejong',
- GYEONGGI = 'Gyeonggi',
- GANGWON = 'Gangwon',
- CHUNGBUK = 'Chungcheongbuk-do',
- CHUNGNAM = 'Chungcheongnam-do',
- JEONBUK = 'Jeollabuk-do',
- JEONNAM = 'Jeollanam-do',
- GYEONGBUK = 'Gyeongsangbuk-do',
- GYEONGNAM = 'Gyeongsangnam-do',
- JEJU = 'Jeju',
+ SEOUL = 'SEOUL',
+ BUSAN = 'BUSAN',
+ DAEGU = 'DAEGU',
+ INCHEON = 'INCHEON',
+ GWANGJU = 'GWANGJU',
+ DAEJEON = 'DAEJEON',
+ ULSAN = 'ULSAN',
+ SEJONG = 'SEJONG',
+ GYEONGGI = 'GYEONGGI',
+ GANGWON = 'GANGWON',
+ CHUNGBUK = 'CHUNGBUK',
+ CHUNGNAM = 'CHUNGNAM',
+ JEONBUK = 'JEONBUK',
+ JEONNAM = 'JEONNAM',
+ GYEONGBUK = 'GYEONGBUK',
+ GYEONGNAM = 'GYEONGNAM',
+ JEJU = 'JEJU',
}
diff --git a/src/entities/job-post/ui/academy-address/index.tsx b/src/entities/job-post/ui/academy-address/index.tsx
index b9e4ebe5..9e006b68 100644
--- a/src/entities/job-post/ui/academy-address/index.tsx
+++ b/src/entities/job-post/ui/academy-address/index.tsx
@@ -1,6 +1,6 @@
'use client'
-import { AdvancedMarker, APIProvider, Map } from '@vis.gl/react-google-maps'
+import { AdvancedMarker, Map } from '@vis.gl/react-google-maps'
import React from 'react'
const Pin = () => {
@@ -29,22 +29,18 @@ type Props = {
}
const AcademyAddress = ({ address, lat, lng }: Props) => {
- const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || ''
-
return (
-
-
-
+
{address ?? '-'}
diff --git a/src/entities/job-post/ui/posting-title/index.tsx b/src/entities/job-post/ui/posting-title/index.tsx
index 8f767735..16f28eed 100644
--- a/src/entities/job-post/ui/posting-title/index.tsx
+++ b/src/entities/job-post/ui/posting-title/index.tsx
@@ -1,5 +1,6 @@
import { format } from 'date-fns'
-import { capitalize } from 'lodash-es'
+import { lowerCase } from 'lodash-es'
+import { useTranslations } from 'next-intl'
import { colors } from 'shared/config'
import { cn } from 'shared/lib'
@@ -15,6 +16,8 @@ type Props = {
}
export const PostingTitle = ({ jobPost, size }: Props) => {
+ const t = useTranslations()
+
const studentType = convertStudentType({
forKindergarten: jobPost.forKindergarten,
forElementary: jobPost.forElementary,
@@ -32,7 +35,9 @@ export const PostingTitle = ({ jobPost, size }: Props) => {
-
- {capitalize(jobPost.locationType ?? '-')}
+
+ {t(`field.location.option.${lowerCase(jobPost.locationType)}`)}
+
-
diff --git a/src/features/job-post-filter/ui/job-post-filters.tsx b/src/features/job-post-filter/ui/job-post-filters.tsx
index 4f5fb579..8260cd82 100644
--- a/src/features/job-post-filter/ui/job-post-filters.tsx
+++ b/src/features/job-post-filter/ui/job-post-filters.tsx
@@ -1,3 +1,5 @@
+import { lowerCase } from 'lodash-es'
+import { useTranslations } from 'next-intl'
import { ChangeEvent, KeyboardEvent } from 'react'
import { Location, StudentType } from 'entities/job-post'
@@ -19,6 +21,8 @@ export const JobPostFilters = ({
useSearchField = false,
onChange,
}: Props) => {
+ const t = useTranslations()
+
const {
filters,
isFilterExist,
@@ -89,7 +93,7 @@ export const JobPostFilters = ({
>
{Object.keys(Location).map(key => (
- {Location[key as keyof typeof Location]}
+ {t(`field.location.option.${lowerCase(key)}`)}
))}
@@ -116,7 +120,7 @@ export const JobPostFilters = ({
{filters.locations.map(location => (
- {Location[location as keyof typeof Location]}
+ {t(`field.location.option.${lowerCase(location as string)}`)}
handleLocationFilterRemove(location)}
diff --git a/src/features/sign-up/lib/use-geocoding.ts b/src/features/sign-up/lib/use-geocoding.ts
new file mode 100644
index 00000000..e768d9ca
--- /dev/null
+++ b/src/features/sign-up/lib/use-geocoding.ts
@@ -0,0 +1,33 @@
+import { useMapsLibrary } from '@vis.gl/react-google-maps'
+import { useEffect, useRef } from 'react'
+
+export const useGeocoding = () => {
+ const lib = useMapsLibrary('geocoding')
+ const geocoder = useRef(null)
+
+ useEffect(() => {
+ if (lib) {
+ geocoder.current = new lib.Geocoder()
+ }
+ }, [lib])
+
+ const geocode = async (
+ address: string,
+ callback: (result: { lat: number; lng: number }) => void,
+ ) => {
+ if (!geocoder.current) return null
+
+ await geocoder.current.geocode({ address }, (results, status) => {
+ if (status === 'OK' && results?.[0]) {
+ const { location } = results[0].geometry
+
+ callback({
+ lat: location.lat(),
+ lng: location.lng(),
+ })
+ }
+ })
+ }
+
+ return { geocode }
+}
diff --git a/src/features/sign-up/lib/util.ts b/src/features/sign-up/lib/util.ts
new file mode 100644
index 00000000..2581f70a
--- /dev/null
+++ b/src/features/sign-up/lib/util.ts
@@ -0,0 +1,42 @@
+import { Location } from 'entities/job-post'
+
+export const convertToLocationType = (sido: string) => {
+ switch (sido) {
+ case '서울':
+ return Location.SEOUL
+ case '부산':
+ return Location.BUSAN
+ case '대구':
+ return Location.DAEGU
+ case '인천':
+ return Location.INCHEON
+ case '광주':
+ return Location.GWANGJU
+ case '대전':
+ return Location.DAEJEON
+ case '울산':
+ return Location.ULSAN
+ case '세종특별자치시':
+ return Location.SEJONG
+ case '경기':
+ return Location.GYEONGGI
+ case '강원특별자치도':
+ return Location.GANGWON
+ case '충북':
+ return Location.CHUNGBUK
+ case '충남':
+ return Location.CHUNGNAM
+ case '전북특별자치도':
+ return Location.JEONBUK
+ case '전남':
+ return Location.JEONNAM
+ case '경북':
+ return Location.GYEONGBUK
+ case '경남':
+ return Location.GYEONGNAM
+ case '제주특별자치도':
+ return Location.JEJU
+ default:
+ return null
+ }
+}
diff --git a/src/features/sign-up/ui/field/address.tsx b/src/features/sign-up/ui/field/address.tsx
index dbe0a304..35221201 100644
--- a/src/features/sign-up/ui/field/address.tsx
+++ b/src/features/sign-up/ui/field/address.tsx
@@ -1,22 +1,76 @@
import { useTranslations } from 'next-intl'
+import { ChangeEvent, useState } from 'react'
+import DaumPostcode, { type Address as AddressType } from 'react-daum-postcode'
+import { useFormContext } from 'react-hook-form'
import { fieldCss } from 'shared/form'
-import { Button, Label, TextField } from 'shared/ui'
+import { Button, Label, Modal, TextField } from 'shared/ui'
+
+import { useGeocoding } from '../../lib/use-geocoding'
+import { convertToLocationType } from '../../lib/util'
export const Address = () => {
const t = useTranslations()
+ const { geocode } = useGeocoding()
+
+ const [isOpen, setIsOpen] = useState(false)
+ const [address, setAddress] = useState('')
+ const [detailedAddress, setDetailedAddress] = useState('')
+
+ const form = useFormContext()
+
+ const handleButtonClick = () => {
+ setIsOpen(true)
+ }
+
+ const handleCompleteSearchingCode = (data: AddressType) => {
+ setAddress(data.address)
+ setDetailedAddress('')
+
+ form.setValue('detailedAddress', data.address)
+ form.setValue('locationType', convertToLocationType(data.sido))
+
+ geocode(data.address, ({ lat, lng }) => {
+ form.setValue('lat', lat)
+ form.setValue('lng', lng)
+ })
+
+ setIsOpen(false)
+ }
+
+ const handleAddressChange = (event: ChangeEvent) => {
+ form.setValue('detailedAddress', `${address} ${event.target.value}`)
+ setDetailedAddress(event.target.value)
+ }
+
return (
-
-
+
+
+
+
+
+
+
-
-
+
)
}
diff --git a/src/shared/config/internationalization/locales/en/field.json b/src/shared/config/internationalization/locales/en/field.json
index b1bbef3f..f2da470f 100644
--- a/src/shared/config/internationalization/locales/en/field.json
+++ b/src/shared/config/internationalization/locales/en/field.json
@@ -49,6 +49,27 @@
"business-registration-number": {
"label": "Business Registration Number",
"placeholder": "Enter 10 digits only (no dashes)"
+ },
+ "location": {
+ "option": {
+ "seoul": "Seoul",
+ "busan": "Busan",
+ "daegu": "Daegu",
+ "incheon": "Incheon",
+ "gwangju": "Gwangju",
+ "daejeon": "Daejeon",
+ "ulsan": "Ulsan",
+ "sejong": "Sejong",
+ "gyeonggi": "Gyeonggi",
+ "gangwon": "Gangwon",
+ "chungbuk": "Chungcheongbuk-do",
+ "chungnam": "Chungcheongnam-do",
+ "jeonbuk": "Jeollabuk-do",
+ "jeonnam": "Jeollanam-do",
+ "gyeongbuk": "Gyeongsangbuk-do",
+ "gyeongnam": "Gyeongsangnam-do",
+ "jeju": "Jeju"
+ }
}
}
}
diff --git a/src/shared/config/internationalization/locales/ko/field.json b/src/shared/config/internationalization/locales/ko/field.json
index dbe46ecb..8b4660f5 100644
--- a/src/shared/config/internationalization/locales/ko/field.json
+++ b/src/shared/config/internationalization/locales/ko/field.json
@@ -49,6 +49,27 @@
"business-registration-number": {
"label": "사업자 등록번호",
"placeholder": "10자리 입력 (‘-’ 제외)"
+ },
+ "location": {
+ "option": {
+ "seoul": "서울",
+ "busan": "부산",
+ "daegu": "대구",
+ "incheon": "인천",
+ "gwangju": "광주",
+ "daejeon": "대전",
+ "ulsan": "울산",
+ "sejong": "세종",
+ "gyeonggi": "경기",
+ "gangwon": "강원",
+ "chungbuk": "충북",
+ "chungnam": "충남",
+ "jeonbuk": "전북",
+ "jeonnam": "전남",
+ "gyeongbuk": "경북",
+ "gyeongnam": "경남",
+ "jeju": "제주"
+ }
}
}
}
diff --git a/src/shared/ui/modal/modal.tsx b/src/shared/ui/modal/modal.tsx
index 0da34eac..a1cd23f3 100644
--- a/src/shared/ui/modal/modal.tsx
+++ b/src/shared/ui/modal/modal.tsx
@@ -13,7 +13,6 @@ import { colors } from 'shared/config'
import { cn } from 'shared/lib'
import { Icon } from '../icon'
-
import { ModalContext } from './use-modal-context'
const ModalRoot = ({