Category: React

0

React 로그인 Modal 만들기 5 - 회원 가입 Back end 구현

React 로그인 Modal 만들기 9 - Redux Saga 적용 React 로그인 Modal 만들기 8 - Redux 사용하기 React 로그인 Modal 만들기 7 - JWT 적용하기 React 로그인 Modal 만들기 6 - Login 구현 React 로그인 Modal 만들기 5 - 회원 가입 Back end 구현 React 로그인 Modal 만들기 4 - 비동기 처리 React 로그인 Modal 만들기 3 - 회원 가입 React 로그인 Modal 만들기 2 - styled-component 사용하기 React 로그인 Modal 만들기 React 로그인 Modal 만들기 5 - 회원 가입 Back end 구현회원 데이터를 저장할 UserInfo 정의회원 가입을 위해 사용자 id, username, email, password와 사용자 별로 권한을 관리하기 위한 UserInfo 객체를 관리하도록 정의 @Entity@NoArgsConstructor@AllArgsConstructor@Getter@Builderpublic class UserInfo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String username; @Column(nullable = false) private String email; @Column(nullable = false) private String password; @Enumerated(EnumType.STRING) private UserRole userRole;} User 권한 목록 권한은 3가지로 일반사용자를 위한 USER 권한, 수정/변경 권한이 있는 MANAGER 권한과 관리자인 ADMIN 3가지 권한으로 분류 한다 public enum UserRole { USER("ROLE_USER"), MANAGER("ROLE_MANAGER"), ADMIN("ROLE_ADMIN"); private String value; UserRole(String value){ this.value = value; } public String getValue(){ return this.value; }} 사용자 이름과 Email을 이용해 사용자 정보를 가져올 수 있도록 메소드를 추가한다.

0

React 로그인 Modal 만들기 4 - 비동기 처리

React 로그인 Modal 만들기 9 - Redux Saga 적용 React 로그인 Modal 만들기 8 - Redux 사용하기 React 로그인 Modal 만들기 7 - JWT 적용하기 React 로그인 Modal 만들기 6 - Login 구현 React 로그인 Modal 만들기 5 - 회원 가입 Back end 구현 React 로그인 Modal 만들기 4 - 비동기 처리 React 로그인 Modal 만들기 3 - 회원 가입 React 로그인 Modal 만들기 2 - styled-component 사용하기 React 로그인 Modal 만들기 React 로그인 Modal 만들기 4 - 비동기 처리import axios from "axios";import React, { useState, useCallback } from "react";import { LoginBody, LoginFooter, LoginHeader, ModalBackGround, ModalBox,} from "../assets/css/login";const LoginTemplate = ({ open, close }) => { const [form, setForm] = useState({ email: "", password: "", }); const onChange = useCallback( (e) => { setForm({ ...form, [e.target.name]: e.target.value, }); }, [form] ); const onClick = useCallback( (e) => { axios .post("/api/login", form) .then((response) => { console.log(response); }) .catch((err) => { console.log(err); }); }, [form] ); return open ? ( <ModalBackGround> <ModalBox> <LoginHeader> 로그인 <button className="close" onClick={close}> {" "} &times;{" "} </button> </LoginHeader> <LoginBody className="login-body"> <div> <input id="email" name="email" type="email" placeholder="Email" onChange={onChange} /> </div> <div> <input id="password" name="password" type="password" placeholder="Password" onChange={onChange} /> </div> </LoginBody> <LoginFooter> <button onClick={onClick}>로그인</button> </LoginFooter> </ModalBox> </ModalBackGround> ) : null;};export default LoginTemplate; import React, { useCallback, useState } from "react";import { ModalBox } from "../assets/css/login";import axios from "axios";import { ModalBackGround, SignUpBody, SignUpFooter, SignUpHeader,} from "../assets/css/signup";const SignUpTemplate = ({ open, onChangeOpen }) => { const [form, setForm] = useState({ username: "", email: "", password: "", }); const onChange = useCallback( (e) => { setForm({ ...form, [e.target.name]: e.target.value, }); }, [form] ); const onClickSignUp = useCallback(() => { axios.post("/api/signup", form).then((Response) => { console.log(Response); }); }, [form]); return open ? ( <ModalBackGround className="signup-modal-box"> <ModalBox> <SignUpHeader> 회원 가입 <button className="close" onClick={onChangeOpen}> {" "} &times;{" "} </button> </SignUpHeader> <SignUpBody> <div> <input type="username" id="username" name="username" placeholder="이름" onChange={onChange} /> </div> <div> <input type="email" id="email" name="email" placeholder="Email" onChange={onChange} /> </div> <div> <input type="password" id="password" name="password" placeholder="Password" onChange={onChange} /> </div> </SignUpBody> <SignUpFooter> <button onClick={onClickSignUp}>회원가입</button> </SignUpFooter> </ModalBox> </ModalBackGround> ) : null;};export default SignUpTemplate;

0

React 로그인 Modal 만들기 3 - 회원 가입

React 로그인 Modal 만들기 9 - Redux Saga 적용 React 로그인 Modal 만들기 8 - Redux 사용하기 React 로그인 Modal 만들기 7 - JWT 적용하기 React 로그인 Modal 만들기 6 - Login 구현 React 로그인 Modal 만들기 5 - 회원 가입 Back end 구현 React 로그인 Modal 만들기 4 - 비동기 처리 React 로그인 Modal 만들기 3 - 회원 가입 React 로그인 Modal 만들기 2 - styled-component 사용하기 React 로그인 Modal 만들기 React 로그인 Modal 만들기 3 - 회원 가입import styled, { keyframes } from "styled-components";export const ModalFade = keyframes`{ from { opacity: 0; margin-top: -50px; } to { opacity: 1; margin-top: 0; }}`;export const ModalBackGround = styled.div` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; animation: modal-bg-show 0.3s;`;export const ModalBox = styled.div` width: 90%; max-width: 400px; margin: 0 auto; border-radius: 0.3rem; background-color: #fff; animation: ModalFade 0.3s; overflow: hidden;`;export const SignUpHeader = styled.div` position: relative; padding: 16px 64px 16px 16px; background-color: #f1f1f1; font-weight: 700; & > button { position: absolute; top: 15px; right: 15px; width: 30px; font-size: 21px; font-weight: 700; text-align: center; color: #999; border: 0; }`;export const SignUpBody = styled.div` padding: 10px; & > div { padding: 5px; display: flex; align-items: center; justify-content: center; } & > div > input { padding-top: 5px; padding-bottom: 5px; padding-left: 15px; padding-right: 15px; width: 290px; height: 30px; font-size: 14px; border: 1px solid rgb(222, 226, 230); }`;export const SignUpFooter = styled.div` border-top: 1px solid; padding: 7px 16px; text-align: right; & > button { padding: 6px 12px; color: #fff; background-color: #6c757d; border-radius: 5px; font-size: 13px; }`; import React, { useCallback, useState } from "react";import { ModalBox } from "../assets/css/login";import { ModalBackGround, SignUpBody, SignUpFooter, SignUpHeader,} from "../assets/css/signup";const SignUpTemplate = ({ open, onChangeOpen }) => { const [form, setForm] = useState({ username: "", email: "", password: "", }); const onChange = useCallback( (e) => { setForm({ ...form, [e.target.name]: e.target.value, }); }, [form] ); return open ? ( <ModalBackGround className="signup-modal-box"> <ModalBox> <SignUpHeader> 회원 가입 <button className="close" onClick={onChangeOpen}> {" "} &times;{" "} </button> </SignUpHeader> <SignUpBody> <div> <input type="username" id="username" name="username" placeholder="이름" onChange={onChange} /> </div> <div> <input type="email" id="email" name="email" placeholder="Email" onChange={onChange} /> </div> <div> <input type="password" id="password" name="password" placeholder="Password" onChange={onChange} /> </div> </SignUpBody> <SignUpFooter> <button onClick={onChangeOpen}>닫기</button> </SignUpFooter> </ModalBox> </ModalBackGround> ) : null;};export default SignUpTemplate;

0

React 로그인 Modal 만들기 2 - styled-component 사용하기

React 로그인 Modal 만들기 9 - Redux Saga 적용 React 로그인 Modal 만들기 8 - Redux 사용하기 React 로그인 Modal 만들기 7 - JWT 적용하기 React 로그인 Modal 만들기 6 - Login 구현 React 로그인 Modal 만들기 5 - 회원 가입 Back end 구현 React 로그인 Modal 만들기 4 - 비동기 처리 React 로그인 Modal 만들기 3 - 회원 가입 React 로그인 Modal 만들기 2 - styled-component 사용하기 React 로그인 Modal 만들기 React 로그인 Modal 만들기 2 - styled-component 사용하기기존에 style 내용들을 css 파일로 관리해도 문제가 없으나, React에서는 style마저도 js파일로 관리할 수 있도록 지원해주는 다양한 프레임 워크들이 있다. js파일로 관리하게 되면 React의 여러 기능을 사용하기 좋기 때문에 기존에 작성해준 Login 관련 css 내용을 js 파일로 변경하려고 한다. import styled, { keyframes } from "styled-components";export const ModalFade = keyframes`{ from { opacity: 0; margin-top: -50px; } to { opacity: 1; margin-top: 0; }}`;export const ModalBackGround = styled.div` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center;`;export const ModalBox = styled.div` width: 90%; max-width: 400px; margin: 0 auto; border-radius: 0.3rem; background-color: #fff; animation: ${ModalFade} 0.5s; overflow: hidden;`;export const LoginHeader = styled.div` position: relative; padding: 16px 64px 16px 16px; background-color: #f1f1f1; font-weight: 700; & > button { position: absolute; top: 15px; right: 15px; width: 30px; font-size: 21px; font-weight: 700; text-align: center; color: #999; border: 0; }`;export const LoginBody = styled.div` padding: 10px; & > div { padding: 5px; display: flex; align-items: center; justify-content: center; } & > div > input { padding-top: 5px; padding-bottom: 5px; padding-left: 15px; padding-right: 15px; width: 290px; height: 30px; font-size: 14px; border: 1px solid rgb(222, 226, 230); }`;export const LoginFooter = styled.div` border-top: 1px solid rgb(222, 226, 230); padding: 7px 16px; text-align: right; & > button { padding: 6px 12px; color: #fff; background-color: #6c757d; border-radius: 5px; font-size: 13px; }`; Styled-Component 적용하기import React, { useState, useCallback } from "react";import { LoginBody, LoginFooter, LoginHeader, ModalBackGround, ModalBox,} from "../assets/css/login";const LoginTemplate = ({ open, close }) => { const [form, setForm] = useState({ email: "", password: "", }); const onChange = useCallback( (e) => { setForm({ ...form, [e.target.name]: e.target.value, }); }, [form] ); return open ? ( <ModalBackGround> <ModalBox> <LoginHeader> 로그인 <button className="close" onClick={close}> {" "} &times;{" "} </button> </LoginHeader> <LoginBody className="login-body"> <div> <input id="email" name="email" type="email" placeholder="Email" onChange={onChange} /> </div> <div> <input id="password" name="password" type="password" placeholder="Password" onChange={onChange} /> </div> </LoginBody> <LoginFooter> <button onClick={close}>닫기</button> </LoginFooter> </ModalBox> </ModalBackGround> ) : null;};export default LoginTemplate;

0

React 로그인 Modal 만들기

React 로그인 Modal 만들기 9 - Redux Saga 적용 React 로그인 Modal 만들기 8 - Redux 사용하기 React 로그인 Modal 만들기 7 - JWT 적용하기 React 로그인 Modal 만들기 6 - Login 구현 React 로그인 Modal 만들기 5 - 회원 가입 Back end 구현 React 로그인 Modal 만들기 4 - 비동기 처리 React 로그인 Modal 만들기 3 - 회원 가입 React 로그인 Modal 만들기 2 - styled-component 사용하기 React 로그인 Modal 만들기 로그인 Modal 만들기기존에 개인 프로젝트로 로그인 창을 만들게 되면 별도의 웹 페이지로 로그인 화면을 구현을 했다. 하지만 좀 더 깔끔하고 반응적인 방법을 찾다가 Modal에 대해 알게 됐고 Modal을 이용해 Login 화면을 구현해 보고자 한다. 로그인 컴포넌트 만들기가장 바깥 Wrapper는 로그인 버튼을 누른 상태와 누르지 않은 상태를 보여줘야 함으로 조건절을 이용해 classname을 변경하도록 설정한다. // Login 페이지가 열리면 modal-wrapper가 선택된다. <div className={open ? "modal-wrapper" : null}> 내부 컴포넌트도 상태에 따라 보이거나 보이지 않게 상태를 변경해줄 필요가 있어 조건절을 통해 내부 컴포넌트를 가져올 수 있도록 설정한다. {open ? ( <div className="login-modal"> <div className="login-header">로그인</div> <div className="login-input"> <div className="login-id"> <input className="email" type="email" id="email" placeholder="Email" onChange={onChangeEmail} /> </div> <div className="login-password"> <input className="password" type="password" id="password" placeholder="Password" onChange={onChangePassword} /> </div> <div className="close-login"> <button className="close-button" onClick={close}> 닫기 </button> </div> </div> </div>) : null} LoginTemplate.js

0

React Toast UI Editor 4 - 이미지 올리기

React Toast UI Editor 4 - 이미지 올리기Editor에서 이미지 업로드를 지원하다고 해서 이미지를 붙여 넣었더니 다음과 같이 이미지가 base64로 인코딩 돼 올라가는 것을 보고 살짝 당황했다. 어떻게 해야지 하면서 문득 생각이 들었던게 Markdown Editor를 사용하는 대표 블로그 사이트인 velog가 생각이 났다. 그래서 당장 velog로 가서 이미지를 올려보니! 다음과 같이 이미지가 저장된 URL을 가져오는 것을 확인 할 수 있었다. 아마 내부적으로 이미지가 들어오면 먼저 서버에 이미지를 저장하고 이미지가 저장된 경로를 받는 로직 같았다. 즉! 이미지를 업로드 하려면 이미지를 저장할 수 있는 서버를 준비해 연동할 필요가 있다는 것이다. Toast UI Editor에 Image 업로드 하기 위한 방법을 찾다 보니 처음에 연결된 Hook을 제거하고 새로운 Hook을 정의해 서버에 이미지를 올릴 수 있는 것을 알게 됐다. https://github.com/nhn/tui.editor/issues/1588 TOAST UI 에 Image Server 에 올리는 기능 구현import React, { useRef, useEffect } from "react";import axios from "axios";// Toast UI Editor 모듈 추가import { Editor } from "@toast-ui/react-editor";import "@toast-ui/editor/dist/toastui-editor.css";// SyntaxHighlight 모듈 추가import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all.js";// prism 모듈 추가import "prismjs/themes/prism.css";const Write = () => { const editorRef = useRef(); const handleClick = () => { console.log(editorRef.current.getInstance().getMarkdown()); }; useEffect(() => { if (editorRef.current) { // 기존에 Image 를 Import 하는 Hook 을 제거한다. editorRef.current.getInstance().removeHook("addImageBlobHook"); // 새롭게 Image 를 Import 하는 Hook 을 생성한다. editorRef.current .getInstance() .addHook("addImageBlobHook", (blob, callback) => { (async () => { let formData = new FormData(); formData.append("file", blob); console.log("이미지가 업로드 됐습니다."); const { data: filename } = await axios.post( "/file/upload", formData, { header: { "content-type": "multipart/formdata" }, } ); // .then((response) => { // console.log(response); // }); const imageUrl = "http://localhost:8080/file/upload/" + filename; // Image 를 가져올 수 있는 URL 을 callback 메서드에 넣어주면 자동으로 이미지를 가져온다. callback(imageUrl, "iamge"); })(); return false; }); } return () => {}; }, [editorRef]); return ( <div> <Editor initialValue="hello react editor world!" previewStyle="vertical" height="800px" initialEditType="markdown" useCommandShortcut={true} ref={editorRef} plugins={[codeSyntaxHighlight]} /> <button onClick={handleClick}>Markdown 반환하기</button> </div> );};export default Write;

0

React Toast UI Editor 3 - 입력 내용 가져오기

React Toast UI Editor 3 - 입력 내용 가져오기Editor에 입력된 내용을 저장하고 관리하기 위해서는 입력된 값을 받아와야 한다. Toast UI Editor에서는 useRef를 이용해 const editorRef = useRef(); 버튼 Event Handler 만들기버튼을 만들어줘 버튼을 눌렀을 때 Editor에 입력된 내용을 반환해 주는 Evnet를 작성한다. 입력된 값을 받아올 때는 Ref 객체인 editorRef 를 이용해 Toast UI Editor 객체를 가져와 입력된 값을 받아올 수 있도록 한다. const handleClick = () => { console.log(editorRef.current.getInstance().getMarkdown());}; Button 클릭에 대한 이벤트 핸들러와 Editor 컴포넌트에 Ref 객체를 넣어준다. <div> <Editor initialValue="hello react editor world!" previewStyle="vertical" height="600px" initialEditType="markdown" useCommandShortcut={true} ref={editorRef} plugins={[codeSyntaxHighlight]} /> <button onClick={handleClick}>Markdown 반환하기</button></div> 전체 소스

0

React Toast UI Editor 2 - Syntax Highlight 적용하기

React로 Toast UI Editor 2 - Syntax Highlight 적용하기https://github.com/nhn/tui.editor/tree/master/plugins/code-syntax-highlight 글을 쓰다보면 Post에 코드를 같이 올릴 경우가 많은데, 초기 Setting은 Code Highlight가 적용돼 있지 않는 것을 확인할 수 있었다. 아무래도 Code Highlight가 없어서 코드를 작성하게 되면 가독성도 좀 떨어지고 Post 작성할 맛도 안나서 Editor에 Code Highlight를 적용하려고 한다. 패키지 추가yarn add @toast-ui/editor-plugin-code-syntax-highlightyarn add prism import React from "react";// Toast UI Editor 모듈 추가import { Editor } from "@toast-ui/react-editor";import "@toast-ui/editor/dist/toastui-editor.css";// SyntaxHighlight 모듈 추가import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all.js";// prism 모듈 추가import "prismjs/themes/prism.css";const Write = () => { return ( <Editor initialValue="hello react editor world!" previewStyle="vertical" height="600px" initialEditType="markdown" useCommandShortcut={true} plugins={[codeSyntaxHighlight]} /> );};export default Write; Code Highlight가 적용된 것을 확인할 수 있다. 덕분에 가독성도 훨씬 좋아져 점점 맘에 들기 시작했다.

0

React로 Toast UI Editor 사용해보기

React로 Toast UI Editor 사용해보기개인 프로젝트도 하고 싶고 나만의 블로그를 만들고 싶은 마음에 UI도 찾아보고 Editor도 찾고 있었었다. 개인적으로 Editor를 찾고 있던 기준은 첫번째는 Markdown 언어가 사용 가능해야 하고 두번째는 이미지 업로드가 가능한 Editor를 찾고 있던 와중에 NHN에서 제공하는 Toast UI Editor를 찾게 됐다. 기능들이 너무너무 맘에 들어 사용해보기로 했다. Toast UI Editor에서는 내가 가장 원했던 Markdown 문법이 사용 가능 했고, 이미지 업로드도 가능 했다.!!! 그리고 코드블럭에 마음에 드는 Syntax Highlighter를 적용할 수 있는 장점과 다양한 기능들이 있어 너무나도 맘에 들었다. 플러그인 설치하기yarn add @toast-ui/react-editor Write.jsx import React from "react";import { Editor } from "@toast-ui/react-editor";import "@toast-ui/editor/dist/toastui-editor.css";const Write = () => { return ( <Editor initialValue="hello react editor world!" previewStyle="vertical" height="600px" initialEditType="markdown" useCommandShortcut={true} /> );};export default Write; 마지막으로 App.js에 새로만든 Write.jsx 컴포넌트를 추가하면 화면에서 Toast UI가 나타나는 것을 확인할 수 있다.

0

리엑트 로그인 페이지 만들기

리엑트 로그인 페이지 만들기import React, { useState, useCallback } from "react";import styled from "styled-components";import axios from "axios";const LoginFragment = styled.div` display:flex; justify-content:center; align-items:center; width:400px; height:400px; position: relative; /* 추후 박스 하단에 추가 버튼을 위치시키기 위한 설정 */ background: white; border-radius: 16px; box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.04); margin: 0 auto;`;const LoginInputs = () => { const [id, setId] = useState(""); const [password, setPassword] = useState(""); const onChangeId = useCallback((e) => { setId(e.target.value); }, []); const onChangePassword = useCallback((e) => { setPassword(e.target.value); }, []); const onSubmit = useCallback((e) => { e.preventDefault(); let data = { username: id, password: password, } console.log(data); axios.post("/api/users/login", data) .then((response => { console.log(response.data) })); }, [id, password]) return ( <LoginFragment> <form style= {{ display: "flex", flexDirection: "column", }} onSubmit={onSubmit} > <label htmlFor="user-id">아이디</label> <input name="user-id" value={id} onChange={onChangeId} /> <label htmlFor="user-password">비밀번호</label> <input name="user-password" type="password" value={password} onChange={onChangePassword} /> <br /> <button type="submit">로그인</button> </form> </LoginFragment > )}export default LoginInputs; import React from "react"import { styled, createGlobalStyle } from "styled-components";import LoginInputs from "./components/LoginInputs";const GlobalStyle = createGlobalStyle` body { background: #e9ecef; }`;function App() { return ( <div> <GlobalStyle /> <LoginInputs /> </div> )}export default App; CORS 해결하기const { createProxyMiddleware } = require("http-proxy-middleware");module.exports = (app) => { app.use( "/api", createProxyMiddleware({ target: "http://localhost:8080", changeOrigin: true, }) );};

0

리엑트 Todo List 만들기 3 - Context API를 이용해 상태 관리하기

리엑트 Todo List 만들기 1 - 기본 템플릿 만들기 리엑트 Todo List 만들기 2 - 리스트 항목 만들기 리엑트 Todo List 만들기 3 - Context API를 이용해 상태 관리하기 리엑트 Todo List 만들기 3 - Context API를 이용해 상태 관리하기Context API 사용 설정하기Reducer 생성function todoReducer(state, action) { switch (action.type) { case "CREATE": return state.concat(action.todo); case "TOGGLE": return state.map((todo) => todo.id === action.id ? { ...todo, done: !todo.done } : todo ); case "REMOVE": return state.filter((todo) => todo.id !== action.id); default: throw new Error(`Unhandled action type: ${action.type}`); }} Conext API 적용하기const TodoStateContext = createContext();const TodoDispatchContext = createContext();const TodoNextIdContext = createContext();export function TodoProvider({ children }) { const [state, dispatch] = useReducer(todoReducer, initialTodos); const nextId = useRef(5); return ( <TodoStateContext.Provider value={state}> <TodoDispatchContext.Provider value={dispatch}> <TodoNextIdContext.Provider value={nextId}> {children} </TodoNextIdContext.Provider> </TodoDispatchContext.Provider> </TodoStateContext.Provider> );} Context 사용을 위한 Custom Hook 생성export function useTodoState() { return useContext(TodoStateContext);}export function useTodoDispatch() { return useContext(TodoDispatchContext);}export function useTodoNextId() { return useContext(TodoNextIdContext);}

0

리엑트 Todo List 만들기 2 - 리스트 항목 만들기

리엑트 Todo List 만들기 1 - 기본 템플릿 만들기 리엑트 Todo List 만들기 2 - 리스트 항목 만들기 리엑트 Todo List 만들기 3 - Context API를 이용해 상태 관리하기 리엑트 Todo List 만들기 2- 리스트 항목 만들기 TodoCreate.js import React, { useState } from 'react';import styled, { css } from 'styled-components';import { MdAdd } from 'react-icons/md';const CircleButton = styled.button` background: #38d9a9; &:hover { background: #63e6be; } &:active { background: #20c997; } z-index: 5; cursor: pointer; width: 80px; height: 80px; display: block; align-items: center; justify-content: center; font-size: 60px; position: absolute; left: 50%; bottom: 0px; transform: translate(-50%, 50%); color: white; border-radius: 50%; border: none; outline: none; display: flex; align-items: center; justify-content: center; transition: 0.125s all ease-in; ${props => props.open && css` background: #ff6b6b; &:hover { background: #ff8787; } &:active { background: #fa5252; } transform: translate(-50%, 50%) rotate(45deg); `}`;const InsertFormPositioner = styled.div` width: 100%; bottom: 0; left: 0; position: absolute;`;const InsertForm = styled.form` background: #f8f9fa; padding-left: 32px; padding-top: 32px; padding-right: 32px; padding-bottom: 72px; border-bottom-left-radius: 16px; border-bottom-right-radius: 16px; border-top: 1px solid #e9ecef;`;const Input = styled.input` padding: 12px; border-radius: 4px; border: 1px solid #dee2e6; width: 100%; outline: none; font-size: 18px; box-sizing: border-box;`;function TodoCreate() { const [open, setOpen] = useState(false); const onToggle = () => setOpen(!open); const isOpen = (open) =>{ if(open){ return( <InsertFormPositioner> <InsertForm> <Input autoFocus placeholder="할 일을 입력 후, Enter 를 누르세요" /> </InsertForm> </InsertFormPositioner>) }else{ return null; } } return ( <div> {isOpen(open)} <CircleButton onClick={onToggle} open={open}> <MdAdd /> </CircleButton> </div> );}export default TodoCreate; TodoItem.js import React from "react";import styled, { css } from "styled-components";import { MdDone, MdDelete } from "react-icons/md";import { useTodoDispatch } from "../TodoContext";const Remove = styled.div` display: flex; align-items: center; justify-content: center; color: #dee2e6; font-size: 24px; cursor: pointer; opacity: 0; &:hover { color: #ff6b6b; }`;const TodoItemBlock = styled.div` display: flex; align-items: center; padding-top: 12px; padding-bottom: 12px; &:hover { ${Remove} { opacity: 1; } }`;const CheckCircle = styled.div` width: 32px; height: 32px; border-radius: 16px; border: 1px solid #ced4da; font-size: 24px; display: flex; align-items: center; justify-content: center; margin-right: 20px; cursor: pointer; ${(props) => props.done && css` border: 1px solid #38d9a9; color: #38d9a9; `}`;const Text = styled.div` flex: 1; font-size: 21px; color: #495057; ${(props) => props.done && css` color: #ced4da; `}`;function TodoItem({ id, done, text }) { const dispatch = useTodoDispatch(); const onToggle = () => dispatch({ type: "TOGGLE", id }); const onRemove = () => dispatch({ type: "REMOVE", id }); return ( <TodoItemBlock> <CheckCircle done={done} onClick={onToggle}> {done && <MdDone />} </CheckCircle> <Text done={done}>{text}</Text> <Remove onClick={onRemove}> <MdDelete /> </Remove> </TodoItemBlock> );}export default TodoItem;

0

리엑트 Todo List 만들기 1 - 기본 템플릿 만들기

