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" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<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.gstatic.com" crossorigin />
|
||||
<link
|
||||
|
@ -1,6 +1,9 @@
|
||||
@import './base.css';
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
margin: 0 auto;
|
||||
font-weight: normal;
|
||||
|
@ -1,18 +1,20 @@
|
||||
<script setup>
|
||||
import { getArticles } from '@/api/article';
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import FilledButton from '../common/FilledButton.vue';
|
||||
import { useMemberStore } from '@/stores/memberStore';
|
||||
|
||||
const articles = ref([]);
|
||||
const params = reactive({
|
||||
pageNo: 1,
|
||||
key: 'all',
|
||||
word: '',
|
||||
const param = computed(() => {
|
||||
return {
|
||||
pageNo: currentPage.value,
|
||||
};
|
||||
});
|
||||
const currentPage = ref(1);
|
||||
const hasNextPage = ref(true);
|
||||
|
||||
const lastElement = ref(null);
|
||||
const memberStore = useMemberStore();
|
||||
|
||||
watch(lastElement, (el) => {
|
||||
if (!el) {
|
||||
@ -20,8 +22,8 @@ watch(lastElement, (el) => {
|
||||
}
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && hasNextPage.value) {
|
||||
params.pageNo += 1;
|
||||
if (entry.isIntersecting && articles.value.length && hasNextPage.value) {
|
||||
currentPage.value += 1;
|
||||
searchList();
|
||||
}
|
||||
},
|
||||
@ -32,8 +34,8 @@ watch(lastElement, (el) => {
|
||||
});
|
||||
|
||||
function searchList() {
|
||||
getArticles(params).then(({ data }) => {
|
||||
if ((data?.articles?.length ?? 0) === 0) {
|
||||
getArticles(param.value).then(({ data }) => {
|
||||
if (data?.page?.total === currentPage.value || (data?.articles?.length ?? 0) === 0) {
|
||||
hasNextPage.value = false;
|
||||
return;
|
||||
}
|
||||
@ -46,7 +48,7 @@ searchList();
|
||||
<template>
|
||||
<header>
|
||||
<h1>게시판</h1>
|
||||
<RouterLink :to="{ name: 'article-create' }">
|
||||
<RouterLink v-if="memberStore.isLogin" :to="{ name: 'article-create' }">
|
||||
<FilledButton primary class="write">글쓰기</FilledButton>
|
||||
</RouterLink>
|
||||
</header>
|
||||
|
@ -2,9 +2,16 @@
|
||||
import { useMemberStore } from '@/stores/memberStore';
|
||||
import CommentForm from './CommentForm.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();
|
||||
|
||||
function updateList(comment) {
|
||||
comments.value.push(comment);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -12,8 +19,8 @@ const memberStore = useMemberStore();
|
||||
<h2>
|
||||
댓글 <span>{{ comments.length }}</span>
|
||||
</h2>
|
||||
<CommentForm :article-id="articleId" v-if="memberStore.isLogin" />
|
||||
<CommentList :comments="comments" />
|
||||
<CommentForm @updateList="updateList" :article-id="articleId" v-if="memberStore.isLogin" />
|
||||
<CommentList :comments="comments" :userId="memberStore.userId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -4,7 +4,8 @@ import FilledButton from '../common/FilledButton.vue';
|
||||
import TextButton from '../common/TextButton.vue';
|
||||
import { addComment } from '@/api/comment';
|
||||
|
||||
const { id } = defineProps({ id: Number });
|
||||
const emit = defineEmits(['updateList']);
|
||||
const { articleId } = defineProps({ articleId: Number });
|
||||
const isActive = ref(false);
|
||||
const textDiv = ref(null);
|
||||
const text = ref('');
|
||||
@ -19,9 +20,8 @@ function handleCancel() {
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
addComment({ id, text: text.value }).then(({ data }) => {
|
||||
console.log(data);
|
||||
// TODO: 댓글 추가 후 처리
|
||||
addComment({ articleId, text: text.value }).then(({ data }) => {
|
||||
emit('updateList', data);
|
||||
});
|
||||
text.value = '';
|
||||
isActive.value = false;
|
||||
|
@ -1,13 +1,15 @@
|
||||
<script setup>
|
||||
import { ref } from '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);
|
||||
|
||||
function handleDelete() {
|
||||
console.log('delete', comment.id);
|
||||
// TODO: add api call
|
||||
deleteComment(comment.id).then(() => {
|
||||
isDeleted.value = true;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -18,7 +20,7 @@ function handleDelete() {
|
||||
<div class="nickname">{{ comment.nickname }}</div>
|
||||
<div class="date">{{ comment.date }}</div>
|
||||
</div>
|
||||
<div v-if="!comment.isAuthor" class="control">
|
||||
<div v-if="comment.authorId === userId" class="control">
|
||||
<TextButton @click="handleDelete">삭제</TextButton>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,12 +1,17 @@
|
||||
<script setup>
|
||||
import CommentItem from './CommentItem.vue';
|
||||
|
||||
const { comments } = defineProps({ comments: Array });
|
||||
const { comments, userId } = defineProps({ comments: Array, userId: String });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
@ -16,13 +16,20 @@ onMounted(() => {
|
||||
};
|
||||
const map = new kakao.maps.Map(mapDiv.value, options);
|
||||
const markers = computed(() =>
|
||||
attractionList.value.map(
|
||||
(attraction) =>
|
||||
new kakao.maps.Marker({
|
||||
position: new kakao.maps.LatLng(attraction.latitude, attraction.longitude),
|
||||
attractionList.value.map((attraction) => {
|
||||
const 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) => {
|
||||
oldMarkers.forEach((marker) => marker.setMap(null));
|
||||
@ -32,6 +39,10 @@ onMounted(() => {
|
||||
setTarget(newMarkers[0].getPosition());
|
||||
}
|
||||
});
|
||||
watch(overlays, (newOverlays, oldOverlays) => {
|
||||
oldOverlays.forEach((overlay) => overlay.setMap(null));
|
||||
newOverlays.forEach((overlay) => overlay.setMap(map));
|
||||
});
|
||||
|
||||
watch(target, (newTarget) => {
|
||||
map.panTo(newTarget);
|
||||
@ -49,3 +60,16 @@ onMounted(() => {
|
||||
height: 100%;
|
||||
}
|
||||
</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>
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { inject, reactive, ref, watch } from 'vue';
|
||||
import Select from '../common/Select.vue';
|
||||
import { useAttractionStore } from '@/stores/attractionStore';
|
||||
import SearchBox from '../common/SearchBox.vue';
|
||||
import { getGugun, getSido } from '@/api/area';
|
||||
|
||||
const { initSido, clearInitSido } = inject('initSido');
|
||||
const { initGugun, clearInitGugun } = inject('initGugun');
|
||||
const contentTypeList = reactive([
|
||||
{ value: 0, name: '전체' },
|
||||
{ value: 12, name: '관광지' },
|
||||
@ -31,11 +33,24 @@ watch(sido, ({ sidoCode }) => {
|
||||
}
|
||||
getGugun(sidoCode).then(({ 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) {
|
||||
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() {
|
||||
@ -59,7 +74,7 @@ function handleSubmit() {
|
||||
placeholder="시/군/구"
|
||||
v-model="gugun"
|
||||
:options="gugunList"
|
||||
:disabled="gugunList.length <= 1"
|
||||
:disabled="(sido?.sidoCode ?? 0) === 0 || gugunList.length <= 1"
|
||||
optionName="gugunName"
|
||||
/>
|
||||
<Select
|
||||
|
@ -59,14 +59,9 @@ const router = createRouter({
|
||||
},
|
||||
{
|
||||
path: '/search',
|
||||
component: () => import('@/views/SearchView.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
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 isLogin = computed(() => accessToken.value !== null);
|
||||
const userId = computed(() => {
|
||||
if (!isLogin.value) return null;
|
||||
return jwtDecode(accessToken.value).userId;
|
||||
});
|
||||
|
||||
|
@ -12,6 +12,7 @@ import AppHeader from '@/components/AppHeader.vue';
|
||||
|
||||
<style scoped>
|
||||
main {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
padding: 20px;
|
||||
margin: 80px auto 0;
|
||||
|
@ -16,7 +16,7 @@ import { RouterLink } from 'vue-router';
|
||||
<h1>여행지를 골라보세요</h1>
|
||||
<ul class="card-list">
|
||||
<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" />
|
||||
<div class="info">
|
||||
<h2>서울</h2>
|
||||
@ -24,7 +24,7 @@ import { RouterLink } from 'vue-router';
|
||||
</RouterLink>
|
||||
</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" />
|
||||
<div class="info">
|
||||
<h2>부산</h2>
|
||||
@ -32,7 +32,7 @@ import { RouterLink } from 'vue-router';
|
||||
</RouterLink>
|
||||
</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" />
|
||||
<div class="info">
|
||||
<h2>제주</h2>
|
||||
@ -40,7 +40,7 @@ import { RouterLink } from 'vue-router';
|
||||
</RouterLink>
|
||||
</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" />
|
||||
<div class="info">
|
||||
<h2>경주</h2>
|
||||
@ -54,6 +54,7 @@ import { RouterLink } from 'vue-router';
|
||||
|
||||
<style scoped>
|
||||
main {
|
||||
width: 100%;
|
||||
margin: 80px auto 0;
|
||||
}
|
||||
section {
|
||||
|
@ -3,6 +3,28 @@ import AppHeader from '@/components/AppHeader.vue';
|
||||
import ChatBotButton from '@/components/chatbot/ChatBotButton.vue';
|
||||
import KakaoMap from '@/components/common/KakaoMap.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>
|
||||
|
||||
<template>
|
||||
|
Loading…
Reference in New Issue
Block a user