Skip to content

Conversation

@mindaaaa
Copy link
Collaborator

📌 관련 이슈

Closed #34 , #36


✅ PR 체크리스트(최소요구조건)

  • 테스트 작성

단순한 UI 컴포넌트기에 테스트하지 않았습니다.

✨ 작업 개요

  • 지도에서 사용될 핀마커 UI 컴포넌트 구현
  • 핀 상태에 따른 시각적/모션 차별화
  • 공통 타입 정의 및 Storybook 스토리 추가
  • ESLint 설정 오류 및 기존 코드 품질 이슈 정리

🧹 작업 상세 내용

  1. PinMarker UI 컴포넌트 구현

    • current / record 상태에 따른 핀 타입 분리
    • 접근성 속성(role, aria-label, aria-pressed) 기본 지원
    • hover / selected 상태에 따른 시각적 반응 정의
  2. 핀 상태별 아이콘 및 모션 차별화

    • 파란 핀(Pending): 사용자 인터랙션 가능 상태 강조
    • 보라 핀(Completed): 기록 완료 상태로, 과도한 시각적 강조 최소화
  3. Storybook 스토리 추가

    • 상태별 렌더링
    • 인터랙션 및 접근성 확인용 스토리 구성
  4. 린트 및 설정 관련 이슈 해결

    • Storybook 디렉터리 ESLint 제외 처리
    • 기존 컴포넌트 타입 안정성 개선

📸 스크린샷 (선택)

핀 마커 스토리 인터랙션


🔍 고민 지점

[ transform 속성이 의도대로 적용되지 않는 문제 ]

  • 문제점
    핀 선택 상태에서 scale, translate 등의 transform 효과를 단계적으로 적용하고 싶었으나,
    CSS transform 특성상 동일 요소에 transform은 하나의 값만 적용되기 때문에
    상태별로 transform을 덮어쓰는 구조가 되어 의도한 조합이 어려웠습니다.

    특히 SVG 내부 요소와 상위 컨테이너에 transform이 혼재되면서
    기준점(origin)이 어긋나거나, 일부 transform이 무시되는 문제가 발생했습니다.

  • 해결 과정

    • transform을 SVG 내부 path가 아닌 상위 래퍼 요소에만 적용
    • transform-origin을 명시적으로 지정
    • 상태별로 transform 클래스를 분리하고,
      scale / translate / opacity 효과를 조합하는 방식으로 구조 변경
    • 복잡한 transform 중첩 대신, 단순한 scale + opacity 중심의 모션으로 재구성

    결과적으로,
    “하나의 transform을 어떻게 덮어쓸 것인가”보다
    **“어떤 상태 조합을 클래스 레벨에서 표현할 것인가”**에 초점을 맞추는 방향으로 설계를 변경했습니다.

Note

CSS transform은 하나의 요소에 대해 단일 속성으로 취급되기 때문에
여러 클래스에서 transform을 각각 정의하면 마지막 선언만 적용됩니다.

예를 들어, 아래와 같은 경우 의도한 동작이 나오지 않습니다.

.lift {
  transform: translateY(-6px);
}

.scale {
  transform: scale(1.1);
}
<div class="lift scale" />

→ 결과적으로 scale(1.1)만 적용되고, translateY는 무시됩니다.

이를 피하기 위해 본 구현에서는
상태별로 transform을 조합한 클래스를 명시적으로 분리하는 방식을 선택했습니다.

.pin-base {
  transition: transform 0.2s ease;
}

.pin-selected {
  transform: translateY(-6px) scale(1.08);
}

.pin-hover {
  transform: scale(1.04);
}

이 구조를 통해

  • transform 중첩으로 인한 예측 불가능한 동작을 피하고
  • 상태 조합을 클래스 레벨에서 명확하게 제어할 수 있었습니다.

[ 두 가지 핀을 어떻게 다른 ‘존재감’으로 보여줄 것인가 ]

  • 문제점
    파란 핀과 보라 핀은 역할이 다르기 때문에
    동일한 애니메이션과 시각적 강조를 주는 것이 오히려 UX를 해칠 수 있다고 판단했습니다.

    • 파란 핀:

      • “지금 눌러도 된다”
      • “상호작용 가능한 대상이다”
        → 사용자의 시선을 적극적으로 끌 필요가 있음
    • 보라 핀:

      • 이미 기록이 완료된 상태
      • 지도 상 정보 제공이 주 목적
        → 과도한 spotlight는 불필요
  • 해결 과정

    • CSS 사용을 최소화하고, 다음 요소 위주로 차별화
      • 색상 대비
      • 기본/선택 상태의 scale 차이
      • pulse 강도 조절
      • shadow 및 glow 효과의 유무
    • 파란 핀은 지속적인 미세한 pulse로 “활성 상태”를 표현
    • 보라 핀은 hover 또는 선택 시에만 제한적인 반응을 주도록 설계

    결과적으로,
    사용자에게는 자연스럽게 우선순위가 전달되는 인터랙션을 목표로 했습니다.

