Skip to content

[FEAT] 알바찾기 페이지 UI & API 구현#33

Open
kim3360 wants to merge 12 commits into
devfrom
feat/ALT-191
Open

[FEAT] 알바찾기 페이지 UI & API 구현#33
kim3360 wants to merge 12 commits into
devfrom
feat/ALT-191

Conversation

@kim3360
Copy link
Copy Markdown
Contributor

@kim3360 kim3360 commented May 16, 2026

ID

  • ALT-191

변경 내용

  • 알바 찾기 화면을 네이버 지도 + 하단 바텀 시트 UI로 구현
  • 공고 목록·상세·지원 API를 연동하고, React Query 훅으로 추가
  • 지도 상단 검색바, 우측 플로팅 버튼(목록 펼치기 / 내 위치)을 추가
  • 라우트를 postingId 경로 파라미터 방식으로 정리 (/user/job-lookup-map-detail/:postingId, /user/job-lookup-map-apply/:postingId)
  • 기존 소셜 드로어 기반 알바 찾기 UI는 제거하고, features/job-lookup-map 도메인으로 구조를 통합

구현 사항

  • index.html에 네이버 지도 SDK 스크립트를 추가하고, GPS 기반 지도 중심 이동 적용
  • Framer Motion drag로 하단 시트(목록 영역)를 드래그해 펼치기/접기 기능 추가
  • AlbaFindCategoryBar: 주변에서 찾기 / 지역에서 찾기 탭 전환, 지역 모드 시 필터 라벨(서울·전체) 및 chevron 표시 분기
  • Albabox, AlbaFindList: API 공고 데이터를 카드 목록으로 표시
  • SearchBar (shared/ui/common): 지도 상단 검색 입력 UI

구현 시연 (필요 시)

참고 사항 (필요 시)

  • 추가적으로 검토가 필요한 사항이나 관련 문서, 참조 문서 등을 첨부
    (예: 확인 사항, 참조 링크 등)

Summary by CodeRabbit

릴리스 노트

  • 신규 기능
    • 지도 기반 채용공고 검색 기능 추가
    • 공고 상세 조회 및 일정 선택을 통한 지원 신청 기능 추가
    • 상단 검색창 및 카테고리 필터 기능 제공
    • 드래그 가능한 하단 시트로 개선된 모바일 경험 제공
    • 지도 내 부유 액션 버튼으로 목록 보기 및 위치 기능 지원

Review Change Stack

@kim3360 kim3360 requested review from dohy-eon and limtjdghks May 16, 2026 12:37
@vercel
Copy link
Copy Markdown

vercel Bot commented May 16, 2026

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

Project Deployment Actions Updated (UTC)
alter-client Ready Ready Preview, Comment May 16, 2026 0:37am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

📝 Walkthrough

Walkthrough

Naver Maps 기반 채용 공고 지도 기능이 신규 구현되며, 공고 조회/상세/지원 페이지와 React Query 무한 스크롤, framer-motion 드래그 시트가 포함됨. 동시에 Kakao OAuth가 redirectUri를 함께 반환하도록 확장되고, alba-find UI 컴포넌트가 shared에서 features로 이전됨.

Changes

Kakao OAuth 다중 반환값 통합

Layer / File(s) Summary
OAuth 인가 코드 결과 타입 및 API 확장
src/shared/lib/socialLogin.ts
KakaoAuthorizationCodeResult 타입이 도입되고, requestFreshKakaoAuthorizationCoderedirectUriOverride 파라미터를 받아 { authorizationCode, redirectUri } 객체를 반환. 팝업 콜백에서 redirectUri 파싱 및 HTTPS 보정 로직 포함.
로그인/가입 호출부 통합
src/features/auth/ui/KakaoLoginButton.tsx, src/pages/signup/hooks/useSignupForm.ts
새로운 반환값을 구조분해하여 authorizationCoderedirectUri를 동시에 얻으며, 가입 플로우에서 kakaoWebRedirectUri 변수로 redirectUri 우선 적용.

채용 공고 기능 전체 구현

