feat: Update search bar component
This commit is contained in:
parent
9e3574cf36
commit
0c5ad07f0e
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
51
src/components/common/SearchBox.vue
Normal file
51
src/components/common/SearchBox.vue
Normal 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>
|
@ -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 {
|
||||||
|
@ -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>
|
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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",
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
|
Loading…
Reference in New Issue
Block a user