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
3 changes: 2 additions & 1 deletion src/components/AnswerDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import styled from 'styled-components';
import {Answer, Model} from '../types/model';
import * as React from 'react';
import {removeSludge} from "../utils/dateHandler";

const Container = styled.div`
display: flex;
Expand Down Expand Up @@ -54,7 +55,7 @@ const AnswerDetail: React.FC<{props: Model<Answer>}> = ({props}) => {
<Avatar>
<AvatarImage src="/jobs_avatar.png" alt="avatar" />
</Avatar>
<Created>{props.createdAt}</Created>
<Created>{removeSludge(props.createdAt)}</Created>
</Left>
<Contents>{props.contents}</Contents>
</Container>
Expand Down
76 changes: 55 additions & 21 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,81 @@
import styled from 'styled-components';
import Link from 'next/link';
import * as React from 'react';
import {useContext, useEffect} from 'react';
import {MemberContext} from "../contexts/MemberContext";
import {clearToken, getIsLogin} from "../utils/tokenHandler";
import {useRouter} from "next/router";

const Container = styled.div`
display: flex;
background: white;
vertical-align: center;
background: var(--white-yellow);
padding: 1rem 2rem;
border: 0.1px solid rgba(0, 0, 0, 0.2);
margin-bottom: 1rem;
`;

const Button = styled.button`
margin: 1rem;
const LeftSideButton = styled.button`
display: inline-block;
color: black;
font-size: large;
background: white;
border-color: black;
border-width: thin;
background-color: var(--yellow);
border: 0.1px solid rgba(0, 0, 0, 0.2);
border-radius: 7px;
padding: 1rem;
width: 8rem;

:hover {
background-color: rgba(255, 241, 118, 0.5);
}
`;

const LoginButton = styled.button`
margin-left: auto;
const RightSideButton = styled.button`
display: inline-block;
float: right;
color: black;
font-size: large;
background: white;
border-color: black;
border-width: thin;
background-color: var(--yellow);
border: 0.1px solid rgba(0, 0, 0, 0.2);
border-radius: 7px;
padding: 1rem;
width: 8rem;
width: 7rem;
margin-left: 2rem;

