Skip to content

✨ feat: 연습 장소 지도 검색·상세·수정·삭제와 일정 삭제 - #263

Merged
JichanPark12 merged 4 commits into
devfrom
feat/practice-room-address-kakao-search
Oct 18, 2026
Merged

JichanPark12 merged 4 commits into
devfrom
feat/practice-room-address-kakao-search

Conversation

@pcwadarong

@pcwadarong pcwadarong commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

⏱ 소요 시간

  • 리뷰 예상 시간: 20분

📌 작업 요약

연습 장소를 "이름만 필수"로 풀고, 위치는 원할 때 지도에서 상호명으로 찾아 붙이게 바꿔요. 고른 위치는 상세 화면에서 정적 지도로 보여주고, 상세 안에서 수정·삭제할 수 있어요. 같은 패턴으로 일정 삭제도 넣었어요.

전 후
필수 입력 이름 + 주소(자유 텍스트 한 칸) 이름만
위치 입력 도로명주소를 직접 타이핑 "지도에서 찾기" → 상호명 검색 → 선택 (주소·좌표가 함께 들어옴)
장소 카드 눌러도 반응 없음 누르면 상세(이름·위치·정적 지도)
장소 수정·삭제 화면 없음 (API만 존재) 상세의 "수정" → 같은 시트 안에서 폼으로 전환 / 앱바 우측 "장소 삭제"
일정 삭제 화면 없음 (API만 존재) 일정 상세·수정 앱바 우측 "일정 삭제"

적용 범위: 밴드 라이브러리의 연습 장소, 일정 추가 폼 안의 장소 추가, 일정 상세(삭제). 백엔드는 places 모듈과 마이그레이션 1건.


📝 작업 내용

1. 주소를 선택값으로, 좌표는 별도 컬럼으로

전                                후
─────────────────────             ─────────────────────────────
name           필수               name           필수
address        필수               address        선택
detailAddress  선택               detailAddress  선택
                                  latitude       선택 (신규)
                                  longitude      선택 (신규)

"동방 1호"처럼 이름만으로 통하는 장소가 많은데 주소가 필수라 아무 글자나 채워야 했어요.

좌표를 따로 저장하는 이유는 지도를 띄울지 판단하는 기준이 필요해서예요. 주소 문자열만으로는 지도에서 고른 것인지 손으로 적은 것인지 구분할 수 없고, 손으로 적은 글자로 지도를 열면 엉뚱한 곳이 나와요. 좌표가 있으면 지도에서 고른 장소예요.

규칙 왜
좌표는 위도·경도 쌍으로, 주소와 함께만 받음 (아니면 400) 반쪽 좌표·주소 없는 좌표는 지도가 엉뚱한 곳을 가리켜요
PATCH로 주소를 바꾸면서 좌표를 안 보내면 좌표를 비움 남겨 두면 새 주소에 옛 위치의 지도가 붙어요
PATCH에서 address·detailAddress·imageUrl은 null(빈 문자열 포함)이 "지움" 기존에는 빈 값을 undefined로 접어서, 한 번 넣은 값을 지울 방법이 없었어요

⚠️ 마이그레이션이 기존 행의 address를 전부 NULL로 비워요. 기존 주소는 모두 손으로 적은 값(좌표 없음)이라 지도와 이어지지 않고, 테스트 데이터뿐이어서 다른 컬럼으로 옮기지 않았어요. 적용 후에는 되돌릴 수 없어요.

2. 검색은 카카오맵 JS SDK를 브라우저에서 바로 호출

REST API로 했다면                    지금 (JavaScript SDK)
──────────────────────               ──────────────────────
브라우저                             브라우저
   ▼ 검색어                             ▼ 검색어 + JS 키(공개값)
우리 백엔드  ← 중계 API 필요          카카오
   ▼ 검색어 + REST 키(비밀값)           ▼ 결과
