Skip to content

Conversation

@junghyungo
Copy link
Member

@junghyungo junghyungo commented Aug 17, 2025

📌 Related Issues

✅ 체크 리스트

  • PR 제목의 형식을 잘 작성했나요? e.g. [Feat/#이슈번호] PR 템플릿 작성
  • 빌드가 성공했나요? (pnpm build)
  • 컨벤션을 지켰나요?
  • 이슈는 등록했나요?
  • 리뷰어와 라벨을 지정했나요?

📄 Tasks

  • 탐색 페이지
  • 핀 리스트 페이지 + 핀 열람 페이지
  • 핀 등록 페이지

⭐ PR Point (To Reviewer)

  • 각 페이지, 컴포넌트의 쿼리 파라미터는 api 연동 및 TanStack 쿼리 적용시 수정할 것임
  • 핀 등록 페이지에서 Zod로 유효성 검사
  • 핀 등록 페이지의 장소 선택 화면 구현 안 됨
  • PopupOverlay 컴포넌트의 Opacity가 전부 0.8로 되어있어서 배경만 적용되도록 수정 필요할 듯

📷 Screenshot

image image image image image

🔔 ETC

Summary by CodeRabbit

  • 신기능

    • 탐색(Explore) 페이지 추가: 검색바, 카테고리 필터, 지도 및 추천/리뷰 섹션 제공.
    • 핀 추가(Pin Add) 흐름 도입: 사진 업로드 미리보기, 별점·리뷰·카테고리 입력, 확인 팝업.
    • 핀 상세 페이지 추가: 장소 정보, 이미지, 별점 표시 및 하단 푸터(저장 토글).
    • 장소별 핀 목록 페이지 및 목록→상세 이동 지원.
  • 리팩터

    • 라우팅 업데이트: 검색 경로 제거, 탐색 및 핀 관련 경로 추가.
  • 스타일

    • 여러 페이지/컴포넌트 레이아웃·간격·색상 스타일 추가·조정.

@coderabbitai
Copy link

coderabbitai bot commented Aug 17, 2025

Walkthrough

탐색(Explore) 페이지, 장소 목록(PlacePins), 핀 상세(PinDetail), 핀 추가(PinAdd) 및 관련 하위 컴포넌트/스타일/목업/검증 훅을 추가하고 기존 Search 컴포넌트와 /search 라우트를 제거했습니다. PlaceReview 네비게이션 및 일부 스타일도 수정되었습니다.

Changes

Cohort / File(s) Summary
Explore 페이지
src/pages/explore/Explore.tsx, src/pages/explore/ExploreHeader.tsx, src/pages/explore/ExploreContent.tsx, src/pages/explore/ExplorePlaces.tsx, src/pages/explore/Explore.css.ts, src/pages/explore/mockup.ts
Explore 페이지 컴포넌트·스타일·목업 추가(헤더, 콘텐츠, 장소 목록, 지도·카테고리 필터 등).
Pin 추가 (PinAdd)
src/pages/pinAdd/PinAdd.tsx, src/pages/pinAdd/PinAdd.css.ts, src/pages/pinAdd/hook/usePinAddValidation.ts, src/pages/pinAdd/component/confirmPopup/ConfirmPopup.tsx, src/pages/pinAdd/component/confirmPopup/ConfirmPopup.css.ts, src/pages/pinAdd/component/pinCategoriesInput/*, src/pages/pinAdd/component/pinPhotoInput/*, src/pages/pinAdd/component/pinPlaceInput/*, src/pages/pinAdd/component/pinReviewInput/*, src/pages/pinAdd/component/pinScoreInput/*
Pin 추가 페이지, 입력 컴포넌트(사진·장소·리뷰·별점·카테고리), 스타일과 Zod 기반 검증 훅, ConfirmPopup 및 API 변환 유틸 추가.
Pin 상세 (PinDetail)
src/pages/pinDetail/PinDetail.tsx, src/pages/pinDetail/PinDetail.css.ts, src/pages/pinDetail/component/PinFooter.tsx, src/pages/pinDetail/component/PinFooter.css.ts
Pin 상세 페이지와 하단 푸터 컴포넌트·스타일 추가(쿼리 파라미터 읽기, 뒤로가기, 저장 토글).
PlacePins 목록
src/pages/placePins/PlacePins.tsx, src/pages/placePins/PlacePins.css.ts, src/pages/placePins/component/PlacePin.tsx, src/pages/placePins/component/PlacePin.css.ts, src/pages/placePins/types/Pins.ts
특정 장소의 핀 목록 페이지·카드 컴포넌트·스타일·타입 추가 및 mockupExplore 매핑/클릭 네비게이션 구현.
라우팅 변경
src/router/Router.tsx, src/router/constant/Routes.ts
/search 라우트 및 Search 컴포넌트 제거, /explore, /pin/add, /pin/:pinId, /place/:placeId/pins 등 라우트 추가 및 라우터 매핑 수정.
공유 컴포넌트 조정
src/shared/components/placeReview/PlaceReview.tsx, src/shared/components/placeReview/PlaceReview.css.ts, src/shared/components/categoryTag/CategoryTag.css.ts
PlaceReview에 Stars 적용, 장소 클릭시 쿼리 포함된 /place/:id/pins 네비게이션으로 변경, 일부 gap 값 조정.
삭제
src/pages/search/Search.tsx
빈 Search 컴포넌트 삭제.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Router
  participant Explore as Explore Page
  participant PlaceReview as PlaceReview List
  participant PlacePins as PlacePins Page
  participant PinDetail as PinDetail Page

  User->>Router: GET /explore
  Router-->>Explore: render
  Explore->>PlaceReview: render sections
  User->>PlaceReview: 장소 카드 클릭
  PlaceReview->>Router: navigate /place/:placeId/pins?placeName=...
  Router-->>PlacePins: render
  PlacePins->>User: 핀 카드 목록 표시
  User->>PlacePins: 핀 카드 클릭
  PlacePins->>Router: navigate /pin/:pinId?placeName=...&userName=...&score=...
  Router-->>PinDetail: render
  PinDetail->>User: 세부정보 표시
Loading
sequenceDiagram
  autonumber
  actor User
  participant PinAdd as PinAdd Page
  participant Form as RHF(usePinAddValidation)
  participant Confirm as ConfirmPopup
  participant Router

  User->>PinAdd: "등록" 버튼 클릭
  PinAdd->>Form: handleSubmit (검증)
  alt 검증 성공
    PinAdd->>Confirm: open=true
    User->>Confirm: "확인" 클릭
    Confirm->>PinAdd: onConfirm
    PinAdd->>Form: watch/getValues()
    PinAdd->>PinAdd: convertToApiData(values)
    PinAdd->>Router: navigate /explore
  else 검증 실패
    PinAdd-->>User: 에러 표시
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25–30 minutes

Assessment against linked issues

Objective Addressed Explanation
탐색 페이지 구현 (#91)
핀 열람 페이지 구현 (#91)
핀 추가 페이지 구현 (#91)

Possibly related PRs

Suggested reviewers

  • hamxxn
  • Hwanggyuun

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#91/explore-page

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions
Copy link

✅ 빌드에 성공했습니다! 🎉

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (57)
src/shared/components/categoryTag/CategoryTag.css.ts (1)

20-21: 태그 간격 2rem 확대 — 모바일(좁은 폭)에서 과도한 가로 스크롤 가능성. 반응형 간격 제안

좁은 화면에서 기존보다 스크롤이 많아질 수 있습니다. 최소/최대 폭에 따라 간격을 줄이는 미디어쿼리 도입을 권장합니다.

아래처럼 폭이 좁을 때 간격을 1rem으로 줄이는 예시입니다.

 export const tagContainer = style({
   display: 'inline-flex',
-  gap: '2rem',
+  gap: '2rem',
+  '@media': {
+    'screen and (max-width: 360px)': {
+      gap: '1rem',
+    },
+  },
 });
package.json (1)

27-27: swagger-typescript-api는 런타임 불필요 — devDependencies로 이동 권장

코드 생성 도구는 런타임 번들에 포함될 필요가 없습니다. dependencies에서 제거하고 devDependencies로 이동을 권장합니다.

dependencies에서 제거:

-    "swagger-typescript-api": "^13.2.7",

devDependencies에 추가(예시):

{
  "devDependencies": {
    "swagger-typescript-api": "^13.2.7"
  }
}
src/pages/pinAdd/component/pinPlaceInput/PinPlaceInput.css.ts (1)

3-7: 아이콘/텍스트 수평 정렬 보장

flex 래핑만으로는 자식 높이가 달라질 때 정렬이 어긋날 수 있습니다. 세로 중앙 정렬을 추가하면 안정적입니다.

 export const placeInputWrapper = style({
   display: 'flex',
   gap: '1rem',
+  alignItems: 'center',
   width: '100%',
 });
src/pages/placePins/PlacePins.css.ts (2)

4-11: 모바일 주소창/시스템바 이슈: 100vh 대신 100dvh 지원 추가 권장

모바일 브라우저에서 100vh는 가변 주소창으로 인해 실제 화면보다 길거나 짧게 표시될 수 있습니다. 100dvh 지원을 추가해 안전한 높이를 보장하는 것을 권장합니다.

 export const placePinsWrapper = style({
   display: 'flex',
   flexDirection: 'column',
   width: '100%',
-  height: '100vh',
+  height: '100vh',
+  '@supports': {
+    '(height: 100dvh)': {
+      height: '100dvh',
+    },
+  },
   backgroundColor: vars.color.blue0,
   overflow: 'hidden',
 });

13-17: iOS 스크롤 가속도 개선 제안

스크롤 영역에 iOS 모멘텀 스크롤을 적용하면 체감 품질이 좋아집니다.

 export const placePinsContainer = style({
   flex: 1,
   width: '100%',
   overflow: 'auto',
+  WebkitOverflowScrolling: 'touch',
 });
src/pages/placePins/component/PlacePin.css.ts (2)

4-10: px → rem 단위 통일 제안

다른 파일에서 rem을 주로 사용 중입니다. padding도 16px 대신 1.6rem로 맞추면 일관성이 좋아집니다.

 export const placePinWrapper = style({
   display: 'flex',
   flexDirection: 'column',
   width: '100%',
-  padding: '16px',
+  padding: '1.6rem',
   backgroundColor: vars.color.white,
 });

25-29: 카테고리 태그 래핑: 줄바꿈 대응 필요 시 flex-wrap 고려

카테고리 수가 많아지는 경우 한 줄 고정이면 넘침이 발생할 수 있습니다. 필요 시 flexWrap 옵션을 도입하세요.

 export const categoryWrapper = style({
   display: 'flex',
   gap: '0.4rem',
   marginTop: '1.2rem',
+  // flexWrap: 'wrap',
 });
src/pages/pinAdd/component/pinScoreInput/PinScoreInput.css.ts (1)

22-27: 호버 스케일에 트랜지션/포커스 스타일 추가 권장 (접근성 + 시각적 매끄러움)

hover 시 즉시 스케일이 적용되어 약간 튐(jank)이 있을 수 있고, 키보드 포커스 스타일이 없어 접근성이 떨어집니다. 간단한 트랜지션과 :focus-visible 스타일을 추가하는 것을 권장합니다.

 export const starButton = style({
-  cursor: 'pointer',
+  cursor: 'pointer',
+  display: 'inline-block',
+  transition: 'transform 120ms ease-out',
   ':hover': {
     transform: 'scale(1.1)',
   },
+  ':focus-visible': {
+    outline: `2px solid ${vars.color.baroBlue}`,
+    outlineOffset: '2px',
+  },
 });
src/pages/placePins/component/PlacePin.tsx (3)

22-24: 점수 텍스트의 기본값 불일치 정리 (UI 일관성)

Text에는 그대로 pin.score를 출력하고 Stars에는 0으로 fallback을 주고 있어, 점수가 없을 때 UI가 불일치합니다. 텍스트도 동일하게 fallback을 적용해주세요.

-        <Text tag='body_14'>{pin.score}</Text>
+        <Text tag='body_14'>{pin.score ?? 0}</Text>

16-16: 카드 전체 클릭 요소에 접근성 보강 권장

Div 클릭만으로는 키보드 접근이 어려워집니다. role, tabIndex 및 Enter/Space 활성화를 추가해 주세요.

-    <div className={styles.placePinWrapper} onClick={handlePinClick}>
+    <div
+      className={styles.placePinWrapper}
+      onClick={handlePinClick}
+      role='button'
+      tabIndex={0}
+      onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ') && handlePinClick()}
+    >

18-20: 프로필 이미지 안전장치 및 UX 개선 제안

프로필 이미지가 없거나 오류일 경우 대비가 없습니다. alt를 동적 문구로 개선하고, 지연 로딩으로 초기 로드 성능을 약간 개선할 수 있습니다.

-        <img src={pin.profileImage} alt='유저 프로필' />
+        <img
+          src={pin.profileImage}
+          alt={pin.userName ? `${pin.userName} 프로필` : '유저 프로필'}
+          loading='lazy'
+        />
src/pages/pinAdd/component/pinCategoriesInput/PinCategoriesInput.css.ts (1)

12-17: 단위 일관성 유지 (px → rem 권장)

다른 곳은 rem을 쓰고 있는데 여기만 px입니다. 일관된 스케일링을 위해 rem으로 통일하는 것을 권장합니다.

 export const categoriesTitle = style({
   display: 'flex',
-  gap: '4px',
+  gap: '0.25rem',
   alignItems: 'center',
   marginTop: '0.5rem',
 });
src/pages/pinAdd/hook/usePinAddValidation.ts (2)

10-12: score 타입 안정성 강화: 문자열 입력 대비 coerce 사용 권장

UI에서 number input을 사용해도 브라우저/라이브러리 설정에 따라 문자열로 들어올 수 있습니다. z.coerce.number()를 사용하면 런타임 타입 문제를 줄일 수 있습니다.

-  score: z.number()
+  score: z.coerce.number()
     .min(1, '별점을 선택해주세요')
     .max(5, '별점은 최대 5점입니다'),

26-31: categoryIds 변환 중복 제거 및 견고성 향상

중복 선택이 들어오면 그대로 중복된 ID가 전송될 수 있습니다. Set으로 중복을 제거해 주세요. (추가로, 'ALL' 같은 UI 전용 항목이 있다면 서버로 보내지 않도록 제외가 필요할 수 있습니다.)

 export const convertToIds = (categories: string[]): number[] => {
-  return categories
-    .map(category => (CATEGORIES as readonly string[]).indexOf(category))
-    .filter(index => index !== -1);
+  const ids = categories
+    .map((category) => (CATEGORIES as readonly string[]).indexOf(category))
+    .filter((index) => index !== -1);
+  return Array.from(new Set(ids));
 };

검증 제안:

  • 'ALL'이 실제 API에 필요한 값인지 확인 필요. UI 전용이면 변환 전에 제외해야 합니다.
src/pages/explore/ExplorePlaces.tsx (1)

10-10: 리스트 key에 인덱스 사용 지양 — 안정적인 식별자 사용 권장

인덱스를 key로 쓰면 정렬/필터 등 리스트 변경 시 불필요한 재마운트가 발생할 수 있습니다. reviewType이 유니크하다면 해당 값을 key로 사용하세요.

-          key={index}
+          key={category.reviewType}
src/pages/pinAdd/component/pinPlaceInput/PinPlaceInput.tsx (1)

9-11: 임시 내비게이션 경로 ('/') 확인 필요 — 라우트 상수로 명시화 권장

장소 선택 화면이 미구현 상태라면 TODO로 명시해 두고, 실제 대상 경로가 정해지는 즉시 라우트 상수를 사용해 교체하세요. 현재 '/'로 이동은 사용자가 작성 중 흐름에서 벗어날 가능성이 있어 의도와 다를 수 있습니다.

예시:

// TODO: 장소 선택 화면 구현 시 아래 경로를 ROUTES.PLACE_SELECT 등으로 교체
// navigate(ROUTES.PLACE_SELECT);

라우트 상수를 이미 사용 중이라면 다음과 같이 import하여 일관성 유지:

import { ROUTES } from '@router/constant/Routes';
src/pages/explore/ExploreHeader.tsx (2)

15-17: 하드코딩된 경로 대신 라우트 상수 사용 권장

'/pin/add' 문자열 하드코딩 대신 ROUTES.PIN_ADD를 사용해 라우팅을 일관되게 관리하세요.

-  const handleAddPinClick = () => {
-    navigate('/pin/add');
-  }
+  const handleAddPinClick = () => {
+    navigate(ROUTES.PIN_ADD);
+  }

추가로 상단에 import:

import { ROUTES } from '@router/constant/Routes';

27-30: 클릭 가능한 SVG의 접근성 개선 필요

SVG 자체에 onClick을 다는 경우 키보드 포커스/역할이 없어 접근성이 떨어집니다. 버튼 요소로 감싸거나 role="button"과 tabIndex=0, aria-label을 부여하세요.

옵션 A: 버튼으로 감싸기

-      <IcWriteBlue
-        className={styles.addPinIcon}
-        onClick={handleAddPinClick}
-      />
+      <button type="button" className={styles.addPinIcon} onClick={handleAddPinClick} aria-label="핀 추가">
+        <IcWriteBlue />
+      </button>

옵션 B: 최소 변경으로 속성 추가

       <IcWriteBlue
         className={styles.addPinIcon}
-        onClick={handleAddPinClick}
+        onClick={handleAddPinClick}
+        role="button"
+        tabIndex={0}
+        aria-label="핀 추가"
       />
src/pages/explore/ExploreContent.tsx (1)

16-21: 카테고리 필터링 계산을 useMemo로 메모이즈해 불필요한 재계산 방지

렌더마다 배열 필터링이 재실행됩니다. 데이터가 늘어날 경우 잦은 재계산을 피하기 위해 메모이즈를 권장합니다.

아래처럼 수정 제안드립니다.

-import { useState } from 'react';
+import { useMemo, useState } from 'react';
@@
-  // 선택된 카테고리에 따라 Pin Data 필터링
-  const filteredPinData = selectedCategory === 'ALL'
-    ? mockupPinData
-    : mockupPinData.filter(pin =>
-      pin.pinCategories.includes(selectedCategory)
-    );
+  // 선택된 카테고리에 따라 Pin Data 필터링
+  const filteredPinData = useMemo(
+    () =>
+      selectedCategory === 'ALL'
+        ? mockupPinData
+        : mockupPinData.filter((pin) => pin.pinCategories.includes(selectedCategory)),
+    [selectedCategory],
+  );
src/pages/placePins/PlacePins.tsx (2)

31-34: Header 아이콘 전달 방식 개선 (크기/클릭 핸들러 일관성 확보)

Header 컴포넌트는 아이콘 컴포넌트 타입과 클릭 핸들러를 분리해 받습니다. 현재처럼 함수로 엘리먼트를 반환하면 Header가 주입하는 width/height가 적용되지 않습니다. 아래처럼 컴포넌트 타입과 핸들러를 분리 전달해 주세요.

-      <Header
-        background='blue0'
-        leftIcon={() => <IcArrowBlueLeft onClick={handleBackClick} />}
-        text={placeName}
-      />
+      <Header
+        background='blue0'
+        leftIcon={IcArrowBlueLeft}
+        onClickLeftIcon={handleBackClick}
+        text={placeName}
+      />

36-43: 리스트 key로 index 대신 고유값(pinId) 사용

리스트 재정렬/삽입 시 불필요한 재마운트와 상태 꼬임을 방지하기 위해 index key 지양이 좋습니다. pinId로 교체 추천합니다.

-        {mockupExplore.map((item, index) => (
+        {mockupExplore.map((item) => (
           <PlacePin
-            key={index}
+            key={item.pinId}
             pin={item.pin}
             categories={item.categories}
             handlePinClick={() => handlePinClick(item)}
           />
         ))}
src/pages/pinAdd/PinAdd.tsx (2)

49-64: Header 좌/우 슬롯 사용 방식 정렬

  • 좌측 아이콘: PlacePins와 동일하게 컴포넌트 타입과 핸들러를 분리 전달하면 Header의 width/height 규격을 유지할 수 있습니다.
  • 우측 "등록" 버튼: Header는 rightIcon을 아이콘 컴포넌트로 가정하고 width/height를 강제합니다. 텍스트 버튼을 넣으려면 Header에 rightSlot(任意 엘리먼트) 같은 슬롯을 추가하거나, 별도의 상단 바를 사용하는 패턴이 더 안전합니다.

좌측 아이콘만 우선 정리하는 최소 수정:

       <Header
-        background='baroblue'
-        leftIcon={() => (
-          <IcArrowLeft onClick={handleBackClick} />
-        )}
+        background='baroBlue'
+        leftIcon={IcArrowLeft}
+        onClickLeftIcon={handleBackClick}
         text='핀 추가'
-        rightIcon={() => (
-          <button
-            type='button'
-            className={styles.submitButton}
-            onClick={handleSubmitClick}
-          >
-            등록
-          </button>
-        )}
+        rightIcon={() => (
+          <button
+            type='button'
+            className={styles.submitButton}
+            onClick={handleSubmitClick}
+          >
+            등록
+          </button>
+        )}
       />

추가로, Header에 rightSlot?: ReactNode를 도입하면 텍스트 버튼을 규격 강제 없이 깔끔하게 수용할 수 있습니다. 필요하시면 해당 리팩터링 패치도 제안드리겠습니다.


30-34: handleSubmit 바인딩 간결화

현재 즉시실행 형태로 두 번 감싸져 있어 가독성이 떨어집니다. 아래처럼 핸들러 자체를 바인딩하면 불필요한 래핑을 줄일 수 있습니다.

-  const handleSubmitClick = () => {
-    handleSubmit(() => {
-      setShowConfirmPopup(true);
-    })();
-  };
+  const handleSubmitClick = handleSubmit(() => {
+    setShowConfirmPopup(true);
+  });
src/pages/pinAdd/component/pinScoreInput/PinScoreInput.tsx (1)

32-45: 별점 버튼 접근성(ARIA) 보강

키보드/스크린리더 사용자를 위해 각 버튼에 레이블과 pressed 상태를 명시하는 것이 좋습니다.

         {[1, 2, 3, 4, 5].map((starIndex) => (
           <button
             key={starIndex}
             type="button"
             className={styles.starButton}
             onClick={() => handleStarClick(starIndex)}
+            aria-label={`${starIndex}점 선택`}
+            aria-pressed={score >= starIndex}
           >
src/pages/pinDetail/PinDetail.css.ts (2)

8-10: 모바일 100vh 이슈 가능성 — dvh 고려 권장

모바일 브라우저(특히 iOS Safari)에서 주소창 표시/숨김에 따라 100vh가 화면보다 커져 레이아웃이 흔들릴 수 있습니다. dvh/svh 등을 고려해 주세요.

원하시면 100vh 대신 100dvh로 전환하거나, 상황에 따라 minHeight/height를 조합한 대안을 제안드릴게요.


21-27: 이미지 스타일 보완: object-fit 및 단위 일관화

이미지를 꽉 채우되 비율 유지가 필요해 보입니다. 또한 px 대신 rem 사용으로 스타일 토큰과의 일관성을 높일 수 있습니다.

아래와 같이 보완을 제안드립니다:

 export const pinPlaceImage = style({
   width: '100%',
   height: '22.8rem',
   marginBottom: '1.6rem',
-  borderRadius: '10px',
+  borderRadius: '1rem',
   backgroundColor: vars.color.gray0,
+  objectFit: 'cover',
 });
src/pages/pinDetail/component/PinFooter.tsx (1)

27-32: 사용자명 미존재 시 폴백 필요

userName이 빈 문자열로 넘어오면 공백만 노출됩니다. 간단한 폴백을 두는 편이 안전합니다.

-          <Text tag='body_14' color='white'>
-            {userName}
+          <Text tag='body_14' color='white'>
+            {userName || '익명 사용자'}
           </Text>
src/pages/pinAdd/PinAdd.css.ts (1)

8-10: 모바일 100vh 이슈 가능성 — dvh 검토

PinDetail과 동일하게 100vh는 모바일에서 레이아웃 흔들림을 유발할 수 있습니다. dvh/svh 검토를 권장합니다.

원하시면 공통 레이아웃 래퍼에 적용할 모범 예시를 드리겠습니다.

src/pages/pinAdd/component/pinReviewInput/PinReviewInput.tsx (2)

12-14: 검증 스키마와 최대 길이 상수 동기화 필요

maxLength = 150이 하드코딩되어 있어 Zod 스키마와 불일치 시 유지보수 비용이 생깁니다. 스키마와 공유하는 상수로 분리/재사용을 권장합니다.

원하시면 usePinAddValidation.ts에서 export const MAX_REVIEW_LENGTH = 150로 정의 후 여기서 import하는 형태로 리팩터 제안 드릴게요.


30-31: 사소한: nullish 병합 연산자로 의도 명확화

길이 계산은 ?? 0이 의도를 더 잘 드러냅니다. 현재 구현도 동작에는 문제 없습니다.

-        {reviewValue?.length || 0} / {maxLength}
+        {reviewValue?.length ?? 0} / {maxLength}
src/pages/pinDetail/component/PinFooter.css.ts (5)

1-1: 테마 변수 사용을 위한 vars import 추가 제안

고정 하단 푸터는 배경색/세이프에어리어 대응이 필요할 수 있어 theme vars 참조가 유용합니다. 아래처럼 vars import를 추가하면 이후 배경/색상 통일성이 좋아집니다.

-import { style } from '@vanilla-extract/css';
+import { style } from '@vanilla-extract/css';
+import { vars } from '@shared/styles/theme.css';

3-10: fixed 푸터: 세이프에어리어/배경/좌우 고정 보강

iOS 하단 홈 인디케이터 영역 및 스크롤 컨텐츠 위에 얹힐 때를 고려해 배경색, z-index, 좌우 고정(left/right)과 safe-area-inset-bottom 반영을 권장합니다.

 export const pinFooterWrapper = style({
   display: 'flex',
   justifyContent: 'space-between',
   position: 'fixed',
   bottom: 0,
-  width: '100%',
-  padding: '1.2rem 2rem 4.4rem 2rem',
+  left: 0,
+  right: 0,
+  width: '100%',
+  padding: '1.2rem 2rem calc(4.4rem + env(safe-area-inset-bottom)) 2rem',
+  backgroundColor: vars.color.white,
+  boxShadow: '0 -2px 12px rgba(0,0,0,0.06)',
+  zIndex: 100,
 });

검증 포인트:

  • 모바일(iOS)에서 홈 인디케이터와 겹치지 않는지
  • Overlay/BottomSheet와의 z-index 충돌이 없는지

12-16: 중복 스타일(PlacePin의 userWrapper) 재사용 고려

src/pages/placePins/component/PlacePin.css.ts의 userWrapper와 동일 구조입니다. 공통 스타일로 추출하거나 재사용하면 유지보수성이 좋아집니다.

원하시면 src/shared/styles/blocks.css.ts 같은 공용 파일로 추출하는 PR 패치 초안을 드리겠습니다.


23-30: marginRight 대신 gap 사용으로 일관성 및 확장성 개선

score 텍스트와 아이콘 간 간격을 wrapper의 gap으로 관리하면 확장성/일관성이 좋아집니다. PlacePin의 scoreWrapper도 gap을 사용합니다.

 export const scoreWrapper = style({
   display: 'flex',
   alignItems: 'center',
+  gap: '0.4rem',
 });
 
 export const scoreText = style({
-  marginRight: '0.4rem',
+  marginRight: 0,
 });

32-35: 모바일 클릭 반응성 개선을 위한 touch-action

모바일에서 빠른 탭 반응을 위해 touchAction: 'manipulation'을 권장합니다.

 export const saveButton = style({
   marginLeft: '2.1rem',
   cursor: 'pointer',
+  touchAction: 'manipulation',
 });
src/pages/pinAdd/component/pinPhotoInput/PinPhotoInput.css.ts (4)

2-2: 테마 import 경로 일관화 (@Styles vs @shared/styles)

본 PR 내 다른 파일들은 @shared/styles/theme.css를 사용합니다. 경로 alias가 둘 다 유효하더라도, 한 가지로 통일하는 편이 좋습니다.

-import { vars } from '@styles/theme.css';
+import { vars } from '@shared/styles/theme.css';

확인 요청:

  • tsconfig/webpack alias에서 @styles@shared/styles가 모두 매핑되어 있는지
  • 팀 코딩 컨벤션에서 선호 경로가 무엇인지

13-19: 업로드 영역: overflow 클리핑/아이콘 배치 대비

라운딩 일관성(자식 내용 클리핑)과 향후 삭제/편집 아이콘 overlay 배치를 위해 overflow와 position을 추가하는 것을 권장합니다.

 export const photoContainer = style({
   width: '100%',
   height: '22.8rem',
   borderRadius: '10px',
   backgroundColor: vars.color.gray0,
   cursor: 'pointer',
+  overflow: 'hidden',
+  position: 'relative',
 });

21-26: borderRadius 중복 지정 제거로 단순화

라운드 처리는 컨테이너에서만 하고, 이미지는 100% 채우기만 담당하면 중복이 줄고 성능/일관성이 좋아집니다.

 export const uploadedImage = style({
   width: '100%',
   height: '100%',
   objectFit: 'cover',
-  borderRadius: '10px',
 });

9-11: 시각적 숨김으로 접근성 개선 제안

파일 입력을 완전히 display:none 하면 포커스/스크린리더 접근성이 떨어질 수 있습니다. label로 트리거하는 구조라면 시각적 숨김(clip/clipPath/width/height 1px)을 쓰는 패턴을 고려해주세요.

공용 visuallyHidden 유틸 스타일을 src/shared/styles/a11y.css.ts에 추가하는 패치를 제공해드릴 수 있습니다.

src/pages/explore/Explore.css.ts (1)

29-41: 배경색 지정 위치 최소화 고려

exploreContentexplorePlaces 모두 배경색을 지정하고 있어 중첩 시 불필요한 중복이 생길 수 있습니다. 상위(exploreWrapper 또는 최상위 섹션)에만 배경을 주고 하위는 투명으로 두는 방식도 고려해 보세요.

src/pages/pinAdd/component/confirmPopup/ConfirmPopup.css.ts (2)

4-11: 바텀 시트 스크롤/세이프에어리어 대응

콘텐츠가 많을 때 뷰포트를 넘치지 않도록 maxHeight/overflow를 주고, 하단 safe-area padding을 추가하면 모바일에서 안정적입니다.

 export const popupContents = style({
   display: 'flex',
   flexDirection: 'column',
   width: '100%',
-  padding: '8.2rem 2.4rem 1.4rem 2.4rem',
+  padding: '8.2rem 2.4rem calc(1.4rem + env(safe-area-inset-bottom)) 2.4rem',
   backgroundColor: vars.color.white,
   borderRadius: '16px 16px 0 0',
+  maxHeight: '80vh',
+  overflowY: 'auto',
 });

27-34: 카테고리 그리드 중복 정의 → 공용/재사용 권장

PinCategoriesInput.css.tscategoriesGrid와 거의 동일한 레이아웃입니다. 동일 토큰 재사용 또는 공용 스타일로 추출하면 UI 일관성이 향상됩니다.

재사용 여부를 검토 부탁드립니다. 필요 시 공용 shared/categories.css.ts로 추출하는 패치를 제안드릴 수 있습니다.

src/pages/pinAdd/component/confirmPopup/ConfirmPopup.tsx (2)

49-57: 확인 팝업에서 비활성 버튼 처리로 UX 혼동 방지

확인 팝업 단계에서는 카테고리 변경이 불가한 듯 보입니다. 클릭 가능해 보이는 버튼을 비활성 처리하면 혼동을 줄일 수 있습니다. Button 컴포넌트가 disabled를 지원한다면 아래처럼 적용을 고려해주세요.

                 <Button
                   key={category}
                   variant={categories.includes(category) ? 'enabled' : 'outlined'}
                   size='category'
                   text={category}
+                  disabled
                 />

확인 요청:

  • Button이 disabled prop을 지원하는지
  • 디자인 가이드 상 확인 단계에서 편집 불가가 맞는지(맞다면 disabled 스타일 가이드도 적용)

9-16: 카테고리 타입을 상수 기반으로 엄격화

string[] 대신 typeof CATEGORIES[number]에서 'ALL'을 제외한 유니언을 쓰면 타입 안전성이 높아집니다.

 interface ConfirmPopupProps {
   open: boolean;
   onClose: () => void;
   onConfirm: () => void;
   score: number;
   max: number;
-  categories: string[];
+  // 'ALL'은 제외하고 선택 가능한 카테고리만 허용
+  categories: ReadonlyArray<Exclude<typeof CATEGORIES[number], 'ALL'>>;
 }
src/pages/pinAdd/component/pinPhotoInput/PinPhotoInput.tsx (6)

5-8: 폼 연동이 불명확합니다: 파일/미리보기 값을 상위 폼으로 전달하는 props 추가 권장

현재 컴포넌트 내부 상태에만 파일/미리보기 URL을 보관하고 있어 제출 단계에서 실제 파일 값이 상위로 전달되지 않을 수 있습니다. 상위 폼과의 연동을 위해 onChange 콜백(파일과 미리보기 URL) 및 초기 value prop을 받도록 확장하는 것을 권장합니다.

아래와 같이 최소 변경으로 컴포넌트 시그니처/상태 초기화를 확장할 수 있습니다:

-export default function PinPhotoInput() {
-  const [uploadedImage, setUploadedImage] = useState<string | null>(null);
+type PinPhotoInputProps = {
+  value?: string | null;
+  onChange?: (file: File | null, previewUrl: string | null) => void;
+  maxSizeMb?: number; // 기본 5MB 등
+};
+
+export default function PinPhotoInput({ value = null, onChange, maxSizeMb = 5 }: PinPhotoInputProps) {
+  const [uploadedImage, setUploadedImage] = useState<string | null>(value);

그리고 파일 선택/삭제 시점에 onChange를 호출해 상위 폼에 동기화해주세요.


13-21: 대용량 이미지/메모리 사용 이슈: FileReader DataURL 대신 Object URL 사용 및 타입/사이즈 검증 추가 권장

  • 현재 Data URL은 매우 길고 메모리를 크게 점유합니다. Object URL(URL.createObjectURL)을 사용하면 메모리 사용을 줄이고, 삭제 시 revoke로 해제할 수 있습니다.
  • accept='image/*'만으로는 불충분합니다. 파일 타입/사이즈를 직접 검증하세요.
-  const reader = new FileReader();
-  reader.onload = (e) => {
-    setUploadedImage(e.target?.result as string);
-  };
-  reader.onerror = () => {
-    alert('파일을 읽는 중 오류가 발생했습니다.');
-  };
-  reader.readAsDataURL(file);
+  // 타입/사이즈 검증
+  const isImage = file.type.startsWith('image/');
+  const MAX_BYTES = (typeof maxSizeMb === 'number' ? maxSizeMb : 5) * 1024 * 1024;
+  if (!isImage) {
+    // TODO: 토스트/에러 영역으로 교체 권장
+    console.warn('이미지 파일만 업로드할 수 있습니다.');
+    return;
+  }
+  if (file.size > MAX_BYTES) {
+    console.warn(`파일 용량이 너무 큽니다. 최대 ${Math.round(MAX_BYTES / (1024 * 1024))}MB`);
+    return;
+  }
+  const url = URL.createObjectURL(file);
+  setUploadedImage(url);
+  onChange?.(file, url);

추가(컴포넌트 내부 어느 곳에나)로 Object URL 해제를 위해 cleanup도 넣어주세요:

// 컴포넌트 내부
React.useEffect(() => {
  return () => {
    // 언마운트 시 마지막 URL 정리
    if (uploadedImage?.startsWith('blob:')) URL.revokeObjectURL(uploadedImage);
  };
}, [uploadedImage]);

17-19: 경고(alert) 대신 일관된 UI 피드백 사용 권장

alert는 UX를 저해합니다. 토스트/에러 메시지 영역 등 앱 공통 피드백 컴포넌트를 사용하거나 상위로 에러를 전달해 일관된 방식으로 표출해주세요.


29-35: 이미지 삭제 시 Object URL 해제 및 폼 동기화 누락

Object URL을 사용한다면 삭제 시 revoke가 필요합니다. 또한 상위 폼(onChange)에 null 전달로 동기화를 보장하세요.

 const handleRemoveImage = (e: React.MouseEvent) => {
   e.stopPropagation();
-  setUploadedImage(null);
+  if (uploadedImage?.startsWith('blob:')) {
+    URL.revokeObjectURL(uploadedImage);
+  }
+  setUploadedImage(null);
+  onChange?.(null, null);
   if (fileInputRef.current) {
     fileInputRef.current.value = '';
   }
 };

47-54: 접근성: 이미지 자체를 클릭해 삭제하는 패턴은 비직관적이고 키보드 접근성이 낮습니다

최소한 role과 포커스 가능(tabIndex)을 부여해 보조기기 접근을 확보하세요. 이상적으로는 명시적인 “삭제” 버튼을 오버레이로 제공하는 것이 좋습니다.

 <img
   src={uploadedImage}
   alt='업로드된 사진'
   className={styles.uploadedImage}
   onClick={handleRemoveImage}
+  role='button'
+  tabIndex={0}
+  aria-label='업로드된 사진 삭제'
 />

원한다면 별도의 “삭제” 버튼(aria-label 포함)을 이미지 우상단에 배치하는 구현으로 개선 가능합니다.


55-63: 접근성: 클릭 가능한 컨테이너는 버튼 요소를 사용하세요

div에 onClick만 부여하면 키보드 사용자가 접근하기 어렵습니다. button 요소로 교체하고 적절한 aria-label을 부여하세요.

-          <div
-            className={styles.uploadPlaceholder}
-            onClick={handlePhotoContainerClick}
-          >
+          <button
+            type='button'
+            className={styles.uploadPlaceholder}
+            onClick={handlePhotoContainerClick}
+            aria-label='장소 사진 추가'
+          >
             <Text tag='body_16' color='gray3'>
               클릭하여 장소 사진을 추가해주세요
             </Text>
-          </div>
+          </button>
src/pages/pinAdd/component/pinCategoriesInput/PinCategoriesInput.tsx (3)

13-16: undefined 안전성 및 에러 제어 API 추가 권장

  • watch('categories')가 초기값 미설정 시 undefined일 수 있어 includes/length에서 런타임 에러가 날 수 있습니다. 안전한 기본값을 적용하세요.
  • MAX 초과 시 사용자에게 즉시 피드백을 주려면 setError/clearErrors를 함께 사용하세요.
-export default function PinCategoriesInput({ form }: PinCategoriesInputProps) {
-  const { setValue, watch, formState: { errors } } = form;
-  const selectedCategories = watch('categories');
+export default function PinCategoriesInput({ form }: PinCategoriesInputProps) {
+  const { setValue, setError, clearErrors, watch, formState: { errors } } = form;
+  const selectedCategories = watch('categories') ?? [];

폼 defaultValues에 categories가 빈 배열로 설정되어 있는지도 확인 부탁드립니다.


17-25: 카테고리 타입 엄격화 및 MAX 초과 피드백 처리

  • category 매개변수는 CategoryType으로 한정하는 것이 안전합니다.
  • MAX를 초과하려는 클릭 시 setError로 즉시 에러 메시지를 노출하세요.
  • setValue 옵션에 shouldDirty/shouldTouch를 추가해 유효성 검증 트리거를 명확히 하세요.
-  const handleCategoryClick = (category: string) => {
-    const newCategories = selectedCategories.includes(category)
+  const handleCategoryClick = (category: (typeof CATEGORIES)[number]) => {
+    if (!selectedCategories.includes(category) && selectedCategories.length >= MAX) {
+      setError('categories', { type: 'max', message: `카테고리는 최대 ${MAX}개까지 선택할 수 있어요` });
+      return;
+    }
+    const newCategories = selectedCategories.includes(category)
       ? selectedCategories.filter(c => c !== category)
       : selectedCategories.length < MAX
         ? [...selectedCategories, category]
         : selectedCategories;
 
-    setValue('categories', newCategories, { shouldValidate: true });
+    if (newCategories.length <= MAX) {
+      clearErrors('categories');
+    }
+    setValue('categories', newCategories, { shouldValidate: true, shouldDirty: true, shouldTouch: true });
   };

46-57: 선택 제한 UX 개선: 비선택 상태에서 MAX 도달 시 버튼 비활성화 + 접근성 속성

MAX에 도달했을 때 선택되지 않은 항목은 disabled 처리하면 불필요한 클릭/검증 호출을 줄일 수 있습니다. 또한 현재 선택 여부를 스크린리더가 알 수 있도록 aria-pressed를 제공하세요. Button 컴포넌트가 임의의 aria props와 disabled를 전달해주는지 확인이 필요합니다.

             <Button
               key={category}
               variant={
                 selectedCategories.includes(category)
                   ? 'enabled'
                   : 'outlined'
               }
               size='category'
               text={category}
+              aria-pressed={selectedCategories.includes(category)}
+              disabled={selectedCategories.length >= MAX && !selectedCategories.includes(category)}
               onClick={() => handleCategoryClick(category)}
             />
src/pages/pinAdd/component/pinReviewInput/PinReviewInput.css.ts (1)

18-21: 중복/비표준 속성 정리 및 사소한 가독성 개선 제안

  • overflowWrap과 wordWrap은 동일 목적의 속성입니다. wordWrap은 오래된 별칭이므로 제거 가능합니다.
  • 텍스트영역 리사이즈를 막으려면 resize: 'none'을 추가할 수 있습니다(디자인 의도에 따라).
   color: vars.color.white,
   overflowWrap: 'break-word',
-  wordWrap: 'break-word',
   wordBreak: 'break-word',
   whiteSpace: 'pre-wrap',
+  resize: 'none',
src/pages/explore/mockup.ts (3)

5-10: ReviewCategory.reviewType를 구체 타입으로 한정

문자열 전반을 허용하면 오타/비일관성 위험이 있습니다. 현재 도메인에 맞춰 'BEST' | CategoryType 유니온으로 한정하세요.

-interface ReviewCategory {
-  reviewType: string;
+interface ReviewCategory {
+  reviewType: 'BEST' | CategoryType;
   description: string;
   places: PlaceData[];
   placeReviewSize: 'SMALL' | 'LARGE';
 }

43-46: mock 데이터 의미상 혼동: placeName에 주소 문자열이 들어가 있습니다

PinResponseDTO.placeName에는 장소명(예: ‘스타벅스 건국대점’)이 들어가는 것이 자연스럽습니다. 주소는 별도 필드가 없다면 placeName을 장소명으로 교체하는 것을 권장합니다.

-      placeName: '서울 광진구 화양동 5-47',
+      placeName: '스타벅스 건국대점',
-      placeName: '서울 광진구 화양동 5-47',
+      placeName: '투썸플레이스 건대점',

Also applies to: 52-55


39-58: 타입 안전성 강화: satisfies 사용 고려

mockupExplore에 satisfies ExploreData[]를 적용하면 과/부족 속성을 컴파일 타임에 엄격히 점검할 수 있습니다. 런타임에는 영향이 없습니다.

예:

export const mockupExplore = [
  /* ... */
] satisfies ExploreData[];
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 43230f7 and ee78151.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (35)
  • package.json (1 hunks)
  • src/pages/explore/Explore.css.ts (1 hunks)
  • src/pages/explore/Explore.tsx (1 hunks)
  • src/pages/explore/ExploreContent.tsx (1 hunks)
  • src/pages/explore/ExploreHeader.tsx (1 hunks)
  • src/pages/explore/ExplorePlaces.tsx (1 hunks)
  • src/pages/explore/mockup.ts (1 hunks)
  • src/pages/pinAdd/PinAdd.css.ts (1 hunks)
  • src/pages/pinAdd/PinAdd.tsx (1 hunks)
  • src/pages/pinAdd/component/confirmPopup/ConfirmPopup.css.ts (1 hunks)
  • src/pages/pinAdd/component/confirmPopup/ConfirmPopup.tsx (1 hunks)
  • src/pages/pinAdd/component/pinCategoriesInput/PinCategoriesInput.css.ts (1 hunks)
  • src/pages/pinAdd/component/pinCategoriesInput/PinCategoriesInput.tsx (1 hunks)
  • src/pages/pinAdd/component/pinPhotoInput/PinPhotoInput.css.ts (1 hunks)
  • src/pages/pinAdd/component/pinPhotoInput/PinPhotoInput.tsx (1 hunks)
  • src/pages/pinAdd/component/pinPlaceInput/PinPlaceInput.css.ts (1 hunks)
  • src/pages/pinAdd/component/pinPlaceInput/PinPlaceInput.tsx (1 hunks)
  • src/pages/pinAdd/component/pinReviewInput/PinReviewInput.css.ts (1 hunks)
  • src/pages/pinAdd/component/pinReviewInput/PinReviewInput.tsx (1 hunks)
  • src/pages/pinAdd/component/pinScoreInput/PinScoreInput.css.ts (1 hunks)
  • src/pages/pinAdd/component/pinScoreInput/PinScoreInput.tsx (1 hunks)
  • src/pages/pinAdd/hook/usePinAddValidation.ts (1 hunks)
  • src/pages/pinDetail/PinDetail.css.ts (1 hunks)
  • src/pages/pinDetail/PinDetail.tsx (1 hunks)
  • src/pages/pinDetail/component/PinFooter.css.ts (1 hunks)
  • src/pages/pinDetail/component/PinFooter.tsx (1 hunks)
  • src/pages/placePins/PlacePins.css.ts (1 hunks)
  • src/pages/placePins/PlacePins.tsx (1 hunks)
  • src/pages/placePins/component/PlacePin.css.ts (1 hunks)
  • src/pages/placePins/component/PlacePin.tsx (1 hunks)
  • src/pages/search/Search.tsx (0 hunks)
  • src/router/Router.tsx (2 hunks)
  • src/router/constant/Routes.ts (1 hunks)
  • src/shared/components/categoryTag/CategoryTag.css.ts (1 hunks)
  • src/shared/components/placeReview/PlaceReview.tsx (2 hunks)
