feat: Update search bar component

This commit is contained in:
jhynsoo 2024-05-22 11:51:49 +09:00
parent 9e3574cf36
commit 0c5ad07f0e
10 changed files with 123 additions and 129 deletions

View File

@ -35,6 +35,10 @@ button {
transform 0.25s; transform 0.25s;
} }
button:active {
transform: scale(0.95);
}
button:disabled { button:disabled {
cursor: initial; cursor: initial;
} }

View File

@ -28,13 +28,12 @@ onMounted(() => {
oldMarkers.forEach((marker) => marker.setMap(null)); oldMarkers.forEach((marker) => marker.setMap(null));
newMarkers.forEach((marker) => marker.setMap(map)); newMarkers.forEach((marker) => marker.setMap(map));
target; target;
setTarget(newMarkers[0].getPosition()); if (!oldMarkers[0] || !newMarkers[0].getPosition().equals(oldMarkers[0].getPosition())) {
setTarget(newMarkers[0].getPosition());
}
}); });
watch(target, (newTarget, oldTarget) => { watch(target, (newTarget) => {
if (oldTarget && newTarget.La === oldTarget.La && newTarget.Ma === oldTarget.Ma) {
return;
}
map.panTo(newTarget); map.panTo(newTarget);
}); });
}); });

View File

@ -0,0 +1,51 @@
<script setup>
import TextButton from './TextButton.vue';
const props = defineProps({
name: String,
onSubmit: Function,
});
const { name, onSubmit } = props;
const model = defineModel();
</script>
<template>
<form @submit.prevent="onSubmit" class="search-box">
<input type="text" :name="name" v-model.lazy="model" placeholder="검색어를 입력하세요" />
<TextButton @click="onSubmit">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-search"
viewBox="0 0 16 16"
part="svg"
>
<path
d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"
></path>
</svg>
</TextButton>
</form>
</template>
<style scoped>
.search-box {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
box-shadow: var(--shadow);
border-radius: 12px;
border: 1px solid var(--color-border);
padding: 12px;
}
input {
padding: 0 8px;
border: none;
width: 100%;
outline: none;
}
</style>

View File

