From 4a1a6eca542e940d0050e98875f65ebf1cf3d6a3 Mon Sep 17 00:00:00 2001 From: sunwin1029 Date: Wed, 28 May 2025 23:17:20 +0900 Subject: [PATCH] =?UTF-8?q?3=EC=A3=BC=EC=B0=A8=20=EC=84=B8=EC=85=98=20?= =?UTF-8?q?=EA=B3=BC=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- week03/Blog/.gitignore | 24 + week03/Blog/.prettierrc | 5 + week03/Blog/README.md | 12 + week03/Blog/eslint.config.js | 11 + week03/Blog/index.html | 13 + week03/Blog/package.json | 30 + week03/Blog/public/vite.svg | 1 + week03/Blog/src/App.jsx | 92 + .../Blog/src/component/list/CommentList.jsx | 14 + .../src/component/list/CommentListItem.jsx | 18 + week03/Blog/src/component/list/PostList.jsx | 19 + .../src/component/list/PostList.module.css | 33 + .../Blog/src/component/list/PostListItem.jsx | 12 + .../component/list/PostListItem.module.css | 20 + week03/Blog/src/component/page/Post.jsx | 63 + .../Blog/src/component/page/Post.module.css | 59 + .../Blog/src/component/page/PostWritePage.jsx | 42 + .../component/page/PostWritePage.module.css | 36 + week03/Blog/src/component/ui/Button.jsx | 24 + week03/Blog/src/component/ui/TextInput.jsx | 24 + week03/Blog/src/main.jsx | 10 + week03/Blog/src/styles/App.module.css | 97 + week03/Blog/src/styles/Button.module.css | 13 + week03/Blog/src/styles/react.css | 43 + week03/Blog/vite.config.js | 7 + week03/Blog/yarn.lock | 2350 +++++++++++++++++ 26 files changed, 3072 insertions(+) create mode 100644 week03/Blog/.gitignore create mode 100644 week03/Blog/.prettierrc create mode 100644 week03/Blog/README.md create mode 100644 week03/Blog/eslint.config.js create mode 100644 week03/Blog/index.html create mode 100644 week03/Blog/package.json create mode 100644 week03/Blog/public/vite.svg create mode 100644 week03/Blog/src/App.jsx create mode 100644 week03/Blog/src/component/list/CommentList.jsx create mode 100644 week03/Blog/src/component/list/CommentListItem.jsx create mode 100644 week03/Blog/src/component/list/PostList.jsx create mode 100644 week03/Blog/src/component/list/PostList.module.css create mode 100644 week03/Blog/src/component/list/PostListItem.jsx create mode 100644 week03/Blog/src/component/list/PostListItem.module.css create mode 100644 week03/Blog/src/component/page/Post.jsx create mode 100644 week03/Blog/src/component/page/Post.module.css create mode 100644 week03/Blog/src/component/page/PostWritePage.jsx create mode 100644 week03/Blog/src/component/page/PostWritePage.module.css create mode 100644 week03/Blog/src/component/ui/Button.jsx create mode 100644 week03/Blog/src/component/ui/TextInput.jsx create mode 100644 week03/Blog/src/main.jsx create mode 100644 week03/Blog/src/styles/App.module.css create mode 100644 week03/Blog/src/styles/Button.module.css create mode 100644 week03/Blog/src/styles/react.css create mode 100644 week03/Blog/vite.config.js create mode 100644 week03/Blog/yarn.lock diff --git a/week03/Blog/.gitignore b/week03/Blog/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/week03/Blog/.gitignore @@ -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? diff --git a/week03/Blog/.prettierrc b/week03/Blog/.prettierrc new file mode 100644 index 0000000..7d2081a --- /dev/null +++ b/week03/Blog/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "semi": true, + "trailingComma": "all" +} diff --git a/week03/Blog/README.md b/week03/Blog/README.md new file mode 100644 index 0000000..7059a96 --- /dev/null +++ b/week03/Blog/README.md @@ -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. diff --git a/week03/Blog/eslint.config.js b/week03/Blog/eslint.config.js new file mode 100644 index 0000000..2ecf4d0 --- /dev/null +++ b/week03/Blog/eslint.config.js @@ -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, +]); diff --git a/week03/Blog/index.html b/week03/Blog/index.html new file mode 100644 index 0000000..0c589ec --- /dev/null +++ b/week03/Blog/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/week03/Blog/package.json b/week03/Blog/package.json new file mode 100644 index 0000000..3be8bec --- /dev/null +++ b/week03/Blog/package.json @@ -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" + } +} diff --git a/week03/Blog/public/vite.svg b/week03/Blog/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/week03/Blog/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/week03/Blog/src/App.jsx b/week03/Blog/src/App.jsx new file mode 100644 index 0000000..be5c457 --- /dev/null +++ b/week03/Blog/src/App.jsx @@ -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(''); + const [selectedPost, setSelectedPost] = useState(null); + const [isWriting, setIsWriting] = useState(false); + 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: '사자의 서재 만드신 운영진 분들이 너무 멋있어요' }, + ], + }, + ]); + + 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 ( +
+ {!selectedPost && !isWriting && ( + <> +
+

블로그

+
+
+ +
+ + + )} + + {isWriting && ( + setIsWriting(false)} + /> + )} + + {selectedPost && !isWriting && ( + setSelectedPost(null)} + onAddComment={handleAddComment} + /> + )} +
+ ); +} + +export default App; diff --git a/week03/Blog/src/component/list/CommentList.jsx b/week03/Blog/src/component/list/CommentList.jsx new file mode 100644 index 0000000..eb0f871 --- /dev/null +++ b/week03/Blog/src/component/list/CommentList.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import CommentListItem from './CommentListItem'; + +function CommentList({ comments }) { + return ( +
+ {comments.map((comment) => ( + + ))} +
+ ); +} + +export default CommentList; diff --git a/week03/Blog/src/component/list/CommentListItem.jsx b/week03/Blog/src/component/list/CommentListItem.jsx new file mode 100644 index 0000000..24f692e --- /dev/null +++ b/week03/Blog/src/component/list/CommentListItem.jsx @@ -0,0 +1,18 @@ +import React from 'react'; + +function CommentListItem({ text }) { + return ( +
+ {text} +
+ ); +} + +export default CommentListItem; diff --git a/week03/Blog/src/component/list/PostList.jsx b/week03/Blog/src/component/list/PostList.jsx new file mode 100644 index 0000000..516bf6c --- /dev/null +++ b/week03/Blog/src/component/list/PostList.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import PostListItem from './PostListItem'; +import styles from './PostList.module.css'; + +function PostList({ posts, onItemClick }) { + return ( +
+ {posts.map((post) => ( + onItemClick(post)} + /> + ))} +
+ ); +} + +export default PostList; diff --git a/week03/Blog/src/component/list/PostList.module.css b/week03/Blog/src/component/list/PostList.module.css new file mode 100644 index 0000000..d62cdcb --- /dev/null +++ b/week03/Blog/src/component/list/PostList.module.css @@ -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; /* 버튼이 겹치지 않게 */ +} diff --git a/week03/Blog/src/component/list/PostListItem.jsx b/week03/Blog/src/component/list/PostListItem.jsx new file mode 100644 index 0000000..5db18c8 --- /dev/null +++ b/week03/Blog/src/component/list/PostListItem.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import styles from './PostListItem.module.css'; + +function PostListItem({ title, onClick }) { + return ( +
+ {title} +
+ ); +} + +export default PostListItem; diff --git a/week03/Blog/src/component/list/PostListItem.module.css b/week03/Blog/src/component/list/PostListItem.module.css new file mode 100644 index 0000000..b111f45 --- /dev/null +++ b/week03/Blog/src/component/list/PostListItem.module.css @@ -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); +} diff --git a/week03/Blog/src/component/page/Post.jsx b/week03/Blog/src/component/page/Post.jsx new file mode 100644 index 0000000..a7becab --- /dev/null +++ b/week03/Blog/src/component/page/Post.jsx @@ -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 ( +
+
+

{post.title}

+

{post.content}

+
+ +
+
+
+ +
+
+ setComment(e.target.value)} + placeholder="댓글을 입력하세요" + /> +
+
+ +
+
+
+
+ ); +} + +Post.propTypes = { + post: PropTypes.shape({ + title: PropTypes.string, + content: PropTypes.string, + comments: PropTypes.array, + }), + onBack: PropTypes.func, + onAddComment: PropTypes.func, +}; + +export default Post; diff --git a/week03/Blog/src/component/page/Post.module.css b/week03/Blog/src/component/page/Post.module.css new file mode 100644 index 0000000..487fe7f --- /dev/null +++ b/week03/Blog/src/component/page/Post.module.css @@ -0,0 +1,59 @@ +.container { + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; + position: relative; + padding-bottom: 100px; +} + +.title { + font-size: 2.5rem; + font-weight: 800; + text-align: center; + margin: 0 0 16px 0; +} + +.content { + font-size: 1.1rem; + margin: 0 0 16px 0; + word-break: break-all; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 20px; + text-align: center; +} + +.buttonArea { + display: flex; + justify-content: flex-end; + margin-bottom: 16px; +} + +.backButton { + background: #1976d2; + color: #fff; + border: none; + border-radius: 6px; + padding: 8px 16px; + font-size: 1rem; + cursor: pointer; + transition: background 0.2s; +} + +.backButton:hover { + background: #1565c0; +} + +.commentArea { + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 120px; +} + +.commentTitle { + font-size: 1.1rem; + margin: 16px 0 8px 0; + text-align: center; +} diff --git a/week03/Blog/src/component/page/PostWritePage.jsx b/week03/Blog/src/component/page/PostWritePage.jsx new file mode 100644 index 0000000..8670e67 --- /dev/null +++ b/week03/Blog/src/component/page/PostWritePage.jsx @@ -0,0 +1,42 @@ +import React, { useState } from 'react'; +import Button from '../ui/Button'; +import TextInput from '../ui/TextInput'; +import styles from './PostWritePage.module.css'; + +function PostWrite({ onSubmit, onCancel }) { + const [title, setTitle] = useState(''); + const [content, setContent] = useState(''); + + const handleSubmit = (e) => { + e.preventDefault(); + if (!title || !content) return; + onSubmit({ title, content }); + setTitle(''); + setContent(''); + }; + + return ( +
+ setTitle(e.target.value)} + placeholder="제목 입력" + /> +