Compare commits
10 Commits
a5019812ce
...
ad087c1eaa
Author | SHA1 | Date | |
---|---|---|---|
ad087c1eaa | |||
|
ab7398668a | ||
c64240e509 | |||
4825289175 | |||
486db7b781 | |||
945b3fff7d | |||
2c183f551c | |||
875ac42329 | |||
7a39717a5e | |||
4a65a80b40 |
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite App</title>
|
<title>EnjoyTrip</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
@import './base.css';
|
@import './base.css';
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { getArticles } from '@/api/article';
|
import { getArticles } from '@/api/article';
|
||||||
import { ref, reactive, watch } from 'vue';
|
import { ref, watch, computed } from 'vue';
|
||||||
import { RouterLink } from 'vue-router';
|
import { RouterLink } from 'vue-router';
|
||||||
import FilledButton from '../common/FilledButton.vue';
|
import FilledButton from '../common/FilledButton.vue';
|
||||||
|
import { useMemberStore } from '@/stores/memberStore';
|
||||||
|
|
||||||
const articles = ref([]);
|
const articles = ref([]);
|
||||||
const params = reactive({
|
const param = computed(() => {
|
||||||
pageNo: 1,
|
return {
|
||||||
key: 'all',
|
pageNo: currentPage.value,
|
||||||
word: '',
|
};
|
||||||
});
|
});
|
||||||
|
const currentPage = ref(1);
|
||||||
const hasNextPage = ref(true);
|
const hasNextPage = ref(true);
|
||||||
|
|
||||||
const lastElement = ref(null);
|
const lastElement = ref(null);
|
||||||
|
const memberStore = useMemberStore();
|
||||||
|
|
||||||
watch(lastElement, (el) => {
|
watch(lastElement, (el) => {
|
||||||
if (!el) {
|
if (!el) {
|
||||||
@ -20,8 +22,8 @@ watch(lastElement, (el) => {
|
|||||||
}
|
}
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
([entry]) => {
|
([entry]) => {
|
||||||
if (entry.isIntersecting && hasNextPage.value) {
|
if (entry.isIntersecting && articles.value.length && hasNextPage.value) {
|
||||||
params.pageNo += 1;
|
currentPage.value += 1;
|
||||||
searchList();
|
searchList();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -32,8 +34,8 @@ watch(lastElement, (el) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function searchList() {
|
function searchList() {
|
||||||
getArticles(params).then(({ data }) => {
|
getArticles(param.value).then(({ data }) => {
|
||||||
if ((data?.articles?.length ?? 0) === 0) {
|
if (data?.page?.total === currentPage.value || (data?.articles?.length ?? 0) === 0) {
|
||||||
hasNextPage.value = false;
|
hasNextPage.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -46,7 +48,7 @@ searchList();
|
|||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<h1>게시판</h1>
|
<h1>게시판</h1>
|
||||||
<RouterLink :to="{ name: 'article-create' }">
|
<RouterLink v-if="memberStore.isLogin" :to="{ name: 'article-create' }">
|
||||||
<FilledButton primary class="write">글쓰기</FilledButton>
|
<FilledButton primary class="write">글쓰기</FilledButton>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</header>
|
</header>
|
||||||
|
@ -2,9 +2,16 @@
|
|||||||
import { useMemberStore } from '@/stores/memberStore';
|
import { useMemberStore } from '@/stores/memberStore';
|
||||||
import CommentForm from './CommentForm.vue';
|
import CommentForm from './CommentForm.vue';
|
||||||
import CommentList from './CommentList.vue';
|
import CommentList from './CommentList.vue';
|
||||||
|
import { toRefs } from 'vue';
|
||||||
|
|
||||||
const { articleId, comments } = defineProps({ articleId: Number, comments: Array });
|
const props = defineProps({ articleId: Number, comments: Array });
|
||||||
|
const { articleId } = props;
|
||||||
|
const { comments } = toRefs(props);
|
||||||
const memberStore = useMemberStore();
|
const memberStore = useMemberStore();
|
||||||
|
|
||||||
|
function updateList(comment) {
|
||||||
|
comments.value.push(comment);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -12,8 +19,8 @@ const memberStore = useMemberStore();
|
|||||||
<h2>
|
<h2>
|
||||||
댓글 <span>{{ comments.length }}</span>
|
댓글 <span>{{ comments.length }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<CommentForm :article-id="articleId" v-if="memberStore.isLogin" />
|
<CommentForm @updateList="updateList" :article-id="articleId" v-if="memberStore.isLogin" />
|
||||||
<CommentList :comments="comments" />
|
<CommentList :comments="comments" :userId="memberStore.userId" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -4,7 +4,8 @@ import FilledButton from '../common/FilledButton.vue';
|
|||||||
import TextButton from '../common/TextButton.vue';
|
import TextButton from '../common/TextButton.vue';
|
||||||
import { addComment } from '@/api/comment';
|
import { addComment } from '@/api/comment';
|
||||||
|
|
||||||
const { id } = defineProps({ id: Number });
|
const emit = defineEmits(['updateList']);
|
||||||
|
const { articleId } = defineProps({ articleId: Number });
|
||||||
const isActive = ref(false);
|
const isActive = ref(false);
|
||||||
const textDiv = ref(null);
|
const textDiv = ref(null);
|
||||||
const text = ref('');
|
const text = ref('');
|
||||||
@ -19,9 +20,8 @@ function handleCancel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
addComment({ id, text: text.value }).then(({ data }) => {
|
addComment({ articleId, text: text.value }).then(({ data }) => {
|
||||||
console.log(data);
|
emit('updateList', data);
|
||||||
// TODO: 댓글 추가 후 처리
|
|
||||||
});
|
});
|
||||||
text.value = '';
|
text.value = '';
|
||||||
isActive.value = false;
|
isActive.value = false;
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import TextButton from '../common/TextButton.vue';
|
import TextButton from '../common/TextButton.vue';
|
||||||
|
import { deleteComment } from '@/api/comment';
|
||||||
|
|
||||||
const { comment } = defineProps({ comment: Object });
|
const { comment, userId } = defineProps({ comment: Object, userId: String });
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
console.log('delete', comment.id);
|
deleteComment(comment.id).then(() => {
|
||||||
// TODO: add api call
|
isDeleted.value = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -18,7 +20,7 @@ function handleDelete() {
|
|||||||
<div class="nickname">{{ comment.nickname }}</div>
|
<div class="nickname">{{ comment.nickname }}</div>
|
||||||
<div class="date">{{ comment.date }}</div>
|
<div class="date">{{ comment.date }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!comment.isAuthor" class="control">
|
<div v-if="comment.authorId === userId" class="control">
|
||||||
<TextButton @click="handleDelete">삭제</TextButton>
|
<TextButton @click="handleDelete">삭제</TextButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import CommentItem from './CommentItem.vue';
|
import CommentItem from './CommentItem.vue';
|
||||||
|
|
||||||
const { comments } = defineProps({ comments: Array });
|
const { comments, userId } = defineProps({ comments: Array, userId: String });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ul>
|
<ul>
|
||||||
<CommentItem v-for="comment in comments" :key="comment.id" :comment="comment" />
|
<CommentItem
|
||||||
|
v-for="comment in comments"
|
||||||
|
:key="comment.id"
|
||||||
|
:comment="comment"
|
||||||
|
:userId="userId"
|
||||||
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -16,13 +16,20 @@ onMounted(() => {
|
|||||||
};
|
};
|
||||||
const map = new kakao.maps.Map(mapDiv.value, options);
|
const map = new kakao.maps.Map(mapDiv.value, options);
|
||||||
const markers = computed(() =>
|
const markers = computed(() =>
|
||||||
attractionList.value.map(
|
attractionList.value.map((attraction) => {
|
||||||
(attraction) =>
|
const position = new kakao.maps.LatLng(attraction.latitude, attraction.longitude);
|
||||||
new kakao.maps.Marker({
|
|
||||||
position: new kakao.maps.LatLng(attraction.latitude, attraction.longitude),
|
return new kakao.maps.Marker({ position });
|
||||||
})
|
})
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
const overlays = computed(() => {
|
||||||
|
return attractionList.value.map((attraction) => {
|
||||||
|
const position = new kakao.maps.LatLng(attraction.latitude, attraction.longitude);
|
||||||
|
const content = `<div class="map-overlay">${attraction.title}</div>`;
|
||||||
|
|
||||||
|
return new kakao.maps.CustomOverlay({ position, content });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
watch(markers, (newMarkers, oldMarkers) => {
|
watch(markers, (newMarkers, oldMarkers) => {
|
||||||
oldMarkers.forEach((marker) => marker.setMap(null));
|
oldMarkers.forEach((marker) => marker.setMap(null));
|
||||||
@ -32,6 +39,10 @@ onMounted(() => {
|
|||||||
setTarget(newMarkers[0].getPosition());
|
setTarget(newMarkers[0].getPosition());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
watch(overlays, (newOverlays, oldOverlays) => {
|
||||||
|
oldOverlays.forEach((overlay) => overlay.setMap(null));
|
||||||
|
newOverlays.forEach((overlay) => overlay.setMap(map));
|
||||||
|
});
|
||||||
|
|
||||||
watch(target, (newTarget) => {
|
watch(target, (newTarget) => {
|
||||||
map.panTo(newTarget);
|
map.panTo(newTarget);
|
||||||
@ -49,3 +60,16 @@ onMounted(() => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.map-overlay {
|
||||||
|
font-size: 12px;
|
||||||
|
transform: translateY(8px);
|
||||||
|
color: black;
|
||||||
|
text-shadow:
|
||||||
|
white -1px 0px,
|
||||||
|
white 0px 1px,
|
||||||
|
white 1px 0px,
|
||||||
|
white 0px -1px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, watch } from 'vue';
|
import { inject, reactive, ref, watch } from 'vue';
|
||||||
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';
|
import SearchBox from '../common/SearchBox.vue';
|
||||||
import { getGugun, getSido } from '@/api/area';
|
import { getGugun, getSido } from '@/api/area';
|
||||||
|
|
||||||
|
const { initSido, clearInitSido } = inject('initSido');
|
||||||
|
const { initGugun, clearInitGugun } = inject('initGugun');
|
||||||
const contentTypeList = reactive([
|
const contentTypeList = reactive([
|
||||||
{ value: 0, name: '전체' },
|
{ value: 0, name: '전체' },
|
||||||
{ value: 12, name: '관광지' },
|
{ value: 12, name: '관광지' },
|
||||||
@ -31,11 +33,24 @@ watch(sido, ({ sidoCode }) => {
|
|||||||
}
|
}
|
||||||
getGugun(sidoCode).then(({ data }) => {
|
getGugun(sidoCode).then(({ data }) => {
|
||||||
gugunList.value = [{ gugunCode: 0, gugunName: '전체' }, ...data];
|
gugunList.value = [{ gugunCode: 0, gugunName: '전체' }, ...data];
|
||||||
|
if (initGugun !== null) {
|
||||||
|
gugun.value = gugunList.value.find((gugun) => gugun.gugunCode === Number(initGugun)) ?? null;
|
||||||
|
clearInitGugun();
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
|
if (initSido !== null) {
|
||||||
|
clearInitSido();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (sidoList.value.length === 0) {
|
if (sidoList.value.length === 0) {
|
||||||
getSido().then(({ data }) => (sidoList.value = [{ sidoCode: 0, sidoName: '전체' }, ...data]));
|
getSido().then(({ data }) => {
|
||||||
|
sidoList.value = [{ sidoCode: 0, sidoName: '전체' }, ...data];
|
||||||
|
if (initSido !== null) {
|
||||||
|
sido.value = sidoList.value.find((sido) => sido.sidoCode === Number(initSido)) ?? null;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
@ -59,7 +74,7 @@ function handleSubmit() {
|
|||||||
placeholder="시/군/구"
|
placeholder="시/군/구"
|
||||||
v-model="gugun"
|
v-model="gugun"
|
||||||
:options="gugunList"
|
:options="gugunList"
|
||||||
:disabled="gugunList.length <= 1"
|
:disabled="(sido?.sidoCode ?? 0) === 0 || gugunList.length <= 1"
|
||||||
optionName="gugunName"
|
optionName="gugunName"
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
|
@ -59,14 +59,9 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/search',
|
path: '/search',
|
||||||
component: () => import('@/views/SearchView.vue'),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'search',
|
name: 'search',
|
||||||
component: () => import('@/components/common/KakaoMap.vue'),
|
component: () => import('@/views/SearchView.vue'),
|
||||||
},
|
props: true,
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -9,6 +9,7 @@ export const useMemberStore = defineStore('memberStore', () => {
|
|||||||
const accessToken = ref(localStorage.getItem('accessToken'));
|
const accessToken = ref(localStorage.getItem('accessToken'));
|
||||||
const isLogin = computed(() => accessToken.value !== null);
|
const isLogin = computed(() => accessToken.value !== null);
|
||||||
const userId = computed(() => {
|
const userId = computed(() => {
|
||||||
|
if (!isLogin.value) return null;
|
||||||
return jwtDecode(accessToken.value).userId;
|
return jwtDecode(accessToken.value).userId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import AppHeader from '@/components/AppHeader.vue';
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
main {
|
main {
|
||||||
|
width: 100%;
|
||||||
max-width: 960px;
|
max-width: 960px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin: 80px auto 0;
|
margin: 80px auto 0;
|
||||||
|
@ -16,7 +16,7 @@ import { RouterLink } from 'vue-router';
|
|||||||
<h1>여행지를 골라보세요</h1>
|
<h1>여행지를 골라보세요</h1>
|
||||||
<ul class="card-list">
|
<ul class="card-list">
|
||||||
<li>
|
<li>
|
||||||
<RouterLink :to="{ name: 'home' }" class="card">
|
<RouterLink :to="{ name: 'search', query: { sidoCode: 1 } }" class="card">
|
||||||
<img src="/img/Seoul.jpg" alt="seoul" class="bg" />
|
<img src="/img/Seoul.jpg" alt="seoul" class="bg" />
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<h2>서울</h2>
|
<h2>서울</h2>
|
||||||
@ -24,7 +24,7 @@ import { RouterLink } from 'vue-router';
|
|||||||
</RouterLink>
|
</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<RouterLink :to="{ name: 'home' }" class="card">
|
<RouterLink :to="{ name: 'search', query: { sidoCode: 6 } }" class="card">
|
||||||
<img src="/img/Busan.jpg" alt="busan" class="bg" />
|
<img src="/img/Busan.jpg" alt="busan" class="bg" />
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<h2>부산</h2>
|
<h2>부산</h2>
|
||||||
@ -32,7 +32,7 @@ import { RouterLink } from 'vue-router';
|
|||||||
</RouterLink>
|
</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<RouterLink :to="{ name: 'home' }" class="card">
|
<RouterLink :to="{ name: 'search', query: { sidoCode: 39 } }" class="card">
|
||||||
<img src="/img/Jeju.jpg" alt="jeju" class="bg" />
|
<img src="/img/Jeju.jpg" alt="jeju" class="bg" />
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<h2>제주</h2>
|
<h2>제주</h2>
|
||||||
@ -40,7 +40,7 @@ import { RouterLink } from 'vue-router';
|
|||||||
</RouterLink>
|
</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<RouterLink :to="{ name: 'home' }" class="card">
|
<RouterLink :to="{ name: 'search', query: { sidoCode: 35, gugunCode: 2 } }" class="card">
|
||||||
<img src="/img/Gyeongju.jpg" alt="seoul" class="bg" />
|
<img src="/img/Gyeongju.jpg" alt="seoul" class="bg" />
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<h2>경주</h2>
|
<h2>경주</h2>
|
||||||
@ -54,6 +54,7 @@ import { RouterLink } from 'vue-router';
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
main {
|
main {
|
||||||
|
width: 100%;
|
||||||
margin: 80px auto 0;
|
margin: 80px auto 0;
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
|
@ -3,6 +3,28 @@ import AppHeader from '@/components/AppHeader.vue';
|
|||||||
import ChatBotButton from '@/components/chatbot/ChatBotButton.vue';
|
import ChatBotButton from '@/components/chatbot/ChatBotButton.vue';
|
||||||
import KakaoMap from '@/components/common/KakaoMap.vue';
|
import KakaoMap from '@/components/common/KakaoMap.vue';
|
||||||
import SideBar from '@/components/search/SideBar.vue';
|
import SideBar from '@/components/search/SideBar.vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { provide, ref } from 'vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const initSido = ref(route.query.sidoCode);
|
||||||
|
const initGugun = ref(route.query.gugunCode);
|
||||||
|
|
||||||
|
router.replace({ query: {} });
|
||||||
|
|
||||||
|
provide('initSido', {
|
||||||
|
initSido: initSido.value,
|
||||||
|
clearInitSido: () => {
|
||||||
|
initSido.value = null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
provide('initGugun', {
|
||||||
|
initGugun: initGugun.value,
|
||||||
|
clearInitGugun: () => {
|
||||||
|
initGugun.value = null;
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
Loading…
Reference in New Issue
Block a user