Merge branch 'comment'

This commit is contained in:
jhynsoo 2024-05-15 19:13:56 +09:00
commit 691ac86d05
12 changed files with 321 additions and 50 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -44,6 +44,7 @@ header {
display: flex;
align-items: center;
gap: 12px;
font-size: 14px;
color: var(--color-text-secondary);
}

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

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

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

View File

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

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

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

View File

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