Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
c.end_date,
c.entry_fee,
c.created_at,
c.challenge_status,
u.nickname AS owner_nickname,
cu.success_day,

Expand Down
20 changes: 13 additions & 7 deletions frontend/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import axios, { AxiosRequestConfig, AxiosResponse } from "axios";

export const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
withCredentials: true, // 쿠키 인증 등에 필요
withCredentials: true, // HttpOnly 쿠키(refreshToken) 자동 포함
headers: { "Content-Type": "application/json" },
});

Expand All @@ -18,14 +18,20 @@ api.interceptors.request.use((config) => {

// 응답 인터셉터
api.interceptors.response.use(
(res) => res,
(err) => {
if (err.response?.status === 401) {
// 로그아웃 처리 or 리프레시
console.error("Unauthorized. Redirecting to login...");
(res) => {
// 백엔드 필터가 새 토큰을 Authorization 헤더에 담아 내려주면
const newToken = res.headers["authorization"] as string | undefined;
if (newToken?.startsWith("Bearer ")) {
useUserStore
.getState()
.setUser(
useUserStore.getState().user!,
newToken.replace("Bearer ", ""),
);
}
return Promise.reject(err);
return res;
},
(err) => Promise.reject(err),
);

// 공통 메서드 정의
Expand Down
19 changes: 18 additions & 1 deletion frontend/src/components/challengeDetail/ChallengeDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import ChallengeDetail from "./ChellengeDetail";
import ChallengeStatus from "./ChallengeStatus";
import { useParams } from "react-router";
import { useMyChallenge } from "@/hooks/useChallenge";
import Toast from "../Toast/Toast";
import { useEffect, useRef } from "react";

function ChallengeDetailPage() {
const { challengeId } = useParams<{ challengeId: string }>();
Expand All @@ -13,6 +15,19 @@ function ChallengeDetailPage() {
error,
} = useMyChallenge(challengeId ?? "");

const hasShownToast = useRef(false);

useEffect(() => {
if (
challenge &&
challenge.challengeStatus === "RECRUITING" &&
!hasShownToast.current
) {
Toast.caution("아직 시작하지 않은 챌린지입니다.");
hasShownToast.current = true;
}
}, [challenge]);

if (!challengeId) return <p>잘못된 경로입니다.</p>;

// 로딩 또는 에러시
Expand Down Expand Up @@ -44,13 +59,15 @@ function ChallengeDetailPage() {
success={challenge.successDay ?? 0}
goal={challenge.goalDay ?? 0}
endDate={challenge.endDate ?? ""}
startDate={challenge.startDate ?? 0}
status={challenge.challengeStatus}
/>
</>
) : (
<p>데이터를 불러오는데 실패했습니다.</p>
)}
</div>
<ChallengeFeed />
{challenge && <ChallengeFeed status={challenge?.challengeStatus} />}
</div>
</section>
</div>
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/components/challengeDetail/ChallengeFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ import { useParams } from "react-router";
import { useChallengeFeeds } from "@/hooks/useFeed";
import { Feed } from "@/types/Feed";
import { useQueryClient } from "@tanstack/react-query";
import { ChallengeStatusType } from "@/types/Challenge";

type ModalState =
| { kind: "create" }
| { kind: "edit"; feed: Feed }
| { kind: "detail"; feed: Feed }
| null;

function ChallengeFeed() {
interface ChallengeFeedProps {
status: ChallengeStatusType;
}

function ChallengeFeed({ status }: ChallengeFeedProps) {
const { challengeId } = useParams<{ challengeId: string }>();
const [modalState, setModalState] = useState<ModalState>(null);

Expand Down Expand Up @@ -69,7 +74,7 @@ function ChallengeFeed() {
<span className="text-center text-xl font-semibold text-gray-200">
챌린지 피드
</span>
<FeedCreateBtn />
{status === "IN_PROGRESS" && <FeedCreateBtn />}
</div>
<div className="flex gap-6">
<div className="flex-1">
Expand All @@ -78,6 +83,7 @@ function ChallengeFeed() {
onCreate={openCreate}
onEdit={openEdit}
onDetail={openDetail}
disabled={status === "RECRUITING"}
/>
</div>

Expand Down
42 changes: 40 additions & 2 deletions frontend/src/components/challengeDetail/ChallengeStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,52 @@
import { ChallengeStatusType } from "@/types/Challenge";

interface ChallengeStatusProps {
success: number;
goal: number;
startDate: string;
endDate: string;
status: ChallengeStatusType;
}

function ChallengeStatus({ success, endDate, goal }: ChallengeStatusProps) {
function ChallengeStatus({
success,
startDate,
endDate,
goal,
status,
}: ChallengeStatusProps) {
const elapsedDays =
Math.floor(
(new Date(endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24),
) + 1;
const daysToStart =
Math.floor(
(new Date(startDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24),
) + 1;
if (status === "RECRUITING") {
return (
<div className="flex flex-col gap-6 rounded-2xl bg-[#1A1A1F] px-8 py-9">
<h4 className="text-lg text-gray-200">나의 달성 현황</h4>
<div className="flex flex-col gap-6 leading-7 text-gray-400">
<p>
아직 시작하지 않은 챌린지예요!
<br />
<span className="text-primary-purple-100 font-medium">
총 {success}일, 100% 달성 중
</span>
</p>
<p>
오늘부터 챌린지 시작일까지
<br />
<span className="text-primary-purple-100 font-medium">
{daysToStart}일 남았어요!
</span>
</p>
</div>
</div>
);
}

return (
<div className="flex flex-col gap-6 rounded-2xl bg-[#1A1A1F] px-8 py-9">
<h4 className="text-lg text-gray-200">나의 달성 현황</h4>
Expand All @@ -23,7 +61,7 @@ function ChallengeStatus({ success, endDate, goal }: ChallengeStatusProps) {
<p>
챌린지 종료일까지 {elapsedDays > 0 ? elapsedDays : 0}일 중<br />
<span className="text-primary-purple-100 font-medium">
{goal - success}일 남았어요!
{goal - success > 0 ? goal - success : 0}일 남았어요!
</span>
</p>
</div>
Expand Down
27 changes: 19 additions & 8 deletions frontend/src/components/feed/MyFeedCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface MyFeedCardProps {
onCreate?: () => void;
onEdit?: (feed: Feed) => void;
onDetail?: (feed: Feed) => void;
disabled: boolean;
}

function isFeedToday(feed: Feed | null): boolean {
Expand All @@ -20,19 +21,29 @@ function isFeedToday(feed: Feed | null): boolean {
);
}

function MyFeedCard({ feed, onCreate, onEdit, onDetail }: MyFeedCardProps) {
function MyFeedCard({
feed,
onCreate,
onEdit,
onDetail,
disabled,
}: MyFeedCardProps) {
if (!feed || !isFeedToday(feed)) {
return (
<>
<p className="mb-4 text-gray-400">
아직 오늘의 인증을 진행하지 않으셨어요.
{disabled
? "챌린지가 오픈되면 인증이 가능합니다."
: "아직 오늘의 인증을 진행하지 않으셨어요."}
</p>
<button
onClick={onCreate}
className="flex aspect-square w-full cursor-pointer select-none items-center justify-center rounded-md border border-[#2d2d2d] bg-[#24242c] text-9xl font-extralight text-gray-600"
>
+
</button>
{!disabled && (
<button
onClick={onCreate}
className="flex aspect-square w-full cursor-pointer select-none items-center justify-center rounded-md border border-[#2d2d2d] bg-[#24242c] text-9xl font-extralight text-gray-600"
>
+
</button>
)}
</>
);
}
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/types/Challenge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export interface Challenge {
successDay?: number; // 삭제 예정
roomPassword: string; // 삭제 예정
memberCount: number;
challengeStatus: string;
challengeStatus: ChallengeStatusType;
participantCount?: number;
}

export type ChallengeStatusType = "RECRUITING" | "ENDED" | "IN_PROGRESS";