💤 Files with no reviewable changes (1)
  • src/pages/search/Search.tsx
🧰 Additional context used
🧬 Code Graph Analysis (32)
src/pages/explore/Explore.tsx (3)
src/pages/explore/ExploreHeader.tsx (1)
  • ExploreHeader (7-33)
src/pages/explore/ExploreContent.tsx (1)
  • ExploreContent (9-34)
src/pages/explore/ExplorePlaces.tsx (1)
  • ExplorePlaces (5-19)
src/pages/pinDetail/component/PinFooter.css.ts (1)
src/pages/placePins/component/PlacePin.css.ts (2)
  • userWrapper (12-16)
  • scoreWrapper (18-23)
src/router/Router.tsx (6)
src/pages/explore/Explore.tsx (1)
  • Explore (6-14)
src/pages/pinAdd/PinAdd.tsx (1)
  • PinAdd (18-83)
src/pages/pinDetail/PinDetail.tsx (1)
  • PinDetail (8-38)
src/pages/placePins/PlacePins.tsx (1)
  • PlacePins (8-47)
src/router/constant/Routes.ts (1)
  • ROUTES (1-14)
api/Places.ts (1)
  • Places (20-118)
src/pages/pinAdd/component/pinPlaceInput/PinPlaceInput.css.ts (1)
src/shared/components/inputBar/InputBar.tsx (4)
  • InputBar (17-75)
  • styles (36-41)
  • InputBarProps (5-15)
  • styles (43-48)
src/pages/pinDetail/PinDetail.css.ts (1)
src/shared/styles/theme.css.ts (1)
  • vars (3-64)
src/pages/placePins/PlacePins.css.ts (1)
src/shared/styles/theme.css.ts (1)
  • vars (3-64)
src/pages/pinAdd/component/pinScoreInput/PinScoreInput.css.ts (1)
src/shared/styles/theme.css.ts (1)
  • vars (3-64)
src/pages/explore/ExploreHeader.tsx (2)
src/shared/components/inputBar/InputBar.tsx (2)
  • InputBar (18-79)
  • InputBarProps (5-15)
src/shared/components/header/Header.tsx (2)
  • Header (17-46)
  • HeaderProps (5-13)
src/pages/pinAdd/component/pinPlaceInput/PinPlaceInput.tsx (1)
src/shared/components/text/Text.tsx (1)
  • Text (57-69)
src/pages/pinAdd/PinAdd.tsx (6)
src/pages/pinAdd/hook/usePinAddValidation.ts (2)
  • usePinAddValidation (41-53)
  • convertToApiData (33-39)
src/pages/pinAdd/component/pinPlaceInput/PinPlaceInput.tsx (1)
  • PinPlaceInput (6-23)
src/pages/pinAdd/component/pinPhotoInput/PinPhotoInput.tsx (1)
  • PinPhotoInput (5-67)
