[#44] 매장 기능 도입 #45

Merged
pricelees merged 116 commits from feat/#44 into main 2025-09-20 03:15:06 +00:00
4 changed files with 203 additions and 7 deletions
Showing only changes of commit a64371b3d2 - Show all commits

View File

@ -0,0 +1,18 @@
import apiClient from "@_api/apiClient";
import type { DongListResponse, RegionCodeResponse, SidoListResponse, SigunguListResponse } from "./regionTypes";
export const fetchSidoList = async (): Promise<SidoListResponse> => {
return await apiClient.get(`/regions/sido`);
};
export const fetchSigunguList = async (sidoCode: string): Promise<SigunguListResponse> => {
return await apiClient.get(`/regions/sigungu?sidoCode=${sidoCode}`);
}
export const fetchDongList = async (sidoCode: string, sigunguCode: string): Promise<DongListResponse> => {
return await apiClient.get(`/regions/dong?sidoCode=${sidoCode}&sigunguCode=${sigunguCode}`);
}
export const fetchRegionCode = async (sidoCode: string, sigunguCode: string, dongCode: string): Promise<RegionCodeResponse> => {
return await apiClient.get(`/regions/code?sidoCode=${sidoCode}&sigunguCode=${sigunguCode}&dongCode=${dongCode}`);
}

View File

@ -0,0 +1,30 @@
export interface SidoResponse {
code: string,
name: string,
}
export interface SidoListResponse {
sidoList: SidoResponse[]
}
export interface SigunguResponse {
code: string,
name: string,
}
export interface SigunguListResponse {
sigunguList: SigunguResponse[]
}
export interface DongResponse {
code: string,
name: string,
}
export interface DongListResponse {
dongList: DongResponse[]
}
export interface RegionCodeResponse {
code: string
}

View File

@ -69,3 +69,12 @@
font-size: 12px; font-size: 12px;
margin-top: 4px; margin-top: 4px;
} }
.region-select-group {
display: flex;
gap: 10px;
}
.region-select-group select {
flex: 1;
}

View File