카카오                               브라우저
대안 안 고른 이유
우편번호(도로명) 검색 상호명으로 못 찾아요. "도로명을 외워서 쳐야 하는" 원래 문제가 그대로 남아요
REST API + 백엔드 중계 REST 키는 비밀값이라 중계 엔드포인트가 하나 더 필요해요. JS 키는 등록 도메인으로 보호되는 공개값이라 같은 결과를 중계 없이 얻어요

SDK는 index.html이 아니라 처음 쓸 때 불러와요(shared/lib/kakao-maps.ts). 장소 검색·상세를 여는 사람만 쓰는 스크립트라서요.

⚠️ VITE_KAKAO_MAP_KEY가 없으면 "지도에서 찾기"와 상세 지도가 에러 없이 숨겨져요. 키는 카카오 디벨로퍼스에 등록한 도메인에서만 동작해요.

⚠️ 카카오 응답은 x가 경도, y가 위도예요. 뒤집혀도 에러 없이 지도만 엉뚱한 곳을 가리켜요(place-search-api.ts에서 변환).

3. 상세는 정적 지도

움직이는 지도는 세로 스크롤 화면에서 손가락이 지도에 걸릴 때마다 스크롤을 막아요. 정적 지도(이미지)로 보여주고, 누르면 카카오맵이 새 탭으로 열려요. SDK가 만드는 링크는 이름이 없고 좌표만 담겨 있어서, 장소명이 붙는 링크와 aria-label로 바꿔 둬요.

좌표가 없는 장소는 "위치" 섹션이 통째로 빠져요.

4. 수정은 상세와 같은 시트 안에서 전환

별도 시트로 띄우면                 지금
─────────────────────              ─────────────────────
[상세 시트]                        [시트 하나]
   └ [수정 시트] ← 한 겹 더           상세 ⇄ 수정 (화면만 교체)
뒤로 가기 2번                      뒤로 가기: 수정 → 상세 → 닫힘

일정 상세(ScheduleCreateModal)와 같은 방식이에요. 폼 본문을 PlaceFormView로 떼어 추가 시트(PlaceCreateModal)와 상세 시트(PlaceDetailModal)가 함께 써요. 폼은 시트가 열릴 때마다 새로 마운트돼서, 열림을 감지해 값을 되돌리던 초기화 로직은 없앴어요.

PATCH는 안 보낸 필드를 유지하므로 지운 값은 null로, 손대지 않은 커버는 보내지 않아요. 상세는 장소 객체가 아니라 ID를 들고 목록에서 다시 찾아요 — 수정 후 목록이 갱신되면 열려 있는 상세도 따라 바뀌어야 해서요.

5. 삭제 — 투표·팀 삭제와 같은 패턴

앱바 우측 액션 + 확인창이에요(SchedulePollDeleteAction과 같은 모양).

누구에게 보이나 삭제 후
장소 삭제 리더·부리더 (백엔드 규칙과 동일) 소프트 삭제. 이 장소로 잡아 둔 일정은 그대로 남아요
일정 삭제 상세·수정 화면을 연 사람 (추가 폼에는 없음) 시트를 닫고 목록 갱신

⚠️ 장소 삭제는 소프트 삭제(isActive=false)라, 목록을 걸러 받지 않으면 지운 장소가 그대로 남아 보여요. 라이브러리와 일정 추가 폼에 where__is_active=true를 붙였어요. 일정 필터 시트는 지난 일정을 찾을 수 있어야 해서 그대로 뒀어요.

⚠️ 일정 삭제 후 상세 캐시까지 무효화하면, 아직 열려 있는 상세가 방금 지운 일정을 다시 조회해 404를 만들어요. 목록만 무효화하고 상세 캐시는 구독이 끊긴 뒤에 지워요.

Changes

커밋 내용
51ffe68 백엔드: 주소 선택화, 좌표 컬럼·검증, PATCH null 지움, Swagger, 마이그레이션
43dad1b 프론트: 지도 검색 모달, 장소 추가·수정 폼, 상세 + 정적 지도
9059e99 프론트: 상세 안에서 수정 전환, 장소·일정 삭제

