From 63b05ac3af659357245e0b9583e4a0afdd877a32 Mon Sep 17 00:00:00 2001 From: mina-gwak Date: Sat, 12 Apr 2025 21:57:34 +0900 Subject: [PATCH 1/4] =?UTF-8?q?Feat:=20PD-248=20=EC=A3=BC=EC=86=8C=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20API=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 12 +++++ src/features/sign-up/lib/util.ts | 42 ++++++++++++++++ src/features/sign-up/ui/field/address.tsx | 59 ++++++++++++++++++++--- 4 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 src/features/sign-up/lib/util.ts 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/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..95f3e073 100644 --- a/src/features/sign-up/ui/field/address.tsx +++ b/src/features/sign-up/ui/field/address.tsx @@ -1,22 +1,67 @@ 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 { convertToLocationType } from '../../lib/util' export const Address = () => { const t = useTranslations() + const [isOpen, setIsOpen] = useState(false) + const [address, setAddress] = useState('') + const [detailedAddress, setDetailedAddress] = useState('') + + const form = useFormContext() + + const handleButtonClick = () => { + setIsOpen(true) + } + + const handleCompleteSearchingCode = (data: AddressType) => { + form.setValue('detailedAddress', data.address) + form.setValue('locationType', convertToLocationType(data.sido)) + + setAddress(data.address) + setDetailedAddress('') + setIsOpen(false) + } + + const handleAddressChange = (event: ChangeEvent) => { + form.setValue('detailedAddress', `${address} ${event.target.value}`) + setDetailedAddress(event.target.value) + } + return (
- - + + + + + + +
- - +
) } From 8a58b610ed01b630adca19ed42aadcd63b27307c Mon Sep 17 00:00:00 2001 From: mina-gwak Date: Sat, 12 Apr 2025 21:58:01 +0900 Subject: [PATCH 2/4] =?UTF-8?q?Fix:=20PD-248=20Location=20enum=20=EA=B0=92?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/job-post/config/location.ts | 34 +++++++++---------- .../job-post/ui/posting-title/index.tsx | 9 +++-- .../job-post-filter/ui/job-post-filters.tsx | 8 +++-- .../locales/en/field.json | 21 ++++++++++++ .../locales/ko/field.json | 21 ++++++++++++ src/shared/ui/modal/modal.tsx | 1 - 6 files changed, 72 insertions(+), 22 deletions(-) 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/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/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 = ({ From 8146cea41d0810fc69bb292f534ba01adba89542 Mon Sep 17 00:00:00 2001 From: mina-gwak Date: Sat, 12 Apr 2025 22:17:07 +0900 Subject: [PATCH 3/4] =?UTF-8?q?Refactor:=20PD-248=20=EA=B5=AC=EA=B8=80?= =?UTF-8?q?=EB=A7=B5=20API=20Provider=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/providers/google-map-provider.tsx | 10 +++++++ src/app/providers/index.tsx | 5 +++- .../job-post/ui/academy-address/index.tsx | 26 ++++++++----------- 3 files changed, 25 insertions(+), 16 deletions(-) create mode 100644 src/app/providers/google-map-provider.tsx 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/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 ?? '-'}
    From d0f1e53016f0afe0246b64f2fab3a46f1383d5e7 Mon Sep 17 00:00:00 2001 From: mina-gwak Date: Sat, 12 Apr 2025 22:18:53 +0900 Subject: [PATCH 4/4] =?UTF-8?q?Feat:=20PD-248=20=EC=A3=BC=EC=86=8C=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=ED=9B=84=20=EC=A2=8C=ED=91=9C=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=ED=95=B4=20=ED=8F=BC=EC=97=90=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/sign-up/lib/use-geocoding.ts | 33 +++++++++++++++++++++++ src/features/sign-up/ui/field/address.tsx | 13 +++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/features/sign-up/lib/use-geocoding.ts 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/ui/field/address.tsx b/src/features/sign-up/ui/field/address.tsx index 95f3e073..35221201 100644 --- a/src/features/sign-up/ui/field/address.tsx +++ b/src/features/sign-up/ui/field/address.tsx @@ -6,11 +6,14 @@ import { useFormContext } from 'react-hook-form' import { fieldCss } from 'shared/form' 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('') @@ -22,11 +25,17 @@ export const Address = () => { } const handleCompleteSearchingCode = (data: AddressType) => { + setAddress(data.address) + setDetailedAddress('') + form.setValue('detailedAddress', data.address) form.setValue('locationType', convertToLocationType(data.sido)) - setAddress(data.address) - setDetailedAddress('') + geocode(data.address, ({ lat, lng }) => { + form.setValue('lat', lat) + form.setValue('lng', lng) + }) + setIsOpen(false) }