workspaces 뒤로 아이디 값이 부여되는데, 워크스페이스 페이지 클릭하면 해당 id인 워크스페이스로 들어가서 그에 맞는 상세정보가 출력되어야 한다.
routes: [
{
path: '/',
component: Home,
},
{
// 내가 가지고 있는 워크스페이스중 첫번째 목록에 존재하는 워크스페이스로 이동하라
// id는 이름을 지정한 것이다. 내장된 옵션이 아니다.
// :id 는 동적 파라미터
path: '/workspaces/:id',
component: Workspace,
},
]
// 매칭하기
<script>
import { mapStores } from 'pinia'
import { useWorkspaceStore } from '~/store/workspace'
export default {
computed: {
...mapStores(useWorkspaceStore)
},
created() {
this.$route.params.id
}
}
</script>
$route
는 페이지 정보를 의미한다.
created()
로 연결해야 해당 컴포넌트가 태어나면(created) 상세 정보 조회 ($route) 할 수 있다.
단일 워크스페이스의 상세 내용을 가져온다.
state() {
return {
workspace: {},
// 기본은 빈배열, 목록을 불러오면 갱신
workspaces: []
}
},
async readWorkspace(id) {
const res = await fetch(`URL${id}`, {
method: 'GET',
headers: {
'content-type': 'application/json',
'apikey': '',
'username': ''
},
})
const workspace = await res.json()
console.log(workspace)
this.workspace = workspace
},
// workspace.vue
<template>
<h1>Workspace!</h1>
<button @click="workspaceStore.createWorkspace">
워크스페이스 생성!
</button>
<section :key="$route.params.id">
<h1
ref="title"
contenteditable>
{{ workspaceStore.workspace.title }}
</h1>
<p
ref="content"
contenteditable>
{{ workspaceStore.workspace.content }}
</p>
</section>
<input
type="text"
@change="onInput" />
</template>
<script>
import { mapStores } from 'pinia'
import { useWorkspaceStore } from '~/store/workspace'
export default {
computed: {
...mapStores(useWorkspaceStore)
},
watch: {
// 페이지가 바뀔 때 마다 감시
$route() {
this.workspaceStore.readWorkspace(this.$route.params.id)
}
},
created() {
// 주소에 들어있을 id값을 추출해서 이 함수의 값으로 사용
this.workspaceStore.readWorkspace(this.$route.params.id)
},
methods: {
// 동시에 알아내서 두개의 요소 한번에 전송하게 로직 짜기
onInput() {
// const title = 요소.textContent (글자만 필요해서 div태그 빼려고. 엔터누르면 content영역으로 가게)
// const content = 요소.innderHTML (div 포함, 내용은 줄바꿈요소도 필요하니까 innderHTML 사용)
const title = this.$refs.title.textContent
const content = this.$refs.content.innderHTML
}
}
}
</script>
// LNB.vue
<template>
<ul>
<li
v-for="workspace in workspaceStore.workspaces"
:key="workspace.id">
<RouterLink :to="`/workspaces/${workspace.id}`">
{{ workspace.title }}
</RouterLink>
<button @click="workspaceStore.deleteWorkspace(workspace.id)">
삭제!
</button>
</li>
</ul>
</template>
워크스페이스 수정하기
워크스페이스의 내용(content)은 <div>
, <br>
태그만 허용
타이틀, 내용이 동시에 수정되는 하나의 API이다.
curl -X 'PUT' \
@param {String} parentId - 부모 워크스페이스 ID, 부모 워크스페이스를 삭제하는 경우 '-1'
@param {title} title - 워크스페이스 제목
@param {content} content - 워크스페이스 내용
@param {content} poster - 워크스페이스 대표 이미지(Base64), 이미지 삭제하는 경우 '-1'
@return {Object} - 수정된 워크스페이스 객체
수정 시 body에 담을 내용
@param {title} title - 워크스페이스 제목
@param {content} content - 워크스페이스 내용
@param {content} poster - 워크스페이스 대표 이미지(Base64)
async updateWorkspace(payload) {
const { id, title, content } = payload
await fetch(`URL${id}`, {
method: 'PUT',
headers: {
'content-type': 'application/json',
'apikey': '',
'username': ''
},
// 데이터 받아줄 body
body: JSON.stringify({
title,
content
})
})
// 제목 수정하면 목록도 바뀌어지게 갱신
this.readWorkspaces()
},
onInput이 실행되면 updateWorkspace action이 실행 되게 수정한다.
methods: {
// 동시에 알아내서 두개의 요소 한번에 전송하게 로직 짜기
onInput() {
// const title = 요소.textContent (글자만 필요해서 div태그 빼려고. 엔터누르면 content영역으로 가게)
// const content = 요소.innderHTML (div 포함, 내용은 줄바꿈요소도 필요하니까 innderHTML 사용)
const title = this.$refs.title.textContent
const content = this.$refs.content.innderHTML
// payload 매개변수가 받을 수 있게 객체형태
this.workspaceStore.updateWorkspace({
id: this.$route.params.id,
title,
content
})
}
}
템플릿도 수정
contenteditable
에서는 수정이 끝나면 블러이벤트가 발생한다.
따라서 제목, 내용 부분에 @blur="onInput"
를 입력한다.
<h1>Workspace!</h1>
<button @click="workspaceStore.createWorkspace">
워크스페이스 생성!
</button>
<section :key="$route.params.id">
<h1
ref="title"
contenteditable
@blur="onInput">
{{ workspaceStore.workspace.title }}
</h1>
<p
ref="content"
contenteditable
@blur="onInput"
v-html="workspaceStore.workspace.content">
</p>
</section>
v-html로 연결한 이유는 수정 텍스트만 보이는것이 아니라 div같은 태그들이 그대로 노출되어서 v-html로 연결한 것이다.
제목 - 엔터키 누르면 포커스가 내용으로 넘어가게하기 그리고 엔터키 줄바꿈 안되게 하기는 아래 코드 추가하면 된다.
@keydown.prevent.enter="$refs.content.focus()
템플릿 영역
placeholder="제목 없음”
placeholder="내용을 입력하세요!”
스타일 영역
<style scoped lang="scss">
/* contenteditabl 속성선택자
- empty: 요소가 가지는 content가 비워져있는지 확인
- 가상요소선택자 사용했으니 content 써야한다.
- attr() CSS 함수
- contenteditable이라는 속성을가지고있는 요소에 내용이비어져있으면
내부 앞쪽에(before) content로 placeholder라는 값을 알아내서 content로 채워라. */
[contenteditable] {
&:empty::before {
content: attr(placeholder);
color: lightgray;
}
}
</style>
근데 br
태그가 남아있으면 placeholder 가 안될수도있다. 그래서 onInput 메소드 if문 추가한다.
methods: {
// 동시에 알아내서 두개의 요소 한번에 전송하게 로직 짜기
onInput() {
// const title = 요소.textContent (글자만 필요해서 div태그 빼려고. 엔터누르면 content영역으로 가게)
// const content = 요소.innderHTML (div 포함, 내용은 줄바꿈요소도 필요하니까 innderHTML 사용)
const title = this.$refs.title.textContent
const content = this.$refs.content.innerHTML
if (!title.trim()) {
this.$refs.title.innerHTML = ''
}
if (!this.$refs.content.textContent.trim()) {
this.$refs.content.innerHTML = ''
}
// payload 매개변수가 받을 수 있게 객체형태
this.workspaceStore.updateWorkspace({
id: this.$route.params.id,
title,
content
})
}
}
}