:hover {
background-color: rgba(255, 241, 118, 0.5);
}
`;

const Navbar: React.FC = () => {
const {member: {isLogin}, setMember} = useContext(MemberContext);
const {push} = useRouter();

useEffect(() => {
setMember({isLogin: getIsLogin()});
}, []);

const logout = async () => {
clearToken();
setMember({isLogin: false});
await push('/');
};

return (
<Container>
<Link href={'/'}>
<Button>Quna</Button>
</Link>
<Link href={'/'}>
<Button>질문 목록</Button>
</Link>
<Link href={'/'}>
<LoginButton>로그인</LoginButton>
<LeftSideButton>Quna</LeftSideButton>
</Link>
{isLogin ? (
<RightSideButton onClick={logout}>로그아웃</RightSideButton>
) : (
<>
<Link href={'/login'}>
<RightSideButton>로그인</RightSideButton>
</Link>
<Link href={'/signup'}>
<RightSideButton>회원가입</RightSideButton>
</Link>
</>
)}
</Container>
);
};
Expand Down
3 changes: 2 additions & 1 deletion src/components/QuestionDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import styled from 'styled-components';
import {Model, Question} from '../types/model';
import * as React from 'react';
import {removeSludge} from "../utils/dateHandler";

const Container = styled.div`
width: 80%;
Expand Down Expand Up @@ -69,7 +70,7 @@ const QuestionDetail: React.FC<{props: Model<Question>}> = ({props}) => {
<Avatar>
<AvatarImage src="/jobs_avatar.png" alt="avatar" />
</Avatar>
<Created>{props.createdAt}</Created>
<Created>{removeSludge(props.createdAt)}</Created>
</Left>
<Title>{props.title}</Title>
</Top>
Expand Down
71 changes: 64 additions & 7 deletions src/components/QuestionInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import React, {ChangeEvent, useState} from 'react';
import * as React from 'react';
import {ChangeEvent, useState, useContext, useEffect} from 'react';
import {MemberContext} from "../contexts/MemberContext";
import styled from 'styled-components';
import Questioner from './Questioner';
import {fetchAPI} from '../constants/api';
import {isCreated} from '../constants/status';
import {getIsLogin} from "../utils/tokenHandler";
import {useRouter} from "next/router";

const Container = styled.div`
/*Layout 적용 이후 width 변경 요망* */
Expand Down Expand Up @@ -29,6 +35,7 @@ const InputTitle = styled.input.attrs({
width: 100%;
border: 0.1px solid rgba(0, 0, 0, 0.2);
border-radius: 7px;

::placeholder {
color: rgba(0, 0, 0, 0.2);
font-weight: 500;
Expand Down Expand Up @@ -68,12 +75,62 @@ const ButtonSubmit = styled.button`
`;

const QuestionInput: React.FC = () => {
const {member: {isLogin}, setMember} = useContext(MemberContext);
const [isFolded, setIsFolded] = useState<boolean>(true);
const [title, setTitle] = useState<string>('');
Comment on lines 79 to 80
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.

initialState로 type을 추론할 수 있지 않을까요?
이런 경우엔 타입을 생략하는 걸 선호하는데, 어떻게 생각하시나요?

Suggested change
const [isFolded, setIsFolded] = useState<boolean>(true);
const [title, setTitle] = useState<string>('');
const [isFolded, setIsFolded] = useState(true);
const [title, setTitle] = useState('');

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

일단 추론 가능한 경우에 type 생략을 선호하신다는 것에 충분히 공감합니다. 'type을 정해주어야한다'는 강박에 매몰되는 걸 경계해야겠구나 하고 생각했어요.
그러면서 타입스크립트를 사용하는 이유에 대해 다시 한 번 생각해보았는데요, 예측 불가능한 실수를 미리 막는다는 점에서 이러한 경우에도 타입을 명시하는 게 더 좋지 않을까(제가 코드를 작성하며 어떤 실수를 할 지 모르니..) 하고 생각했어요. 또 레포를 몇 개 돌아다니면서 봤는데 명시해주는 경우가 더 많은 것 같더라고요. 그래서 그러한 경우에도 저는 타입을 명시해줄 것 같습니다!

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.

제 pr에 남겨두셨던 코멘트와 비슷한 내용이군요.

어떻게하면 좋을지 생각해보죠!
저번에는 생략쪽에 기울었는데 다희님께서 다른 레포를 참고하시고 명시하는 경우가 더 많다니까 다시 고려해볼만 하네요.

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 [mainContent, setMainContent] = useState<string>('');
const {push} = useRouter();

useEffect(() => {
setMember({isLogin: getIsLogin()});
}, []);

const isNotVerified = () => {
if (!title) {
alert('질문 제목을 입력해주세요.');
return true;
} else if (!mainContent) {
alert('질문 내용을 입력해주세요.');
return true;
} else if (!isLogin) {
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.

로그인이 안되어있으니 로그인 페이지로 보내는것도 좋을것 같아요

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

오옹 좋은 제안입니다. 다음 커밋에 반영할게요!

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.

좋네용
반영해서 푸쉬하면 될 듯 합니다😀

alert('로그인이 필요합니다.');
return true;
}
return false;
}

const handleClickNext = () => {
if (!title) {
alert('질문 제목을 입력해주세요.');
return;
}

setIsFolded(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.

불필요한 공백을 발견했슴니당😬

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

지금 제 eslint와 prettier 설정이 비정상적인 부분이 많은데..... 다음 프로젝트 들어갈 땐 확실히 설정하고 시작해야겠어요! 반영하겠습니다 😃

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 handleClickSubmit = async () => {
if (isNotVerified()) {
return;
}
/**responderId 관련 로직 확인 필요 */
const request = {
title : title,
contents : mainContent,
responderId : null,
}

const handleClick = () => {
setIsFolded(!isFolded);
try {
const {status} = await fetchAPI('POST', 'Questions', null, request);

if (isCreated(status)) {
alert('질문이 등록되었습니다.');
await push('/');
}
} catch (e) {
console.error(e);
alert('질문 등록에 실패했습니다. 다시 시도해주세요.');
}
};

const handleTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
Expand All @@ -90,18 +147,18 @@ const QuestionInput: React.FC = () => {
<>
<HeaderContainer>
<Questioner />
<InputTitle onChange={handleTitleChange} value={title} />
<ButtonNext onClick={handleClick}>Next</ButtonNext>
<InputTitle onChange={handleTitleChange} />
<ButtonNext onClick={handleClickNext}>Next</ButtonNext>
</HeaderContainer>
</>
) : (
<div>
<HeaderContainer>
<Questioner />
<InputTitle value={title} />
<InputTitle onChange={handleTitleChange} defaultValue={title}/>
</HeaderContainer>
<InputMain onChange={handleMainChange} value={mainContent} />
<ButtonSubmit onClick={handleClick}>Submit</ButtonSubmit>
<ButtonSubmit onClick={handleClickSubmit}>Submit</ButtonSubmit>
</div>
)}
</Container>
Expand Down
6 changes: 3 additions & 3 deletions src/components/QuestionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ const Container = styled.div`
`;

const QuestionList: React.FC<{data: Model<Question>[]}> = ({data}) => {
return (
return data ? (
<Container>
{data.map(question => (
<QuestionListItem data={question} />
<QuestionListItem key={question.id} data={question} />
))}
</Container>
);
): <Container />
};

export default QuestionList;
6 changes: 3 additions & 3 deletions src/components/QuestionListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ const Title = styled.div`
font-weight: 600;
`;

const QuestionListItem: React.FC<{data: Model<Question>}> = ({data}) => {
const QuestionListItem: React.FC<{data: Model<Question>}> = ({data: {id, title}}) => {
return (
<Link href={`/questions/${data.id}`}>
<Link href={{pathname: '/questions', query: {id}}} as={'/questions'}>
<Container>
<Questioner />
<Title>{data.title}</Title>
<Title>{title}</Title>
</Container>
</Link>
);
Expand Down
1 change: 0 additions & 1 deletion src/components/Questioner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: left;
`;

const Image = styled.img`
Expand Down
64 changes: 52 additions & 12 deletions src/constants/api.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,55 @@
const BASE_URL = 'http://localhost:8080';
import axios from "axios";
import {TOKEN, TOKEN_TYPE} from "./token";

export const API = (
model: 'Question' | 'Answer' | 'Signup' | 'Login' | string,
questionsId?: string
const BASE_URL = 'http://13.124.134.56:8080';
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.

👏👏👏👏👏


type API = 'Questions' | 'Answers' | 'Signup' | 'Login' | 'QuestionDetail';

const client = axios.create({
baseURL: "",
})

export const getAPIPath = (
model: API,
questionsId?: string | number
) =>
model === 'Question'
model === 'Questions'
? `${BASE_URL}/questions`
: model === 'Answer'
? `${BASE_URL}/questions/${questionsId}/answers`
: model === 'Login'
? `${BASE_URL}/login`
: model === 'Signup'
? `${BASE_URL}/members/`
: '';
: model === 'QuestionDetail'
? `${BASE_URL}/questions/${questionsId}`
: model === 'Answers'
? `${BASE_URL}/questions/${questionsId}/answers`
: model === 'Login'
? `${BASE_URL}/login`
: model === 'Signup'
? `${BASE_URL}/members`
: '';

export const fetchAPI = async (method: 'POST' | 'GET' | 'PUT' | 'DELETE', api: API, questionsId?: string | number, content?: {}) => {


if (method === 'POST' && api === 'Signup') {
return await client.post(getAPIPath(api, questionsId), content);
}
if (method === 'POST') {
return await client.post(getAPIPath(api, questionsId), content,
{
headers: {Authorization: `${localStorage.getItem(TOKEN_TYPE)} ${localStorage.getItem(TOKEN)}`}
})
}
if (method === 'GET') {
return await client.get(getAPIPath(api, questionsId))
}
if (method === 'PUT') {
return await client.put(getAPIPath(api, questionsId), content,
{
headers: {Authorization: `${TOKEN_TYPE} ${TOKEN}`}
})
}
if (method === 'DELETE') {
return await client.delete(getAPIPath(api, questionsId), {
headers: {Authorization: `${TOKEN_TYPE} ${TOKEN}`}
})
}
throw new Error();
}
7 changes: 7 additions & 0 deletions src/constants/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const
isOk = (status: number) => status === 200,
isCreated = (status: number) => status === 201,
isNoContent = (status: number) => status === 204,
isBadRequest = (status: number) => status === 400,
isUnauthorized = (status: number) => status === 401,
isNotFound = (status: number) => status === 404;
25 changes: 25 additions & 0 deletions src/contexts/MemberContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from 'react';
import {useState} from 'react';

const MemberContext = React.createContext(null);

const MemberProvider = ({children}) => {
const [member, setMember] = useState({
isLogin: false,
});

const memberState = {
member, setMember
};

return (
<MemberContext.Provider value={memberState}>
{children}
</MemberContext.Provider>
)
};

export {
MemberProvider,
MemberContext
};
Loading