Layer / File(s) Summary
공고 데이터 타입 및 API 계약
src/features/job-lookup-map/types/posting.ts, src/features/job-lookup-map/api/posting.ts
PostingListResponse, PostingDetailResponse, Posting, Schedule 등 공고 도메인 타입 정의. fetchPostings(커서 기반 목록), fetchPostingDetail(상세/래퍼 처리), applyPosting(지원) API 함수 구현.
공고 데이터 페칭 React Query 훅
src/features/job-lookup-map/hooks/usePosting.ts, usePostingDetail.ts, useApplyPosting.ts
usePostings(무한 스크롤, useInfiniteQuery), usePostingDetail(조건부 조회), useApplyPosting(mutation, 성공 시 쿼리 무효화).
공고 표시 UI 컴포넌트
src/features/job-lookup-map/common/AlbaFindCategoryBar.tsx, Albabox.tsx, MapFloatingActions.tsx
카테고리/필터 바(모드/필터 상태 토글), 공고 카드(북마크/클릭 핸들러), 지도 플로팅 액션(목록/내 위치 버튼).
공고 데이터 표시 형식 변환
src/features/job-lookup-map/lib/postingToAlbaboxProps.ts
PostingAlbabox props 변환, formatPostedAgo(상대 시간), formatWorkDaysForDisplay(요일 정규화/정렬).
공고 상세 페이지
src/pages/user/job-lookup-map-detail/index.tsx
postingId 파라미터 해석, usePostingDetail 조회, 시간 범위/요일/인원 표시, "지원하기" 버튼으로 지원 페이지 이동.
공고 지원 페이지
src/pages/user/job-lookup-map-apply/index.tsx
공고 상세 재표시, 근무 스케줄 선택(ShiftCard), 자기소개 입력, useApplyPosting mutation으로 제출. Enter/Space 키 접근성 포함.
공고 지도 메인 페이지
src/pages/user/job-lookup-map/index.tsx
Naver Maps 인스턴스 생성/파기, geolocation watchPosition, ResizeObserver 기반 시트 높이 동기화, framer-motion 드래그 시트(snapTo spring 애니메이션), 무한 스크롤 trigger(Intersection Observer), 북마크 토글, 상세 페이지 네비게이션.
라우팅 설정
src/app/App.tsx, src/shared/constants/routes.ts
ROUTES.USER.JOB_LOOKUP_MAP_DETAIL, ROUTES.USER.JOB_LOOKUP_MAP_APPLY 라우트 추가 및 페이지 컴포넌트 연결.

의존성 추가 및 컴포넌트 정리

Layer / File(s) Summary
새 외부 의존성
index.html, package.json
Naver Maps API v3 script 로딩(VITE_NAVER_MAP_CLIENT_ID 환경변수 사용), framer-motion(^12.38.0) 패키지 추가.
alba-find 컴포넌트 이전 및 정리
src/features/job-lookup-map/common/*, src/shared/ui/manager/alba-find/*, src/features/social/index.ts
shared/manager의 AlbaFindCategoryBar/Albabox가 삭제되고 features/job-lookup-map/common으로 재구현. social 기능에서 drawer 관련 export 제거(AlbaFindDrawer, DrawerPeekStrip, drawer.tsx 모두 삭제).
공통 UI 컴포넌트 추가 및 리소스 업데이트
src/shared/ui/common/SearchBar.tsx, src/shared/ui/manager/OngoingPostingCard.tsx
SearchBar 검색 입력 컴포넌트 신규 추가. Clock/Calendar 아이콘 경로 업데이트(alba → job-lookup-map).

🎯 추정 검토 난이도

🎯 4 (Complex) | ⏱️ ~50분

이유: 다중 페이지 지형/지도 통합, 복잡한 상태 관리(위치 추적·드래그·무한 스크롤), OAuth 반환값 확장의 이중 호출부 업데이트, 컴포넌트 재배치로 인한 이주 검증 필요. 단, 로직 밀도는 중간 수준이고 대부분 신규 파일이라 회귀 범위는 제한적.

🔍 시니어 리뷰 - 핵심 이슈

⚠️ 1. OAuth 반환값 확장의 일관성 문제

  • 발견: requestFreshKakaoAuthorizationCoderedirectUriOverride 파라미터를 받지만, 기존 가입 플로우(useSignupForm.ts)에서만 활용되고 로그인(KakaoLoginButton.tsx)에서는 무시됨.
    // KakaoLoginButton.tsx (불완전)
    const { authorizationCode, redirectUri } = await requestFreshKakaoAuthorizationCode(
      getKakaoOAuthRedirectUri()  // ← 인자로 전달되지만
    );
    // 반환된 redirectUri가 loginSocial에 사용됨
  • 위험: 로그인과 가입 간 redirectUri 소스 불일치 가능. 콜백 URL이 환경에 따라 달라질 경우 CSRF/리다이렉트 공격 벡터.
  • 수정안: 두 호출부 모두 동일한 redirectUri 우선 적용 로직 필요. 또는 환경 설정에서 일관된 callback URL 관리.

⚠️ 2. Map 인스턴스 라이프사이클 리크 위험

  • 발견: JobLookupMapPage에서 Naver Map 인스턴스가 useRef에 저장되지만, 언마운트 시 명시적 파기(map.destroy() 또는 유사)가 없음.
    // range_6933bb9efda8: useEffect cleanup에서
    // geolocation 정리만 있고 map 인스턴스 정리 누락
  • 위험: 메모리 누수, 특히 페이지 네비게이션 반복 시 맵 인스턴스 누적.
  • 수정안: useEffect cleanup에 map?.destroy?.() 또는 Naver Maps 문서의 권장 파기 로직 추가.

⚠️ 3. 무한 스크롤 Sentinel 요소 접근성 문제

  • 발견: usePostings에서 hasNextPage일 때 sentinel div를 렌더링하지만, ref 바인딩이 명시되지 않음. IntersectionObserver 트리거 메커니즘이 보이지 않음.
    // range_7315857f28cd: 
    // {hasNextPage && <div>로딩 중...</div>}
    // IntersectionObserver 호출이 어디인지 불명확
  • 위험: sentinel이 뷰포트 진입했을 때 fetchNextPage 호출 시점이 불명확. 무한 중복 요청 또는 스크롤 멈춤 가능.
  • 수정안: sentinel 요소에 useEffect + useRefIntersectionObserver 명시적 바인딩. 또는 기존 구현이 있다면 코드 주석 추가.

⚠️ 4. postingDetail 응답 타입 검사 로직의 불확실성

  • 발견: fetchPostingDetail에서 응답이 CommonApiResponse 래퍼인지 판단:
    // range_2c03035c87f0
    if (
      typeof d === 'object' &&
      d !== null &&
      'timestamp' in d &&
      'data' in d &&
      d.data !== null
    ) {
      return d.data;
    }
    return d as PostingDetailResponse;
  • 위험: CommonApiResponse<PostingDetailResponse>PostingDetailResponse의 스키마가 겹칠 경우, 잘못된 분기. API 버전 변경 시 취약.
  • 수정안: API 응답 스키마를 명확히 분리하거나, 항상 래퍼 형식으로 통일. 또는 d.data 필드명이 응답의 최상위에서만 나타나도록 보장.

⚠️ 5. framer-motion 드래그 시트의 경계 조건

  • 발견: maxTranslateY 계산과 snapTo 스프링 애니메이션의 경계 처리:
    // range_88c4965e2b87, range_8f66d87850ba
    // maxTranslateY가 음수 또는 0일 경우 드래그 동작 예상 불가
    // snapTo에서 경계값(0, maxTranslateY) 외 클릭 감지 가능성
  • 위험: 작은 화면/특수 창 크기에서 시트가 제대로 스냅되지 않거나, 과도하게 드래그될 수 있음.
  • 수정안: maxTranslateY = Math.max(0, calculatedHeight) 명시적 하한선 설정. snapTo에서 Math.clamp(position, 0, maxTranslateY).

⚠️ 6. 북마크 상태 동기화 누락

  • 발견: JobLookupMapPage에서 북마크 토글 후 postingToAlbaboxProps로 변환된 UI가 즉시 갱신되지 않을 가능성:
    // range_7315857f28cd
    // bookmarkById[posting.id] 상태가 업데이트되었더라도
    // postingToAlbaboxProps는 원본 posting 객체를 기반으로 하므로
    // saved: posting.xxx (아니라 bookmarkById[posting.id] 사용)
  • 위험: UI에서 북마크 버튼이 클릭되어도 시각적 피드백 지연 또는 불일치.
  • 수정안: Albabox props 구성 시 postingToAlbaboxProps(posting) 결과를 받은 후 saved: bookmarkById[posting.id] ?? /* 기본값 */ 로 덮어쓰기.

