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" />
<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

View File

@ -1,6 +1,9 @@
@import './base.css';
#app {
display: flex;
flex-direction: column;
width: 100%;
height: 100vh;
margin: 0 auto;
font-weight: normal;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import AppHeader from '@/components/AppHeader.vue';
<style scoped>
main {
width: 100%;
max-width: 960px;
padding: 20px;
margin: 80px auto 0;

View File

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

View File

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