src/pages/pinAdd/component/pinReviewInput/PinReviewInput.tsx (1)
  • PinReviewInput (10-34)
src/pages/pinAdd/component/pinScoreInput/PinScoreInput.tsx (1)
  • PinScoreInput (11-49)
src/pages/pinAdd/component/pinCategoriesInput/PinCategoriesInput.tsx (1)
  • PinCategoriesInput (12-61)
src/router/constant/Routes.ts (1)
api/Places.ts (1)
  • Places (20-118)
src/pages/pinAdd/component/pinPhotoInput/PinPhotoInput.css.ts (1)
src/shared/styles/theme.css.ts (1)
  • vars (3-64)
src/pages/placePins/PlacePins.tsx (3)
src/pages/explore/mockup.ts (1)
  • mockupExplore (39-58)
src/shared/components/header/Header.tsx (1)
  • Header (17-46)
src/pages/placePins/component/PlacePin.tsx (1)
  • PlacePin (14-35)
src/pages/pinAdd/component/pinScoreInput/PinScoreInput.tsx (4)
src/pages/pinAdd/hook/usePinAddValidation.ts (1)
  • PinAddFormData (18-18)
src/shared/components/text/Text.tsx (1)
  • Text (57-69)
src/shared/components/stars/Stars.tsx (4)
  • Stars (9-30)
  • StarsProps (4-7)
  • index (13-23)
  • _ (27-27)
src/shared/components/stars/SelectStars.tsx (2)
  • SelectStars (7-28)
  • SelectStarsProps (3-5)
src/shared/components/placeReview/PlaceReview.tsx (2)
api/Places.ts (3)
  • placeId (111-117)
  • Places (20-118)
  • placeId (88-94)
src/shared/components/kakaoMap/mockup.ts (1)
  • PinData (9-17)
src/pages/pinAdd/component/pinReviewInput/PinReviewInput.css.ts (1)
src/shared/components/inputBar/InputBar.tsx (1)
  • styles (36-41)
src/pages/pinAdd/component/pinCategoriesInput/PinCategoriesInput.css.ts (2)
src/shared/styles/theme.css.ts (1)
  • vars (3-64)
src/pages/pinAdd/component/confirmPopup/ConfirmPopup.css.ts (1)
  • categoriesGrid (27-34)
src/pages/pinAdd/component/confirmPopup/ConfirmPopup.css.ts (4)
src/shared/styles/theme.css.ts (1)
  • vars (3-64)
src/pages/pinAdd/component/pinCategoriesInput/PinCategoriesInput.css.ts (1)
  • categoriesGrid (19-26)
src/shared/components/popup-overlay/PopupOverlay.stories.tsx (5)
  • Template (61-62)
  • PopupOverlayProps (22-27)
  • Template (70-71)
  • Template (65-66)
  • args (30-58)
src/shared/components/popup-overlay/PopupOverlay.tsx (1)
  • PopupOverlayProps (6-13)
src/pages/explore/ExploreContent.tsx (6)
src/shared/constant/category.ts (1)
  • CategoryType (18-18)
