Skip to content

인증 플로우 구축과 매물/지도 관련 주요 기능 및 UI 구현#5

Merged
HSCHEOL merged 14 commits into
mainfrom
dev
Feb 3, 2026
Merged

인증 플로우 구축과 매물/지도 관련 주요 기능 및 UI 구현#5
HSCHEOL merged 14 commits into
mainfrom
dev

Conversation

@HSCHEOL

@HSCHEOL HSCHEOL commented Feb 3, 2026

Copy link
Copy Markdown
Contributor

작업 내용

  • 로그인 / 로그아웃 기능 완전 구현
    • swagger 기반 API hook 생성
    • 로그인 요청 body를 FormData로 변경
    • 로그인·회원가입 API 연동
  • 로그인 페이지 및 마이페이지 퍼블리싱
  • 매물 목록 및 상세 페이지 구현
    • 카카오 지도 연동
    • 관련 UI 컴포넌트 및 아이콘 추가
  • 상단 FilterBar 구현
  • 연락처 확인용 ContactModal 구현
    • Portal 적용으로 레이어 겹침 문제 해결
  • 학교 상세 정보 컴포넌트 퍼블리싱
  • listDetail.tsx 내 CurationSection 수정
  • 프로젝트 폴더 구조 정리

참고 사항

  • 인증 플로우부터 주요 서비스 화면까지 한 번에 연결되는 PR입니다.
  • UI 퍼블리싱과 기능 구현이 함께 포함되어 있어 변경 범위가 다소 큽니다.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 회원가입 및 로그인 기능 추가
    • 지도 기반 맞춤형 검색 필터 (거래유형, 주거형태, 평수 필터링)
    • 매물 상세 정보 페이지 (학군, 시설, 평가, 부동산 정보 포함)
    • 마이페이지 (회원정보 관리, 관심 매물, 최근 본 매물, 리포트)
    • 우선순위 기반 맞춤 정보 조회
  • 사용성 개선

    • 인증 상태별 네비게이션 메뉴 최적화
    • 스크롤바 숨김 처리로 깔끔한 UI 제공

@HSCHEOL HSCHEOL self-assigned this Feb 3, 2026
@HSCHEOL HSCHEOL added the feature Something isn't working label Feb 3, 2026
@vercel

vercel Bot commented Feb 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontend Ready Ready Preview, Comment Feb 3, 2026 7:04am

@coderabbitai

coderabbitai Bot commented Feb 3, 2026

Copy link
Copy Markdown
📝 Walkthrough

개요

이 PR은 대규모 기능 추가 및 리팩토링으로, 새로운 Navbar 컴포넌트, 확장된 Curation 컴포넌트, List 및 ListDetail과 관련된 다양한 섹션 컴포넌트들, 지도 관련 필터바 및 드롭다운, 마이페이지 전체 구성, 인증 시스템 개선, 상태 관리(Zustand), 및 디렉토리 구조 재정렬을 포함합니다.

Walkthrough

이 PR은 전체 애플리케이션 구조를 재정렬하여 부동산 매물 조회 시스템을 확장합니다. 새로운 지도 기반 필터링, 상세 매물 목록 뷰, 사용자 계정 관리 페이지를 추가하고, 인증 플로우를 개선하며, 컴포넌트 계층 구조를 정리하고 원격 API 설정을 업데이트합니다.

Changes

