Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
import store.oneul.mvc.challenge.dto.ChallengeDTO;
import store.oneul.mvc.challenge.dto.ChallengeUserDTO;
import store.oneul.mvc.challenge.exception.ChallengeAlreadyJoinedException;
import store.oneul.mvc.common.exception.ForbiddenException;
import store.oneul.mvc.common.exception.InvalidParameterException;
import store.oneul.mvc.common.exception.NotFoundException;

@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -43,7 +45,14 @@ public void deleteChallenge(Long challengeId) {

@Override
public ChallengeDTO getMyChallenge(Map<String, Object> paramMap) {
return challengeDAO.getMyChallenge(paramMap);
ChallengeDTO dto = challengeDAO.getMyChallenge(paramMap);
if(dto == null) {
throw new NotFoundException("챌린지를 찾을 수 없습니다.");
}
if(dto.getSuccessDay() == null) {
throw new ForbiddenException("챌린지 조회 권한이 없습니다.");
}
return dto;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package store.oneul.mvc.common.exception;

public class ForbiddenException extends RuntimeException {
public ForbiddenException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ public enum ErrorCode {
// 필요에 따라 추가
NOT_FOUND,
CHALLENGE_ALREADY_JOINED,
FORBIDDEN
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.web.bind.annotation.RestControllerAdvice;

import store.oneul.mvc.challenge.exception.ChallengeAlreadyJoinedException;
import store.oneul.mvc.common.exception.ForbiddenException;
import store.oneul.mvc.common.exception.InvalidParameterException;
import store.oneul.mvc.common.exception.NotFoundException;
import store.oneul.mvc.payment.dto.TossErrorInfo;
Expand Down Expand Up @@ -72,5 +73,11 @@ public ResponseEntity<ApiResponse<String>> handleAlreadyJoined(ChallengeAlreadyJ
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ApiResponse.error(ex.getMessage(), ErrorCode.CHALLENGE_ALREADY_JOINED.name()));
}

@ExceptionHandler(ForbiddenException.class)
public ResponseEntity<ApiResponse<String>> handleForbidden(ForbiddenException ex) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ApiResponse.error(ex.getMessage(), ErrorCode.FORBIDDEN.name()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
c.member_count
FROM challenge c
JOIN user u ON c.owner_id = u.user_id
JOIN challenge_user cu
LEFT JOIN challenge_user cu
ON cu.challenge_id = c.challenge_id AND cu.user_id = #{loginUserId}
WHERE c.challenge_id = #{challengeId}
</select>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import PaymentSuccessPage from "@components/payment/PaymentSuccessPage";
import PaymentFailPage from "@components/payment/PaymentFailPage";
import MyPage from "@components/mypage/MyPage";
import ProtectedLayout from "./layouts/ProtectedLayout";
import NotFoundPage from "./components/common/NotFoundPage";

function App() {
const { user } = useUserStore();
Expand Down Expand Up @@ -136,6 +137,7 @@ function App() {
/>
<Route path="/mypage" element={<MyPage />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
</Route>

{/* MainLayout이 필요없는 라우트들 */}
Expand Down
27 changes: 22 additions & 5 deletions frontend/src/components/challengeDetail/ChallengeDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import ChallengeFeed from "./ChallengeFeed";
import ChallengeDetail from "./ChellengeDetail";
import ChallengeStatus from "./ChallengeStatus";
import { useParams } from "react-router";
import { Navigate, useNavigate, useParams } from "react-router";
import { useMyChallenge } from "@/hooks/useChallenge";
import Toast from "../Toast/Toast";
import { useEffect, useRef, useState } from "react";
import { FiClock } from "react-icons/fi";
import { useUserStore } from "@/stores/userStore";
import ChallengeFeedCheck from "../feedCheck/ChallengeFeedCheck";
import NotFoundPage from "../common/NotFoundPage";
import { AiOutlineLoading3Quarters } from "react-icons/ai";

const tabs = [
{ key: "challenge", label: "챌린지 디테일" },
Expand All @@ -16,12 +18,15 @@ const tabs = [

function ChallengeDetailPage() {
const { challengeId } = useParams<{ challengeId: string }>();
const idNum = Number(challengeId);
const {
data: challenge,
isLoading,
isError,
error,
statusCode,
} = useMyChallenge(challengeId ?? "");
const navigate = useNavigate();

const hasShownToast = useRef(false);
const [activeTab, setActiveTab] = useState("challenge");
Expand Down Expand Up @@ -64,19 +69,31 @@ function ChallengeDetailPage() {
}
};

if (!challengeId) return <p>잘못된 경로입니다.</p>;
if (Number.isNaN(idNum) || statusCode === 404) {
return <NotFoundPage />;
}

if (statusCode === 403) {
Toast.caution("참여중인 챌린지가 아닙니다.");
navigate("/challenge/search", { replace: true });
}

if (statusCode === 401) {
return <Navigate to="/login" replace state={{ showAuthToast: true }} />;
}

// 로딩 또는 에러시
if (isLoading) {
return (
<div className="flex h-screen items-center justify-center text-white">
로딩 중
<div className="mt-[-60px] flex h-screen flex-col items-center justify-center gap-6">
<AiOutlineLoading3Quarters className="text-primary-purple-100 h-10 w-10 animate-spin" />
<p className="text-gray-300">챌린지 정보 불러오는 중</p>
</div>
);
}
if (isError) {
return (
<div className="flex h-screen items-center justify-center text-red-400">
<div className="mt-[-60px] flex h-screen items-center justify-center text-red-400">
에러: {(error as Error).message}
</div>
);
Expand Down
69 changes: 69 additions & 0 deletions frontend/src/components/common/NotFoundPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useNavigate } from "react-router";

function NotFoundPage() {
const navigate = useNavigate();
return (
<div className="mt-[-60px] flex h-screen w-full flex-col items-center justify-center gap-6 p-[200px]">
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 600"
preserveAspectRatio="xMidYMid meet"
width={200}
height={200}
>
<g
transform="translate(0,512) scale(0.1,-0.1)"
className="fill-primary-purple-100"
stroke="none"
>
<path
d="M2445 5106 c-28 -7 -72 -22 -98 -35 -95 -46 -135 -95 -312 -383 -210
-341 -686 -1103 -1001 -1603 -387 -613 -413 -660 -422 -770 -18 -229 134 -436
356 -484 75 -16 3108 -16 3184 0 221 47 374 256 355 486 -7 90 -63 199 -270
528 -540 859 -833 1327 -1057 1690 -148 241 -271 429 -298 457 -101 105 -282
152 -437 114z m265 -1486 l0 -600 -150 0 -150 0 0 600 0 600 150 0 150 0 0
-600z m0 -1050 l0 -150 -150 0 -150 0 0 150 0 150 150 0 150 0 0 -150z"
/>
</g>
<g
transform="translate(0,580) scale(0.1,-0.1)"
className="fill-primary-purple-100"
stroke="none"
>
<path
d="M610 1070 l0 -450 300 0 300 0 0 -310 0 -310 150 0 150 0 0 310 0
310 150 0 150 0 0 150 0 150 -150 0 -150 0 0 300 0 300 -150 0 -150 0 0 -300
0 -300 -150 0 -150 0 0 300 0 300 -150 0 -150 0 0 -450z"
/>
<path
d="M2110 760 l0 -760 450 0 450 0 0 760 0 760 -450 0 -450 0 0 -760z
m600 0 l0 -460 -150 0 -150 0 0 460 0 460 150 0 150 0 0 -460z"
/>
<path
d="M3310 1070 l0 -450 300 0 300 0 0 -310 0 -310 150 0 150 0 0 310 0
310 150 0 150 0 0 150 0 150 -150 0 -150 0 0 300 0 300 -150 0 -150 0 0 -300
0 -300 -150 0 -150 0 0 300 0 300 -150 0 -150 0 0 -450z"
/>
</g>
</svg>
<div className="flex flex-col gap-4">
<h2 className="text-primary-purple-100 text-2xl font-semibold">
페이지를 찾을 수 없습니다.
</h2>
<p className="text-center text-gray-300">
올바른 URL 경로가 아닙니다. <br /> 페이지 경로를 다시 한 번
확인해주세요.
</p>
</div>
<button
className="bg-primary-purple-200 hover:bg-primary-purple-200/80 mt-2 rounded-lg px-6 py-3 text-white transition"
onClick={() => navigate("/", { replace: true })}
>
홈페이지로 돌아가기
</button>
</div>
);
}

export default NotFoundPage;
9 changes: 8 additions & 1 deletion frontend/src/hooks/useChallenge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { joinChallenge } from "@/api/challenge";
/** 챌린지 정보 호출 */
// 내가 가입한 챌린지의 정보
export function useMyChallenge(challengeId: string) {
return useGet<Challenge>(
const query = useGet<Challenge, AxiosError>(
["myChallenge", challengeId],
`/challenges/my/${challengeId}`,
{
Expand All @@ -17,6 +17,13 @@ export function useMyChallenge(challengeId: string) {
enabled: Boolean(challengeId),
},
);

const statusCode = query.error?.response?.status;

return {
...query,
statusCode,
};
}

// 가입 유무 상관 없이 가져오는 챌린지 정보
Expand Down