src/shared/components/kakaoMap/mockup.ts (2)
  • mockupPinData (38-84)
  • PinData (9-17)
src/shared/components/categoryTag/CategoryTag.tsx (2)
  • CategoryTag (9-36)
  • category (20-32)
src/shared/components/kakaoMap/constant/mapSize.ts (1)
  • MAP_SIZE (1-5)
src/shared/components/categoryTag/CategoryTag.stories.tsx (1)
  • category (21-23)
src/shared/components/category/Category.tsx (1)
  • Category (10-20)
src/pages/explore/ExplorePlaces.tsx (2)
src/pages/explore/mockup.ts (1)
  • reviewCategories (12-31)
src/shared/components/placeReview/PlaceReview.tsx (1)
  • PlaceReviewList (62-103)
src/pages/placePins/component/PlacePin.css.ts (2)
src/shared/styles/theme.css.ts (1)
  • vars (3-64)
src/pages/pinDetail/component/PinFooter.css.ts (2)
  • userWrapper (12-16)
  • scoreWrapper (23-26)
src/pages/pinAdd/PinAdd.css.ts (2)
src/shared/styles/theme.css.ts (1)
  • vars (3-64)
src/shared/components/inputBar/InputBar.tsx (1)
  • styles (36-41)
src/pages/placePins/component/PlacePin.tsx (4)
api/data-contracts.ts (1)
  • PinResponseDTO (1082-1113)
src/shared/constant/category.ts (1)
  • CategoryType (18-18)
src/shared/components/text/Text.tsx (1)
  • Text (57-69)
src/shared/components/stars/Stars.tsx (1)
  • Stars (9-30)
src/pages/pinDetail/PinDetail.tsx (3)
src/shared/components/header/Header.tsx (1)
  • Header (17-46)
src/shared/components/text/Text.tsx (1)
  • Text (57-69)
src/pages/pinDetail/component/PinFooter.tsx (1)
  • PinFooter (12-52)
src/pages/pinDetail/component/PinFooter.tsx (2)
src/shared/components/text/Text.tsx (1)
  • Text (57-69)
src/shared/components/stars/Stars.tsx (2)
  • Stars (9-30)
  • StarsProps (4-7)
src/pages/explore/Explore.css.ts (1)
src/shared/styles/theme.css.ts (1)
  • vars (3-64)
src/pages/pinAdd/component/pinReviewInput/PinReviewInput.tsx (4)
src/pages/pinAdd/hook/usePinAddValidation.ts (1)
  • PinAddFormData (18-18)
src/shared/components/text/Text.tsx (1)
  • Text (57-69)
api/data-contracts.ts (1)
  • PinResponseDTO (1082-1113)
api/Pin.ts (1)
  • Pin (21-116)
src/pages/pinAdd/component/pinCategoriesInput/PinCategoriesInput.tsx (5)
src/pages/pinAdd/hook/usePinAddValidation.ts (1)
  • PinAddFormData (18-18)
src/shared/components/text/Text.tsx (1)
  • Text (57-69)
src/shared/constant/category.ts (1)
  • CATEGORIES (1-16)
src/shared/components/categoryTag/CategoryTag.tsx (3)
  • CategoryTag (9-36)
  • category (20-32)
  • CategoryTagProps (5-7)
src/shared/components/category/Category.tsx (1)
  • Category (10-20)
src/pages/pinAdd/component/confirmPopup/ConfirmPopup.tsx (4)
src/shared/components/text/Text.tsx (1)
  • Text (57-69)
src/shared/components/stars/Stars.tsx (2)
  • Stars (9-30)
  • StarsProps (4-7)
src/shared/constant/category.ts (1)
  • CATEGORIES (1-16)
src/shared/components/popup-overlay/PopupOverlay.tsx (2)
  • PopupOverlayProps (6-13)
  • open (16-33)
src/pages/pinAdd/hook/usePinAddValidation.ts (2)
src/shared/constant/category.ts (1)
  • CATEGORIES (1-16)
api/Pin.ts (2)
  • Pin (21-116)
  • query (40-73)
src/pages/explore/mockup.ts (3)
src/shared/components/placeReview/mockup.ts (1)
  • PlaceData (1-7)
api/data-contracts.ts (2)
  • PinListResponseDTO (1041-1068)
  • PinResponseDTO (1082-1113)
src/shared/constant/category.ts (1)
  • CategoryType (18-18)
src/pages/pinAdd/component/pinPhotoInput/PinPhotoInput.tsx (2)
src/pages/pinAdd/component/pinPhotoInput/PinPhotoInput.css.ts (1)
  • uploadedImage (21-26)
src/shared/components/text/Text.tsx (1)
  • Text (57-69)
🪛 Biome (2.1.2)
src/pages/placePins/component/PlacePin.tsx

[error] 30-31: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

🔇 Additional comments (22)
package.json (1)

16-29: ✅ react-hook-form 5.x 및 Zod v4 호환성 확인 완료

  • @hookform/resolvers v5.2.1 이상에서 Zod v4를 공식 지원합니다.
  • src/pages/pinAdd/hook/usePinAddValidation.ts 파일에서
    import { zodResolver } from '@hookform/resolvers/zod';
    을 사용한 점이 올바릅니다.
  • useForm({ resolver: zodResolver(pinAddSchema) }) 호출 역시 정상 동작합니다.

따라서 별도 수정 없이 머지하셔도 됩니다.

src/pages/placePins/component/PlacePin.css.ts (2)

12-16: userWrapper 구성 적절

PinFooter.css.ts의 userWrapper와 간격/정렬이 일관적입니다. 재사용성 측면에서도 무난합니다.


18-23: 점수 영역 간격/정렬 적절

아이콘 + 텍스트 케이스에 맞는 정렬과 간격 설정으로 보입니다.

src/pages/pinAdd/component/pinCategoriesInput/PinCategoriesInput.css.ts (1)

4-10: 스타일 토큰 활용 및 레이아웃 구성이 명확합니다

전역 토큰(vars.color.blue0) 사용과 레이아웃 구성이 명확하며, 다른 입력 섹션들과의 시각적 일관성도 좋아 보입니다.

src/pages/pinAdd/hook/usePinAddValidation.ts (1)

41-50: score 기본값(0)과 스키마 최소값(1) 불일치 확인 필요
src/pages/pinAdd/hook/usePinAddValidation.ts의 defaultValues에서 score를 0으로 설정하고 있지만, pinAddSchema에서는 .min(1, '별점을 선택해주세요')를 요구하고 있습니다.

  • 초기 invalid 상태로 의도된 것인지, 사용자 경험이나 검증 타이밍에 영향이 없는지 확인해주세요.
  • 또한, 실제 점수 입력 필드 등록 시 register('score')valueAsNumber 옵션이 적용되어 숫자로 보장되는지, 혹은 Controller를 사용해 숫자 변환이 처리되는지도 점검이 필요합니다.
src/router/Router.tsx (2)

10-13: 지연 로딩 도입 적절

Explore/PinAdd/PinDetail/PlacePins에 lazy 로딩 적용이 적절합니다. 초기 번들 크기 관점에서 긍정적입니다.


37-51: 새 라우트 매핑 적절 (EXPLORE/PIN_ADD/PIN_DETAIL/PLACE_PINS)

경로 정의가 상수와 일치하며, 레이아웃 하위 children 구성도 정상입니다. 통합 시 Suspense 경계만 상위에서 보장되면 무리 없겠습니다.

src/shared/components/placeReview/PlaceReview.tsx (1)

94-96: 핸들러 시그니처 변경 반영 — LGTM

onClick에서 placeId와 placeName을 모두 전달하도록 수정된 부분은 의도에 부합하고 정상 동작으로 보입니다.

src/pages/explore/Explore.tsx (1)

8-12: 페이지 컴포지션 구성 — LGTM

헤더/콘텐츠/플레이스 섹션의 단순 조합이 명확하고 유지보수에 용이합니다.

src/pages/explore/ExploreContent.tsx (1)

23-32: 구성 좋습니다

CategoryTag로 선택 상태를 끌어올려 Map에 필터링된 pinData를 전달하는 흐름이 명확합니다. 초기 상태 'ALL'도 합리적입니다.

src/pages/pinAdd/PinAdd.tsx (1)

50-50: Header 배경 토큰 오타('baroblue' → 'baroBlue' 가능성)

다른 곳에서 사용하는 컬러 토큰이 baroBlue(카멜케이스)로 보입니다. 현재 값은 소문자여서 스타일 변형(recipies/variants) 매칭이 실패할 수 있습니다. 실제 정의된 토큰명으로 수정해 주세요.

-        background='baroblue'
+        background='baroBlue'
⛔ Skipped due to learnings
Learnt from: junghyungo
PR: KUIT-BARO/BARO-FRONTEND#90
File: src/pages/home/Home.tsx:44-46
Timestamp: 2025-08-09T16:50:58.381Z
Learning: BARO-FRONTEND 프로젝트의 Header 컴포넌트의 background prop은 'blue0' | 'baroblue' 타입을 받으며, 'baroblue'는 모두 소문자로 작성하는 것이 올바른 사용법이다.
src/pages/pinAdd/component/pinScoreInput/PinScoreInput.tsx (1)

11-17: 폼 연동 및 에러 처리 방식 적절

react-hook-form과의 연계가 간결하고, 클릭 시 shouldValidate: true로 검증을 트리거하는 부분도 좋습니다.

src/router/constant/Routes.ts (2)

5-8: 라우팅 리뉴얼 방향성 적합

Explore/Pin 관련 라우트 추가와 Search 제거는 본 PR 목표와 정합적입니다.


8-8: 혼용된 경로 문자열 없음 확인

실행 결과:

  • /place/:placeId/pinssrc/router/constant/Routes.tsPLACE_PINS 상수로만 정의·사용되고, 직접 문자열 사용은 없습니다.
  • /search 관련 잔존 코드도 발견되지 않았습니다.
  • ${…} 형태의 /pin/... 경로 비상수 사용도 없습니다.

코드상 상수와 직접 문자열이 혼용되어 있지 않아 혼동 우려는 해소되었습니다. 네이밍 일관화를 원할 경우 별도 검토를 권장합니다.

src/pages/pinDetail/PinDetail.tsx (2)

31-33: 하드코딩된 리뷰 문구 — 실제 데이터 연결 시나리오 확인 필요

현재 더미 텍스트로 되어 있어 사용자 인풋/서버 데이터와 불일치합니다. API/쿼리 파라미터 연동 시 review 렌더링으로 전환되는지 확인 부탁드립니다.

연동 준비가 되면 주석 처리된 review 사용으로 전환하거나 스켈레톤/플레이스홀더 처리 도와드릴 수 있습니다.


23-27: Header 배경 토큰 오타로 스타일 미적용 가능성

background='baroblue'는 casing이 맞지 않아(아마 baroBlue 의도) 스타일 레시피에서 매칭되지 않을 확률이 큽니다. 또한 leftIcon은 아이콘 컴포넌트를 직접 넘기고 클릭 핸들러는 onClickLeftIcon으로 전달하는 편이 Header의 사이즈/클릭 처리 방식과 일관됩니다.

아래처럼 수정 제안드립니다:

       <Header
-        background='baroblue'
-        leftIcon={() => <IcArrowLeft onClick={handleBackClick} />}
+        background='baroBlue'
+        leftIcon={IcArrowLeft}
+        onClickLeftIcon={handleBackClick}
         text={placeName}
       />
⛔ Skipped due to learnings
Learnt from: junghyungo
PR: KUIT-BARO/BARO-FRONTEND#90
File: src/pages/home/Home.tsx:44-46
Timestamp: 2025-08-09T16:50:58.381Z
Learning: BARO-FRONTEND 프로젝트의 Header 컴포넌트의 background prop은 'blue0' | 'baroblue' 타입을 받으며, 'baroblue'는 모두 소문자로 작성하는 것이 올바른 사용법이다.
Learnt from: junghyungo
PR: KUIT-BARO/BARO-FRONTEND#90
File: src/pages/home/Home.tsx:18-31
Timestamp: 2025-08-14T11:50:59.420Z
Learning: In the BARO-FRONTEND project, the team prefers to embed logout button functionality directly within the Header component using inline rightIcon functions rather than using Header's onClickRightIcon prop pattern.
src/pages/pinDetail/component/PinFooter.tsx (1)

30-32: 하드코딩된 핸들(@Jiwhan_lee) — 실제 데이터 필드 확인 필요

닉네임/핸들이 고정 문자열입니다. API 스펙(userName, userEmail, profileImage 등)과 맞춰 동적 데이터 사용 계획을 확인해 주세요.

원하시면 PinResponseDTO를 기준으로 타입/표시 우선순위를 정의해 드립니다.

src/pages/pinAdd/PinAdd.css.ts (1)

2-2: 테마 경로 별칭 불일치 가능성

다른 파일은 @shared/styles/theme.css를 사용하고 본 파일은 @styles/theme.css를 사용합니다. 둘 다 alias가 잡혀 있지 않으면 빌드 에러가 납니다. 경로 통일/검증 부탁드립니다.

