Post

데브코스 TIL - Day 52

24년 2월 2일 강의를 들은 내용과 추가로 학습한 내용을 기록한 글입니다.

실습 - 게시글 등록/삭제

전체코드

✨App.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import { useState } from "react";
import "./App.css";
import Post from "./Post";
import AddPost from "./AddPost";
import DetailPost from "./DetailPost";

/** 포스트 타입 */
export interface IPost {
  id: number;
  title: string;
  date: string;
  text?: string;
  isLike: boolean;
}
/** 포스트 상세보기 관련 타입 */
export interface IViewDetail {
  view: boolean;
  id: number | null;
}

/** 포스트 초기값 */
const initialState: IPost[] = [
  {
    id: 1,
    title: "미움받을 용기",
    text: "사람은 누구나 변화를 원한다. 지금보다 더 자유로운 삶, 지금보다 더 성공적인 삶, 지금보다 더 행복한 삶. 하지만 우리는 쉽게 핑계를 대고, 쉽게 포기한다. 지금 자신의 인생을 되돌아보자. “내게 조금 더 능력이 있었더라면”, “좀 더 부잣집에서 태어났더라면”, “참고 견디면 언젠가 나아지겠지”라며 환경을 탓하거나 현실을 외면하지는 않았는지 말이다. 이런 우리에게 “인간은 변할 수 있고, 누구나 행복해질 수 있다”라고 단언하는 철학자가 있다. 그간 상식으로 일컬어지던 프로이트의 ‘원인론’을 정면으로 부정하며, 자유도 행복도 모두 ‘용기’의 문제일 뿐 환경이나 능력의 문제가 아님을 보여준 알프레드 아들러(Alfred Adler)다.",
    date: "2022-12-28",
    isLike: true
  },
  {
    id: 2,
    title: "나는 메트로폴리탄 미술관의 경비원입니다",
    date: "2023-11-24",
    isLike: false
  },
  {
    id: 3,
    title: "퓨처 셀프",
    text: "",
    date: "2023-8-30",
    isLike: false
  }
];

function App() {
  const [posts, setPosts] = useState(initialState);
  const [viewDetail, setViewDetail] = useState<IViewDetail>({
    view: false,
    id: null
  });
  const [addMode, setAddMode] = useState(false);

  // 좋아요 토글
  const toggleLike = (id: number) => {
    const newPosts = [...posts].map((post) =>
      post.id === id ? { ...post, isLike: !post.isLike } : post
    );
    setPosts(newPosts);
  };

  /** 포스트 생성 */
  const addPost = ({
    title,
    date,
    text
  }: {
    title: string;
    date: string;
    text?: string;
  }) => {
    if (!title || !date) return alert("제목 또는 날짜가 작성되지 않았습니다.");
    const lastPost = posts[posts.length - 1];
    const lastPostId = lastPost.id ?? 0;

    const newPost = {
      id: lastPostId + 1,
      title,
      text,
      date,
      isLike: false
    };

    setPosts([...posts, newPost]);
  };

  /** 포스트 삭제 */
  const deletePost = (id: number) => {
    const newPosts = [...posts].filter((post) => post.id !== id);
    setPosts(newPosts);
  };

  const filterPost = (id: number) => posts.filter((post) => post.id === id)[0];

  return (
    <div className="container">
      <header>
        <h1 className="title">나의 블로그</h1>
      </header>
      <ul className="post-list">
        {posts.map((post) => (
          <Post
            key={post.id}
            {...post}
            toggleLike={toggleLike}
            setViewDetail={setViewDetail}
            deletePost={deletePost}
          />
        ))}
      </ul>

      {/* 새로운 글쓰기 */}
      {addMode ? (
        <AddPost addPost={addPost} setAddMode={setAddMode} />
      ) : (
        <button className="add-post-btn" onClick={() => setAddMode(true)}>
          +  글쓰기
        </button>
      )}

      {/* 상세보기 */}
      {viewDetail.view && viewDetail.id && (
        <DetailPost
          post={filterPost(viewDetail.id)}
          setViewDetail={setViewDetail}
          viewDetail={viewDetail}
        />
      )}
    </div>
  );
}

export default App;

✨Post.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { IPost, IViewDetail } from "./App";

interface PostProps extends IPost {
  toggleLike: (id: number) => void;
  deletePost: (id: number) => void;
  setViewDetail: (viewDetail: IViewDetail) => void;
}

const Post = ({
  title,
  date,
  isLike,
  id,
  toggleLike,
  setViewDetail,
  deletePost
}: PostProps) => {
  return (
    <li className="post">
      <h3 onClick={() => setViewDetail({ id, view: true })}>{title}</h3>
      <p className="date">{date}</p>
      <button
        className={`btn-like ${isLike ? "liked" : ""}`}
        onClick={() => toggleLike(id)}
      >
         좋아요
      </button>
      <button onClick={() => deletePost(id)}>게시글 삭제</button>
    </li>
  );
};

export default Post;

✨Modal.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React from "react";

interface ModalProps {
  children: React.ReactNode;
  title: string;
  closeAction: () => void;
}

const Modal = ({ children, title, closeAction }: ModalProps) => {
  return (
    <div className="modal">
      <div className="inner">
        <div className="modal-header">
          <h2>{title}</h2>
          <button onClick={closeAction}>닫기</button>
        </div>

        {children}
      </div>
    </div>
  );
};

export default Modal;

✨DetailPost.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from "react";
import Modal from "./Modal";
import { IPost, IViewDetail } from "./App";

interface DetailPostProps {
  setViewDetail: ({ view }: IViewDetail) => void;
  viewDetail: IViewDetail;
  post: IPost;
}

const DetailPost = ({ post, setViewDetail, viewDetail }: DetailPostProps) => {
  return (
    <Modal
      title="상세보기"
      closeAction={() => setViewDetail({ ...viewDetail, view: false })}
    >
      <div className="view-detail-container">
        <h3>{post.title}</h3>
        {post.text && <p>{post.text}</p>}
        <p>{post.date}</p>
      </div>
    </Modal>
  );
};

export default DetailPost;

✨AddPost.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import React, { FormEvent, useState } from "react";
import Modal from "./Modal";

interface AddPostProps {
  addPost: ({
    title,
    date,
    text
  }: {
    title: string;
    date: string;
    text: string;
  }) => void;
  setAddMode: (addMode: boolean) => void;
}

const AddPost = ({ addPost, setAddMode }: AddPostProps) => {
  const [title, setTitle] = useState("");
  const [date, setDate] = useState("");
  const [text, setText] = useState("");

  const onSubmit = (e: FormEvent) => {
    e.preventDefault();
    addPost({ title, date, text });
    setAddMode(false);
  };

  return (
    <Modal title="새로운 포스트 작성" closeAction={() => setAddMode(false)}>
      <div className="add-post-container">
        <form onSubmit={onSubmit}>
          <div className="input-row">
            <label>타이틀</label>
            <input
              type="text"
              onChange={(e) => setTitle(e.target.value)}
              required
            />
          </div>
          <div className="input-row">
            <label>내용</label>
            <textarea
              rows={10}
              onChange={(e) => setText(e.target.value)}
              defaultValue={text}
            ></textarea>
          </div>
          <div className="input-row">
            <label>날짜</label>
            <input
              type="date"
              onChange={(e) => setDate(e.target.value)}
              required
            />
          </div>

          <button type="submit">등록하기</button>
        </form>
      </div>
    </Modal>
  );
};

export default AddPost;
This post is licensed under CC BY 4.0 by the author.