feat: 강아지 등록 중성화 필수 선택 + AI 추천 중성화 조건 옵션
CI / build (push) Failing after 11m51s

- 등록 폼: 중성화 여부(완료/안 함) 필수 토글, 미선택 시 등록 불가
- AI 추천: 중성화 찾기 옵션(상관없음/완료/안 함) → 조건 강아지 중에서만 추천

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sb
2026-07-11 22:14:42 +09:00
parent f29d5e106c
commit eb51201669
3 changed files with 60 additions and 6 deletions
+5 -2
View File
@@ -40,8 +40,11 @@ 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) => aiMates: (spotId: number, dogId: number, neutered?: boolean) =>
http.get<AiMate[]>(`/api/spots/${spotId}/ai-mates?dogId=${dogId}`), http.get<AiMate[]>(
`/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`),
+23 -1
View File
@@ -40,6 +40,26 @@
</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="row items-center q-mb-sm">
<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 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" />
<span class="text-caption">친구 찾는 </span> <span class="text-caption">친구 찾는 </span>
@@ -153,6 +173,7 @@ 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)
@@ -218,8 +239,9 @@ 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) aiMates.value = await spotsApi.aiMates(currentSpot.value.id, dogs.currentDogId, neutered)
} catch { } catch {
aiMates.value = [] aiMates.value = []
} finally { } finally {
+32 -3
View File
@@ -102,6 +102,22 @@
emit-value emit-value
map-options map-options
/> />
<div>
<div class="text-caption text-grey-7 q-mb-xs">중성화 여부 <span class="text-negative">*</span></div>
<q-btn-toggle
v-model="form.neutered"
spread
unelevated
no-caps
toggle-color="primary"
color="grey-3"
text-color="grey-8"
:options="[
{ label: '중성화 완료', value: true },
{ label: '안 함', value: false },
]"
/>
</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 />
@@ -110,7 +126,7 @@
unelevated unelevated
label="등록" label="등록"
:loading="creating" :loading="creating"
:disable="!form.name" :disable="!form.name || form.neutered === null"
@click="onCreate" @click="onCreate"
/> />
</q-card-actions> </q-card-actions>
@@ -141,10 +157,16 @@ const sizeOptions = [
{ label: '중형견', value: 'MEDIUM' }, { label: '중형견', value: 'MEDIUM' },
{ label: '대형견', value: 'LARGE' }, { label: '대형견', value: 'LARGE' },
] ]
const form = reactive<{ name: string; breed: string; size: string | null }>({ const form = reactive<{
name: string
breed: string
size: string | null
neutered: boolean | null
}>({
name: '', name: '',
breed: '', breed: '',
size: null, size: null,
neutered: null,
}) })
onMounted(async () => { onMounted(async () => {
@@ -208,13 +230,20 @@ function openCreate() {
form.name = '' form.name = ''
form.breed = '' form.breed = ''
form.size = null form.size = null
form.neutered = null
createOpen.value = true createOpen.value = true
} }
async function onCreate() { async function onCreate() {
if (form.neutered === null) return
creating.value = true creating.value = true
try { try {
await dogs.createDog({ name: form.name, breed: form.breed || null, size: form.size }) await dogs.createDog({
name: form.name,
breed: form.breed || null,
size: form.size,
neutered: form.neutered,
})
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' })
} catch (e) { } catch (e) {