-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[#82] 메인 페이지 추가 요구 기능 #111
The head ref may contain hidden characters: "82-\uBA54\uC778-\uD398\uC774\uC9C0-\uCD94\uAC00-\uC694\uAD6C-\uAE30\uB2A5"
Conversation
…o-with-me into 82-메인-페이지-추가-요구-기능
…o-with-me into 82-메인-페이지-추가-요구-기능
export default function Aside(props: AsideProps) { | ||
return ( | ||
<aside> | ||
<h2>문제 목록</h2> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
h1,h2,...h6 태그는 SEO에서 중요하게 읽혀서 문제 목록은 span이면 충분 할 것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
몰랐는데 감사합니다!:D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
componenets 아래에 pageHeader는 어색한 것 같아요. 대회 페이지 내의 대회 정보를 담고 있는 헤더라면 CompetitionHeader 정도면 될 것 같네요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CompetitionPage의 Header로 들어갈 부분이라서 그렇게 이름을 지었는데, 말씀하신 대로 components 아래에 들어가는 걸 고려해보니까 어색한 것 같네요
감사합니다!
setCurrentProblemIndex: (index: number) => void; | ||
} | ||
|
||
export default function Aside(props: AsideProps) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
단순히 Aside라는 이름은 컴포넌트의 역할이 모호하게 드러나는 것 같아요. 그리고 Aside의 기능보단 Problem을 선택할 수 있는 기능에 더 가까운 것 같습니다. ContestProblemSelector 정도가 어떨까요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
확실히 나은 이름이네요, 항상 감사합니다!
|
||
interface AsideProps { | ||
problemIds: number[]; | ||
setCurrentProblemIndex: (index: number) => void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Props로 내려주는 이벤트 핸들러 이름은 on으로 시작하게 해주세요.
onChangeProblemIndex: (index:number)=>void;
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
앗, 미처 신경쓰지 못했네요
감사합니다, 다음부터는 on으로 시작하게 하겠습니다!
<header> | ||
<div className={titleContainerStyle}> | ||
{crumbs.map((crumb, index) => ( | ||
<span key={index} className={crumbStyle}> | ||
{crumb} | ||
</span> | ||
))} | ||
</div> | ||
</header> | ||
<div className={titleContainerStyle}> | ||
{crumbs.map((crumb, index) => ( | ||
<span key={index} className={crumbStyle}> | ||
{crumb} | ||
</span> | ||
))} | ||
</div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋아요 👍
try { | ||
await axios.post( | ||
`${apiUrl}/competitions/${props.id}/participations`, | ||
{}, | ||
{ | ||
headers: { | ||
Authorization: `Bearer ${token}`, | ||
}, | ||
}, | ||
); | ||
|
||
alert('대회에 성공적으로 참여했습니다.'); | ||
window.location.reload(); | ||
} catch (error: unknown) { | ||
let errorMessage = 'Unexpected error occurred.'; | ||
|
||
if (axios.isAxiosError(error)) { | ||
if (error.response) { | ||
if (error.response.status === 403) { | ||
errorMessage = '대회 참여에 실패했습니다. 서버에서 거절되었습니다.'; | ||
} else if (error.response.status === 400) { | ||
errorMessage = '이미 참여한 대회입니다.'; | ||
} else { | ||
errorMessage = `HTTP Error ${error.response.status}`; | ||
} | ||
} else { | ||
errorMessage = 'Network error occurred.'; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
apis 폴더로 빼는게 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵, 분리하겠습니다 감사합니다!
const fetchCompetitions = async () => { | ||
try { | ||
const response = await axios.get<Competition[]>('http://101.101.208.240:3000/competitions'); | ||
setCompetitions(response.data); | ||
} catch (error) { | ||
console.error('Error fetching competitions:', (error as Error).message); | ||
} | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fetchCompetition 함수가 제 브랜치에 있어서 햇갈릴 것 같은데 fetchCompetitionList로 이름을 바꿀 수 있을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
그리고 우찬님도 가능하면 http://~~ 하는 부분을 .env
파일 만들고 VITE_API_URL=http://101.101.208.240:3000/
내용 넣어서 이거 사용해주세요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
함수명 변경이랑, .env파일 사용 모두 하겠습니다. 감사합니다!
제 쪽 브랜치 코드 때문에 나중에 충돌이 좀 날 것 같은데, 이거 먼저 머지되면 제 쪽에서 수정하겠습니다. 제 브랜치가 좀 커졌는데, 실수했네요. 머지가 오래걸릴거라면 쪼개서 PR을 먼저 올렸어야했는데 |
const token = queryParams.get(TOKEN_KEY) || localStorage.getItem(TOKEN_KEY); | ||
const navigate = useNavigate(); | ||
|
||
const handleNavigate = () => { | ||
if (!token) { | ||
alert('로그인이 필요합니다.'); | ||
navigate('/login'); | ||
} else { | ||
navigate('/contest/create'); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
token이 아니고
import useAuth from '@/src/hooks/login/useAuth' 대충 이런 path
const { isLoggedin } = useAuth(); 이렇게
isLoggedin:boolean인데 true면 로그인된 상태 false면 안 된 상태입니다.
const apiUrl = import.meta.env.VITE_API_URL; | ||
|
||
const queryParams = new URLSearchParams(location.search); | ||
const token = queryParams.get(TOKEN_KEY) || localStorage.getItem(TOKEN_KEY); | ||
|
||
const handleJoinClick = async () => { | ||
if (!token) { | ||
alert('로그인이 필요합니다.'); | ||
window.location.href = '/login'; | ||
return; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기도 token은 사용자 검증할 때 사용되고 token으로 검증된 상태면 const { isLoggedin } = useAuth();이 true일 거예요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
감사합니다!
{ | ||
headers: { | ||
Authorization: `Bearer ${token}`, | ||
}, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
검증된 사용자만 저 api에 응답받을 수 있나보네요?!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Swagger UI를 보면 그런 것 같아요:D
if (axios.isAxiosError(error)) { | ||
if (error.response) { | ||
if (error.response.status === 403) { | ||
errorMessage = '대회 참여에 실패했습니다. 서버에서 거절되었습니다.'; | ||
} else if (error.response.status === 400) { | ||
errorMessage = '이미 참여한 대회입니다.'; | ||
} else { | ||
errorMessage = `HTTP Error ${error.response.status}`; | ||
} | ||
} else { | ||
errorMessage = 'Network error occurred.'; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const {status} = error.response
switch문은 어떠신가요 ?!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아 정말 좋은 생각이네요, 감사합니다!
const response = await axios.get<Competition[]>('http://101.101.208.240:3000/competitions'); | ||
setCompetitions(response.data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
http://101.101.208.240:3000은 .env에서 꺼내서 사용하는 게 좋겠습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
감사합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
�제 pr을 merge 안해서 임시로 만드신 거 겠죠??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
merge하기 전에 생성했는데, 삭제하면 될까요?
…o-with-me into 82-메인-페이지-추가-요구-기능
}, | ||
); | ||
|
||
alert('대회에 성공적으로 참여했습니다.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
alert도 api에서 관리할 로직이 아니라고 생각돼요. alert를 띄우는 것은 외부 데이터를 관리한 것에 대한 mainpage의 반응 이라고 생각됩니다. 즉, alert는 view를 담당하는 mainpage의 역할이라고 생각됩니다.
우찬님은 어떻게 생각하시나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
확실히 alert도 페이지에서 관리하는 게 좋을 것 같네요, 감사합니다!
if (axios.isAxiosError(error)) { | ||
if (error.response) { | ||
switch (error.response.status) { | ||
case 403: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사실 지금 만으로도 충분하지만, 살짝 욕심을 내보자면,
403같은 상태 코드는 상수로 관리되는 것이 좋아보여요. 물론 상태코드는 어느정도 명시적이지만, 그럼에도 403은 Forbidden을 나타낸다는 의미를 포함하면 좋을 것 같습니다.
const STATUS = {
Forbidden: 403,
} as const;
... case STATUS.Forbidden: ...
이 부분은 우찬님도 욕심이 난다면 적용해주세요. 하지만 지금 상태로도 좋습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
감사합니다!
import type { CompetitionApiData } from '@/apis/joinCompetition/types'; | ||
import useAuth from '@/hooks/login/useAuth'; | ||
|
||
const TOKEN_KEY = 'accessToken'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
상수화하여 관리하는 것 아주 좋아요.
개인적인 생각엔 useAuth에서 token까지 반환하는게 어떨까요?
const token = queryParams.get(TOKEN_KEY) || localStorage.getItem(TOKEN_KEY);
위 코드를 useAuth 안에 넣고 token을 꺼내 쓰면 더 깔끔할 것 같아요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
리뷰 감사합니다!
이 부분은 유석님이 상수화해서 관리하던 코드를 제가 그대로 가져와서 사용하기만 한 거라서, 이번 PR에서는 넘어가도 될까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
알겠습니다 :)
const handleJoinClick = async () => { | ||
if (!isLoggedin) { | ||
alert('로그인이 필요합니다.'); | ||
window.location.href = '/login'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분이 배포에서도 정상작동할지 모르겠네요??
useNavigator를 쓰는게 더 좋아보이긴 합니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
감사합니다, useNavigator로 변경하겠습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 😎
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
충돌일어나서 생성하신 건가요?!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
netlify 빌드 과정에서 대소문자 오류가 생겨서 지웠다가 다시 생성했어요!
await api.post( | ||
`/competitions/${id}/participations`, | ||
{}, | ||
{ | ||
headers: { | ||
Authorization: `Bearer ${token}`, | ||
}, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
api 서버가 아직 구축 안 되어서 body가 비었다 생각했는데, 오로지 검증을 위한 거라면 post 대신 get을 써도 되지않을까요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
클라이언트에서 보내줄 데이터가 없어보여서요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분은 일단 API가 post를 사용하게 되어있어 따르는게 좋아보이고,
제 사견으론 의미론적인 GET과 POST의 차이를 봤을 때 POST가 맞는 것 같아요.
GET은 데이터를 가져오겠다라는 의미이며 멱등성을 보장하는 것이 좋습니다.
POST는 데이터를 변경하겠다는 의미를 포함해요.
여기선 변경에 필요한 데이터를 보내주지 않지만, 내 상태를 join으로 바꾸겠다는 의미에서 POST는 이상해보이진 않습니다.
유석님은 어떻게 생각하시나요
if (axios.isAxiosError(error)) { | ||
if (error.response) { | ||
switch (error.response.status) { | ||
case STATUS.Forbidden: | ||
errorMessage = '대회 참여에 실패했습니다. 서버에서 거절되었습니다.'; | ||
break; | ||
case 400: | ||
errorMessage = '이미 참여한 대회입니다.'; | ||
break; | ||
default: | ||
errorMessage = `HTTP Error ${error.response.status}`; | ||
break; | ||
} | ||
} else { | ||
errorMessage = 'Network error occurred.'; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isAxiosError 사용한 부분 아주 좋습니다. :) 제 개인적인 의견으로는 가독성을 위해
if (axios.isAxiosError(error)) { | |
if (error.response) { | |
switch (error.response.status) { | |
case STATUS.Forbidden: | |
errorMessage = '대회 참여에 실패했습니다. 서버에서 거절되었습니다.'; | |
break; | |
case 400: | |
errorMessage = '이미 참여한 대회입니다.'; | |
break; | |
default: | |
errorMessage = `HTTP Error ${error.response.status}`; | |
break; | |
} | |
} else { | |
errorMessage = 'Network error occurred.'; | |
} | |
} | |
if (!axios.isAxiosError(error)) { | |
return 'Unexpected error occurred'; | |
} | |
if (!error.response) { | |
return 'Network error occurred'; | |
} | |
switch(error.response.status) { | |
case STATUS.Forbidden: | |
errorMessage = '대회 참여에 실패했습니다. 서버에서 거절되었습니다.'; | |
break; | |
case 400: | |
errorMessage = '이미 참여한 대회입니다.'; | |
break; | |
default: | |
errorMessage = `HTTP Error ${error.response.status}`; | |
break; | |
} |
같은 코드가 어때요?
그리고 에러를 다양하게 핸들링한 부분 좋았어요. 기왕이면 STATUS.BadRequest도 사용하면 좋을 것 같네요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
감사합니다! 확실히 가독성이 좋아졌네요:D
<header className={headerStyle}> | ||
<ContestBreadCrumb crumbs={props.crumbs} /> | ||
<ViewDashboardButton id={props.id} /> | ||
</header> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아마 로그인 정보등을 담는 그냥 헤더와 대회 헤더가 나뉘어진 것 같은데,
header 태그가 중복되는 것은 접근성에 좋아보이지 않아요.
div나 다른 태그를 사용하는게 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
감사합니다, div태그로 수정할게요!
const fetchCompetitionList = async () => { | ||
try { | ||
const response = await axios.get<Competition[]>(`${apiUrl}competitions`); | ||
setCompetitions(response.data); | ||
} catch (error) { | ||
console.error('Error fetching competitions:', (error as Error).message); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
외부 서버로부터 정보를 받아오는 로직은 apis 폴더로 뺄 수 있을까요?
그리고 utils에 api파일을 만들어둬서 apiUrl없이 api.get과 같이 접근해서 데이터 패치 가능해요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵, 앞으로도 api로직은 분리하겠습니다 감사합니다!
try { | ||
const response = await api.get<Competition[]>(`${apiUrl}competitions`); | ||
return response.data; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아하 이 부분을 이해 못하셨군요.
Axios의 create 인터페이스를 활용하면 Axios 인스턴스라는 것을 만들 수 있습니다. 이 인스턴스는 baseUrl을 설정하면 다음 url에 대해 baseUrl + url 처럼 동작합니다.
const instance = axios.create({ baseUrl: 'http://root.domain.com' })
const request = instance.get('/competitions'); // http://root.domain.com/competitions 와 같이 동작합니다.
이런식으로 변경해달라는 요청이었어요. api 폴더의 다른 요청들을 보면 이해가 빠를 겁니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
앗, 감사합니다!
let errorMessage = 'Unexpected error occurred.'; | ||
|
||
if (!axios.isAxiosError(error)) { | ||
return 'Unexpected error occurred'; | ||
} | ||
|
||
if (!error.response) { | ||
return 'Network error occurred'; | ||
} | ||
|
||
switch (error.response.status) { | ||
case STATUS.Forbidden: | ||
errorMessage = '대회 참여에 실패했습니다. 서버에서 거절되었습니다.'; | ||
break; | ||
case STATUS.BadRequest: | ||
errorMessage = '이미 참여한 대회입니다.'; | ||
break; | ||
default: | ||
errorMessage = `HTTP Error ${error.response.status}`; | ||
break; | ||
} | ||
|
||
return errorMessage; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
27번줄과 49번줄은 더 이상 필요 없어보여요. 대신 switch문 안에서 return을 해주면 될 것 같네요
switch() {
case aaa:
return '대회 어쩌구'
default:
return '어쩌구'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
감사합니다!
import ContestBreadCrumb from './ContestBreadCrumb'; | ||
interface Props { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
악 개인적인 요청사항이지만 한 칸만 띄워주세요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵 다음부터는 미리 확인하겠습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:)
한 일
ContestPage
MainPage
메인 페이지
로그인이 되어있지 않은 경우(참여하기 버튼 클릭시 동일하게 동작)
대회 참여 버튼 클릭시 로그인이 되어있는 경우
이미 참여한 대회에 대해 참여하기 버튼을 클릭시