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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*

# npm config (contains sensitive tokens)
.npmrc

# vercel
.vercel

Expand Down
3 changes: 0 additions & 3 deletions .npmrc

This file was deleted.

75 changes: 75 additions & 0 deletions app/(default)/_hooks/useCarousel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"use client";

import { useEffect, useRef, useState } from "react";

interface UseCarouselOptions<T> {
items: T[];
onSelect?: (item: T) => void;
}

export function useCarousel<T extends { id: string }>({
items,
onSelect,
}: UseCarouselOptions<T>) {
const [currentIndex, setCurrentIndex] = useState(0);
const onSelectRef = useRef(onSelect);
const isInitialMount = useRef(true);

// onSelect์˜ ์ตœ์‹  ๊ฐ’์„ ref์— ์ €์žฅ
useEffect(() => {
onSelectRef.current = onSelect;
}, [onSelect]);

const currentItem = items?.[currentIndex];
Comment on lines +14 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸก Minor

Index may become stale when items array changes.

If the items array shrinks (e.g., from 4 items to 2), currentIndex could remain at an invalid position, causing currentItem to be undefined and potential runtime errors in consumers.

๐Ÿ›ก๏ธ Suggested fix to reset index when items change
+	// Reset index if it becomes out of bounds when items change
+	useEffect(() => {
+		if (currentIndex >= items.length && items.length > 0) {
+			setCurrentIndex(0);
+		}
+	}, [items.length, currentIndex]);
+
 	const currentItem = items?.[currentIndex];
๐Ÿค– Prompt for AI Agents
In `@app/`(default)/_hooks/useCarousel.ts around lines 14 - 23, The currentIndex
state in the useCarousel hook can become invalid when the items array shrinks;
add a useEffect that watches items (and optionally items.length) and
resets/clamps currentIndex to a valid value (e.g., set to 0 if items is empty or
set to Math.min(currentIndex, items.length - 1)) so currentItem
(items[currentIndex]) never becomes undefined; update setCurrentIndex inside
that effect and reference the hook's currentIndex/items identifiers to locate
where to add this fix.


// currentIndex๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ ์ƒ์œ„๋กœ ์ „๋‹ฌ (์ดˆ๊ธฐ ๋งˆ์šดํŠธ ์ œ์™ธ)
useEffect(() => {
// ์ดˆ๊ธฐ ๋งˆ์šดํŠธ ์‹œ์—๋Š” ์‹คํ–‰ํ•˜์ง€ ์•Š์Œ
if (isInitialMount.current) {
isInitialMount.current = false;
return;
}

if (currentItem && onSelectRef.current) {
onSelectRef.current(currentItem);
}
}, [currentItem]);

const handlePrevious = () => {
setCurrentIndex((prev) => {
const newIndex = prev === 0 ? items.length - 1 : prev - 1;
const newItem = items[newIndex];
if (newItem) {
onSelect?.(newItem);
}
return newIndex;
});
};

const handleNext = () => {
setCurrentIndex((prev) => {
const newIndex = prev === items.length - 1 ? 0 : prev + 1;
const newItem = items[newIndex];
if (newItem) {
onSelect?.(newItem);
}
return newIndex;
});
};

const handleDotClick = (index: number) => {
setCurrentIndex(index);
const item = items[index];
if (item) {
onSelect?.(item);
}
};
Comment on lines +26 to +66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

onSelect is called twice per navigation action.

The handlers (handlePrevious, handleNext, handleDotClick) directly call onSelect, and then the useEffect on lines 26-36 also fires when currentItem changes, calling onSelectRef.current again. This causes the callback to execute twice.

Since currentIndex is internal state only modified by these handlers, the useEffect is redundant.

๐Ÿ› Remove redundant useEffect or direct calls

Option 1: Remove the useEffect (recommended - handlers already call onSelect)

-	// currentIndex๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ ์ƒ์œ„๋กœ ์ „๋‹ฌ (์ดˆ๊ธฐ ๋งˆ์šดํŠธ ์ œ์™ธ)
-	useEffect(() => {
-		// ์ดˆ๊ธฐ ๋งˆ์šดํŠธ ์‹œ์—๋Š” ์‹คํ–‰ํ•˜์ง€ ์•Š์Œ
-		if (isInitialMount.current) {
-			isInitialMount.current = false;
-			return;
-		}
-
-		if (currentItem && onSelectRef.current) {
-			onSelectRef.current(currentItem);
-		}
-	}, [currentItem]);

Option 2: Keep useEffect and remove direct calls in handlers

 	const handlePrevious = () => {
 		setCurrentIndex((prev) => {
 			const newIndex = prev === 0 ? items.length - 1 : prev - 1;
-			const newItem = items[newIndex];
-			if (newItem) {
-				onSelect?.(newItem);
-			}
 			return newIndex;
 		});
 	};
๐Ÿค– Prompt for AI Agents
In `@app/`(default)/_hooks/useCarousel.ts around lines 26 - 66, The useEffect
block watching currentItem (which checks isInitialMount and calls
onSelectRef.current) is redundant because the handlers handlePrevious,
handleNext, and handleDotClick already call onSelect directly, causing duplicate
calls; remove that useEffect (the block referencing isInitialMount.current,
currentItem, and onSelectRef.current) so onSelect is invoked only once from the
handlers, leaving handlePrevious, handleNext, handleDotClick, and the onSelect
prop as the single source of truth for selection callbacks.


return {
currentIndex,
currentItem,
handlePrevious,
handleNext,
handleDotClick,
};
}
41 changes: 41 additions & 0 deletions app/(default)/_hooks/useRandomPractice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"use client";