함께 고친 것

  • PATCH에 name: null을 보내면 @IsOptional이 통과시켜 DB NOT NULL에서 500이 나던 것 → 400
  • 장소 모듈에 빠져 있던 Swagger 데코레이터 (컨트롤러 5개 엔드포인트, DTO 4개)
  • 곡 추가 화면에만 있던 폼 안쪽 보조 버튼 크기를 Button의 size="inline"으로 올려 "지도에서 찾기"와 함께 사용

시안과 다르게 간 곳 (장소 상세)

시안 구현 이유
헤더 제목 "Wonderwall" "연습 장소" 곡 화면에서 복사된 문구로 보여요
이름 24 Bold, "위치" 20 Bold typo-xl-sb(24 SemiBold), typo-lg-b(18 Bold) 타이포 표에 24 Bold·20px이 없어요
주소 ↔ "위치" 간격 68px 24px 라이브러리 섹션 간격에 맞췄어요

의도적으로 안 한 것

  • 생성·수정 폼 안의 지도 미리보기: 선택 칸에 상호명·주소가 이미 보여서 얻는 게 작고, 모바일에서 아래 칸이 화면 밖으로 밀려요.
  • 라이브러리 카드의 두 줄 주소: 카드 폭이 좁아 주소+상세 위치를 한 줄 말줄임으로 유지해요. 상세에서는 두 줄이에요.
  • 교체·삭제한 커버 파일 정리: 스토리지에 옛 파일이 남아요. 다른 업로드도 같은 상태예요.
  • 장소 수정 후 일정 화면 캐시 무효화: 일정 카드의 장소 이름은 다시 불러올 때 바뀌어요.

🚨 주요 고민 및 해결 과정

문제

주소 입력에 지도·주소 검색을 붙이려 했는데, 사람들은 합주실을 도로명이 아니라 상호명으로 기억하고, 주소가 아예 필요 없는 장소도 많았어요.

해결 과정

주소 입력을 "검증된 주소 입력"으로 조이는 대신 반대로 풀었어요. 이름만 필수로 두고, 위치는 지도에서 고른 것만 구조화된 값(주소+좌표)으로 받고, 손으로 적는 메모는 "상세 위치"가 맡아요. 그래서 주소가 있으면 지도와 이어지고, 없으면 지도를 띄우지 않는다는 한 가지 기준으로 화면이 갈려요.


📑 참고 문서/ ADR


✅ Test plan

  • 백엔드 sh ./scripts/verify.sh (lint → format:check → build → test) — 779 tests 통과
  • 프론트 pnpm --dir frontend exec tsc -b — 통과
  • 프론트 pnpm --dir frontend run lint — 에러 0 (기존 경고 1건: BandMemberSettings.tsx exhaustive-deps)
  • 프론트 pnpm --dir frontend test — 498 tests 통과
  • 로컬 dev(5173, MSW mock + 실제 카카오 키): "합정 합주실" 검색 15건 → 선택 시 위치·이름 채워짐, 상세에서 정적 지도 로드, 수정 → 저장 후 상세·카드 갱신
  • 로컬 dev(5173): 장소 상세 → 수정 → 뒤로 가기 동안 시트가 1개로 유지, "장소 삭제" → 확인 → 카드 5개에서 4개로 감소
  • 로컬 dev(5173): 일정 카드 → 상세 앱바 "일정 삭제" → 확인 → 시트 닫힘 + "일정을 삭제했어요." 토스트
  • 장소 삭제 권한 분기(일반 멤버에게 숨김)는 브라우저에서 보지 않았어요 — 미실행 (단위 테스트만 통과)
  • 스크린샷으로 시안 대조 — 미실행 (수치로만 확인)
  • 실제 백엔드 연동 — 미실행 (마이그레이션을 DB에 적용하지 않았어요)
  • 배포 도메인(band-co.vercel.app)에서 카카오 검색·지도 — 미실행

💬 리뷰 요구사항

  1. 마이그레이션의 UPDATE "places" SET "address" = NULL — 기존 주소를 지워도 되는지. 되돌릴 수 없어요.
  2. PATCH null 의미 변경 (update-place.dto.ts) — 빈 문자열이 "유지"에서 "지움"으로 바뀌어요. 장소 수정 API를 쓰는 화면은 이번에 처음 생겨서 기존 호출부는 없어요.
  3. 일정 수정·삭제 API에 인증 가드가 없어요 (schedules.controller.ts의 PATCH·DELETE) — 이번 PR 이전부터 그랬고 건드리지 않았어요. 삭제 버튼이 생기면서 눈에 띄게 돼서 적어 둬요. 별도 PR로 막을지 의견 주세요.
  4. PlaceStaticMap.tsx — SDK가 그린 DOM(a, img)을 직접 고쳐요(링크 주소·aria-label·alt). SDK가 구조를 바꾸면 조용히 빠질 수 있는 자리예요.
  5. 배포 순서 — 백엔드(마이그레이션 포함) 먼저, 프론트 나중이에요. 반대면 이름만 넣은 장소 추가가 400으로 실패해요. Vercel에 VITE_KAKAO_MAP_KEY도 필요해요.

🔜 후속

  • 일정 수정·삭제 권한 — 백엔드에 가드와 권한 규칙(작성자? 리더?)이 정해져야 프론트에서 삭제 버튼 노출 조건을 맞출 수 있어요. 지금은 상세를 연 누구에게나 보여요.
  • 일정 상세에서 지도 보기 — 일정 응답의 place가 이름·주소만 내려줘요. 백엔드가 좌표를 함께 내려줘야 시작할 수 있어요.
  • 네이티브 앱·호출량 제한이 필요해지면 검색을 백엔드 중계로 옮겨야 해요. JS 키는 웹 도메인에서만 동작하고 사용자별 제한을 걸 수 없어요. 지금은 쿼터가 문제 될 규모가 아니에요.
  • VITE_KAKAO_MAP_KEY 문서화 — 예시 env 파일 위치가 정해지면 추가해요.

🤖 Generated with Claude Code

pcwadarong and others added 2 commits October 11, 2026 03:25
"동방 1호"처럼 이름만으로 통하는 장소가 많은데 address가 필수라 아무 글자나 채워야 했다.
주소를 선택값으로 풀고, 지도 검색으로 고른 위치는 위도·경도를 함께 저장한다.

좌표를 따로 두는 이유: 주소 문자열만으로는 지도에서 고른 것인지 손으로 적은 것인지
구분할 수 없다. 손으로 적은 "동방 1호"로 지도를 열면 엉뚱한 곳이 나오므로, 좌표가
있는 장소만 지도를 띄우는 기준으로 쓴다.

- 좌표는 위도·경도 쌍으로, 주소와 함께만 받는다. 반쪽 좌표나 주소 없는 좌표는 400.
- PATCH로 주소를 바꾸면서 좌표를 안 보내면 이전 좌표를 비운다. 남겨 두면 새 주소에
  옛 위치의 지도가 붙는다.
- PATCH에서 address·detailAddress·imageUrl은 null(또는 빈 문자열)을 "지움"으로 받는다.
  기존에는 빈 값을 undefined로 접어 한 번 넣은 값을 지울 방법이 없었다.
- name은 @IsOptional이 null을 통과시켜 DB NOT NULL에서 500이 났다. null을 400으로 막는다.
- 기존 행의 주소는 전부 손으로 적은 값이라 마이그레이션에서 비운다(테스트 데이터뿐).
- 일정 상세 응답의 place.address도 nullable이 된다.

장소 모듈에 빠져 있던 Swagger 데코레이터도 컨벤션대로 채운다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
주소를 도로명으로 직접 치게 하는 대신, 이름만 필수로 두고 위치는 원할 때 지도에서
상호명으로 찾아 붙이게 한다. 상세 화면에서는 고른 위치를 정적 지도로 보여준다.

검색은 카카오맵 JS SDK의 키워드 검색을 브라우저에서 바로 부른다. REST API는 비밀
키라 백엔드 중계 엔드포인트가 하나 더 필요한데, JS 키는 등록 도메인으로 보호되는
공개값이라 같은 결과를 중계 없이 얻는다. 우편번호 검색은 상호명으로 찾을 수 없어
"도로명을 외워서 쳐야 하는" 원래 문제가 그대로 남는다.

SDK는 index.html이 아니라 처음 쓸 때 불러온다 — 장소 검색·상세를 여는 사람만 쓴다.
VITE_KAKAO_MAP_KEY가 없으면 검색 진입점과 상세 지도를 숨긴다.

- 장소 추가 폼: 이름 / 위치(지도에서 찾기) / 상세 위치 / 커버. 고른 장소의 상호명은
  이름 칸이 비어 있을 때만 채운다.
- 상세: 카드를 누르면 열린다. 좌표가 있는 장소만 정적 지도를 보여주고, 누르면
  카카오맵으로 간다. 움직이는 지도는 세로 스크롤 화면에서 손가락이 걸릴 때마다
  스크롤을 막아서 쓰지 않는다.
- 수정: 상세의 "수정"이 같은 폼을 값이 채워진 채로 연다. PATCH는 안 보낸 필드를
  유지하므로 지운 값은 null로 보낸다.
- 상세는 장소 객체가 아니라 ID를 들고 목록에서 다시 찾는다. 수정 후 목록이 갱신되면
  열려 있는 상세도 따라 바뀐다.
- 곡 추가 화면에만 있던 폼 안쪽 보조 버튼 크기를 버튼 size(inline)로 올려 함께 쓴다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
band-co Error Error Oct 18, 2026 12:13am UTC

@pcwadarong pcwadarong self-assigned this Oct 10, 2026
@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Repository: BandCo-House/BandCo/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 885ced06-08cd-4ddb-8bcd-e39c103f4543
📥 Commits

Reviewing files that changed from the base of the PR and between 8668473 and 28435a4.

📒 Files selected for processing: 53
  • backend/docs/backend/api-docs/place.md
  • backend/prisma/migrations/20261011120000_add_place_coordinates/migration.sql
  • backend/prisma/schema.prisma
  • backend/src/common/validation-message/coordinate-validation.message.ts
  • backend/src/common/validation-message/number-validation.message.ts
  • backend/src/common/validation/transform.util.ts
  • backend/src/modules/places/dto/create-place.dto.ts
  • backend/src/modules/places/dto/get-band-places-query.dto.ts
  • backend/src/modules/places/dto/get-place-detail-query.dto.ts
  • backend/src/modules/places/dto/update-place.dto.spec.ts
  • backend/src/modules/places/dto/update-place.dto.ts
  • backend/src/modules/places/places.controller.ts
  • backend/src/modules/places/places.service.spec.ts
  • backend/src/modules/places/places.service.ts
  • backend/src/modules/places/repositories/places.prisma-repository.ts
  • backend/src/modules/places/types/create-place-result.type.ts
  • backend/src/modules/places/types/place-detail.type.ts
  • backend/src/modules/places/types/place-list.type.ts
  • backend/src/modules/places/types/update-place-result.type.ts
  • backend/src/modules/schedules/types/schedule-detail.type.ts
  • frontend/src/entities/place/api/place-api.ts
  • frontend/src/entities/place/api/place-search-api.test.ts
  • frontend/src/entities/place/api/place-search-api.ts
  • frontend/src/entities/place/api/useDeletePlace.ts
  • frontend/src/entities/place/api/useSearchPlaces.ts
  • frontend/src/entities/place/api/useUpdatePlace.ts
  • frontend/src/entities/place/lib/place-location.ts
  • frontend/src/entities/place/lib/place-map-url.ts
  • frontend/src/entities/place/model/schema.ts
  • frontend/src/entities/place/ui/PlaceCard.test.tsx
  • frontend/src/entities/place/ui/PlaceCard.tsx
  • frontend/src/entities/place/ui/PlaceDetailView.test.tsx
  • frontend/src/entities/place/ui/PlaceDetailView.tsx
  • frontend/src/entities/place/ui/PlaceStaticMap.tsx
  • frontend/src/entities/schedule/api/index.ts
  • frontend/src/entities/schedule/model/types.ts
  • frontend/src/features/place-create/ui/PlaceCreateModal.test.tsx
  • frontend/src/features/place-create/ui/PlaceCreateModal.tsx
  • frontend/src/features/place-create/ui/PlaceDetailModal.test.tsx
  • frontend/src/features/place-create/ui/PlaceDetailModal.tsx
  • frontend/src/features/place-create/ui/PlaceFormView.tsx
  • frontend/src/features/place-create/ui/PlaceSearchModal.tsx
  • frontend/src/features/schedule-create/api/useDeleteSchedule.ts
  • frontend/src/features/schedule-create/ui/ScheduleCreateModal.test.tsx
  • frontend/src/features/schedule-create/ui/ScheduleCreateModal.tsx
  • frontend/src/features/schedule-create/ui/ScheduleFormView.tsx
  • frontend/src/features/song-create/ui/SongCreateModal.tsx
  • frontend/src/mocks/place/handlers.ts
  • frontend/src/mocks/schedule/handlers.ts
  • frontend/src/shared/api/config.ts
  • frontend/src/shared/lib/kakao-maps.ts
  • frontend/src/shared/ui/button/button-variants.ts
  • frontend/src/widgets/band-library/ui/BandLibrary.tsx
+-----------------------------------------------------------------+
| Granted, I'm not human, but I still know when your code is bad. |
+-----------------------------------------------------------------+
  \
   \     /)/)
        ( '')
        /  |
      o(___)
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

장소 상세의 "수정"이 폼 시트를 한 겹 더 띄우던 것을, 일정 상세처럼 한 시트 안에서
화면만 바꾸게 한다. 같은 장소 위에 시트가 또 올라오면 뒤로 가기가 어디로 가는지
헷갈리고, 닫을 때도 두 번 닫아야 했다.

그러려고 폼 본문을 PlaceFormView로 떼어 추가 시트(PlaceCreateModal)와 상세 시트
(PlaceDetailModal)가 함께 쓴다. 폼은 시트가 열릴 때마다 새로 마운트되므로, 열림을
감지해 값을 되돌리던 초기화 로직은 없앤다.

삭제는 투표·팀 삭제와 같은 패턴(앱바 우측 액션 + 확인창)으로 넣는다.

- 장소 삭제는 리더·부리더만 가능해(백엔드 규칙) 그 역할에게만 보인다.
- 장소 삭제는 소프트 삭제(isActive=false)라, 라이브러리와 일정 추가 폼이 활성 장소만
  받도록 where__is_active=true를 붙인다. 안 붙이면 지운 장소가 그대로 남아 보인다.
- 일정 삭제는 상세·수정 화면에서만 보인다. 추가 폼에는 지울 대상이 없다.
- 일정 삭제 후에는 목록 캐시만 무효화한다. 상세까지 무효화하면 아직 열려 있는 상세가
  방금 지운 일정을 다시 조회해 404를 만든다. 상세 캐시는 구독이 끊긴 뒤에 지운다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pcwadarong pcwadarong changed the title ✨ feat: 연습 장소 지도 검색·상세 지도·수정 ✨ feat: 연습 장소 지도 검색·상세·수정·삭제와 일정 삭제 Oct 10, 2026
@JichanPark12
JichanPark12 merged commit fcabb76 into dev Oct 18, 2026
1 of 3 checks passed
@JichanPark12
JichanPark12 deleted the feat/practice-room-address-kakao-search branch October 18, 2026 00:13

This branch had an error being deployed

1 failed deployment
Preview — 28435a40 Deployed Oct 18, 2026 by vercel[bot]
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