장재혁

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