import { useRouter } from "next/navigation";
import {
detailSituations,
type SituationId,
} from "../practice/_constants/detailSituations";

export function useRandomPractice() {
const router = useRouter();

const getRandomSituation = () => {
// ๋ชจ๋“  ์ƒ์„ธ์ƒํ™ฉ์„ ํ‰ํƒ„ํ™”ํ•˜์—ฌ ๋ฐฐ์—ด๋กœ ๋งŒ๋“ค๊ธฐ
const allDetailSituations: Array<{
situationId: SituationId;
detailId: string;
}> = [];

(Object.keys(detailSituations) as SituationId[]).forEach((situationId) => {
detailSituations[situationId].forEach((detail) => {
allDetailSituations.push({
situationId,
detailId: detail.id,
});
});
});

// ๋žœ๋ค์œผ๋กœ ํ•˜๋‚˜ ์„ ํƒ
const randomIndex = Math.floor(Math.random() * allDetailSituations.length);
return allDetailSituations[randomIndex];
};

const navigateToRandomPractice = () => {
const selected = getRandomSituation();
router.push(`/practice/${selected.situationId}/${selected.detailId}/dial`);
Comment on lines +28 to +35
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸก Minor

Add a guard for empty array access.

If allDetailSituations is empty (e.g., data changes or import issues), randomIndex will be 0 and allDetailSituations[0] returns undefined, causing a runtime error when accessing selected.situationId.

๐Ÿ›ก๏ธ Suggested defensive check
 	const navigateToRandomPractice = () => {
 		const selected = getRandomSituation();
+		if (!selected) return;
 		router.push(`/practice/${selected.situationId}/${selected.detailId}/dial`);
 	};
๐Ÿค– Prompt for AI Agents
In `@app/`(default)/_hooks/useRandomPractice.ts around lines 28 - 35,
getRandomSituation currently assumes allDetailSituations has items and may
return undefined; update getRandomSituation (and its caller
navigateToRandomPractice) to guard against an empty array by checking
allDetailSituations.length === 0 and handling that case (e.g., return
null/undefined or throw a controlled error), then in navigateToRandomPractice
check the returned value before accessing selected.situationId/selected.detailId
and bail out or show an error/notification if no selection exists; modify
functions named getRandomSituation and navigateToRandomPractice accordingly so
they safely handle empty allDetailSituations.

};

return {
navigateToRandomPractice,
};
}
49 changes: 16 additions & 33 deletions app/(default)/components/EmergencySituationCarousel.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use client";

import { useEffect, useState } from "react";
import { IconWrapper } from "@/components/icons/IconWrapper";
import type { CarouselItem } from "../constants/practiceItems";
import { useCarousel } from "../_hooks/useCarousel";
import type { CarouselItem } from "../practice/_constants/practiceItems";

