feat: 강아지 등록 개선(크기 토글·성별·연령) + AI 추천 조건
- 등록 폼: 크기 셀렉트→토글, 성별(남아/여아)·연령(출생 연·월) 추가 - 프로필/등록에 'AI 추천 조건'(선호 성별/연령/중성화) 섹션 - HomePage AI 섹션의 중성화 토글 제거 → 저장된 조건을 백엔드가 사용 - 체크인 완료 알림 문구에서 스팟 채팅 문구 제거 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,9 @@ export interface Dog {
|
|||||||
gender: string | null
|
gender: string | null
|
||||||
neutered: boolean
|
neutered: boolean
|
||||||
imageUrl: string | null
|
imageUrl: string | null
|
||||||
|
prefGender: string
|
||||||
|
prefAge: string
|
||||||
|
prefNeutered: string
|
||||||
traits: TraitTag[]
|
traits: TraitTag[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +30,9 @@ export interface DogSaveRequest {
|
|||||||
gender?: string | null
|
gender?: string | null
|
||||||
neutered?: boolean
|
neutered?: boolean
|
||||||
imageUrl?: string | null
|
imageUrl?: string | null
|
||||||
|
prefGender?: string
|
||||||
|
prefAge?: string
|
||||||
|
prefNeutered?: string
|
||||||
traitIds?: number[]
|
traitIds?: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-5
@@ -40,11 +40,8 @@ export const spotsApi = {
|
|||||||
list: () => http.get<Spot[]>('/api/spots'),
|
list: () => http.get<Spot[]>('/api/spots'),
|
||||||
mates: (spotId: number, userId?: number) =>
|
mates: (spotId: number, userId?: number) =>
|
||||||
http.get<Mate[]>(`/api/spots/${spotId}/mates${userId ? `?userId=${userId}` : ''}`),
|
http.get<Mate[]>(`/api/spots/${spotId}/mates${userId ? `?userId=${userId}` : ''}`),
|
||||||
aiMates: (spotId: number, dogId: number, neutered?: boolean) =>
|
aiMates: (spotId: number, dogId: number) =>
|
||||||
http.get<AiMate[]>(
|
http.get<AiMate[]>(`/api/spots/${spotId}/ai-mates?dogId=${dogId}`),
|
||||||
`/api/spots/${spotId}/ai-mates?dogId=${dogId}` +
|
|
||||||
(neutered != null ? `&neutered=${neutered}` : ''),
|
|
||||||
),
|
|
||||||
frequentBreeds: (spotId: number) =>
|
frequentBreeds: (spotId: number) =>
|
||||||
http.get<{ breeds: string[] }>(`/api/spots/${spotId}/frequent-breeds`),
|
http.get<{ breeds: string[] }>(`/api/spots/${spotId}/frequent-breeds`),
|
||||||
reviews: (spotId: number) => http.get<Review[]>(`/api/spots/${spotId}/reviews`),
|
reviews: (spotId: number) => http.get<Review[]>(`/api/spots/${spotId}/reviews`),
|
||||||
|
|||||||
+6
-24
@@ -40,25 +40,8 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="text-caption text-grey-5 q-ml-xs">· AI 분석으로 실제와 다를 수 있어요</span>
|
<span class="text-caption text-grey-5 q-ml-xs">· AI 분석으로 실제와 다를 수 있어요</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 찾기 옵션: 중성화 조건 -->
|
<div class="text-caption text-grey-6 q-mb-sm">
|
||||||
<div class="row items-center q-mb-sm">
|
추천 조건은 <span class="text-primary">우리 강아지 > AI 추천 조건</span>에서 설정해요
|
||||||
<span class="text-caption text-grey-7 q-mr-sm">중성화</span>
|
|
||||||
<q-btn-toggle
|
|
||||||
v-model="aiNeutered"
|
|
||||||
dense
|
|
||||||
unelevated
|
|
||||||
no-caps
|
|
||||||
size="sm"
|
|
||||||
toggle-color="primary"
|
|
||||||
color="grey-3"
|
|
||||||
text-color="grey-8"
|
|
||||||
:options="[
|
|
||||||
{ label: '상관없음', value: 'any' },
|
|
||||||
{ label: '완료', value: 'yes' },
|
|
||||||
{ label: '안 함', value: 'no' },
|
|
||||||
]"
|
|
||||||
@update:model-value="loadAiMates"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="aiLoading" class="row items-center q-gutter-sm text-grey-6">
|
<div v-if="aiLoading" class="row items-center q-gutter-sm text-grey-6">
|
||||||
<q-spinner-dots size="24px" color="primary" />
|
<q-spinner-dots size="24px" color="primary" />
|
||||||
@@ -186,7 +169,6 @@ const loadError = ref(false)
|
|||||||
const aiMates = ref<AiMate[]>([])
|
const aiMates = ref<AiMate[]>([])
|
||||||
const aiLoading = ref(false)
|
const aiLoading = ref(false)
|
||||||
const aiTried = ref(false)
|
const aiTried = ref(false)
|
||||||
const aiNeutered = ref<'any' | 'yes' | 'no'>('any') // AI 찾기 옵션: 중성화 조건
|
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
|
|
||||||
@@ -238,7 +220,7 @@ async function onCheckIn() {
|
|||||||
await spotsApi.checkIn(currentSpot.value.id, currentUserId(), dogs.currentDogId)
|
await spotsApi.checkIn(currentSpot.value.id, currentUserId(), dogs.currentDogId)
|
||||||
checkedIn.value = true
|
checkedIn.value = true
|
||||||
await refreshSpot(currentSpot.value.id)
|
await refreshSpot(currentSpot.value.id)
|
||||||
$q.notify({ color: 'primary', message: '체크인 완료! 스팟 채팅이 열렸어요.', icon: 'place', position: 'top' })
|
$q.notify({ color: 'primary', message: '체크인 완료!', icon: 'place', position: 'top' })
|
||||||
loadAiMates() // 체크인 후 AI 궁합 추천 (비동기)
|
loadAiMates() // 체크인 후 AI 궁합 추천 (비동기)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
notifyError(e)
|
notifyError(e)
|
||||||
@@ -247,14 +229,14 @@ async function onCheckIn() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 체크인 후 AI 궁합 추천 로드 — 이 스팟의 강아지 중 내 개와 사이좋게 지낼 만한 목록. */
|
/** 체크인 후 AI 궁합 추천 로드 — 이 스팟의 강아지 중 내 개와 사이좋게 지낼 만한 목록.
|
||||||
|
* 추천 조건(성별/나이/중성화)은 내 강아지 프로필에 저장된 값을 백엔드가 사용한다. */
|
||||||
async function loadAiMates() {
|
async function loadAiMates() {
|
||||||
if (!currentSpot.value || dogs.currentDogId == null) return
|
if (!currentSpot.value || dogs.currentDogId == null) return
|
||||||
aiLoading.value = true
|
aiLoading.value = true
|
||||||
aiTried.value = true
|
aiTried.value = true
|
||||||
const neutered = aiNeutered.value === 'yes' ? true : aiNeutered.value === 'no' ? false : undefined
|
|
||||||
try {
|
try {
|
||||||
aiMates.value = await spotsApi.aiMates(currentSpot.value.id, dogs.currentDogId, neutered)
|
aiMates.value = await spotsApi.aiMates(currentSpot.value.id, dogs.currentDogId)
|
||||||
} catch {
|
} catch {
|
||||||
aiMates.value = []
|
aiMates.value = []
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
+228
-7
@@ -53,7 +53,9 @@
|
|||||||
<div class="q-ml-md">
|
<div class="q-ml-md">
|
||||||
<div class="text-h6">{{ current.name }}</div>
|
<div class="text-h6">{{ current.name }}</div>
|
||||||
<div class="text-caption text-grey-7">
|
<div class="text-caption text-grey-7">
|
||||||
{{ current.breed || '견종 미상' }}<span v-if="current.size"> · {{ sizeLabel(current.size) }}</span>
|
{{ current.breed || '견종 미상' }}<span v-if="current.size"> · {{ sizeLabel(current.size) }}</span
|
||||||
|
><span v-if="current.gender"> · {{ genderLabel(current.gender) }}</span
|
||||||
|
><span v-if="dogAge(current.birthDate)"> · {{ dogAge(current.birthDate) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="hoodScore" class="text-caption row items-center no-wrap q-mt-xs">
|
<div v-if="hoodScore" class="text-caption row items-center no-wrap q-mt-xs">
|
||||||
<q-icon name="groups" size="14px" color="teal" class="q-mr-xs" />
|
<q-icon name="groups" size="14px" color="teal" class="q-mr-xs" />
|
||||||
@@ -80,11 +82,47 @@
|
|||||||
{{ tag.label }}
|
{{ tag.label }}
|
||||||
</q-chip>
|
</q-chip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- AI 추천 조건 (원하는 메이트 조건 → AI 분석 반영) -->
|
||||||
|
<div class="text-subtitle1 text-weight-bold q-mb-xs row items-center">
|
||||||
|
<q-icon name="auto_awesome" color="primary" size="20px" class="q-mr-xs" />AI 추천 조건
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey-6 q-mb-sm">
|
||||||
|
AI 추천 찰떡 메이트를 찾을 때 이 조건을 반영해요
|
||||||
|
</div>
|
||||||
|
<div class="q-mb-sm">
|
||||||
|
<div class="text-caption text-grey-7 q-mb-xs">선호 성별</div>
|
||||||
|
<q-btn-toggle
|
||||||
|
v-model="prefGender"
|
||||||
|
spread dense unelevated no-caps size="sm"
|
||||||
|
toggle-color="primary" color="grey-3" text-color="grey-8"
|
||||||
|
:options="prefGenderOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="q-mb-sm">
|
||||||
|
<div class="text-caption text-grey-7 q-mb-xs">선호 연령</div>
|
||||||
|
<q-btn-toggle
|
||||||
|
v-model="prefAge"
|
||||||
|
spread dense unelevated no-caps size="sm"
|
||||||
|
toggle-color="primary" color="grey-3" text-color="grey-8"
|
||||||
|
:options="prefAgeOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="q-mb-md">
|
||||||
|
<div class="text-caption text-grey-7 q-mb-xs">선호 중성화</div>
|
||||||
|
<q-btn-toggle
|
||||||
|
v-model="prefNeutered"
|
||||||
|
spread dense unelevated no-caps size="sm"
|
||||||
|
toggle-color="primary" color="grey-3" text-color="grey-8"
|
||||||
|
:options="prefNeuteredOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
unelevated
|
unelevated
|
||||||
icon="save"
|
icon="save"
|
||||||
label="성향 저장"
|
label="저장"
|
||||||
:loading="saving"
|
:loading="saving"
|
||||||
:disable="!dirty"
|
:disable="!dirty"
|
||||||
@click="saveTraits"
|
@click="saveTraits"
|
||||||
@@ -98,15 +136,66 @@
|
|||||||
<q-card-section class="q-gutter-sm">
|
<q-card-section class="q-gutter-sm">
|
||||||
<q-input v-model="form.name" label="이름 *" dense outlined />
|
<q-input v-model="form.name" label="이름 *" dense outlined />
|
||||||
<q-input v-model="form.breed" label="견종" dense outlined />
|
<q-input v-model="form.breed" label="견종" dense outlined />
|
||||||
<q-select
|
|
||||||
|
<div>
|
||||||
|
<div class="text-caption text-grey-7 q-mb-xs">크기</div>
|
||||||
|
<q-btn-toggle
|
||||||
v-model="form.size"
|
v-model="form.size"
|
||||||
|
spread
|
||||||
|
unelevated
|
||||||
|
no-caps
|
||||||
|
toggle-color="primary"
|
||||||
|
color="grey-3"
|
||||||
|
text-color="grey-8"
|
||||||
:options="sizeOptions"
|
:options="sizeOptions"
|
||||||
label="크기"
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="text-caption text-grey-7 q-mb-xs">성별</div>
|
||||||
|
<q-btn-toggle
|
||||||
|
v-model="form.gender"
|
||||||
|
spread
|
||||||
|
unelevated
|
||||||
|
no-caps
|
||||||
|
toggle-color="primary"
|
||||||
|
color="grey-3"
|
||||||
|
text-color="grey-8"
|
||||||
|
:options="[
|
||||||
|
{ label: '남아', value: 'MALE' },
|
||||||
|
{ label: '여아', value: 'FEMALE' },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="text-caption text-grey-7 q-mb-xs">
|
||||||
|
연령<span v-if="ageText" class="text-primary q-ml-xs">· {{ ageText }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="row q-col-gutter-sm">
|
||||||
|
<q-select
|
||||||
|
v-model="form.birthYear"
|
||||||
|
:options="yearOptions"
|
||||||
|
label="출생 연도"
|
||||||
|
class="col"
|
||||||
dense
|
dense
|
||||||
outlined
|
outlined
|
||||||
emit-value
|
emit-value
|
||||||
map-options
|
map-options
|
||||||
/>
|
/>
|
||||||
|
<q-select
|
||||||
|
v-model="form.birthMonth"
|
||||||
|
:options="monthOptions"
|
||||||
|
label="월"
|
||||||
|
class="col-5"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="text-caption text-grey-7 q-mb-xs">중성화 여부 <span class="text-negative">*</span></div>
|
<div class="text-caption text-grey-7 q-mb-xs">중성화 여부 <span class="text-negative">*</span></div>
|
||||||
<q-btn-toggle
|
<q-btn-toggle
|
||||||
@@ -123,6 +212,40 @@
|
|||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- AI 추천 조건: 원하는 메이트 조건 (AI 분석에 반영) -->
|
||||||
|
<q-separator class="q-my-xs" />
|
||||||
|
<div class="text-subtitle2 row items-center">
|
||||||
|
<q-icon name="auto_awesome" color="primary" size="18px" class="q-mr-xs" />AI 추천 조건
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey-6 q-mb-xs">이런 친구를 찾아달라고 AI에게 알려줘요</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-caption text-grey-7 q-mb-xs">선호 성별</div>
|
||||||
|
<q-btn-toggle
|
||||||
|
v-model="form.prefGender"
|
||||||
|
spread dense unelevated no-caps size="sm"
|
||||||
|
toggle-color="primary" color="grey-3" text-color="grey-8"
|
||||||
|
:options="prefGenderOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-caption text-grey-7 q-mb-xs">선호 연령</div>
|
||||||
|
<q-btn-toggle
|
||||||
|
v-model="form.prefAge"
|
||||||
|
spread dense unelevated no-caps size="sm"
|
||||||
|
toggle-color="primary" color="grey-3" text-color="grey-8"
|
||||||
|
:options="prefAgeOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-caption text-grey-7 q-mb-xs">선호 중성화</div>
|
||||||
|
<q-btn-toggle
|
||||||
|
v-model="form.prefNeutered"
|
||||||
|
spread dense unelevated no-caps size="sm"
|
||||||
|
toggle-color="primary" color="grey-3" text-color="grey-8"
|
||||||
|
:options="prefNeuteredOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn flat label="취소" v-close-popup />
|
<q-btn flat label="취소" v-close-popup />
|
||||||
@@ -179,16 +302,66 @@ const sizeOptions = [
|
|||||||
{ label: '중형견', value: 'MEDIUM' },
|
{ label: '중형견', value: 'MEDIUM' },
|
||||||
{ label: '대형견', value: 'LARGE' },
|
{ label: '대형견', value: 'LARGE' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// AI 추천 조건 선택지 (ANY = 상관없음)
|
||||||
|
const prefGenderOptions = [
|
||||||
|
{ label: '상관없음', value: 'ANY' },
|
||||||
|
{ label: '남아', value: 'MALE' },
|
||||||
|
{ label: '여아', value: 'FEMALE' },
|
||||||
|
]
|
||||||
|
const prefAgeOptions = [
|
||||||
|
{ label: '상관없음', value: 'ANY' },
|
||||||
|
{ label: '퍼피', value: 'PUPPY' },
|
||||||
|
{ label: '성견', value: 'ADULT' },
|
||||||
|
{ label: '노령', value: 'SENIOR' },
|
||||||
|
]
|
||||||
|
const prefNeuteredOptions = [
|
||||||
|
{ label: '상관없음', value: 'ANY' },
|
||||||
|
{ label: '완료', value: 'YES' },
|
||||||
|
{ label: '안 함', value: 'NO' },
|
||||||
|
]
|
||||||
|
|
||||||
const form = reactive<{
|
const form = reactive<{
|
||||||
name: string
|
name: string
|
||||||
breed: string
|
breed: string
|
||||||
size: string | null
|
size: string | null
|
||||||
|
gender: string | null
|
||||||
|
birthYear: number | null
|
||||||
|
birthMonth: number | null
|
||||||
neutered: boolean | null
|
neutered: boolean | null
|
||||||
|
prefGender: string
|
||||||
|
prefAge: string
|
||||||
|
prefNeutered: string
|
||||||
}>({
|
}>({
|
||||||
name: '',
|
name: '',
|
||||||
breed: '',
|
breed: '',
|
||||||
size: null,
|
size: null,
|
||||||
|
gender: null,
|
||||||
|
birthYear: null,
|
||||||
|
birthMonth: null,
|
||||||
neutered: null,
|
neutered: null,
|
||||||
|
prefGender: 'ANY',
|
||||||
|
prefAge: 'ANY',
|
||||||
|
prefNeutered: 'ANY',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 프로필 화면에서 편집하는 현재 강아지의 AI 추천 조건
|
||||||
|
const prefGender = ref('ANY')
|
||||||
|
const prefAge = ref('ANY')
|
||||||
|
const prefNeutered = ref('ANY')
|
||||||
|
|
||||||
|
// 출생 연도(올해 ~ 20년 전) / 월 선택지
|
||||||
|
const thisYear = new Date().getFullYear()
|
||||||
|
const yearOptions = Array.from({ length: 21 }, (_, i) => {
|
||||||
|
const y = thisYear - i
|
||||||
|
return { label: `${y}년`, value: y }
|
||||||
|
})
|
||||||
|
const monthOptions = Array.from({ length: 12 }, (_, i) => ({ label: `${i + 1}월`, value: i + 1 }))
|
||||||
|
|
||||||
|
// 선택한 출생 연·월 → 연령 텍스트 (예: 2살 3개월)
|
||||||
|
const ageText = computed(() => {
|
||||||
|
if (form.birthYear == null || form.birthMonth == null) return ''
|
||||||
|
return ageFromBirth(form.birthYear, form.birthMonth)
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -199,20 +372,26 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 대표 강아지가 바뀌면 선택된 성향 동기화
|
// 대표 강아지가 바뀌면 선택된 성향 + AI 추천 조건 동기화
|
||||||
watch(
|
watch(
|
||||||
current,
|
current,
|
||||||
(d) => {
|
(d) => {
|
||||||
selected.value = new Set(d?.traits.map((t) => t.id) ?? [])
|
selected.value = new Set(d?.traits.map((t) => t.id) ?? [])
|
||||||
|
prefGender.value = d?.prefGender ?? 'ANY'
|
||||||
|
prefAge.value = d?.prefAge ?? 'ANY'
|
||||||
|
prefNeutered.value = d?.prefNeutered ?? 'ANY'
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
// 변경 감지(저장 버튼 활성화)
|
// 변경 감지(저장 버튼 활성화) — 성향 태그 또는 AI 추천 조건 변경 시
|
||||||
const dirty = computed(() => {
|
const dirty = computed(() => {
|
||||||
const orig = new Set(current.value?.traits.map((t) => t.id) ?? [])
|
const orig = new Set(current.value?.traits.map((t) => t.id) ?? [])
|
||||||
if (orig.size !== selected.value.size) return true
|
if (orig.size !== selected.value.size) return true
|
||||||
for (const id of selected.value) if (!orig.has(id)) return true
|
for (const id of selected.value) if (!orig.has(id)) return true
|
||||||
|
if (prefGender.value !== (current.value?.prefGender ?? 'ANY')) return true
|
||||||
|
if (prefAge.value !== (current.value?.prefAge ?? 'ANY')) return true
|
||||||
|
if (prefNeutered.value !== (current.value?.prefNeutered ?? 'ANY')) return true
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -226,6 +405,18 @@ function sizeLabel(size: string) {
|
|||||||
return sizeOptions.find((o) => o.value === size)?.label ?? size
|
return sizeOptions.find((o) => o.value === size)?.label ?? size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function genderLabel(gender: string) {
|
||||||
|
return gender === 'MALE' ? '남아' : gender === 'FEMALE' ? '여아' : gender
|
||||||
|
}
|
||||||
|
|
||||||
|
/** birthDate(YYYY-MM-DD) → 연령 텍스트. 없으면 빈 문자열. */
|
||||||
|
function dogAge(birthDate: string | null): string {
|
||||||
|
if (!birthDate) return ''
|
||||||
|
const [y, m] = birthDate.split('-').map(Number)
|
||||||
|
if (!y || !m) return ''
|
||||||
|
return ageFromBirth(y, m)
|
||||||
|
}
|
||||||
|
|
||||||
async function saveTraits() {
|
async function saveTraits() {
|
||||||
if (!current.value) return
|
if (!current.value) return
|
||||||
saving.value = true
|
saving.value = true
|
||||||
@@ -238,9 +429,12 @@ async function saveTraits() {
|
|||||||
gender: current.value.gender,
|
gender: current.value.gender,
|
||||||
neutered: current.value.neutered,
|
neutered: current.value.neutered,
|
||||||
imageUrl: current.value.imageUrl,
|
imageUrl: current.value.imageUrl,
|
||||||
|
prefGender: prefGender.value,
|
||||||
|
prefAge: prefAge.value,
|
||||||
|
prefNeutered: prefNeutered.value,
|
||||||
traitIds: [...selected.value],
|
traitIds: [...selected.value],
|
||||||
})
|
})
|
||||||
$q.notify({ color: 'primary', message: '성향을 저장했어요.', icon: 'check', position: 'top' })
|
$q.notify({ color: 'primary', message: '저장했어요.', icon: 'check', position: 'top' })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
notifyError(e)
|
notifyError(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -252,7 +446,13 @@ function openCreate() {
|
|||||||
form.name = ''
|
form.name = ''
|
||||||
form.breed = ''
|
form.breed = ''
|
||||||
form.size = null
|
form.size = null
|
||||||
|
form.gender = null
|
||||||
|
form.birthYear = null
|
||||||
|
form.birthMonth = null
|
||||||
form.neutered = null
|
form.neutered = null
|
||||||
|
form.prefGender = 'ANY'
|
||||||
|
form.prefAge = 'ANY'
|
||||||
|
form.prefNeutered = 'ANY'
|
||||||
createOpen.value = true
|
createOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,11 +460,21 @@ async function onCreate() {
|
|||||||
if (form.neutered === null) return
|
if (form.neutered === null) return
|
||||||
creating.value = true
|
creating.value = true
|
||||||
try {
|
try {
|
||||||
|
// 출생 연·월 → birthDate (해당 월 1일). 둘 다 선택해야 저장.
|
||||||
|
const birthDate =
|
||||||
|
form.birthYear != null && form.birthMonth != null
|
||||||
|
? `${form.birthYear}-${String(form.birthMonth).padStart(2, '0')}-01`
|
||||||
|
: null
|
||||||
await dogs.createDog({
|
await dogs.createDog({
|
||||||
name: form.name,
|
name: form.name,
|
||||||
breed: form.breed || null,
|
breed: form.breed || null,
|
||||||
size: form.size,
|
size: form.size,
|
||||||
|
gender: form.gender,
|
||||||
|
birthDate,
|
||||||
neutered: form.neutered,
|
neutered: form.neutered,
|
||||||
|
prefGender: form.prefGender,
|
||||||
|
prefAge: form.prefAge,
|
||||||
|
prefNeutered: form.prefNeutered,
|
||||||
})
|
})
|
||||||
createOpen.value = false
|
createOpen.value = false
|
||||||
$q.notify({ color: 'primary', message: `${form.name} 등록 완료!`, icon: 'pets', position: 'top' })
|
$q.notify({ color: 'primary', message: `${form.name} 등록 완료!`, icon: 'pets', position: 'top' })
|
||||||
@@ -275,6 +485,17 @@ async function onCreate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 출생 연·월로 연령 텍스트 계산 (예: 2살 3개월 / 5개월). */
|
||||||
|
function ageFromBirth(year: number, month: number): string {
|
||||||
|
const now = new Date()
|
||||||
|
let months = (now.getFullYear() - year) * 12 + (now.getMonth() + 1 - month)
|
||||||
|
if (months < 0) months = 0
|
||||||
|
const y = Math.floor(months / 12)
|
||||||
|
const mo = months % 12
|
||||||
|
if (y <= 0) return `${mo}개월`
|
||||||
|
return mo > 0 ? `${y}살 ${mo}개월` : `${y}살`
|
||||||
|
}
|
||||||
|
|
||||||
async function onDelete() {
|
async function onDelete() {
|
||||||
if (!current.value) return
|
if (!current.value) return
|
||||||
$q.dialog({
|
$q.dialog({
|
||||||
|
|||||||
Reference in New Issue
Block a user