Skip to content

Commit

Permalink
솔루션 상세 페이지 구현 (issue #349) (#364)
Browse files Browse the repository at this point in the history
* 로그아웃 뮤테이션 생성 및 제출시 로직 개선 (issue #306) (#327)

* feat: 로그아웃 관련 뮤테이션 생성

* refactor: useModal 리팩토링

* refactor: 불필요한 주석 제거

* fix: 미션 제출 후 새로고침 로직 개선

* refactor: 대쉬보드 프로필 레이아웃 수정

* refactor: description null값도 보낼수 있도록 수정

* refactor: 미션명 검증 섹션 추가

* fix: 린트 disable

* refactor: 수정사항 반영

* 해시태그 버튼 및 필터링 구현 (issue #303) (#332)

* feat: 미션 리스트 필터링 기능 구현

* refactor: 미션 호출 api의 url 분리 및 queryKey에 filter 추가

* fix: hashTagButton 충돌 해결

* feat: 전체 데이터 불러오는 'all'로 변경, HashTag 필터링 mock data 임시 구현

* feat: 솔루션 필터링 적용, useSolutions를 useSolutionSummaries로 변경

* design: 솔루션 리스트 디자인 수정

* design: 미션리스트 디자인 수정

* feat: api 배포에 따른 mock data 제거

* test: useMissions 필터링 기능 테스트 코드 추가

* design: 미션, 솔루션 리스트 렌더링 시 애니메이션 효과 적용

* refactor: 'all'을 상수로 변경

* refactor: useToggleHashTag 제거 및 해시태그 토글 상태 추가

* chore: 개발 환경과 배포환경에 따른 백엔드 API 경로 수정 (#344)

* fix: 대시보드 렌더링 분기처리 오류 수정 (#346)

* feat: 솔루션 상세 페이지 구현

---------

Co-authored-by: JEON TAEHEON <[email protected]>
Co-authored-by: Minji <[email protected]>
  • Loading branch information
3 people committed Aug 21, 2024
1 parent f3ef134 commit 57085fd
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 9 deletions.
1 change: 1 addition & 0 deletions frontend/src/apis/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const PATH = {
logout: '/auth/logout',
hashTags: '/hash-tags',
missionInProgress: '/missions/in-progress',
solutions: '/solutions',
};

export const PATH_FORMATTER = {
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/apis/solutions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ export const getSolutionSummaries = async (
return data;
};

interface GetSolutionResponse {
data: Solution;
}

export const getSolutionById = async (solutionId: number): Promise<Solution> => {
const { data } = await develupAPIClient.get<GetSolutionResponse>(
`${PATH.solutions}/${solutionId}`,
);

return data;
};

export interface PostSolutionResponse {
data: Solution;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import SanitizedMDPreview from '@/components/common/SanitizedMDPreview';
import styled from 'styled-components';

export const CommentListContainer = styled.div`
margin-top: 3rem;
margin: 3rem 0;
`;

export const CommentItemContainer = styled.div`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ export const SolutionDetailPageContainer = styled.div`
export const CommentFormWrapper = styled.div`
margin-top: 4rem;
`;

export const SeparationLine = styled.div`
border-top: solid 4px ${({ theme }) => theme.colors.grey200};
margin: 10rem 0 4rem;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Solution } from '@/types/solution';
import * as S from './SolutionSection.styled';
import HashTagButton from '@/components/common/HashTagButton';

interface SolutionDetailHeaderProps {
solution: Solution;
}

export default function SolutionDetailHeader({ solution }: SolutionDetailHeaderProps) {
const { mission, member, title } = solution;

return (
<S.SolutionDetailHeaderContainer>
<S.ThumbnailWrapper>
<S.ThumbnailImg src={mission.thumbnail} alt="미션 썸네일 이미지" />
<S.GradientOverlay />
<S.HeaderLeftArea>
<S.MissionTitle># {mission.title}</S.MissionTitle>
<S.Title>{title}</S.Title>
<S.HeaderUserInfo>
<S.HeaderProfileImg src={member.imageUrl} />
<S.HeaderUserName>{member.name}</S.HeaderUserName>
</S.HeaderUserInfo>
</S.HeaderLeftArea>
<S.HashTagWrapper>
{mission.hashTags &&
mission.hashTags.map((tag) => <HashTagButton key={tag.id}># {tag.name}</HashTagButton>)}
</S.HashTagWrapper>
</S.ThumbnailWrapper>
</S.SolutionDetailHeaderContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import styled from 'styled-components';
import javaIcon from '@/assets/images/java.svg';
import GithubLogo from '@/assets/images/githubLogo.svg';

export const SolutionDetailTitle = styled.h2`
margin: 4rem 0 2rem 0;
${({ theme }) => theme.font.heading1}
`;

export const MissionTitle = styled.div`
width: fit-content;
${({ theme }) => theme.font.badge}
background-color: ${({ theme }) => theme.colors.danger50};
padding: 1rem 2rem;
border-radius: 2rem;
`;

export const HeaderUserName = styled.div`
color: ${({ theme }) => theme.colors.white};
${({ theme }) => theme.font.bodyBold}
`;

export const HeaderUserInfo = styled.div`
display: flex;
align-items: center;
gap: 1.2rem;
`;

export const SolutionDetailHeaderContainer = styled.div`
width: 100%;
height: 20rem;
margin: 0 auto;
display: flex;
flex-direction: column;
position: relative;
`;

export const GithubIcon = styled(GithubLogo)`
width: 2.2rem;
height: 2.2rem;
display: flex;
justify-content: center;
margin-right: 0.3rem;
`;

export const ThumbnailWrapper = styled.div`
position: relative;
height: 100%;
border-radius: 1rem;
overflow: hidden;
`;

export const ThumbnailImg = styled.img`
width: 100%;
height: 100%;
object-fit: cover;
`;

export const GradientOverlay = styled.div`
position: absolute;
inset: 0;
background: linear-gradient(rgba(0, 0, 0, 0), ${(props) => props.theme.colors.black});
opacity: 0.5;
pointer-events: none; // 그라데이션이 클릭 이벤트를 방지하지 않도록 설정
`;

export const HeaderLeftArea = styled.div`
position: absolute;
left: 2.1rem;
bottom: 2.4rem;
display: flex;
flex-direction: column;
`;

export const HeaderProfileImg = styled.img`
width: 4.2rem;
border-radius: 10rem;
`;

export const Title = styled.h1`
margin: 1rem 0;
${(props) => props.theme.font.heading1}
color: ${(props) => props.theme.colors.white};
`;

export const JavaIcon = styled(javaIcon)``;

export const HashTagWrapper = styled.ul`
display: flex;
align-items: center;
justify-content: center;
gap: 1.1rem;
position: absolute;
right: 2.1rem;
bottom: 2.4rem;
`;

export const CodeViewButtonWrapper = styled.div`
margin: 3rem 0;
`;

export const SolutionDescription = styled.div`
margin-top: 3rem;
${({ theme }) => theme.font.body}
`;
26 changes: 26 additions & 0 deletions frontend/src/components/SolutionDetail/SolutionSection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as S from './SolutionSection.styled';
import type { Solution } from '@/types/solution';
import Button from '@/components/common/Button/Button';
import SolutionDetailHeader from './SolutionDetailHeader';

interface SolutionDetailProps {
solution: Solution;
}

export default function SolutionSection({ solution }: SolutionDetailProps) {
const { description } = solution;

return (
<section>
<S.SolutionDetailTitle>📝 Solution</S.SolutionDetailTitle>
<SolutionDetailHeader solution={solution} />
<S.CodeViewButtonWrapper>
<Button variant="default">
<S.GithubIcon />
코드 보러 가기
</Button>
</S.CodeViewButtonWrapper>
<S.SolutionDescription>{description}</S.SolutionDescription>
</section>
);
}
1 change: 1 addition & 0 deletions frontend/src/hooks/queries/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const commentKeys = {

export const solutionKeys = {
all: ['solutions'],
detail: (id: number) => [...solutionKeys.all, id],
summaries: ['solutionSummaries'],
submitted: ['submitted solutions'],
};
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/hooks/useSolution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { solutionKeys } from './queries/keys';
import type { Solution } from '@/types/solution';
import { getSolutionById } from '@/apis/solutions';

const useSolution = (solutionId: number) => {
return useSuspenseQuery<Solution>({
queryKey: solutionKeys.detail(solutionId),
queryFn: () => getSolutionById(solutionId),
});
};

export default useSolution;
11 changes: 10 additions & 1 deletion frontend/src/pages/SolutionDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,25 @@ import * as S from '@/components/SolutionDetail/SolutionDetail.styled';
import usePathnameAt from '@/hooks/usePathnameAt';
import useUserInfo from '@/hooks/useUserInfo';
import { useComments } from '@/hooks/useComments';
import useSolution from '@/hooks/useSolution';
import SolutionSection from '@/components/SolutionDetail/SolutionSection';

export default function SolutionDetailPage() {
const { data: userInfo } = useUserInfo();
const solutionId = Number(usePathnameAt(-1));

const { data: solution } = useSolution(solutionId);
const { data: comments } = useComments(solutionId);

const hasComment = comments.length > 0;
const isLoggedIn = Boolean(userInfo);

return (
<S.SolutionDetailPageContainer>
<SolutionSection solution={solution} />
{(hasComment || isLoggedIn) && <S.SeparationLine />}
<CommentList comments={comments} />
{userInfo && (
{isLoggedIn && (
<S.CommentFormWrapper>
<CommentForm solutionId={solutionId} />
</S.CommentFormWrapper>
Expand Down
8 changes: 1 addition & 7 deletions frontend/src/types/solution.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
interface Mission {
id: number;
title: string;
thumbnail: string;
url: string;
descriptionUrl: string;
}
import type { Mission } from '.';

interface Member {
id: number;
Expand Down

0 comments on commit 57085fd

Please sign in to comment.