리엑트 Todo List 만들기 1 - 기본 템플릿 만들기 리엑트 Todo List 만들기 2 - 리스트 항목 만들기 리엑트 Todo List 만들기 3 - Context API를 이용해 상태 관리하기 리엑트 Todo List 만들기 1 - 기본 템플릿 만들기 TodoTemplate.js import React from "react";import styled from "styled-components";const TodoTemplateBlock = styled.div` width: 512px; height: 768px; position: relative; /* 추후 박스 하단에 추가 버튼을 위치시키기 위한 설정 */ background: white; border-radius: 16px; box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.04); margin: 0 auto; /* 페이지 중앙에 나타나도록 설정 */ margin-top: 96px; margin-bottom: 32px; display: flex; flex-direction: column;`;const TodoTemplate = ({ children }) => { return <TodoTemplateBlock>{children}</TodoTemplateBlock>;}export default TodoTemplate; TodoHead.js import React from 'react';import styled from 'styled-components';import { useTodoState } from '../TodoContext';const TodoHeadBlock = styled.div` padding-top: 48px; padding-left: 32px; padding-right: 32px; padding-bottom: 24px; border-bottom: 1px solid #e9ecef; h1 { margin: 0; font-size: 36px; color: #343a40; } .day { margin-top: 4px; color: #868e96; font-size: 21px; } .tasks-left { color: #20c997; font-size: 18px; margin-top: 40px; font-weight: bold; }`;function TodoHead() { const todos = useTodoState(); const undoneTasks = todos.filter(todo => !todo.done); const today = new Date(); const dateString = today.toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' }); const dayName = today.toLocaleDateString('ko-KR', { weekday: 'long' }); return ( <TodoHeadBlock> <h1>{dateString}</h1> <div className="day">{dayName}</div> <div className="tasks-left">할 일 {undoneTasks.length}개 남음</div> </TodoHeadBlock> );}export default TodoHead;

0

리엑트 블로그 만들기 15 - Post Card List 만들기 2

리엑트 블로그 만들기 1 - 초기 setting 하기 리엑트 블로그 만들기 2 - Footer 작성하기 리엑트 블로그 만들기 3 - Header 작성하기 리엑트 블로그 만들기 4 - Navbar 작성하기 리엑트 블로그 만들기 5 - 리덕스 세팅하기 리엑트 블로그 만들기 6 - Login Modal 만들기 1 리엑트 블로그 만들기 7 - Login Modal 만들기 2 리엑트 블로그 만들기 8 - 로그인 & 로그아웃 리엑트 블로그 만들기 9 - 회원가입 리엑트 블로그 만들기 10 - 회원가입 2 리엑트 블로그 만들기 11 - 회원가입 리덕스 작업 리엑트 블로그 만들기 12 - Loading 만들기 리엑트 블로그 만들기 13 - 라우트 리엑트 블로그 만들기 14 - Post Card List 만들기 리엑트 블로그 만들기 15 - Post Card List 만들기 2 리엑트 블로그 만들기 15 - Post Card List 만들기 2import { POSTS_LOADING_FAILURE, POSTS_LOADING_REQUEST, POSTS_LOADING_SUCCESS } from "../types";const initialState = { isAuthenticated: null, posts: [], postDetails: "", postCount: "", loading: false, error: "", creatorId: "", categoryFindResult: "", title: "", searchBy: "", searchResult: "",};export default function postReducer(state = initialState, action) { switch (action.type) { case POSTS_LOADING_REQUEST: return { ...state, posts: [], loading: true, } case POSTS_LOADING_SUCCESS: return { ...state, posts: [...state.posts, ...action.payload], loading: false, }; case POSTS_LOADING_FAILURE: return { ...state, loading: false, }; default: return state; };} // POST WRITEexport const POSTS_WRITE_REQUEST = "POST_WRITE_REQUEST";export const POSTS_WRITE_FAILURE = "POST_WRITE_FAILURE";export const POSTS_WRITE_SUCCESS = "POST_WRITE_SUCCESS";// POST LOADINGexport const POSTS_LOADING_REQUEST = "POST_LOADING_REQUEST";export const POSTS_LOADING_FAILURE = "POST_LOADING_FAILURE";export const POSTS_LOADING_SUCCESS = "POST_LOADING_SUCCESS"; import { combineReducers } from "redux";import { connectRouter } from "connected-react-router";import authReducer from "./authReducer.js";import postReducer from "./postReducer.js";import commentReducer from "./commentReducer.js";const createRootReducer = (history) => combineReducers({ router: connectRouter(history), auth: authReducer, post: postReducer, comment: commentReducer, });export default createRootReducer; import axios from "axios";import { put, call, takeEvery, all, fork } from "redux-saga/effects";import { push } from "connected-react-router";import { POSTS_LOADING_FAILURE, POSTS_LOADING_SUCCESS, POSTS_LOADING_REQUEST, POST_UPLOADING_SUCCESS, POST_UPLOADING_FAILURE, POST_UPLOADING_REQUEST, POST_DETAIL_LOADING_SUCCESS, POST_DETAIL_LOADING_FAILURE, POST_DETAIL_LOADING_REQUEST, POST_DELETE_SUCCESS, POST_DELETE_FAILURE, POST_DELETE_REQUEST, POST_EDIT_LOADING_SUCCESS, POST_EDIT_LOADING_FAILURE, POST_EDIT_UPLOADING_SUCCESS, POST_EDIT_UPLOADING_FAILURE, POST_EDIT_UPLOADING_REQUEST, POST_EDIT_LOADING_REQUEST, CATEGORY_FIND_FAILURE, CATEGORY_FIND_SUCCESS, CATEGORY_FIND_REQUEST, SEARCH_SUCCESS, SEARCH_FAILURE, SEARCH_REQUEST,} from "../types";// All Posts loadconst loadPostAPI = (payload) => { return axios.get(`/api/post/skip/${payload}`);};function* loadPosts(action) { try { const result = yield call(loadPostAPI, action.payload); console.log(result, "loadPosts"); yield put({ type: POSTS_LOADING_SUCCESS, payload: result.data, }); } catch (e) { yield put({ type: POSTS_LOADING_FAILURE, payload: e, }); }}function* watchLoadPosts() { yield takeEvery(POSTS_LOADING_REQUEST, loadPosts);}// Post Uploadconst uploadPostAPI = (payload) => { const config = { headers: { "Content-Type": "application/json", }, }; const token = payload.token; if (token) { config.headers["x-auth-token"] = token; } return axios.post("/api/post", payload, config);};function* uploadPosts(action) { try { console.log(action, "uploadPost function"); const result = yield call(uploadPostAPI, action.payload); console.log(result, "uploadPostAPI, action.payload"); yield put({ type: POST_UPLOADING_SUCCESS, payload: result.data, }); yield put(push(`/post/${result.data._id}`)); } catch (e) { yield put({ type: POST_UPLOADING_FAILURE, payload: e, }); yield put(push("/")); }}function* watchuploadPosts() { yield takeEvery(POST_UPLOADING_REQUEST, uploadPosts);}// Post Detailconst loadPostDetailAPI = (payload) => { console.log(payload); return axios.get(`/api/post/${payload}`);};function* loadPostDetail(action) { try { console.log(action); const result = yield call(loadPostDetailAPI, action.payload); console.log(result, "post_detail_saga_data"); yield put({ type: POST_DETAIL_LOADING_SUCCESS, payload: result.data, }); } catch (e) { yield put({ type: POST_DETAIL_LOADING_FAILURE, payload: e, }); yield put(push("/")); }}function* watchloadPostDetail() { yield takeEvery(POST_DETAIL_LOADING_REQUEST, loadPostDetail);}// Post Deleteconst DeletePostAPI = (payload) => { const config = { headers: { "Content-Type": "application/json", }, }; const token = payload.token; if (token) { config.headers["x-auth-token"] = token; } return axios.delete(`/api/post/${payload.id}`, config);};function* DeletePost(action) { try { const result = yield call(DeletePostAPI, action.payload); yield put({ type: POST_DELETE_SUCCESS, payload: result.data, }); yield put(push("/")); } catch (e) { yield put({ type: POST_DELETE_FAILURE, payload: e, }); }}function* watchDeletePost() { yield takeEvery(POST_DELETE_REQUEST, DeletePost);}// Post Edit Loadconst PostEditLoadAPI = (payload) => { const config = { headers: { "Content-Type": "application/json", }, }; const token = payload.token; if (token) { config.headers["x-auth-token"] = token; } return axios.get(`/api/post/${payload.id}/edit`, config);};function* PostEditLoad(action) { try { const result = yield call(PostEditLoadAPI, action.payload); yield put({ type: POST_EDIT_LOADING_SUCCESS, payload: result.data, }); } catch (e) { yield put({ type: POST_EDIT_LOADING_FAILURE, payload: e, }); yield put(push("/")); }}function* watchPostEditLoad() { yield takeEvery(POST_EDIT_LOADING_REQUEST, PostEditLoad);}// Post Edit UpLoadconst PostEditUploadAPI = (payload) => { const config = { headers: { "Content-Type": "application/json", }, }; const token = payload.token; if (token) { config.headers["x-auth-token"] = token; } return axios.post(`/api/post/${payload.id}/edit`, payload, config);};function* PostEditUpload(action) { try { const result = yield call(PostEditUploadAPI, action.payload); yield put({ type: POST_EDIT_UPLOADING_SUCCESS, payload: result.data, }); yield put(push(`/post/${result.data._id}`)); } catch (e) { yield put({ type: POST_EDIT_UPLOADING_FAILURE, payload: e, }); }}function* watchPostEditUpload() { yield takeEvery(POST_EDIT_UPLOADING_REQUEST, PostEditUpload);}// Category Findconst CategoryFindAPI = (payload) => { return axios.get(`/api/post/category/${encodeURIComponent(payload)}`);};function* CategoryFind(action) { try { const result = yield call(CategoryFindAPI, action.payload); yield put({ type: CATEGORY_FIND_SUCCESS, payload: result.data, }); } catch (e) { yield put({ type: CATEGORY_FIND_FAILURE, payload: e, }); }}function* watchCategoryFind() { yield takeEvery(CATEGORY_FIND_REQUEST, CategoryFind);}// Search Findconst SearchResultAPI = (payload) => { return axios.get(`/api/search/${encodeURIComponent(payload)}`);};function* SearchResult(action) { try { const result = yield call(SearchResultAPI, action.payload); yield put({ type: SEARCH_SUCCESS, payload: result.data, }); yield put(push(`/search/${encodeURIComponent(action.payload)}`)); } catch (e) { yield put({ type: SEARCH_FAILURE, payload: e, }); yield put(push("/")); }}function* watchSearchResult() { yield takeEvery(SEARCH_REQUEST, SearchResult);}export default function* postSaga() { yield all([ fork(watchLoadPosts), fork(watchuploadPosts), fork(watchloadPostDetail), fork(watchDeletePost), fork(watchPostEditLoad), fork(watchPostEditUpload), fork(watchCategoryFind), fork(watchSearchResult), ]);} import React, { Fragment } from "react";import { Row, Spinner } from "reactstrap";export const GrowingSpinner = ( <Fragment> <Row className="d-flex justify-content-center m-5"> <Spinner style={{ width: "2rem", height: "2rem" }} type="grow" color="primary" /> <Spinner style={{ width: "2rem", height: "2rem" }} type="grow" color="secondary" /> <Spinner style={{ width: "2rem", height: "2rem" }} type="grow" color="success" /> <Spinner style={{ width: "2rem", height: "2rem" }} type="grow" color="danger" /> <Spinner style={{ width: "2rem", height: "2rem" }} type="grow" color="warning" /> <Spinner style={{ width: "2rem", height: "2rem" }} type="grow" color="info" /> <Spinner style={{ width: "2rem", height: "2rem" }} type="grow" color="light" /> <Spinner style={{ width: "2rem", height: "2rem" }} type="grow" color="dark" /> </Row> </Fragment>); import React, { Fragment } from "react";import { Card, CardImg, CardBody, CardTitle, Button, Badge, Row,} from "reactstrap";import { Link } from "react-router-dom";import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";import { faMouse } from "@fortawesome/free-solid-svg-icons";const PostCardOne = ({ posts }) => { return ( <Fragment> {Array.isArray(posts) ? posts.map(({ _id, title, fileUrl, comments, views }) => { return ( <div key={_id} className="col-md-4"> <Link to={`/post/${_id}`} className="text-dark text-decoration-none" > <Card className="mb-3"> <CardImg top alt="카드이미지" src={fileUrl} /> <CardBody> <CardTitle className="text-truncate d-flex justify-content-between"> <span className="text-truncate">{title} </span> <span> <FontAwesomeIcon icon={faMouse} /> &nbsp;&nbsp; <span>{views}</span> </span> </CardTitle> <Row> <Button color="primary" className="p-2 btn-block"> More <Badge color="light">{comments.length}</Badge> </Button> </Row> </CardBody> </Card> </Link> </div> ); }) : ""} </Fragment> );};export default PostCardOne;

0

리엑트 블로그 만들기 14 - Post Card List 만들기

리엑트 블로그 만들기 1 - 초기 setting 하기 리엑트 블로그 만들기 2 - Footer 작성하기 리엑트 블로그 만들기 3 - Header 작성하기 리엑트 블로그 만들기 4 - Navbar 작성하기 리엑트 블로그 만들기 5 - 리덕스 세팅하기 리엑트 블로그 만들기 6 - Login Modal 만들기 1 리엑트 블로그 만들기 7 - Login Modal 만들기 2 리엑트 블로그 만들기 8 - 로그인 & 로그아웃 리엑트 블로그 만들기 9 - 회원가입 리엑트 블로그 만들기 10 - 회원가입 2 리엑트 블로그 만들기 11 - 회원가입 리덕스 작업 리엑트 블로그 만들기 12 - Loading 만들기 리엑트 블로그 만들기 13 - 라우트 리엑트 블로그 만들기 14 - Post Card List 만들기 리엑트 블로그 만들기 15 - Post Card List 만들기 2 리엑트 블로그 만들기 14 - Post Card List 만들기import { POSTS_LOADING_FAILURE, POSTS_LOADING_REQUEST, POSTS_LOADING_SUCCESS } from "../types";const initialState = { isAuthenticated: null, posts: [], postDetails: "", postCount: "", loading: false, error: "", creatorId: "", categoryFindResult: "", title: "", searchBy: "", searchResult: "",};export default function postReducer(state = initialState, action) { switch (action.type) { case POSTS_LOADING_REQUEST: return { ...state, posts: [], loading: true, } case POSTS_LOADING_SUCCESS: return { ...state, posts: [...state.posts, ...action.payload], loading: false, }; case POSTS_LOADING_FAILURE: return { ...state, loading: false, }; default: return state; };} // POST WRITEexport const POSTS_WRITE_REQUEST = "POST_WRITE_REQUEST";export const POSTS_WRITE_FAILURE = "POST_WRITE_FAILURE";export const POSTS_WRITE_SUCCESS = "POST_WRITE_SUCCESS";// POST LOADINGexport const POSTS_LOADING_REQUEST = "POST_LOADING_REQUEST";export const POSTS_LOADING_FAILURE = "POST_LOADING_FAILURE";export const POSTS_LOADING_SUCCESS = "POST_LOADING_SUCCESS"; import { combineReducers } from "redux";import { connectRouter } from "connected-react-router";import authReducer from "./authReducer.js";import postReducer from "./postReducer.js";import commentReducer from "./commentReducer.js";const createRootReducer = (history) => combineReducers({ router: connectRouter(history), auth: authReducer, post: postReducer, comment: commentReducer, });export default createRootReducer; import axios from "axios";import { put, call, takeEvery, all, fork } from "redux-saga/effects";import { push } from "connected-react-router";import { POSTS_LOADING_FAILURE, POSTS_LOADING_SUCCESS, POSTS_LOADING_REQUEST, POST_UPLOADING_SUCCESS, POST_UPLOADING_FAILURE, POST_UPLOADING_REQUEST, POST_DETAIL_LOADING_SUCCESS, POST_DETAIL_LOADING_FAILURE, POST_DETAIL_LOADING_REQUEST, POST_DELETE_SUCCESS, POST_DELETE_FAILURE, POST_DELETE_REQUEST, POST_EDIT_LOADING_SUCCESS, POST_EDIT_LOADING_FAILURE, POST_EDIT_UPLOADING_SUCCESS, POST_EDIT_UPLOADING_FAILURE, POST_EDIT_UPLOADING_REQUEST, POST_EDIT_LOADING_REQUEST, CATEGORY_FIND_FAILURE, CATEGORY_FIND_SUCCESS, CATEGORY_FIND_REQUEST, SEARCH_SUCCESS, SEARCH_FAILURE, SEARCH_REQUEST,} from "../types";// All Posts loadconst loadPostAPI = (payload) => { return axios.get(`/api/post/skip/${payload}`);};function* loadPosts(action) { try { const result = yield call(loadPostAPI, action.payload); console.log(result, "loadPosts"); yield put({ type: POSTS_LOADING_SUCCESS, payload: result.data, }); } catch (e) { yield put({ type: POSTS_LOADING_FAILURE, payload: e, }); }}function* watchLoadPosts() { yield takeEvery(POSTS_LOADING_REQUEST, loadPosts);}// Post Uploadconst uploadPostAPI = (payload) => { const config = { headers: { "Content-Type": "application/json", }, }; const token = payload.token; if (token) { config.headers["x-auth-token"] = token; } return axios.post("/api/post", payload, config);};function* uploadPosts(action) { try { console.log(action, "uploadPost function"); const result = yield call(uploadPostAPI, action.payload); console.log(result, "uploadPostAPI, action.payload"); yield put({ type: POST_UPLOADING_SUCCESS, payload: result.data, }); yield put(push(`/post/${result.data._id}`)); } catch (e) { yield put({ type: POST_UPLOADING_FAILURE, payload: e, }); yield put(push("/")); }}function* watchuploadPosts() { yield takeEvery(POST_UPLOADING_REQUEST, uploadPosts);}// Post Detailconst loadPostDetailAPI = (payload) => { console.log(payload); return axios.get(`/api/post/${payload}`);};function* loadPostDetail(action) { try { console.log(action); const result = yield call(loadPostDetailAPI, action.payload); console.log(result, "post_detail_saga_data"); yield put({ type: POST_DETAIL_LOADING_SUCCESS, payload: result.data, }); } catch (e) { yield put({ type: POST_DETAIL_LOADING_FAILURE, payload: e, }); yield put(push("/")); }}function* watchloadPostDetail() { yield takeEvery(POST_DETAIL_LOADING_REQUEST, loadPostDetail);}// Post Deleteconst DeletePostAPI = (payload) => { const config = { headers: { "Content-Type": "application/json", }, }; const token = payload.token; if (token) { config.headers["x-auth-token"] = token; } return axios.delete(`/api/post/${payload.id}`, config);};function* DeletePost(action) { try { const result = yield call(DeletePostAPI, action.payload); yield put({ type: POST_DELETE_SUCCESS, payload: result.data, }); yield put(push("/")); } catch (e) { yield put({ type: POST_DELETE_FAILURE, payload: e, }); }}function* watchDeletePost() { yield takeEvery(POST_DELETE_REQUEST, DeletePost);}// Post Edit Loadconst PostEditLoadAPI = (payload) => { const config = { headers: { "Content-Type": "application/json", }, }; const token = payload.token; if (token) { config.headers["x-auth-token"] = token; } return axios.get(`/api/post/${payload.id}/edit`, config);};function* PostEditLoad(action) { try { const result = yield call(PostEditLoadAPI, action.payload); yield put({ type: POST_EDIT_LOADING_SUCCESS, payload: result.data, }); } catch (e) { yield put({ type: POST_EDIT_LOADING_FAILURE, payload: e, }); yield put(push("/")); }}function* watchPostEditLoad() { yield takeEvery(POST_EDIT_LOADING_REQUEST, PostEditLoad);}// Post Edit UpLoadconst PostEditUploadAPI = (payload) => { const config = { headers: { "Content-Type": "application/json", }, }; const token = payload.token; if (token) { config.headers["x-auth-token"] = token; } return axios.post(`/api/post/${payload.id}/edit`, payload, config);};function* PostEditUpload(action) { try { const result = yield call(PostEditUploadAPI, action.payload); yield put({ type: POST_EDIT_UPLOADING_SUCCESS, payload: result.data, }); yield put(push(`/post/${result.data._id}`)); } catch (e) { yield put({ type: POST_EDIT_UPLOADING_FAILURE, payload: e, }); }}function* watchPostEditUpload() { yield takeEvery(POST_EDIT_UPLOADING_REQUEST, PostEditUpload);}// Category Findconst CategoryFindAPI = (payload) => { return axios.get(`/api/post/category/${encodeURIComponent(payload)}`);};function* CategoryFind(action) { try { const result = yield call(CategoryFindAPI, action.payload); yield put({ type: CATEGORY_FIND_SUCCESS, payload: result.data, }); } catch (e) { yield put({ type: CATEGORY_FIND_FAILURE, payload: e, }); }}function* watchCategoryFind() { yield takeEvery(CATEGORY_FIND_REQUEST, CategoryFind);}// Search Findconst SearchResultAPI = (payload) => { return axios.get(`/api/search/${encodeURIComponent(payload)}`);};function* SearchResult(action) { try { const result = yield call(SearchResultAPI, action.payload); yield put({ type: SEARCH_SUCCESS, payload: result.data, }); yield put(push(`/search/${encodeURIComponent(action.payload)}`)); } catch (e) { yield put({ type: SEARCH_FAILURE, payload: e, }); yield put(push("/")); }}function* watchSearchResult() { yield takeEvery(SEARCH_REQUEST, SearchResult);}export default function* postSaga() { yield all([ fork(watchLoadPosts), fork(watchuploadPosts), fork(watchloadPostDetail), fork(watchDeletePost), fork(watchPostEditLoad), fork(watchPostEditUpload), fork(watchCategoryFind), fork(watchSearchResult), ]);}