Cohort / File(s) 요약
Navbar 재구성
app/components/Navbar.tsx, app/components/layout/Navbar.tsx
기존 Navbar 컴포넌트를 제거하고 새로운 위치에 클라이언트 사이드 Navbar를 재작성했습니다. 새 버전은 사용자 상태 관리(useUserStore), 로그아웃 액션, 조건부 UI(로그인 상태에 따른 메뉴 렌더링)를 포함합니다.
Curation 컴포넌트 확장
app/components/curation/Curation.tsx
props를 선택적으로 변경하고 isSubComponent, showButton 옵션을 추가했습니다. 서브컴포넌트 모드 지원, 조건부 배경 오버레이, 자산 경로 업데이트를 포함합니다.
List 및 ListDetail 구성
app/components/list/List.tsx, app/components/list/ListDetail.tsx, app/components/list/ListItem.tsx
매물 목록 UI를 구성하는 새로운 컴포넌트들입니다. List는 우선순위 관리 및 탭 기반 선택을 지원하고, ListDetail은 요약/큐레이션/상세정보/편의시설/학군/공인중개사/리뷰 섹션을 포함합니다.
List 섹션 컴포넌트
app/components/list/list_section/{SummarySection, CurationSection, DetailInfoSection, FacilitiesSection, ReviewSection, SchoolSection, SchoolItem, AgencySection, ContactModal, SchoolDetail}.tsx
List 상세 뷰의 개별 섹션들을 렌더링하는 컴포넌트들입니다. 대화형 우선순위 배지, 레이더 차트 시각화, 학군 정보 오버레이, 공인중개사 정보, 연락처 모달을 포함합니다.
지도 및 필터 UI
app/components/map/{FilterBar, KakaoMap}.tsx, app/components/map/dropdown/{TransactionTypeDropdown, ResidenceTypeDropdown, SpaceDropdown}.tsx
카카오 지도 통합 및 다중 필터 시스템입니다. FilterBar는 거래 유형, 주거 형태, 평수 필터를 관리하고, 드롭다운들은 범위 슬라이더와 체크박스를 통해 선택을 제어합니다.
마이페이지 전체 구성
app/mypage/{MyPageContext, layout, page}.tsx, app/components/mypage/{MyPageSidebar, UserInfo, MemberInfo, ChangePassword, CurationInfo, LikedHouses, RecentlyViewedHouses, HomeReport, HouseCard}.tsx
사용자 정보, 회원정보, 비밀번호 변경, 큐레이션 설정, 찜한 집, 최근 본 집, D.HOME 리포트를 포함하는 전체 마이페이지 시스템입니다. Context 기반 메뉴 네비게이션을 사용합니다.
인증 및 로그인 개선
app/login/{actions, page}.tsx, app/signup/{actions, page}.tsx, app/types/login.ts
로그인/회원가입 페이지를 재작성하고 서버 액션(loginAction, logoutAction, signupAction)을 추가했습니다. React Query 뮤테이션, Zod 검증, HttpOnly 쿠키 기반 토큰 저장을 포함합니다.
상태 관리 및 유틸리티
app/store/userStore.ts, shared/api/auth.ts, app/components/ui/{Checkbox, Portal, PriorityToggle, RangeSlider}.tsx
Zustand 기반 사용자 상태 저장소, 인증 API 호출 및 React Query 훅, 재사용 가능한 UI 컴포넌트들(체크박스, 포탈, 우선순위 토글, 범위 슬라이더)을 추가했습니다.
경로 및 레이아웃 업데이트
app/layout.tsx, app/map/layout.tsx, app/map/page.tsx, app/page.tsx, app/onboarding/page.tsx
Navbar 임포트 경로 업데이트, 마이페이지 및 지도 레이아웃 추가, 온보딩 컴포넌트 경로 재정렬, 최종 버튼 링크를 "/" 에서 "/map"으로 변경했습니다.
온보딩 단계별 정리
app/components/onboarding/step{1,2,3}/*
온보딩 컴포넌트들을 단계별 디렉토리로 재정렬하고, 자산 경로를 통일된 위치(/icons/...)로 업데이트했습니다.
구성 및 스타일 업데이트
.gitignore, app/globals.css, next.config.ts, orval.config.ts, package.json
새로운 의존성 추가(radix-ui/react-slider, react-kakao-maps-sdk, zustand), 스크롤바 숨김 유틸리티 추가, 원격 이미지 로딩 허용, Orval 설정을 로컬 파일에서 원격 URL로 변경했습니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 새로운 지도 위에 매물들이 춤을 추고,
필터로 거르면 완벽한 집을 만나고,
마이페이지에서 꿈의 집들을 모으며,
빠르고 안전한 인증이 문을 열어주네요!
구조 정리 하나, 기능 추가 둘, 셋!
코드는 깔끔하고 사용자는 행복하고 🏠✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 PR의 주요 변경사항을 정확하게 반영합니다. 인증 플로우 구축과 매물/지도 관련 주요 기능 및 UI 구현이라는 핵심 내용이 명확하게 드러나 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
package.json (1)

14-28: ⚠️ Potential issue | 🟡 Minor

@radix-ui/react-slider@1.3.6 React 19 런타임 호환성 확인 필요

@radix-ui/react-slider@1.3.6은 peerDependencies에서 React 19를 명시적으로 지원하나, React 19 환경에서 @radix-ui/react-primitive 버전 불일치로 인한 런타임 크래시가 보고되었습니다. node_modules 또는 package-lock.json에서 @radix-ui/react-primitive2.1.3으로 제대로 해결되는지 확인하세요. 해결되지 않으면 lockfile을 재생성하세요.

react-kakao-maps-sdk@1.2.0은 React 16.8+ 지원으로 React 19와 호환성 문제가 없습니다.

app/onboarding/page.tsx (1)

39-43: ⚠️ Potential issue | 🔴 Critical

렌더링 중 setTimeout 호출은 버그를 유발합니다.

setTimeout이 컴포넌트 렌더 함수 본문에서 직접 호출되고 있습니다. 이는 다음과 같은 문제를 야기합니다:

  1. 매 렌더링마다 새로운 타이머가 생성됨
  2. 컴포넌트 언마운트 시 정리(cleanup)가 되지 않아 메모리 누수 발생
  3. 예측 불가능한 상태 업데이트

useEffect로 감싸고 cleanup 함수를 추가해야 합니다.

🐛 권장 수정 사항
+import React, { useState, useEffect } from "react";
-import React, { useState } from "react";
-  if (currentStep === 3) {
-    setTimeout(() => {
-      setIsLoading(false);
-    }, 5000);
-  }
+  useEffect(() => {
+    if (currentStep === 3) {
+      const timer = setTimeout(() => {
+        setIsLoading(false);
+      }, 5000);
+      return () => clearTimeout(timer);
+    }
+  }, [currentStep]);
🤖 Fix all issues with AI agents
In @.gitignore:
- Line 42: Remove orval.config.ts from .gitignore so the config file remains
tracked (it is required to regenerate API clients), and instead add the
generated output paths to .gitignore: ignore the specific generated client file
shared/api/generated/dingle.ts and the generated models directory
shared/api/generated/model (or a pattern such as shared/api/generated/**) so
generated artifacts are not committed while keeping orval.config.ts under
version control.

In `@app/components/layout/Navbar.tsx`:
- Around line 3-20: Replace the useEffect/setMounted hydration guard in Navbar
with a useSyncExternalStore-based client check: remove the useEffect and mounted
state, and instead call useSyncExternalStore(subscribe, getSnapshot,
getServerSnapshot) where getSnapshot returns typeof window !== "undefined"
(client true) and getServerSnapshot returns false, and subscribe is a
no-op/unsubscribe function; update code references from mounted to the new
isClient value and keep the rest of Navbar (user, clearUser, router, etc.)
unchanged.

In `@app/components/list/list_section/detail/ContactModal.tsx`:
- Line 19: The ContactModal component uses non-existent Tailwind classes (e.g.,
"z-150", "animate-in", "fade-in", "zoom-in", "max-w-100", "h-18.5", "pt-7.5") in
the className on ContactModal.tsx; fix it by either installing and enabling the
tailwindcss-animate plugin and adding it to the plugins array (so animate-*
classes are available) and/or defining the custom z, spacing and size tokens in
your tailwind.config.ts under theme.extend (or in the `@theme` block in
app/globals.css) so "z-150", "max-w-100", "h-18.5", "pt-7.5" resolve, and
alternatively replace "z-150" with a built-in z-index like "z-50" if you prefer
not to add custom z-values; update ContactModal.tsx className accordingly after
making these config/plugin changes.

In `@app/components/mypage/HomeReport.tsx`:
- Around line 98-123: Replace the non-focusable div checkbox in HomeReport.tsx
with an accessible control: use a semantic input[type="checkbox"] or a button
with role="checkbox" (referencing toggleSelection, report.id and isSelected) so
it is keyboard-focusable and operable; ensure it exposes
aria-checked={isSelected}, proper tabIndex, and calls toggleSelection(report.id)
on click and onKeyDown (Space/Enter) if you choose a button, and preserve the
existing className logic (bg-main-400/border-main-400 vs
bg-white/border-gray-300) so visuals remain the same.

In `@app/components/mypage/HouseCard.tsx`:
- Line 47: The Tailwind classes like top-17, w-30 and h-30 used in the HouseCard
component's class string are not part of the default spacing scale and aren't
defined in globals.css, so they won't generate CSS; either replace those tokens
in the HouseCard JSX (look for the class string containing "absolute top-17
left-5 w-5 h-5 ...") with valid Tailwind spacing classes (e.g., top-16/top-20,
w-8/w-32, h-8/h-32) that match the intended layout, or add explicit spacing keys
to your Tailwind config under theme.extend.spacing (e.g., "17": "4.25rem", "30":
"7.5rem") and rebuild Tailwind so top-17/w-30/h-30 generate styles—pick one
approach and update the class string or tailwind.config.js accordingly.

In `@app/components/mypage/MyPageSidebar.tsx`:
- Around line 45-57: The menu items rendered in MyPageSidebar.tsx using
section.items.map are only clickable via mouse; make them keyboard-accessible by
either replacing the <li> with a <button> (preferred) or by adding
role="button", tabIndex={0}, and an onKeyDown handler that calls
onMenuChange(item) when Enter or Space is pressed; also ensure the currentMenu
state is reflected via aria-current or aria-pressed for screen readers and keep
the existing cn class logic intact so styling (currentMenu === item) remains
unchanged.

In `@app/components/signup/SignupForm.tsx`:
- Around line 44-48: The debug console.log in the SignupForm that prints
watchedValues (from watch() inside the useEffect) can leak PII; remove the
unconditional console.log or guard it so it only runs in non-production (e.g.,
check process.env.NODE_ENV !== 'production' or a DEV flag) and avoid logging raw
form values (or explicitly redact sensitive fields like email, phone, password)
inside the useEffect that references watchedValues.

In `@app/components/ui/Portal.tsx`:
- Around line 19-21: The Portal component returns createPortal(children,
document.querySelector(selector) as HTMLElement) which can throw if
document.querySelector(selector) is null; update the mounting path in the Portal
(check the result of document.querySelector(selector) into a variable like
mountNode) and only call createPortal when mountNode is non-null (or
create/fallback to a container) to avoid the unsafe cast; ensure you still
respect the mounted flag and return null when no mount node exists.

In `@app/components/ui/PriorityToggle.tsx`:
- Around line 77-79: In the PriorityToggle component update the image path
string to fix the typo: replace the incorrect "ativate" segment with "activate"
in the src template used for the <Image> (the expression using isSelected ?
"ativate" : "deactivate" should become isSelected ? "activate" : "deactivate"),
so that icons load correctly for each p.id and p.label.

In `@app/login/page.tsx`:
- Line 24: The stayLoggedIn checkbox is currently only a label click target and
its state (stayLoggedIn) is never passed to the server; either implement a
proper accessible <input type="checkbox"> bound to state and propagate
stayLoggedIn to the server action, or remove the UI. To implement: update the
form in page.tsx to include a real checkbox input (controlled by stayLoggedIn
via setStayLoggedIn) with an associated <label htmlFor="stayLoggedIn"> to ensure
keyboard/screenreader access, include stayLoggedIn in the form submission
payload so loginAction receives it, and modify the server-side
loginAction/session creation logic to use the received stayLoggedIn to set token
maxAge dynamically (instead of the hardcoded 1 day) or apply conditional session
expiry. Reference symbols: stayLoggedIn, setStayLoggedIn, loginAction, and any
session/token maxAge handling in the server action.

In `@app/map/layout.tsx`:
- Around line 5-20: The layout function currently ignores the required Next.js
App Router children prop, so update the exported layout component (layout) to
accept and render a children prop (e.g., function signature (props: { children:
React.ReactNode }) or ({ children })) and place {children} where page content
should be rendered (for example inside the list/map container or replacing <List
/> if appropriate); alternatively, if you intend this to be a regular component
rather than an app layout, rename/remove the layout export and move this JSX
into a normal component used by page.tsx (e.g., MapShell) and ensure page.tsx
imports and renders FilterBar, List, KakaoMap and the page content accordingly.

In `@app/store/userStore.ts`:
- Around line 41-42: Add the persist skipHydration flag and manual rehydrate
call to avoid SSR hydration mismatches: update the persist config where
createJSONStorage is used to include skipHydration: true (so the persisted
localStorage state is not applied during server render), and in the client-only
component lifecycle call useUserStore.persist.rehydrate() inside a useEffect to
restore state on the client; reference the persist config that contains
createJSONStorage(...) and the useUserStore.persist.rehydrate() API when making
these changes.

In `@app/types/login.ts`:
- Around line 3-6: The Zod v4 change requires passing error details as an object
to .min()/.max(), so update loginSchema (export const loginSchema) to replace
z.string().min(1, "아이디를 입력해주세요.") and z.string().min(1, "비밀번호를 입력해주세요.") with
the v4 form z.string().min(1, { message: "..." }) and make the same conversion
for all .min()/.max() usages in app/types/signup.ts; ensure you keep the same
numeric bounds and messages but wrap the message in { message: "..." } for each
affected schema call.

In `@orval.config.ts`:
- Line 6: 현재 orval.config.ts의 target이 원격
URL("https://d-home.o-r.kr/v3/api-docs")에 하드코딩되어 있어 빌드 재현성과 가용성 리스크가 있으니, target
값을 환경변수 기반 또는 로컬 파일로 대체하도록 변경하세요: 변경할 위치는 orval.config.ts의 target 프로퍼티이며 값은
process.env.ORVAL_OPENAPI_URL ?? "./openapi.json" 형태로 바꿔서 환경변수로 주입 가능하게 하고, 프로젝트
루트에 openapi.json을 커밋해 두세요(또는 둘다 지원). 추가로 CI 설정에 yarn api:gen(또는 사용하는 스크립트) 단계를
추가해 빌드 시 자동으로 orval 코드를 생성하도록 구성하세요.

In `@shared/api/auth.ts`:
- Around line 34-40: The login function is setting a manual "Content-Type":
"multipart/form-data" header which prevents Axios from adding the required
boundary for FormData; update the login implementation (the login function that
calls customInstance) to remove the manual headers when data is a FormData
instance so Axios can auto-generate the Content-Type/boundary (i.e., stop
passing the headers object or only pass headers when not sending FormData).
🟡 Minor comments (15)
app/components/list/ListItem.tsx-59-62 (1)

59-62: ⚠️ Potential issue | 🟡 Minor

좋아요 버튼 핸들러가 비어 있습니다.

좋아요 버튼의 클릭 핸들러가 구현되지 않았습니다. 추후 구현 예정이라면 TODO 주석을 더 명확하게 작성하거나, 기능이 준비될 때까지 버튼을 비활성화하는 것을 고려해 보세요.

app/components/mypage/MemberInfo.tsx-80-85 (1)

80-85: ⚠️ Potential issue | 🟡 Minor

저장 버튼에 기능이 구현되지 않았습니다.

저장 버튼이 있지만 onClick 핸들러가 없어 클릭해도 아무 동작을 하지 않습니다. 사용자 경험 측면에서 혼란을 줄 수 있습니다.

이 기능 구현을 위한 코드를 생성해 드릴까요, 아니면 추후 구현을 위한 이슈를 생성할까요?

app/components/list/list_section/detail/SchoolDetail.tsx-145-149 (1)

145-149: ⚠️ Potential issue | 🟡 Minor

링크 스타일이 적용되었으나 클릭 불가

isLink: true일 때 파란색 밑줄 스타일이 적용되지만, 실제로는 <span> 요소라 클릭할 수 없습니다. 사용자가 클릭을 시도할 수 있어 혼란을 줄 수 있습니다.

🔧 권장 수정안
-                <span
-                  className={`flex-1 font-bold text-black ${row.isLink ? "text-blue-500 underline" : ""}`}
-                >
-                  {row.value}
-                </span>
+                {row.isLink ? (
+                  <a
+                    href={row.value}
+                    target="_blank"
+                    rel="noopener noreferrer"
+                    className="flex-1 font-bold text-blue-500 underline hover:opacity-70"
+                  >
+                    {row.value}
+                  </a>
+                ) : (
+                  <span className="flex-1 font-bold text-black">
+                    {row.value}
+                  </span>
+                )}
app/components/list/list_section/SchoolItem.tsx-18-18 (1)

18-18: ⚠️ Potential issue | 🟡 Minor

미사용 변수로 인한 파이프라인 실패

time prop이 정의되었지만 사용되지 않아 ESLint 경고가 발생하고 있습니다. 이 변수를 사용하거나 제거해야 합니다.

🔧 수정 방안

방안 1: time을 사용하지 않는 경우 제거

 const SchoolItem = ({
   name,
   type,
   distance,
-  time,
   onClick,
 }: SchoolItemProps) => {

인터페이스에서도 제거:

 interface SchoolItemProps {
   name: string;
   type: string;
   distance: string;
-  time?: string;
   onClick?: () => void;
 }

방안 2: time을 실제로 사용

             <div className="flex items-center gap-1 ml-1 pt-0.5">
               <Image
                 src="/icons/feature/list_detail/school/mappin.svg"
                 alt="Marker"
                 width={14}
                 height={14}
               />
               <span className="text-[18px] font-bold text-[`#2EA98C`]">
                 {distance}
               </span>
+              {time && (
+                <span className="text-[14px] text-[`#9D9D9D`] ml-2">
+                  ({time})
+                </span>
+              )}
             </div>
app/components/mypage/UserInfo.tsx-32-32 (1)

32-32: ⚠️ Potential issue | 🟡 Minor

preferences 미사용으로 ESLint 경고 발생

Line 32의 preferences가 렌더링에 쓰이지 않아 CI 경고가 납니다. 배열을 활용해 렌더링하도록 정리하거나 변수를 제거해 주세요.

🔧 배열 기반 렌더링 예시
-  const preferences = ["오피스텔", "강남구", "관악구", "송파구"];
+  const preferences = ["오피스텔", "강남구", "관악구", "송파구"];
+  const [preferredType, ...preferredAreas] = preferences;
...
-                <span className="px-3 py-1.5 bg-[`#DEFAF2`] border border-main-400 text-main-400 rounded-md text-[14px] font-medium">
-                  오피스텔
-                </span>
+                <span className="px-3 py-1.5 bg-[`#DEFAF2`] border border-main-400 text-main-400 rounded-md text-[14px] font-medium">
+                  {preferredType}
+                </span>
...
-                <span className="px-3 py-1.5 bg-[`#DEFAF2`] border border-main-400 text-main-400 rounded-full text-[14px] font-medium">
-                  강남구
-                </span>
-                <span className="px-3 py-1.5 bg-[`#DEFAF2`] border border-main-400 text-main-400 rounded-full text-[14px] font-medium">
-                  관악구
-                </span>
-                <span className="px-3 py-1.5 bg-[`#DEFAF2`] border border-main-400 text-main-400 rounded-full text-[14px] font-medium">
-                  송파구
-                </span>
+                {preferredAreas.map((area) => (
+                  <span
+                    key={area}
+                    className="px-3 py-1.5 bg-[`#DEFAF2`] border border-main-400 text-main-400 rounded-full text-[14px] font-medium"
+                  >
+                    {area}
+                  </span>
+                ))}

Also applies to: 136-155

app/components/list/list_section/SchoolSection.tsx-95-100 (1)

95-100: ⚠️ Potential issue | 🟡 Minor

잘못된 클래스 토큰(bg-) 제거 필요

유효하지 않은 클래스 문자열이 남아 있어 스타일 빌드에 잡음이 됩니다. 제거해 주세요.

🧹 정리 예시
-        <div className="flex items-center px-3 py-2 bg-[`#F8FAFB`] rounded-lg mb-6 bg-">
+        <div className="flex items-center px-3 py-2 bg-[`#F8FAFB`] rounded-lg mb-6">
app/components/mypage/LikedHouses.tsx-74-76 (1)

74-76: ⚠️ Potential issue | 🟡 Minor

기본 선택값 하드코딩은 실사용 시 오동작 가능성이 있습니다.
초기 상태가 ["1","2"]라 비교 버튼이 항상 활성화됩니다. 실제 데이터 기반으로 초기화하거나 빈 배열로 시작하는 편이 안전합니다.

🔧 개선 예시
-  const [selectedIds, setSelectedIds] = useState<string[]>(["1", "2"]);
+  const [selectedIds, setSelectedIds] = useState<string[]>([]);
app/components/list/list_section/SummarySection.tsx-32-103 (1)

32-103: ⚠️ Potential issue | 🟡 Minor

아이콘 버튼/이미지의 접근성 라벨을 보완해주세요.

하트 버튼에 aria-label이 없고, 5개의 이미지 컴포넌트가 모두 alt="Area"로 설정되어 있습니다. 스크린 리더 사용자가 각 요소의 의미를 정확히 인식할 수 있도록 다음과 같이 수정해 주세요:

🔧 개선 예시
-          <button className="text-[`#30CEA1`] mt-2">
+          <button className="text-[`#30CEA1`] mt-2" aria-label="관심 매물 토글">
             <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
               <path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
             </svg>
           </button>

-              alt="Area"
+              alt="면적"

-              alt="Area"
+              alt="방/욕실"

-              alt="Area"
+              alt="층수"

-              alt="Area"
+              alt="방향"

-              alt="Area"
+              alt="큐레이션"
app/components/list/list_section/CurationSection.tsx-101-133 (1)

101-133: ⚠️ Potential issue | 🟡 Minor

툴팁 트리거를 키보드/스크린리더에서도 접근 가능하게 해주세요.

현재 모든 카테고리 마커(안전, 환경, 접근성, 편의, 소음)가 div + hover 이벤트로만 작동하여 포커스가 불가능합니다. 각 마커를 button + onFocus/onBlur + aria-describedby(카테고리별 유니크 id) + role="tooltip"으로 전환해 주세요. 키보드 탭/스페이스로 모든 마커의 툴팁이 열리는지 확인 부탁드립니다.

🔧 개선 예시 (1개 마커 기준)
-          <div
-            className="absolute -top-13 left-1/2 -translate-x-1/2 flex flex-col items-center cursor-help group"
-            onMouseEnter={() => setHoveredCategory("안전")}
-            onMouseLeave={() => setHoveredCategory(null)}
-          >
+          <button
+            type="button"
+            className="absolute -top-13 left-1/2 -translate-x-1/2 flex flex-col items-center cursor-help group bg-transparent border-0 p-0"
+            onMouseEnter={() => setHoveredCategory("안전")}
+            onMouseLeave={() => setHoveredCategory(null)}
+            onFocus={() => setHoveredCategory("안전")}
+            onBlur={() => setHoveredCategory(null)}
+            aria-describedby="tooltip-safe"
+          >
...
-            {hoveredCategory === "안전" && (
-              <div className="absolute mb-2 top-5 -left-1 bg-[`#E8FBF6`] border border-[`#30CEA1`] rounded-md px-3 py-2 shadow-lg z-50 w-45">
+            {hoveredCategory === "안전" && (
+              <div id="tooltip-safe" role="tooltip" className="absolute mb-2 top-5 -left-1 bg-[`#E8FBF6`] border border-[`#30CEA1`] rounded-md px-3 py-2 shadow-lg z-50 w-45">
...
-          </div>
+          </button>
app/components/list/ListDetail.tsx-54-70 (1)

54-70: ⚠️ Potential issue | 🟡 Minor

<main> 중첩은 HTML5 표준 위반입니다.
HTML Living Standard에 따르면 문서 내 <main> 요소는 1개만 허용되며, <main> 요소 내에 또 다른 <main>을 중첩할 수 없습니다. 외부 컨테이너를 div 또는 section으로 변경해 주세요.

🔧 개선 예시
-    <main className="relative flex flex-col w-full h-full bg-white overflow-hidden">
+    <div className="relative flex flex-col w-full h-full bg-white overflow-hidden">
       {/* Top Header (Fixed) */}
       <header className="sticky top-0 z-50 flex items-center justify-between w-full h-16 py-5 px-5 bg-white border-b border-[`#E5E5E5`] flex-none">
         <h1 className="flex-1 text-[20px] font-bold text-[`#000000`] truncate">
           약수하이츠 11동
         </h1>
         <button
           onClick={onClose}
           className="flex-1 flex justify-end text-[`#000000`] hover:opacity-70 transition-opacity"
         >
           <X className="w-6 h-6" />
         </button>
       </header>

       {/* Main Scrollable Content */}
       <main className="flex-1 overflow-y-auto no-scrollbar scroll-smooth">
         <SummarySection />