@ -1,8 +1,19 @@
import {signup} from '@_api/user/userAPI'; import {
import type {UserCreateRequest, UserCreateResponse} from '@_api/user/userTypes'; fetchDongList,
fetchRegionCode,
fetchSidoList,
fetchSigunguList,
} from '@_api/region/regionAPI';
import type {
DongResponse,
SidoResponse,
SigunguResponse,
} from '@_api/region/regionTypes';
import { signup } from '@_api/user/userAPI';
import type { UserCreateRequest, UserCreateResponse } from '@_api/user/userTypes';
import '@_css/signup-page-v2.css'; import '@_css/signup-page-v2.css';
import React, {useEffect, useState} from 'react'; import React, { useEffect, useState } from 'react';
import {useNavigate} from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
const MIN_PASSWORD_LENGTH = 8; const MIN_PASSWORD_LENGTH = 8;
@ -14,8 +25,67 @@ const SignupPage: React.FC = () => {
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
const [hasSubmitted, setHasSubmitted] = useState(false); const [hasSubmitted, setHasSubmitted] = useState(false);
const [sidoList, setSidoList] = useState<SidoResponse[]>([]);
const [sigunguList, setSigunguList] = useState<SigunguResponse[]>([]);
const [dongList, setDongList] = useState<DongResponse[]>([]);
const [selectedSidoCode, setSelectedSidoCode] = useState('');
const [selectedSigunguCode, setSelectedSigunguCode] = useState('');
const [selectedDongCode, setSelectedDongCode] = useState('');
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => {
const fetchSido = async () => {
try {
const response = await fetchSidoList();
setSidoList(response.sidoList);
} catch (error) {
console.error('시/도 목록을 불러오는 데 실패했습니다.', error);
}
};
fetchSido();
}, []);
useEffect(() => {
if (selectedSidoCode) {
const fetchSigungu = async () => {
try {
const response = await fetchSigunguList(selectedSidoCode);
setSigunguList(response.sigunguList);
setDongList([]);
setSelectedSigunguCode('');
setSelectedDongCode('');
} catch (error) {
console.error('시/군/구 목록을 불러오는 데 실패했습니다.', error);
}
};
fetchSigungu();
} else {
setSigunguList([]);
setDongList([]);
setSelectedSigunguCode('');
setSelectedDongCode('');
}
}, [selectedSidoCode]);
useEffect(() => {
if (selectedSidoCode && selectedSigunguCode) {
const fetchDong = async () => {
try {
const response = await fetchDongList(selectedSidoCode, selectedSigunguCode);
setDongList(response.dongList);
setSelectedDongCode('');
} catch (error) {
console.error('읍/면/동 목록을 불러오는 데 실패했습니다.', error);
}
};
fetchDong();
} else {
setDongList([]);
setSelectedDongCode('');
}
}, [selectedSigunguCode]);
const validate = () => { const validate = () => {
const newErrors: Record<string, string> = {}; const newErrors: Record<string, string> = {};
@ -36,6 +106,12 @@ const SignupPage: React.FC = () => {
newErrors.phone = '올바른 휴대폰 번호 형식이 아닙니다. (예: 01012345678)'; newErrors.phone = '올바른 휴대폰 번호 형식이 아닙니다. (예: 01012345678)';
} }
if (selectedSidoCode || selectedSigunguCode || selectedDongCode) {
if (!selectedSidoCode || !selectedSigunguCode || !selectedDongCode) {
newErrors.region = '모든 지역 정보를 선택해주세요.';
}
}
return newErrors; return newErrors;
}; };
@ -44,7 +120,7 @@ const SignupPage: React.FC = () => {
if (hasSubmitted) { if (hasSubmitted) {
setErrors(validate()); setErrors(validate());
} }
}, [email, password, name, phone, hasSubmitted]); }, [email, password, name, phone, hasSubmitted, selectedSidoCode, selectedSigunguCode, selectedDongCode]);
const handleSignup = async (e: React.FormEvent) => { const handleSignup = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@ -55,7 +131,23 @@ const SignupPage: React.FC = () => {
if (Object.keys(newErrors).length > 0) return; if (Object.keys(newErrors).length > 0) return;
const request: UserCreateRequest = { email, password, name, phone, regionCode: null }; let regionCode: string | null = null;
if (selectedSidoCode && selectedSigunguCode && selectedDongCode) {
try {
const response = await fetchRegionCode(
selectedSidoCode,
selectedSigunguCode,
selectedDongCode
);
regionCode = response.code;
} catch (error) {
alert('지역 코드를 가져오는 데 실패했습니다.');
console.error(error);
return;
}
}
const request: UserCreateRequest = { email, password, name, phone, regionCode };
try { try {
const response: UserCreateResponse = await signup(request); const response: UserCreateResponse = await signup(request);
alert(`${response.name}님, 회원가입을 축하드려요. 로그인 후 이용해주세요!`); alert(`${response.name}님, 회원가입을 축하드려요. 로그인 후 이용해주세요!`);
@ -133,6 +225,53 @@ const SignupPage: React.FC = () => {
)} )}
</div> </div>
<div className="form-group">
<label className="form-label"> ()</label>
<div className="region-select-group">
<select
className="form-input"
value={selectedSidoCode}
onChange={e => setSelectedSidoCode(e.target.value)}
>
<option value="">/</option>
{sidoList.map(s => (
<option key={s.code} value={s.code}>
{s.name}
</option>
))}
</select>
<select
className="form-input"
value={selectedSigunguCode}
onChange={e => setSelectedSigunguCode(e.target.value)}
disabled={!selectedSidoCode}
>
<option value="">//</option>
{sigunguList.map(s => (
<option key={s.code} value={s.code}>
{s.name}
</option>
))}
</select>
<select
className="form-input"
value={selectedDongCode}
onChange={e => setSelectedDongCode(e.target.value)}
disabled={!selectedSigunguCode}
>
<option value="">//</option>
{dongList.map(d => (
<option key={d.code} value={d.code}>
{d.name}
</option>
))}
</select>
</div>
{hasSubmitted && errors.region && (
<p className="error-text">{errors.region}</p>
)}
</div>
<button <button
type="submit" type="submit"
className="btn-primary" className="btn-primary"