@ -1,9 +1,9 @@
<!-- eslint-disable vue/multi-word-component-names --> <!-- eslint-disable vue/multi-word-component-names -->
<script setup> <script setup>
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref, toRefs } from 'vue';
import FilledButton from './FilledButton.vue'; import FilledButton from './FilledButton.vue';
const { placeholder, options, optionName, disabled } = defineProps({ const props = defineProps({
placeholder: { placeholder: {
type: String, type: String,
default: '----', default: '----',
@ -17,20 +17,27 @@ const { placeholder, options, optionName, disabled } = defineProps({
default: 'value', default: 'value',
}, },
modelValue: { modelValue: {
sidoCode: Number, type: Object,
default: null,
},
defaultIndex: {
type: Number || null,
default: null,
}, },
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}); });
const { placeholder, optionName } = props;
const { modelValue, options, disabled } = toRefs(props);
const updateModel = defineEmits(['update:modelValue']); const updateModel = defineEmits(['update:modelValue']);
const body = document.querySelector('body'); const body = document.querySelector('body');
const buttonRef = ref(null); const buttonRef = ref(null);
const isOpen = ref(false); const isOpen = ref(false);
const menuStyle = ref(''); const menuStyle = ref('');
const selected = ref(null); const selected = modelValue;
onMounted(() => { onMounted(() => {
const rect = buttonRef.value.$el.getBoundingClientRect(); const rect = buttonRef.value.$el.getBoundingClientRect();
@ -38,7 +45,7 @@ onMounted(() => {
menuStyle.value = `transform: translate(${rect.left}px, ${rect.bottom + 4}px)`; menuStyle.value = `transform: translate(${rect.left}px, ${rect.bottom + 4}px)`;
}); });
const selectedName = computed(() => selected.value?.[optionName] ?? selected.value); const selectedName = computed(() => modelValue.value?.[optionName] ?? modelValue.value);
function handleOpen() { function handleOpen() {
isOpen.value = true; isOpen.value = true;
@ -60,7 +67,6 @@ function handleClose({ target }) {
} }
function handleSelect(option) { function handleSelect(option) {
selected.value = option;
updateModel('update:modelValue', option); updateModel('update:modelValue', option);
closeMenu(); closeMenu();
} }
@ -73,7 +79,7 @@ function handleSelect(option) {
ref="buttonRef" ref="buttonRef"
@mousedown="handleOpen" @mousedown="handleOpen"
> >
<span>{{ selectedName ?? placeholder }}</span> <div class="select-label">{{ selectedName ?? placeholder }}</div>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="16" width="16"
@ -118,6 +124,12 @@ function handleSelect(option) {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
overflow: hidden;
}
.select-label {
overflow: hidden;
white-space: nowrap;
} }
.active { .active {

View File

@ -1,20 +0,0 @@
<script setup>
import { ref} from 'vue'
defineProps({ selectOptions: Array })
const emit = defineEmits(["onKeySelect"])
const key = ref('all')
const onSelect = () => {
console.log('vselect................... key:', key.value)
emit('onKeySelect', key.value)
}
</script>
<template>
<select class="form-select" v-model="key" @change="onSelect">
<option v-for="(k, index) in selectOptions" :key="index" :value="k.value">
{{k.text}}
</option>
</select>
</template>
<style scoped>
</style>

View File

@ -60,6 +60,11 @@ function handleClick() {
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
background-color: var(--color-background); background-color: var(--color-background);
cursor: pointer; cursor: pointer;
transition: transform 0.25s;
}
.attraction:active {
transform: scale(0.98);
} }
.info { .info {

View File

@ -1,11 +1,12 @@
<script setup> <script setup>
import { ref, watch } from 'vue'; import { reactive, ref, watch } from 'vue';
import TextButton from '../common/TextButton.vue';
import axios from 'axios'; import axios from 'axios';
import Select from '../common/Select.vue'; import Select from '../common/Select.vue';
import { useAttractionStore } from '@/stores/attractionStore'; import { useAttractionStore } from '@/stores/attractionStore';
import SearchBox from '../common/SearchBox.vue';
const contentTypeList = [ const contentTypeList = reactive([
{ value: 0, name: '전체' },
{ value: 12, name: '관광지' }, { value: 12, name: '관광지' },
{ value: 14, name: '문화시설' }, { value: 14, name: '문화시설' },
{ value: 15, name: '축제공연행사' }, { value: 15, name: '축제공연행사' },
@ -14,31 +15,37 @@ const contentTypeList = [
{ value: 32, name: '숙박' }, { value: 32, name: '숙박' },
{ value: 38, name: '쇼핑' }, { value: 38, name: '쇼핑' },
{ value: 39, name: '음식점' }, { value: 39, name: '음식점' },
]; ]);
const { search, setQueryString } = useAttractionStore(); const { search, setQueryString } = useAttractionStore();
const sidoList = ref([]); const sidoList = ref([]);
const gugunList = ref([]); const gugunList = ref([]);
const sido = ref({ sidoCode: 0 }); const sido = ref(null);
const gugun = ref({ gugunCode: 0 }); const gugun = ref(null);
const contentType = ref(0); const contentType = ref(null);
const keyword = ref(''); const keyword = ref('');
watch(sido, ({ sidoCode }) => { watch(sido, ({ sidoCode }) => {
gugun.value = null;
if (sidoCode === 0) {
return;
}
axios.get(`//localhost:8000/area/gugun?sidoCode=${sidoCode}`).then(({ data }) => { axios.get(`//localhost:8000/area/gugun?sidoCode=${sidoCode}`).then(({ data }) => {
gugunList.value = data; gugunList.value = [{ gugunCode: 0, gugunName: '전체' }, ...data];
}); });
}); });
if (sidoList.value.length === 0) { if (sidoList.value.length === 0) {
axios.get('//localhost:8000/area/sido').then(({ data }) => (sidoList.value = data)); axios
.get('//localhost:8000/area/sido')
.then(({ data }) => (sidoList.value = [{ sidoCode: 0, sidoName: '전체' }, ...data]));
} }
function handleSubmit() { function handleSubmit() {
const { sidoCode } = sido.value; const { sidoCode } = sido.value ?? {};
const { gugunCode } = gugun.value; const { gugunCode } = gugun.value ?? {};
const areaQuery = `sidoCode=${sidoCode}` + (sidoCode ? `&gugunCode=${gugunCode}` : ''); const areaQuery = `sidoCode=${sidoCode ?? 0}` + (sidoCode ? `&gugunCode=${gugunCode ?? 0}` : '');
const keywordQuery = `title=${keyword.value}`; const keywordQuery = `title=${keyword.value}`;
const contentTypeQuery = `contentTypeId=${contentType.value.value}`; const contentTypeQuery = `contentTypeId=${contentType.value?.value ?? 0}`;
const queryString = [areaQuery, keywordQuery, contentTypeQuery].join('&'); const queryString = [areaQuery, keywordQuery, contentTypeQuery].join('&');
setQueryString(queryString); setQueryString(queryString);
@ -54,34 +61,17 @@ function handleSubmit() {
placeholder="시/군/구" placeholder="시/군/구"
v-model="gugun" v-model="gugun"
:options="gugunList" :options="gugunList"
:disabled="gugunList.length === 0" :disabled="gugunList.length <= 1"
optionName="gugunName" optionName="gugunName"
/> />
<Select <Select
placeholder="류" placeholder="류"
v-model="contentType" v-model="contentType"
:options="contentTypeList" :options="contentTypeList"
optionName="name" optionName="name"
/> />
</nav> </nav>
<form @submit.prevent="handleSubmit" class="search-box"> <SearchBox name="keyword" v-model.lazy="keyword" :onSubmit="handleSubmit" />
<input type="text" name="keyword" v-model.lazy="keyword" placeholder="검색어를 입력하세요" />
<TextButton @click="handleSubmit">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-search"
viewBox="0 0 16 16"
part="svg"
>
<path
d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"
></path>
</svg>
</TextButton>
</form>
</div> </div>
</template> </template>
@ -101,27 +91,4 @@ function handleSubmit() {
.select-wrapper > * { .select-wrapper > * {
width: 100%; width: 100%;
} }
.search-box {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
box-shadow: var(--shadow);
border-radius: 12px;
border: 1px solid var(--color-border);
padding: 12px;
}
input {
padding: 0 8px;
border: none;
width: 100%;
outline: none;
}
button {
display: flex;
align-items: center;
}
</style> </style>

View File

@ -1,22 +1,22 @@
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '@/views/HomeView.vue'; import HomeView from '@/views/HomeView.vue';
import { storeToRefs } from "pinia"; import { storeToRefs } from 'pinia';
import { useMemberStore } from "@/stores/memberStore"; import { useMemberStore } from '@/stores/memberStore';
const onlyAuthUser = async (to, from, next) => { const onlyAuthUser = async (to, from, next) => {
const memberStore = useMemberStore(); const memberStore = useMemberStore();
const { userInfo, isValidToken } = storeToRefs(memberStore); const { userInfo, isValidToken } = storeToRefs(memberStore);
const { getUserInfo } = memberStore; const { getUserInfo } = memberStore;
let token = sessionStorage.getItem("accessToken"); let token = sessionStorage.getItem('accessToken');
if (userInfo.value != null && token) { if (userInfo.value != null && token) {
await getUserInfo(token); await getUserInfo(token);
} }
if (!isValidToken.value || userInfo.value === null) { if (!isValidToken.value || userInfo.value === null) {
next({ name: "user-login" }); next({ name: 'user-login' });
} else { } else {
next(); next();
} }
@ -31,25 +31,25 @@ const router = createRouter({
component: HomeView, component: HomeView,
}, },
{ {
path: "/user", path: '/user',
name: "user", name: 'user',
component: () => import("@/views/TheUserView.vue"), component: () => import('@/views/TheUserView.vue'),
children: [ children: [
{ {
path: "login", path: 'login',
name: "user-login", name: 'user-login',
component: () => import("@/components/users/UserLogin.vue"), component: () => import('@/components/users/UserLogin.vue'),
}, },
{ {
path: "join", path: 'join',
name: "user-join", name: 'user-join',
component: () => import("@/components/users/UserRegister.vue"), component: () => import('@/components/users/UserRegister.vue'),
}, },
{ {
path: "mypage", path: 'mypage',
name: "user-mypage", name: 'user-mypage',
beforeEnter: onlyAuthUser, beforeEnter: onlyAuthUser,
component: () => import("@/components/users/UserMyPage.vue"), component: () => import('@/components/users/UserMyPage.vue'),
}, },
// { // {
// path: "modify/:userid", // path: "modify/:userid",

View File

@ -11,6 +11,7 @@ export const useAttractionStore = defineStore('attraction', () => {
function setQueryString(value) { function setQueryString(value) {
queryString.value = value; queryString.value = value;
page.value = 0;
} }
function search(preventClear = false) { function search(preventClear = false) {

View File

@ -1,25 +0,0 @@
<template>
<div id="app">
<TravelChatBot />
</div>
</template>
<script>
import TravelChatBot from '@/components/chatbot/TravelChatBot.vue';
export default {
name: 'App',
components: {
TravelChatBot,
},
};
</script>
<style>
body {
font-family: Avenir, Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
</style>