-    </main>
+    </div>
app/components/mypage/CurationInfo.tsx-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor

사용되지 않는 MapPin import를 제거하세요.

ESLint 경고에서 확인된 바와 같이 MapPin이 import되었지만 사용되지 않습니다.

🔧 수정 제안
-import { ChevronDown, ChevronUp, MapPin } from "lucide-react";
+import { ChevronDown, ChevronUp } from "lucide-react";
app/components/curation/Curation.tsx-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor

사용되지 않는 useEffect import를 제거하세요.

ESLint 경고에서 확인된 바와 같이 useEffect가 import되었지만 사용되지 않습니다.

🔧 수정 제안
-import React, { useEffect } from "react";
+import React from "react";
app/map/page.tsx-1-6 (1)

1-6: ⚠️ Potential issue | 🟡 Minor

사용되지 않는 import를 제거하세요.

ReactFilterBar 모두 import되었지만 사용되지 않습니다. Next.js App Router에서는 React를 명시적으로 import할 필요가 없습니다.

또한 페이지가 빈 <div>만 반환하는데, layout.tsx의 문제와 연관되어 있습니다. layout이 children을 렌더링하지 않으므로 이 페이지 내용은 화면에 표시되지 않습니다.

🔧 수정 제안
-import React from "react";
-import FilterBar from "../components/map/FilterBar";
-
-const page = () => {
-  return <div></div>;
+export default function MapPage() {
+  return null;
-};
-
-export default page;
+}
app/components/map/FilterBar.tsx-135-139 (1)

135-139: ⚠️ Potential issue | 🟡 Minor

검색 입력에 접근성 라벨이 필요합니다.

시각적 텍스트만으로는 스크린리더가 입력 목적을 알 수 없습니다. aria-label 추가를 권장합니다.

🔧 제안 수정안
         <input
           type="text"
           placeholder="지역, 단지, 지하철역 등을 입력하세요"
+          aria-label="검색어 입력"
           className="w-[366px] h-12 pl-5 pr-12 bg-[`#F8FAFB`] border border-[`#E4E4E4`] rounded-lg text-[16px] text-[`#707070`] placeholder:text-[`#C4C4C4`] focus:outline-none focus:border-[`#30CEA1`] transition-colors"
         />
app/components/map/FilterBar.tsx-68-91 (1)

68-91: ⚠️ Potential issue | 🟡 Minor

가격 라벨의 ‘최대’ 기준이 월세/매매가와 불일치합니다.

현재 formatmax >= 100000일 때만 “최대”로 표시되어, 월세(최대 500만)는 “최대”가 표시되지 않고, 매매가(최대 100억)는 10억(100000)에서도 “최대”로 표시되는 문제가 있습니다. 각 범위의 상한값을 기준으로 판단하도록 분리하는 게 정확합니다.

🔧 제안 수정안
-    const format = (range: [number, number], unit: string) => {
+    const format = (
+      range: [number, number],
+      unit: string,
+      maxLimit: number,
+    ) => {
       const min = range[0];
       const max = range[1];
       const formattedMin =
         min >= 10000 ? `${(min / 10000).toFixed(0)}억` : `${min}만`;
       const formattedMax =
-        max >= 100000
+        max >= maxLimit
           ? "최대"
           : max >= 10000
             ? `${(max / 10000).toFixed(0)}억`
             : `${max}만`;
       return `${unit}${formattedMin}~${formattedMax}`;
     };

     const priceParts: string[] = [];
     if (selectedTypes.includes("전세") || selectedTypes.includes("월세")) {
-      priceParts.push(`보${format(depositRange, "")}`);
+      priceParts.push(`보${format(depositRange, "", 100000)}`);
     }
     if (selectedTypes.includes("월세")) {
-      priceParts.push(`월${format(monthlyRentRange, "")}`);
+      priceParts.push(`월${format(monthlyRentRange, "", 500)}`);
     }
     if (selectedTypes.includes("매매")) {
-      priceParts.push(`매${format(salePriceRange, "")}`);
+      priceParts.push(`매${format(salePriceRange, "", 1000000)}`);
     }
🧹 Nitpick comments (24)
app/signup/page.tsx (1)

10-10: 스크롤바 숨김 UX 영향만 한 번 체크해주세요.

Line 10처럼 no-scrollbar를 적용하면 스크롤 가능 영역의 발견성이 떨어질 수 있습니다. 필요 시 시각적 힌트(그라디언트/섀도)나 스크롤바 유지 옵션을 함께 고려해 주세요.

next.config.ts (1)

4-12: localhost 허용은 dev 한정으로 두는 편이 안전합니다.

Line 4-12의 설정이 프로덕션에 포함될 필요가 없다면 NODE_ENV 기반으로 제한해 두는 걸 권장합니다.

✅ 제안 변경안 (dev 한정)
 import type { NextConfig } from "next";

-const nextConfig: NextConfig = {
-  images: {
-    remotePatterns: [
-      {
-        protocol: "http",
-        hostname: "localhost",
-        port: "3845",
-      },
-    ],
-  },
-  /* config options here */
-};
+const isDev = process.env.NODE_ENV === "development";
+
+const nextConfig: NextConfig = {
+  images: isDev
+    ? {
+        remotePatterns: [
+          {
+            protocol: "http",
+            hostname: "localhost",
+            port: "3845",
+          },
+        ],
+      }
+    : undefined,
+  /* config options here */
+};
app/components/list/list_section/AgencySection.tsx (1)

24-71: 중개사 정보는 props로 주입되도록 분리하는 것을 권장합니다.
현재 하드코딩된 값은 실제 데이터 연동 시 재사용/테스트가 어려워집니다.

app/components/list/list_section/FacilitiesSection.tsx (1)

6-34: 정적 데이터는 컴포넌트 밖으로 분리하고 key는 label 사용 권장.
렌더마다 배열이 재생성되고 idx key는 향후 정렬 변경 시 UI 깨짐을 유발할 수 있습니다.

♻️ 제안 수정
-const FacilitiesSection = () => {
-  const options = [
+const options = [
     { label: "냉장고", icon: "/icons/option/refrigerator.svg" },
     ...
-  ];
-
-  const buildingFacilities = [
+  ];
+
+const buildingFacilities = [
     { label: "헬스장", icon: "/icons/option/gym.svg" },
     ...
-  ];
-
+  ];
+
+const FacilitiesSection = () => {
   return (
     <section className="px-5 py-8 bg-white" id="facilities">
       ...
-          {options.map((item, idx) => (
-            <div key={idx} className="flex flex-col items-center gap-2">
+          {options.map((item) => (
+            <div key={item.label} className="flex flex-col items-center gap-2">
...
-          {buildingFacilities.map((item, idx) => (
-            <div key={idx} className="flex flex-col items-center gap-2">
+          {buildingFacilities.map((item) => (
+            <div key={item.label} className="flex flex-col items-center gap-2">

Also applies to: 46-73

app/components/map/KakaoMap.tsx (1)

7-9: Kakao API 키 미설정 시 더 명확한 오류 메시지 표시 고려

현재 코드는 useKakaoLoader 훅의 error 상태(13-18줄)를 통해 이미 로더 실패를 처리하고 있습니다. 그러나 API 키가 미설정될 경우 "지도를 불러올 수 없습니다"라는 일반적인 오류 메시지만 표시되어 디버깅이 어렵습니다.

API 키 미설정을 명시적으로 감지하여 "Kakao 지도 API 키가 설정되지 않았습니다"와 같은 구체적인 메시지를 표시하면 개발 단계에서 원인 파악이 더 용이해집니다.

🛡️ 제안 수정
+  const appKey = process.env.NEXT_PUBLIC_KAKAO_MAP_API_KEY;
   const [loading, error] = useKakaoLoader({
-    appkey: process.env.NEXT_PUBLIC_KAKAO_MAP_API_KEY || "", // 발급받은 JavaScript 키
+    appkey: appKey ?? "", // 발급받은 JavaScript 키
   });
+
+  if (!appKey) {
+    return (
+      <div className="w-full h-full flex items-center justify-center bg-gray-100 text-red-500">
+        Kakao 지도 API 키가 설정되지 않았습니다.
+      </div>
+    );
+  }
app/components/list/list_section/ReviewSection.tsx (1)

16-22: fill 사용 시 sizes prop이 필요합니다.

Next.js 공식 문서에 따르면, fill을 사용할 때 sizes를 지정하지 않으면 브라우저가 100vw로 가정하여 불필요하게 큰 이미지를 다운로드할 수 있습니다. 제안된 sizes="100vw"는 브라우저 기본값과 동일하므로 개선 효과가 제한적입니다.

높이가 고정된(h-56) 상황이므로 두 가지 방안을 검토하세요:

  1. width/height prop 사용: 실제 크기를 알 수 있다면 fill 대신 명시적 크기 지정
  2. fill 유지 시: 컨테이너의 실제 너비 레이아웃에 맞는 sizes 지정 (예: sizes="100vw" 또는 반응형 레이아웃에 맞는 값)
app/components/mypage/MemberInfo.tsx (2)

9-24: 하드코딩된 사용자 데이터 확인 필요

username을 제외한 id, phone, email 필드가 모두 하드코딩되어 있습니다. 실제 사용자 데이터와 연동되어야 한다면 userStore에서 해당 필드들을 가져오거나, 의도적인 목업 데이터라면 주석으로 명시하는 것이 좋습니다.


47-75: 비제어 컴포넌트(defaultValue) 사용 시 주의사항

defaultValue를 사용하면 React가 입력값을 추적하지 않아 폼 제출 시 변경된 값을 가져오기 어렵습니다. 저장 기능 구현 시 useStatevalue/onChange를 사용하는 제어 컴포넌트 패턴이나, useRef로 값을 읽는 방식을 고려하세요.

app/components/list/ListItem.tsx (2)

100-117: 태그 스타일링 로직을 별도 유틸리티로 분리하세요.

중첩된 삼항 연산자가 가독성을 저하시킵니다. 태그별 스타일을 객체 매핑으로 분리하면 유지보수가 용이해집니다.

♻️ 권장 리팩토링
+const TAG_STYLES: Record<string, string> = {
+  소음: "border-[`#FBBA78`] text-[`#FBBA78`] bg-[`#FFFCF6`]",
+  접근성: "border-[`#7CB7CD`] text-[`#7CB7CD`] bg-[`#F7FCFE`]",
+  안전: "border-[`#F48787`] text-[`#F48787`] bg-[`#FFF7F7`]",
+  편의: "border-[`#AB9FD5`] text-[`#AB9FD5`] bg-[`#FAF9FD`]",
+  default: "border-[`#82AA82`] text-[`#82AA82`] bg-[`#F8FCF8`]",
+};
+
+const getTagStyle = (tag: string) => TAG_STYLES[tag] || TAG_STYLES.default;
           {property.tags.map((tag, idx) => (
             <span
               key={idx}
-              className={`px-3 py-1 text-[12px] rounded-full font-bold border ${
-                tag === "소음"
-                  ? "border-[`#FBBA78`] text-[`#FBBA78`] bg-[`#FFFCF6`]"
-                  : tag === "접근성"
-                    ? "border-[`#7CB7CD`] text-[`#7CB7CD`] bg-[`#F7FCFE`]"
-                    : tag === "안전"
-                      ? "border-[`#F48787`] text-[`#F48787`] bg-[`#FFF7F7`]"
-                      : tag === "편의"
-                        ? "border-[`#AB9FD5`] text-[`#AB9FD5`] bg-[`#FAF9FD`]"
-                        : "border-[`#82AA82`] text-[`#82AA82`] bg-[`#F8FCF8`]"
-              }`}
+              className={`px-3 py-1 text-[12px] rounded-full font-bold border ${getTagStyle(tag)}`}
             >

100-101: 배열 인덱스를 key로 사용하는 것은 권장되지 않습니다.

태그 순서가 변경되거나 동적으로 추가/삭제되는 경우, 인덱스 기반 key는 불필요한 리렌더링이나 상태 불일치를 유발할 수 있습니다. 태그 문자열 자체를 key로 사용하세요 (중복이 없다면).

♻️ 권장 수정
-          {property.tags.map((tag, idx) => (
-            <span
-              key={idx}
+          {property.tags.map((tag) => (
+            <span
+              key={tag}
app/components/list/list_section/detail/ContactModal.tsx (1)

17-21: 키보드 접근성 개선을 고려하세요.

현재 모달은 Escape 키로 닫기, 포커스 트랩(focus trap) 기능이 없습니다. 접근성(a11y) 향상을 위해 다음 기능 추가를 권장합니다:

  • Escape 키로 모달 닫기
  • 모달 열릴 때 첫 번째 포커스 가능 요소로 포커스 이동
  • role="dialog"aria-modal="true" 속성 추가
♻️ Escape 키 핸들러 예시
+import { useEffect } from "react";
+
 const ContactModal = ({ isOpen, onClose }: ContactModalProps) => {
+  useEffect(() => {
+    const handleEscape = (e: KeyboardEvent) => {
+      if (e.key === "Escape") onClose();
+    };
+    if (isOpen) {
+      document.addEventListener("keydown", handleEscape);
+      return () => document.removeEventListener("keydown", handleEscape);
+    }
+  }, [isOpen, onClose]);
+
   if (!isOpen) return null;
       <div
         className="fixed inset-0 z-150 flex items-center justify-center px-5 bg-black/50"
         onClick={onClose}
+        role="dialog"
+        aria-modal="true"
       >
app/components/list/list_section/DetailInfoSection.tsx (2)

23-31: 비표준 Tailwind aspect ratio 클래스 사용

aspect-401/300은 Tailwind CSS의 기본 클래스가 아닙니다. tailwind.config에 커스텀 설정이 없다면 이 클래스는 동작하지 않습니다.

♻️ 권장 수정안
-      <div className="relative w-full aspect-401/300 bg-white border border-[`#E5E5E5`] rounded-xl mb-8 overflow-hidden flex items-center justify-center p-4">
+      <div className="relative w-full aspect-[401/300] bg-white border border-[`#E5E5E5`] rounded-xl mb-8 overflow-hidden flex items-center justify-center p-4">

또는 Tailwind의 기본 aspect ratio 클래스(aspect-video, aspect-square 등)를 사용하세요.


47-51: subValue 로직 명확성 개선 필요

subValue"/"로 설정되어 있지만, 실제로는 사용되지 않고 단순히 "면적" 버튼 표시 여부를 결정합니다. boolean 플래그(showAreaButton)를 사용하는 것이 더 명확합니다.

♻️ 권장 수정안
     { label: "단지명", value: "약수하이츠" },
     { label: "주소", value: "서울시 중구 다산로10길 30 101동 1102호" },
     { label: "매물 형태", value: "아파트" },
-    { label: "전용/공용면적", value: "110.33m²/142.65m²", subValue: "/" },
+    { label: "전용/공용면적", value: "110.33m²/142.65m²", showAreaButton: true },
-              {detail.subValue && (
+              {detail.showAreaButton && (
app/components/list/list_section/SchoolItem.tsx (1)

22-22: 선택적 onClick에 대한 커서 스타일 고려

onClickundefined일 때도 cursor-pointer 클래스가 적용됩니다. 클릭 핸들러가 없을 때는 포인터 커서를 제거하는 것이 UX에 더 좋습니다.

♻️ 권장 수정안
-    <div className="group cursor-pointer relative" onClick={onClick}>
+    <div className={`group relative ${onClick ? 'cursor-pointer' : ''}`} onClick={onClick}>
app/components/list/list_section/detail/SchoolDetail.tsx (1)

207-218: 인라인 SVG 대신 lucide-react 아이콘 사용 권장

이미 lucide-react를 import하고 있으므로, 인라인 SVG 대신 ChevronDown 아이콘을 사용하면 일관성을 유지할 수 있습니다.

♻️ 권장 수정안

import 추가:

-import { X, ChevronLeft, Building2, Users } from "lucide-react";
+import { X, ChevronLeft, ChevronDown, Building2, Users } from "lucide-react";

SVG 교체:

           <button className="w-full h-14 border border-[`#E5E5E5`] rounded-xl flex items-center justify-center gap-2 text-[16px] font-bold text-black hover:bg-gray-50 transition-colors">
             배정 단지 더보기
-            <svg
-              width="20"
-              height="20"
-              viewBox="0 0 24 24"
-              fill="none"
-              stroke="currentColor"
-              strokeWidth="2"
-              strokeLinecap="round"
-              strokeLinejoin="round"
-            >
-              <path d="m6 9 6 6 6-6" />
-            </svg>
+            <ChevronDown className="w-5 h-5" />
           </button>
app/components/mypage/RecentlyViewedHouses.tsx (2)

73-76: 디버그용 console.log 및 주석 처리된 코드

console.log는 프로덕션 코드에서 제거해야 합니다. 주석 처리된 네비게이션 코드가 있다면 TODO 주석을 추가하거나 구현하는 것이 좋습니다.

♻️ 권장 수정안
   const handleHouseClick = (id: string) => {
-    console.log("Navigating to house detail:", id);
-    // window.location.href = `/property/${id}`;
+    // TODO: 매물 상세 페이지로 네비게이션 구현
+    // router.push(`/property/${id}`);
   };

103-136: 페이지네이션 UI가 비기능적

페이지네이션 버튼이 렌더링되지만 실제로 동작하지 않습니다. 현재 페이지 상태 관리나 클릭 핸들러가 없습니다. 추후 구현 예정이라면 TODO 주석을 추가하는 것이 좋습니다.

페이지네이션 기능 구현을 도와드릴까요? 또는 이 작업을 추적할 이슈를 생성할까요?

app/components/ui/RangeSlider.tsx (1)

29-29: 매직 스트링 사용 개선 필요

title !== "매매가" 체크는 매직 스트링에 의존하고 있어 유지보수가 어렵습니다. 단위 표시 로직을 더 명확하게 제어할 수 있는 방법을 고려하세요.

♻️ 권장 수정안

unitSuffix prop을 추가하여 단위를 명시적으로 전달:

 interface RangeSliderProps {
   title: string;
   range: [number, number];
   setRange: (value: [number, number]) => void;
   max: number;
   formatValue: (val: number) => string;
   showUnit?: boolean;
+  unitSuffix?: string;
 }

 export default function RangeSlider({
   title,
   range,
   setRange,
   max,
   formatValue,
   showUnit = true,
+  unitSuffix = "",
 }: RangeSliderProps) {
         <span className="text-[16px] font-bold text-[`#30CEA1`]">
           {formatValue(range[0])}
-          {showUnit && title !== "매매가" ? "원" : ""} ~ {formatValue(range[1])}
+          {showUnit ? unitSuffix : ""} ~ {formatValue(range[1])}{showUnit ? unitSuffix : ""}
         </span>
app/components/map/dropdown/SpaceDropdown.tsx (1)

6-9: 타입 일관성 개선 권장

setRange 타입이 React.Dispatch<React.SetStateAction<[number, number]>>로 정의되어 있지만, RangeSlider(value: [number, number]) => void를 기대합니다. 기능적으로는 호환되지만, 타입을 일치시키면 코드 일관성이 향상됩니다.

♻️ 권장 수정안
 interface SpaceDropdownProps {
   range: [number, number];
-  setRange: React.Dispatch<React.SetStateAction<[number, number]>>;
+  setRange: (value: [number, number]) => void;
 }
app/components/mypage/CurationInfo.tsx (1)

250-263: 저장 버튼이 콘솔에만 로깅하고 있습니다.

현재 console.log만 실행되며 실제 API 연동이 없습니다. 퍼블리싱 단계로 의도된 것이라면 TODO 주석을 추가하는 것이 좋겠습니다.

💡 제안
       <button
          onClick={() => {
+           // TODO: API 연동 필요
            console.log("Saving Curation Data:", {
              selectedPriorities,
              selectedRegions,
              selectedHousingType,
            });
          }}
app/components/mypage/ChangePassword.tsx (1)

35-54: 폼 입력 필드에 상태 관리와 접근성 속성이 누락되었습니다.

  1. 입력 필드가 제어되지 않는 컴포넌트(uncontrolled)입니다. 실제 기능 구현 시 useState로 값을 관리해야 합니다.
  2. <label><input> 연결을 위한 id/htmlFor 속성이 없어 접근성이 떨어집니다.
  3. name 속성이 없어 폼 제출 시 데이터 식별이 불가능합니다.
♻️ 접근성 개선 제안
          {[
-           { label: "현재 비밀번호", placeholder: "" },
-           { label: "새 비밀번호", placeholder: "" },
-           { label: "새 비밀번호 확인", placeholder: "" },
+           { label: "현재 비밀번호", name: "currentPassword" },
+           { label: "새 비밀번호", name: "newPassword" },
+           { label: "새 비밀번호 확인", name: "confirmPassword" },
          ].map((item) => (
-           <div key={item.label} className="flex gap-18 items-center w-full">
+           <div key={item.name} className="flex gap-18 items-center w-full">
              <div className="w-40 flex items-start shrink-0">
-               <span className="text-[20px] font-bold text-black leading-normal">
+               <label
+                 htmlFor={item.name}
+                 className="text-[20px] font-bold text-black leading-normal"
+               >
                  {item.label}
-               </span>
+               </label>
              </div>

              <input
+               id={item.name}
+               name={item.name}
                type="password"
                className="w-100 h-12 border border-gray-300 rounded-md px-4 outline-none focus:border-main-400 transition-colors"
              />
            </div>
          ))}
app/components/ui/Checkbox.tsx (1)

11-39: 접근성 개선이 필요합니다: 키보드 탐색 및 스크린 리더 지원 부족.

현재 구현은 <div>onClick을 사용하고 있어 다음 문제가 있습니다:

  1. 키보드로 포커스 및 토글 불가능 (Tab/Space/Enter)
  2. 스크린 리더가 체크박스로 인식하지 못함

숨겨진 native <input type="checkbox">를 사용하거나, 최소한 role, tabIndex, onKeyDown 속성을 추가하는 것이 좋습니다.

♿ 접근성 개선 제안 (hidden native input 사용)
 export default function Checkbox({ label, checked, onChange }: CheckboxProps) {
   return (
     <label className="flex items-center gap-2.5 cursor-pointer select-none group">
+      <input
+        type="checkbox"
+        checked={checked}
+        onChange={onChange}
+        className="sr-only"
+      />
       <div
-        onClick={onChange}
         className={`w-5 h-5 rounded border flex items-center justify-center transition-colors ${
           checked
             ? "bg-[`#30CEA1`] border-[`#30CEA1`]"
             : "border-[`#D9D9D9`] bg-white group-hover:border-[`#30CEA1`]"
         }`}
+        aria-hidden="true"
       >
app/components/mypage/MyPageSidebar.tsx (1)

6-13: MyPageMenu 타입 중복 정의는 드리프트 위험이 큽니다.

공통 타입을 한 곳에서 export하고 여기서는 import로 재사용하는 편이 안전합니다.

♻️ 제안 변경
-import { cn } from "@/app/lib/utils";
-
-export type MyPageMenu =
-  | "나의정보"
-  | "회원정보"
-  | "비밀번호 변경"
-  | "큐레이션 정보"
-  | "찜한 집"
-  | "최근 본 집"
-  | "D.HOME 리포트";
+import { cn } from "@/app/lib/utils";
+import type { MyPageMenu } from "@/app/mypage/MyPageContext";
app/mypage/MyPageContext.tsx (1)

5-12: MyPageMenu 타입 중복 정의는 드리프트 위험이 큽니다.

app/components/mypage/MyPageSidebar.tsx에도 동일한 유니온 타입이 별도로 정의되어 있어 메뉴 라벨 변경 시 불일치가 발생할 수 있습니다. 공용 타입 파일로 분리해 단일 소스로 관리하는 편이 안전합니다.

Comment thread .gitignore
# typescript
*.tsbuildinfo
next-env.d.ts
orval.config.ts No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# orval.config.ts가 이미 Git에 추적되는지 확인
echo "=== Git tracking status for orval.config.ts ==="
git ls-files -- orval.config.ts

# 생성물 경로가 이미 ignore되고 있는지 확인
echo ""
echo "=== Checking .gitignore for shared/api/generated ==="
rg -n "shared/api/generated" .gitignore

# .gitignore 전체 내용 확인 (라인 40-45 근처)
echo ""
echo "=== .gitignore context around line 42 ==="
cat -n .gitignore | sed -n '35,50p'

Repository: D-ingle/frontend

Length of output: 352


🏁 Script executed:

#!/bin/bash
# orval.config.ts 파일 존재 여부 및 내용 확인
echo "=== Checking orval.config.ts existence and content ==="
if [ -f "orval.config.ts" ]; then
  echo "File exists. First 30 lines:"
  head -30 orval.config.ts
else
  echo "orval.config.ts not found"
fi

# 생성된 API 파일이 있는지 확인
echo ""
echo "=== Looking for generated API files ==="
find . -type f -name "*generated*" -o -name "*api*" | grep -E "(src|shared|lib)" | head -20

# 전체 .gitignore 내용 확인
echo ""
echo "=== Full .gitignore content ==="
cat .gitignore

Repository: D-ingle/frontend

Length of output: 2384


orval.config.ts를 .gitignore에 추가하면 안 됩니다. 대신 생성된 파일들을 ignore하세요.

orval.config.ts는 API 클라이언트 재생성에 필요한 설정 파일이므로, Git에서 추적되어야 하고 모든 개발자/CI가 접근 가능해야 합니다. 현재 코드에서 확인된 생성 경로는 shared/api/generated/dingle.tsshared/api/generated/model이므로, 다음과 같이 변경하세요:

-orval.config.ts
+/shared/api/generated/
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
orval.config.ts
/shared/api/generated/
🤖 Prompt for AI Agents
In @.gitignore at line 42, Remove orval.config.ts from .gitignore so the config
file remains tracked (it is required to regenerate API clients), and instead add
the generated output paths to .gitignore: ignore the specific generated client
file shared/api/generated/dingle.ts and the generated models directory
shared/api/generated/model (or a pattern such as shared/api/generated/**) so
generated artifacts are not committed while keeping orval.config.ts under
version control.

Comment on lines +3 to +20
import React from "react";
import Image from "next/image";
import Link from "next/link";
import { cn } from "@/app/lib/utils";
import { useEffect, useState } from "react";
import { useUserStore } from "@/app/store/userStore";
import { logoutAction } from "@/app/login/actions";
import { useRouter } from "next/navigation";

export default function Navbar() {
const router = useRouter();
const { user, clearUser } = useUserStore();
const [mounted, setMounted] = useState(false);

// hydration 에러 방지 (client-side의 localStorage 데이터가 서버 렌더링 결과와 다를 수 있음)
useEffect(() => {
setMounted(true);
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

hydration guard useSyncExternalStore React best practice

💡 Result:

A “hydration guard” (e.g., const [mounted,setMounted]=useState(false); useEffect(()=>setMounted(true),[]) and rendering null until mounted) avoids hydration mismatches by skipping SSR output—but it also throws away SSR benefits (content/SEO/TTFB) and is usually a last resort.

Best practice: if the mismatch is caused by reading client-only state (media query, localStorage, time, window size, etc.), prefer useSyncExternalStore with getServerSnapshot so the server render and the hydration render use the same snapshot, and then React can switch to the real client snapshot after hydration. This is exactly what the third argument is for. [1][2]

Recommended pattern (replace “hydration guard” with SSR-safe snapshots)

import { useSyncExternalStore } from "react";

function subscribeNoop() {
  return () => {};
}

function useClientOnlyValue<T>(getClient: () => T, getServer: () => T) {
  return useSyncExternalStore(subscribeNoop, getClient, getServer);
}

// Example: prefers-reduced-motion
function usePrefersReducedMotion() {
  return useClientOnlyValue(
    () => window.matchMedia("(prefers-reduced-motion: reduce)").matches,
    () => false // must match what the server rendered during hydration
  );
}

Using a “no-op store” subscription is a known/accepted approach when you’re not truly subscribing to an external store, but want the SSR/hydration snapshot split. [2]

Practical rules of thumb

  • Always provide getServerSnapshot in SSR; omitting it can cause SSR/hydration problems (and is a common pitfall). [1][3]
  • getSnapshot must be pure + cached/stable: if nothing changed, return the exact same value (especially for objects/arrays) or React will warn/re-render excessively. [1]
  • Keep subscribe / getSnapshot stable (define outside the component or memoize) to avoid unnecessary re-subscribes. [4]

If you share what your current “hydration guard” is protecting (e.g., theme from localStorage, media queries, date/time), I can sketch the exact getServerSnapshot/getSnapshot pair for it.

Sources: React docs on useSyncExternalStore and snapshot caching/SSR semantics [1], TkDodo on avoiding hydration mismatches with getServerSnapshot (including no-op subscription) [2], React issue discussing SSR usage pitfalls [3], Epic React best-practices around stable functions and snapshots [4].

References: [1] (react.dev) [2] (tkdodo.eu) [3] (github.com) [4] (epicreact.dev)

Citations:


🌐 Web query:

react-hooks/set-state-in-effect lint error useEffect setState hydration

💡 Result:

The react-hooks/set-state-in-effect lint error is triggered when you call a useState setter synchronously inside useEffect (e.g. useEffect(() => setX(...), [...])). React’s hook lint docs flag this because it forces an extra render pass and is often “derived state” that can be computed during render instead. [1]

Fix patterns (pick the one that matches your case)

1) If the state is derived from props/state: don’t store it—derive it

function Component({ selectedId, items }) {
  const selected = items.find(i => i.id === selectedId);
  return <div>{selected?.name}</div>;
}

This is exactly what the rule wants. [1]

2) If you were “initializing” state in an effect: use a lazy useState initializer

Works when the value is available on first client render (and you can safely guard window):

const [theme, setTheme] = useState(() => {
  if (typeof window === "undefined") return "light"; // SSR fallback
  return localStorage.getItem("theme") ?? "light";
});

This often eliminates the effect entirely (and the lint error). [1]

3) If it’s a hydration / SSR case (client-only data like localStorage) and you must update after mount

You generally have three sane options:

A. Render a consistent SSR fallback, then update after mount (accept the extra render)

const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);

if (!mounted) return null; // or skeleton that matches SSR
return <ClientOnlyStuff />;

If the lint rule is blocking you, you may need to disable it locally for this line/component (because you are intentionally doing client-only initialization).

B. Use a subscription-style API for external state (preferred when the value can change and you want correctness)
React tooling ecosystems commonly recommend moving “external store” reads away from “setState in effect”; some lint rule sets explicitly push toward this model. [2]

C. If you’re measuring layout (DOM size), use useLayoutEffect instead
React’s lint docs treat layout measurement as a valid case. [1]

Why this relates to hydration

On SSR, the server can’t read browser-only sources (like localStorage). If your first client render uses different values than the server HTML, you can get hydration mismatches—so you either:

  • render a stable fallback until mount, or
  • accept a post-mount update (often via an effect), or
  • model it as an external store/subscription.

Sources: React lint rule docs for set-state-in-effect [1]; ESLint React rule description of “no direct setState in useEffect” [2].


🌐 Web query:

Next.js hydration guard pattern 2025 useSyncExternalStore

💡 Result:

A solid “hydration guard” in Next.js (App Router) is to use useSyncExternalStore with a getServerSnapshot so React forces a stable value during SSR + the hydration pass, then immediately switches to the client value after hydration—avoiding mismatches without the “isMounted + useEffect” flicker pattern.

Pattern: useIsHydrating() (or useIsServerRender())

'use client'
import * as React from 'react'

function useIsHydrating() {
  return React.useSyncExternalStore(
    () => () => {}, // no-op subscription
    () => false,    // after hydration: "not hydrating"
    () => true      // SSR + hydration pass: "hydrating"
  )
}

export function HydrationGuard({ fallback, children }: {
  fallback: React.ReactNode
  children: React.ReactNode
}) {
  const isHydrating = useIsHydrating()
  return isHydrating ? fallback : children
}

Why it works

  • React uses the 3rd arg (getServerSnapshot) during server rendering and during hydration, and it must match what the server rendered. This is specifically what prevents hydration mismatches. [1]
  • This behavior was introduced to handle concurrent / selective hydration where the external store could change mid-hydration; React will read the “server snapshot” during hydration, then re-render with the client snapshot helping correctness. [2]
  • You can use a no-op store because you only need the server-vs-client split; you’re not actually subscribing to updates. [3]
  • This is widely described as a “concurrent hydration” trick: render one thing for SSR/hydration, then immediately render the real client thing. [4]

When to use it in Next.js

Use this guard around UI that depends on client-only state (e.g., localStorage, theme, media queries, extensions-injected DOM, Date.now(), random IDs) where you want SSR output to be deterministic and avoid hydration errors.

Sources: React docs on useSyncExternalStore/getServerSnapshot [1], React 18 working group explanation of hydration snapshot guarantees [2], TkDodo’s hydration-mismatch guard approach [3], Jacob Groß’s concurrent hydration example/pattern [4].


hydration guard 구현이 린트 에러로 CI를 깨뜨립니다

useEffect에서 즉시 setMounted를 호출해 react-hooks/set-state-in-effect 에러가 발생합니다. useSyncExternalStore 기반의 클라이언트 여부 체크로 바꾸면 린트를 통과하면서 추가 렌더링 없이 동일한 목적을 달성할 수 있습니다.

🛠️ 대체 구현
-import React from "react";
+import React, { useSyncExternalStore } from "react";
 ...
-import { useEffect, useState } from "react";
 ...
-  const [mounted, setMounted] = useState(false);
-
-  // hydration 에러 방지 (client-side의 localStorage 데이터가 서버 렌더링 결과와 다를 수 있음)
-  useEffect(() => {
-    setMounted(true);
-  }, []);
+  // hydration 에러 방지 (client-side의 localStorage 데이터가 서버 렌더링 결과와 다를 수 있음)
+  const mounted = useSyncExternalStore(
+    () => () => {},
+    () => true,
+    () => false,
+  );

이 패턴은 React 공식 권장 사항으로, 서버 렌더링과 하이드레이션 단계에서는 false를 반환하고 클라이언트 렌더링 후에는 true를 반환하여 하이드레이션 불일치를 방지합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import React from "react";
import Image from "next/image";
import Link from "next/link";
import { cn } from "@/app/lib/utils";
import { useEffect, useState } from "react";
import { useUserStore } from "@/app/store/userStore";
import { logoutAction } from "@/app/login/actions";
import { useRouter } from "next/navigation";
export default function Navbar() {
const router = useRouter();
const { user, clearUser } = useUserStore();
const [mounted, setMounted] = useState(false);
// hydration 에러 방지 (client-side의 localStorage 데이터가 서버 렌더링 결과와 다를 수 있음)
useEffect(() => {
setMounted(true);
}, []);
import React, { useSyncExternalStore } from "react";
import Image from "next/image";
import Link from "next/link";
import { cn } from "@/app/lib/utils";
import { useUserStore } from "@/app/store/userStore";
import { logoutAction } from "@/app/login/actions";
import { useRouter } from "next/navigation";
export default function Navbar() {
const router = useRouter();
const { user, clearUser } = useUserStore();
// hydration 에러 방지 (client-side의 localStorage 데이터가 서버 렌더링 결과와 다를 수 있음)
const mounted = useSyncExternalStore(
() => () => {},
() => true,
() => false,
);
🧰 Tools
🪛 ESLint

[error] 19-19: 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/components/layout/Navbar.tsx:19:5
17 | // hydration 에러 방지 (client-side의 localStorage 데이터가 서버 렌더링 결과와 다를 수 있음)
18 | useEffect(() => {

19 | setMounted(true);
| ^^^^^^^^^^ Avoid calling setState() directly within an effect
20 | }, []);
21 |
22 | const handleLogout = async () => {

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

🪛 GitHub Actions: Frontend CI

[error] 19-19: ESLint: Calling setState synchronously within an effect can trigger cascading renders. Avoid calling setMounted(true) directly within the effect.

🤖 Prompt for AI Agents
In `@app/components/layout/Navbar.tsx` around lines 3 - 20, Replace the
useEffect/setMounted hydration guard in Navbar with a useSyncExternalStore-based
client check: remove the useEffect and mounted state, and instead call
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) where
getSnapshot returns typeof window !== "undefined" (client true) and
getServerSnapshot returns false, and subscribe is a no-op/unsubscribe function;
update code references from mounted to the new isClient value and keep the rest
of Navbar (user, clearUser, router, etc.) unchanged.

return (
<Portal>
<div
className="fixed inset-0 z-150 flex items-center justify-center px-5 bg-black/50"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find tailwind config files and check structure
find . -name "tailwind.config*" -type f | head -20

Repository: D-ingle/frontend

Length of output: 42


🏁 Script executed:

# Check package.json for dependencies
cat package.json | grep -A 5 -B 5 "tailwindcss\|animate"

Repository: D-ingle/frontend

Length of output: 487


🏁 Script executed:

# Search for where these custom classes might be defined
rg "z-150|animate-in|fade-in|zoom-in" --type ts --type tsx --type js --type css -A 2 -B 2 | head -50

Repository: D-ingle/frontend

Length of output: 87


🏁 Script executed:

# Look at the ContactModal.tsx file to understand context
cat -n app/components/list/list_section/detail/ContactModal.tsx | sed -n '15,30p'

Repository: D-ingle/frontend

Length of output: 799


🏁 Script executed:

# Find tailwind config files
fd "tailwind.config" --type f

Repository: D-ingle/frontend

Length of output: 42


🏁 Script executed:

# If found, read the config
cat -n tailwind.config.ts 2>/dev/null || cat -n tailwind.config.js 2>/dev/null || echo "Config not found in root"

Repository: D-ingle/frontend

Length of output: 84


🏁 Script executed:

# Also check for theme extensions or global CSS that might define these
fd "globals.css|tailwind.css" --type f

Repository: D-ingle/frontend

Length of output: 75


🏁 Script executed:

# Read the globals.css file
cat -n app/globals.css

Repository: D-ingle/frontend

Length of output: 1114


🏁 Script executed:

# Search more broadly for tailwind config files anywhere in the repo
find . -name "*tailwind*" -type f 2>/dev/null | grep -v node_modules

Repository: D-ingle/frontend

Length of output: 42


🏁 Script executed:

# Check if there's a postcss or other config that might define these
find . -maxdepth 2 -name "*.config.*" -type f 2>/dev/null | grep -v node_modules

Repository: D-ingle/frontend

Length of output: 135


🏁 Script executed:

# Check postcss config for Tailwind plugins
cat -n postcss.config.mjs

Repository: D-ingle/frontend

Length of output: 202


🏁 Script executed:

# Check next.config for Tailwind settings
cat -n next.config.ts

Repository: D-ingle/frontend

Length of output: 446


🏁 Script executed:

# Search for all custom class definitions in the codebase
rg "@layer|@apply" app/ --type css --type ts -B 2 -A 2 | head -100

Repository: D-ingle/frontend

Length of output: 217


🏁 Script executed:

# Check if there are any CSS files with custom class definitions
find app -name "*.css" -type f 2>/dev/null

Repository: D-ingle/frontend

Length of output: 75


🏁 Script executed:

# Search for theme customizations that might define these classes
rg "@theme|--" app/globals.css -A 20

Repository: D-ingle/frontend

Length of output: 768


🏁 Script executed:

# Check if there's a tailwind config in package.json or tsconfig
cat -n tsconfig.json 2>/dev/null | head -30

Repository: D-ingle/frontend

Length of output: 883


🏁 Script executed:

# Look for any other CSS files that might have these definitions
find . -path ./node_modules -prune -o -name "*.css" -type f -print 2>/dev/null | xargs grep -l "z-150\|animate-in\|max-w-100\|h-18.5" 2>/dev/null

Repository: D-ingle/frontend

Length of output: 44


🏁 Script executed:

# Check Tailwind v4 documentation - search for references to these classes in comments or code
rg "z-\[150\]|z-150" app/ --type ts -B 2 -A 2

Repository: D-ingle/frontend

Length of output: 492


Tailwind 커스텀 클래스 미정의로 인한 스타일 미적용

z-150, animate-in, fade-in, zoom-in, max-w-100, h-18.5, pt-7.5 등의 클래스가 프로젝트에 정의되어 있지 않습니다.

  • tailwindcss-animate 플러그인이 설치되지 않았으며, @theme 블록에도 이들 클래스가 정의되어 있지 않습니다
  • Tailwind v4의 기본 z-값 범위는 z-0~z-50이므로 z-150은 기본 설정에 없습니다
  • max-w-100, h-18.5 등의 커스텀 사이즈값도 정의되지 않았습니다

tailwindcss-animate 플러그인을 설치하고, 커스텀 값들을 위한 tailwind.config.ts 파일을 생성하여 extend.theme에 정의하거나 app/globals.css@theme 블록에 추가하세요.

🤖 Prompt for AI Agents
In `@app/components/list/list_section/detail/ContactModal.tsx` at line 19, The
ContactModal component uses non-existent Tailwind classes (e.g., "z-150",
"animate-in", "fade-in", "zoom-in", "max-w-100", "h-18.5", "pt-7.5") in the
className on ContactModal.tsx; fix it by either installing and enabling the
tailwindcss-animate plugin and adding it to the plugins array (so animate-*
classes are available) and/or defining the custom z, spacing and size tokens in
your tailwind.config.ts under theme.extend (or in the `@theme` block in
app/globals.css) so "z-150", "max-w-100", "h-18.5", "pt-7.5" resolve, and
alternatively replace "z-150" with a built-in z-index like "z-50" if you prefer
not to add custom z-values; update ContactModal.tsx className accordingly after
making these config/plugin changes.

Comment on lines +98 to +123
<div
onClick={() => toggleSelection(report.id)}
className={cn(
"w-5 h-5 flex items-center justify-center rounded-sm border cursor-pointer transition-colors shrink-0",
isSelected
? "bg-main-400 border-main-400"
: "bg-white border-gray-300",
)}
>
{isSelected && (
<svg
width="12"
height="9"
viewBox="0 0 12 9"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 4L4.5 7.5L11 1"
stroke="white"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
)}
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the file structure and locate the file
find . -name "HomeReport.tsx" -type f

Repository: D-ingle/frontend

Length of output: 98


🏁 Script executed:

# Read the specific file to understand the context around lines 98-123
wc -l app/components/mypage/HomeReport.tsx

Repository: D-ingle/frontend

Length of output: 100


🏁 Script executed:

# Read a larger section around lines 98-123 to understand the full context
sed -n '80,140p' app/components/mypage/HomeReport.tsx

Repository: D-ingle/frontend

Length of output: 2621


🏁 Script executed:

# Look for the toggleSelection function to understand how it's implemented
rg "toggleSelection" app/components/mypage/HomeReport.tsx -B 3 -A 3

Repository: D-ingle/frontend

Length of output: 706


🏁 Script executed:

# Check for any onKeyDown or keyboard event handling on this div or similar elements
rg "onKeyDown|onKeyPress|aria-" app/components/mypage/HomeReport.tsx -B 2 -A 2

Repository: D-ingle/frontend

Length of output: 42


🏁 Script executed:

# Check if there are any keyboard event handlers at the component level or parent
sed -n '1,80p' app/components/mypage/HomeReport.tsx

Repository: D-ingle/frontend

Length of output: 2465


🏁 Script executed:

# Check if there's a custom checkbox component defined elsewhere that might be used
rg "checkbox" app/components/mypage/ -l

Repository: D-ingle/frontend

Length of output: 133


🏁 Script executed:

# Verify the complete implementation of the checkbox section
sed -n '98,130p' app/components/mypage/HomeReport.tsx

Repository: D-ingle/frontend

Length of output: 1458


커스텀 체크박스가 키보드로 접근 불가능합니다.

div 요소는 onClick만 지원하므로 Tab 키로 포커스되지 않고 Space/Enter로 선택할 수 없습니다. 키보드 사용자와 스크린 리더 사용자가 일괄 다운로드 기능을 사용할 수 없으므로, button(role="checkbox") 또는 input[type=checkbox]로 변경해 주세요.

🔧 개선 예시
-                  <div
-                    onClick={() => toggleSelection(report.id)}
-                    className={cn(
-                      "w-5 h-5 flex items-center justify-center rounded-sm border cursor-pointer transition-colors shrink-0",
-                      isSelected
-                        ? "bg-main-400 border-main-400"
-                        : "bg-white border-gray-300",
-                    )}
-                  >
+                  <button
+                    type="button"
+                    role="checkbox"
+                    aria-checked={isSelected}
+                    aria-label={`${report.title} 선택`}
+                    onClick={() => toggleSelection(report.id)}
+                    className={cn(
+                      "w-5 h-5 flex items-center justify-center rounded-sm border cursor-pointer transition-colors shrink-0",
+                      isSelected
+                        ? "bg-main-400 border-main-400"
+                        : "bg-white border-gray-300",
+                    )}
+                  >
...
-                  </div>
+                  </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
onClick={() => toggleSelection(report.id)}
className={cn(
"w-5 h-5 flex items-center justify-center rounded-sm border cursor-pointer transition-colors shrink-0",
isSelected
? "bg-main-400 border-main-400"
: "bg-white border-gray-300",
)}
>
{isSelected && (
<svg
width="12"
height="9"
viewBox="0 0 12 9"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 4L4.5 7.5L11 1"
stroke="white"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
)}
</div>
<button
type="button"
role="checkbox"
aria-checked={isSelected}
aria-label={`${report.title} 선택`}
onClick={() => toggleSelection(report.id)}
className={cn(
"w-5 h-5 flex items-center justify-center rounded-sm border cursor-pointer transition-colors shrink-0",
isSelected
? "bg-main-400 border-main-400"
: "bg-white border-gray-300",
)}
>
{isSelected && (
<svg
width="12"
height="9"
viewBox="0 0 12 9"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 4L4.5 7.5L11 1"
stroke="white"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
)}
</button>
🤖 Prompt for AI Agents
In `@app/components/mypage/HomeReport.tsx` around lines 98 - 123, Replace the
non-focusable div checkbox in HomeReport.tsx with an accessible control: use a
semantic input[type="checkbox"] or a button with role="checkbox" (referencing
toggleSelection, report.id and isSelected) so it is keyboard-focusable and
operable; ensure it exposes aria-checked={isSelected}, proper tabIndex, and
calls toggleSelection(report.id) on click and onKeyDown (Space/Enter) if you
choose a button, and preserve the existing className logic
(bg-main-400/border-main-400 vs bg-white/border-gray-300) so visuals remain the
same.

{variant === "checkbox" && (
<div
className={cn(
"absolute top-17 left-5 w-5 h-5 flex items-center justify-center rounded-sm border transition-colors",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Tailwind 설정 파일 찾기
fd -type f \( -name 'tailwind.config.*' -o -name 'tailwind.js' \) 

# 프로젝트 구조 확인
git ls-files | head -20

Repository: D-ingle/frontend

Length of output: 1052


🏁 Script executed:

#!/bin/bash
# top-17이 코드베이스에서 사용되는 곳 확인
rg "top-17" --type ts --type tsx --type js --type css

Repository: D-ingle/frontend

Length of output: 87


🏁 Script executed:

#!/bin/bash
# HouseCard.tsx 파일 확인
cat -n app/components/mypage/HouseCard.tsx

Repository: D-ingle/frontend

Length of output: 3705


🏁 Script executed:

#!/bin/bash
# 루트 디렉토리에서 Tailwind 설정 파일 찾기
find . -maxdepth 2 -name 'tailwind.config.*' -o -name 'postcss.config.*' | head -20

Repository: D-ingle/frontend

Length of output: 80


🏁 Script executed:

#!/bin/bash
# top-17 클래스가 코드베이스에서 사용되는 모든 곳 확인
rg "top-17"

Repository: D-ingle/frontend

Length of output: 210


🏁 Script executed:

#!/bin/bash
# 프로젝트 루트의 주요 설정 파일 목록
ls -la | grep -E 'tailwind|postcss|next\.config'

Repository: D-ingle/frontend

Length of output: 199


🏁 Script executed:

#!/bin/bash
# globals.css 또는 기타 CSS 파일에서 커스텀 spacing 정의 확인
find . -name '*.css' -type f | head -10 | xargs grep -l "top-17\|--spacing\|extend" 2>/dev/null || echo "No CSS files with custom spacing found"

Repository: D-ingle/frontend

Length of output: 141


🏁 Script executed:

#!/bin/bash
# postcss.config.mjs 파일 내용 확인
cat -n postcss.config.mjs

Repository: D-ingle/frontend

Length of output: 202


🏁 Script executed:

#!/bin/bash
# 전체 repository에서 tailwind 관련 설정 찾기
rg "tailwind|extend.*theme" --type-list | head -20 || rg "tailwind|extend.*theme" | head -20

Repository: D-ingle/frontend

Length of output: 449


🏁 Script executed:

#!/bin/bash
# package.json에서 tailwind 버전 확인
cat package.json | grep -A5 -B5 "tailwindcss"

Repository: D-ingle/frontend

Length of output: 487


🏁 Script executed:

#!/bin/bash
# 프로젝트 전체에서 tailwind.config 파일 찾기 (hidden 포함)
find . -name '*tailwind*' -type f 2>/dev/null

Repository: D-ingle/frontend

Length of output: 42


🏁 Script executed:

#!/bin/bash
# CSS 파일 찾기 (globals, app, styles 등)
find . -name 'globals.css' -o -name 'app.css' -o -name 'styles.css' 2>/dev/null

Repository: D-ingle/frontend

Length of output: 77


🏁 Script executed:

#!/bin/bash
# layout.tsx에서 CSS import 확인
find . -name 'layout.tsx' -exec cat {} \; 2>/dev/null | head -20

Repository: D-ingle/frontend

Length of output: 469


🏁 Script executed:

#!/bin/bash
# Tailwind v4의 기본 spacing 값 확인 및 top-17, w-30, h-30 클래스가 생성되는지 검증
# 프로젝트에서 tailwind 관련 모든 config 찾기
rg "@layer|extend:" --max-count 20

Repository: D-ingle/frontend

Length of output: 94


🏁 Script executed:

#!/bin/bash
# globals.css 전체 내용 확인
cat -n app/globals.css

Repository: D-ingle/frontend

Length of output: 1114


비표준 Tailwind spacing 클래스 사용 - 커스텀 설정 없음

top-17, w-30, h-30 같은 클래스들은 Tailwind CSS v4의 기본 spacing scale에 없으며, 프로젝트의 globals.css에도 정의되어 있지 않습니다. 이 클래스들은 CSS를 생성하지 않아 체크박스 위치와 이미지 크기가 제대로 적용되지 않을 것입니다. Tailwind 설정에서 이 값들을 명시적으로 확장하거나 인라인 스타일을 사용해야 합니다.

🤖 Prompt for AI Agents
In `@app/components/mypage/HouseCard.tsx` at line 47, The Tailwind classes like
top-17, w-30 and h-30 used in the HouseCard component's class string are not
part of the default spacing scale and aren't defined in globals.css, so they
won't generate CSS; either replace those tokens in the HouseCard JSX (look for
the class string containing "absolute top-17 left-5 w-5 h-5 ...") with valid
Tailwind spacing classes (e.g., top-16/top-20, w-8/w-32, h-8/h-32) that match
the intended layout, or add explicit spacing keys to your Tailwind config under
theme.extend.spacing (e.g., "17": "4.25rem", "30": "7.5rem") and rebuild
Tailwind so top-17/w-30/h-30 generate styles—pick one approach and update the
class string or tailwind.config.js accordingly.

Comment thread app/map/layout.tsx
Comment on lines +5 to +20
const layout = () => {
return (
<div className="flex flex-col h-screen overflow-hidden">
<FilterBar />
<div className="flex-1 flex overflow-hidden">
<div className="w-100 h-full z-10">
<List />
</div>
{/* Map area */}
<div className="flex-1 relative">
<KakaoMap />
</div>
</div>
</div>
);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Next.js App Router의 layout 규칙을 따르지 않습니다.

Next.js App Router에서 layout.tsx는 반드시 children prop을 받아서 렌더링해야 합니다. 현재 구현은 children을 무시하고 있어 page.tsx의 내용이 렌더링되지 않습니다.

만약 현재 구조가 의도된 것이라면, 이 파일을 layout이 아닌 일반 컴포넌트로 분리하고 page.tsx에서 사용하는 것이 올바른 패턴입니다.

🔧 수정 제안 (layout 패턴 유지 시)
+import { ReactNode } from "react";
 import FilterBar from "../components/map/FilterBar";
 import List from "../components/list/List";
 import KakaoMap from "../components/map/KakaoMap";

-const layout = () => {
+export default function MapLayout({ children }: { children: ReactNode }) {
   return (
     <div className="flex flex-col h-screen overflow-hidden">
       <FilterBar />
       <div className="flex-1 flex overflow-hidden">
         <div className="w-100 h-full z-10">
           <List />
         </div>
         {/* Map area */}
         <div className="flex-1 relative">
           <KakaoMap />
+          {children}
         </div>
       </div>
     </div>
   );
-};
-
-export default layout;
+}
🤖 Prompt for AI Agents
In `@app/map/layout.tsx` around lines 5 - 20, The layout function currently
ignores the required Next.js App Router children prop, so update the exported
layout component (layout) to accept and render a children prop (e.g., function
signature (props: { children: React.ReactNode }) or ({ children })) and place
{children} where page content should be rendered (for example inside the
list/map container or replacing <List /> if appropriate); alternatively, if you
intend this to be a regular component rather than an app layout, rename/remove
the layout export and move this JSX into a normal component used by page.tsx
(e.g., MapShell) and ensure page.tsx imports and renders FilterBar, List,
KakaoMap and the page content accordingly.

Comment thread app/store/userStore.ts
Comment on lines +41 to +42
name: "dhome-user-storage", // localStorage에 저장될 키 이름
storage: createJSONStorage(() => localStorage), // 저장소로 localStorage 사용

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -type f -name "userStore.ts" | head -5

Repository: D-ingle/frontend

Length of output: 230


🌐 Web query:

zustand persist middleware skipHydration Next.js SSR hydration mismatch

💡 Result:

When you use persist with localStorage in Next.js SSR, the server always renders with the default initial state (no access to localStorage), but the client can immediately rehydrate from storage and render different UI during hydration—causing a hydration mismatch (server HTML ≠ first client render). (zustand.docs.pmnd.rs)

Fix: skipHydration: true + manually rehydrate() on the client

Zustand persist has skipHydration. If true, Zustand won’t auto-rehydrate on initialization; you manually trigger it on the client (e.g., in a useEffect). This is explicitly recommended for SSR apps. (zustand.docs.pmnd.rs)

Store

import { create } from "zustand";
import { persist } from "zustand/middleware";

type State = { theme: "light" | "dark"; setTheme: (t: State["theme"]) => void };

export const useAppStore = create<State>()(
  persist(
    (set) => ({
      theme: "light",
      setTheme: (theme) => set({ theme }),
    }),
    {
      name: "app",
      skipHydration: true, // important for SSR
    }
  )
);

Client-only hydrator (App Router: put in app/layout.tsx)

"use client";

import { useEffect } from "react";
import { useAppStore } from "./store";

export function StoreHydrator() {
  useEffect(() => {
    useAppStore.persist.rehydrate();
  }, []);
  return null;
}

Then render it once near the root:

// app/layout.tsx
import { StoreHydrator } from "./StoreHydrator";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <StoreHydrator />
        {children}
      </body>
    </html>
  );
}

This pattern is commonly used in Next.js + Zustand to avoid SSR hydration mismatches by ensuring the first client render matches the server, and only then applying persisted state. (zustand.docs.pmnd.rs)

Citations:


🏁 Script executed:

cat -n app/store/userStore.ts

Repository: D-ingle/frontend

Length of output: 1389


SSR 하이드레이션 불일치 문제를 해결하기 위해 skipHydration 옵션 적용 필요

Next.js의 SSR 환경에서 localStorage를 사용하는 현재 코드는 서버에서는 기본 상태로 렌더링되고 클라이언트에서는 localStorage에서 자동으로 상태를 복원하게 되어 하이드레이션 불일치가 발생할 수 있습니다.

Zustand의 공식 권장 방식에 따라 다음과 같이 수정하세요:

  1. persist 옵션에 skipHydration: true 추가
  2. 클라이언트 컴포넌트에서 useEffect를 통해 useUserStore.persist.rehydrate() 수동 호출

이를 통해 서버와 클라이언트의 초기 렌더링을 일치시키고, 클라이언트에서만 localStorage 상태를 적용할 수 있습니다.

🤖 Prompt for AI Agents
In `@app/store/userStore.ts` around lines 41 - 42, Add the persist skipHydration
flag and manual rehydrate call to avoid SSR hydration mismatches: update the
persist config where createJSONStorage is used to include skipHydration: true
(so the persisted localStorage state is not applied during server render), and
in the client-only component lifecycle call useUserStore.persist.rehydrate()
inside a useEffect to restore state on the client; reference the persist config
that contains createJSONStorage(...) and the useUserStore.persist.rehydrate()
API when making these changes.

Comment thread app/types/login.ts
Comment on lines +3 to +6
export const loginSchema = z.object({
userId: z.string().min(1, "아이디를 입력해주세요."),
password: z.string().min(1, "비밀번호를 입력해주세요."),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Zod v4 string min() method second argument error message format

💡 Result:

In Zod v4, z.string().min()’s 2nd argument is an options object whose key is error (not message):

import * as z from "zod/v4";

z.string().min(5, { error: "Must be at least 5 characters" });

error can be either a string or an error map function:

z.string().min(5, {
  error: (iss) => iss.input === undefined ? "Required" : "Too short",
});

Zod v4 unified the old v3 message + errorMap into a single error option, so { message: "..." } will cause a type/error mismatch in v4 typings. [1][2]

Sources:
[1] Zod “Customizing errors” (v4 error param, includes .min(..., { error: ... })) (zod.dev)
[2] Zod v3→v4 codemod notes (messageerror for .min) (hypermod.io)

Citations:


🏁 Script executed:

# Check Zod version in package.json
cd app 2>/dev/null || cd . 
find . -name "package.json" -type f | head -5 | xargs grep -l "zod" | head -1 | xargs cat | grep -A2 -B2 '"zod"'

Repository: D-ingle/frontend

Length of output: 42


🏁 Script executed:

# Also check for package-lock.json or yarn.lock to see exact version
find . -name "package-lock.json" -o -name "yarn.lock" | head -1 | xargs grep -A5 '"zod"' | head -20

Repository: D-ingle/frontend

Length of output: 42


🏁 Script executed:

# Search for all .min() usages in the codebase to see pattern consistency
rg "\.min\(" --type ts --type js -A 1 | head -50

Repository: D-ingle/frontend

Length of output: 1128


🏁 Script executed:

# More direct check for Zod version in package.json
find . -name "package.json" -type f -exec cat {} \; | grep -E '"zod"' | head -5

Repository: D-ingle/frontend

Length of output: 249


🏁 Script executed:

# Check if there's a lock file showing zod version
find . -type f \( -name "package.json" -o -name "package-lock.json" -o -name "yarn.lock" -o -name "pnpm-lock.yaml" \) | while read f; do echo "=== $f ==="; head -100 "$f" | grep -i zod || echo "no zod found in first 100 lines"; done

Repository: D-ingle/frontend

Length of output: 50372


🏁 Script executed:

# Check the exact file structure to find package.json
ls -la | head -20
find . -maxdepth 3 -name "package.json" -type f

Repository: D-ingle/frontend

Length of output: 12228


Zod v4 API 호환성 문제: .min() 에러 메시지 형식 변경 필요

Zod v4에서는 .min() 메서드의 두 번째 인자 시그니처가 변경되었습니다. 현재 코드는 문자열을 직접 전달하고 있으나, v4에서는 반드시 { error: "..." } 객체 형식으로 전달해야 합니다.

수정 방법
 export const loginSchema = z.object({
-  userId: z.string().min(1, "아이디를 입력해주세요."),
-  password: z.string().min(1, "비밀번호를 입력해주세요."),
+  userId: z.string().min(1, { error: "아이디를 입력해주세요." }),
+  password: z.string().min(1, { error: "비밀번호를 입력해주세요." }),
 });

app/types/signup.ts.min(), .max() 호출도 동일한 형식으로 수정이 필요합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const loginSchema = z.object({
userId: z.string().min(1, "아이디를 입력해주세요."),
password: z.string().min(1, "비밀번호를 입력해주세요."),
});
export const loginSchema = z.object({
userId: z.string().min(1, { error: "아이디를 입력해주세요." }),
password: z.string().min(1, { error: "비밀번호를 입력해주세요." }),
});
🤖 Prompt for AI Agents
In `@app/types/login.ts` around lines 3 - 6, The Zod v4 change requires passing
error details as an object to .min()/.max(), so update loginSchema (export const
loginSchema) to replace z.string().min(1, "아이디를 입력해주세요.") and z.string().min(1,
"비밀번호를 입력해주세요.") with the v4 form z.string().min(1, { message: "..." }) and make
the same conversion for all .min()/.max() usages in app/types/signup.ts; ensure
you keep the same numeric bounds and messages but wrap the message in { message:
"..." } for each affected schema call.

Comment thread orval.config.ts
dingle: {
input: {
target: "./openapi.json", // TODO: 백엔드 Swagger URL이나 파일 경로로 수정 필요
target: "https://d-home.o-r.kr/v3/api-docs",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# 파일 구조 및 orval.config.ts 확인
git ls-files | head -20

Repository: D-ingle/frontend

Length of output: 864


🏁 Script executed:

# orval.config.ts 내용 확인
find . -name "orval.config.ts" -type f

Repository: D-ingle/frontend

Length of output: 77


🏁 Script executed:

# 로컬 OpenAPI 스펙 파일 존재 여부 확인
find . -name "*.json" -o -name "*.yaml" -o -name "*.yml" | grep -i "openapi\|api\|spec" | head -20

Repository: D-ingle/frontend

Length of output: 757


🏁 Script executed:

# orval.config.ts 파일 내용 확인
cat -n ./orval.config.ts

Repository: D-ingle/frontend

Length of output: 675


🏁 Script executed:

# 로컬 OpenAPI 스펙 파일 검색 (node_modules 제외, 더 정확한 패턴)
find . -type f \( -name "*.json" -o -name "*.yaml" -o -name "*.yml" \) ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" | head -30

Repository: D-ingle/frontend

Length of output: 130


🏁 Script executed:

# API 관련 폴더 구조 확인
find . -maxdepth 3 -type d ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" -name "*api*" -o -name "*spec*" -o -name "*openapi*" 2>/dev/null | head -20

Repository: D-ingle/frontend

Length of output: 189


🏁 Script executed:

# shared/api 폴더 내용 확인
find ./shared/api -type f 2>/dev/null | head -20

Repository: D-ingle/frontend

Length of output: 1149


🏁 Script executed:

# CI 워크플로우에서 orval 관련 설정 확인
cat -n ./.github/workflows/ci.yml

Repository: D-ingle/frontend

Length of output: 945


🏁 Script executed:

# package.json에서 orval 관련 스크립트 확인
grep -A 2 -B 2 "orval" ./package.json

Repository: D-ingle/frontend

Length of output: 281


원격 OpenAPI URL 의존으로 인한 빌드 재현성/가용성 리스크

Line 6의 원격 스펙(https://d-home.o-r.kr/v3/api-docs)은 네트워크 장애나 원격 스펙 변경 시 빌드를 깨뜨릴 수 있습니다. 현재 CI에서 orval 생성 단계가 자동화되지 않아 개발자가 로컬에서 수동 실행해야 하므로 더욱 리스크합니다.

권장사항:

  1. OpenAPI 스펙을 로컬 파일로 커밋 (예: ./openapi.json)
  2. CI 워크플로우에 yarn api:gen 단계 추가
  3. 또는 환경변수로 URL 주입: target: process.env.ORVAL_OPENAPI_URL ?? "./openapi.json"
🤖 Prompt for AI Agents
In `@orval.config.ts` at line 6, 현재 orval.config.ts의 target이 원격
URL("https://d-home.o-r.kr/v3/api-docs")에 하드코딩되어 있어 빌드 재현성과 가용성 리스크가 있으니, target
값을 환경변수 기반 또는 로컬 파일로 대체하도록 변경하세요: 변경할 위치는 orval.config.ts의 target 프로퍼티이며 값은
process.env.ORVAL_OPENAPI_URL ?? "./openapi.json" 형태로 바꿔서 환경변수로 주입 가능하게 하고, 프로젝트
루트에 openapi.json을 커밋해 두세요(또는 둘다 지원). 추가로 CI 설정에 yarn api:gen(또는 사용하는 스크립트) 단계를
추가해 빌드 시 자동으로 orval 코드를 생성하도록 구성하세요.

Comment thread shared/api/auth.ts
Comment on lines +34 to +40
export const login = (data: FormData) => {
return customInstance<LoginResponse>({
url: "/login",
method: "POST",
headers: { "Content-Type": "multipart/form-data" },
data,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Axios FormData Content-Type boundary automatic handling documentation

💡 Result:

  • If you pass a real FormData instance to Axios, do not manually set Content-Type. The runtime (browser) or Axios/Node integration will generate multipart/form-data; boundary=... automatically; manually setting Content-Type: multipart/form-data can prevent the boundary from being added and break uploads. [1], [2]

    const form = new FormData();
    form.append('file', fileInput.files[0]);
    
    await axios.post('/upload', form); // ✅ boundary handled automatically
  • If you want Axios to build FormData for you from a plain object, then you do set Content-Type: multipart/form-data (or use postForm/putForm/patchForm). Axios will serialize to FormData. [1]

    await axios.post('/upload', { file: fileInput.files[0] }, {
      headers: { 'Content-Type': 'multipart/form-data' }
    });
    // or: axios.postForm('/upload', { file: fileInput.files[0] })
  • Node.js note: if you use a Node form-data implementation, you typically send its generated headers (which include the boundary), e.g. headers: form.getHeaders(). [2]

Sources: Axios “Multipart Bodies” docs [1]; discussion/bug report showing missing boundary when Content-Type is set manually with FormData [2].


🏁 Script executed:

# Search for FormData usage in codebase
rg "FormData" -t ts -t tsx -A 3 -B 1

Repository: D-ingle/frontend

Length of output: 87


🏁 Script executed:

# Find customInstance implementation to understand the context
fd "customInstance" -t ts -t tsx

Repository: D-ingle/frontend

Length of output: 229


🏁 Script executed:

# Search for axios configuration and other FormData usages
rg "multipart/form-data" -t ts -t tsx -B 2 -A 2

Repository: D-ingle/frontend

Length of output: 87


🏁 Script executed:

# Search for FormData usage in TypeScript files
rg "FormData" --type js --type ts -A 3 -B 1 2>/dev/null || rg "FormData" -A 3 -B 1

Repository: D-ingle/frontend

Length of output: 1821


🏁 Script executed:

# Search for multipart/form-data references
rg "multipart/form-data" -A 2 -B 2

Repository: D-ingle/frontend

Length of output: 265


🏁 Script executed:

# Find and inspect the customInstance implementation
rg "customInstance" -A 10 -B 2

Repository: D-ingle/frontend

Length of output: 50372


🏁 Script executed:

# Look at the auth.ts file to see full context
fd "auth.ts" | head -5

Repository: D-ingle/frontend

Length of output: 78


FormData 요청 시 수동으로 Content-Type 헤더를 지정하면 boundary가 누락되어 요청이 실패합니다.

Axios에 FormData 인스턴스를 전달할 때 Content-Type 헤더를 수동으로 지정하면 boundary가 자동 생성되지 않습니다. 이로 인해 서버가 요청을 거부할 수 있습니다. 헤더를 제거하고 Axios에 자동 처리를 맡겨야 합니다.

수정 방법
 export const login = (data: FormData) => {
   return customInstance<LoginResponse>({
     url: "/login",
     method: "POST",
-    headers: { "Content-Type": "multipart/form-data" },
     data,
   });
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const login = (data: FormData) => {
return customInstance<LoginResponse>({
url: "/login",
method: "POST",
headers: { "Content-Type": "multipart/form-data" },
data,
});
export const login = (data: FormData) => {
return customInstance<LoginResponse>({
url: "/login",
method: "POST",
data,
});
};
🤖 Prompt for AI Agents
In `@shared/api/auth.ts` around lines 34 - 40, The login function is setting a
manual "Content-Type": "multipart/form-data" header which prevents Axios from
adding the required boundary for FormData; update the login implementation (the
login function that calls customInstance) to remove the manual headers when data
is a FormData instance so Axios can auto-generate the Content-Type/boundary
(i.e., stop passing the headers object or only pass headers when not sending
FormData).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant