0%

Input

첫 번째 줄에는 지도의 크기 N(정사각형이므로 가로와 세로의 크기는 같으며 5≤N≤25)이 입력되고,

그 다음 N줄에는 각각 N개의 자료(0혹은 1)가 입력된다.

1
2
N = int(input())
apt = [list(int(c) for c in input()) for _ in range(N)]

DFS

  • 배열의 요소를 하나씩 방문하면서, 1일 경우 위/아래/왼/오가 1인지 체크하고,
  • 1일 경우, 방문했던 곳이 아니라면 동일한 방법으로 체크한다.
1
2
3
visited = [[False]*N for _ in range(N)] # apt for문 돌면서 중복해서 방문하지 않도록 체크

dir = [[0,1], [0,-1], [1,0], [-1,0]]
1
2
3
4
5
for i in range(N):
for j in range(N):
if apt[i][j] == 1 and not visited[i][j]:
count = 0
dfs(i, j)
1
2
3
4
5
6
7
for d in dir:
y = dy + d[0]
x = dx + d[1]

if 0 <= y < N and 0 <= x < N:
if apt[y][x] and not visited[y][x]:
dfs(y, x)
  • 이 때, 단지별 아파트 개수를 출력하기 위해 count+1 을 수행한다.
1
2
3
4
def dfs(dy, dx):
visited[dy][dx] = True
global count
count += 1
  • apt[i][j] == 0 or visited[i][j] == True 이면 더 이상 dfs 를 호출하지 않고, 이는 즉 단지가 구성되었음을 의미
  • 다음 단지로 넘어가기 전 sort 를 사용하기 위해 list에 count를 append한다.
1
2
3
4
if apt[i][j] == 1 and not visited[i][j]:
count = 0
dfs(i, j)
countList.append(count)

Output

첫 번째 줄에는 총 단지수를 출력하시오.

그리고 각 단지내 집의 수를 오름차순으로 정렬하여 한 줄에 하나씩 출력하시오.

1
2
3
4
countList.sort()
print(len(countList))
for num in countList:
print(num)

Input

첫째 줄에 A(1 ≤ A ≤ 9999), P(1 ≤ P ≤ 5)가 주어진다.

1
A, P = map(int, input().split())

DFS

  • 반복되는 수를 찾을 때까지 같은 작업 반복
  • 반복되는 수를 찾았을 경우, 배열에 추가하지 않고 처음에 담겨있는 index를 받아 출력하면 중복되지 않는 수의 개수와 동일하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
visited = []

def dfs(num):
.
.
.

# 중복되지 않을 경우
visited.append(num)
digit = [int(x) for x in str(num)]
now = 0

for x in digit:
now += x ** P

dfs(now)

Output

첫째 줄에 반복되는 부분을 제외했을 때, 수열에 남게 되는 수들의 개수를 출력한다.

1
2
3
4
if num in visited:
idx = visited.index(num)
print(idx)
return

dependencies

These are your normal dependencies, or rather ones that you need when running your code (e.g. React or ImmutableJS).

devDependencies

These are your development dependencies. Dependencies that you need at some point in the development workflow but not while running your code (e.g. Babel or Flow).

Input

첫째 줄에 테스트 케이스의 개수 T가 주어진다.

각 테스트 케이스의 첫째 줄에는 순열의 크기 N (2 ≤ N ≤ 1,000)이 주어진다.

둘째 줄에는 순열이 주어지며, 각 정수는 공백으로 구분되어 있다.

1
2
3
4
5
6
T = int(input())

for _ in range(T):
N = int(input())
P = list(map(int, input().split()))
P.insert(0, None)

BFS

  • 각 index에 들어있는 value를 다시 index로 설정해서 visited[index] == False 일 때까지 이를 반복한다.

  • 결국 queue가 빌 때까지 이를 반복한다고 생각하면, BFS로 해결할 수 있다.

  • 해당 사이클이 모두 visited 라면, 1을 반환하여 count++ 한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def bfs(idx):
    queue = [idx]

    while queue:
    now = queue.pop(0)
    if not visited[now]:
    visited[now] = True
    val = P[now]
    queue.append(val)

    return 1
  • 이후 다음 사이클로 넘어가야 하는데, 이는 결국 배열의 모든 요소를 돌아 사이클을 확인해봐야 하는 것과 같다. 따라서 visited 를 활용해 이미 방문한 요소 외의 것들의 사이클을 확인한다.

    1
    2
    3
    4
    5
    6
    7
    visited = [False for _ in range(N+1)]
    count = 0

    # idx -> val -> idx 계속 따라가기
    for idx in range(1, N+1):
    if not visited[idx]:
    count += bfs(idx)

