2024년 11월 7일
프로젝트 생성
drag and drop 라이브러리 사용
// App.js
import React, { useState } from "react";
import "./App.css";
import { DragDropContext } from "react-beautiful-dnd";
import { Droppable } from "react-beautiful-dnd";
import { Draggable } from "react-beautiful-dnd";
const finalSpaceCharacters = [
{
id: "gary",
name: "Gary Goodspeed",
},
{
id: "cato",
name: "Little Cato",
},
{
id: "kvn",
name: "KVN",
},
];
function App() {
const [characters, setCharacters] = useState(finalSpaceCharacters);
const handleEnd = (result) => {
// result 매개변수에는 source 항목 및 대상 위치와 같은 드래그 이벤트에 대한 정보가 포함
console.log(result);
// 목적지가 없으면 이 함수를 종료합니다.
if (!result.destination) return;
// 리액트 불변성을 지켜주기 위해 새로운 todoData 생성
const items = Array.from(characters);
// 1. 변경 시키는 아이템을 배열에서 지워주기
// 2. return 값으로 지워진 아이템을 반환함
const [reorderedItem] = items.splice(result.source.index, 1);
console.log(reorderedItem); // 지운 아이를 반환 & dropend후이니깐, result.destination.index의 순서가 바뀜
// 원하는 자리(result.destination.index)에 reorderedItem을 insert 해줍니다.
items.splice(result.destination.index, 0, reorderedItem);
setCharacters(items);
};
return (
<div className="App">
<header className="App-header">
<h1>Final Space Characters</h1>
<DragDropContext onDragEnd={handleEnd}>
<Droppable droppableId="characters">
{(provided) => (
<ul
className="characters"
{...provided.droppableProps}
ref={provided.innerRef}
>
{characters.map(({ id, name }, index) => {
return (
<Draggable key={id} draggableId={id} index={index}>
{(provided) => (
<li
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<p>{name}</p>
</li>
)}
</Draggable>
);
})}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
</header>
</div>
);
}
export default App;
// App.tsx
... 생략 ...
import { DragDropContext } from 'react-beautiful-dnd';
function App() {
... 생략 ...
const handleDragEnd = () => {
}
return (
... 생략 ...
<div className={board}>
<DragDropContext onDragEnd={handleDragEnd}>
<ListsContainer lists={lists} boardId={getActiveBoard.boardId}/>
</DragDropContext>
</div>
... 생략 ...
// components / List / List.tsx
... 생략 ...
import { Droppable } from 'react-beautiful-dnd';
... 생략 ...
const List: FC<TListProps>= ({
list,
boardId
}) => {
... 생략 ...
return (
<Droppable droppableId={list.listId}>
{provided => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className={listWrapper}>
<div className={header}>
<div className={name}>{list.listName}</div>
<GrSubtract className={deleteButton} onClick={() => handleListDelete(list.listId)}/>
</div>
{
list.tasks.map((task, index) => (
<div
onClick={() => handleTaskChange(boardId, list.listId, task.taskId, task)}
key={task.taskId}
>
<Task
taskName={task.taskName}
taskDescription={task.taskDescription}
boardId={boardId}
id={task.taskId}
index={index}
/>
</div>))
}
{provided.placeholder}
<ActionButton
boardId={boardId}
listId={list.listId}
/>
</div>
)}
</Droppable>
)
}
export default List
// components / Task / Task.tsx
... 생략 ...
import { Draggable } from 'react-beautiful-dnd';
... 생략 ...
const Task: FC<TTaskProps> = ({
index,
id,
boardId,
taskName,
taskDescription
}) => {
return (
<Draggable draggableId={id} index={index}>
{provided => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} className={container}>
<div className={title}>{taskName}</div>
<div className={description}>{taskDescription}</div>
</div>
)}
</Draggable>
)
}
export default Task
// App.tsx
... 생략 ...
import { DragDropContext } from 'react-beautiful-dnd';
function App() {
... 생략 ...
const handleDragEnd = (result: any) => {
const {destination, source, draggableId} = result;
const sourceList = lists.filter((list) => list.listId === source.droppableId)[0];
dispatch(
sort({
boardIndex: boards.findIndex(board => board.boardId === activeBoardId),
droppableIdStart: source.droppableId,
droppableIdEnd: destination.droppableId,
droppableIndexStart: source.index,
droppableIndexEnd: destination.index,
draggableId: draggableId // (task 어떤걸 전달하는지)
})
);
dispatch(
addLog({
logId: uuidv4(),
logMessage : `
리스트 "${sourceList.listName}"에서
리스트 "${lists.filter(list => list.listId === destination.droppableId)[0].listName}"으로
"${sourceList.tasks.filter(task => task.taskId === draggableId)[0]}"을 옮김."`,
logAuthor : "User",
logTimeStamp : String(Date.now())
})
);
}
return (
... 생략 ...
<div className={board}>
<DragDropContext onDragEnd={handleDragEnd}>
<ListsContainer lists={lists} boardId={getActiveBoard.boardId}/>
</DragDropContext>
</div>
... 생략 ...
// store / slice / boardSlice.ts
... 생략 ...
type TSortAction = {
boardIndex: number;
droppableIdStart: string;
droppableIdEnd: string;
droppableIndexStart: number;
droppableIndexEnd: number;
draggableId: string
}
... 생략 ...
const boardsSlice = createSlice({
name: 'boards',
initialState,
reducers :
... 생략 ...
sort : (state, {payload}: PayloadAction<TSortAction>) => {
}
}
})
export const { sort, addBoard, deleteBoard, deleteList, setModalActive, addList, addTask, updateTask, deleteTask} = boardsSlice.actions;
export const boardsReducer = boardsSlice.reducer;
// store / slice / boardsSlice.ts
... 생략 ...
sort : (state, {payload}: PayloadAction<TSortAction>) => {
// same list
if(payload.droppableIdStart === payload.droppableIdEnd) {
const list = state.boardArray[payload.boardIndex].lists.find(
list => list.listId === payload.droppableIdStart
)
// 변경시키는 아이템을 배열에서 지워주고
// return 값으로 지워진 아이템을 줌.
const card= list?.tasks.splice(payload.droppableIndexStart, 1);
list?.tasks.splice(payload.droppableIndexEnd, 0, ...card!);
}
// other list
if(payload.droppableIdStart !== payload.droppableIdEnd) {
const listStart = state.boardArray[payload.boardIndex].lists.find(
list => list.listId === payload.droppableIdStart
)
const card = listStart!.tasks.splice(payload.droppableIndexStart, 1);
const listEnd = state.boardArray[payload.boardIndex].lists.find(
list => list.listId === payload.droppableIdEnd
)
listEnd?.tasks.splice(payload.droppableIndexEnd, 0, ...card);
}
}
}
... 생략 ...
// components / BoardList / BoardList.tsx
... 생략 ...
import { GoSignOut } from 'react-icons/go';
import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
import { app } from '../../firebase';
... 생략 ...
const auth = getAuth(app);
const provider = new GoogleAuthProvider();
const handleLogin = () => {
signInWithPopup(auth, provider) // 팝업 뜨게함 -> 로그인 하고나서
.then(userCredential => { // 그 다음 과정은 여기서 처리
// 구글로 로그인한 사람의 정보가 userCredential에 있음
console.log(userCredential);
})
}
... 생략 ...
<GoSignOut className={addButton} />
<FiLogIn className={addButton} onClick={handleLogin}/>
</div>
</div>
)
}
export default BoardList
// components / BoardList / BoardList.tsx
... 생략 ...
import { GoSignOut } from 'react-icons/go';
import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
import { app } from '../../firebase';
import { setUser } from '../../store/slices/userSlice';
... 생략 ...
const dispatch = useTypedDispatch();
const auth = getAuth(app);
const provider = new GoogleAuthProvider();
const handleLogin = () => {
signInWithPopup(auth, provider) // 팝업 뜨게함 -> 로그인 하고나서
.then(userCredential => { // 그 다음 과정은 여기서 처리
// 구글로 로그인한 사람의 정보가 userCredential에 있음
console.log(userCredential);
dispatch(
setUser({
email: userCredential.user.email,
id: userCredential.user.uid,
})
);
})
.catch(error => {
console.error(error);
})
}
... 생략 ...
// store / slices / userSlice.ts
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
email: '',
id: ''
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUser : (state, action) => {
state.email = action.payload.email;
state.id = action.payload.id;
}
}
})
export const { setUser } = userSlice.actions;
export const userReducer = userSlice.reducer;
// store / reducer / reducer.ts
import { boardsReducer } from '../slices/boardsSlice';
import { loggerReducer } from '../slices/loggerSlice';
import { modalReducer } from '../slices/modalSlice';
import { userReducer } from '../slices/userSlice';
const reducer = {
logger: loggerReducer,
boards: boardsReducer,
modal: modalReducer,
user: userReducer
}
export default reducer;
// components / BoardList / BoardList.tsx
... 생략 ...
const dispatch = useTypedDispatch();
const auth = getAuth(app);
const provider = new GoogleAuthProvider();
const { isAuth } = useAuth();
// console.log(isAuth);
const handleLogin = () => {
signInWithPopup(auth, provider) // 팝업 뜨게함 -> 로그인 하고나서
.then(userCredential => { // 그 다음 과정은 여기서 처리
// 구글로 로그인한 사람의 정보가 userCredential에 있음
// console.log(userCredential);
dispatch(
setUser({
email: userCredential.user.email,
id: userCredential.user.uid,
})
);
})
.catch(error => {
console.error(error);
})
}
const handleSignOut = () => {
signOut(auth)
.then(() => {
dispatch(
removeUser()
);
})
.catch((error) => {
console.error(error);
})
}
... 생략 ...
{
isAuth ? <GoSignOut className={addButton} onClick={handleSignOut}/>
:
<FiLogIn className={addButton} onClick={handleLogin}/>
}
</div>
</div>
)
}
export default BoardList
// hooks / useAuth.ts
import { useTypedSelector } from './redux'
export function useAuth() {
const {id, email} = useTypedSelector((state) => state.user);
return {
isAuth : !!email,
email,
id
}
}
// stoer / slices / userSlice.ts
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
email: '',
id: ''
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUser : (state, action) => {
state.email = action.payload.email;
state.id = action.payload.id;
},
removeUser : (state) => {
state.email = '';
state.id = '';
}
}
})
export const { setUser, removeUser } = userSlice.actions;
export const userReducer = userSlice.reducer;
AWS, Vercell, github 등을 이용해서 배포 가능
Firebase를 이용해서 배포 예정
echo "# react-task-app" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/HongbiKo/react-task-app.gitgit push -u origin main
git add .
git commit -m "initial"
git push
firebase 서비스를 이용해야함
firebase tools 라는 모듈을 전역으로 설치해줘야함
사용
리액트앱 같은 경우, 배포를 할때 웹팩으로 파일들을 번들링해서 빌드 파일을 만듦
빌드하기
빌드가 완료 되면 → dist 폴더 생김 → 이 폴더 안의 파일들만 이용해서 배포할 것임
firebase init
깃에 푸시
오류 상황
// firebase-hosting-merge.yml
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools
name: Deploy to Firebase Hosting on merge
on:
push:
branches:
- main
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
// 아래 run 부분 넣어야함
- run: npm install && npm run build
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_REACT_TEST_APP_7E790 }}
channelId: live
projectId: react-test-app-7e790
// firebase-hosting-pull-request.yml
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools
name: Deploy to Firebase Hosting on PR
on: pull_request
permissions:
checks: write
contents: read
pull-requests: write
jobs:
build_and_preview:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
// 아래 run 부분 넣어야함
- run: npm install && npm run build
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_REACT_TEST_APP_7E790 }}
projectId: react-test-app-7e790
// firebase.json
{
"hosting": {
"public": "dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
],
// 이부분 넣어야함
"predeploy": [
"npm run build"
]
}
}