Skip to content

Commit c4500e1

Browse files
authored
Merge pull request #96 from prgrms-web-devcourse-final-project/50-feature/add-dog-detail-profile
[Feature] #50 반려견 등록 페이지 기능 완성
2 parents fb3bea7 + 45db168 commit c4500e1

File tree

22 files changed

+487
-88
lines changed

22 files changed

+487
-88
lines changed

src/components/GenderSelectButton/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ interface GenderSelectButtonProps {
1111

1212
export default function GenderSelectButton({ gender, isActive, onClick }: GenderSelectButtonProps) {
1313
return (
14-
<S.GenderBtn isActive={isActive} onClick={onClick}>
15-
<S.GenderIcon isActive={isActive} src={gender === 'male' ? Male : Female} alt='성별' />
14+
<S.GenderBtn $isActive={isActive} onClick={onClick}>
15+
<S.GenderIcon $isActive={isActive} src={gender === 'male' ? Male : Female} alt='성별' />
1616
<Typo17 weight={isActive ? '700' : '400'}>{gender === 'male' ? '남' : '여'}</Typo17>
1717
</S.GenderBtn>
1818
)
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { styled } from 'styled-components'
2-
export const GenderBtn = styled.button<{ isActive: boolean }>`
3-
border: solid 2px ${({ isActive, theme }) => (isActive ? theme.colors.brand.darken : theme.colors.grayscale.gc_1)};
2+
export const GenderBtn = styled.button<{ $isActive: boolean }>`
3+
border: solid 2px ${({ $isActive, theme }) => ($isActive ? theme.colors.brand.darken : theme.colors.grayscale.gc_1)};
44
border-radius: 8px;
55
width: auto;
66
height: 102px;
7-
color: ${({ isActive, theme }) => (isActive ? theme.colors.brand.darken : theme.colors.grayscale.font_3)};
7+
color: ${({ $isActive, theme }) => ($isActive ? theme.colors.brand.darken : theme.colors.grayscale.font_3)};
88
99
display: flex;
1010
flex-direction: column;
@@ -13,9 +13,9 @@ export const GenderBtn = styled.button<{ isActive: boolean }>`
1313
gap: 0.2rem;
1414
`
1515

16-
export const GenderIcon = styled.img<{ isActive: boolean }>`
16+
export const GenderIcon = styled.img<{ $isActive: boolean }>`
1717
filter: ${props =>
18-
props.isActive
18+
props.$isActive
1919
? 'invert(12%) sepia(70%) saturate(924%) hue-rotate(351deg) brightness(96%) contrast(97%)'
2020
: 'invert(31%) sepia(4%) saturate(61%) hue-rotate(332deg) brightness(99%) contrast(97%)'};
2121
`

src/components/Toast/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// components/Toast/index.tsx
21
import * as S from './styles'
32
import { useEffect } from 'react'
43
import { useToastStore } from '~/stores/toastStore'
@@ -16,7 +15,7 @@ export default function Toast() {
1615
}, [isVisible])
1716

1817
return (
19-
<S.ToastWrapper isVisible={isVisible}>
18+
<S.ToastWrapper $isVisible={isVisible}>
2019
<S.Toast>{content}</S.Toast>
2120
</S.ToastWrapper>
2221
)

src/components/Toast/styles.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { styled } from 'styled-components'
22

3-
export const ToastWrapper = styled.div<{ isVisible: boolean }>`
3+
export const ToastWrapper = styled.div<{ $isVisible: boolean }>`
44
position: absolute;
55
top: -50px;
66
77
width: 100%;
88
display: flex;
99
justify-content: center;
1010
11-
visibility: ${({ isVisible }) => (isVisible ? 'visiblie' : 'hidden')};
12-
opacity: ${({ isVisible }) => (isVisible ? 1 : 0)};
11+
visibility: ${({ $isVisible }) => ($isVisible ? 'visiblie' : 'hidden')};
12+
opacity: ${({ $isVisible }) => ($isVisible ? 1 : 0)};
1313
transition:
1414
opacity 0.3s ease,
1515
visibility 0.3s ease;

src/data/breeds.json

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
{
2+
"breeds": [
3+
"래브라도 리트리버",
4+
"골든 리트리버",
5+
"저먼 셰퍼드",
6+
"도베르만",
7+
"그레이트 데인",
8+
"버니즈 마운틴 독",
9+
"뉴펀들랜드",
10+
"로트와일러",
11+
"세인트 버나드",
12+
"아이리시 울프하운드",
13+
"잉글리시 마스티프",
14+
"그레이트 피레니즈",
15+
"시베리안 허스키",
16+
"알래스칸 맬러뮤트",
17+
"보더 콜리",
18+
"비글",
19+
"불독",
20+
"차우차우",
21+
"달마시안",
22+
"사모예드",
23+
"시바견",
24+
"웰시코기",
25+
"진도견",
26+
"아키타",
27+
"바셋하운드",
28+
"브리타니",
29+
"콜리",
30+
"잉글리시 세터",
31+
"잉글리시 스프링거 스패니얼",
32+
"벨기에 말리노이즈",
33+
"포인터",
34+
"에어데일 테리어",
35+
"휘펫",
36+
"불 테리어",
37+
"스탠더드 푸들",
38+
"아메리칸 에스키모",
39+
"보더 테리어",
40+
"웨스트하이랜드 화이트 테리어",
41+
"프렌치 불독",
42+
"포메라니안",
43+
"치와와",
44+
"요크셔테리어",
45+
"미니어처 슈나우저",
46+
"미니어처 푸들",
47+
"토이 푸들",
48+
"말티즈",
49+
"시츄",
50+
"닥스훈트",
51+
"비숑 프리제",
52+
"파피용",
53+
"퍼그",
54+
"페키니즈",
55+
"잭 러셀 테리어",
56+
"미니어처 핀셔",
57+
"캐벌리어 킹 찰스 스패니얼",
58+
"보스턴 테리어",
59+
"이탈리안 그레이하운드",
60+
"스코티시 테리어",
61+
"실키 테리어",
62+
"케언 테리어",
63+
"노리치 테리어",
64+
"아프간 하운드",
65+
"살루키",
66+
"바센지",
67+
"차이니즈 샤페이",
68+
"아메리칸 코카 스패니얼",
69+
"잉글리시 코카 스패니얼",
70+
"클럼버 스패니얼",
71+
"필드 스패니얼",
72+
"저먼 와이어헤어드 포인터",
73+
"체서피크 베이 리트리버",
74+
"컬리코티드 리트리버",
75+
"플랫코티드 리트리버",
76+
"아이리시 세터",
77+
"고든 세터",
78+
"올드 잉글리시 시프도그",
79+
"셔틀랜드 시프도그",
80+
"벨기안 시프도그",
81+
"오스트레일리안 캐틀 독",
82+
"핀란드 스피츠",
83+
"케이스혼드",
84+
"티베탄 마스티프",
85+
"불마스티프",
86+
"네아폴리탄 마스티프",
87+
"블러드하운드",
88+
"그레이하운드",
89+
"노르웨이언 엘크하운드",
90+
"아이리시 워터 스패니얼",
91+
"웨일즈 스프링거 스패니얼",
92+
"스탠더드 슈나우저",
93+
"자이언트 슈나우저",
94+
"스코티시 디어하운드",
95+
"맨체스터 테리어",
96+
"노퍽 테리어",
97+
"래빗 닥스훈트",
98+
"롱헤어드 닥스훈트",
99+
"아메리칸 불리",
100+
"버니두들",
101+
"골든두들",
102+
"래브라두들",
103+
"포르투갈 워터 독",
104+
"오스트레일리안 셰퍼드",
105+
"벨기에 터뷰런",
106+
"블랙 러시안 테리어",
107+
"불 마스티프",
108+
"체서피크 베이 리트리버",
109+
"샤페이",
110+
"클럼버 스패니얼",
111+
"컬리 코티드 리트리버",
112+
"댄디 딘몬트 테리어",
113+
"잉글리시 폭스하운드",
114+
"필드 스패니얼",
115+
"핀란드 라프훈드",
116+
"자이언트 슈나우저",
117+
"아이비전 하운드"
118+
]
119+
}

src/modals/DatePickerModal/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export default function DatePickerModal({ date, setDate }: DatePickerModalProps)
4040

4141
return (
4242
<S.ModalOverlay onClick={close}>
43-
<S.DatePickerModal isExiting={isExiting} onClick={handleModalClick}>
43+
<S.DatePickerModal $isExiting={isExiting} onClick={handleModalClick}>
4444
<S.ConfirmBtn onClick={handleConfirmBtn}>확인</S.ConfirmBtn>
4545
<S.Divider />
4646
<DatePicker

src/modals/DatePickerModal/styles.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const slideDown = keyframes`
1919
`
2020

2121
export const ModalOverlay = styled.div`
22+
background-color: rgba(0, 0, 0, 0.4);
23+
z-index: 200;
2224
position: fixed;
2325
top: 0;
2426
left: 0;
@@ -28,11 +30,11 @@ export const ModalOverlay = styled.div`
2830
align-items: flex-end;
2931
`
3032

31-
export const DatePickerModal = styled.div<{ isExiting: boolean }>`
33+
export const DatePickerModal = styled.div<{ $isExiting: boolean }>`
3234
background-color: white;
3335
width: 100%;
3436
35-
animation: ${({ isExiting }) => (isExiting ? slideDown : slideUp)} 0.3s ease-out;
37+
animation: ${({ $isExiting }) => ($isExiting ? slideDown : slideUp)} 0.3s ease-out;
3638
3739
> div {
3840
padding: 1rem;

src/modals/RegisterDogModal/CheckDogProfileSection/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import Header from '~components/Header/index'
44
import { Typo24 } from '~components/Typo/index'
55
import { Profile } from '~components/Profile'
66
import Tag from '~components/Tag'
7+
import { useModalStore } from '~stores/modalStore'
78

89
export default function CheckDogProfileSection() {
9-
const handleClickPrev = () => {}
10+
const { popModal } = useModalStore()
1011

1112
return (
1213
<>
13-
<Header type='sm' onClickPrev={handleClickPrev} prevBtn={true} />
1414
<S.CheckDogProfileSection>
15+
<Header type='sm' onClickPrev={popModal} prevBtn={true} />
1516
<S.ProfileArea>
1617
<S.TypoWrapper>
1718
<Typo24 weight='700'>

src/modals/RegisterDogModal/CheckDogProfileSection/styles.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { styled } from 'styled-components'
22

33
export const CheckDogProfileSection = styled.div`
4+
z-index: 200;
45
padding: 180px 20px 24px 20px;
56
background-color: ${({ theme }) => theme.colors.grayscale.gc_4};
6-
height: 100dvh;
7+
position: absolute;
8+
top: 0;
9+
left: 0;
10+
right: 0;
11+
bottom: 0;
712
813
display: flex;
914
flex-direction: column;

src/modals/RegisterDogModal/DogProfileDetailSection/index.tsx

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,53 +5,66 @@ import GenderSelectButton from '~components/GenderSelectButton'
55
import { Typo24 } from '~components/Typo/index'
66
import Check from '~assets/check.svg'
77
import Header from '~components/Header/index'
8+
import SearchModal from '~modals/SearchModal'
9+
import { useModalStore } from '~stores/modalStore'
10+
import { useDogProfileStore } from '~/stores/dogProfileStore'
11+
import { validateDogDetailProfile } from '~utils/validateDogProfile'
12+
import { useToastStore } from '~stores/toastStore'
13+
import Toast from '~components/Toast'
814

915
export default function DogProfileDetailSection() {
10-
const [isNeutered, setIsNeutered] = useState(false)
11-
const [selectedGender, setSelectedGender] = useState<'male' | 'female' | null>(null)
12-
const [weight, setWeight] = useState('')
16+
const { dogProfile, setDogProfile } = useDogProfileStore()
17+
const { pushModal, popModal } = useModalStore()
18+
const { showToast } = useToastStore()
1319

1420
const [displayValue, setDisplayValue] = useState('')
1521
const [inputType, setInputType] = useState('text')
1622

1723
const handleGenderSelect = (gender: 'male' | 'female') => {
18-
setSelectedGender(gender)
24+
setDogProfile({ gender })
1925
}
2026

2127
const onChangeWeightInput = (e: React.ChangeEvent<HTMLInputElement>) => {
2228
const value = e.target.value
2329
if (value === '') {
24-
setWeight('')
30+
setDogProfile({ weight: '' })
2531
setDisplayValue('')
2632
return
2733
}
2834

2935
if (/^\d*\.?\d*$/.test(value)) {
3036
const formatted = value.includes('.') ? value.match(/^\d*\.?\d{0,2}/)![0] : value
3137

32-
setWeight(formatted)
38+
setDogProfile({ weight: formatted })
3339
setDisplayValue(inputType === 'number' ? formatted : `${formatted}kg`)
3440
}
3541
}
3642

3743
const handleFocus = () => {
3844
setInputType('number')
39-
setDisplayValue(weight)
45+
setDisplayValue(dogProfile.weight)
4046
}
4147

4248
const handleBlur = () => {
4349
setInputType('text')
44-
if (weight) {
45-
setDisplayValue(`${weight}kg`)
50+
if (dogProfile.weight) {
51+
setDisplayValue(`${dogProfile.weight}kg`)
4652
}
4753
}
4854

49-
const handleClickPrev = () => {}
55+
const handleComfirmClick = () => {
56+
const alertMessage = validateDogDetailProfile(dogProfile)
57+
if (alertMessage) {
58+
showToast(alertMessage)
59+
return
60+
}
61+
console.log('이제 백엔드로 전송')
62+
}
5063

5164
return (
5265
<>
53-
<Header type='sm' onClickPrev={handleClickPrev} prevBtn />
5466
<S.DogProfileDetailSection>
67+
<Header type='sm' onClickPrev={popModal} prevBtn />
5568
<S.TypoWrapper>
5669
<Typo24 weight='700'>
5770
반려견 상세 정보를
@@ -62,34 +75,45 @@ export default function DogProfileDetailSection() {
6275
<S.GenderSelectBtnWrapper>
6376
<GenderSelectButton
6477
gender='male'
65-
isActive={selectedGender === 'male'}
78+
isActive={dogProfile.gender === 'male'}
6679
onClick={() => handleGenderSelect('male')}
6780
/>
6881
<GenderSelectButton
6982
gender='female'
70-
isActive={selectedGender === 'female'}
83+
isActive={dogProfile.gender === 'female'}
7184
onClick={() => handleGenderSelect('female')}
7285
/>
7386
</S.GenderSelectBtnWrapper>
74-
<S.CheckboxWrapper onClick={() => setIsNeutered(!isNeutered)}>
75-
<S.CheckboxCircle isChecked={isNeutered}>
76-
{isNeutered && <img src={Check} alt='check'></img>}
87+
<S.CheckboxWrapper onClick={() => setDogProfile({ isNeutered: !dogProfile.isNeutered })}>
88+
<S.CheckboxCircle $isChecked={dogProfile.isNeutered}>
89+
{dogProfile.isNeutered && <img src={Check} alt='check'></img>}
7790
</S.CheckboxCircle>
78-
<S.CheckboxLabel isChecked={isNeutered}>중성화 했어요</S.CheckboxLabel>
91+
<S.CheckboxLabel $isChecked={dogProfile.isNeutered}>중성화 했어요</S.CheckboxLabel>
7992
</S.CheckboxWrapper>
8093
</S.GenderBtnArea>
8194
<S.InputArea>
82-
<S.PickerBtn>견종 입력</S.PickerBtn>
95+
<S.PickerBtn onClick={() => pushModal(<SearchModal />)} $hasBreed={!!dogProfile.breed}>
96+
{dogProfile.breed || '견종 입력'}
97+
</S.PickerBtn>
8398
<S.WeightInput
84-
placeholder='몸무게 입력'
99+
placeholder='몸무게 입력 (kg)'
85100
type={inputType}
86101
value={displayValue}
87102
onChange={onChangeWeightInput}
88103
onFocus={handleFocus}
89104
onBlur={handleBlur}
105+
$hasWeight={!!dogProfile.weight}
90106
/>
91107
</S.InputArea>
92-
<ActionButton>확인</ActionButton>
108+
<S.ToastWrapper>
109+
<ActionButton
110+
$bgColor={validateDogDetailProfile(dogProfile) ? 'gc_1' : 'default'}
111+
onClick={handleComfirmClick}
112+
>
113+
확인
114+
</ActionButton>
115+
<Toast />
116+
</S.ToastWrapper>
93117
</S.DogProfileDetailSection>
94118
</>
95119
)

0 commit comments

Comments
 (0)