저번 포스팅에서 교내 시설 로드뷰를 만들기 위한 사전 준비를 모두 마쳤다.
먼저 로드뷰에 사용될 사진을 촬영하여야 한다.
학교에 인스타 360 카메라가 있어서 남는 시간에 학교 곳곳을 촬영하였다.
촬영한 이미지는 insp라는 확장자로 나오는데 인스타 360 스튜디오를 통해서 한번 가공 후 jpg로 변환하여야 한다.
위와 같이 인스타 360 스튜디오로 가져왔다면 우측 상단에 내보내기 버튼이 존재한다.
이 버튼을 클릭해서 jpg 파일로 내보내면 된다.
이제부터 코딩을 시작할 것이다.
이번 코딩은 그닥 어렵지 않다.
Vue.js를 통해서 제작할 것이기 때문에 Vue.js 프로젝트를 만든다.
새로운 폴더를 만들어 VSCODE로 열고 터미널을 열어 아래와 같이 입력한다.
npm create vue@latest
위 명령어는 Vue.js 프로젝트를 빠르게 만들기 위한 실행 명령어다.
명령어를 입력하면 프로젝트 구성에 필요한 것들을 물어보는데 아래와 같이 답하면 된다.
✔ Project name: roadview
✔ Add TypeScript? No
✔ Add JSX Support? No
✔ Add Vue Router for Single Page Application development? Yes
✔ Add Pinia for state management? No
✔ Add Vitest for Unit testing? No
✔ Add an End-to-End Testing Solution? No
✔ Add ESLint for code quality? No
✔ Add Prettier for code formatting? No
✔ Add Vue DevTools 7 extension for debugging? No
Scaffolding project in ./roadview
Done.
대부분 옵션을 No로 답하였는데 네 번째 Vue Router는 필요하기에 Yes로 답변하여야 한다.
Vue Router는 React, Angular, Vue 등의 SPA 프레임워크에서 페이지 이동을 위한 프레임워크라고 생각하면 된다.
360도 이미지를 움직일 수 있도록 하고 이미지 위에 플로팅 객체 등을 넣을 수 있는 VIEW360 모듈을 설치한다.
네이버에서 만든 프로젝트로 보인다.
아래 명령어를 통해 모듈을 설치할 수 있다.
npm install @egjs/vue3-view360@next
View360은 현재 베타 버전으로 계속 개발되고 있으며 최신 버전을 사용하기 위해 @next를 붙였다.
설치를 완료하였다면 해당 모듈 사용을 Vue 프로젝트에 명시하여야 한다.
Vue 프로젝트의 src/main.js 파일을 열어보자
(스타일 파일은 모두 삭제하였다)
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import View360 from'@egjs/vue3-view360' // View360 가져오기
import '@egjs/vue3-view360/css/view360.min.css' // View360 스타일 가져오기
const app = createApp(App)
app.use(router)
app.use(View360) // View360 사용 명시
app.mount('#app')
위 코드와 같이 작성하면 이제 모든 준비가 마무리 되었다.
프로젝트 자동 완성을 통해서 불필요한 HomeView.vue와 AboutView.vue 등의 파일이 많이 생성되었다.
불필요한 파일을 다 지우고 다음과 같이 구성하였다.
components/spots 폴더는 교내 시설 각각의 지점에 대한 컴포넌츠를 넣을 것이고 상위 폴더의 Header와 MapViewPage는 상단 고정 메뉴와 하단 고정 지도를 표시해줄 것이다.
views 폴더에는 HomeView와 RoadViewPage를 만들었다. HomeView는 현재는 사용하지 않지만 추후 피드백을 통해서 소개 페이지가 필요하다고 느끼면 활용할 예정이다. RoadViewPage는 실제로 로드뷰가 나오는, View360 컴포넌츠를 사용할 파일이다.
app.vue는 vue에서 화면을 표현하는 최상위 파일이다.
이 파일에는 상단 고정 메뉴인 Header와 라우터 페이지를 보여주기 위한 RouterView만 넣으면 된다.
<script setup>
import { RouterLink, RouterView } from 'vue-router'
import Header from './components/Header.vue'
</script>
<template>
<Header/>
<RouterView />
</template>
<style scoped>
</style>
RoadViewPage.vue에서는 실제 로드뷰 화면을 보여줄 것이다.
코드는 주석을 통해 설명하였으며 사전 지식은 라우터의 파라미터를 통해서 현재의 위치와 층수를 가져오고 기존 링크에 해당 파라미터가 없다면 기본 설정으로 로드하도록 제작하였다.
<script setup>
import { onBeforeMount, onMounted, ref, shallowRef, watch } from 'vue';
import Lobby from '@/components/spots/Lobby.vue'; // 중앙현관 컴포넌츠 가져오기
import MapViewPage from '@/components/MapViewPage.vue'; // 하단 고정 지도 가져오기
import { useRoute, useRouter } from 'vue-router'; // 라우터에서 파라미터와 페이지 이동 함수를 위함.
const route = useRoute()
const router = useRouter()
const views = { 'lobby': Lobby } // 파라미터의 값과 컴포넌츠 연동 추후 다양하게 추가
const pos = ref("lobby") // 기본 위치
const currentView = shallowRef(views[pos.value]) // 컴포넌츠는 shallowRef를 통해서 동적 컴포넌츠 전환 사용, views 객체에 있는 값들중 pos의 값과 일치하는 키를 가져와서 currentView에 값을 넣음
onMounted(() => {
if (route.query.pos == null || route.query.floor == null) {
router.push("/roadview?pos=lobby&floor=0") // 링크에 파라미터 존재하지 않으면 기본 페이지 이동
} else {
pos.value = route.query.pos // 현재 위치를 파라미터를 통해 저장
currentView.value = views[pos.value] // 현재 화면을 views에 pos값을 통해서 키 매칭으로 저장 -> 동적 변환을 위해 추후 watch 함수로 전환 예정
}
})
</script>
<template>
<div style="position: fixed; z-index: 10000; bottom: 0px; margin-bottom: 10px; margin-left: 10px;">
<MapViewPage />
</div>
<div>
<div style="width: 100%; height: 100%;">
<component :is="currentView" style="" /> // 동적 컴포넌츠
</div>
</div>
</template>
<style>
.view360-canvas {
outline: 0;
border-radius: 0px;
}
.viewer {
width: 100%;
height: 100vh;
margin-left: auto;
margin-right: auto;
}
.spot {
background-color: white;
padding: 10px;
cursor: pointer;
border-radius: 20px;
transition: 0.5s;
}
.spot:hover {
transition: 0.5s;
transform: scale(1.2);
background-color: rgb(201, 201, 201);
padding: 10px;
cursor: pointer;
border-radius: 20px;
}
</style>
스타일은 이후 spots 폴더의 파일을 살펴보면서 이해할 수 있다.
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import RoadView from '../views/RoadViewPage.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/roadview',
component:RoadView
}
]
})
export default router
라우터 파일은 별거 없다.
이 파일은 로드뷰 하단에 지도를 표시하는 컴포넌츠이다.
<template>
<TransitionGroup name="list" > <!-- 효과를 주기 위한 트랜지션 그룹-->
<div class="map-icon" v-if="!isOpen" @click="isOpen = !isOpen"> <!-- 최소화 하였을 때 표시될 아이콘 -->
<img src="../assets/mapicon.svg" style="width: 30px; margin-top: 5px;"/>
</div>
<div style="padding: 10px; background-color: white; border-radius: 10px;" v-if="isOpen">
<div style="display: flex;margin-bottom: 10px;">
<div>{{floor}}층 지도</div> <!-- URL의 파라미터를 통해 동적으로 변동 -->
<div @click="isOpen = !isOpen" style="margin-left: auto; cursor: pointer;">
닫기
</div>
</div>
<img :src="mapImage" style="width: 35vw;">
</div>
</TransitionGroup >
</template>
<script setup>
import { onBeforeMount, ref } from 'vue';
import { useRoute } from 'vue-router';
import firstFloor from '../assets/floor.png' // 1층 지도 사진
const mapImages = [firstFloor] // 층별 지도 사진 배열
const mapImage = ref() // 현재 지도사진
const isOpen = ref(false) // 최소화 여부
const floor = ref(0) // 층수
const route = useRoute()
onBeforeMount(()=>{
floor.value = parseInt(route.query.floor)+1 // URL의 파라미터를 통해 층수를 가져와 1을 더한다.
mapImage.value = mapImages[floor.value-1] // 층수에 1을 뺀 값의 순서의 배열을 가져온다.
})
</script>
<style>
#lobby{
margin-left: 100px;
}
.dot{
position: absolute; width: 12px; height: 12px; background-color: #0362fc; border-radius: 100px;
}
.list-move, /* apply transition to moving elements */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from{
opacity: 0;
transform: translateY(-30px);
}
.list-leave-to {
opacity: 0;
transform: translateY(-30px);
}
.list-leave-active {
position: absolute;
}
.map-icon{
border-radius: 100px;
padding: 10px;
width: 40px;
height: 40px;
text-align: center;
vertical-align: middle;
transition: 0.5s;
background-color: white;
cursor: pointer;
}
.map-icon:hover{
background-color: rgb(194, 194, 194);
transition: 0.5s;
}
</style>
이 파일은 상단에 고정되어 표시될 헤더 파일이다.
<template>
<div style=" position: absolute; z-index: 1000; width: 100%; ">
<div
style="padding-top: 10px; padding-bottom: 10px; background-color: rgba(255, 255, 255,50%); width: 100%;">
<div style="position: absolute; left: 50%; transform: translate(-50%);">
{{ placeName }}
</div>
<div style="margin-left: 10px;">
대영고등학교 교내 로드뷰
</div>
</div>
</div>
</template>
<script setup>
import { onBeforeMount, ref } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute()
const placeName = ref("")
const places = {
'lobby':"중앙현관",
}
onBeforeMount(()=>{
placeName.value = places[route.query.pos]
})
</script>
지금까지의 코드를 이해하였다면 위의 코드는 쉽게 이해할 수 있을 것이다.
이 파일은 로비의 360도 이미지 파일을 표시해주는 컴포넌츠 파일이다.
이게 본 포스팅의 가장 핵심이다.
<script setup>
import { View360, EquirectProjection } from '@egjs/vue3-view360'; // View360과 이미지를 가져오는 클래스 가져오기
import { onBeforeMount, ref } from 'vue';
import img from '../../assets/lobby.jpg'
const projection = new EquirectProjection({
src: img
})
</script>
<style>
</style>
<template>
<div>
<!-- 아래와 같이 표현해야 정상적으로 표시됨 -->
<View360 fov="130" class="viewer" :projection="projection">
<div class="view360-hotspots">
<div class="view360-hotspot" data-yaw="140" data-pitch="-12">
<div class="spot">서편 복도</div>
</div>
<div class="view360-hotspot" data-yaw="60" data-pitch="-15">
<div class="spot">동편 복도</div>
</div>
</div>
</View360>
</div>
</template>
위 코드를 보면 별 다른 스타일이 없다.
왜냐하면 RoadViewPage.vue 파일에 스타일을 모두 선언해두었기 때문에 불필요한 스타일 코드는 정리하였다.
위 코드를 통해서 아래와 같은 로드뷰 서비스를 구축할 수 있다.
다음 포스팅에서는 여러층과 각 스팟에 설정된 버튼을 클릭하여 이동하는 동작까지 구현하고 마치려고 한다.