- /PROJECT
└─ /public
└─ favicon.ico
└─ index.html
└─ /src
└─ /api
└─ /components
└─ 기능별 폴더
└─ layout (혹은 common)
└─ /router
└─ index.js
└─ /store
└─ /modules
└─ index.js
└─ /views
└─ App.vue
└─ main.js
axios를 import하여 백엔드 디폴트 주소를 설정하여 원하는 api로 만들어 사용
import axios from "axios";
// axios 객체 생성
const http = axios.create({
baseURL: "백엔드 default 주소",
// "https://cors-anywhere.herokuapp.com/http://i7a801.p.ssafy.io:8080/api": cors 이슈로 proxy서버를 사용할 때 앞에 붙여 사용
headers: {
"Content-type": "application/json",
},
});
export default http;
//http.interceptors.response.use( ~~~ );
// jwt 헤더를 포함시킨 store 예시 샘플
import http from "@/api/http";
export const rankingStore = {
state: {
data: null,
},
getters: {
getData(state) {
return state.data;
},
},
mutations: {
SET_DATA(state, data) {
state.data = data;
},
},
actions: {
async getData({ commit, getters }, param) {
await http
.get("ranking/til/" + param, {
headers: getters.authHeader,
})
.then(({ data }) => {
console.log("데이터 받아오기 성공");
commit("SET_DATA", data);
})
.catch((e) => {
console.error(e);
});
},
},
modules: {},
};
<template>
<router-view></router-view>
</template>
<script>
export default {
name: "ProfileView",
components: {},
};
</script>
<style></style>
import { createRouter, createWebHashHistory } from "vue-router";
import MainView from "../views/MainView.vue";
const routes = [
{
path: "/",
name: "mainview",
component: MainView,
},
{
path: "/til",
name: "TilView",
component: () => import("@/views/TilView.vue"),
children: [
{
path: "create",
name: "TilCreate",
component: () => import("@/components/til/TilCreate.vue"),
},
{
path: ":tilPk/edit",
name: "TilUpdate",
component: () => import("@/components/til/TilUpdate.vue"),
},
{
path: "list/my/:userId",
name: "TilList",
component: () => import("@/components/til/TilListLayout.vue"),
},
],
},
];
//여기서 히스토리, 해시모드를 지정 가능
const router = createRouter({
history: createWebHashHistory(),
routes,
});
// 네비게이션 가드(ex. 로그인을 안할 시, 모든 페이지는 로그인 페이지로 이동됨)
import store from "@/store";
let profileBool = false;
router.beforeEach((to, from, next) => {
console.log("#to ", to);
console.log("#from ", from);
console.log("#from FullPath ", from.fullPath);
console.log("#to FullPath ", to.fullPath);
if (
to.name != "SignIn" &&
to.name != "SignUp" &&
to.name != "PassWordSeek" &&
!store.getters.isLoggedIn
)
next({ name: "SignIn" });
else if (
from.name == "profile" &&
to.name == "profile" &&
from.fullPath != to.fullPath &&
!profileBool
) {
console.log("라우터 변경 감지");
next();
} else next();
});
<script>
import { computed } from "vue";
import { useStore } from "vuex";
import { reactive, refs } from "vue";
export default {
name: "TestComponent",
setup() {
const store = useStore(); //store 사용
const getters = computed(() => store.getters); //getters 불러오기
const state = reactive({ // reactive로 변수 선언
title: null,
personnel: null,
hour: null,
min: null,
content: null,
});
const name = refs("김진회"); //refs로 변수 선언
const month = refs(4);
function updateState() {
store.commit("SET_STORE_STATE", 10); // mutations함수로 state값 바꾸기 (수정) [commit]
}
function callState() {
console.log(getters.value.getIsCompeteStarted); //setup()내에서 computed한 값을 사용할 때는 value 붙이기 (state 호출)
console.log(state.content); // 컴포넌트의 변수 호출
}
}
function callActions() {
store.dispatch("testAction", 10); //Action함수에 파라미터로 10값 넘기기 (actions 호출) [dispatch]
}
return { store, getters, updateState, callState, callActions, state, name, month }; //setup에서 선언한 변수와 함수는 전부 return해주기
},
components: { },
};
</script>
vue3에서 컴포넌트안에서의 변수 선언은 크게 reactive와 refs가 있다.
각각 장단점이 있으며 보통 원시타입을 지정해서 쓸 경우 refs를 사용하고, 원시타입을 지정하지 않거나 객체로 사용하는 경우 reactive를 사용한다. (출처 StackOverflow)
reactive를 ref처럼 사용하는 방법으로 toRef, toRefs 등도 있음
부모의 값을 자식한테 전달할 때 사용
<coop-item v-for="(room, i) in getters.getRooms" :room="room" :key="i" />
(스크립트) props: ["room"], (템플릿) <div>{{ room.title }}</div>
(스크립트) props: {room:Object}, (템플릿) <div>{{ room.title }}</div>
자식의 값을 부모한테 전달할 때 사용
//template
<PeopleItem :person="person" @setModal="setModal" />
...
//script
setup() {
function setModal(data) {
state.emitValue = data;
}
return { setModal }
}
//template
<button class="모달창키는버튼" @click="setModal">모달창</button>
setup(props, { emit }) {
function setModal() {
emit("setModal", props.person);
}
*emit은 이 방식말고도 약간 다른 방식도 있으니 필요 시 찾아볼 것본인은 부트스트랩의 modal을 사용
data-bs-target을 적용할 모달 id와 동일하게 작성할 것.
list에서 각각의 item을 클릭 시 해당 모달창이 뜨는 형식을 예로 들면 (뉴스피드, 게임방 등)
그냥 모달창을 선언하고 사용하면 item이 전부 생성되고 모달창에 들어갈 값이 하나의 item값만 들어가기 때문에 내가 클릭한 item의 값만 모달창에 적용되도록 해야 함
ex) item을 클릭 시 모달창이 나오고 모달창에는 해당 유저에게 dm보내기가 출력
//부모
<template>
<h2>부모내용생략</h2>
<span v-for="(projectPerson, i) in getters.getProjectPeople" :key="i">
<PeopleItem :projectPerson="projectPerson" @setModal="setModal" />
</span>
<!-- 모달 -->
<div
v-if="emitPerson != null"
class="modal fade"
id="personDetailInfo"
tabindex="-1"
aria-labelledby="personDetailInfoLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<!-- 모달닫기버튼 -->
<div class="modal-header justify-content-space-evenly">
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<!-- emit 받은 값 사용 -->
<div class="profile-name">
{{ emitPerson.userNickname }}
</div>
<!-- DM 보내기 -->
<div
class="modal-footer justify-content-center"
data-bs-dismiss="modal"
aria-label="Close"
>
<button type="button" class="btn">
<font-awesome-icon icon="fa-solid fa-comments" />
DM 보내기
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import PeopleItem from "@/components/recruit/projectPeople/PeopleItem.vue";
import { computed, ref } from "vue";
import { useStore } from "vuex";
import router from "@/router";
export default {
name: "PeopleList",
setup(props) {
const store = useStore();
const getters = computed(() => store.getters);
// 모달창emit관련
let emitPerson = ref("");
function setModal(data) {
emitPerson.value = data;
}
return { store, getters, emitPerson, setModal };
},
components: {
PeopleItem,
},
};
//자식
<template>
<div
class="user-info flex-fill"
data-bs-toggle="modal"
data-bs-target="#personDetailInfo"
@click="setModal"
>
<h3>{{ projectPerson.userNickname }}</h3>
</div>
</template>
<script>
import { reactive, computed } from "vue";
import { useStore } from "vuex";
export default {
name: "PeopleItem",
props: ["projectPerson"],
setup(props, { emit }) {
function setModal() {
emit("setModal", props.projectPerson);
}
const store = useStore();
const getters = computed(() => store.getters);
return { state, getters, setModal };
},
components: {},
};
</script>
1분에 한 번씩 백엔드에 요청을 보낸다던지 타이머 등에 사용
주의사항: 작성 코드에 따라 의도치 않게 interval이 중복으로 실행되는 경우가 있을 수 있으니 방지해야 함.
const interval = setInterval(() => {
store.dispatch("sendTime", 1);
}, 60000);
...
clearInterval(interval);
1) 프론트(구독)
//카톡같은 채팅방 예제
function connect() {
store.dispatch("getChatRoomList", getters.value.getLoginUserId); //db에 저장된 자신의 채팅방 목록 가져오기
const roomList = getters.value.getRoomList;
const serverURL = "백엔드 주소";
let socket = new SockJS(serverURL);
this.stompClient = Stomp.over(socket);
console.log("소켓 연결을 시도합니다.");
this.stompClient.connect(
{},
() => {
// 소켓 연결 성공
console.log("소켓 연결 성공");
for (const room of roomList) {
this.stompClient.subscribe("/send/" + room.chatRoomId, (res) => { //해당 채팅방 구독
const data = JSON.parse(res.body);
if (
getters.value.getChatUserId == data.sendUserId ||
data.sendUserId == getters.value.getLoginUserId
) {
console.log("수신 메시지: ", data);
const sendData = {
userId: getters.value.getLoginUserId,
chatRoomId: room.chatRoomId,
chatId: data.chatId,
};
store.dispatch("sendRecentReadMsg", sendData);
store.commit("APPEND_RECV_LIST", data);
}
});
}
},
(error) => {
console.log("소켓 연결 실패", error);
}
);
store.commit("SET_STOMP_CLIENT", this.stompClient); // 다른 컴포넌트에서 이 stompClient를 쓰기 위해서 저장
}
2) 프론트(메세지보내기)
function send() {
console.log("송신메세지:" + state.message);
if (
getters.value.getStompClient &&
getters.value.getStompClient.connected
) {
const msg = {
chatRoomId: state.chatRoomId,
sendUserId: state.userId,
message: state.message,
};
getters.value.getStompClient.send("/receive", JSON.stringify(msg), {}); //메세지 보내기. 백엔드와 보내는 데이터 형식 맞추기
}
}
3) 백엔드(SocketController.java)
package com.cogether.api.chat.web;
import com.cogether.api.chat.domain.ChatRequest;
import com.cogether.api.chat.domain.ChatResponse;
import com.cogether.api.chat.service.ChatService;
import com.cogether.api.liveCoop.domain.LiveCoopRequest;
import com.cogether.api.liveCoop.domain.LiveCoopResponse;
import com.cogether.api.liveCoop.service.LiveCoopService;
import lombok.AllArgsConstructor;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
@AllArgsConstructor
@Controller
public class SocketController {
private final SimpMessagingTemplate template; //특정 Broker로 메세지를 전달
private final ChatService chatService;
private final LiveCoopService liveCoopService;
@MessageMapping(value = "/receive") //채팅받기
public void chat(ChatRequest.CreateChat request) {
ChatResponse.GetChat response = chatService.createChat(request);
template.convertAndSend("/send/" + request.getChatRoomId(), response); //sub한 사람들에게 보내기
}
@MessageMapping(value = "/receive/coop") //협력모드 채팅받기
public void getLiveCoop(LiveCoopRequest.SocketLiveCoop request) {
LiveCoopResponse.SocketLiveCoop response = liveCoopService.socketLiveCoop(request);
template.convertAndSend("/send/coop/" + request.getLiveCoopId(), response); //협력모드 채팅보내기
}
}