Compare commits

...

10 Commits

Author SHA1 Message Date
ad087c1eaa Feat: Add overlay window in map 2024-05-23 21:14:40 +09:00
jhyns
ab7398668a Fix: prevent active disabled select 2024-05-23 20:22:10 +09:00
c64240e509 Fix: fix article list observer 2024-05-23 17:21:24 +09:00
4825289175 Fix: Fix comment area 2024-05-23 17:12:31 +09:00
486db7b781 Fix: fix layout 2024-05-23 17:04:19 +09:00
945b3fff7d Docs: Change title 2024-05-23 17:02:43 +09:00
2c183f551c Design: Remove margin 2024-05-23 16:58:08 +09:00
875ac42329 Feat: Add home card 2024-05-23 16:51:34 +09:00
7a39717a5e Feat: Add comment delete feature 2024-05-23 15:25:59 +09:00
4a65a80b40 Feat: Add comment write 2024-05-23 15:21:39 +09:00
14 changed files with 123 additions and 45 deletions

View File

@ -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

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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,
],
}, },
], ],
}); });

View File

@ -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;
}); });

View File

@ -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;

View File

@ -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 {

View File

@ -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>