장재혁

Merge branch 'allinone' into 'master'

Check k8s setting and Add Allinone Page



See merge request !4
Showing 44 changed files with 1185 additions and 55 deletions
......@@ -28,7 +28,7 @@ export class PostService {
async findSome(input: Partial<GetPostInput>): Promise<Post[]> {
return this.postRepository
.createQueryBuilder('post')
.where('post.id like :id', { id: input.id })
.orWhere('post.id = :id', { id: input.id })
.orWhere('post.author like :author', { author: input.author })
.orWhere('post.category like :category', { category: input.category })
.getMany()
......
@import './shared.scss';
html,
body {
height: 100%;
width: 100%;
}
* {
margin: 0;
padding: 0;
text-decoration: none;
}
#__next {
height: 100%;
min-width: 236px;
}
.app-layout {
min-height: 100vh;
height: auto;
display: block;
}
.app-header {
@media screen and (max-width: $medium_tablet_width) {
padding: 0px;
position: fixed;
width: 100%;
z-index: 1;
.ant-menu-submenu-horizontal {
float: right;
}
}
}
.outer-container {
width: 100%;
max-width: $window_max_width;
min-height: calc(100vh - 64px);
padding: 12px 50px;
margin: 0 auto;
overflow-y: scroll;
@media screen and (max-width: $medium_tablet_width) {
padding: 76px 16px 12px 16px;
min-height: 100vh;
}
@media screen and (max-width: $small_smart_phone_width) {
padding: 10px 8px;
}
}
@import './shared.scss';
.category-table-container {
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
.ant-table {
border-radius: $border_radius;
overflow: hidden;
.ant-table-title {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin: 0 4px;
@media screen and (max-width: $medium_smart_phone_width) {
h2 {
font-size: 14px;
}
}
}
tr {
@media screen and (max-width: $small_tablet_width) {
.ant-table-cell:nth-child(3) {
display: none;
}
}
@media screen and (max-width: $medium_smart_phone_width) {
.ant-table-cell:nth-child(1) {
display: none;
}
.ant-table-cell:nth-child(2) {
width: 100%;
}
}
@media screen and (max-width: $small_smart_phone_width) {
.ant-table-cell:nth-child(2) {
font-size: 12px;
}
.ant-table-cell:nth-child(4) {
font-size: 11px;
}
}
}
}
}
@import './shared.scss';
.create-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
.form-container {
border-radius: $border_radius;
background-color: white;
padding: 24px;
form {
height: 100%;
.form-button {
float: right;
&.cancel-button {
margin-right: 8px;
}
}
#form-textarea {
height: 50vh;
}
}
}
}
@import './shared.scss';
.main-card-container {
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
align-items: center;
@media screen and (max-width: $medium_tablet_width) {
display: block;
}
.main-card {
width: 47%;
height: 48%;
border-radius: $border_radius;
@media screen and (max-width: $medium_tablet_width) {
width: 100%;
height: 300px;
margin-bottom: 12px;
}
@media screen and (max-width: $large_smart_phone_width) {
height: 240px;
}
@media screen and (max-width: $medium_smart_phone_width) {
height: 220px;
}
.ant-card-body {
height: calc(100% - 58px);
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: stretch;
padding: 18px;
.card-row {
display: flex;
justify-content: space-between;
align-items: center;
border-radius: $border_radius;
padding: 5px 18px;
height: 45px;
@media screen and (max-width: $medium_tablet_width) {
&:nth-child(4),
&:nth-child(5) {
display: none;
}
}
&.has-content {
transition: background-color 0.3s;
padding: 2px 12px;
&:hover {
background-color: rgba(26, 144, 255, 0.2);
transition: background-color 0.3s;
}
}
.card-row-title {
margin: 0;
font-size: 17px;
font-weight: 400;
@media screen and (max-width: $large_smart_phone_width) {
font-size: 14px;
}
}
.card-row-recomment {
color: $red;
font-weight: 600;
margin-left: 16px;
@media screen and (max-width: $large_smart_phone_width) {
font-size: 13px;
}
@media screen and (max-width: $medium_smart_phone_width) {
display: none;
}
}
}
}
}
}
@import './shared.scss';
.post-container {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: stretch;
height: 100%;
.post-content {
flex: 1;
border-radius: $border_radius;
margin-bottom: 24px;
}
.post-comments {
min-height: $comment_height;
border-radius: $border_radius;
overflow-y: scroll;
.post-comments-num {
font-size: 14px;
color: #888;
}
}
}
.comments-textarea {
margin-top: 12px;
}
.comments-submit-button {
float: right;
margin-top: 8px;
}
$window_max_width: 1240px;
$header_height: 64px;
$border_radius: 10px;
$comment_height: 158px;
// color
$black: #333;
$red: #eb4d4b;
// media query
$large_tablet_width: 1024px;
$medium_tablet_width: 768px;
$small_tablet_width: 500px;
$large_smart_phone_width: 444px;
$medium_smart_phone_width: 370px;
$small_smart_phone_width: 300px;
......@@ -20,7 +20,8 @@
"graphql": "15.3.0",
"next": "latest",
"react": "^16.13.1",
"react-dom": "^16.13.1"
"react-dom": "^16.13.1",
"sass": "^1.34.1"
},
"devDependencies": {
"@graphql-codegen/cli": "^1.17.8",
......
import React from 'react';
import { useRouter } from 'next/router';
import { useQuery } from '@apollo/client';
import { GET_POST_WITH_COMMENTS } from '@src/gql/post-with-comments';
import Content from '@src/views/Post/Content';
import Comments from '@src/views/Post/Comment';
export default function ArticlePage() {
const { query } = useRouter();
if (!query.num) {
return null; // or redirect 404 ?
}
const { error, data } = useQuery(GET_POST_WITH_COMMENTS, {
variables: {
post_id: Number(query.num),
inputComment: { post_id: Number(query.num) },
},
});
if (error) console.log(JSON.stringify(error, null, 2));
const post = data?.getPost || {};
return (
<div className={'outer-container post-container'}>
<Content {...post} />
<Comments comments={data?.getSomeComments || []} postId={post.id} />
</div>
);
}
import React from 'react';
import { useRouter } from 'next/router';
import { useMutation } from '@apollo/client';
import { CREATE_POST } from '@src/gql/create-post';
import { Form, Input } from 'antd';
import TextArea from 'antd/lib/input/TextArea';
import { CreateButtons, CreateInputs } from '@src/views/Create';
export default function CreatePage() {
const router = useRouter();
const [createPost] = useMutation(CREATE_POST);
const [form] = Form.useForm();
const [contents, setContents] = React.useState({
title: '',
description: '',
});
const handleChange = (e) => {
const {
target: { name, value },
} = e;
setContents({
...contents,
[name]: value,
});
};
const handleSubmit = async (e) => {
e.preventDefault();
const { title, description } = contents;
const {
query: { name },
} = router;
if (!(title && description)) {
alert('필수항목을 모두 입력해주세요');
return;
}
const { data } = await createPost({
variables: {
input: {
category: name,
content: description,
title,
},
},
});
router.push(`/${name}/article?num=${data.createPost.id}`);
};
const handleCancel = () => {
router.back();
};
return (
<div className={'outer-container create-container'}>
<div className={'form-container'}>
<Form form={form} layout={'vertical'}>
<CreateInputs
forms={[
{
form: {
label: '글제목',
tooltip: '게시글 제목은 필수항목입니다',
},
input: {
Item: Input,
value: 'title',
onChange: handleChange,
},
},
{
form: {
label: '글내용',
tooltip: '게시글의 내용은 필수항목입니다',
},
input: {
Item: TextArea,
value: 'description',
id: 'form-textarea',
onChange: handleChange,
},
},
]}
/>
<CreateButtons
buttons={[
{
title: 'Submit',
onClick: handleSubmit,
type: 'primary',
className: 'form-button submit-button',
},
{
title: 'Cancel',
onClick: handleCancel,
className: 'form-button cancel-button',
},
]}
/>
</Form>
</div>
</div>
);
}
import { AppProps } from "next/app";
import { ApolloProvider } from "@apollo/client";
import { useApollo } from "../lib/apollo";
import "antd/dist/antd.css";
import Router from 'next/router';
import { AppProps } from 'next/app';
import Head from 'next/head';
import { ApolloProvider } from '@apollo/client';
import { useApollo } from '../lib/apollo';
import 'antd/dist/antd.css';
import '../assets/styles/app.scss';
import '../assets/styles/main.scss';
import '../assets/styles/category.scss';
import '../assets/styles/post.scss';
import '../assets/styles/create.scss';
import Layout from 'antd/lib/layout/layout';
import Header from '@components/Header';
import Loader, { startLoading, finishLoading } from '@components/Loader';
Router.events.on('routeChangeStart', startLoading);
Router.events.on('routeChangeComplete', finishLoading);
Router.events.on('routeChangeError', finishLoading);
export default function App({ Component, pageProps }: AppProps) {
const apolloClient = useApollo(pageProps);
return (
<ApolloProvider client={apolloClient}>
<Component {...pageProps} />
</ApolloProvider>
<>
<Head>
<meta
name={'viewport'}
content={
'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'
}
/>
</Head>
<Loader />
<Layout className={'app-layout'}>
<ApolloProvider client={apolloClient}>
<Header />
<Component {...pageProps} />
</ApolloProvider>
</Layout>
</>
);
}
......
import Link from "next/link";
export default function About() {
return (
<div>
Welcome to the about page. Go to the{" "}
<Link href="/">
<a>Home</a>
</Link>{" "}
page.
</div>
);
}
import { useRouter } from 'next/router';
import { useQuery } from '@apollo/client';
import { GET_SOME_POSTS } from '@src/gql/get-some-posts';
import Category from '@src/views/Category';
export default function CategoryPage() {
const {
query: { name },
} = useRouter();
const { error, data } = useQuery(GET_SOME_POSTS, {
variables: {
input: { category: name },
},
});
if (error) console.log(JSON.stringify(error, null, 2));
const getCategoryPosts = data?.getSomePosts || [];
const articleList = getCategoryPosts.filter((post) => post.category === name);
return <Category category={name} articleList={articleList} />;
}
import { GetPostInput, Post } from "@graphql-community/shared";
import { useQuery, gql } from "@apollo/client";
import { message } from "antd";
import { useQuery } from '@apollo/client';
const GET_SOME_POST_QUERY = gql`
query GetSomePosts($getSomePostInput: GetPostInput!) {
getSomePosts(input: $getSomePostInput) {
author
category
}
}
`;
import { GET_ALL_POSTS } from '@src/gql/get-all-posts';
const Index = () => {
const { data, error } = useQuery<
{ getSomePosts: Post[] },
{ getSomePostInput: GetPostInput }
>(GET_SOME_POST_QUERY, {
variables: {
getSomePostInput: {
id: 1,
},
},
});
import Main from '@views/Main';
export default function IndexPage() {
const { error, data } = useQuery(GET_ALL_POSTS);
if (error) console.log(JSON.stringify(error, null, 2));
return (
<>
<div onClick={() => message.success("hi")}>index </div>
<div>{data?.getSomePosts[0].author}</div>
<div>{data?.getSomePosts[0].category}</div>
</>
);
};
let categories = [];
let getAllPosts = data?.getAllPosts || [];
getAllPosts.forEach((post) => {
if (!categories.find((category) => post.category === category)) {
categories.push(post.category);
}
});
export default Index;
return <Main categories={categories} posts={getAllPosts} />;
}
......
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/dist/client/router';
import { Layout, Menu } from 'antd';
import { MenuOutlined } from '@ant-design/icons';
import { useQuery } from '@apollo/client';
import { GET_ALL_POSTS } from '@src/gql/get-all-posts';
const { Header: HeaderContainer } = Layout;
export default function Header() {
const [selected, setSelected] = useState(null);
const { query } = useRouter();
const { error, data } = useQuery(GET_ALL_POSTS);
if (error) console.log(JSON.stringify(error, null, 2));
let list = [];
let getAllPosts = data?.getAllPosts || [];
getAllPosts.forEach((post) => {
if (!list.find((category) => post.category === category)) {
list.push(post.category);
}
});
useEffect(() => {
setSelected(query.name || '/');
}, [query]);
return (
<HeaderContainer className={'app-header'}>
<Menu
overflowedIndicator={<MenuOutlined className={'header-hbg-menu'} />}
theme={'dark'}
mode={'horizontal'}
selectedKeys={[selected]}
>
{/* logo */}
<Menu.Item key={'/'}>
<Link href={'/'}>
<a
style={{
float: 'left',
width: '120px',
height: '31px',
margin: '16px 24px 16px 0',
background: 'rgba(255, 255, 255, 0.3)',
}}
/>
</Link>
</Menu.Item>
{list.map((item) => (
<Menu.Item key={item} className={'header-item'}>
<Link href={`/category/${item}`}>
<a>{item}</a>
</Link>
</Menu.Item>
))}
</Menu>
</HeaderContainer>
);
}
import { Spin, Space } from 'antd';
export const startLoading = () => {
const element = document.getElementById('app-loader');
element.style.display = 'flex';
};
export const finishLoading = () => {
const element = document.getElementById('app-loader');
element.style.display = 'none';
};
export default function Loader() {
return (
<Space
id={'app-loader'}
size="middle"
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
display: 'none',
alignItems: 'center',
justifyContent: 'center',
zIndex: 2,
}}
>
<Spin size="large" />
</Space>
);
}
export const GQL_URI =
'http://a5784f2e906ca4512adac13dd73dd23a-8e11745c200f4ae4.elb.ap-northeast-2.amazonaws.com:5000/graphql';
import gql from 'graphql-tag';
export const CREATE_COMMENT = gql`
mutation CreateComment($input: CreateCommentInput!) {
createComment(input: $input) {
id
author
content
created_date
post_id
}
}
`;
import gql from 'graphql-tag';
export const CREATE_POST = gql`
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
}
}
`;
import gql from 'graphql-tag';
export const GET_ALL_POSTS = gql`
query GetAllPosts {
getAllPosts {
id
title
category
}
}
`;
import gql from 'graphql-tag';
export const GET_SOME_POSTS = gql`
query GetSomePosts($input: GetPostInput!) {
getSomePosts(input: $input) {
category
id
author
title
created_date
}
}
`;
import gql from 'graphql-tag';
export const GET_POST_WITH_COMMENTS = gql`
query GetPostWithComments($post_id: Float!, $inputComment: GetCommentInput!) {
getPost(id: $post_id) {
id
author
category
created_date
title
content
}
getSomeComments(input: $inputComment) {
id
author
content
created_date
}
}
`;
export { default as useWindowSize } from './useWindowSize';
import { useLayoutEffect, useState } from 'react';
export default function useWindowSize() {
const [size, setSize] = useState([0, 0]);
useLayoutEffect(() => {
function updateSize() {
setSize([window.innerWidth, window.innerHeight]);
}
window.addEventListener('resize', updateSize);
updateSize();
return () => window.removeEventListener('resize', updateSize);
}, []);
return size;
}
import moment from 'moment';
import { useWindowSize } from '@src/hooks';
export const getMaxLengthByWidth = (width: number) => {
if (width > 590) return 30;
if (width > 474) return 20;
else return 13;
};
export const sliceTextByLength = (text: string, length: number) =>
text.length > length ? text.substr(0, length) + '...' : text;
export const makeArticleURLWithNumber = (name: string, id: number) => {
return `${window.location.origin}/${name}/article?num=${id}`;
};
export const makeDateForDayOrTime = (date) =>
moment().diff(moment(date), 'days') > 1
? moment(date).format('YY.MM.DD')
: moment(date).format('HH:mm:ss');
export const makeCategoryTableBody = (list) => {
const [width] = useWindowSize();
const newData = list.map(({ title, id, created_date, ...rest }) => {
const textMaxLength = getMaxLengthByWidth(width);
const newTitle = sliceTextByLength(title, textMaxLength);
const createdDate = makeDateForDayOrTime(created_date);
return {
...rest,
id,
key: String(id),
title: newTitle,
created_date: createdDate,
};
});
newData.sort((a, b) => Number(b.id) - Number(a.id));
return newData;
};
//@ts-nocheck
import React from 'react';
import { Table as TableContainer, TableProps } from 'antd';
import { useWindowSize } from '@src/hooks';
import TableHeader from './TableHeader';
interface NewTableProps extends TableProps<any> {
title: string;
columns: Object[];
data: Object[];
}
export default function Table({
title,
columns,
data,
...rest
}: NewTableProps) {
const [width, height] = useWindowSize();
const size = width > 1000 ? 'middle' : 'small';
const cellHeight = size === 'small' ? 39 : 55;
const pageSize = Math.ceil((height * 2) / 3 / cellHeight);
const emptyRowNum = pageSize - (data.length % pageSize);
const emptyRow = columns.reduce(
(acc, curr) => ((acc[curr['dataIndex']] = '-'), acc),
{},
);
const body = [...data, ...Array(emptyRowNum || 0).fill(emptyRow)];
return (
<TableContainer
size={size}
title={() => <TableHeader title={title} />}
columns={columns}
dataSource={body}
pagination={{
responsive: true,
showLessItems: true,
pageSize,
}}
{...rest}
/>
);
}
import React from 'react';
import { Button } from 'antd';
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function TableHeader({ title }) {
const { query } = useRouter();
return (
<div className={'ant-table-title'}>
<h2>{title} 게시판</h2>
<Button>
<Link href={`/${query.name}/create`}>{'글쓰기'}</Link>
</Button>
</div>
);
}
import Table from './Table';
import {
makeArticleURLWithNumber,
makeCategoryTableBody,
} from '@shared/functions';
import { useRouter } from 'next/router';
export default function Category({ category, articleList }) {
const router = useRouter();
// example
const newData = makeCategoryTableBody(articleList);
const handleRoute = ({ id }) => (e) => {
e.preventDefault();
if (id === '-') return;
const {
query: { name: categoryName },
} = router;
const URL = makeArticleURLWithNumber(categoryName as string, id);
router.push(URL);
};
return (
<div className={'outer-container category-table-container'}>
<Table
title={category}
columns={[
{
key: '1',
title: 'No.',
dataIndex: 'id',
align: 'center',
},
{ key: '2', title: '제목', dataIndex: 'title', width: '70%' },
{
key: '3',
title: '작성자',
dataIndex: 'author',
align: 'center',
},
{
key: '4',
title: '등록일',
dataIndex: 'created_date',
align: 'center',
},
]}
data={newData}
onRow={(record) => ({
onClick: handleRoute(record),
})}
/>
</div>
);
}
import React from 'react';
import { Button } from 'antd';
export default function Buttons({ buttons }) {
return (
<div>
{buttons.map(({ title, ...rest }) => (
<Button key={title} {...rest}>
{title}
</Button>
))}
</div>
);
}
import React from 'react';
import { Form } from 'antd';
export default function Inputs({ forms }) {
return (
<>
{forms.map(({ form, input: { Item, value, ...rest } }) => (
<Form.Item {...form} required key={value}>
<Item placeholder={value} name={value} {...rest} />
</Form.Item>
))}
</>
);
}
export { default as CreateInputs } from './Inputs';
export { default as CreateButtons } from './Buttons';
import Link from 'next/link';
import { Card as CardItem } from 'antd';
import { Row } from './Row';
function MoreButton(category) {
return <Link href={`/category/${category}`}>더보기</Link>;
}
export default function Card({ category, posts, ...rest }) {
const postsNum = posts.length;
const emptyRows = postsNum < 5 ? Array(5 - postsNum).fill(null) : [];
const sliced = postsNum > 5 ? posts.slice(postsNum - 5, postsNum) : posts;
return (
<CardItem title={category} extra={MoreButton(category)} {...rest}>
{sliced.map(({ title, id }) => (
<Row key={title} category={category} title={title} id={id} />
))}
{emptyRows.map((_, idx) => (
<div className={'card-row'} key={idx} />
))}
</CardItem>
);
}
import { useRouter } from 'next/router';
import { makeArticleURLWithNumber } from '@src/shared/functions';
export const Row = ({ category, title, id }) => {
const router = useRouter();
const sliced = title.length > 20 ? title.substr(0, 20) + '...' : title;
const handleClickArticle = () => {
const URL = makeArticleURLWithNumber(category, id);
router.push(URL);
};
return (
<div className={'card-row has-content'} onClick={handleClickArticle}>
<h2 className={'card-row-title'}>{sliced}</h2>
<span className={'card-row-recomment'}>{'?'}</span>
</div>
);
};
import Card from '@src/views/Main/Card';
export default function Main({ categories, posts }) {
return (
<div className={'outer-container main-card-container'}>
{categories?.map((category) => {
const filtered = posts.filter((post) => post.category === category);
return (
<Card
key={category}
category={category}
posts={filtered}
className={'main-card'}
/>
);
})}
</div>
);
}
import React, { useState } from 'react';
import { Comment as CommentItem, Divider } from 'antd';
import Profile from './Profile';
import Datetime from './Datetime';
import Like from './Like';
import Dislike from './Dislike';
export default function Comment({
author,
content,
created_date,
idx,
commentsNum,
}) {
const [likes, setLikes] = useState(0);
const [dislikes, setDislikes] = useState(0);
const [action, setAction] = useState(null);
const like = () => {
setLikes(1);
setDislikes(0);
setAction('liked');
};
const dislike = () => {
setLikes(0);
setDislikes(1);
setAction('disliked');
};
return (
<>
<CommentItem
actions={[
Like({ action, like, likes }),
Dislike({
action,
dislike,
dislikes,
}),
]}
author={<a>{author}</a>}
avatar={Profile({ src: '', author })}
content={<div dangerouslySetInnerHTML={{ __html: content }} />}
datetime={Datetime({ created_date })}
/>
{commentsNum - 1 !== idx && <Divider />}
</>
);
}
import React from 'react';
import { Tooltip } from 'antd';
import moment from 'moment';
export default function Datetime({ created_date }) {
return (
<Tooltip title={moment(created_date).format('YYYY-MM-DD HH:mm:ss')}>
<span>{moment(created_date).fromNow()}</span>
</Tooltip>
);
}
import React, { createElement } from 'react';
import { Tooltip } from 'antd';
import { DislikeOutlined, DislikeFilled } from '@ant-design/icons';
export default function Dislike({ dislike, action, dislikes }) {
return (
<Tooltip key="comment-basic-dislike" title="Dislike">
<span onClick={dislike}>
{createElement(action === 'disliked' ? DislikeFilled : DislikeOutlined)}
<span className="comment-action">{dislikes}</span>
</span>
</Tooltip>
);
}
import React, { createElement } from 'react';
import { Tooltip } from 'antd';
import { LikeOutlined, LikeFilled } from '@ant-design/icons';
export default function Like({ action, like, likes }) {
return (
<Tooltip key="comment-basic-like" title="Like">
<span onClick={like}>
{createElement(action === 'liked' ? LikeFilled : LikeOutlined)}
<span className="comment-action">{likes}</span>
</span>
</Tooltip>
);
}
import React from 'react';
import { Avatar } from 'antd';
export default function Profile({ src, author }) {
return (
<Avatar
src={
src
? src
: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png'
}
alt={author}
/>
);
}
import React from 'react';
import TextArea from 'antd/lib/input/TextArea';
import { Button } from 'antd';
import { useMutation } from '@apollo/client';
import { CREATE_COMMENT } from '@src/gql/create-comment';
export default function Submit({ title, postId, addCommentList }) {
const [content, setContent] = React.useState('');
const [createComment] = useMutation(CREATE_COMMENT);
const handleChange = (e) => {
setContent(e.target.value);
};
const handleSubmit = async () => {
const { data } = await createComment({
variables: { input: { content, post_id: postId } },
});
setContent('');
data && addCommentList(data.createComment);
};
return (
<>
<TextArea
value={content}
onChange={handleChange}
placeholder={'댓글을 입력하세요'}
autoSize={{ minRows: 3, maxRows: 5 }}
className={'comments-textarea'}
/>
<Button
type={'primary'}
size={'large'}
onClick={handleSubmit}
className={'comments-submit-button'}
>
{title}
</Button>
</>
);
}
import React, { useState } from 'react';
import { Card } from 'antd';
import Submit from './Submit';
import Comment from './Comment';
export default function CommentsContainer({ comments, postId }) {
const [tempComments, setTempComments] = useState(comments);
const commentsNum = tempComments?.length;
const addCommentList = (data) => {
setTempComments((prev) => [...prev, data]);
};
React.useEffect(() => {
comments && setTempComments(comments);
}, [comments]);
return (
<Card className={'post-comments'}>
<h1 className={'post-comments-num'}>{`COMMENTS (${commentsNum})`}</h1>
{tempComments?.map(({ author, content, created_date }, idx) => (
<Comment
key={created_date}
author={author}
content={content}
created_date={created_date}
idx={idx}
commentsNum={commentsNum}
/>
))}
<Submit title={'작성'} postId={postId} addCommentList={addCommentList} />
</Card>
);
}
import React from 'react';
import { Card, Descriptions } from 'antd';
import moment from 'moment';
export default function Content({
id,
author,
category,
created_date,
title,
content,
}) {
return (
<Card className={'post-content'}>
<Descriptions title={title} layout={'horizontal'} column={3}>
<Descriptions.Item label={'작성자'} span={3}>
{author}
</Descriptions.Item>
<Descriptions.Item
label={'작성일'}
span={3}
style={{ textAlign: 'right' }}
>
{moment(created_date).format('YYYY.MM.DD HH:mm:ss')}
</Descriptions.Item>
<Descriptions.Item label={'게시글'} span={3}>
<br />
<div dangerouslySetInnerHTML={{ __html: content }} />
</Descriptions.Item>
</Descriptions>
</Card>
);
}
......@@ -18,7 +18,19 @@
"resolveJsonModule": true,
"skipLibCheck": true,
"target": "esnext",
"strict": false
"strict": false,
"baseUrl": ".",
"rootDir": ".",
"paths": {
"@src/*": ["src/*"],
"@components/*": ["src/components/*"],
"@config/*": ["src/config/*"],
"@constants/*": ["src/constants/*"],
"@hooks/*": ["src/hooks/*"],
"@gql/*": ["src/gql/*"],
"@views/*": ["src/views/*"],
"@shared/*": ["src/shared/*"]
}
},
"exclude": [
"node_modules"
......
......@@ -5007,7 +5007,7 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
chokidar@3.5.1, chokidar@^3.4.2, chokidar@^3.5.1:
chokidar@3.5.1, "chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.2, chokidar@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a"
integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==
......@@ -12191,6 +12191,13 @@ sane@^4.0.3:
minimist "^1.1.1"
walker "~1.0.5"
sass@^1.34.1:
version "1.34.1"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.34.1.tgz#30f45c606c483d47b634f1e7371e13ff773c96ef"
integrity sha512-scLA7EIZM+MmYlej6sdVr0HRbZX5caX5ofDT9asWnUJj21oqgsC+1LuNfm0eg+vM0fCTZHhwImTiCU0sx9h9CQ==
dependencies:
chokidar ">=3.0.0 <4.0.0"
sax@>=0.6.0, sax@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
......