Better Solution

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
### 런타임 에러 바로잡기!!! `sys.setrecursionlimit(2000)`
import sys
sys.setrecursionlimit(2000) #최대 재귀를 늘려줘야 런타임 에러를 피할 수 있다

def dfs(x): #DFS 함수 정의
visited[x] = True #방문 체크
number = numbers[x] #다음 방문 장소
if not visited[number]: #방문하지 않았다면
dfs(number) #재귀

# 변수 할당 없이 바로 for문 돌리기! (재사용 안 함)
for _ in range(int(input())):
N = int(input())

# index를 임의로 설정하기 위해서,
# - 따로 생성한 뒤 append이든
# - 초기화 후 insert 하지 않고!
### `[0] +` 로 생성할 때 바로 임의로 초기화 값 전달!
numbers = [0] + list(map(int, input().split()))

# for 문 대신, 동일한 값으로 초기화 시 `*` 사용하기!
visited = [True] + [False] * N #방문여부확인용
result = 0

for i in range(1, N+1):
if not visited[i]: #방문하지 않았다면
# dfs의 역할:
# 1. visited 체크
# 2. 재귀호출로 사이클 체크 (visited)
dfs(i) #DFS실행
result += 1 #결과값 += 1
print(result)

Output

각 테스트 케이스마다, 입력으로 주어진 순열에 존재하는 순열 사이클의 개수를 출력한다.

1
print(count)  

Input

첫째 줄에 두 정수 N, M(2 ≤ N, M ≤ 100)이 주어진다.

다음 N개의 줄에는 M개의 정수로 미로가 주어진다.

  • 각각의 수들은 붙어서 입력으로 주어진다.
1
2
3
4
N, M = map(int, input().split())
miro = []
for _ in range(N):
miro.append(list(int(s) for s in input()))

BFS

  • 모든 경우의 수를 배열에 담아놓는다.

    1
    2
    dy = [-1,0,0,1]
    dx = [0,-1,1,0]
  • 한 경우에 대해서가 아니라, 모든 경우의 수에 대해서 차례대로 확인 및 저장하므로

  • BFS = 큐를 이용한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    queue = [[0,0]]

    while(queue):
    a, b = queue[0][0], queue[0][1]
    del queue[0]

    for i in range(4):
    y = a + dy[i]
    x = b + dx[i]

    if 0 <= y < N and 0 <= x < M and miro[y][x] == 1:
    miro[y][x] = miro[a][b] + 1
    queue.append([y,x])

Output

첫째 줄에 지나야 하는 최소의 칸 수를 출력한다.

  • 항상 도착위치로 이동할 수 있는 경우만 입력으로 주어진다.
1
print(miro[N-1][M-1])

Input

첫째 줄에 정점의 개수 N(1 ≤ N ≤ 1,000), 간선의 개수 M(1 ≤ M ≤ 10,000), 탐색을 시작할 정점의 번호 V가 주어진다.

다음 M개의 줄에는 간선이 연결하는 두 정점의 번호가 주어진다.

1
2
3
4
5
6
N, M, V = map(int, input().split())
visited = [False for _ in range(N+1)]
graph = [[0]*(N+1) for _ in range(N+1)] # for 문이 아니라 *로 초기화
for _ in range(M):
s, e = map(int, input().split())
graph[s][e] = graph[e][s] = 1 # 한 줄로 동일값 넣기

DFS

  • 들어오는 대로 바로바로 처리 (먼저 깊게 들어가고 다시 올라와 옆으로)

  • LIFO = 스택

1
2
3
4
5
6
7
8
9
10
# 밑으로 방문 후 pop
def dfs(V):
# end 조건이 없어도 되는 이유
# for 1~N 까지 돌면서, 남은 요소 중 조건을 만족하는 게 없으면 끝남