필요 시 tsconfig.paths/webpack alias 기준으로 일괄 정리 스크립트 제공 가능.

src/pages/explore/Explore.css.ts (1)

11-22: Sticky 헤더 구현 깔끔합니다

top, z-index, 배경 지정까지 포함되어 스크롤시 안정적으로 동작할 것으로 보입니다.

src/pages/pinAdd/component/confirmPopup/ConfirmPopup.tsx (1)

61-73: 액션 영역 구성 적절함

확인/수정 콜 투 액션이 명확하고, 핸들러 연결도 적절합니다.

src/pages/pinAdd/component/pinCategoriesInput/PinCategoriesInput.tsx (1)

37-41: 에러 메시지 표출 처리 LGTM

폼의 categories 필드 오류를 즉시 노출하는 흐름이 명확합니다.

src/pages/explore/mockup.ts (1)

33-37: ExploreData.pinId의 선택적 타입 여부 확인

PinListResponseDTO['pinId']는 API 타입 상 optional입니다. 실제 ExploreData에서 pinId가 항상 필수라면 number로 고정하는 편이 사용처에서 안전합니다. 반대로 optional일 수 있다면 사용처에서 undefined 처리를 고려해야 합니다.

Comment on lines 16 to 20
const placeName = searchParams.get('placeName') || '';
const userName = searchParams.get('userName') || '';
// const review = searchParams.get('review') || '';
const score = parseInt(searchParams.get('score') || '0');

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

빈 src 이미지 렌더링은 불필요한 요청/브로큰 이미지 유발

<img src='' />는 브라우저가 현재 문서를 재요청하거나 브로큰 이미지를 표시할 수 있습니다. 이미지가 없으면 대체 블록을 렌더링하도록 조정하는 것이 안전합니다.

아래와 같이 searchParam에서 선택적 이미지 주소를 읽고 조건부 렌더링을 적용해 주세요:

   const placeName = searchParams.get('placeName') || '';
   const userName = searchParams.get('userName') || '';
   // const review = searchParams.get('review') || '';
   const score = parseInt(searchParams.get('score') || '0');
+  const imageSrc = searchParams.get('image') || '';

       <div className={styles.pinDetailContainer}>
-        <img src='' alt='핀 장소 이미지' className={styles.pinPlaceImage} />
+        {imageSrc ? (
+          <img src={imageSrc} alt='핀 장소 이미지' className={styles.pinPlaceImage} />
+        ) : (
+          <div className={styles.pinPlaceImage} aria-label='핀 장소 이미지 없음' />
+        )}
         <Text tag='body_16' color='white'>
           {/* {review} */}
           분위기가 너무 좋아서 오래 머물고 싶어지는 카페였어요. 창가 자리에서 햇빛을 받으며 책을 읽기 딱 좋았습니다.
         </Text>

Also applies to: 28-33

🤖 Prompt for AI Agents
In src/pages/pinDetail/PinDetail.tsx around lines 16-20 (and similarly for lines
28-33), the code pulls values from searchParams but may pass an empty string
into an <img src=""> which causes unnecessary requests or broken-image UI;
change the logic to read the optional image URL from searchParams (e.g., const
imageUrl = searchParams.get('image') || '') then conditionally render the <img>
only when imageUrl.trim() is non-empty (otherwise render a placeholder div or
nothing), ensuring you never pass an empty string into the img src attribute.

@KUIT-BARO KUIT-BARO deleted a comment from coderabbitai bot Aug 17, 2025
@KUIT-BARO KUIT-BARO deleted a comment from coderabbitai bot Aug 17, 2025
@github-actions
Copy link

✅ 빌드에 성공했습니다! 🎉

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/shared/components/placeReview/PlaceReview.css.ts (1)

62-69: 세로 정렬 보완 제안: alignItems: 'center' 추가 권장

Stars 컴포넌트와 숫자/텍스트가 함께 배치된다면, 교차축(세로) 정렬이 미세하게 어긋날 수 있습니다. alignItems: 'center'를 추가해 정렬 안정성을 높이는 것을 권장합니다.

 export const placeReviewRatingContainer = style({
   display: 'flex',
+  alignItems: 'center',
   gap: '0.4rem',
   justifyContent: 'center',
   textAlign: 'center',
   width: '100%',
   padding: '0.4rem 0 1.3rem 0',
 });
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ee78151 and 9fa2fa2.

📒 Files selected for processing (6)
  • src/pages/pinDetail/PinDetail.tsx (1 hunks)
  • src/pages/pinDetail/component/PinFooter.tsx (1 hunks)
  • src/pages/placePins/PlacePins.tsx (1 hunks)
  • src/pages/placePins/component/PlacePin.tsx (1 hunks)
  • src/shared/components/placeReview/PlaceReview.css.ts (1 hunks)
  • src/shared/components/placeReview/PlaceReview.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/shared/components/placeReview/PlaceReview.tsx
  • src/pages/pinDetail/PinDetail.tsx
  • src/pages/placePins/component/PlacePin.tsx
  • src/pages/pinDetail/component/PinFooter.tsx
  • src/pages/placePins/PlacePins.tsx
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/shared/components/placeReview/PlaceReview.css.ts (2)
src/shared/components/stars/Stars.tsx (2)
  • StarsProps (4-7)
  • Stars (9-30)
src/shared/components/container/Container.tsx (1)
  • Container (9-15)
🔇 Additional comments (2)
src/shared/components/placeReview/PlaceReview.css.ts (2)

62-69: gap 추가 👍 — 별/텍스트 간 간격 제어가 명확해졌습니다

gap: '0.4rem' 추가로 자식 간 간격이 일관되게 관리됩니다. 레이아웃 가독성 측면에서 적절한 선택입니다.


64-64: Stars 컴포넌트 내부 여백 설정 없음 확인

Stars 관련 코드(SelectStars.tsx, Stars.css.ts)에서 margin 혹은 gap 속성이 전혀 사용되지 않음을 확인했습니다. 따라서 부모 컨테이너의 gap: 0.4rem만으로도 의도한 간격이 유지되며, 중복 여백 문제는 발생하지 않습니다. 현재 상태를 그대로 유지하셔도 좋습니다.

Copy link
Contributor

@hamxxn hamxxn left a comment

Choose a reason for hiding this comment

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

너무 수고 많았습니다!

categories: string[];
}

