Merge branch 'comment'
This commit is contained in:
commit
691ac86d05
@ -89,7 +89,6 @@ body {
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@ -116,4 +115,9 @@ textarea {
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--color-background);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
white-space: pre;
|
||||
}
|
||||
|
@ -3,16 +3,41 @@ import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import BoardHeader from './BoardHeader.vue';
|
||||
import { getArticle } from '@/api/article';
|
||||
import CommentList from '@/components/article/CommentList.vue';
|
||||
import CommentArea from './CommentArea.vue';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const id = Number(route.params.id);
|
||||
const article = ref({});
|
||||
const comments = ref([]);
|
||||
|
||||
getArticle(id, ({ data }) => {
|
||||
article.value = data;
|
||||
});
|
||||
|
||||
// FIXME: remove mock data
|
||||
setTimeout(() => {
|
||||
article.value = {
|
||||
title: '제목',
|
||||
text: '내용',
|
||||
author: '작성자',
|
||||
date: '2024-04-11 13:12:14',
|
||||
};
|
||||
comments.value = [
|
||||
{
|
||||
id: 1,
|
||||
nickname: '닉네임',
|
||||
date: '2024-04-11 13:12:14',
|
||||
text: '댓글 내용',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
nickname: '닉네임',
|
||||
date: '2024-04-11 13:12:14',
|
||||
text: '댓글 내용',
|
||||
},
|
||||
];
|
||||
}, 100);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -26,8 +51,7 @@ getArticle(id, ({ data }) => {
|
||||
</template>
|
||||
</BoardHeader>
|
||||
<p>{{ article.text }}</p>
|
||||
|
||||
<CommentList :id="id"></CommentList>
|
||||
<CommentArea :article-id="id" :comments="comments" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@ -36,6 +60,5 @@ p {
|
||||
line-height: 1.6;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
|
@ -89,12 +89,12 @@ form {
|
||||
}
|
||||
|
||||
#title {
|
||||
font-size: 1.25rem;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#text {
|
||||
font-size: 1rem;
|
||||
font-size: 16px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
@ -106,7 +106,7 @@ form {
|
||||
|
||||
.errorMessage {
|
||||
color: var(--color-error);
|
||||
font-size: 0.85rem;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button {
|
||||
@ -116,7 +116,7 @@ button {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
transition:
|
||||
scale 0.25s,
|
||||
|
@ -2,6 +2,7 @@
|
||||
import { getArticles } from '@/api/article';
|
||||
import { ref } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import FilledButton from '../common/FilledButton.vue';
|
||||
|
||||
const articles = ref([]);
|
||||
|
||||
@ -11,7 +12,9 @@ getArticles(({ data }) => (articles.value = data));
|
||||
<template>
|
||||
<header>
|
||||
<h1>게시판</h1>
|
||||
<RouterLink class="write" :to="{ name: 'article-create' }">글쓰기</RouterLink>
|
||||
<RouterLink :to="{ name: 'article-create' }">
|
||||
<FilledButton primary class="write">글쓰기</FilledButton>
|
||||
</RouterLink>
|
||||
</header>
|
||||
<ul>
|
||||
<li v-for="article in articles" :key="article.id">
|
||||
@ -47,14 +50,7 @@ header {
|
||||
.write {
|
||||
padding: 8px 16px;
|
||||
border-radius: 12px;
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-background);
|
||||
text-decoration: none;
|
||||
transition: scale 0.25s;
|
||||
}
|
||||
|
||||
.write:active {
|
||||
scale: 0.95;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
@ -65,7 +61,7 @@ header {
|
||||
}
|
||||
|
||||
.title h2 {
|
||||
font-size: 1.1rem;
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@ -107,7 +103,7 @@ header {
|
||||
.info {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
</style>
|
||||
|
@ -44,6 +44,7 @@ header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
|
34
src/components/article/CommentArea.vue
Normal file
34
src/components/article/CommentArea.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<script setup>
|
||||
import CommentForm from './CommentForm.vue';
|
||||
import CommentList from './CommentList.vue';
|
||||
|
||||
const { articleId, comments } = defineProps({ articleId: Number, comments: Array });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="comments">
|
||||
<h2>
|
||||
댓글 <span>{{ comments.length }}</span>
|
||||
</h2>
|
||||
<CommentForm :article-id="articleId" />
|
||||
<CommentList :comments="comments" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h2 span {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.comments {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 20px;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
82
src/components/article/CommentForm.vue
Normal file
82
src/components/article/CommentForm.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import FilledButton from '../common/FilledButton.vue';
|
||||
import TextButton from '../common/TextButton.vue';
|
||||
|
||||
const { id } = defineProps({ id: Number });
|
||||
const isActive = ref(false);
|
||||
const textDiv = ref(null);
|
||||
const text = ref('');
|
||||
|
||||
function handleFocus(e) {
|
||||
console.log(e);
|
||||
isActive.value = true;
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
text.value = '';
|
||||
isActive.value = false;
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
console.log(id, text.value);
|
||||
// TODO: add api call
|
||||
text.value = '';
|
||||
isActive.value = false;
|
||||
textDiv.value.blur();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form action="POST" @submit.prevent="handleSubmit">
|
||||
<input type="text" ref="textDiv" v-model="text" @focus="handleFocus" placeholder="댓글 쓰기" />
|
||||
<div class="control" v-if="isActive">
|
||||
<TextButton @click="handleCancel">취소</TextButton>
|
||||
<FilledButton primary type="submit">등록</FilledButton>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control {
|
||||
display: flex;
|
||||
justify-self: flex-start;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
button:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
|
||||
button[type='submit']:hover {
|
||||
background-color: var(--color-primary-soft);
|
||||
}
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
|
||||
button[type='submit'] {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-background);
|
||||
}
|
||||
|
||||
button[type='submit']:active {
|
||||
background-color: var(--color-primary-soft);
|
||||
}
|
||||
</style>
|
50
src/components/article/CommentItem.vue
Normal file
50
src/components/article/CommentItem.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import TextButton from '../common/TextButton.vue';
|
||||
|
||||
const { comment } = defineProps({ comment: Object });
|
||||
const isDeleted = ref(false);
|
||||
|
||||
function handleDelete() {
|
||||
console.log('delete', comment.id);
|
||||
// TODO: add api call
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li v-if="!isDeleted">
|
||||
<div class="header">
|
||||
<div class="info">
|
||||
<div class="nickname">{{ comment.nickname }}</div>
|
||||
<div class="date">{{ comment.date }}</div>
|
||||
</div>
|
||||
<div v-if="!comment.isAuthor" class="control">
|
||||
<TextButton @click="handleDelete">삭제</TextButton>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text">{{ comment.text }}</p>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
li {
|
||||
padding: 16px 12px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info,
|
||||
.control {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.date {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
</style>
|
@ -1,41 +1,18 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { getComment } from '@/api/comment';
|
||||
import CommentItem from './CommentItem.vue';
|
||||
|
||||
const props = defineProps({ id: Number });
|
||||
const id = props.id;
|
||||
|
||||
const comments = ref([]);
|
||||
const text = ref('');
|
||||
|
||||
getComment(id, ({ data }) => {
|
||||
comments.value = data;
|
||||
});
|
||||
const { comments } = defineProps({ comments: Array });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2>댓글</h2>
|
||||
<input type="text" ref="textDiv" v-model="text" />
|
||||
</div>
|
||||
<ul>
|
||||
<li v-for="comment in comments" :key="comment.id">
|
||||
<div class="info">
|
||||
<div class="nickname">{{ comment.nickname }}</div>
|
||||
<div class="date">{{ comment.date }}</div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="text">{{ comment.text }}</div>
|
||||
</div>
|
||||
</li>
|
||||
<CommentItem v-for="comment in comments" :key="comment.id" :comment="comment" />
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.info {
|
||||
ul {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-secondary);
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
56
src/components/common/FilledButton.vue
Normal file
56
src/components/common/FilledButton.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
const { type, primary } = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
},
|
||||
primary: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button :type="type" :class="primary ? 'primary' : ''">
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
background-color: var(--color-background-soft);
|
||||
color: var(--color-text-secondary);
|
||||
transition:
|
||||
background-color 0.25s,
|
||||
transform 0.25s;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-background);
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
button:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
|
||||
button.primary:hover {
|
||||
background-color: var(--color-primary-soft);
|
||||
}
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
button.primary:active {
|
||||
background-color: var(--color-primary-soft);
|
||||
}
|
||||
</style>
|
48
src/components/common/TextButton.vue
Normal file
48
src/components/common/TextButton.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const { type, primary, filled } = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
},
|
||||
primary: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const classes = computed(() => [primary ? 'primary' : '', filled ? 'filled' : '']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button :type="type" :class="classes">
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
background: none;
|
||||
padding: 0 4px;
|
||||
color: var(--color-text-secondary);
|
||||
transition: transform 0.25s;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
@ -19,12 +19,12 @@ const router = createRouter({
|
||||
component: () => import('@/components/article/ArticleList.vue'),
|
||||
},
|
||||
{
|
||||
path: '/article/:id',
|
||||
path: 'article/:id',
|
||||
name: 'article',
|
||||
component: () => import('@/components/article/ArticleDetail.vue'),
|
||||
},
|
||||
{
|
||||
path: '/article/form',
|
||||
path: 'article/form',
|
||||
name: 'article-create',
|
||||
component: () => import('@/components/article/ArticleForm.vue'),
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user