Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions week03/Blog/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
5 changes: 5 additions & 0 deletions week03/Blog/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"singleQuote": true,
"semi": true,
"trailingComma": "all"
}
12 changes: 12 additions & 0 deletions week03/Blog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# React + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
11 changes: 11 additions & 0 deletions week03/Blog/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import js from "@eslint/js";
import globals from "globals";
import pluginReact from "eslint-plugin-react";
import { defineConfig } from "eslint/config";


export default defineConfig([
{ files: ["**/*.{js,mjs,cjs,jsx}"], plugins: { js }, extends: ["js/recommended"] },
{ files: ["**/*.{js,mjs,cjs,jsx}"], languageOptions: { globals: globals.browser } },
pluginReact.configs.flat.recommended,
]);
13 changes: 13 additions & 0 deletions week03/Blog/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
30 changes: 30 additions & 0 deletions week03/Blog/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "blog",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@eslint/js": "^9.27.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.27.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.2.0",
"husky": "^9.1.7",
"prettier": "^3.5.3",
"vite": "^6.3.5"
}
}
1 change: 1 addition & 0 deletions week03/Blog/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
92 changes: 92 additions & 0 deletions week03/Blog/src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { useState } from 'react';
import './styles/react.css';
import Button from './component/ui/Button';
import PostList from './component/list/PostList';
import Post from './component/page/Post';
import PostWrite from './component/page/PostWritePage';
import styles from './styles/App.module.css';

function App() {
const [input, setInput] = useState('');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용하지 않는 state는 주석처리나 삭제해도 될 것 같아요!

const [selectedPost, setSelectedPost] = useState(null);
const [isWriting, setIsWriting] = useState(false);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selectedPost와 isWriting 상태로 화면 전환을 관리한 점 정말 좋았습니다! 조건부 렌더링도 자연스럽게 잘 구현해주셨어요!

const [posts, setPosts] = useState([
{
id: 1,
title: '프론트엔드를 하기에 갈 길이 멀어요',
content: 'js, css, html, react, node.js, 등등 너무 많은 것들을 배워야 해요',
comments: [
{ id: 1, text: '저도 공부할게 많아요' },
{ id: 2, text: 'css가 말썽꾸러기에요' },
],
},
{ id: 2, title: '디자인이 원하는 대로 안만들어져요', content: '종강하면 하루 종일 프론트 공부만 열심히 해야겠어요',
comments: [
{ id: 3, text: 'ai가 없던 시절 개발자들은 어떻게 프로그래밍을 했을까요' },
{ id: 4, text: '24시간이 부족해요' },
{ id: 5, text: '사자의 서재 만드신 운영진 분들이 너무 멋있어요' },
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

감사합니다... 😎

],
},
]);

const handleAddPost = (newPost) => {
setPosts([...posts, { id: Date.now(), ...newPost }]);
setIsWriting(false);
};

const handleAddComment = (commentText) => {
setPosts(
posts.map((post) =>
post.id === selectedPost.id
? {
...post,
comments: [
...(post.comments || []),
{ id: Date.now(), text: commentText },
],
}
: post,
),
);
setSelectedPost({
...selectedPost,
comments: [
...(selectedPost.comments || []),
{ id: Date.now(), text: commentText },
],
});
};

return (
<div className={styles.container}>
{!selectedPost && !isWriting && (
<>
<div className={styles.headerArea}>
<h2 className={styles.title}>블로그</h2>
</div>
<div className={styles.buttonArea}>
<Button onClick={() => setIsWriting(true)}>글 작성</Button>
</div>
<PostList posts={posts} onItemClick={setSelectedPost} />
</>
)}

{isWriting && (
<PostWrite
onSubmit={handleAddPost}
onCancel={() => setIsWriting(false)}
/>
)}

{selectedPost && !isWriting && (
<Post
post={selectedPost}
onBack={() => setSelectedPost(null)}
onAddComment={handleAddComment}
/>
)}
</div>
);
}

