Conversation
Review Summary by Qodo관리자 페이지 전체 기능 구현 (인증, 사용자 관리, 이벤트, 공지사항)
WalkthroughsDescription• 관리자 페이지 전체 기능 구현 (인증, 사용자 관리, 이벤트, 공지사항 등) • 관리자 인증 관련 훅(useAdminAuth) 및 관리 기능 훅(useAdminManagement) 추가 • 로그인, 회원가입, 웹메일 인증 페이지 컴포넌트 구현 • 사용자 검색, 상세정보 조회, 포인트 조정, 경고 메시지 전송 기능 페이지 구현 • 이벤트 관리(매칭 기회, 포인트 충전 할인) 및 공지사항 예약 기능 페이지 구현 • 충전 요청 관리, 결제 내역 조회, 경고 히스토리 조회 페이지 구현 • 역할별(마스터, 오퍼레이터) 메뉴 및 네비게이션 컴포넌트 구현 • 드롭다운, 모달, 페이지네이션, 날짜/시간 선택 등 재사용 가능한 UI 컴포넌트 추가 • 날짜/시간 포맷팅 유틸리티 함수 추가 • 관리자 페이지 레이아웃 및 인증 처리 구현 Diagramflowchart LR
Auth["인증<br/>(로그인/회원가입)"]
UserMgmt["사용자 관리<br/>(검색/상세정보)"]
PointMgmt["포인트 관리<br/>(조정/결제내역)"]
WarnMgmt["경고 관리<br/>(메시지/히스토리)"]
EventMgmt["이벤트 관리<br/>(예약/히스토리)"]
NoticeMgmt["공지사항 관리<br/>(예약/히스토리)"]
PayReq["충전 요청<br/>(목록/승인)"]
Hooks["API 훅<br/>(useAdminAuth<br/>useAdminManagement)"]
Components["UI 컴포넌트<br/>(드롭다운/모달<br/>페이지네이션)"]
Utils["유틸리티<br/>(dateFormatter)"]
Hooks --> Auth
Hooks --> UserMgmt
Hooks --> PointMgmt
Hooks --> WarnMgmt
Hooks --> EventMgmt
Hooks --> NoticeMgmt
Hooks --> PayReq
Components --> Auth
Components --> UserMgmt
Components --> EventMgmt
Components --> NoticeMgmt
Utils --> EventMgmt
Utils --> NoticeMgmt
Utils --> PayReq
File Changes1. hooks/useAdminManagement.ts
|
Enabling\disabling automation
meaning the
the tool will replace every marker of the form
Note that when markers are enabled, if the original PR description does not contain any markers, the tool will not alter the description at all. |
Custom labelsThe default labels of the If you specify custom labels in the repo's labels page or via configuration file, you can get tailored labels for your use cases.
The list above is eclectic, and aims to give an idea of different possibilities. Define custom labels that are relevant for your repo and use cases. |
Inline File Walkthrough 💎For enhanced user experience, the To enable inline file summary, set
|
Utilizing extra instructionsThe Be specific, clear, and concise in the instructions. With extra instructions, you are the prompter. Notice that the general structure of the description is fixed, and cannot be changed. Extra instructions can change the content or style of each sub-section of the PR description. Examples for extra instructions: Use triple quotes to write multi-line instructions. Use bullet points to make the instructions more readable. |
More PR-Agent commands
|
See the describe usage page for a comprehensive guide on using this tool.
Code Review by Qodo
1. Admin 401 misredirects login
|
| const adminLogin = async ( | ||
| payload: AdminLoginRequest, | ||
| ): Promise<AdminLoginResponse> => { | ||
| const { data } = await api.post<AdminLoginResponse>("/admin/login", payload); | ||
| return data; | ||
| }; | ||
|
|
||
| const getAdminInfo = async (): Promise<AdminInfoResponse> => { | ||
| const { data } = await api.get<AdminInfoResponse>("/auth/any-admin/info"); | ||
| return data; | ||
| }; |
There was a problem hiding this comment.
1. Admin 401 misredirects login 🐞 Bug ⛯ Reliability
The new admin hooks use the shared axios client, whose 401 interceptor refreshes via /api/auth/login and redirects to /login on failure; this can send admins to the non-admin login page and break admin auth flows on any 401 from admin endpoints. This PR introduces the admin endpoints onto that client, so the issue becomes user-facing for the new admin UI.
Agent Prompt
### Issue description
Admin API calls introduced by this PR use the shared `api` axios instance. That instance has a global 401 interceptor that tries to refresh via `/api/auth/login` and redirects to `/login` on failure, which can incorrectly send admin users to the non-admin login and break admin UX.
### Issue Context
Admin pages call endpoints like `/auth/any-admin/info` using `api`. Any 401 from these endpoints triggers the interceptor.
### Fix Focus Areas
- hooks/useAdminAuth.ts[44-54]
- lib/axios.ts[13-37]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| <div | ||
| onClick={() => router.push("/adminpage/myPage/event/page")} // or event main | ||
| className="flex flex-1 cursor-pointer flex-col gap-2 rounded-[24px] border border-white/30 bg-white p-6 pt-[26px] shadow-[1px_1px_20px_rgba(196,196,196,0.3)] transition-shadow hover:shadow-lg" | ||
| > |
There was a problem hiding this comment.
2. Event link uses wrong path 🐞 Bug ✓ Correctness
The notice completion screen navigates to /adminpage/myPage/event/page, but the event index route is /adminpage/myPage/event, so the click will 404. Users finishing notice reservation cannot reach the event reservation screen via this CTA.
Agent Prompt
### Issue description
Notice complete CTA navigates to a non-existent route (`/adminpage/myPage/event/page`), causing a 404.
### Issue Context
The event index route is defined by `app/adminpage/myPage/event/page.tsx`, so the correct URL is `/adminpage/myPage/event`.
### Fix Focus Areas
- app/adminpage/myPage/notice/complete/_components/ScreenNoticeRegisterCompletePage.tsx[53-56]
- app/adminpage/myPage/event/page.tsx[1-5]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const handleConfirm = () => { | ||
| const sH = parseInt(startTime); | ||
| const sM = parseInt(startMinutes); | ||
| const eH = parseInt(endTime); | ||
| const eM = parseInt(endMinutes); | ||
|
|
||
| if (isNaN(sH) || isNaN(sM) || isNaN(eH) || isNaN(eM)) { | ||
| alert("시간을 올바르게 선택해주세요."); | ||
| return; | ||
| } | ||
| if (selectedDiscount === "선택") { | ||
| alert("할인율을 선택해주세요."); | ||
| return; | ||
| } | ||
|
|
||
| const startTotal = sH * 60 + sM; | ||
| const endTotal = eH * 60 + eM; | ||
|
|
||
| if (startTotal >= endTotal) { | ||
| setErrorMessage( | ||
| <> | ||
| 이벤트 시작 시간이 종료 시간보다 <br /> 같거나 늦을 수 없습니다. | ||
| </>, | ||
| ); | ||
| setShowModal(true); | ||
| return; | ||
| } | ||
|
|
||
| // API Call logic | ||
| router.push("/adminpage/myPage/event/registercomplete"); | ||
| }; |
There was a problem hiding this comment.
3. Discount event not created 🐞 Bug ✓ Correctness
ScreenEventDiscountPage's confirm handler never calls the discount-event registration API and only routes to the completion page, so no discount event is actually created. This makes the UI report success without persisting anything.
Agent Prompt
### Issue description
Discount event reservation currently never calls the backend registration endpoint; it only navigates to the completion page.
### Issue Context
`hooks/useAdminManagement.ts` already defines the discount event registration API (`/auth/admin/event/discount`).
### Fix Focus Areas
- app/adminpage/myPage/event/discount/_components/ScreenEventDiscountPage.tsx[50-80]
- hooks/useAdminManagement.ts[114-125]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const handleVerify = async () => { | ||
| const codeString = values.join(""); | ||
| if (codeString.length !== 6) { | ||
| alert("6자리 숫자를 모두 입력해주세요."); | ||
| return; | ||
| } | ||
| if (timeLeft <= 0) { | ||
| alert("시간이 만료되었습니다. 재발송 버튼을 눌러주세요."); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| await verifyMutation.mutateAsync(codeString); | ||
| alert("인증에 성공했습니다."); | ||
| router.push("/adminpage"); | ||
| } catch (error) { | ||
| setFailCount((prev) => prev + 1); | ||
| alert("인증코드가 일치하지 않습니다."); | ||
| } | ||
| }; | ||
|
|
||
| const minutes = Math.floor(timeLeft / 60); | ||
| const seconds = timeLeft % 60; | ||
|
|
||
| return ( | ||
| <div className="min-h-screen bg-[#f4f4f4] font-sans"> | ||
| <AdminRegisterHeader /> | ||
|
|
||
| <main className="mt-10 flex flex-col items-center gap-6 p-6 md:px-[max(5vw,20px)]"> | ||
| <div | ||
| onClick={handleVerify} | ||
| className="flex w-full cursor-pointer flex-col gap-4 rounded-[24px] border border-white/30 bg-white p-10 shadow-[1px_1px_20px_rgba(196,196,196,0.3)] transition-shadow hover:shadow-lg" | ||
| > | ||
| <div className="border-none text-[32px] font-bold text-black"> | ||
| 웹메일 인증하기 | ||
| </div> | ||
| <div className="text-xl leading-relaxed font-medium text-[#858585]"> | ||
| 이전 절차에서 입력한 웹메일로 6자리 숫자코드가 전송되었습니다. | ||
| <br /> | ||
| 이메일 코드는 3분 뒤에 만료되며, 최대 10번의 입력이 가능합니다. | ||
| 이후에는 재발송 버튼을 눌러 인증코드를 다시 받아 주십시오. | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="flex w-full flex-col items-center gap-6 rounded-[24px] border border-white/30 bg-white p-10 shadow-[1px_1px_20px_rgba(196,196,196,0.3)]"> | ||
| <div className="flex gap-2 md:gap-4"> | ||
| {values.map((value, index) => ( | ||
| <input | ||
| key={index} | ||
| ref={(el) => { | ||
| inputRefs.current[index] = el; | ||
| }} | ||
| type="text" | ||
| inputMode="numeric" | ||
| maxLength={1} | ||
| value={value} | ||
| onChange={(e) => handleChange(e, index)} | ||
| onKeyDown={(e) => handleKeyDown(e, index)} | ||
| className="h-12 w-12 rounded-xl border-none bg-[#e5e5e5] text-center text-3xl font-bold text-black transition-all outline-none focus:ring-2 focus:ring-[#ff775e] md:h-16 md:w-16" | ||
| /> | ||
| ))} | ||
| </div> | ||
|
|
||
| <div className="mt-4 flex w-full items-center justify-end gap-8"> | ||
| <div className="text-2xl font-medium text-[#858585]"> | ||
| {timeLeft > 0 ? ( | ||
| <> | ||
| 남은 시간: {minutes}:{seconds.toString().padStart(2, "0")} | ||
| </> | ||
| ) : ( | ||
| "유효시간이 만료되었습니다." | ||
| )} | ||
| </div> | ||
| <button | ||
| onClick={handleResend} | ||
| className="rounded-lg bg-[#ff775e] px-6 py-3 text-xl font-bold text-white shadow-md transition-opacity hover:opacity-90" | ||
| > | ||
| 재발송 | ||
| </button> | ||
| </div> | ||
|
|
||
| <div className="w-full text-right text-2xl font-semibold text-[#666]"> | ||
| 인증이 가능한 횟수 {10 - failCount}회 | ||
| </div> |
There was a problem hiding this comment.
4. Unlimited webmail verify attempts 🐞 Bug ⛨ Security
ScreenAdminWebmailPage increments failCount on verification failure but never blocks verification after 10 attempts, and the remaining-attempts UI can go negative. This enables unlimited brute-force attempts despite the UI claiming a 10-try limit.
Agent Prompt
### Issue description
Webmail verification is intended to allow only 10 attempts, but the code does not enforce this limit and the remaining-attempts display can go negative.
### Issue Context
`failCount` is incremented on verify failure but never checked before calling `verifyMutation`.
### Fix Focus Areas
- app/adminpage/webmail-check/_components/ScreenAdminWebmailPage.tsx[77-96]
- app/adminpage/webmail-check/_components/ScreenAdminWebmailPage.tsx[158-160]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a complete administrative interface, enabling robust management of users, events, and notices. It provides a secure authentication system with webmail verification and role-based access, ensuring that different levels of administrators can efficiently perform their respective tasks through a newly designed set of UI components and integrated API hooks. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
전반적으로 어드민 페이지의 많은 기능이 추가되었습니다. 컴포넌트, 훅, 페이지가 잘 분리되어 구조가 좋습니다. 다만, 여러 파일에 걸쳐 반복적으로 나타나는 몇 가지 개선점이 보입니다.
- 접근성(Accessibility): 클릭 가능한
div요소가 버튼이나 링크 대신 사용되고 있으며, 폼 입력 필드에<label>이 누락된 경우가 많습니다. 이는 웹 표준 및 접근성 가이드라인(A11y) 위반입니다. - 타입 안정성:
any타입이 API 응답 데이터나 컴포넌트 props에 광범위하게 사용되고 있습니다. 이는 타입스크립트의 이점을 살리지 못하므로, 구체적인 타입을 정의하는 것이 좋습니다. - 네비게이션: 페이지 이동 시
router.push를 사용하는div대신 Next.js의<Link>컴포넌트를 사용하는 것이 시맨틱 HTML, SEO, 그리고 성능(프리페칭) 면에서 더 유리합니다. - React Keys:
map함수 사용 시 배열의 인덱스를key로 사용하는 것은 리스트가 동적으로 변경될 때 문제를 일으킬 수 있습니다. 데이터의 고유 ID를key로 사용해야 합니다. - 날짜/시간 처리: 타임존 변환이 수동으로 처리되어 일관성이 없고 오류에 취약합니다.
Intl.DateTimeFormat이나date-fns와 같은 라이브러리를 사용하여 안정적으로 처리하는 것을 권장합니다.
아래에 각 파일별로 구체적인 피드백을 남겼습니다.
| const formatDateTime = (isoString: string) => { | ||
| if (!isoString) return "알 수 없음"; | ||
| try { | ||
| const date = new Date(isoString); | ||
| if (isNaN(date.getTime())) return "알 수 없음"; | ||
|
|
||
| // KST 시간대 적용 (UTC+9) | ||
| const kstDate = new Date(date.getTime() + 9 * 60 * 60 * 1000); | ||
|
|
||
| const year = kstDate.getUTCFullYear(); | ||
| const month = String(kstDate.getUTCMonth() + 1).padStart(2, "0"); | ||
| const day = String(kstDate.getUTCDate()).padStart(2, "0"); | ||
| const hours = String(kstDate.getUTCHours()).padStart(2, "0"); | ||
| const minutes = String(kstDate.getUTCMinutes()).padStart(2, "0"); | ||
|
|
||
| return `${year}-${month}-${day} ${hours}시 ${minutes}분`; | ||
| } catch (error) { | ||
| return "알 수 없음"; | ||
| } | ||
| }; |
| return ( | ||
| <div className="flex min-h-screen w-screen flex-col bg-[#f4f4f4]"> | ||
| <AdminRegisterHeader /> | ||
| <main className="flex flex-1 flex-col items-center gap-4 px-[max(5vw,20px)] py-6 font-sans"> | ||
| <div | ||
| onClick={handleSubmit} | ||
| className="flex h-[117px] w-full cursor-pointer flex-col justify-center gap-2 rounded-[24px] border border-white/30 bg-white p-6 text-left shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] transition-shadow hover:shadow-lg" | ||
| > | ||
| <span className="text-[32px] font-bold text-black">가입하기</span> | ||
| <span className="text-left text-base font-medium text-[#858585]"> | ||
| 관리자의 승인을 받은 이후 오퍼레이터 권한을 사용할 수 있습니다 | ||
| </span> | ||
| </div> | ||
|
|
||
| <div className="grid w-full grid-cols-1 gap-10 rounded-[24px] border border-white/30 bg-white p-6 shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] md:grid-cols-2 md:gap-x-14 lg:grid-cols-3 lg:pr-[120px]"> | ||
| {inputFields.map((field, index) => | ||
| field === null ? ( | ||
| <div key={index} className="hidden lg:block"></div> | ||
| ) : ( | ||
| <InputComponent | ||
| key={index} | ||
| title={field.title} | ||
| placeholder={field.placeholder} | ||
| type={field.type} | ||
| name={field.name} | ||
| options={field.options} | ||
| onChange={handleChange} | ||
| value={(formData as any)[field.name]} | ||
| /> | ||
| ), | ||
| )} | ||
| </div> | ||
| </main> | ||
| </div> |
There was a problem hiding this comment.
웹 표준과 접근성을 준수하기 위해 이 컴포넌트 전체를 <form> 태그로 감싸고, '가입하기' 버튼은 <button type="submit">으로 구현하는 것이 좋습니다. 또한, 각 InputComponent에 대해 <label>을 제공해야 합니다. 현재는 시맨틱한 구조가 부족하여 키보드 네비게이션이나 스크린 리더 사용에 어려움이 있을 수 있습니다.
References
- 폼 태그 내의 입력 필드는 label과 연결되어야 하며, 제출 버튼은 type="submit"을 명시하는 등 웹 표준을 준수해야 합니다. (link)
| export const formatDateTime = (input: any) => { | ||
| if (!input) return { date: "N/A", time: "N/A" }; | ||
|
|
||
| if (Array.isArray(input) && input.length >= 5) { | ||
| const [year, month, day, hour, minute] = input; | ||
| return { | ||
| date: `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`, | ||
| time: `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`, | ||
| }; | ||
| } | ||
|
|
||
| if (typeof input === "string") { | ||
| try { | ||
| const date = new Date(input); | ||
| if (isNaN(date.getTime())) return { date: "N/A", time: "N/A" }; | ||
| const year = date.getFullYear(); | ||
| const month = String(date.getMonth() + 1).padStart(2, "0"); | ||
| const day = String(date.getDate()).padStart(2, "0"); | ||
| const hours = String(date.getHours()).padStart(2, "0"); | ||
| const minutes = String(date.getMinutes()).padStart(2, "0"); | ||
| return { | ||
| date: `${year}-${month}-${day}`, | ||
| time: `${hours}:${minutes}`, | ||
| }; | ||
| } catch { | ||
| return { date: "N/A", time: "N/A" }; | ||
| } | ||
| } | ||
| return { date: "N/A", time: "N/A" }; | ||
| }; |
There was a problem hiding this comment.
날짜와 시간을 포맷하는 이 유틸리티 함수는 타임존 처리에 취약합니다. new Date(input)은 브라우저와 입력 문자열 형식에 따라 다르게 동작할 수 있으며, 타임존 정보가 없는 경우 클라이언트의 로컬 타임존을 기준으로 해석하여 예기치 않은 결과를 낳을 수 있습니다. date-fns-tz와 같은 라이브러리를 사용하거나, Intl.DateTimeFormat API를 일관되게 활용하여 명시적으로 타임존('Asia/Seoul' 등)을 지정하고 포맷팅하는 것을 강력히 권장합니다. 이는 애플리케이션 전체에서 날짜/시간 데이터의 일관성과 정확성을 보장하는 데 중요합니다.
| <div | ||
| onClick={() => setIsOpen(!isOpen)} | ||
| className="w-full h-full bg-[#f4f4f4] rounded-lg border border-[#e5e5e5] px-3 flex items-center justify-between cursor-pointer text-[18px] font-semibold text-black" | ||
| > | ||
| <span>{selectedValue}</span> | ||
| <ChevronDown | ||
| size={18} | ||
| className={`transition-transform duration-300 text-gray-400 ${isOpen ? "rotate-180" : ""}`} | ||
| /> | ||
| </div> |
There was a problem hiding this comment.
웹 접근성을 위해 클릭 가능한 div 대신 <button> 요소를 사용하는 것이 좋습니다. type="button"을 명시하고, aria-haspopup="listbox"와 aria-expanded={isOpen} 속성을 추가하면 스크린 리더가 이 요소의 역할과 상태를 정확히 인지할 수 있습니다.
References
- 아이콘 전용 버튼의 aria-label, 키보드 네비게이션 가능 여부를 확인해야 합니다. 현재 div는 키보드로 포커스 및 활성화가 어렵습니다. (link)
| <div | ||
| onClick={goToMainButton} | ||
| className={`px-6 py-[29.5px] text-2xl font-semibold cursor-pointer h-full flex items-center transition-all ${adminSelect === "Main" ? "border-b-4 border-black text-black" : "text-[#808080]"}`} | ||
| > | ||
| Main | ||
| </div> |
There was a problem hiding this comment.
시맨틱 HTML과 Next.js의 성능 최적화 기능을 활용하기 위해 div와 onClick 조합 대신 next/link 컴포넌트를 사용하시는 것을 권장합니다. <Link> 컴포넌트는 페이지 사전 로딩(prefetching)을 지원하여 사용자 경험을 향상시키고, 검색 엔진 최적화(SEO)에도 더 유리합니다.
References
- 의미에 맞는 태그를 사용해야 합니다. 네비게이션 링크는
div가 아닌a태그(Next.js에서는Link컴포넌트)가 더 적합합니다. (link)
| <div | ||
| key={idx} | ||
| onClick={() => router.push(item.path)} | ||
| className="bg-white rounded-[24px] border border-white/30 shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] flex flex-col justify-center text-left p-6 gap-2 cursor-pointer min-w-[317px]" | ||
| > | ||
| <div className="text-[32px] font-bold text-black">{item.title}</div> | ||
| <div className="text-base font-medium text-[#858585]">{item.sub}</div> | ||
| </div> |
There was a problem hiding this comment.
페이지 이동을 위한 UI 요소는 div에 onClick을 사용하는 것보다 Next.js의 <Link> 컴포넌트를 사용하는 것이 좋습니다. <Link>는 시맨틱한 <a> 태그로 렌더링되어 웹 접근성을 향상시키고, 코드 분할 및 프리페칭(prefetching)과 같은 Next.js의 내장 최적화 기능을 활용할 수 있어 성능에도 이점이 있습니다.
References
- 의미에 맞는 태그를 사용해야 합니다. 네비게이션 링크는
div가 아닌a태그(Next.js에서는Link컴포넌트)가 더 적합합니다. (link)
| <input | ||
| type="text" | ||
| name="accountId" | ||
| placeholder="ID입력" | ||
| className="w-[70%] rounded-[0.8em] border border-gray-300 p-[0.9em] text-[18px] shadow-sm outline-none" | ||
| value={formData.accountId} | ||
| onChange={handleChange} | ||
| /> |
There was a problem hiding this comment.
웹 접근성 표준(WCAG)에 따라 모든 폼 입력 필드는 <label> 요소와 연결되어야 합니다. placeholder는 레이블을 대체할 수 없습니다. 스크린 리더 사용자를 위해 id와 htmlFor 속성을 사용하여 input과 label을 명시적으로 연결하고, 시각적으로 레이블을 숨기고 싶다면 sr-only (screen-reader-only) CSS 클래스를 사용하는 것이 좋습니다.
References
- 폼 내의 입력 필드는 label과 연결되어야 웹 표준을 준수하고 접근성이 향상됩니다. (link)
| userData.map((user, idx) => ( | ||
| <SearchUserComponent | ||
| key={idx} | ||
| email={user.email} | ||
| nickname={user.username} | ||
| uuid={user.uuid} | ||
| /> | ||
| )) |
There was a problem hiding this comment.
React에서 리스트를 렌더링할 때 배열의 인덱스를 key로 사용하는 것은 리스트의 항목이 추가, 삭제, 또는 재정렬될 때 예기치 않은 동작이나 성능 저하를 유발할 수 있습니다. 각 user 객체에 있는 고유한 uuid를 key로 사용해야 합니다.
| userData.map((user, idx) => ( | |
| <SearchUserComponent | |
| key={idx} | |
| email={user.email} | |
| nickname={user.username} | |
| uuid={user.uuid} | |
| /> | |
| )) | |
| userData.map((user) => ( | |
| <SearchUserComponent | |
| key={user.uuid} | |
| email={user.email} | |
| nickname={user.username} | |
| uuid={user.uuid} | |
| /> | |
| )) |
| export type AdminActionResponse = { | ||
| status: number; | ||
| message: string; | ||
| data: any; |
There was a problem hiding this comment.
스타일 가이드(53: Strict Typing)에 따라 any 타입 사용은 지양해야 합니다. API 응답 데이터에 대해 구체적인 타입을 정의하면 타입 안정성이 향상되고, 개발 중 실수를 방지하며, 코드의 가독성과 유지보수성이 좋아집니다. AdminActionResponse의 data 필드에 any 대신 실제 데이터 구조에 맞는 타입을 선언해주세요.
References
- any 타입 사용을 금지하고, unknown이나 never를 적절히 활용하여 타입을 좁히도록 권장합니다. (link)
No description provided.