const ConfirmPopup: React.FC<ConfirmPopupProps> = ({
Copy link
Contributor

Choose a reason for hiding this comment

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

rfc

export default function PlacePins() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const placeName = searchParams.get('placeName') || '핀 목록';
Copy link
Contributor

Choose a reason for hiding this comment

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

or 뒤에 빼도 될 것 같아요~

const queryParams = new URLSearchParams({
placeName: placeName,
userName: item.pin.userName || '',
score: item.pin.score?.toString() || '0',
Copy link
Contributor

Choose a reason for hiding this comment

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

score 까지 파라미터에 넣으신 이유 있으실까요??

Copy link
Member Author

Choose a reason for hiding this comment

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

image

핀 열람 페이지 (/pin/:pinId)에 해당 핀의 점수도 전달해주기 위함이었습니다,

navigate(-1);
};

const handlePinClick = (item: typeof mockupExplore[0]) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

타입 api 폴더 아래에 있는거 사용하면 될 것 같아요!
없다면 types 안에 만들어두고 이후 해당 api 만들어지면 타입 삭제해주세요~

}

return (
<button
Copy link
Contributor

Choose a reason for hiding this comment

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

해당 핀의 위치를 찍어주는게 나을 것 같아요!
버튼은 필요없을 것 같습니다!

Copy link
Member Author

@junghyungo junghyungo Aug 21, 2025

Choose a reason for hiding this comment

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

image

핀 등록 페이지의 장소 선택 화면을 구현하지 못했는데, 클릭으로 이 페이지로 넘어가서 장소위치를 선택하려면 버튼이 낫지 않을까요?

wrap='soft'
/>
{errors.review && (
<Text tag='body_10' color='red1' className={styles.errorMessage}>
Copy link
Contributor

Choose a reason for hiding this comment

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

150자를 넘겼을 때 에러 메시지가 안보이고,
150/150 text가 사용자가 작성한 텍스트랑 겹쳐서 보여요!
수정 부탁드립니다!

)}
</div>
<div className={styles.starsContainer}>
{[1, 2, 3, 4, 5].map((starIndex) => (
Copy link
Contributor

Choose a reason for hiding this comment

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

Array.from({ length: 5 }, (_, index) => index + 1)
로 바꿔보는건 어떨까요??

mode: 'onChange',
});

return form;
Copy link
Contributor

Choose a reason for hiding this comment

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

{ register, watch, formState: { errors } } 만 사용하고 있는 것 같은데 해당 부분만 return 하는건 어떨까요??

Copy link
Member Author

Choose a reason for hiding this comment

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

PinAdd.tsx에서 다른것도 사용하기 때문에 그대로 두겠습니다,,


export default function PinAdd() {
const navigate = useNavigate();
const form = usePinAddValidation();
Copy link
Contributor

Choose a reason for hiding this comment

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

{ handleSubmit, watch } 로 받아오는 건 어떨까요??

Copy link
Member Author

Choose a reason for hiding this comment

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

아하!

},
];

export interface ExploreData {
Copy link
Contributor

Choose a reason for hiding this comment

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

해당 타입 api 안에 있을 것 같은데 없을까요??

Copy link
Member Author

Choose a reason for hiding this comment

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

수정했습니다!

Copy link
Contributor

@Hwanggyuun Hwanggyuun left a comment

Choose a reason for hiding this comment

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

popup 공컴 제가 고치겠습니다...

};

export const usePinAddValidation = () => {
const form = useForm<PinAddFormData>({
Copy link
Contributor

Choose a reason for hiding this comment

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

form 대신에 { register, watch, formState: { errors } } 넣고 return { register, watch, errors } 같은 느낌으로 사용할 수 있습니다!

export const pinFooterWrapper = style({
display: 'flex',
justifyContent: 'space-between',
position: 'fixed',
Copy link
Contributor

Choose a reason for hiding this comment

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

혹시 pinfooter가 fixed 가 맞나요? 저 화면을 위 아래로 드래그 해보는데 살짝 이상한 감이 있어서 여쭤봅니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

sticky로 수정하겠슴니다

display: 'flex',
flexDirection: 'column',
width: '100%',
padding: '16px',
Copy link
Contributor

Choose a reason for hiding this comment

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

rem 빼놓으신거 같습니다!

@github-actions
Copy link

✅ 빌드에 성공했습니다! 🎉

@github-actions
Copy link

✅ 빌드에 성공했습니다! 🎉

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (2)
src/pages/pinAdd/hook/usePinAddValidation.ts (1)

41-55: 훅 반환 형태 단순화(중첩 제거)로 사용성 개선

현재 return { register, watch, formState: { errors }, ... }로 중첩된 형태입니다. 소비 측에서 매번 중첩 해제를 피하려면 errors를 평탄화해서 반환하는 편이 사용성이 좋습니다. 과거 코멘트와도 일치합니다.

-  const { register, watch, formState: { errors }, setValue, handleSubmit } = form;
-
-  return { register, watch, formState: { errors }, setValue, handleSubmit };
+  const { register, watch, formState: { errors }, setValue, handleSubmit } = form;
+  return { register, watch, errors, setValue, handleSubmit };
src/pages/pinAdd/component/pinScoreInput/PinScoreInput.tsx (1)

33-33: 사소하지만 좋은 변경: Array.from 사용 👍

과거 제안대로 Array.from({ length: 5 }, (_, index) => index + 1) 적용되어 가독성과 안전성이 좋아졌습니다.

🧹 Nitpick comments (11)
src/pages/placePins/types/Pins.ts (1)

6-7: PinResponseDTO의 전역 optional 특성으로 인한 UI 불안정성 완화

PinResponseDTO는 대부분 필드가 optional입니다. 소비 컴포넌트(PlacePin)에서 userName, profileImage, placeName 등을 바로 사용하므로 최소한 이들만이라도 필수로 강제한 좁은 타입을 두면 안정적입니다.

예시:

// (추가) 최소 표시 요건을 강제하는 타입
export type PinForList = Omit<PinResponseDTO, 'userName' | 'profileImage' | 'placeName'> & {
  userName: string;
  profileImage: string;
  placeName: string;
  // score는 UI에서 기본값 처리 가능하면 optional 유지
  score?: number;
};
-  pin: PinResponseDTO;
+  pin: PinForList;

또는 소비처에서 안전 가드(placeholder 이미지, 익명 사용자명 등)를 추가하는 방향도 가능합니다. 둘 중 하나를 선택해 정합성을 맞춰 주세요.

src/pages/placePins/PlacePins.tsx (1)

36-41: 리스트 key에 index 사용 — 재정렬/삽입 시 재사용 버그 유발 가능

React 리스트에서 index를 key로 쓰면 항목 삽입/삭제/정렬 시 리렌더링 문제가 발생합니다. pinId를 사용해 안정 키를 보장해 주세요.

-        {mockupExplore.map((item, index) => (
+        {mockupExplore.map((item) => (
           <PlacePin
-            key={index}
+            key={item.pinId}
             pin={item.pin}
             categories={item.categories}
             handlePinClick={() => handlePinClick(item)}
           />
         ))}
src/pages/explore/mockup.ts (1)

32-57: PinListData 목업과 카테고리 직렬화 일관성 체크

  • categoriesCategoryType[]으로 제한되므로 실제 상수 CATEGORIES에 존재하는 값만 사용되는지 확인해 주세요. 빗나가면 빌드 타임에 잡히지만, 디자인/카피 변경 시 싱크가 흔들릴 수 있습니다.
  • placeName에 주소가 들어가 있는데, 의미상 장소명과 주소가 분리돼야 한다면 필드 구분을 명확히 해두는 것이 좋습니다. (API에 placeAddress가 별도로 존재)
src/pages/pinAdd/hook/usePinAddValidation.ts (3)

10-12: score 스키마는 z.coerce.number() 사용 권장

향후 점수 입력 방식을 바꾸거나 네이티브 input을 사용할 경우 문자열이 들어올 수 있습니다. z.coerce.number()로 방어해 두면 안정적입니다.

-  score: z.number()
+  score: z.coerce.number()
     .min(1, '별점을 선택해주세요')
     .max(5, '별점은 최대 5점입니다'),

13-15: 카테고리 값 범위 제한 및 'ALL' 선택 방지

현재 z.array(z.string())라 임의 문자열이 통과합니다. 런타임에서 CATEGORIES에 포함된 값만 허용하고, 목록 상 첫 항목인 'ALL'은 서버 전송 대상이 아닐 가능성이 높습니다. 검증 단계에서 차단해 주세요.

-  categories: z.array(z.string())
+  categories: z.array(
+    z.string().refine(
+      (v) => (CATEGORIES as readonly string[]).includes(v) && v !== 'ALL',
+      '유효한 카테고리를 선택해주세요',
+    )
+  )
     .min(1, '카테고리를 최소 1개 선택해주세요')
     .max(5, '카테고리는 최대 5개까지 선택 가능합니다')

백엔드 계약에 'ALL'이 전송 가능한지 확인 부탁드립니다. 전송 불가라면 위와 같이 스키마에서 막는 게 안전합니다.


44-48: 초기값과 검증 규칙의 긴장 관계 점검(Score=0, Categories=[])

기본값이 score: 0, categories: []이고 검증은 최소 1개/1점입니다. 모드가 onChange라 초기 에러는 보이지 않겠지만, 제출 시 막히는 흐름이 의도인지 확인 필요합니다. 만약 가이드성 오류 표시를 초기부터 노출하려면 mode: 'onTouched' 또는 UI 힌트를 병행하세요.

src/pages/pinAdd/component/pinReviewInput/PinReviewInput.tsx (2)

18-25: DOM 직접 수정 대신 RHF 상태 갱신만으로 제한 적용

target.value 직접 수정은 RHF의 언컨트롤드 모델과 충돌 여지가 있습니다. 브라우저의 maxLength가 이미 존재하므로, 붙잡아야 한다면 setValue만 사용하고 검증/dirty 반영 옵션을 켜 주세요.

-  const handleInput = (e: React.FormEvent<HTMLTextAreaElement>) => {
-    const target = e.target as HTMLTextAreaElement;
-    if (target.value.length > maxLength) {
-      const truncatedValue = target.value.slice(0, maxLength);
-      target.value = truncatedValue;
-      setValue('review', truncatedValue);
-    }
-  };
+  const handleInput = (e: React.FormEvent<HTMLTextAreaElement>) => {
+    const target = e.target as HTMLTextAreaElement;
+    if (target.value.length > maxLength) {
+      const truncatedValue = target.value.slice(0, maxLength);
+      setValue('review', truncatedValue, { shouldValidate: true, shouldDirty: true });
+    }
+  };

33-36: onInput 대신 onChange 사용 고려

RHF는 기본적으로 onChange 기반입니다. 이벤트를 통일하면 부수효과를 줄일 수 있습니다. 필요 시 아래처럼 교체하세요.

-        onInput={handleInput}
+        onChange={handleInput}
src/pages/pinAdd/component/pinScoreInput/PinScoreInput.tsx (3)

23-25: Text 컴포넌트의 tag 변형값(body_17) 유효성 확인

디자인 토큰에 body_17 변형이 실제로 존재하는지 확인 필요합니다. 존재하지 않으면 기본값으로 폴백되거나 스타일 경고가 발생할 수 있습니다. 필요 시 body_16 등 유효한 값으로 교체하세요.


16-18: 별점 클릭 시 dirty 플래그도 함께 설정

검증만이 아니라 dirty 상태도 필요한 곳이 많습니다. shouldDirty: true를 함께 지정해 주세요.

-    setValue('score', starIndex, { shouldValidate: true });
+    setValue('score', starIndex, { shouldValidate: true, shouldDirty: true });

33-46: 접근성(a11y) 보강: 라벨과 pressed 상태 제공

별 버튼은 시각적 아이콘만 있어 스크린리더 접근성이 낮습니다. aria-label, aria-pressed, title을 추가해 주세요.

-          <button
+          <button
             key={starIndex}
             type="button"
             className={styles.starButton}
-            onClick={() => handleStarClick(starIndex)}
+            onClick={() => handleStarClick(starIndex)}
+            aria-label={`${starIndex}점 선택`}
+            aria-pressed={score >= starIndex}
+            title={`${starIndex}점`}
           >
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 70cf9ab and 2c37069.

📒 Files selected for processing (12)
  • src/pages/explore/mockup.ts (1 hunks)
  • src/pages/pinAdd/PinAdd.tsx (1 hunks)
  • src/pages/pinAdd/component/confirmPopup/ConfirmPopup.tsx (1 hunks)
  • src/pages/pinAdd/component/pinCategoriesInput/PinCategoriesInput.tsx (1 hunks)
  • src/pages/pinAdd/component/pinReviewInput/PinReviewInput.css.ts (1 hunks)
  • src/pages/pinAdd/component/pinReviewInput/PinReviewInput.tsx (1 hunks)
  • src/pages/pinAdd/component/pinScoreInput/PinScoreInput.tsx (1 hunks)
  • src/pages/pinAdd/hook/usePinAddValidation.ts (1 hunks)
  • src/pages/pinDetail/component/PinFooter.css.ts (1 hunks)
  • src/pages/placePins/PlacePins.tsx (1 hunks)
  • src/pages/placePins/component/PlacePin.css.ts (1 hunks)
  • src/pages/placePins/types/Pins.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/pages/pinAdd/component/confirmPopup/ConfirmPopup.tsx
  • src/pages/pinAdd/PinAdd.tsx
  • src/pages/pinAdd/component/pinReviewInput/PinReviewInput.css.ts
  • src/pages/placePins/component/PlacePin.css.ts
  • src/pages/pinDetail/component/PinFooter.css.ts
  • src/pages/pinAdd/component/pinCategoriesInput/PinCategoriesInput.tsx
🧰 Additional context used
🧬 Code Graph Analysis (6)
src/pages/placePins/types/Pins.ts (2)
api/data-contracts.ts (2)
  • PinListResponseDTO (1041-1068)
  • PinResponseDTO (1082-1113)
src/shared/constant/category.ts (1)
  • CategoryType (18-18)
src/pages/pinAdd/component/pinScoreInput/PinScoreInput.tsx (2)
src/pages/pinAdd/hook/usePinAddValidation.ts (1)
  • PinAddFormData (18-18)
src/shared/components/text/Text.tsx (1)
  • Text (57-69)
src/pages/explore/mockup.ts (4)
src/shared/components/placeReview/mockup.ts (1)
  • PlaceData (1-7)
src/pages/placePins/types/Pins.ts (1)
  • PinListData (4-8)
api/data-contracts.ts (1)
  • PinResponseDTO (1082-1113)
src/shared/components/placeReview/PlaceReview.tsx (2)
  • PlaceReviewListProps (17-22)
  • PlaceReviewProps (8-15)
src/pages/pinAdd/hook/usePinAddValidation.ts (2)
src/shared/constant/category.ts (1)
  • CATEGORIES (1-16)
api/Pin.ts (2)
  • Pin (21-116)
  • query (40-73)
src/pages/pinAdd/component/pinReviewInput/PinReviewInput.tsx (2)
src/pages/pinAdd/hook/usePinAddValidation.ts (1)
  • PinAddFormData (18-18)
src/shared/components/text/Text.tsx (1)
  • Text (57-69)
src/pages/placePins/PlacePins.tsx (4)
src/pages/placePins/types/Pins.ts (1)
  • PinListData (4-8)
src/shared/components/header/Header.tsx (1)
  • Header (17-46)
src/pages/explore/mockup.ts (1)
  • mockupExplore (32-57)
src/pages/placePins/component/PlacePin.tsx (1)
  • PlacePin (14-35)
🔇 Additional comments (3)
src/pages/placePins/PlacePins.tsx (1)

25-26: 라우트 파라미터에 undefined가 들어갈 가능성 확인 필요

/pin/${item.pinId}에서 pinIdundefined이면 잘못된 경로가 생성됩니다. 상위 타입을 위 코멘트대로 필수화(NonNullable)했는지 확인하거나, 런타임 가드로 방어 로직을 추가해 주세요.

src/pages/explore/mockup.ts (1)

11-30: 리뷰 카테고리 목업 구조 적절 — 컴포넌트 소비 요건과 합치

ReviewCategoryreviewCategories 구성은 PlaceReview 컴포넌트의 요구사항과 일치합니다. 목업 단계에서 충분히 명확합니다.

src/pages/pinAdd/component/pinReviewInput/PinReviewInput.tsx (1)

37-41: 에러 메시지 표시 안정성

문구가 길어질 때 레이아웃 겹침 이슈가 과거 보고되었습니다. CSS에서 줄바꿈/여백(word-break, line-height, margin-top)을 보장하는지 한번 더 확인 부탁드립니다.


export default function PinReviewInput({ register, watch, errors, setValue }: PinReviewInputProps) {
const reviewValue = watch('review');
const maxLength = 150;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

여기서의 150자 상한과 스키마의 149자 상한이 불일치

maxLength = 150인데 검증 스키마는 .max(149)라 제출이 막힐 수 있습니다. 스키마를 150으로 맞추는 쪽이 자연스럽습니다. 관련 코멘트는 훅 파일에 상세히 남겼습니다.

🤖 Prompt for AI Agents
In src/pages/pinAdd/component/pinReviewInput/PinReviewInput.tsx around line 16,
the local maxLength constant is set to 150 but the validation schema uses
.max(149), causing submissions to be rejected; update the validation schema to
.max(150) to match this constant (or alternatively change the constant to 149 if
you prefer the stricter limit), and ensure any related hook/file comments and
tests reflect the chosen limit so validation and UI remain consistent.

Comment on lines 26 to 31
// 카테고리 문자열 인덱스로 변환
export const convertToIds = (categories: string[]): number[] => {
return categories
.map(category => (CATEGORIES as readonly string[]).indexOf(category))
.filter(index => index !== -1);
};
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

카테고리 ID 변환 시 'ALL' 제외, 중복 제거, 미존재 값 방어

현재는 인덱스 매핑만 수행합니다. 'ALL'(인덱스 0) 포함 위험, 중복, 미존재(-1) 처리 등이 혼재할 수 있습니다. 아래처럼 보강을 권장합니다.

 export const convertToIds = (categories: string[]): number[] => {
-  return categories
-    .map(category => (CATEGORIES as readonly string[]).indexOf(category))
-    .filter(index => index !== -1);
+  const ids = categories
+    .filter((c) => c !== 'ALL')
+    .map((c) => (CATEGORIES as readonly string[]).indexOf(c))
+    // 0: 'ALL' 제외, -1: 미존재 값 제외
+    .filter((idx) => idx > 0);
+  // 중복 제거
+  return Array.from(new Set(ids));
 };

서버에서 기대하는 categoryIds가 위 인덱스 체계(상수 배열 기반)와 일치하는지도 함께 확인 부탁드립니다.


CATEGORIES 상수 배열 정의를 확인하여 ‘ALL’이 실제로 인덱스 0에 위치하는지, 배열 내용이 어떻게 구성되어 있는지 검증합니다.


🏁 Script executed:

#!/bin/bash
# CATEGORIES 상수 정의 위치 및 내용 조회
rg -nP "export\s+const\s+CATEGORIES\s*[:=]\s*\[" -C2 --glob "*.ts"

Length of output: 216


CATEGORIES 상수 배열 전체를 확인하고, categoryIds가 사용되는 위치를 검색해 서버가 기대하는 형태를 파악합니다.


🏁 Script executed:

#!/bin/bash
# CATEGORIES 상수 전체 내용 출력
sed -n '1,200p' src/shared/constant/category.ts

# categoryIds 사용 위치 검색
rg -n "categoryIds" -C3 --glob "*.ts"

Length of output: 1488


convertToIds 함수에서 ‘ALL’ 제외·중복 제거 로직 추가 필요

현재 CATEGORIES[0]이 ‘ALL’이므로, 기본 구현 시 ‘ALL’을 0으로 서버에 전송할 수 있습니다. 또한 중복·미존재(-1) 값 필터링이 필요합니다.

  • 파일: src/pages/pinAdd/hook/usePinAddValidation.ts
  • 위치: 26–31행 (convertToIds 함수)

제안하는 수정 사항:

 export const convertToIds = (categories: string[]): number[] => {
-  return categories
-    .map(category => (CATEGORIES as readonly string[]).indexOf(category))
-    .filter(index => index !== -1);
+  const ids = categories
+    // ‘ALL’ 제외
+    .filter(c => c !== 'ALL')
+    // 문자열 → 인덱스 변환
+    .map(c => (CATEGORIES as readonly string[]).indexOf(c))
+    // 0(‘ALL’) 및 -1(미존재) 제거
+    .filter(idx => idx > 0);
+  // 중복 제거
+  return Array.from(new Set(ids));
 };

서버가 기대하는 categoryIds 인덱스 체계(0 기반 CATEGORIES 배열)와 일치하는지 꼭 확인 부탁드립니다.

📝 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 convertToIds = (categories: string[]): number[] => {
return categories
.map(category => (CATEGORIES as readonly string[]).indexOf(category))
.filter(index => index !== -1);
};
// 카테고리 문자열 인덱스로 변환
export const convertToIds = (categories: string[]): number[] => {
const ids = categories
// ‘ALL’ 제외
.filter(c => c !== 'ALL')
// 문자열 → 인덱스 변환
.map(c => (CATEGORIES as readonly string[]).indexOf(c))
// 0(‘ALL’) 및 -1(미존재) 제거
.filter(idx => idx > 0);
// 중복 제거
return Array.from(new Set(ids));
};
🤖 Prompt for AI Agents
In src/pages/pinAdd/hook/usePinAddValidation.ts around lines 26 to 31,
convertToIds currently maps categories to CATEGORIES indexes but can return 0
for the 'ALL' entry and include duplicates or -1; update the function to (1) map
each category to its index in CATEGORIES, (2) filter out -1 and the 'ALL' index
(assume CATEGORIES[0] === 'ALL' so exclude index 0), and (3) remove duplicate
indexes before returning so the resulting number[] contains only valid, unique
0-based category IDs expected by the server.

@github-actions
Copy link

github-actions bot commented Sep 7, 2025

✅ 빌드에 성공했습니다! 🎉

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
src/pages/pinAdd/hook/usePinAddValidation.ts (3)

69-71: 반환 형태 슬림화는 선택(팀 컨벤션 우선)

현재 반환은 충분히 사용 가능하므로 유지해도 무방합니다. 다만 필요 시 { register, watch, errors, setValue, handleSubmit } 형태로 단순화 가능합니다.


18-21: 리뷰 글자 수 한도 불일치(149 vs 150) — 실제 전송 불가 UX 가능

UI가 150자를 허용한다면 스키마도 150자로 맞춰주세요.

   review: z.string()
     .min(1, '리뷰를 작성해주세요')
-    .max(149, '리뷰는 150자 이하로 작성해주세요'),
+    .max(150, '리뷰는 150자 이하로 작성해주세요'),

43-47: 카테고리 ID 변환: 'ALL' 제외·중복 제거 필요

CATEGORIES[0]이 'ALL'이라면 0이 서버로 나갈 수 있습니다. 또한 중복/미존재(-1) 제거가 필요합니다.

 export const convertToIds = (categories: string[]): number[] => {
-  return categories
-    .map(category => (CATEGORIES as readonly string[]).indexOf(category))
-    .filter(index => index !== -1);
+  const ids = categories
+    .filter((c) => c !== 'ALL')
+    .map((c) => (CATEGORIES as readonly string[]).indexOf(c))
+    // 0('ALL') 및 -1(미존재) 제외
+    .filter((idx) => idx > 0);
+  // 중복 제거
+  return Array.from(new Set(ids));
 };

서버가 기대하는 인덱스 체계(CATEGORIES 기반 0/1 시작 여부)를 한번만 더 확인 부탁드립니다.

🧹 Nitpick comments (10)
src/pages/pinAdd/hook/usePinAddValidation.ts (3)

21-23: score 문자열 입력 대비: z.coerce.number() 사용 권장

브라우저 입력은 문자열이 기본입니다. 안전하게 number로 강제하세요.

-  score: z.number()
+  score: z.coerce.number()
     .min(1, '별점을 선택해주세요')
     .max(5, '별점은 최대 5점입니다'),

확인: PinScoreInput에서 register 시 valueAsNumber를 쓰지 않는다면 위 변경이 필요합니다.


27-31: SSR/테스트 환경에서 File 전역 부재 가능성

Next/SSR·테스트 런타임에서 File이 없으면 schema 평가 시 ReferenceError가 날 수 있습니다. 안전 가드로 대체를 고려해주세요.

예시(아이디어):

const fileSchema = z.custom<File | undefined>((v) =>
  v === undefined || (typeof File !== 'undefined' && v instanceof File)
);

image: fileSchema
  .optional()
  .refine(validateImageFile, { message: 'jpg, jpeg, png, webp 형식의 이미지 파일만 업로드 가능합니다.' });

6-15: 이미지 확장자/검증 로직 중복 — 단일 유틸로 DRY

본 훅과 PinPhotoInput 모두 같은 상수·검증을 중복합니다. 공용 유틸로 분리해 단일 소스로 관리하세요.

예시(새 파일): src/shared/utils/imageValidation.ts

export const ALLOWED_IMAGE_EXTENSIONS = ['jpg','jpeg','png','webp'] as const;

export const isAllowedImageFile = (file?: File | null) => {
  if (!file) return true;
  const ext = file.name.toLowerCase().split('.').pop();
  return !!ext && ALLOWED_IMAGE_EXTENSIONS.includes(ext);
};

이 파일을 본 훅과 PinPhotoInput에서 import해 사용하세요.

src/pages/pinAdd/component/pinPhotoInput/PinPhotoInput.tsx (7)

12-20: 이미지 검증 상수/로직 중복 — 공용 유틸로 통합 권장

usePinAddValidation와 동일 로직이 중복됩니다. 공용 유틸(예: src/shared/utils/imageValidation.ts)로 이동해 단일 진실 공급원을 유지하세요. MIME 타입도 함께 점검하면 위장 파일 방지에 도움됩니다.

예시:

// validateImageFile 내부에서 ext + file.type 모두 확인
const allowedTypes = new Set(['image/jpeg','image/png','image/webp']);
return (!!ext && ALLOWED_IMAGE_EXTENSIONS.includes(ext)) && allowedTypes.has(file.type);

39-41: 파일 선택 시 즉시 검증 트리거

setValue 호출에 shouldValidate를 추가해 폼 에러 상태를 즉시 반영하세요.

-    setValue('image', file);
+    setValue('image', file, { shouldValidate: true, shouldDirty: true });

46-49: 파일 읽기 오류 시 폼 상태 정리

read 실패 시 RHF 값도 초기화하고 재검증을 트리거하세요.

   reader.onerror = () => {
     alert('파일을 읽는 중 오류가 발생했습니다.');
+    setUploadedImage(null);
+    setValue('image', undefined, { shouldValidate: true });
+    if (fileInputRef.current) fileInputRef.current.value = '';
   };

58-65: 이미지 제거 시 즉시 검증 트리거

삭제 시에도 shouldValidate를 추가해 에러 메시지/상태를 즉시 반영하세요.

-    setUploadedImage(null);
-    setValue('image', undefined);
+    setUploadedImage(null);
+    setValue('image', undefined, { shouldValidate: true, shouldDirty: true });

31-37: 유효하지 않은 파일 선택 UX

현재는 alert 후 input만 리셋합니다. 이전 유효 이미지가 있었다면 유지할지, 강제로 제거할지 정책을 명확히 하세요. 강제 제거를 원하면 setValue(...undefined, { shouldValidate: true })를 추가하세요.


85-92: 접근성(a11y) 향상: 키보드 포커스/역할 추가

플레이스홀더 div에 role/button, tabIndex, onKeyDown(Enter/Space)을 부여해 키보드 접근성을 확보하세요.

-          <div
-            className={styles.uploadPlaceholder}
-            onClick={handlePhotoContainerClick}
-          >
+          <div
+            className={styles.uploadPlaceholder}
+            role="button"
+            tabIndex={0}
+            onClick={handlePhotoContainerClick}
+            onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ') && handlePhotoContainerClick()}
+          >

67-101: 전반 구현은 명확하고 일관적입니다

미리보기, 제거, 에러 표시 흐름이 단순·명확합니다. 소소한 개선만 반영하면 좋겠습니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2c37069 and 5f3eb63.

📒 Files selected for processing (3)
  • src/pages/pinAdd/PinAdd.tsx (1 hunks)
  • src/pages/pinAdd/component/pinPhotoInput/PinPhotoInput.tsx (1 hunks)
  • src/pages/pinAdd/hook/usePinAddValidation.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/pages/pinAdd/PinAdd.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/pages/pinAdd/hook/usePinAddValidation.ts (2)
api/Pin.ts (2)
  • Pin (21-116)
  • query (40-73)
api/data-contracts.ts (1)
  • PinResponseDTO (1082-1113)

@KUIT-BARO KUIT-BARO deleted a comment from coderabbitai bot Sep 7, 2025
@github-actions
Copy link

github-actions bot commented Sep 7, 2025

✅ 빌드에 성공했습니다! 🎉

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/pages/pinAdd/component/pinReviewInput/PinReviewInput.tsx (1)

13-15: UI 상한(150) ↔ 검증 스키마 상한 일치 재확인 필요

과거 코멘트에서 .max(149) 불일치 지적이 있었습니다. 현재도 150로 통일되었는지 확인 부탁드립니다. 통일되지 않았다면 스키마를 150으로 맞추거나 상수를 공유하세요.

아래 스크립트로 최대 길이 정의를 찾아 일치 여부를 확인해 주세요.

#!/bin/bash
# review length의 .max(N) 선언들을 탐색
rg -nP -C2 --type=ts '(review).*(max\(\s*\d+\s*\))' src
# usePinAddValidation 내 상수/스키마도 확인
fd -t f usePinAddValidation.ts src | xargs -I{} rg -n --pretty '.max\(\d+\)|MAX_?REVIEW' {}
🧹 Nitpick comments (4)
src/pages/pinAdd/component/pinReviewInput/PinReviewInput.tsx (4)

24-28: 에러 메시지와 입력 요소 연결(접근성 개선)

에러 영역에 id/role을 부여해 스크린리더 공지 및 aria-describedby 연결을 완성하세요.

-      {errors.review && (
-        <Text tag='body_10' color='red1' className={styles.errorMessage}>
+      {errors.review && (
+        <Text tag='body_10' color='red1' className={styles.errorMessage} id='pin-review-error' role='alert' aria-live='assertive'>
           {errors.review.message}
         </Text>
       )}

29-31: 글자 수 카운터 접근성/시각적 충돌 완화

  • 카운터에 보조설명 id/라이브 영역을 부여합니다.
  • 겹침 이슈는 CSS에서 characterCount를 절대배치하는 경우 textarea에 적절한 padding-right/bottom을 주어 회피하세요.
-      <div className={styles.characterCount}>
-        {reviewValue?.length || 0} / {maxLength}
+      <div className={styles.characterCount} id='pin-review-help' aria-live='polite'>
+        {Math.min(reviewValue?.length || 0, maxLength)} / {maxLength}
       </div>

CSS 제안(참고):

  • .reviewTextarea에 padding-right를 카운터 너비만큼 추가
  • .characterCount { position: absolute; right: 12px; bottom: 8px; pointer-events: none; }

22-22: 불필요 속성 제거 제안

wrap='soft'는 textarea 기본값입니다. 유지 목적이 아니라면 제거해도 됩니다.

-        wrap='soft'

13-15: 하드코딩된 150 상수는 공용 상수로 분리 권장

UI/스키마 동기화를 위해 상수화를 추천합니다.

아래처럼 공용 상수를 도입해 양쪽에서 참조해 주세요.

// 예: src/shared/constants/pin.ts
export const MAX_REVIEW_LENGTH = 150;
-  const maxLength = 150;
+  const maxLength = MAX_REVIEW_LENGTH;
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f3eb63 and 1f34fba.

📒 Files selected for processing (4)
  • src/pages/pinAdd/PinAdd.tsx (1 hunks)
  • src/pages/pinAdd/component/pinReviewInput/PinReviewInput.tsx (1 hunks)
  • src/pages/pinAdd/hook/usePinAddValidation.ts (1 hunks)
  • src/pages/placePins/PlacePins.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/pages/pinAdd/PinAdd.tsx
  • src/pages/pinAdd/hook/usePinAddValidation.ts
  • src/pages/placePins/PlacePins.tsx
🔇 Additional comments (1)
src/pages/pinAdd/component/pinReviewInput/PinReviewInput.tsx (1)

1-11: 타입/프로퍼티 구성은 적절합니다

react-hook-form 타입 지정과 props 분리 모두 명확합니다.

@KUIT-BARO KUIT-BARO deleted a comment from coderabbitai bot Sep 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 탐색 페이지 구현

4 participants