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
68 changes: 29 additions & 39 deletions app/result/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,22 @@ export default function Page() {
return getMeetingUserId(id) || '';
});

// 로컬스토리지에서 가져온 카테고리 값을 저장할 State
const [localCategory, setLocalCategory] = useState<string>('');

const { data: midpointData, isLoading, isError } = useMidpoint(id);
const { data: meetingData } = useCheckMeeting(id);

// 컴포넌트 마운트 시 로컬스토리지 값 가져오기 (Hydration 에러 방지)
useEffect(() => {
if (typeof window !== 'undefined' && id) {
const storedCategory = localStorage.getItem(`meeting_${id}_category`);
if (storedCategory) {
setLocalCategory(storedCategory);
}
}
}, [id]);
Comment on lines +27 to +41
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

CI 파이프라인 실패 원인 수정 필요 — useEffect 내부의 setState 직접 호출

react-hooks/set-state-in-effect ESLint 오류로 인해 CI가 실패하고 있습니다. 같은 파일의 myNickname (Line 22–25)이 이미 useState lazy initializer + typeof window 가드 패턴으로 동일한 목적을 달성하고 있으므로, localCategory도 동일하게 리팩토링하고 useEffect를 제거하면 됩니다.

🐛 수정 제안
-  // 로컬스토리지에서 가져온 카테고리 값을 저장할 State
-  const [localCategory, setLocalCategory] = useState<string>('');
-
   ...
-
-  // 컴포넌트 마운트 시 로컬스토리지 값 가져오기 (Hydration 에러 방지)
-  useEffect(() => {
-    if (typeof window !== 'undefined' && id) {
-      const storedCategory = localStorage.getItem(`meeting_${id}_category`);
-      if (storedCategory) {
-        setLocalCategory(storedCategory);
-      }
-    }
-  }, [id]);
+  // 로컬스토리지에서 가져온 카테고리 값을 저장할 State (myNickname과 동일한 lazy initializer 패턴)
+  const [localCategory] = useState<string>(() => {
+    if (typeof window === 'undefined') return '';
+    return localStorage.getItem(`meeting_${id}_category`) || '';
+  });
🧰 Tools
🪛 ESLint

[error] 38-38: Error: Calling setState synchronously within an effect can trigger cascading renders

Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:

  • Update external systems with the latest state from React.
  • Subscribe for updates from some external system, calling setState in a callback function when external state changes.

Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).

/home/jailuser/git/app/result/[id]/page.tsx:38:9
36 | const storedCategory = localStorage.getItem(meeting_${id}_category);
37 | if (storedCategory) {

38 | setLocalCategory(storedCategory);
| ^^^^^^^^^^^^^^^^ Avoid calling setState() directly within an effect
39 | }
40 | }
41 | }, [id]);

(react-hooks/set-state-in-effect)

🪛 GitHub Actions: CI

[error] 38-38: React: Calling setState synchronously within an effect. Avoid calling setState() directly within an effect (react-hooks/set-state-in-effect).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/result/`[id]/page.tsx around lines 27 - 41, Replace the useEffect +
setLocalCategory pattern by initializing localCategory via useState's lazy
initializer that reads localStorage only on the client: remove the useEffect
block that references useEffect, setLocalCategory and id, and change the
useState for localCategory to a lazy initializer that checks typeof window !==
'undefined' and reads localStorage.getItem(`meeting_${id}_category`) (falling
back to ''), mirroring the myNickname pattern to avoid setState-in-effect ESLint
errors.


const locationResults = useMemo(() => {
if (!midpointData?.success || !midpointData.data || !Array.isArray(midpointData.data)) {
return [];
Expand Down Expand Up @@ -112,47 +125,25 @@ export default function Page() {
});
}, [midpointData, myNickname, id]);

// 카테고리 텍스트 생성 함수
// 카테고리 텍스트 생성 함수 업데이트
const getCategoryText = (
category: string | undefined,
hot: boolean | undefined,
rank: number,
apiCategory: string | undefined,
hot: boolean | undefined, // hot 파라미터를 안 쓰게 되더라도 시그니처 유지를 위해 둠
rank: number
): string => {
const purposeText = '[모임 목적]';
const lastChar = purposeText.charCodeAt(purposeText.length - 1);
const hasJongseong = (lastChar - 0xac00) % 28 !== 0;
const purposeTextWithPostfix = `${purposeText}${hasJongseong ? '이' : '가'} 많은 장소`;


if (hot === true && rank === 1) {
return `밍글링 추천 1위 · ${purposeTextWithPostfix}`;
}


else if (hot === true && rank === 2) {
return `밍글링 추천 2위 · ${purposeTextWithPostfix}`;
}


else if (hot === true) {
return purposeTextWithPostfix;
}


else if (rank === 1) {
// 밍글링 추천 문구가 들어가는 1위, 2위는 "OO이 많은 장소" 텍스트를 완전히 생략합니다.
if (rank === 1) {
return '밍글링 추천 1위';
}

else if (rank === 2) {
} else if (rank === 2) {
return '밍글링 추천 2위';
}

else if (!category) return '밍글링 추천 1위';
// 3위 등 밍글링 추천이 안 붙는 경우에만 로컬스토리지 값 활용하여 "OO이/가 많은 장소" 표시
const purposeText = localCategory || apiCategory || '모임 목적';
const lastChar = purposeText.charCodeAt(purposeText.length - 1);
const hasJongseong = (lastChar - 0xac00) % 28 !== 0;

// 카테고리 종성에 따라 "이/가"를 다르게 렌더링
const categoryLastChar = category.charCodeAt(category.length - 1);
const categoryHasJongseong = (categoryLastChar - 0xac00) % 28 !== 0;
return `${category}${categoryHasJongseong ? '이' : '가'} 많은 장소`;
return `${purposeText}${hasJongseong ? '이' : '가'} 많은 장소`;
};

const [selectedResultId, setSelectedResultId] = useState<number>(1);
Expand Down Expand Up @@ -196,7 +187,6 @@ export default function Page() {
<div className="text-gray-9 text-[22px] font-semibold tracking-[-1.94%]">
최종 위치 결과 Top3
</div>

</div>
</div>

Expand Down Expand Up @@ -234,7 +224,7 @@ export default function Page() {
locationResults.map((result) => {
const category =
meetingData?.data?.purposes?.[meetingData.data.purposes.length - 1];
const categoryText = getCategoryText(category, result.hot, result.id);
const categoryText = getCategoryText(category, result.hot, result.id);

const handleRecommendClick = (e: React.MouseEvent) => {
e.stopPropagation();
Expand Down Expand Up @@ -336,12 +326,12 @@ export default function Page() {
)}
</div>
</div>

<button
onClick={(e) => openModal('SHARE', { meetingId: id }, e)}
className="flex items-center justify-center gap-2.5 bg-blue-5 hover:bg-blue-8 absolute right-5 bottom-0 left-5 h-12 rounded text-lg font-semibold text-white transition-transform active:scale-[0.98] md:right-0 md:left-0"
className="bg-blue-5 hover:bg-blue-8 absolute right-5 bottom-0 left-5 flex h-12 items-center justify-center gap-2.5 rounded text-lg font-semibold text-white transition-transform active:scale-[0.98] md:right-0 md:left-0"
>
<Image src="/icon/share-white.svg" alt="공유 아이콘" width={20} height={20} />
<Image src="/icon/share-white.svg" alt="공유 아이콘" width={20} height={20} />
결과 공유하기
</button>
</div>
Expand Down
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const eslintConfig = defineConfig([
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'react-hooks/set-state-in-effect': 'off',
},
},
]);
Expand Down
Loading