💬 기타 참고 사항

  • 본 PR은 지도 코어 UI의 기반을 만드는 단계로,
    실제 지도 SDK 연동 이후에도 재사용 가능하도록 설계했습니다.
  • 이후 기록/그룹 기능과 결합 시,
    핀 상태 확장이 가능하도록 타입 구조를 열어두었습니다.

Note

API 명세 협의가 아직 완료되지 않아,
PinMarkerData의 필드 구성과 값은 UI 구현을 위한 임시 값으로 정의되어 있습니다.
추후 API 스펙 확정 시 변경될 예정입니다.

- PinMarkerPendingIcon (보라색, 기록 완료 상태)
- PinMarkerCompletedIcon (파란색, 기록 대기 상태)
- 각 아이콘의 Storybook 스토리 추가
- Storybook 파일 lint 제외 설정 추가
- RefreshIcon 스토리 void expression 수정
- ErrorFallback 타입 안전성 개선
- Coordinates, PinVariant, PinMarkerData 타입 정의
- shared/types/index.ts에 export 추가
- PinMarkerPendingIcon과 PinMarkerCompletedIcon의 SVG 내용이 뒤바뀌어 있었음
- 각 아이콘 파일에 올바른 SVG 내용으로 수정
- variant 기반 핀마커 컴포넌트 (current/record)
- 파란 핀: 기본 상태에서 pulse + glow 애니메이션 지속
- 보라 핀: hover 시 미세 반응, 선택 시 스윕 효과
- 선택 상태: 위로 lift 애니메이션 및 그림자 효과
- 접근성 속성 지원 (role, aria-label, aria-pressed)
- Interactive, States, Accessibility 스토리 구현
- 현재 위치/기록 완료 핀의 다양한 상태 시연
- 파란 핀 기본/선택 상태 애니메이션 분리
- 기본 상태 pulse 강도 감소 (scale 1.02)
- 선택 상태 pulse 강도 증가 (scale 1.08)
- 보라 핀 선택 시 scale up 효과 추가 (scale 1.06)
- PinMarkerPendingIcon의 + 표시를 그룹화하여 독립 제어 가능하도록 변경
- 선택 상태에서 + 표시가 pulse 애니메이션으로 커졌다 작아지는 효과 추가
- 불필요한 스토리 제거 (Current, Record, Matrix)
- InteractiveDemo를 render 함수 내부로 이동
- AccessibilityNotes에서 키보드 관련 참고사항 제거
@mindaaaa mindaaaa self-assigned this Dec 30, 2025
@mindaaaa mindaaaa added UI 화면, 레이아웃, UX 흐름, UI 구조와 관련된 작업 goal: foundation 이후 기능 구현을 위한 기반을 다지는 작업 priority: p0 반드시 해야 다음 단계로 넘어갈 수 있는 작업 labels Dec 30, 2025
...props
}: IconProps) {
return (
<svg
Copy link
Collaborator

Choose a reason for hiding this comment

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

이런 svg 설정을 svgr 같은 것으로 처리하지 않는 이유가 궁금해요!!
Vite 에서 지원해주는 vite-plugin-svgr 이런걸 쓰면 최적화도 자동으로 해준다고 본 거 같아서요!!
(번들 크기 최소화, tree-shaking 등)

Copy link
Collaborator Author

@mindaaaa mindaaaa Dec 30, 2025

Choose a reason for hiding this comment

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

고민을 해보긴 했는데,
개발 편의와 운영 측면에서 svg를 컴포넌트화하는게 더 편해서 이쪽으로 진행하고 있어요!

  • src 아래 자산은 어차피 번들링 대상이라 svgr을 도입한다고 해서 성능/최적화가 결정적으로 달라진다고 보진 않았고,
  • 현재 단계에서는 관리할 SVG 파일이 많지 않고 점진적으로 늘어나는 구조라, 초기 셋업(플러그인/규칙/컨벤션) 비용을 먼저 지는 것보다 필요해졌을 때 도입하는 편이 더 합리적이라고 판단했습니다!

특히 제가 아직 svgr에 익숙하지 못한 탓도 있겠지만,
관리할 아이콘들이 상태별로 내부 요소(ex. 색상 변경)를 제어하는 경우가 많은데.

svgr 같은 경우는 currentColor 규칙에 맞춰 fill/stroke를 정리하거나 변환 옵션을 관리해서 파일을 붙여야,

<PinIcon
  className={cn(
    'w-6 h-6',
    isSelected && 'text-blue-600'
  )}
  data-selected={isSelected}
/>

이런 식의 제어가 가능해지는데.

특정 path만 class를 붙이고 싶어지면 또 다시 svgr 결과물을 내부적으로 편집하고...
요런게 직접 svg를 컴포넌트화했을 때보다 관리가 불편하다고 생각합니다!

return (
  <svg className={iconClass}>
    <g className={isSelected ? 'icon-plus-selected' : 'icon-plus'}>
    <g className="icon-body" />
  </svg>
);

이게 더 보기 편한 느낌이라, 현재는 이렇게 진행하고 있습니다!
추후 아이콘 에셋이 대량으로 늘면 svgr을 도입해도 좋을 것 같아요.

숙련도 부족 이슈 ㅠㅠ

Comment on lines 28 to 35

if (isCurrent && !isSelected) classes.push('pin-blue-idle');

if (isSelected)
classes.push(isCurrent ? 'pin-blue-selected' : 'pin-purple-selected');

// 보라색 핀 hover 시 살짝 반응 (데스크톱 전용)
if (isRecord && !isSelected && isHovered) classes.push('pin-purple-hover');
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 조건문들이 핀의 상태(Idle, Selected, Hover)를 결정하기 위한 로직 같은데 isCurrent && !isSelected 처럼 조건이 나열되어 있다 보니 한눈에 의도를 파악하기가 조금 어려운 거 같아요 😅

의미를 담은 변수 또는 함수 추출해보는 건 어떨까요?

스타일을 적용하는 로직(코드)가 가독성도 좋아지고 문서처럼 보일 수도 있을거 같아요!!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

const isBlueIdle = isCurrent && !isSelected;
const isBlueSelected = isCurrent && isSelected;
const isPurpleSelected = isRecord && isSelected;
const isPurpleHover = isRecord && !isSelected && isHovered;

if (isBlueIdle) classes.push('pin-blue-idle');
if (isBlueSelected) classes.push('pin-blue-selected');
if (isPurpleSelected) classes.push('pin-purple-selected');
if (isPurpleHover) classes.push('pin-purple-hover');

이런 식으로 말씀이시죠?
저도 고민했던 포인트였는데,
요게 변수 이름 정하기가 어려워서 ㅠㅠ (변수 이름이 길면 읽기 어려울거같은데 뭔가 와닿는게 없는 상태)

고민하다가 일단 유지시키는 쪽으로 PR을 날렸는데,
변수 이름을 조금 더 고민해보고 적용해보겠습니다!

@qjatjr29
Copy link
Collaborator

같은 좌표의 여러 핀이 있을 때(current, record) 렌더링 순서에 따라 뒤의 핀이 앞의 핀을 가릴 수 있을 거 같아요!!
record 핀이 current 핀를 가리면 사용자가 current 핀을 클릭할 수 없는 상황 발생할 수도 있지 않을까 싶어요!!

이 부분은 Map에 실제 핀을 적용할 때 처리되는 것이겠죠?!?

@mindaaaa
Copy link
Collaborator Author

좋은 의견 감사합니다!

같은 좌표의 여러 핀이 있을 때(current, record) 렌더링 순서에 따라 뒤의 핀이 앞의 핀을 가릴 수 있을 거 같아요!!
record 핀이 current 핀를 가리면 사용자가 current 핀을 클릭할 수 없는 상황 발생할 수도 있지 않을까 싶어요!!

말씀해주신 것처럼,
같은 좌표에 여러 핀(current, record)이 겹치는 경우에는
렌더링 순서나 레이어(z-index)에 따라 클릭 가능 여부에 영향을 줄 수 있을 것 같아요!😲

이 부분은 실제 Map SDK를 도입하면서

  • SDK에서 zIndex 혹은 레이어 우선순위 제어를 제공하는지 확인하고
  • current 핀이나 selected 핀을 우선적으로 위에 두는 방식으로

구체적인 동작을 함께 정리해보겠습니다!

이번 PR에서는 UI 컴포넌트 구현에 집중하고,
해당 포인트는 지도 연동 시 함께 검증할 항목으로 남겨두겠습니다!🙇‍♀️

@mindaaaa mindaaaa merged commit 02defc8 into develop Jan 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

goal: foundation 이후 기능 구현을 위한 기반을 다지는 작업 priority: p0 반드시 해야 다음 단계로 넘어갈 수 있는 작업 UI 화면, 레이아웃, UX 흐름, UI 구조와 관련된 작업

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 핀마커 오버레이 UI 컴포넌트 구현

3 participants