visited[V] = True
print(V, end=" ")
for i in range(1, N+1):
if graph[V][i] == 1 and not visited[i]:
dfs(i)

BFS

  • 같은 레벨 전부, 그 다음 밑으로
  • FIFO = 큐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 옆으로 방문하며 append
# 이전 V의 자식들 방문
def bfs(V):

# dfs와 동일한 배열을 사용하므로, True/False 번갈아가며...
visited[V] = False
queue = [V]

while queue:
V = queue.pop(0)
print(V, end=" ")
i = 1

for i in range(1, N+1):
if graph[V][i] == 1 and visited[i]:
visited[i] = False
queue.append(i)

Output

첫째 줄에 DFS를 수행한 결과를, 그 다음 줄에는 BFS를 수행한 결과를 출력한다.

1
2
3
dfs(V)
print()
bfs(V)

◾ HashRouter : 사이트 방문시 URL 맨 뒤에 /#/이 붙은채로 시작함
◾ BrowserRouter : 사이트 방문시 # 등 없이 깔끔함

HashRouter

URL에 #기호가 붙는 HashRouter를 왜쓸까?
◾ 원래는 브라우저 주소창에 뭔가 페이지를 입력하면 서버에게 특정 페이지좀 보여달라는 요청이 된다.
◾ 근데 우리는 요청할 서버가 없고 그냥 리액트가 라우팅을 담당하고 있음
◾ 그래서 잘못하면 있지도 않은 페이지를 서버에 요청을 해서 404 Page Not Found 이런 에러도 뜰 수있다.
◾ 실수로 서버에게 요청하지 않게 하려면 안전하게 #을 붙어야한다.
◾ 왜냐면 브라우저 주소창에서 # 뒤에 붙은 것들은 절대 서버로 요청되지 않기때문이다.

BrowserRouter

그럼 BrowserRouter는 어떨 때 써야 하는가?

◾ BrowserRouter를 쓰려면 서버에서 셋팅을 해야함
◾ 특정 경로로 들어오는 요청은 404 대신 라우팅을 해야 한다, 등
◾ 위와 같은 경로를 API를 짜놓으면 된다.

Diary App

6️⃣

NewCard에 수정 API 연결하기

api.js

card를 추가함에 따라 기존의 rawData를 수정해야 한다. rawData[year][month]에 카드를 추가하면 데이터가 수정되기 때문. 하지만 json-server에는 수정 API (PUT method) 가 없기 때문에, 기존의 데이터에 수정된 데이터를 붙여 새로운 데이터를 만들어 POST 해줘야 한다.

PUT과 POST의 차이는 다음과 같다.

The PUT method requests that the enclosed entity be stored under the supplied Request-URI. If the Request-URI refers to an already existing resource, the enclosed entity SHOULD be considered as a modified version of the one residing on the origin server.

1
2
3
4
const instance = axios.create({
baseURL: "http://localhost:3001",
timeout: 1000
});

똑같은 instance를 사용한 method가 2개 이상이 됨에 따라 instance를 생성하여 get , post 와 같은 method를 이용하였다. 이 때 timeout 이란, 해당 시간 동안 (이는 ms 기준이다!) 답을 얻지 못하면 block을 시켜버리는 것이다. 이 시간을 잘 조절해야 한다… (1000ms로 안 되는 데이터 송수신 양은 뜻하지 않게 에러를 부를 수 있다…)