export default App;
14 changes: 14 additions & 0 deletions week03/Blog/src/component/list/CommentList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import CommentListItem from './CommentListItem';

function CommentList({ comments }) {
return (
<div>
{comments.map((comment) => (
<CommentListItem key={comment.id} text={comment.text} />
))}
</div>
);
}

export default CommentList;
18 changes: 18 additions & 0 deletions week03/Blog/src/component/list/CommentListItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';

function CommentListItem({ text }) {
return (
<div
style={{
padding: '16px',
marginBottom: '8px',
border: '1px solid #e0e0e0',
borderRadius: '8px',
}}
>
{text}
</div>
);
}

export default CommentListItem;
19 changes: 19 additions & 0 deletions week03/Blog/src/component/list/PostList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import PostListItem from './PostListItem';
import styles from './PostList.module.css';

function PostList({ posts, onItemClick }) {
return (
<div className={styles.listContainer}>
{posts.map((post) => (
<PostListItem
key={post.id}
title={post.title}
onClick={() => onItemClick(post)}
/>
))}
</div>
);
}

export default PostList;
33 changes: 33 additions & 0 deletions week03/Blog/src/component/list/PostList.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.listContainer {
margin-top: 16px;
padding: 16px 0;
background: none;
border-radius: 0;
}

.headerArea {
display: flex;
flex-direction: row;
align-items: flex-end;
margin-bottom: 1.2em;
}

.buttonArea {
position: absolute;
left: 24px;
bottom: 24px;
margin: 0;
}

.title {
flex: 1;
text-align: center;
font-size: 2rem;
font-weight: 700;
margin: 0;
}

.container {
position: relative;
min-height: 400px; /* 버튼이 겹치지 않게 */
}
12 changes: 12 additions & 0 deletions week03/Blog/src/component/list/PostListItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import styles from './PostListItem.module.css';

function PostListItem({ title, onClick }) {
return (
<div className={styles.item} onClick={onClick}>
{title}
</div>
);
}

export default PostListItem;
20 changes: 20 additions & 0 deletions week03/Blog/src/component/list/PostListItem.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.item {
padding: 16px 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #fff;
font-size: 1.1rem;
cursor: pointer;
transition:
box-shadow 0.15s,
background 0.15s;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
}
.item:last-child {
margin-bottom: 0;
}
.item:hover {
background: #f5f7fa;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
}
63 changes: 63 additions & 0 deletions week03/Blog/src/component/page/Post.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useState } from 'react';
import CommentList from '../list/CommentList';
import styles from './Post.module.css';
import appStyles from '../../styles/App.module.css';
import PropTypes from 'prop-types';
import TextInput from '../ui/TextInput';
import Button from '../ui/Button';

function Post({ post, onBack, onAddComment }) {
const [comment, setComment] = useState('');
if (!post) return null;

const handleSubmit = (e) => {
e.preventDefault();
if (!comment.trim()) return;
onAddComment(comment);
setComment('');
};

return (
<div className={styles.container}>
<div className={styles.topArea}>
<h2 className={styles.title}>{post.title}</h2>
<p className={styles.content}>{post.content}</p>
<div className={styles.buttonArea}>
<button className={styles.backButton} onClick={onBack}>
목록으로
</button>
</div>
</div>
<div className={styles.commentArea}>
<CommentList comments={post.comments || []} />
<form className={appStyles.commentWriteForm} onSubmit={handleSubmit}>
<div className={appStyles.inputWrapper}>
<TextInput
className={appStyles.commentInput}
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="댓글을 입력하세요"
/>
</div>
<div className={appStyles.buttonWrapper}>
<Button type="submit" className={appStyles.commentSubmitButton}>
댓글 작성
</Button>
</div>
</form>
</div>
</div>
);
}

Post.propTypes = {
post: PropTypes.shape({
title: PropTypes.string,
content: PropTypes.string,
comments: PropTypes.array,
}),
onBack: PropTypes.func,
onAddComment: PropTypes.func,
};

export default Post;
Loading