From 57085fd557c5c572037dfa36a4b8616b6005c39c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=ED=95=9C=EC=98=81=28Ryan=29?= Date: Wed, 21 Aug 2024 13:12:43 +0900 Subject: [PATCH] =?UTF-8?q?=EC=86=94=EB=A3=A8=EC=85=98=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(issue=20#349)=20(#364)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 로그아웃 뮤테이션 생성 및 제출시 로직 개선 (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 Co-authored-by: Minji <121149171+chosim-dvlpr@users.noreply.github.com> --- frontend/src/apis/paths.ts | 1 + frontend/src/apis/solutions.ts | 12 ++ .../CommentList/CommentList.styled.ts | 2 +- .../SolutionDetail/SolutionDetail.styled.ts | 5 + .../SolutionSection/SolutionDetailHeader.tsx | 32 ++++++ .../SolutionSection/SolutionSection.styled.ts | 106 ++++++++++++++++++ .../SolutionDetail/SolutionSection/index.tsx | 26 +++++ frontend/src/hooks/queries/keys.ts | 1 + frontend/src/hooks/useSolution.ts | 13 +++ frontend/src/pages/SolutionDetailPage.tsx | 11 +- frontend/src/types/solution.ts | 8 +- 11 files changed, 208 insertions(+), 9 deletions(-) create mode 100644 frontend/src/components/SolutionDetail/SolutionSection/SolutionDetailHeader.tsx create mode 100644 frontend/src/components/SolutionDetail/SolutionSection/SolutionSection.styled.ts create mode 100644 frontend/src/components/SolutionDetail/SolutionSection/index.tsx create mode 100644 frontend/src/hooks/useSolution.ts diff --git a/frontend/src/apis/paths.ts b/frontend/src/apis/paths.ts index 78ea2ec5..58874690 100644 --- a/frontend/src/apis/paths.ts +++ b/frontend/src/apis/paths.ts @@ -12,6 +12,7 @@ export const PATH = { logout: '/auth/logout', hashTags: '/hash-tags', missionInProgress: '/missions/in-progress', + solutions: '/solutions', }; export const PATH_FORMATTER = { diff --git a/frontend/src/apis/solutions.ts b/frontend/src/apis/solutions.ts index 63d3182a..c51e5bc7 100644 --- a/frontend/src/apis/solutions.ts +++ b/frontend/src/apis/solutions.ts @@ -29,6 +29,18 @@ export const getSolutionSummaries = async ( return data; }; +interface GetSolutionResponse { + data: Solution; +} + +export const getSolutionById = async (solutionId: number): Promise => { + const { data } = await develupAPIClient.get( + `${PATH.solutions}/${solutionId}`, + ); + + return data; +}; + export interface PostSolutionResponse { data: Solution; } diff --git a/frontend/src/components/SolutionDetail/CommentList/CommentList.styled.ts b/frontend/src/components/SolutionDetail/CommentList/CommentList.styled.ts index bcc68fee..9b8813b5 100644 --- a/frontend/src/components/SolutionDetail/CommentList/CommentList.styled.ts +++ b/frontend/src/components/SolutionDetail/CommentList/CommentList.styled.ts @@ -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` diff --git a/frontend/src/components/SolutionDetail/SolutionDetail.styled.ts b/frontend/src/components/SolutionDetail/SolutionDetail.styled.ts index d0e66375..bee8e22c 100644 --- a/frontend/src/components/SolutionDetail/SolutionDetail.styled.ts +++ b/frontend/src/components/SolutionDetail/SolutionDetail.styled.ts @@ -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; +`; diff --git a/frontend/src/components/SolutionDetail/SolutionSection/SolutionDetailHeader.tsx b/frontend/src/components/SolutionDetail/SolutionSection/SolutionDetailHeader.tsx new file mode 100644 index 00000000..8108ac8f --- /dev/null +++ b/frontend/src/components/SolutionDetail/SolutionSection/SolutionDetailHeader.tsx @@ -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 ( + + + + + + # {mission.title} + {title} + + + {member.name} + + + + {mission.hashTags && + mission.hashTags.map((tag) => # {tag.name})} + + + + ); +} diff --git a/frontend/src/components/SolutionDetail/SolutionSection/SolutionSection.styled.ts b/frontend/src/components/SolutionDetail/SolutionSection/SolutionSection.styled.ts new file mode 100644 index 00000000..30ecfb40 --- /dev/null +++ b/frontend/src/components/SolutionDetail/SolutionSection/SolutionSection.styled.ts @@ -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} +`; diff --git a/frontend/src/components/SolutionDetail/SolutionSection/index.tsx b/frontend/src/components/SolutionDetail/SolutionSection/index.tsx new file mode 100644 index 00000000..4446b731 --- /dev/null +++ b/frontend/src/components/SolutionDetail/SolutionSection/index.tsx @@ -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 ( +
+ 📝 Solution + + + + + {description} +
+ ); +} diff --git a/frontend/src/hooks/queries/keys.ts b/frontend/src/hooks/queries/keys.ts index b5cef85e..60ba3825 100644 --- a/frontend/src/hooks/queries/keys.ts +++ b/frontend/src/hooks/queries/keys.ts @@ -16,6 +16,7 @@ export const commentKeys = { export const solutionKeys = { all: ['solutions'], + detail: (id: number) => [...solutionKeys.all, id], summaries: ['solutionSummaries'], submitted: ['submitted solutions'], }; diff --git a/frontend/src/hooks/useSolution.ts b/frontend/src/hooks/useSolution.ts new file mode 100644 index 00000000..9a5d034e --- /dev/null +++ b/frontend/src/hooks/useSolution.ts @@ -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({ + queryKey: solutionKeys.detail(solutionId), + queryFn: () => getSolutionById(solutionId), + }); +}; + +export default useSolution; diff --git a/frontend/src/pages/SolutionDetailPage.tsx b/frontend/src/pages/SolutionDetailPage.tsx index 2c116d6e..c9729471 100644 --- a/frontend/src/pages/SolutionDetailPage.tsx +++ b/frontend/src/pages/SolutionDetailPage.tsx @@ -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 ( + + {(hasComment || isLoggedIn) && } - {userInfo && ( + {isLoggedIn && ( diff --git a/frontend/src/types/solution.ts b/frontend/src/types/solution.ts index 5d903b12..e22e0ec5 100644 --- a/frontend/src/types/solution.ts +++ b/frontend/src/types/solution.ts @@ -1,10 +1,4 @@ -interface Mission { - id: number; - title: string; - thumbnail: string; - url: string; - descriptionUrl: string; -} +import type { Mission } from '.'; interface Member { id: number;