1
2
3
4
5
6
7
8
export const createCardData = async (userData) => {
try {
const rawData = await instance.post("/posts", {
data: userData
});
console.log("[SUCCESS] POST card data");
return rawData.data.data;

수정할 데이터를 인자로 받으면, 이를 post 에 데이터로 전달한다. return data는 rawData라는 data 객체에서 data 파트의 우리가 정의한 data를 읽어와야 하기 때문에 rawData.data.data 가 된다.

Main

1
2
const [userData, setUserData] = useState(null);
const [rawData, setRawData] = useState(null);

api로부터 받는 rawData 자체와 이를 year, month에 맞게 가져오는 userData 를 따로 정의한다.

1
2
3
4
5
6
7
useEffect(() => {
(async () => {
const data = await getCardData();
setRawData(data);
data[year] && setUserData(data[year][month]);
})();
}, [year, month]);

getCardData 로 api를 호출하고 이를 rawData로 세팅한다. 이후 데이터가 존재하면, userData에 year과 month를 알맞게 전달하여 데이터를 받아 세팅한다.

따로 받아오는 이유는 다음과 같은데,

1
2
3
4
{userData &&
userData.map((data, index) => {
return <Card key={index} userData={data} />;
})}

Card 에는 userData 를 전달해 데이터를 올바르게 읽어와야 하고,

1
2
3
4
5
6
<NewCard
year={year}
month={month}
rawData={rawData}
setUserData={setUserData}
/>

NewCard 에는 year, month 뿐만 아니라 rawData , setUserData 를 전달해야 하기 때문이다. 이 때 rawData를 전달하는 이유는, 새로운 카드를 생성할 때 그 때의 시점에서 rawData에 데이터를 추가해야 하기 때문이다. 또한, 해당 카드를 사용하기 위해 setUserData를 전달하여 시점을 해당 카드로 이동한다.

NewCard

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const NewCard = ({ year, month, rawData, setUserData }) => {
const createCard = async () => {
const cardForm = {
date: getDate(),
title: "",
image: "",
weather: "",
tags: [],
summary: "",
text: ""
};
rawData[year][month].push(cardForm);
const data = await createCardData(rawData);
data[year] && setUserData(data[year][month]);
};

위에서 내려준 데이터를 받아 카드를 새로 생성하는 컴포넌트이다. createCard는 데이터를 받아 세팅해야 하니 async 로 설정하고, 기본적인 cardForm 을 만들어 이를 rawData의 year, month에 설정함으로써 새로운 데이터를 push한다. 이후 createCardData api를 통해 post하여 data를 생성 (원래는 수정) 한다. 정상적으로 작동할 경우 data를 반환해야 하니 해당 데이터가 반환되는 것이 확인되면, setUserData 를 통해 데이터를 세팅한다.

1
2
3
<div className="card" onClick={createCard}>
<div className="card__text">+ 추가해 주세요</div>
</div>

카드를 생성하기 위해서, 클릭했을 때 위에서 정의한 createCard 를 onClick에 대한 함수로 설정한다. 이 때, tag 종류에 상관 없이 onClick이 정의 가능하다! (button, div 등)

Card

1
<div className="card__title">{title ? title : "제목 없음"}</div>

기존의 card__title 형식을 {title}에서 조건을 추가하여, title이 없을 경우 “제목 없음”을 렌더링하도록 설정한다.


7️⃣

카드 클릭시 Diary 뷰로 이동하기

App.js

1
2
3
4
5
6
7
8
<Route
path="/diary/:id"
component={() => <Diary year={year} month={month} />}
/>
<Route
path="/diary/edit/:id"
component={() => <Diary year={year} month={month} />}
/>

기존에는 component에 컴포넌트만 전달했다면, 이제는 year, month를 props로 전달한다.

diary/Card.js

그리고, diary page에서 보이는 card 컴포넌트를 정의한다. diary에서는 카드를 읽고/ 수정할 수 있기 때문에, 이를 url 매칭 여부에 따라 조절한다.

1
2
3
const Card = ({ userData, match }) => {
const isReadOnly = match.path === "/diary/:id" ? true : false;
const { title, date, image, weather, tags, summary, text } = userData;

받아온 userData를 구조분해할당하여 일단 다음과 같이 렌더링 하고,

1
2
3
4
5
6
7
8
9
10
11
return (
<CardWrap>
<p>{title}</p>
<p>{date}</p>
<img src={image} width="200" alt="" />
<p>{weather}</p>
<p>{tags}</p>
<p>{summary}</p>
<p>{text}</p>
</CardWrap>
);

match를 이용하기 위해 withRouter를 사용한다.

1
export default withRouter(Card);

Main.js

history 사용을 위해 withRouter를 설정한다.

1
2
3
4
5
6
7
import { withRouter } from "react-router-dom";

const Main = ({ year, month, history }) => {
...
}

export default withRouter(Main);

카드를 렌더링한 후 개별적으로 선택할 때마다, history에 각각이 가지고 있는 id 를 포함한 url을 push하여 카드가 렌더링되도록 한다. 이를 위해 Card 컴포넌트에 onClickFunc 을 전달한다. 여기서 이 function을 정의하고 전달하는 이유는, 카드 목록에서 선택한 카드를 렌더링하기 위해서는 목록에서 선택한 후 해당 url로 이동해야 하기 때문이다.

1
2
3
4
5
<Card
key={index}
userData={data}
onClickFunc={() => history.push(`/diary/${data.id}`)}
/>

카드 목록 끝에는 새 카드를 생성하는 NewCard 컴포넌트가 필요하다. 이 때, 생성하는 시점의 year, month, 해당 카드를 추가하기 위한 rawData, 설정하기 위한 setUserData, 그리고 id를 생성해 props로 설정한다.

1
2
3
4
5
6
7
<NewCard
year={year}
month={month}
rawData={rawData}
setUserData={setUserData}
id={userData ? userData.length + 1 : 1}
/>

main/Card.js

onClickFunc 을 전달하여 onClick 에 대한 함수로 설정한다.

1
2
3
4
const Card = ({ userData, onClickFunc }) => {
return (
<CardWrap>
<div className="card" onClick={onClickFunc}>

NewCard.js

위에서 전달한 id 를 받아 cardForm의 프로퍼티로 추가한다.

1
2
3
4
5
6
7
8
9
10
11
const NewCard = ({ id, year, month, rawData, setUserData }) => {
const cardForm = {
date: getDate(),
id: id,
title: "",
image: "",
weather: "",
tags: [],
summary: "",
text: ""
};

Diary.js

withRouter를 설정하여 match를 사용할 수 있도록 한다음, match에서 전달받은 (현재 선택한) 카드의 id를 받아온다. 이후 api를 사용하여 rawData를 불러와 find 를 통해 해당 id와 일치하는 데이터를 가져와 지금의 diaryData로 세팅한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Diary = ({ year, month, match }) => {
const id = match.params.id;
const [diaryData, setDiaryData] = useState(null);

useEffect(() => {
(async () => {
const data = await getCardData();
data[year] && setDiaryData(data[year][month].find((el) => el.id === id));
})();
}, [id]);

...
}

export default withRouter(Diary);

앞에서 설정한 diaryData 를 가지고 Card 컴포넌트를 렌더링한다. 계속 사용하고 있었지만, api를 사용해서 데이터를 불러와 세팅하는 과정을 useEffect로 포함한다면 이를 올바르게 렌더링하기 위해 (null 값이 뜨는 버그가 발생하지 않게…) 데이터가 존재할 때만 렌더링되도록 && 조건을 설정해야 한다!

1
return diaryData && <Card diaryData={diaryData} />;

8️⃣

Diary 뷰 Header 구현하기

diary/Card.js

앞서 정의했던 isReadOnlyCardHeader 컴포넌트에 전달하여 수정 상태 여부에 따라 다르게 렌더링 될 수 있도록 한다.

1
<CardHeader title={title} isReadOnly={isReadOnly} />

diary/CardHeader.js

title 의 경우 직접 작성하는 것이므로 input 으로 설정해 수정되는 값을 전달하고 받을 수 있도록 설정한다. 특히 input에서 주의할 점은 value를 설정해야 한다는 것! value를 설정하지 않으면 input이 적절하게 작동하지 않는다..

1
2
3
4
5
6
7
8
9
10
const CardHeader = ({ title, isReadOnly }) => {
return (
<CardHeaderWrap>
<input
type="text"
className="header__title"
placeholder="제목을 입력해 주세요"
value={title}
readOnly={isReadOnly}
/>

이 외에도 수정, 삭제를 위해 버튼을 생성한다.

1
2
<button className="header__edit">수정</button>
<button className="header__delete">삭제</button>

9️⃣

Diary 상세기능 뷰 구현하기

Card.js : Card의 모든 내용을 포함하고 있는, 카드 전체

구조분해할당으로 일일히 가져오던 데이터를 state 에 저장하여 사용하도록 한다. 이유는 onChange에 따른 변화를 저장하기 위함이다.

1
2
3
4
5
6
7
8
9
10
const [state, setState] = useState(userData);

const handleChange = (e) => {
const name = e.target.name;

setState({
...state,
[name]: e.target.value
});
};

이 때 setState에 기본적으로 userData 를 넣어놨기 때문에, 수정된 부분만 반영하기 위해서 ... 로 기존의 정보를 풀고, [변수] 을 이용하여 해당 변수 가 담고 있는 값을 가진 프로퍼티를 수정한다.

해당 handleChange 를 적용하기 위해, 각 부분에 이를 전달한다. 이 때, 각 컴포넌트는 state 로 전달하는 값을 변경하고 저장한다. 또한, 읽기모드에서는 수정이 불가하게 하기 위해 isReadOnly 를 통해 이를 관리한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<CardHeader
title={state.title}
handleChange={handleChange}
isReadOnly={isReadOnly}
/>
<CardInfo
userData={state}
isReadOnly={isReadOnly}
handleChange={handleChange}
/>
<textarea
placeholder="오늘을 기록해 주세요"
isReadOnly={isReadOnly}
value={state.text}
name="text"
onChange={handleChange}
/>

CardInfo.js : Card의 상세정보를 담고 있는 컴포넌트

1
2
3
4
import FormControl from "@material-ui/core/FormControl";
import NativeSelect from "@material-ui/core/NativeSelect";
import { makeStyles, withStyles } from "@material-ui/core/styles";
import InputBase from "@material-ui/core/InputBase";

위의 라이브러리는 모두 materialUI 를 사용하기 위해 import 한 것이다.

스타일을 사용한 부분은 날씨 select 파트로, 옵션에 스타일을 주기 위해 정의하였다. materialUI에서 선택한 테마를 사용하기 위해 정의한 것들은 다음과 같다.

  1. makeStyles

    1
    2
    3
    4
    5
    6
    7
    const useStyles = makeStyles({
    select: {
    "& .MuiSvgIcon-root": {
    display: "none"
    }
    }
    });
    1
    const classes = useStyles();
    1
    2
    3
    4
    <NativeSelect
    className={classes.select}
    ...
    >
  2. withStyles

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const BootstrapInput = withStyles((theme) => ({
    input: {
    borderRadius: 5,
    position: "relative",
    backgroundColor: "white",
    border: "1px solid #CEA0E3",
    fontSize: 18,
    padding: "5px 7px",
    transition: theme.transitions.create(["border-color", "box-shadow"]),
    background: `url(${Select}) no-repeat 95% 50%`,
    "&:focus": {
    borderRadius: 5,
    borderColor: "#CEA0E3",
    backgroundColor: "white",
    boxShadow: "0 0 0 0.2rem rgba(206,160,227,.25)"
    }
    }
    }))(InputBase);
    1
    2
    3
    4
    <NativeSelect
    ...
    input={<BootstrapInput />}
    >

    위와 같은 스타일이 적용된 전체적인 구조는 다음과 같다.

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
<span>날씨</span>
{isReadOnly ? (
<input
type="text"
isReadOnly={isReadOnly}
value={weather}
placeholder="날씨를 선택해주세요"
/>
) : (
<FormControl>
<NativeSelect
className={classes.select}
value={weather}
name="weather"
onChange={handleChange}
input={<BootstrapInput />}
>
<option value="" disabled>
날씨를 선택해주세요
</option>
<option value={"맑음"}>맑음</option>
<option value={"구름"}>구름</option>
<option value={"흐림"}>흐림</option>
<option value={"비"}>비</option>
<option value={"눈"}>눈</option>
<option value={"바람"}>바람</option>
</NativeSelect>
</FormControl>
)}

태그의 경우, 태그 배열이 비어있지 않은 경우, 즉 길이가 0보다 큰 경우에만 map 으로 배열을 순회하도록 하였다.

1
2
3
4
<div className="info__tags">
<span>태그</span>
{tags.length > 0 ? (
tags.map((tag, index) => {

만약 배열이 비어있다면, input 필드로 락을 걸어놓는다.

1
2
3
4
5
6
<input
type="text"
isReadOnly={true}
value=""
placeholder="태그를 선택해주세요"
/>

그 외는 textarea 로 내용을 담는다.

1
2
3
4
5
6
7
<textarea
placeholder="오늘을 기록해 주세요"
isReadOnly={isReadOnly}
value={state.text}
name="text"
onChange={handleChange}
/>

🔟

카드 수정 기능 구현

diary/Card.js

CardHeader에서 수정 기능을 이용할 수 있도록, 이를 포함하고 있는 diary 페이지의 Main 최상단 컴포넌트 Card에서 handleEdit 을 정의하고 넘겨준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
const id = parseInt(match.params.id);  

const handleEdit = async () => {
// const index = rawData[year][month].findIndex((data) => data.id === id);

// newList를 따로 만드는 이유? rawData가 state의 readOnly 값이기 때문에 newList로 값을 복사해서 사용합니다
const newList = rawData[year].filter((data) => data); // 새로운 배열 반환
newList[month][id] = state;
const data = await createCardData(rawData);
history.goBack();
};

export default withRouter(Card);

rawData 자체는 읽기 전용이기 때문에, 이를 준수하기 위해 filter 함수를 이용해 새로운 리스트인 newList 를 생성하고 데이터를 담는다. 여기서 id에 맞게 state를 저장한 후, 새롭게 변경된 rawData는 POST api를 통해 (PUT 대신) createCardData로 업로드 해준다. 마지막으로, 수정이 완료되었을 때 완료 버튼을 클릭하여 해당 과정을 진행하는 것이므로, 이를 끝마친 후 확인 상태로 돌아가기 위해 history.goBack() 을 통해 뷰 단계로 돌아간다.

diary/CardHeader.js

Card에서 내려받은 handleEdit 을 이용하여 직접 수정하는 부분은 다음과 같다.

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
const CardHeader = ({
title,
isReadOnly,
handleChange,
handleEdit,
match,
history
}) => {
const id = match.params.id;
.
.
.

{isReadOnly ? (
<button
className="header__edit"
onClick={() => history.push(`/diary/edit/${id}`)}
>
수정
</button>
) : (
<button className="header__edit" onClick={handleEdit}>
완료
</button>
)}
  1. 수정 버튼을 클릭하면 history.push 를 통해 현재 id 의 edit url로 이동한다.
  2. 완료 버튼을 클릭하면 전달받은 handleEdit 을 실행시켜 변경된 데이터를 저장하고 메인 뷰로 돌아간다.

pages/Diary.js

먼저, year에 맞는 data가 존재한다면 (rough하게 year만 찾기) setDiaryData를 이용해 현재 match의 id 값과 일치하는 데이터를 month 배열에서 찾아 저장한다. 또한, rawData 자체도 저장하여 Card에 이를 전달하여 수정/이용할 수 있도록 한다.

마찬가지로, null 에러가 뜨지 않기 위해 diaryData && 를 통해 데이터가 존재할 때만 Card 컴포넌트를 렌더링 하도록 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const id = match.params.id;

useEffect(() => {
(async () => {
const data = await getCardData();
data[year] && setDiaryData(data[year][month].find((el) => el.id === id));
setRawData(data);
})();
}, [id]);

return (
diaryData && (
<Card userData={diaryData} rawData={rawData} year={year} month={month} />
)
);

카드 삭제 기능 구현

diary/Card.js

삭제 기능은 위의 handleEdit 과 비슷하게, newList를 만들어 그에 변경된 데이터를 집어넣고 POST api를 통해 업로드한 뒤 전 url로 돌아간다.

여기서 다른 점은, filter 함수를 통해 삭제하고자 하는 현 카드 외의 데이터만 남겨 filteredList 에 반환해야 한다는 점이다.

1
2
3
4
5
6
7
const handleDelete = async () => {
const filteredList = rawData[year][month].filter((data) => data.id !== id);
const newList = rawData[year].filter((data) => data);
newList[month] = filteredList;
const data = await createCardData(rawData);
history.goBack();
};

이를 CardHeader에 전달하고 CardHeader은 이를 전달받아 onClick 함수로 지정하는 모든 과정은 수정과 동일하다.

commit 하는 습관을 들이자! ✨

Initial commit : 처음 세팅 (e.g. npx create-react-app 이후)

feat : feature 수정 시

fix : error fix 시

chore : production code는 변함 없지만 부가적인 정보 수정 시

docs : docs 업데이트 (e.g. readme.md 수정 시)

merge : merge 시…