interface EmergencySituationCarouselProps {
items: CarouselItem[];
Expand All @@ -13,41 +13,24 @@ export function EmergencySituationCarousel({
items,
onSelect,
}: EmergencySituationCarouselProps) {
const [currentIndex, setCurrentIndex] = useState(0);

const currentItem = items?.[currentIndex];

// ์ดˆ๊ธฐ ์„ ํƒ๋œ ํ•ญ๋ชฉ์„ ์ƒ์œ„๋กœ ์ „๋‹ฌ (ํ›…์€ early return ์ „์— ํ˜ธ์ถœ๋˜์–ด์•ผ ํ•จ)
useEffect(() => {
if (currentItem && onSelect) {
onSelect(currentItem.id);
}
}, [currentItem, onSelect]);
const {
currentIndex,
currentItem,
handlePrevious,
handleNext,
handleDotClick,
} = useCarousel({
items,
onSelect: onSelect
? (item) => {
onSelect(item.id);
}
: undefined,
});

// ์•ˆ์ „์žฅ์น˜: items๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฉด ๋ Œ๋”๋ง X
if (!items || items.length === 0) return null;

const handlePrevious = () => {
setCurrentIndex((prev) => {
const newIndex = prev === 0 ? items.length - 1 : prev - 1;
onSelect?.(items[newIndex].id);
return newIndex;
});
};

const handleNext = () => {
setCurrentIndex((prev) => {
const newIndex = prev === items.length - 1 ? 0 : prev + 1;
onSelect?.(items[newIndex].id);
return newIndex;
});
};

const handleDotClick = (index: number) => {
setCurrentIndex(index);
onSelect?.(items[index].id);
};

return (
<div className="w-full flex flex-col flex-1 min-h-0">
{/* ์บ๋Ÿฌ์…€ ์˜์—ญ(๋‚จ๋Š” ์„ธ๋กœ ๊ณต๊ฐ„์„ ์นด๋“œ๊ฐ€ ๋จน๋Š” ์˜์—ญ) */}
Expand Down
34 changes: 3 additions & 31 deletions app/(default)/components/RandomPracticeCTA.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,17 @@
"use client";

import { useRouter } from "next/navigation";
import { IconWrapper } from "@/components/icons/IconWrapper";
import {
detailSituations,
type SituationId,
} from "../constants/detailSituations";
import { useRandomPractice } from "../_hooks/useRandomPractice";
import { PhoneIcon } from "./PhoneIcon";

export function RandomPracticeCTA() {
const router = useRouter();

const handleClick = () => {
// ๋ชจ๋“  ์ƒ์„ธ์ƒํ™ฉ์„ ํ‰ํƒ„ํ™”ํ•˜์—ฌ ๋ฐฐ์—ด๋กœ ๋งŒ๋“ค๊ธฐ
const allDetailSituations: Array<{
situationId: SituationId;
detailId: string;
}> = [];

(Object.keys(detailSituations) as SituationId[]).forEach((situationId) => {
detailSituations[situationId].forEach((detail) => {
allDetailSituations.push({
situationId,
detailId: detail.id,
});
});
});

// ๋žœ๋ค์œผ๋กœ ํ•˜๋‚˜ ์„ ํƒ
const randomIndex = Math.floor(Math.random() * allDetailSituations.length);
const selected = allDetailSituations[randomIndex];

// ๋‹ค์ด์–ผ ํŽ˜์ด์ง€๋กœ ์ด๋™
router.push(`/practice/${selected.situationId}/${selected.detailId}/dial`);
};
const { navigateToRandomPractice } = useRandomPractice();

return (
<div className="pt-6 px-5 flex-11 flex items-center justify-center">
<button
type="button"
onClick={handleClick}
onClick={navigateToRandomPractice}
className="bg-white w-full rounded-2xl flex gap-3 p-4 items-center max-w-[280px]"
>
<div className="bg-primary-400 rounded-full w-8 h-8 flex justify-center items-center shrink-0">
Expand Down
2 changes: 1 addition & 1 deletion app/(default)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { EmergencySituationCarousel } from "./components/EmergencySituationCarou
import { PracticeSectionHeader } from "./components/PracticeSectionHeader";
import { RandomPracticeCTA } from "./components/RandomPracticeCTA";
import { StartPracticeButton } from "./components/StartPracticeButton";
import { practiceItems } from "./constants/practiceItems";
import { practiceItems } from "./practice/_constants/practiceItems";

export default function HomePage() {
const [selectedSituationId, setSelectedSituationId] = useState<string>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { Button, Modal } from "@team-numberone/daepiro-design-system";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { LottieAnimation } from "../../../../components/LottieAnimation";
import { LottieAnimation } from "./LottieAnimation";

interface PracticeOptionButtonProps {
children: React.ReactNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { notFound } from "next/navigation";
import {
detailSituations,
type SituationId,
} from "../../../../constants/detailSituations";
import { practiceQuestions } from "../../../../constants/practiceQuestions";
import { getSituationImagePath } from "../../../../utils/situationImage";
import { PracticeOptionButton } from "../components/PracticeOptionButton";
} from "../../../_constants/detailSituations";
import { practiceQuestions } from "../../../_constants/practiceQuestions";
import { getSituationImagePath } from "../../../_utils/situationImage";
import { PracticeOptionButton } from "./components/PracticeOptionButton";

interface PracticeQuestionPageProps {
params: Promise<{
Expand Down Expand Up @@ -47,7 +47,7 @@ export default async function PracticeQuestionPage({
}

// ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ (์ƒํ™ฉ๋ณ„๋กœ ๋™์ผํ•œ ์ด๋ฏธ์ง€ ์‚ฌ์šฉ)
const imagePath = getSituationImagePath(situationId, pageIndex);
const imagePath = getSituationImagePath(situationId, detailId, pageIndex);

return (
<div className="h-full overflow-hidden flex flex-col gap-[28px]">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import type { PracticeQuestion } from "../../../../constants/practiceQuestions";
import type { PracticeQuestion } from "../../../_constants/practiceQuestions";

interface PracticeQuestionContentProps {
question: PracticeQuestion;
Expand All @@ -21,7 +21,6 @@ export function PracticeQuestionContent({
<div className="w-full bg-white rounded-[20px] p-6 flex flex-col gap-4">
{/* ์งˆ๋ฌธ */}
<div className="text-center">
<h2 className="text-h5 text-gray-700 mb-3">์–ด๋–ค ์ƒํ™ฉ์ด์•ผ?</h2>
<p className="text-body-1 text-gray-600">{question.situation}</p>
</div>

Expand Down
Loading