양호한 패턴

  • React Query 무효화 전략 명확 (useApplyPosting에서 postingDetail + jobLookupMap.postings 동시 무효화).
  • Kakao OAuth 반환값 통합 설계 자체는 좋음 (단, 일관성 확보 필요).
  • SearchBar, 데이터 포맷팅 함수 등 공통 로직 추상화 양호.

Possibly related PRs

  • alter-app/alter-client#15: Kakao OAuth 로그인/가입 흐름 통합 — redirectUri 다중 반환값 확장 호환.
  • alter-app/alter-client#11: 기존 alba-find AlbaFindCategoryBar/Albabox 구현 — 본 PR에서 features 폴더로 이전된 컴포넌트와 직접 중복.
  • alter-app/alter-client#14: 채팅방 기능 구현 — social 피처 내보내기 정리와 연동 (drawer UI 제거 반영).

Suggested reviewers

  • dohy-eon
  • limtjdghks
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경 사항인 알바찾기 페이지 UI & API 구현을 명확하게 반영하고 있습니다.
Description check ✅ Passed PR 설명이 템플릿의 필수 섹션(ID, 변경 내용, 구현 사항)을 모두 포함하고 있으며 구체적인 변경 내용을 제시하고 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ALT-191

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@kim3360 kim3360 changed the title [FEAT] : 알바찾기 페이지 UI & API 구현 [FEAT] 알바찾기 페이지 UI & API 구현 May 16, 2026
@kim3360 kim3360 self-assigned this May 16, 2026
Copy link
Copy Markdown

@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

🧹 Nitpick comments (4)
src/pages/user/job-lookup-map/index.tsx (1)

62-169: 🏗️ Heavy lift

페이지 레이어 책임이 커져서 유지보수 리스크가 높습니다.

지도 생성/위치추적/시트 물리/페이징 트리거 로직을 feature 훅(예: useJobLookupMapController)으로 분리하면 페이지는 조합만 담당하고 회귀 범위를 크게 줄일 수 있습니다.

As per coding guidelines src/pages/**: "페이지 컴포넌트가 비즈니스 로직 없이 조합(Composition)만 하는지".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/user/job-lookup-map/index.tsx` around lines 62 - 169, The page
component contains map creation, geolocation watch, sheet physics, and
infinite-scroll observer logic that should be extracted into a feature hook;
implement a new hook named useJobLookupMapController that encapsulates the logic
currently inside the useEffect blocks, useLayoutEffect, the IntersectionObserver
setup, lastPositionRef/mapInstanceRef/sheetRef/loadMoreRef state, and the snapTo
animation so the page becomes purely composition. Move responsibilities:
initialize Naver map and FALLBACK_LAT/LNG center, start/clear
navigator.geolocation.watchPosition, manage map.setCenter calls (used by
handleMyLocationClick), create and cleanup ResizeObserver for sheet resizing
(updateBounds and setMaxTranslateY/y updates), setup IntersectionObserver to
drive fetchNextPage/isFetchingNextPage/hasNextPage, and return refs
(mapContainerRef, sheetRef, loadMoreRef), state (maxTranslateY), and handlers
(snapTo, handleMyLocationClick, handleListClick) so the page component imports
useJobLookupMapController and only wires props and UI.
src/pages/user/job-lookup-map-detail/index.tsx (1)

13-59: ⚡ Quick win

페이지 레이어에 비즈니스 포맷팅 로직이 과도하게 들어와 있습니다.

Line 13-59의 요일 파싱/근무시간 계산 로직은 features 또는 전용 selector/helper로 분리하고 페이지는 조합에 집중하는 편이 유지보수와 재사용에 안전합니다.

As per coding guidelines src/pages/**: "페이지 컴포넌트가 비즈니스 로직 없이 조합(Composition)만 하는지".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/user/job-lookup-map-detail/index.tsx` around lines 13 - 59, The
page component JobLookupMapDetailPage currently contains business logic
functions parseWorkDayLabels and formatDurationHint and computes
schedule-derived values (workDaysLine, selectedDays, timeRange, durationHint);
extract these into a feature/helper (e.g., a new module with exported helpers
like parseWorkDayLabels, formatDurationHint, and a selector function
formatScheduleForDisplay that uses formatWorkDaysForDisplay) and import them
into the page so JobLookupMapDetailPage only calls the helpers to get
selectedDays, timeRange and durationHint; update usages of
schedule.startTime/schedule.endTime to route through the new helper API and
remove the inline implementations from the page.
src/shared/constants/routes.ts (1)

16-17: 💤 Low value

동적 라우트 네이밍 일관성 검토를 제안합니다.

기존 동적 세그먼트를 포함한 라우트 상수들(WORKSPACE_MEMBERS_PATTERN, WORKSPACE_DETAIL_PATTERN)은 _PATTERN 접미사를 사용하는데, 신규 추가된 라우트는 이를 따르지 않습니다. 일관된 네이밍 컨벤션을 유지하면 코드베이스 탐색과 유지보수가 더 용이합니다.

📝 일관성을 위한 네이밍 제안
-JOB_LOOKUP_MAP_DETAIL: '/user/job-lookup-map-detail/:postingId',
-JOB_LOOKUP_MAP_APPLY: '/user/job-lookup-map-apply/:postingId',
+JOB_LOOKUP_MAP_DETAIL_PATTERN: '/user/job-lookup-map-detail/:postingId',
+JOB_LOOKUP_MAP_APPLY_PATTERN: '/user/job-lookup-map-apply/:postingId',

App.tsx에서도 해당 상수명 업데이트 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/constants/routes.ts` around lines 16 - 17, Rename the new dynamic
route constants to follow the existing `_PATTERN` naming convention so they
match WORKSPACE_MEMBERS_PATTERN and WORKSPACE_DETAIL_PATTERN; specifically
update JOB_LOOKUP_MAP_DETAIL and JOB_LOOKUP_MAP_APPLY to use the `_PATTERN`
suffix (and update any usages), then adjust references in App.tsx to the new
constant names to avoid breaking imports/usages and keep naming consistent
across the codebase.
src/app/App.tsx (1)

16-17: ⚡ Quick win

신규 페이지에 lazy loading 적용을 권장합니다.

SignupPage는 lazy import를 사용하지만, 새로 추가된 JobLookupMapApplyPageJobLookupMapDetailPage는 eager import되어 초기 번들 크기가 증가합니다. 사용자 흐름상 이 페이지들은 지도 페이지 이후에 접근하므로 code splitting이 적합합니다.

⚡ lazy import로 변경하는 제안
-import { JobLookupMapApplyPage } from '`@/pages/user/job-lookup-map-apply`'
-import { JobLookupMapDetailPage } from '`@/pages/user/job-lookup-map-detail`'
+
+const JobLookupMapApplyPage = lazy(async () => {
+  const m = await import('`@/pages/user/job-lookup-map-apply`')
+  return { default: m.JobLookupMapApplyPage }
+})
+
+const JobLookupMapDetailPage = lazy(async () => {
+  const m = await import('`@/pages/user/job-lookup-map-detail`')
+  return { default: m.JobLookupMapDetailPage }
+})

Route 정의에 Suspense 추가:

 <Route
   path={ROUTES.USER.JOB_LOOKUP_MAP_DETAIL}
-  element={<JobLookupMapDetailPage />}
+  element={
+    <Suspense fallback={null}>
+      <JobLookupMapDetailPage />
+    </Suspense>
+  }
 />
 <Route
   path={ROUTES.USER.JOB_LOOKUP_MAP_APPLY}
-  element={<JobLookupMapApplyPage />}
+  element={
+    <Suspense fallback={null}>
+      <JobLookupMapApplyPage />
+    </Suspense>
+  }
 />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/App.tsx` around lines 16 - 17, 현재 eager로 가져오는 JobLookupMapApplyPage와
JobLookupMapDetailPage는 초기 번들에 포함되므로 React.lazy로 변경해 코드 스플리팅을 적용하세요: replace the
direct imports of JobLookupMapApplyPage and JobLookupMapDetailPage with lazy
imports using React.lazy(() => import('...')) and ensure the routes that render
these components are wrapped in a React.Suspense with an appropriate fallback
(similar to how SignupPage is already lazy-loaded); confirm the target modules
export default components so lazy import works and add React/Suspense imports
where needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/job-lookup-map/api/posting.ts`:
- Around line 37-48: The function currently attempts to unwrap a
CommonApiResponse<PostingDetailResponse> by checking 'timestamp' and 'data' but
then force-casts to PostingDetailResponse even when body.data is empty/invalid,
causing unsafe casting; modify the unwrap logic that returns (body as
CommonApiResponse<PostingDetailResponse>).data so it first validates that
body.data contains the expected PostingDetailResponse shape (or required fields)
and, if not valid, either return null/undefined or throw a descriptive Error
instead of casting the wrapper to PostingDetailResponse; update the conditional
around body/data checks and the code paths that return the unwrapped value
(references: the body variable, CommonApiResponse<PostingDetailResponse>, and
PostingDetailResponse) so callers never receive an incorrectly cast object.

In `@src/features/job-lookup-map/common/Albabox.tsx`:
- Around line 34-37: The article element in the Albabox component uses only
onClick (onClick) so keyboard users cannot activate the card; replace the
clickable article with a semantic interactive element (e.g., button or Link) or
at minimum make the article focusable and keyboard-activatable by adding
tabIndex={0}, role="button", and a keyDown handler that calls the same handler
used by onClick when Enter or Space is pressed; update the component where
onClick is declared/used (Albabox) to reuse the existing click handler for
keyboard events to ensure identical behavior and accessibility.

In `@src/pages/user/job-lookup-map-apply/index.tsx`:
- Around line 273-279: The submit button currently silently returns when no
schedule exists (selectedScheduleId ?? data.schedules[0]?.id) so users see an
unresponsive button; update the button logic in the component that uses
isSubmitting, selectedScheduleId, data.schedules and submitApply to instead
disable the button when data.schedules.length === 0 (add that condition to the
disabled prop) and render a visible helper/alert text near the button explaining
"No schedules available" (or similar) so users understand why submission is
disabled; ensure the onClick no longer needs to guard for missing schedule since
the button will be disabled in that case.

In `@src/pages/user/job-lookup-map-detail/index.tsx`:
- Around line 178-183: The "더보기" button rendered in the JSX (the <button> with
text "더보기" and className "mt-3 h-12 w-full rounded-2xl border border-line-2
typography-body02-semibold text-text-70") currently has no behavior; either wire
it to a real handler (e.g., add an onClick prop like onClick={handleMoreClick}
and implement/export handleMoreClick in this component to perform the intended
action) or make it non-interactive to avoid appearing broken (add disabled and
aria-disabled attributes and update styling, or conditionally hide it via a
boolean flag and render null when not ready). Ensure the chosen approach touches
this exact button element so the UI no longer shows an inert clickable control.

In `@src/pages/user/job-lookup-map/index.tsx`:
- Around line 144-148: The updateBounds function currently forces the sheet
closed on every resize by unconditionally calling y.set(nextMax); change
updateBounds to only adjust maxTranslateY (call setMaxTranslateY(nextMax)) and
then clamp the current y value to the new allowed range instead of overwriting
it — use the existing y value and only set y when it exceeds the new bounds
(e.g., clamp y to [0, nextMax] or set to nextMax only if y.get() > nextMax) so
the sheet stays in its current open position unless out-of-range; reference
updateBounds, nextMax, setMaxTranslateY, y and SHEET_PEEK_HEIGHT when making the
change.

In `@src/shared/ui/common/SearchBar.tsx`:
- Around line 12-33: The SearchBar component can render an unnamed <input> if
callers don't pass aria-label/aria-labelledby; update SearchBar to ensure an
accessible name by detecting if props includes aria-label or aria-labelledby
and, if absent, provide a sensible fallback (e.g., use the placeholder or a
default string like "Search") when rendering the input; refer to the SearchBar
function, the input element that spreads {...props}, and the placeholder prop to
implement this fallback so screen readers always receive an accessible name.

---

Nitpick comments:
In `@src/app/App.tsx`:
- Around line 16-17: 현재 eager로 가져오는 JobLookupMapApplyPage와
JobLookupMapDetailPage는 초기 번들에 포함되므로 React.lazy로 변경해 코드 스플리팅을 적용하세요: replace the
direct imports of JobLookupMapApplyPage and JobLookupMapDetailPage with lazy
imports using React.lazy(() => import('...')) and ensure the routes that render
these components are wrapped in a React.Suspense with an appropriate fallback
(similar to how SignupPage is already lazy-loaded); confirm the target modules
export default components so lazy import works and add React/Suspense imports
where needed.

In `@src/pages/user/job-lookup-map-detail/index.tsx`:
- Around line 13-59: The page component JobLookupMapDetailPage currently
contains business logic functions parseWorkDayLabels and formatDurationHint and
computes schedule-derived values (workDaysLine, selectedDays, timeRange,
durationHint); extract these into a feature/helper (e.g., a new module with
exported helpers like parseWorkDayLabels, formatDurationHint, and a selector
function formatScheduleForDisplay that uses formatWorkDaysForDisplay) and import
them into the page so JobLookupMapDetailPage only calls the helpers to get
selectedDays, timeRange and durationHint; update usages of
schedule.startTime/schedule.endTime to route through the new helper API and
remove the inline implementations from the page.

In `@src/pages/user/job-lookup-map/index.tsx`:
- Around line 62-169: The page component contains map creation, geolocation
watch, sheet physics, and infinite-scroll observer logic that should be
extracted into a feature hook; implement a new hook named
useJobLookupMapController that encapsulates the logic currently inside the
useEffect blocks, useLayoutEffect, the IntersectionObserver setup,
lastPositionRef/mapInstanceRef/sheetRef/loadMoreRef state, and the snapTo
animation so the page becomes purely composition. Move responsibilities:
initialize Naver map and FALLBACK_LAT/LNG center, start/clear
navigator.geolocation.watchPosition, manage map.setCenter calls (used by
handleMyLocationClick), create and cleanup ResizeObserver for sheet resizing
(updateBounds and setMaxTranslateY/y updates), setup IntersectionObserver to
drive fetchNextPage/isFetchingNextPage/hasNextPage, and return refs
(mapContainerRef, sheetRef, loadMoreRef), state (maxTranslateY), and handlers
(snapTo, handleMyLocationClick, handleListClick) so the page component imports
useJobLookupMapController and only wires props and UI.

In `@src/shared/constants/routes.ts`:
- Around line 16-17: Rename the new dynamic route constants to follow the
existing `_PATTERN` naming convention so they match WORKSPACE_MEMBERS_PATTERN
and WORKSPACE_DETAIL_PATTERN; specifically update JOB_LOOKUP_MAP_DETAIL and
JOB_LOOKUP_MAP_APPLY to use the `_PATTERN` suffix (and update any usages), then
adjust references in App.tsx to the new constant names to avoid breaking
imports/usages and keep naming consistent across the codebase.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c53ac4df-ef24-468e-b767-8ac9cba1e1b0

📥 Commits

Reviewing files that changed from the base of the PR and between 4f7367e and 5a111bb.

⛔ Files ignored due to path filters (8)
  • package-lock.json is excluded by !**/package-lock.json
  • src/assets/icons/job-lookup-map/Bookmark.svg is excluded by !**/*.svg
  • src/assets/icons/job-lookup-map/Calendar.svg is excluded by !**/*.svg
  • src/assets/icons/job-lookup-map/Chevrondown.svg is excluded by !**/*.svg
  • src/assets/icons/job-lookup-map/Clock.svg is excluded by !**/*.svg
  • src/assets/icons/job-lookup-map/List.svg is excluded by !**/*.svg
  • src/assets/icons/job-lookup-map/Mappin.svg is excluded by !**/*.svg
  • src/assets/icons/job-lookup-map/Thumbsup.svg is excluded by !**/*.svg
📒 Files selected for processing (29)
  • index.html
  • package.json
  • src/app/App.tsx
  • src/features/auth/ui/KakaoLoginButton.tsx
  • src/features/job-lookup-map/api/posting.ts
  • src/features/job-lookup-map/common/AlbaFindCategoryBar.tsx
  • src/features/job-lookup-map/common/AlbaFindList.tsx
  • src/features/job-lookup-map/common/Albabox.tsx
  • src/features/job-lookup-map/common/MapFloatingActions.tsx
  • src/features/job-lookup-map/hooks/useApplyPosting.ts
  • src/features/job-lookup-map/hooks/usePosting.ts
  • src/features/job-lookup-map/hooks/usePostingDetail.ts
  • src/features/job-lookup-map/lib/postingToAlbaboxProps.ts
  • src/features/job-lookup-map/types/posting.ts
  • src/features/social/index.ts
  • src/features/social/ui/AlbaFindDrawer.tsx
  • src/features/social/ui/DrawerHandleBar.tsx
  • src/features/social/ui/DrawerPeekStrip.tsx
  • src/features/social/ui/drawer.tsx
  • src/pages/signup/hooks/useSignupForm.ts
  • src/pages/user/job-lookup-map-apply/index.tsx
  • src/pages/user/job-lookup-map-detail/index.tsx
  • src/pages/user/job-lookup-map/index.tsx
  • src/shared/constants/routes.ts
  • src/shared/lib/socialLogin.ts
  • src/shared/ui/common/SearchBar.tsx
  • src/shared/ui/manager/OngoingPostingCard.tsx
  • src/shared/ui/manager/alba-find/AlbaFindCategoryBar.tsx
  • src/shared/ui/manager/alba-find/Albabox.tsx
💤 Files with no reviewable changes (7)
  • src/features/social/ui/DrawerHandleBar.tsx
  • src/features/social/ui/AlbaFindDrawer.tsx
  • src/shared/ui/manager/alba-find/AlbaFindCategoryBar.tsx
  • src/features/social/ui/drawer.tsx
  • src/features/social/index.ts
  • src/features/social/ui/DrawerPeekStrip.tsx
  • src/shared/ui/manager/alba-find/Albabox.tsx

Comment on lines +37 to +48
if (
body &&
typeof body === 'object' &&
'timestamp' in body &&
'data' in body &&
body.data != null &&
typeof body.data === 'object'
) {
return (body as CommonApiResponse<PostingDetailResponse>).data
}
return body as PostingDetailResponse
}
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 | ⚡ Quick win

상세 응답 언래핑 실패 시 잘못된 캐스팅이 발생합니다.

CommonApiResponse 형태인데 data가 비어있거나 비정상이면 현재 로직은 wrapper 전체를 PostingDetailResponse로 캐스팅해 반환합니다(Line 47). 이 경우 호출부에서 필드 접근 시 런타임 오류로 이어질 수 있습니다.

수정 예시
   const body = response.data
-  if (
-    body &&
-    typeof body === 'object' &&
-    'timestamp' in body &&
-    'data' in body &&
-    body.data != null &&
-    typeof body.data === 'object'
-  ) {
-    return (body as CommonApiResponse<PostingDetailResponse>).data
-  }
-  return body as PostingDetailResponse
+  if (body && typeof body === 'object' && 'timestamp' in body && 'data' in body) {
+    const wrapped = body as CommonApiResponse<PostingDetailResponse | null>
+    if (!wrapped.data) {
+      throw new Error('Invalid posting detail response: data is null')
+    }
+    return wrapped.data
+  }
+  return body as PostingDetailResponse
📝 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
if (
body &&
typeof body === 'object' &&
'timestamp' in body &&
'data' in body &&
body.data != null &&
typeof body.data === 'object'
) {
return (body as CommonApiResponse<PostingDetailResponse>).data
}
return body as PostingDetailResponse
}
const body = response.data
if (body && typeof body === 'object' && 'timestamp' in body && 'data' in body) {
const wrapped = body as CommonApiResponse<PostingDetailResponse | null>
if (!wrapped.data) {
throw new Error('Invalid posting detail response: data is null')
}
return wrapped.data
}
return body as PostingDetailResponse
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/job-lookup-map/api/posting.ts` around lines 37 - 48, The
function currently attempts to unwrap a CommonApiResponse<PostingDetailResponse>
by checking 'timestamp' and 'data' but then force-casts to PostingDetailResponse
even when body.data is empty/invalid, causing unsafe casting; modify the unwrap
logic that returns (body as CommonApiResponse<PostingDetailResponse>).data so it
first validates that body.data contains the expected PostingDetailResponse shape
(or required fields) and, if not valid, either return null/undefined or throw a
descriptive Error instead of casting the wrapper to PostingDetailResponse;
update the conditional around body/data checks and the code paths that return
the unwrapped value (references: the body variable,
CommonApiResponse<PostingDetailResponse>, and PostingDetailResponse) so callers
never receive an incorrectly cast object.

Comment on lines +34 to +37
<article
className="border-b border-line-1 py-5 last:border-b-0 cursor-pointer"
onClick={onClick}
>
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 | ⚡ Quick win

카드 클릭 진입이 키보드로 불가능합니다.

Line 34-37에서 상세 진입이 onClick만으로 연결되어 있어 키보드 사용자(탭/엔터)로는 공고 상세 이동을 수행할 수 없습니다. 카드 진입은 button/Link 기반으로 바꾸거나, 최소한 포커스/키보드 활성화(Enter/Space)를 보장해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/job-lookup-map/common/Albabox.tsx` around lines 34 - 37, The
article element in the Albabox component uses only onClick (onClick) so keyboard
users cannot activate the card; replace the clickable article with a semantic
interactive element (e.g., button or Link) or at minimum make the article
focusable and keyboard-activatable by adding tabIndex={0}, role="button", and a
keyDown handler that calls the same handler used by onClick when Enter or Space
is pressed; update the component where onClick is declared/used (Albabox) to
reuse the existing click handler for keyboard events to ensure identical
behavior and accessibility.

Comment on lines +273 to +279
<button
type="button"
disabled={isSubmitting}
onClick={() => {
const scheduleId = selectedScheduleId ?? data.schedules[0]?.id
if (!scheduleId) return
submitApply({
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 | 🟡 Minor | ⚡ Quick win

스케줄이 없을 때 제출 버튼이 무반응입니다.

Line 277-279에서 스케줄이 없으면 조용히 return해서, 사용자는 버튼이 먹통처럼 보입니다. data.schedules.length === 0일 때 제출 버튼을 비활성화하고 안내 문구를 같이 노출해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/user/job-lookup-map-apply/index.tsx` around lines 273 - 279, The
submit button currently silently returns when no schedule exists
(selectedScheduleId ?? data.schedules[0]?.id) so users see an unresponsive
button; update the button logic in the component that uses isSubmitting,
selectedScheduleId, data.schedules and submitApply to instead disable the button
when data.schedules.length === 0 (add that condition to the disabled prop) and
render a visible helper/alert text near the button explaining "No schedules
available" (or similar) so users understand why submission is disabled; ensure
the onClick no longer needs to guard for missing schedule since the button will
be disabled in that case.

Comment on lines +178 to +183
<button
type="button"
className="mt-3 h-12 w-full rounded-2xl border border-line-2 typography-body02-semibold text-text-70"
>
더보기
</button>
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 | ⚡ Quick win

동작 없는 버튼이 노출되어 있습니다.

Line 178-183의 더보기 버튼은 클릭 시 아무 동작이 없어 사용자 입장에서 고장난 UI로 보입니다. 실제 동작을 연결하거나, 준비 중이라면 비활성/숨김 처리로 오동작 인상을 막아주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/user/job-lookup-map-detail/index.tsx` around lines 178 - 183, The
"더보기" button rendered in the JSX (the <button> with text "더보기" and className
"mt-3 h-12 w-full rounded-2xl border border-line-2 typography-body02-semibold
text-text-70") currently has no behavior; either wire it to a real handler
(e.g., add an onClick prop like onClick={handleMoreClick} and implement/export
handleMoreClick in this component to perform the intended action) or make it
non-interactive to avoid appearing broken (add disabled and aria-disabled
attributes and update styling, or conditionally hide it via a boolean flag and
render null when not ready). Ensure the chosen approach touches this exact
button element so the UI no longer shows an inert clickable control.

Comment on lines +144 to +148
const updateBounds = () => {
const nextMax = Math.max(0, sheet.offsetHeight - SHEET_PEEK_HEIGHT)
setMaxTranslateY(nextMax)
y.set(nextMax)
}
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 | ⚡ Quick win

시트 위치가 리사이즈 때마다 강제로 접히는 버그가 있습니다.

Line 147에서 y.set(nextMax)를 매번 실행해, 사용자가 펼쳐둔 시트가 콘텐츠/뷰포트 변경 시 갑자기 내려갑니다. 현재 y를 유지하고 새 범위를 벗어날 때만 clamp 하도록 바꿔주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/user/job-lookup-map/index.tsx` around lines 144 - 148, The
updateBounds function currently forces the sheet closed on every resize by
unconditionally calling y.set(nextMax); change updateBounds to only adjust
maxTranslateY (call setMaxTranslateY(nextMax)) and then clamp the current y
value to the new allowed range instead of overwriting it — use the existing y
value and only set y when it exceeds the new bounds (e.g., clamp y to [0,
nextMax] or set to nextMax only if y.get() > nextMax) so the sheet stays in its
current open position unless out-of-range; reference updateBounds, nextMax,
setMaxTranslateY, y and SHEET_PEEK_HEIGHT when making the change.

Comment on lines +12 to +33
export function SearchBar({
className = '',
wrapperClassName = '',
placeholder = '',
...props
}: SearchBarProps) {
return (
<div
className={`relative flex h-12 w-full items-center rounded-[16px] border border-line-2 bg-white shadow-[0px_2px_8px_rgba(0,0,0,0.08)] ${wrapperClassName}`}
>
<img
src={searchIcon}
alt=""
className="pointer-events-none absolute left-4 h-[22px] w-[22px] shrink-0"
aria-hidden
/>
<input
type="search"
placeholder={placeholder}
className={`h-full w-full rounded-full bg-transparent py-0 pl-11 pr-4 typography-body03-regular text-text-100 placeholder:text-text-50 outline-none ${className}`}
{...props}
/>
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 | ⚡ Quick win

검색 입력의 접근 가능한 이름을 보장해주세요.

현재 구현은 호출부가 aria-label/aria-labelledby를 넘기지 않으면 이름 없는 <input>이 렌더링될 수 있어 스크린리더 사용자가 필드 목적을 알기 어렵습니다. shared 컴포넌트에서 최소 하나의 접근성 이름 소스를 강제하는 게 안전합니다.

수정 예시
 export type SearchBarProps = Omit<
   InputHTMLAttributes<HTMLInputElement>,
   'type' | 'className'
 > & {
   className?: string
   wrapperClassName?: string
+  ariaLabel?: string
 }

 export function SearchBar({
   className = '',
   wrapperClassName = '',
   placeholder = '',
+  ariaLabel,
   ...props
 }: SearchBarProps) {
   return (
@@
       <input
         type="search"
         placeholder={placeholder}
+        aria-label={ariaLabel ?? placeholder || '검색'}
         className={`h-full w-full rounded-full bg-transparent py-0 pl-11 pr-4 typography-body03-regular text-text-100 placeholder:text-text-50 outline-none ${className}`}
         {...props}
       />

As per coding guidelines, "**/*.tsx에서는 a11y는 서비스를 막는 수준(의미 있는 버튼/폼/이미지)만 지적" 규칙에 따라 폼 입력의 접근 가능한 이름 누락 가능성을 우선 지적했습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/ui/common/SearchBar.tsx` around lines 12 - 33, The SearchBar
component can render an unnamed <input> if callers don't pass
aria-label/aria-labelledby; update SearchBar to ensure an accessible name by
detecting if props includes aria-label or aria-labelledby and, if absent,
provide a sensible fallback (e.g., use the placeholder or a default string like
"Search") when rendering the input; refer to the SearchBar function, the input
element that spreads {...props}, and the placeholder prop to implement this
fallback so screen readers always receive an accessible name.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants