From 62d7a9d409db8f31d8924664a5bea0312f79c299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=A3=BC?= Date: Sun, 21 Jun 2026 23:36:14 +0900 Subject: [PATCH 1/9] =?UTF-8?q?build:=20React=20Compiler=20=EC=84=A4?= =?UTF-8?q?=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pnp.cjs | 37 ++++++++++++++++++++++++++++++++++++- next.config.mjs | 1 + package.json | 1 + yarn.lock | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/.pnp.cjs b/.pnp.cjs index 420db4d5d..703a4fcc7 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -49,6 +49,7 @@ const RAW_RUNTIME_STATE = ["@types/react-dom", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:19.2.3"],\ ["@types/react-window", "npm:1.8.8"],\ ["axios", "npm:0.31.1"],\ + ["babel-plugin-react-compiler", "npm:1.0.0"],\ ["dayjs", "npm:1.11.12"],\ ["dotenv", "npm:17.2.3"],\ ["embla-carousel-autoplay", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:8.0.4"],\ @@ -838,6 +839,13 @@ const RAW_RUNTIME_STATE = ["@babel/helper-string-parser", "npm:7.27.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.29.7", {\ + "packageLocation": "./.yarn/cache/@babel-helper-string-parser-npm-7.29.7-87998d618e-4d8ef0ef71.zip/node_modules/@babel/helper-string-parser/",\ + "packageDependencies": [\ + ["@babel/helper-string-parser", "npm:7.29.7"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@babel/helper-validator-identifier", [\ @@ -875,6 +883,13 @@ const RAW_RUNTIME_STATE = ["@babel/helper-validator-identifier", "npm:7.28.5"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.29.7", {\ + "packageLocation": "./.yarn/cache/@babel-helper-validator-identifier-npm-7.29.7-9939aac13d-2efa42701e.zip/node_modules/@babel/helper-validator-identifier/",\ + "packageDependencies": [\ + ["@babel/helper-validator-identifier", "npm:7.29.7"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@babel/helper-validator-option", [\ @@ -3629,6 +3644,15 @@ const RAW_RUNTIME_STATE = ["@babel/types", "npm:7.29.0"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.29.7", {\ + "packageLocation": "./.yarn/cache/@babel-types-npm-7.29.7-8e5b8d613f-bd4f5635db.zip/node_modules/@babel/types/",\ + "packageDependencies": [\ + ["@babel/helper-string-parser", "npm:7.29.7"],\ + ["@babel/helper-validator-identifier", "npm:7.29.7"],\ + ["@babel/types", "npm:7.29.7"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@bcoe/v8-coverage", [\ @@ -8647,6 +8671,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["babel-plugin-react-compiler", [\ + ["npm:1.0.0", {\ + "packageLocation": "./.yarn/cache/babel-plugin-react-compiler-npm-1.0.0-5beba4221c-51358f4da7.zip/node_modules/babel-plugin-react-compiler/",\ + "packageDependencies": [\ + ["@babel/types", "npm:7.29.7"],\ + ["babel-plugin-react-compiler", "npm:1.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["babel-preset-current-node-syntax", [\ ["npm:1.1.0", {\ "packageLocation": "./.yarn/cache/babel-preset-current-node-syntax-npm-1.1.0-a3b84fe89f-46331111ae.zip/node_modules/babel-preset-current-node-syntax/",\ @@ -13493,6 +13527,7 @@ const RAW_RUNTIME_STATE = ["@types/react-dom", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:19.2.3"],\ ["@types/react-window", "npm:1.8.8"],\ ["axios", "npm:0.31.1"],\ + ["babel-plugin-react-compiler", "npm:1.0.0"],\ ["dayjs", "npm:1.11.12"],\ ["dotenv", "npm:17.2.3"],\ ["embla-carousel-autoplay", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:8.0.4"],\ @@ -14237,7 +14272,7 @@ const RAW_RUNTIME_STATE = ["@types/react", "npm:19.2.14"],\ ["@types/react-dom", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:19.2.3"],\ ["@types/sass", null],\ - ["babel-plugin-react-compiler", null],\ + ["babel-plugin-react-compiler", "npm:1.0.0"],\ ["caniuse-lite", "npm:1.0.30001734"],\ ["next", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:15.5.18"],\ ["postcss", "npm:8.4.31"],\ diff --git a/next.config.mjs b/next.config.mjs index c7fb6736d..4bd7b6edd 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -32,6 +32,7 @@ const nextConfig = { return config; }, experimental: { + reactCompiler: process.env.REACT_COMPILER === 'true', workerThreads: false, }, images: { diff --git a/package.json b/package.json index 5eb593dae..e0d64feab 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/react-window": "^1.8.5", + "babel-plugin-react-compiler": "1.0.0", "dotenv": "^17.2.3", "eslint": "^9.38.0", "eslint-config-prettier": "^10.1.8", diff --git a/yarn.lock b/yarn.lock index ef3196a8c..0b54c220a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -615,6 +615,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-string-parser@npm:7.29.7" + checksum: 10/4d8ef0ef7105f3d9fe4361137c8f42e5b4c7a52b5380b962762f2a528a1ba89064e2c6236090716ce34b63707b886ae0ebf36b2c2fcc2851f27e652febfc3648 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-validator-identifier@npm:7.22.20" @@ -650,6 +657,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-validator-identifier@npm:7.29.7" + checksum: 10/2efa42701eb05babf26dff3332109c9e5e1a3400a71fb9e68ee27af28235036a2a72c2494c04bdab3f909075f42a58b2e8271074372bc7f8e79ec02bd364d7a7 + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.24.8": version: 7.24.8 resolution: "@babel/helper-validator-option@npm:7.24.8" @@ -2117,6 +2131,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.26.0": + version: 7.29.7 + resolution: "@babel/types@npm:7.29.7" + dependencies: + "@babel/helper-string-parser": "npm:^7.29.7" + "@babel/helper-validator-identifier": "npm:^7.29.7" + checksum: 10/bd4f5635db1057bd0abeebf93eb3ae38399e152271cea8dce8288350f0afa13ed3e2db2e16e22bd3303068890eec18965a83420539afbe0dde31432b4cf9636d + languageName: node + linkType: hard + "@babel/types@npm:^7.27.1": version: 7.27.1 resolution: "@babel/types@npm:7.27.1" @@ -5857,6 +5881,15 @@ __metadata: languageName: node linkType: hard +"babel-plugin-react-compiler@npm:1.0.0": + version: 1.0.0 + resolution: "babel-plugin-react-compiler@npm:1.0.0" + dependencies: + "@babel/types": "npm:^7.26.0" + checksum: 10/51358f4da7dd338afd93ca647af3b5b95fdedd2ffde78719fbba5c542e3d66d338f020bc61234caf4ddf25cddbf517ae101ff76e362e3244d1047c1e64d90a60 + languageName: node + linkType: hard + "babel-preset-current-node-syntax@npm:^1.0.0": version: 1.1.0 resolution: "babel-preset-current-node-syntax@npm:1.1.0" @@ -10048,6 +10081,7 @@ __metadata: "@types/react-dom": "npm:^19.2.3" "@types/react-window": "npm:^1.8.5" axios: "npm:^0.31.1" + babel-plugin-react-compiler: "npm:1.0.0" dayjs: "npm:^1.11.12" dotenv: "npm:^17.2.3" embla-carousel-autoplay: "npm:^8.0.4" From c4abb6c52e3d0563e95fcec772fa283748769163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=A3=BC?= Date: Sun, 21 Jun 2026 23:39:55 +0900 Subject: [PATCH 2/9] =?UTF-8?q?refactor:=20App=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8,=20=EB=B6=84=EC=8B=A4=EB=AC=BC,=20=EA=B3=B5?= =?UTF-8?q?=EC=A7=80=EC=82=AC=ED=95=AD,=20usePagination=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20useMemo=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Articles/hooks/usePagination.ts | 15 +++++++-------- src/pages/_app.tsx | 9 +++++---- src/pages/articles/index.tsx | 10 ++++------ src/pages/lost-item/index.tsx | 7 ++----- 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/components/Articles/hooks/usePagination.ts b/src/components/Articles/hooks/usePagination.ts index f6ecc3f39..b95edae24 100644 --- a/src/components/Articles/hooks/usePagination.ts +++ b/src/components/Articles/hooks/usePagination.ts @@ -1,4 +1,3 @@ -import { useMemo } from 'react'; import useParamsHandler from 'utils/hooks/routing/useParamsHandler'; const PAGE_LIMIT = 5; @@ -9,14 +8,14 @@ const usePagination = (totalPageNum: number) => { const raw = Number(params.page) || 1; const currentPage = Math.min(Math.max(raw, 1), Math.max(totalPageNum, 1)); - const pages = useMemo(() => { - const half = Math.floor(PAGE_LIMIT / 2); - const maxStart = Math.max(1, totalPageNum - PAGE_LIMIT + 1); - const startPage = Math.min(Math.max(1, currentPage - half), maxStart); - const length = Math.min(PAGE_LIMIT, totalPageNum); - return Array.from({ length }, (_, i) => startPage + i); - }, [currentPage, totalPageNum]); + const half = Math.floor(PAGE_LIMIT / 2); + const maxStart = Math.max(1, totalPageNum - PAGE_LIMIT + 1); + const startPage = Math.min(Math.max(1, currentPage - half), maxStart); + const length = Math.min(PAGE_LIMIT, totalPageNum); + + const pages = Array.from({ length }, (_, i) => startPage + i); + const movePage = (target: number | 'prev' | 'next') => { let nextPage = currentPage; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 4994605d5..d76590678 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -79,10 +79,11 @@ export default function App({ Component, pageProps }: AppPropsWithAuth) { const getLayout = Component.getLayout || ((page) => {page}); - const pageTitle = React.useMemo(() => { - if (!Component.title) return undefined; - return typeof Component.title === 'function' ? Component.title(router.asPath) : Component.title; - }, [Component, router.asPath]); + const pageTitle = !Component.title + ? undefined + : typeof Component.title === 'function' + ? Component.title(router.asPath) + : Component.title; // ios 브릿지 useEffect(() => { diff --git a/src/pages/articles/index.tsx b/src/pages/articles/index.tsx index 8f03de4e6..4d2e8cde6 100644 --- a/src/pages/articles/index.tsx +++ b/src/pages/articles/index.tsx @@ -1,4 +1,3 @@ -import { useMemo } from 'react'; import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import { useRouter } from 'next/router'; import { dehydrate, keepPreviousData, QueryClient, useQuery } from '@tanstack/react-query'; @@ -43,11 +42,10 @@ function usePageParams(initialPage: string) { const router = useRouter(); const mounted = useMount(); - return useMemo(() => { - if (!mounted) return initialPage; // SSR 초기값 - const page = router.query.page; - return typeof page === 'string' ? page : initialPage; - }, [mounted, router.query.page, initialPage]); + if (!mounted) return initialPage; // SSR 초기값 + const page = router.query.page; + return typeof page === 'string' ? page : initialPage; + } export default function ArticleListPage({ initialPage }: InferGetServerSidePropsType) { diff --git a/src/pages/lost-item/index.tsx b/src/pages/lost-item/index.tsx index f004f8089..7ed6c1562 100644 --- a/src/pages/lost-item/index.tsx +++ b/src/pages/lost-item/index.tsx @@ -1,4 +1,3 @@ -import { useMemo } from 'react'; import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import { useRouter } from 'next/router'; import { dehydrate, keepPreviousData, QueryClient, useQuery } from '@tanstack/react-query'; @@ -51,10 +50,8 @@ function useLostItemParams(initialParams: LostItemParams) { const router = useRouter(); const mounted = useMount(); - return useMemo(() => { - if (!mounted) return initialParams; - return parseLostItemQuery(router.query, initialParams); - }, [mounted, router.query, initialParams]); + if (!mounted) return initialParams; + return parseLostItemQuery(router.query, initialParams); } function toLostItemArticlesRequest(params: LostItemParams): LostItemArticlesRequest { From fb74de9fc3cff1dbdf14b65e9394f0c752675f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=A3=BC?= Date: Mon, 22 Jun 2026 15:00:16 +0900 Subject: [PATCH 3/9] =?UTF-8?q?refactor:=20=EC=A0=9C=EA=B1=B0=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=20useMemo=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Calendar/index.tsx | 16 ++++------ .../MobileDatePicker/MobileDatePicker.tsx | 8 ++--- .../components/CallvanChatRoom/index.tsx | 18 +++++------ .../components/TimePicker/index.tsx | 14 +++++---- .../Course/hooks/useSelectedCourses.ts | 12 +++---- .../IndexComponents/IndexLostItem/index.tsx | 30 ++++++++---------- .../UserInfoModal/hooks/useUserInfoModal.ts | 23 +++++--------- src/pages/bus/city/index.tsx | 31 +++++++++---------- src/pages/callvan/index.tsx | 11 +++---- 9 files changed, 71 insertions(+), 92 deletions(-) diff --git a/src/components/Articles/LostItemWritePage/components/Calendar/index.tsx b/src/components/Articles/LostItemWritePage/components/Calendar/index.tsx index 8fca4fac0..02758eeeb 100644 --- a/src/components/Articles/LostItemWritePage/components/Calendar/index.tsx +++ b/src/components/Articles/LostItemWritePage/components/Calendar/index.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import { cn } from '@bcsdlab/utils'; import ChevronLeftIcon from 'assets/svg/Articles/chevron-left.svg'; import ChevronRightIcon from 'assets/svg/Articles/chevron-right.svg'; @@ -25,15 +25,11 @@ interface CalendarProps { export default function Calendar({ selectedDate, setSelectedDate }: CalendarProps) { const today = new Date(); const [currentMonthDate, setCurrentMonthDate] = useState(selectedDate); - const days = useMemo( - () => - Array.from({ length: 35 }, (_, i) => { - const startDate = new Date(currentMonthDate.getFullYear(), currentMonthDate.getMonth(), 1); - startDate.setDate(startDate.getDate() - startDate.getDay()); - return new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate() + i); - }), - [currentMonthDate], - ); + const days = Array.from({ length: 35 }, (_, i) => { + const startDate = new Date(currentMonthDate.getFullYear(), currentMonthDate.getMonth(), 1); + startDate.setDate(startDate.getDate() - startDate.getDay()); + return new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate() + i); + }); const handleMonthChevronClick = (diff: number) => { setCurrentMonthDate((prev) => new Date(prev.getFullYear(), prev.getMonth() + diff, 1)); diff --git a/src/components/Articles/LostItemWritePage/components/MobileDatePicker/MobileDatePicker.tsx b/src/components/Articles/LostItemWritePage/components/MobileDatePicker/MobileDatePicker.tsx index 6d96c44eb..d4e7da452 100644 --- a/src/components/Articles/LostItemWritePage/components/MobileDatePicker/MobileDatePicker.tsx +++ b/src/components/Articles/LostItemWritePage/components/MobileDatePicker/MobileDatePicker.tsx @@ -18,10 +18,10 @@ export default function MobileDatePicker({ selectedDate, setSelectedDate }: Mobi const today = useMemo(() => new Date(), []); const [pendingDate, setPendingDate] = useState(selectedDate); - const { years, months, days, currentYear, currentMonth, currentDay } = useMemo( - () => getDateRange({ selectedDate: pendingDate, maxDate: today }), - [pendingDate, today], - ); + const { years, months, days, currentYear, currentMonth, currentDay } = getDateRange({ + selectedDate: pendingDate, + maxDate: today, + }); const { syncScrollPosition, createScrollHandler } = useScrollPicker({ itemHeight: ITEM_HEIGHT }); diff --git a/src/components/Callvan/components/CallvanChatRoom/index.tsx b/src/components/Callvan/components/CallvanChatRoom/index.tsx index 59c35a077..77a4cb898 100644 --- a/src/components/Callvan/components/CallvanChatRoom/index.tsx +++ b/src/components/Callvan/components/CallvanChatRoom/index.tsx @@ -1,4 +1,4 @@ -import { useRef, useState, useEffect, useMemo } from 'react'; +import { useRef, useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import { useSuspenseQuery } from '@tanstack/react-query'; import { CallvanChatMessage } from 'api/callvan/entity'; @@ -112,15 +112,13 @@ export default function CallvanChatRoom({ postId }: CallvanChatRoomProps) { } }; - const senderColorMap = useMemo(() => { - const map = new Map(); - data.messages.forEach((msg) => { - if (!msg.is_mine && !map.has(msg.user_id)) { - map.set(msg.user_id, map.size); - } - }); - return map; - }, [data.messages]); + const senderColorMap = new Map(); + + data.messages.forEach((message) => { + if (!message.is_mine && !senderColorMap.has(message.user_id)) { + senderColorMap.set(message.user_id, senderColorMap.size); + } + }); const messageGroups = groupMessagesByDate(data.messages); diff --git a/src/components/Club/NewClubEvent/components/TimePicker/index.tsx b/src/components/Club/NewClubEvent/components/TimePicker/index.tsx index c28c9ad24..88c74f0bc 100644 --- a/src/components/Club/NewClubEvent/components/TimePicker/index.tsx +++ b/src/components/Club/NewClubEvent/components/TimePicker/index.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useMemo } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { cn } from '@bcsdlab/utils'; import { useEscapeKeyDown } from 'utils/hooks/ui/useEscapeKeyDown'; import { useOutsideClick } from 'utils/hooks/ui/useOutsideClick'; @@ -12,6 +12,11 @@ interface TimePickerProps { } const ITEM_HEIGHT = 36; +const BASE_HOURS = Array.from({ length: 24 }, (_, i) => i); +const BASE_MINUTE = Array.from({ length: 60 }, (_, i) => i).filter((m) => m % 5 === 0); + +const HOURS = [101, 102, ...BASE_HOURS, 103, 104]; +const MINUTES = [101, 102, ...BASE_MINUTE, 103, 104]; export default function TimePicker({ onChange, onClose, hour, minute }: TimePickerProps) { const [tempHour, setTempHour] = useState(hour); @@ -19,13 +24,10 @@ export default function TimePicker({ onChange, onClose, hour, minute }: TimePick const { backgroundRef } = useOutsideClick({ onOutsideClick: onClose }); useEscapeKeyDown({ onEscape: onClose }); - - const baseHours = useMemo(() => Array.from({ length: 24 }, (_, i) => i), []); - const baseMinutes = useMemo(() => Array.from({ length: 60 }, (_, i) => i).filter((m) => m % 5 === 0), []); + const hours = HOURS; + const minutes = MINUTES; // 더미 데이터 추가 (101~104) - const hours = useMemo(() => [101, 102, ...baseHours, 103, 104], [baseHours]); - const minutes = useMemo(() => [101, 102, ...baseMinutes, 103, 104], [baseMinutes]); const hourRef = useRef(null); const minuteRef = useRef(null); diff --git a/src/components/Course/hooks/useSelectedCourses.ts b/src/components/Course/hooks/useSelectedCourses.ts index a8944df57..e1cee4a74 100644 --- a/src/components/Course/hooks/useSelectedCourses.ts +++ b/src/components/Course/hooks/useSelectedCourses.ts @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import { PreCourse } from 'api/course/entity'; import { isomorphicSessionStorage } from 'utils/ts/env'; @@ -22,12 +22,10 @@ export const getCourseKey = (course: PreCourse) => { export default function useSelectedCourses() { const [selectedCourses, setSelectedCourses] = useState(getStoredCourses); - const selectedCredits = useMemo(() => { - return selectedCourses.reduce((total, course) => { - const credit = Number(course.grades.split('-')[0]); - return Number.isNaN(credit) ? total : total + credit; - }, 0); - }, [selectedCourses]); + const selectedCredits = selectedCourses.reduce((total, course) => { + const credit = Number(course.grades.split('-')[0]); + return Number.isNaN(credit) ? total : total + credit; + }, 0); const hasTimeConflict = (candidate: PreCourse) => selectedCourses.some((course) => course.class_time_raw.some((time) => candidate.class_time_raw.includes(time))); diff --git a/src/components/IndexComponents/IndexLostItem/index.tsx b/src/components/IndexComponents/IndexLostItem/index.tsx index d711158c1..5b3ac218a 100644 --- a/src/components/IndexComponents/IndexLostItem/index.tsx +++ b/src/components/IndexComponents/IndexLostItem/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import Link from 'next/link'; import { useSuspenseQuery } from '@tanstack/react-query'; import { articleQueries } from 'api/articles/queries'; @@ -15,25 +15,21 @@ function IndexLostItem() { const { data: lostItemStat } = useSuspenseQuery(articleQueries.lostItemStat()); const [currentIndex, setCurrentIndex] = useState(0); - const cardMessages = useMemo(() => { - const messages = []; + const cardMessages = []; - if (lostItemStat.found_count >= MIN_FOUND_COUNT) { - messages.push({ - prefix: '지금까지 ', - highlight: `${lostItemStat.found_count}명`, - suffix: '이 분실물을 찾았어요.', - }); - } - - messages.push({ - prefix: '', - highlight: `${lostItemStat.not_found_count}개`, - suffix: '의 분실물이 주인을 찾고 있어요.', + if (lostItemStat.found_count >= MIN_FOUND_COUNT) { + cardMessages.push({ + prefix: '지금까지 ', + highlight: `${lostItemStat.found_count}명`, + suffix: '이 분실물을 찾았어요.', }); + } - return messages; - }, [lostItemStat.found_count, lostItemStat.not_found_count]); + cardMessages.push({ + prefix: '', + highlight: `${lostItemStat.not_found_count}개`, + suffix: '의 분실물이 주인을 찾고 있어요.', + }); const shouldAnimate = cardMessages.length > 1; diff --git a/src/components/ui/UserInfoModal/hooks/useUserInfoModal.ts b/src/components/ui/UserInfoModal/hooks/useUserInfoModal.ts index c65c90fd1..1f737c261 100644 --- a/src/components/ui/UserInfoModal/hooks/useUserInfoModal.ts +++ b/src/components/ui/UserInfoModal/hooks/useUserInfoModal.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from 'react'; +import { useEffect } from 'react'; import { STORAGE_KEY, COMPLETION_STATUS } from 'static/auth'; import { useUser } from 'utils/hooks/state/useUser'; import { useLocalStorage, useSessionStorage } from 'utils/hooks/state/useWebStorage'; @@ -17,21 +17,12 @@ export default function useUserInfoModal() { const isStudent = isStudentUser(userInfo); - const isInfoMissing = useMemo(() => { - if (!isStudent) return false; - const requiredFields: (keyof typeof userInfo)[] = [ - 'login_id', - 'gender', - 'major', - 'name', - 'phone_number', - 'student_number', - ]; - return requiredFields.some((field) => { - const v = userInfo?.[field] as unknown; - return v === undefined || v === null || v === ''; - }); - }, [isStudent, userInfo]); + const isInfoMissing = isStudent + ? (['login_id', 'gender', 'major', 'name', 'phone_number', 'student_number'] as const).some((field) => { + const v = userInfo[field]; + return v === undefined || v === null || v === ''; + }) + : false; const canOpen = !!token && isStudent && completion !== COMPLETION_STATUS.COMPLETED && isInfoMissing && !sessionShown; diff --git a/src/pages/bus/city/index.tsx b/src/pages/bus/city/index.tsx index 02c74b4bc..22f171f14 100644 --- a/src/pages/bus/city/index.tsx +++ b/src/pages/bus/city/index.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import { cn } from '@bcsdlab/utils'; import { useSuspenseQuery } from '@tanstack/react-query'; import { CityInfo, DirectionType } from 'api/bus/entity'; @@ -17,6 +17,9 @@ type CityDirectionOption = { value: DirectionType; }; +type DayType = '평일' | '주말'; +type TimetableRow = [am: string, pm: string]; + const cityBusDirections: CityDirectionOption[] = [ { label: '천안방면', value: 'from' }, { label: '병천방면', value: 'to' }, @@ -67,23 +70,19 @@ export default function CityBusTimetable() { return CITY_COURSES_MAP.get(`${bus_number}-${direction_type}`)?.direction ?? ''; } - const processedTimetable = useMemo(() => { - if (!timetable?.info?.bus_timetables) return []; - - const today = dayjs().day(); - const dayType = today === 0 || today === 6 ? '주말' : '평일'; - const todayTimetable = timetable.info.bus_timetables.find((info) => info.day_of_week === dayType); - - if (!todayTimetable) return []; - - const departInfo = todayTimetable.depart_info; - const amTimes = departInfo.filter((time) => getHours(time) < 12); - const pmTimes = departInfo.filter((time) => getHours(time) >= 12); + const today = dayjs().day(); + const dayType:DayType = today === 0 || today === 6 ? '주말' : '평일'; + const todayTimetable = timetable.info.bus_timetables.find((item)=>item.day_of_week === dayType); + const departInfo = todayTimetable?.depart_info ?? []; - const maxLength = Math.max(amTimes.length, pmTimes.length); + const amTimes = departInfo.filter((time) => getHours(time) < 12); + const pmTimes = departInfo.filter((time) => getHours(time) >= 12); + const maxLength = Math.max(amTimes.length, pmTimes.length); - return Array.from({ length: maxLength }, (_, idx) => [amTimes[idx] || '', pmTimes[idx] || '']); - }, [timetable]); + const processedTimetable: TimetableRow[] = Array.from({ length: maxLength }, (_, index) => [ + amTimes[index] || '', + pmTimes[index] || '', + ]); return ( diff --git a/src/pages/callvan/index.tsx b/src/pages/callvan/index.tsx index 79a1530c1..22c4c6d16 100644 --- a/src/pages/callvan/index.tsx +++ b/src/pages/callvan/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import { useRouter } from 'next/router'; import { dehydrate, QueryClient, useInfiniteQuery } from '@tanstack/react-query'; @@ -68,10 +68,9 @@ function useCallvanParams(initialParams: CallvanParams): CallvanParams { const router = useRouter(); const mounted = useMount(); - return useMemo(() => { - if (!mounted) return initialParams; - return parseCallvanQuery(router.query, DEFAULT_PARAMS); - }, [mounted, router.query, initialParams]); + if (!mounted) return initialParams; + return parseCallvanQuery(router.query, DEFAULT_PARAMS); + } export default function CallvanPage({ initialParams }: InferGetServerSidePropsType) { @@ -108,7 +107,7 @@ function CallvanContent({ params }: CallvanContentProps) { enabled: !!token, }); - const posts = useMemo(() => data?.pages.flatMap((page) => page.posts) ?? [], [data]); + const posts = data?.pages.flatMap((page) => page.posts) ?? [] const scrollTriggerRef = useInfiniteScroll(fetchNextPage, hasNextPage, isFetchingNextPage); From 5b35343b4e8ba75f463591880eb58ac4586efc5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=A3=BC?= Date: Mon, 22 Jun 2026 15:02:21 +0900 Subject: [PATCH 4/9] =?UTF-8?q?build:=20react-compiler-eslint=20=EC=84=A4?= =?UTF-8?q?=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pnp.cjs | 24 ++++++++++++------------ package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.pnp.cjs b/.pnp.cjs index 703a4fcc7..953dd47ea 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -61,7 +61,7 @@ const RAW_RUNTIME_STATE = ["eslint-plugin-jsx-a11y", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:6.10.2"],\ ["eslint-plugin-prettier", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.5.4"],\ ["eslint-plugin-react", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:7.37.5"],\ - ["eslint-plugin-react-hooks", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:7.0.1"],\ + ["eslint-plugin-react-hooks", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:7.1.1"],\ ["file-saver", "npm:2.0.5"],\ ["framer-motion", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:12.4.4"],\ ["globals", "npm:16.4.0"],\ @@ -10787,24 +10787,24 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["eslint-plugin-react-hooks", [\ - ["npm:7.0.1", {\ - "packageLocation": "./.yarn/cache/eslint-plugin-react-hooks-npm-7.0.1-218b8cae26-12e96c68d5.zip/node_modules/eslint-plugin-react-hooks/",\ + ["npm:7.1.1", {\ + "packageLocation": "./.yarn/cache/eslint-plugin-react-hooks-npm-7.1.1-cf5b028313-9dfe543af9.zip/node_modules/eslint-plugin-react-hooks/",\ "packageDependencies": [\ - ["eslint-plugin-react-hooks", "npm:7.0.1"]\ + ["eslint-plugin-react-hooks", "npm:7.1.1"]\ ],\ "linkType": "SOFT"\ }],\ - ["virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:7.0.1", {\ - "packageLocation": "./.yarn/__virtual__/eslint-plugin-react-hooks-virtual-758179043b/0/cache/eslint-plugin-react-hooks-npm-7.0.1-218b8cae26-12e96c68d5.zip/node_modules/eslint-plugin-react-hooks/",\ + ["virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:7.1.1", {\ + "packageLocation": "./.yarn/__virtual__/eslint-plugin-react-hooks-virtual-a6f48f4c6d/0/cache/eslint-plugin-react-hooks-npm-7.1.1-cf5b028313-9dfe543af9.zip/node_modules/eslint-plugin-react-hooks/",\ "packageDependencies": [\ ["@babel/core", "npm:7.28.4"],\ ["@babel/parser", "npm:7.28.4"],\ ["@types/eslint", null],\ ["eslint", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:9.38.0"],\ - ["eslint-plugin-react-hooks", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:7.0.1"],\ + ["eslint-plugin-react-hooks", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:7.1.1"],\ ["hermes-parser", "npm:0.25.1"],\ ["zod", "npm:4.1.12"],\ - ["zod-validation-error", "virtual:758179043b9deddcfbb089b62330b887fa4298018695f4e8d592a92d1ed5bf19827ba10fa2c688eac687ac095d51931642ed1c1f0c93756afec64acd860d2ebb#npm:4.0.2"]\ + ["zod-validation-error", "virtual:a6f48f4c6d8e758bae63cda12dd9ccb8bdbf51f3bb0efdf6a777cdf782fdcd03f081a844906c59146e1410c30c0efdd89184dbcce6aa9478ac74aa65256fe746#npm:4.0.2"]\ ],\ "packagePeers": [\ "@types/eslint",\ @@ -13539,7 +13539,7 @@ const RAW_RUNTIME_STATE = ["eslint-plugin-jsx-a11y", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:6.10.2"],\ ["eslint-plugin-prettier", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:5.5.4"],\ ["eslint-plugin-react", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:7.37.5"],\ - ["eslint-plugin-react-hooks", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:7.0.1"],\ + ["eslint-plugin-react-hooks", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:7.1.1"],\ ["file-saver", "npm:2.0.5"],\ ["framer-motion", "virtual:921150aa31da2575af7c36f953e9f13b3419705f08359e02e507cdb46eef3a76096cce8027f1cca0709c04e91d009a713934e907c9c1efc1e28e5b528ec25863#npm:12.4.4"],\ ["globals", "npm:16.4.0"],\ @@ -18007,12 +18007,12 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:758179043b9deddcfbb089b62330b887fa4298018695f4e8d592a92d1ed5bf19827ba10fa2c688eac687ac095d51931642ed1c1f0c93756afec64acd860d2ebb#npm:4.0.2", {\ - "packageLocation": "./.yarn/__virtual__/zod-validation-error-virtual-5c0be35633/0/cache/zod-validation-error-npm-4.0.2-1b963160c8-5e35ca8ebb.zip/node_modules/zod-validation-error/",\ + ["virtual:a6f48f4c6d8e758bae63cda12dd9ccb8bdbf51f3bb0efdf6a777cdf782fdcd03f081a844906c59146e1410c30c0efdd89184dbcce6aa9478ac74aa65256fe746#npm:4.0.2", {\ + "packageLocation": "./.yarn/__virtual__/zod-validation-error-virtual-13e3d9764a/0/cache/zod-validation-error-npm-4.0.2-1b963160c8-5e35ca8ebb.zip/node_modules/zod-validation-error/",\ "packageDependencies": [\ ["@types/zod", null],\ ["zod", "npm:4.1.12"],\ - ["zod-validation-error", "virtual:758179043b9deddcfbb089b62330b887fa4298018695f4e8d592a92d1ed5bf19827ba10fa2c688eac687ac095d51931642ed1c1f0c93756afec64acd860d2ebb#npm:4.0.2"]\ + ["zod-validation-error", "virtual:a6f48f4c6d8e758bae63cda12dd9ccb8bdbf51f3bb0efdf6a777cdf782fdcd03f081a844906c59146e1410c30c0efdd89184dbcce6aa9478ac74aa65256fe746#npm:4.0.2"]\ ],\ "packagePeers": [\ "@types/zod",\ diff --git a/package.json b/package.json index e0d64feab..1daa7c761 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-hooks": "^7.1.1", "globals": "^16.4.0", "jest": "^29.7.0", "next-sitemap": "^4.2.3", diff --git a/yarn.lock b/yarn.lock index 0b54c220a..1971a67ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7569,9 +7569,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-react-hooks@npm:^7.0.1": - version: 7.0.1 - resolution: "eslint-plugin-react-hooks@npm:7.0.1" +"eslint-plugin-react-hooks@npm:^7.1.1": + version: 7.1.1 + resolution: "eslint-plugin-react-hooks@npm:7.1.1" dependencies: "@babel/core": "npm:^7.24.4" "@babel/parser": "npm:^7.24.4" @@ -7579,8 +7579,8 @@ __metadata: zod: "npm:^3.25.0 || ^4.0.0" zod-validation-error: "npm:^3.5.0 || ^4.0.0" peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - checksum: 10/12e96c68d58c6588305fd17d660524a1ef1e872650ec591d5b138f059431290831c373d4b1c9ae8991fb25f96c43935497d2149678c027e65d0417d3d99ecc85 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + checksum: 10/9dfe543af9813343f7cc7c5079b02da94a3b4c15df5cefdeef731f220120df26471d0db24c943222d2d9c6b43c3b78faea47ada9acc9dfd9c28492153cbc6902 languageName: node linkType: hard @@ -10093,7 +10093,7 @@ __metadata: eslint-plugin-jsx-a11y: "npm:^6.10.2" eslint-plugin-prettier: "npm:^5.5.4" eslint-plugin-react: "npm:^7.37.5" - eslint-plugin-react-hooks: "npm:^7.0.1" + eslint-plugin-react-hooks: "npm:^7.1.1" file-saver: "npm:^2.0.5" framer-motion: "npm:^12.4.4" globals: "npm:^16.4.0" From 8dff53725a4094364be98626dbc673314440d730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=A3=BC?= Date: Thu, 25 Jun 2026 14:51:57 +0900 Subject: [PATCH 5/9] =?UTF-8?q?refactor:=20useCallback=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=20=EB=B0=8F=20eslint=20=EA=B7=9C=EC=B9=99=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint.config.mjs | 3 +- .../Auth/SignupPage/Steps/Terms/index.tsx | 1 + .../Callvan/components/AddPostForm/index.tsx | 10 +++---- .../components/CallvanFilterPanel/index.tsx | 30 +++++-------------- .../components/CallvanPageLayout/index.tsx | 4 +-- .../Callvan/components/DateDropdown/index.tsx | 29 +++++++----------- .../Callvan/components/TimeDropdown/index.tsx | 10 +++---- .../components/ClubIntrodution/index.tsx | 9 ++---- .../components/ClubSearchContainer/index.tsx | 15 ++++------ .../components/SearchBarModal/index.tsx | 6 ++-- src/pages/callvan/notifications/index.tsx | 13 ++++---- src/pages/lost-item/chat/index.tsx | 10 +++---- 12 files changed, 55 insertions(+), 85 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 37cdbf905..c3258fbca 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -58,7 +58,8 @@ export default [ ...next.configs['core-web-vitals'].rules, ...react.configs.recommended.rules, ...react.configs['jsx-runtime'].rules, - ...reactHooks.configs['recommended-latest'].rules, + 'react-hooks/rules-of-hooks':'error', + 'react-hooks/exhaustive-deps':'warn', 'import/order': [ 'error', diff --git a/src/components/Auth/SignupPage/Steps/Terms/index.tsx b/src/components/Auth/SignupPage/Steps/Terms/index.tsx index 4c1fc69d1..4a17f5256 100644 --- a/src/components/Auth/SignupPage/Steps/Terms/index.tsx +++ b/src/components/Auth/SignupPage/Steps/Terms/index.tsx @@ -1,3 +1,4 @@ +'use no memo'; import { cn } from '@bcsdlab/utils'; import CustomCheckbox from 'components/Auth/SignupPage/components/CustomCheckbox'; import { useFormContext } from 'react-hook-form'; diff --git a/src/components/Callvan/components/AddPostForm/index.tsx b/src/components/Callvan/components/AddPostForm/index.tsx index d563b2be7..ed09db97f 100644 --- a/src/components/Callvan/components/AddPostForm/index.tsx +++ b/src/components/Callvan/components/AddPostForm/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useState } from 'react'; import { useRouter } from 'next/router'; import { cn } from '@bcsdlab/utils'; import { CALLVAN_POST_LOCATION_LABEL, CallvanPostLocationType } from 'api/callvan/entity'; @@ -70,7 +70,7 @@ export default function AddPostForm() { (form.departureType !== 'CUSTOM' || form.departureCustomName.trim() !== '') && (form.arrivalType !== 'CUSTOM' || form.arrivalCustomName.trim() !== ''); - const handleSwapLocation = useCallback(() => { + const handleSwapLocation = () => { setForm((prev) => ({ ...prev, departureType: prev.arrivalType, @@ -78,14 +78,14 @@ export default function AddPostForm() { arrivalType: prev.departureType, arrivalCustomName: prev.departureCustomName, })); - }, []); + } - const handleParticipantsChange = useCallback((delta: number) => { + const handleParticipantsChange = (delta: number) => { setForm((prev) => ({ ...prev, maxParticipants: Math.min(11, Math.max(2, prev.maxParticipants + delta)), })); - }, []); + } const handleSubmit = () => { if (!form.departureType || !form.arrivalType || isPending) return; diff --git a/src/components/Callvan/components/CallvanFilterPanel/index.tsx b/src/components/Callvan/components/CallvanFilterPanel/index.tsx index 11606e2c9..602dbe94b 100644 --- a/src/components/Callvan/components/CallvanFilterPanel/index.tsx +++ b/src/components/Callvan/components/CallvanFilterPanel/index.tsx @@ -1,11 +1,5 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { - CallvanAuthor, - CallvanLocation, - CallvanSort, - CallvanStatus, - CALLVAN_LOCATION_LABEL, -} from 'api/callvan/entity'; +import { useEffect, useRef, useState } from 'react'; +import { CallvanAuthor, CallvanLocation, CallvanSort, CallvanStatus, CALLVAN_LOCATION_LABEL } from 'api/callvan/entity'; import SpinIcon from 'assets/svg/Callvan/spin.svg'; import CloseIcon from 'assets/svg/close-icon-black.svg'; import StatusBadge from 'components/Callvan/components/StatusBadge'; @@ -81,12 +75,12 @@ export default function CallvanFilterPanel({ return () => document.removeEventListener('keydown', handleKeyDown); }, [isOpen, onClose]); - const toggleStatus = useCallback((value: CallvanStatus) => { + const toggleStatus = (value: CallvanStatus) => { setLocalStatuses((prev) => { if (prev.includes(value)) return prev.filter((s) => s !== value); return [...prev, value]; }); - }, []); + }; const toggleDeparture = (value: CallvanLocation) => { setLocalDepartures((prev) => { @@ -94,7 +88,7 @@ export default function CallvanFilterPanel({ const next = [...prev, value]; return next.length === CALLVAN_FILTER_LOCATIONS.length ? [] : next; }); - } + }; const toggleArrival = (value: CallvanLocation) => { setLocalArrivals((prev) => { @@ -102,7 +96,7 @@ export default function CallvanFilterPanel({ const next = [...prev, value]; return next.length === CALLVAN_FILTER_LOCATIONS.length ? [] : next; }); - } + }; const handleApply = () => { onApply({ @@ -191,11 +185,7 @@ export default function CallvanFilterPanel({

모집 상태

- setLocalStatuses([])} - /> + setLocalStatuses([])} /> {STATUS_OPTIONS.map((opt) => ( 기타 장소는 검색창을 이용해주세요.
- setLocalArrivals([])} - /> + setLocalArrivals([])} /> {CALLVAN_FILTER_LOCATIONS.map((loc) => ( { + const handleSearch = () => { router.replace({ pathname: router.pathname, query: { @@ -102,7 +102,7 @@ export default function CallvanPageLayout({ page: undefined, }, }); - }, [router, title]); + }; return (
diff --git a/src/components/Callvan/components/DateDropdown/index.tsx b/src/components/Callvan/components/DateDropdown/index.tsx index d96ab2bb9..4945ed743 100644 --- a/src/components/Callvan/components/DateDropdown/index.tsx +++ b/src/components/Callvan/components/DateDropdown/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { cn } from '@bcsdlab/utils'; import ChevronDownIcon from 'assets/svg/Callvan/chevron-down.svg'; import useBooleanState from 'utils/hooks/state/useBooleanState'; @@ -25,7 +25,7 @@ function ScrollColumn({ items, selectedIndex, onSelect }: ScrollColumnProps) { ref.current.scrollTo({ top: selectedIndex * ITEM_HEIGHT, behavior: 'smooth' }); }, [selectedIndex]); - const handleScroll = useCallback(() => { + const handleScroll = () => { if (!ref.current) return; isScrollingRef.current = true; @@ -38,7 +38,7 @@ function ScrollColumn({ items, selectedIndex, onSelect }: ScrollColumnProps) { ref.current.scrollTo({ top: clamped * ITEM_HEIGHT, behavior: 'smooth' }); isScrollingRef.current = false; }, 150); - }, [items.length, onSelect]); + }; const handleItemClick = (index: number) => { onSelect(index); @@ -46,11 +46,7 @@ function ScrollColumn({ items, selectedIndex, onSelect }: ScrollColumnProps) { }; return ( -
+
{items.map((item, i) => (
{isOpen && ( -
+
setTempYear(years[i])} /> - setTempMonth(i)} - /> - setTempDay(i + 1)} - /> + setTempMonth(i)} /> + setTempDay(i + 1)} />
diff --git a/src/components/Callvan/components/TimeDropdown/index.tsx b/src/components/Callvan/components/TimeDropdown/index.tsx index d19f7c697..7db499aa4 100644 --- a/src/components/Callvan/components/TimeDropdown/index.tsx +++ b/src/components/Callvan/components/TimeDropdown/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { cn } from '@bcsdlab/utils'; import useLogger from 'utils/hooks/analytics/useLogger'; import useBooleanState from 'utils/hooks/state/useBooleanState'; @@ -24,7 +24,7 @@ function ScrollColumn({ items, selectedIndex, onSelect }: ScrollColumnProps) { ref.current.scrollTo({ top: selectedIndex * ITEM_HEIGHT, behavior: 'smooth' }); }, [selectedIndex]); - const handleScroll = useCallback(() => { + const handleScroll = () => { if (!ref.current) return; isScrollingRef.current = true; if (scrollTimerRef.current) clearTimeout(scrollTimerRef.current); @@ -36,7 +36,7 @@ function ScrollColumn({ items, selectedIndex, onSelect }: ScrollColumnProps) { ref.current.scrollTop = clamped * ITEM_HEIGHT; isScrollingRef.current = false; }, 150); - }, [items.length, onSelect]); + }; return (
@@ -102,7 +102,7 @@ function InfiniteScrollColumn({ items, selectedIndex, onSelect }: InfiniteScroll ref.current.scrollTop = (middleOffset + selectedIndex) * ITEM_HEIGHT; }, [selectedIndex, middleOffset]); - const handleScroll = useCallback(() => { + const handleScroll = () => { if (!ref.current || isTeleportingRef.current) return; if (scrollTimerRef.current) clearTimeout(scrollTimerRef.current); @@ -131,7 +131,7 @@ function InfiniteScrollColumn({ items, selectedIndex, onSelect }: InfiniteScroll const actualIndex = ((virtualIndex % count) + count) % count; onSelect(actualIndex); }, 150); - }, [count, middleOffset, onSelect]); + }; return (
diff --git a/src/components/Club/ClubDetailPage/components/ClubIntrodution/index.tsx b/src/components/Club/ClubDetailPage/components/ClubIntrodution/index.tsx index b1d724955..da7dc4b91 100644 --- a/src/components/Club/ClubDetailPage/components/ClubIntrodution/index.tsx +++ b/src/components/Club/ClubDetailPage/components/ClubIntrodution/index.tsx @@ -75,12 +75,9 @@ export default function ClubIntroduction({ isEdit, introduction, setIntroduction }; }, [isEdit, imageHandler]); - const handleChange = useCallback( - (html: string) => { - if (isEdit) setIntroduction(html); - }, - [isEdit, setIntroduction], - ); + const handleChange = (html: string) => { + if (isEdit) setIntroduction(html); + }; return (
diff --git a/src/components/Club/ClubListPage/components/ClubSearchContainer/index.tsx b/src/components/Club/ClubListPage/components/ClubSearchContainer/index.tsx index 748c3e7a2..db5eaa14f 100644 --- a/src/components/Club/ClubListPage/components/ClubSearchContainer/index.tsx +++ b/src/components/Club/ClubListPage/components/ClubSearchContainer/index.tsx @@ -1,4 +1,4 @@ -import { type ChangeEvent, useCallback, useState } from 'react'; +import { type ChangeEvent, useState } from 'react'; import { useRouter } from 'next/router'; import { cn } from '@bcsdlab/utils'; import { searchClub } from 'api/club'; @@ -31,14 +31,11 @@ export default function ClubSearchContainer() { setRelateSearchItems(data); }, 300); - const handleInputChange = useCallback( - (e: ChangeEvent) => { - const inputValue = e.target.value.trim(); - setSearchValue(inputValue); - debouncedSearch(inputValue); - }, - [debouncedSearch], - ); + const handleInputChange = (e: ChangeEvent) => { + const inputValue = e.target.value.trim(); + setSearchValue(inputValue); + debouncedSearch(inputValue); + }; const handleSearch = () => { const value = searchValue.trim(); diff --git a/src/components/Store/StorePage/components/SearchBarModal/index.tsx b/src/components/Store/StorePage/components/SearchBarModal/index.tsx index 2044d3b79..a29ccf619 100644 --- a/src/components/Store/StorePage/components/SearchBarModal/index.tsx +++ b/src/components/Store/StorePage/components/SearchBarModal/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useRouter } from 'next/router'; import { useSuspenseQuery } from '@tanstack/react-query'; import { getRelateSearch } from 'api/store'; @@ -31,7 +31,7 @@ export default function SearchBarModal({ onClose }: SearchBarModalProps) { storeRef.current?.focus(); }, []); const debounceTimeout = useRef(null); - const handleInputChange = useCallback((e: React.ChangeEvent) => { + const handleInputChange = (e: React.ChangeEvent) => { const inputValue = e.target.value.trim(); if (debounceTimeout.current) clearTimeout(debounceTimeout.current); debounceTimeout.current = setTimeout(async () => { @@ -42,7 +42,7 @@ export default function SearchBarModal({ onClose }: SearchBarModalProps) { const data = await getRelateSearch(inputValue); setRelateSearchItems(data); }, 200); - }, []); + } const handleSearch = () => { const value = storeRef.current?.value ?? ''; diff --git a/src/pages/callvan/notifications/index.tsx b/src/pages/callvan/notifications/index.tsx index 2b15049a2..31959678f 100644 --- a/src/pages/callvan/notifications/index.tsx +++ b/src/pages/callvan/notifications/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import type { GetServerSidePropsContext } from 'next'; import { useRouter } from 'next/router'; import { dehydrate, QueryClient, useQuery } from '@tanstack/react-query'; @@ -65,13 +65,10 @@ export default function CallvanNotificationsPage() { onSuccess: () => setIsDeleteModalOpen(false), }); - const handleCardClick = useCallback( - (id: number, isRead: boolean) => { - if (isRead) return; - markRead(id); - }, - [markRead], - ); + const handleCardClick = (id: number, isRead: boolean) => { + if (isRead) return; + markRead(id); + }; if (mounted && !isMobile) { return null; diff --git a/src/pages/lost-item/chat/index.tsx b/src/pages/lost-item/chat/index.tsx index 288c20fec..94c440a5a 100644 --- a/src/pages/lost-item/chat/index.tsx +++ b/src/pages/lost-item/chat/index.tsx @@ -3,7 +3,7 @@ // NOTE: 이 페이지는 이미지가 동적으로 바뀌고(채팅/썸네일/메시지), 크기·비율이 제각각입니다. // next/image 도입 시 sizes/fill 등 설정·관리 비용이 커지는데 비해(특히 작은/반복 이미지) 체감 이득이 작아 를 유지합니다. -import React, { useEffect, useRef, useState, useCallback } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import Link from 'next/link'; import BlockIcon from 'assets/svg/Articles/block.svg'; @@ -63,7 +63,7 @@ function LostItemChatPage({ token }: { token: string }) { const prevMessagesLengthRef = useRef(0); - const uploadImage = useCallback(async () => { + const uploadImage = async () => { try { if (userInfo === null || !chatroomDetail) { showToast('error', '유저정보 혹은 채팅방 정보를 불러오는데 실패했습니다.'); @@ -79,16 +79,16 @@ function LostItemChatPage({ token }: { token: string }) { showToast('error', error.message); } } - }, [userInfo, chatroomDetail, saveImgFile, sendChatMessage]); + } - const sendMessage = useCallback(() => { + const sendMessage = () => { if (!inputValue.trim() || userInfo === null || !chatroomDetail) { return; } sendChatMessage({ content: inputValue }); setInputValue(''); - }, [inputValue, userInfo, chatroomDetail, sendChatMessage]); + } const sendMessageToEnterKeyDown = async (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { From f91bbfb0b7fb924dccb65ea90ff3da79d9fc904b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=A3=BC?= Date: Thu, 25 Jun 2026 15:59:03 +0900 Subject: [PATCH 6/9] =?UTF-8?q?fix:React=20compiler=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=EC=9D=84=20true=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next.config.mjs b/next.config.mjs index 4bd7b6edd..d959df537 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -32,7 +32,7 @@ const nextConfig = { return config; }, experimental: { - reactCompiler: process.env.REACT_COMPILER === 'true', + reactCompiler: true, workerThreads: false, }, images: { From e337d74d945538326445e2a2dd24bddb1ef05de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=A3=BC?= Date: Thu, 25 Jun 2026 16:09:04 +0900 Subject: [PATCH 7/9] =?UTF-8?q?fix:=20use=20no=20memo=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Auth/SignupPage/Steps/Terms/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Auth/SignupPage/Steps/Terms/index.tsx b/src/components/Auth/SignupPage/Steps/Terms/index.tsx index 4a17f5256..eba890934 100644 --- a/src/components/Auth/SignupPage/Steps/Terms/index.tsx +++ b/src/components/Auth/SignupPage/Steps/Terms/index.tsx @@ -1,4 +1,4 @@ -'use no memo'; +'use no memo'; //react compiler로 인한 watch 오류 방지 import { cn } from '@bcsdlab/utils'; import CustomCheckbox from 'components/Auth/SignupPage/components/CustomCheckbox'; import { useFormContext } from 'react-hook-form'; From 3fbda51fef5b7cec25e6a0e3f974dccafbf7210f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=A3=BC?= Date: Sat, 27 Jun 2026 20:51:54 +0900 Subject: [PATCH 8/9] =?UTF-8?q?refactor:=20lint=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint.config.mjs | 3 +- .../Steps/MobileVerificationStep/index.tsx | 36 +++++++++++----- .../components/StudentForm/index.tsx | 29 +++---------- .../IndexComponents/IndexStore/index.tsx | 2 +- .../components/CustomLecture/index.tsx | 3 ++ .../layout/Header/PCHeader/index.tsx | 43 ++++++++++--------- src/components/modal/Modal/PortalProvider.tsx | 13 ++---- src/pages/auth/modifyinfo/index.tsx | 30 ++++--------- 8 files changed, 73 insertions(+), 86 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index c3258fbca..37cdbf905 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -58,8 +58,7 @@ export default [ ...next.configs['core-web-vitals'].rules, ...react.configs.recommended.rules, ...react.configs['jsx-runtime'].rules, - 'react-hooks/rules-of-hooks':'error', - 'react-hooks/exhaustive-deps':'warn', + ...reactHooks.configs['recommended-latest'].rules, 'import/order': [ 'error', diff --git a/src/components/Auth/SignupPage/Steps/MobileVerificationStep/index.tsx b/src/components/Auth/SignupPage/Steps/MobileVerificationStep/index.tsx index 92272cfe6..53b95563d 100644 --- a/src/components/Auth/SignupPage/Steps/MobileVerificationStep/index.tsx +++ b/src/components/Auth/SignupPage/Steps/MobileVerificationStep/index.tsx @@ -1,6 +1,5 @@ -/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable no-restricted-imports */ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { isKoinError } from '@bcsdlab/koin'; import { useMutation } from '@tanstack/react-query'; import { checkPhone, smsSend, smsVerify } from 'api/auth'; @@ -52,6 +51,7 @@ function MobileVerification({ onNext }: MobileVerificationProps) { const [isVerified, enableVerified] = useBooleanState(false); const [isCodeCorrect, setCorrect, setIncorrect] = useBooleanState(false); const [smsSendCountData, setSmsSendCountData] = useState(null); + const verificationMessageTimerRef = useRef | null>(null); const { isRunning: isTimer, @@ -65,12 +65,23 @@ function MobileVerification({ onNext }: MobileVerificationProps) { }, }); + const clearVerificationMessageTimer = () => { + if (verificationMessageTimerRef.current) { + clearTimeout(verificationMessageTimerRef.current); + verificationMessageTimerRef.current = null; + } + }; + const { mutate: sendSMSToUser } = useMutation({ mutationFn: smsSend, onSuccess: (data: SmsSendResponse) => { setPhoneMessage({ type: 'success', content: MESSAGES.PHONE.CODE_SENT }); runTimer(); showVerification(); + clearVerificationMessageTimer(); + verificationMessageTimerRef.current = setTimeout(() => { + setVerificationMessage({ type: 'default', content: MESSAGES.VERIFICATION.DEFAULT }); + }, 60000); setSmsSendCountData({ total_count: data.total_count, remaining_count: data.remaining_count, @@ -149,21 +160,20 @@ function MobileVerification({ onNext }: MobileVerificationProps) { }); }; - const isNameAndGenderFilled = name?.trim() && gender?.length > 0; - - useEffect(() => { + const resetVerificationState = () => { + clearVerificationMessageTimer(); disableButton(); stopTimer(); setVerificationMessage(null); setPhoneMessage(null); setIncorrect(); - }, [phoneNumber]); + }; + + const isNameAndGenderFilled = name?.trim() && gender?.length > 0; useEffect(() => { - if (timerValue === 120) { - setVerificationMessage({ type: 'default', content: MESSAGES.VERIFICATION.DEFAULT }); - } - }, [timerValue]); + return () => clearVerificationMessageTimer(); + }, []); return (
@@ -208,6 +218,12 @@ function MobileVerification({ onNext }: MobileVerificationProps) { render={({ field }) => ( { + if (e.target.value !== field.value) { + resetVerificationState(); + } + field.onChange(e); + }} placeholder="- 없이 번호를 입력해 주세요." isDelete={!isVerified} message={phoneMessage} diff --git a/src/components/GraduationCalculatorPage/components/StudentForm/index.tsx b/src/components/GraduationCalculatorPage/components/StudentForm/index.tsx index 20405f26e..82a3e8bbf 100644 --- a/src/components/GraduationCalculatorPage/components/StudentForm/index.tsx +++ b/src/components/GraduationCalculatorPage/components/StudentForm/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useSuspenseQuery } from '@tanstack/react-query'; import { authQueries } from 'api/auth/queries'; import { deptQueries } from 'api/dept/queries'; @@ -17,9 +17,11 @@ function StudentForm() { const [studentNumber, setStudentNumber] = useState(academicInfo?.student_number ?? ''); const [department, setDepartment] = useState(academicInfo?.department ?? ''); const [major, setMajor] = useState(academicInfo?.major ?? ''); - const [majorOptionList, setMajorOptionList] = useState<{ label: string; value: string }[]>([ - { label: '', value: '' }, - ]); + const majorOptionList = + deptMajorList + .find((deptMajor) => deptMajor.department === department) + ?.majors.map((majorName) => ({ label: majorName, value: majorName })) ?? + []; const departmentOptionList = deptMajorList.map((deptMajor) => ({ label: deptMajor.department, @@ -30,14 +32,6 @@ function StudentForm() { setStudentNumber(e.target.value); }; - const handleMajor = (dept: string) => { - const selectedDeptMajor = deptMajorList.find((deptMajor) => deptMajor.department === dept); - const majorsOption = selectedDeptMajor - ? selectedDeptMajor.majors.map((majors) => ({ label: majors, value: majors })) - : []; - setMajorOptionList(majorsOption); - }; - const handleDepartment = ({ target }: { target: { value: string } }) => { logger.actionEventClick({ team: 'USER', @@ -45,7 +39,6 @@ function StudentForm() { value: `학과 드롭다운_${target.value}`, }); setDepartment(target.value); - handleMajor(target.value); setMajor(''); }; @@ -61,16 +54,6 @@ function StudentForm() { }); }; - useEffect(() => { - if (academicInfo) { - setStudentNumber(academicInfo.student_number); - setDepartment(academicInfo.department); - handleMajor(academicInfo.department); - setMajor(academicInfo.major); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [academicInfo]); - return (
diff --git a/src/components/IndexComponents/IndexStore/index.tsx b/src/components/IndexComponents/IndexStore/index.tsx index 1c943f276..1b1723189 100644 --- a/src/components/IndexComponents/IndexStore/index.tsx +++ b/src/components/IndexComponents/IndexStore/index.tsx @@ -78,7 +78,7 @@ export default function IndexStore({ categories }: { categories: StoreCategories }); if (route.startsWith('http')) { - window.location.href = route; + window.location.assign(route); return; } diff --git a/src/components/TimetablePage/components/CustomLecture/index.tsx b/src/components/TimetablePage/components/CustomLecture/index.tsx index aa83c3fd8..eb62a1686 100644 --- a/src/components/TimetablePage/components/CustomLecture/index.tsx +++ b/src/components/TimetablePage/components/CustomLecture/index.tsx @@ -1,3 +1,6 @@ +'use no memo'; +/* eslint-disable react-hooks/refs */ +/* eslint-disable react-hooks/set-state-in-effect */ import { useState, useEffect, useRef } from 'react'; import { cn } from '@bcsdlab/utils'; import { MyLectureInfo } from 'api/timetable/entity'; diff --git a/src/components/layout/Header/PCHeader/index.tsx b/src/components/layout/Header/PCHeader/index.tsx index 43b85a671..5a0560dd9 100644 --- a/src/components/layout/Header/PCHeader/index.tsx +++ b/src/components/layout/Header/PCHeader/index.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { cn } from '@bcsdlab/utils'; @@ -10,6 +10,7 @@ import useLogger from 'utils/hooks/analytics/useLogger'; import { useSessionLogger } from 'utils/hooks/analytics/useSessionLogger'; import { useLogout } from 'utils/hooks/auth/useLogout'; import useModalPortal from 'utils/hooks/layout/useModalPortal'; +import useMount from 'utils/hooks/state/useMount'; import useTokenState from 'utils/hooks/state/useTokenState'; import { isomorphicSessionStorage } from 'utils/ts/env'; import type { Portal } from 'components/modal/Modal/PortalProvider'; @@ -73,8 +74,7 @@ export default function PCHeader({ openModal }: PCHeaderProps) { const pathname = asPath.split('?')[0] || '/'; const search = asPath.includes('?') ? `?${asPath.split('?')[1]}` : ''; const isStage = process.env.NEXT_PUBLIC_API_PATH?.includes('stage'); - const [mounted, setMounted] = useState(false); - useEffect(() => setMounted(true), []); + const mounted = useMount(); const isLoggedin = mounted && !!token; @@ -237,23 +237,26 @@ export default function PCHeader({ openModal }: PCHeaderProps) { aria-labelledby={Array.from({ length: 2 }, (_, index) => ID[`LABEL${index + 1}`]).join(' ')} >
    - {panelMenuList?.slice(0, -4).filter((menu) => !menu.mobileOnly).map((menu) => { - const preferred = isStage && menu.stageLink ? menu.stageLink : menu.link; - const href = preferred ?? ROUTES.Main(); - return ( -
  • - {/* TODO: 키보드 Focus 접근성 향상 */} - handleMenuClick(e, menu.title)} - > - {menu.title} - -
  • - ); - })} + {panelMenuList + ?.slice(0, -4) + .filter((menu) => !menu.mobileOnly) + .map((menu) => { + const preferred = isStage && menu.stageLink ? menu.stageLink : menu.link; + const href = preferred ?? ROUTES.Main(); + return ( +
  • + {/* TODO: 키보드 Focus 접근성 향상 */} + handleMenuClick(e, menu.title)} + > + {menu.title} + +
  • + ); + })}
diff --git a/src/components/modal/Modal/PortalProvider.tsx b/src/components/modal/Modal/PortalProvider.tsx index a266fd09f..017d9f618 100644 --- a/src/components/modal/Modal/PortalProvider.tsx +++ b/src/components/modal/Modal/PortalProvider.tsx @@ -1,5 +1,6 @@ import React, { ReactNode } from 'react'; import ReactDOM from 'react-dom'; +import useMount from 'utils/hooks/state/useMount'; export interface Portal { close: () => void; @@ -27,8 +28,9 @@ interface PortalProviderProps { const PortalProvider = function PortalProvider({ children }: PortalProviderProps) { const [modalPortal, setModalPortal] = React.useState(); - const [mounted, setMounted] = React.useState(false); + const mounted = useMount(); const [container, setContainer] = React.useState(null); + const portalContainer = container ?? (mounted && typeof document !== 'undefined' ? document.body : null); const open: OpenFunc = React.useCallback((element, options = {}) => { const { onClose, appendTo } = options; @@ -58,17 +60,10 @@ const PortalProvider = function PortalProvider({ children }: PortalProviderProps [open], ); - React.useEffect(() => { - setMounted(true); - if (typeof document !== 'undefined') { - setContainer((prev) => prev ?? document.body); - } - }, []); - return ( {children} - {mounted && modalPortal && container ? ReactDOM.createPortal(modalPortal, container) : null} + {modalPortal && portalContainer ? ReactDOM.createPortal(modalPortal, portalContainer) : null} ); }; diff --git a/src/pages/auth/modifyinfo/index.tsx b/src/pages/auth/modifyinfo/index.tsx index 3037a716d..d323959b8 100644 --- a/src/pages/auth/modifyinfo/index.tsx +++ b/src/pages/auth/modifyinfo/index.tsx @@ -256,7 +256,7 @@ const PasswordForm = React.forwardRef ({ ...prev, isPasswordValid: true, isFieldChanged: true })); + setIsValid((prev) => ({ ...prev, isPasswordValid: true })); }; const handlePasswordConfirmChange = (e: React.ChangeEvent) => { @@ -294,7 +294,7 @@ const PasswordForm = React.forwardRef ({ ...prev, isPasswordValid: true, isFieldChanged: true })); + setIsValid((prev) => ({ ...prev, isPasswordValid: true })); }; return ( @@ -516,14 +516,13 @@ const NicknameForm = React.forwardRef((props, ref) => { const { data: userInfo } = useUser(); const { data: deptList } = useSuspenseQuery(deptQueries.list()); - - // ✅ 안전 기본값 - const [studentNumber, setStudentNumber] = useState(''); - const [major, setMajor] = useState(null); + const isStudent = isStudentUser(userInfo); + // userInfo가 없거나 학생 정보가 없을 때도 controlled input을 유지하기 위한 기본값 + const [studentNumber, setStudentNumber] = useState(isStudent ? (userInfo.student_number ?? '') : ''); + const [major, setMajor] = useState(isStudent ? (userInfo.major ?? null) : null); const deptOptionList = (deptList ?? []).map((dept) => ({ label: dept.name, value: dept.name })); const { setIsValid } = useValidationContext(); - const isStudent = isStudentUser(userInfo); const isMobile = useMediaQuery(); const handleChangeStudentId = (event: React.ChangeEvent) => { @@ -550,8 +549,6 @@ const MajorInput = React.forwardRef((pr useEffect(() => { if (isStudentUser(userInfo)) { - setStudentNumber(userInfo.student_number ?? ''); - setMajor(userInfo.major ?? null); // 초기 동일 값이면 valid 표시만 setIsValid((prev) => ({ ...prev, @@ -573,16 +570,6 @@ const MajorInput = React.forwardRef((pr return { value: { studentNumber, major }, valid }; }, [studentNumber, major]); - useEffect(() => { - if (isStudent) { - if (studentNumber === userInfo?.student_number) { - setIsValid((prev) => ({ ...prev, isStudentIdValid: true })); - } - if (major === userInfo?.major) { - setIsValid((prev) => ({ ...prev, isStudentMajorValid: true })); - } - } - }, []); return (
{isMobile ? ( @@ -652,14 +639,15 @@ const MajorInput = React.forwardRef((pr const GenderInput = React.forwardRef((_, ref) => { const { data: userInfo } = useUser(); - const [selectedValue, setSelectedValue] = React.useState(null); + const gender = userInfo?.gender; + const initialSelectedValue = gender === 0 || gender === 1 ? String(gender) : null; + const [selectedValue, setSelectedValue] = React.useState(initialSelectedValue); const { setIsValid } = useValidationContext(); useImperativeHandle(ref, () => ({ value: selectedValue })); useEffect(() => { const g = userInfo?.gender; // 0 | 1 | null | undefined - setSelectedValue(g === 0 || g === 1 ? String(g) : null); if (g === 0 || g === 1) { setIsValid((prev) => ({ ...prev, isGenderValid: true })); } From 0622592a9ef05676e6c43527c6a1fa468c380484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=A3=BC?= Date: Sat, 27 Jun 2026 21:41:27 +0900 Subject: [PATCH 9/9] =?UTF-8?q?fix:codeRabbit=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/MobileDatePicker/MobileDatePicker.tsx | 8 ++++---- .../SignupPage/Steps/MobileVerificationStep/index.tsx | 1 + src/pages/bus/city/index.tsx | 5 +++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/Articles/LostItemWritePage/components/MobileDatePicker/MobileDatePicker.tsx b/src/components/Articles/LostItemWritePage/components/MobileDatePicker/MobileDatePicker.tsx index d4e7da452..6d96c44eb 100644 --- a/src/components/Articles/LostItemWritePage/components/MobileDatePicker/MobileDatePicker.tsx +++ b/src/components/Articles/LostItemWritePage/components/MobileDatePicker/MobileDatePicker.tsx @@ -18,10 +18,10 @@ export default function MobileDatePicker({ selectedDate, setSelectedDate }: Mobi const today = useMemo(() => new Date(), []); const [pendingDate, setPendingDate] = useState(selectedDate); - const { years, months, days, currentYear, currentMonth, currentDay } = getDateRange({ - selectedDate: pendingDate, - maxDate: today, - }); + const { years, months, days, currentYear, currentMonth, currentDay } = useMemo( + () => getDateRange({ selectedDate: pendingDate, maxDate: today }), + [pendingDate, today], + ); const { syncScrollPosition, createScrollHandler } = useScrollPicker({ itemHeight: ITEM_HEIGHT }); diff --git a/src/components/Auth/SignupPage/Steps/MobileVerificationStep/index.tsx b/src/components/Auth/SignupPage/Steps/MobileVerificationStep/index.tsx index 53b95563d..0986be8bc 100644 --- a/src/components/Auth/SignupPage/Steps/MobileVerificationStep/index.tsx +++ b/src/components/Auth/SignupPage/Steps/MobileVerificationStep/index.tsx @@ -75,6 +75,7 @@ function MobileVerification({ onNext }: MobileVerificationProps) { const { mutate: sendSMSToUser } = useMutation({ mutationFn: smsSend, onSuccess: (data: SmsSendResponse) => { + clearVerificationMessageTimer(); setPhoneMessage({ type: 'success', content: MESSAGES.PHONE.CODE_SENT }); runTimer(); showVerification(); diff --git a/src/pages/bus/city/index.tsx b/src/pages/bus/city/index.tsx index 22f171f14..f5479af51 100644 --- a/src/pages/bus/city/index.tsx +++ b/src/pages/bus/city/index.tsx @@ -71,8 +71,9 @@ export default function CityBusTimetable() { } const today = dayjs().day(); - const dayType:DayType = today === 0 || today === 6 ? '주말' : '평일'; - const todayTimetable = timetable.info.bus_timetables.find((item)=>item.day_of_week === dayType); + const dayType: DayType = today === 0 || today === 6 ? '주말' : '평일'; + const busTimetables = timetable.info.bus_timetables ?? []; + const todayTimetable = busTimetables.find((item) => item.day_of_week === dayType); const departInfo = todayTimetable?.depart_info ?? []; const amTimes = departInfo.filter((time) => getHours(time) < 12);