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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/app/providers/google-map-provider.tsx
Original file line number Diff line number Diff line change
@@ -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 <APIProvider apiKey={apiKey}>{children}</APIProvider>
}
5 changes: 4 additions & 1 deletion src/app/providers/index.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -15,7 +16,9 @@ export const AppProviders = ({ basePath, children }: Props) => {
<SessionProvider basePath={basePath}>
<MSWProvider>
<ToastProvider>
<QueryProvider>{children}</QueryProvider>
<QueryProvider>
<GoogleMapProvider>{children}</GoogleMapProvider>
</QueryProvider>
</ToastProvider>
</MSWProvider>
</SessionProvider>
Expand Down
34 changes: 17 additions & 17 deletions src/entities/job-post/config/location.ts
Original file line number Diff line number Diff line change
@@ -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',
}
26 changes: 11 additions & 15 deletions src/entities/job-post/ui/academy-address/index.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => {
Expand Down Expand Up @@ -29,22 +29,18 @@ type Props = {
}

const AcademyAddress = ({ address, lat, lng }: Props) => {
const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || ''

return (
<div className="overflow-hidden rounded-[10px] border border-gray-300">
<APIProvider apiKey={apiKey}>
<Map
mapId="academy-address-map"
defaultZoom={15}
defaultCenter={{ lat, lng }}
className="h-[232px] w-full"
>
<AdvancedMarker key={address} position={{ lat, lng }}>
<Pin />
</AdvancedMarker>
</Map>
</APIProvider>
<Map
mapId="academy-address-map"
defaultZoom={15}
defaultCenter={{ lat, lng }}
className="h-[232px] w-full"
>
<AdvancedMarker key={address} position={{ lat, lng }}>
<Pin />
</AdvancedMarker>
</Map>
<div className="title-small h-12 w-full bg-white p-3 font-normal text-gray-900">
{address ?? '-'}
</div>
Expand Down
9 changes: 7 additions & 2 deletions src/entities/job-post/ui/posting-title/index.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -15,6 +16,8 @@ type Props = {
}

export const PostingTitle = ({ jobPost, size }: Props) => {
const t = useTranslations()

const studentType = convertStudentType({
forKindergarten: jobPost.forKindergarten,
forElementary: jobPost.forElementary,
Expand All @@ -32,7 +35,9 @@ export const PostingTitle = ({ jobPost, size }: Props) => {
<ul className="flex flex-col gap-1">
<li className={cn(css.description({ size }))}>
<Icon name="LocationFilled" color={colors.gray[500]} size={size} />
<span>{capitalize(jobPost.locationType ?? '-')}</span>
<span>
{t(`field.location.option.${lowerCase(jobPost.locationType)}`)}
</span>
</li>
<li className={cn(css.description({ size }))}>
<Icon name="User" color={colors.gray[500]} size={size} />
Expand Down
8 changes: 6 additions & 2 deletions src/features/job-post-filter/ui/job-post-filters.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -19,6 +21,8 @@ export const JobPostFilters = ({
useSearchField = false,
onChange,
}: Props) => {
const t = useTranslations()

const {
filters,
isFilterExist,
Expand Down Expand Up @@ -89,7 +93,7 @@ export const JobPostFilters = ({
>
{Object.keys(Location).map(key => (
<Filter.Item key={key} value={key}>
{Location[key as keyof typeof Location]}
{t(`field.location.option.${lowerCase(key)}`)}
</Filter.Item>
))}
</Filter>
Expand All @@ -116,7 +120,7 @@ export const JobPostFilters = ({
{filters.locations.map(location => (
<Chip key={location} selected>
<Chip.Label>
{Location[location as keyof typeof Location]}
{t(`field.location.option.${lowerCase(location as string)}`)}
</Chip.Label>
<Chip.RemoveButton
onClick={() => handleLocationFilterRemove(location)}
Expand Down
33 changes: 33 additions & 0 deletions src/features/sign-up/lib/use-geocoding.ts
Original file line number Diff line number Diff line change
@@ -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<google.maps.Geocoder | null>(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 }
}
42 changes: 42 additions & 0 deletions src/features/sign-up/lib/util.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
68 changes: 61 additions & 7 deletions src/features/sign-up/ui/field/address.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) => {
form.setValue('detailedAddress', `${address} ${event.target.value}`)
setDetailedAddress(event.target.value)
}

return (
<div className={fieldCss.fieldWrapper()}>
<Label required>{t('field.address.label')}</Label>
<div className="flex gap-2">
<TextField readOnly className="bg-gray-100" />
<Button variant="lined" size="large" className="w-[120px] shrink-0">
주소 검색
</Button>
<TextField value={address} readOnly className="bg-gray-100" />
<Modal open={isOpen} onOpenChange={setIsOpen}>
<Button
variant="lined"
size="large"
className="w-[120px] shrink-0"
onClick={handleButtonClick}
>
주소 검색
</Button>
<Modal.Content className="h-[600px] w-[450px]">
<DaumPostcode
style={{ height: '500px' }}
onComplete={handleCompleteSearchingCode}
/>
</Modal.Content>
</Modal>
</div>
<TextField readOnly className="bg-gray-100" />
<TextField placeholder={t('field.address.placeholder')} />
<TextField
value={detailedAddress}
placeholder={t('field.address.placeholder')}
onChange={handleAddressChange}
/>
</div>
)
}
21 changes: 21 additions & 0 deletions src/shared/config/internationalization/locales/en/field.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}
Loading
Loading