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;