Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
1480903
채팅 위치 보내기 추가 : feat : ChatMessage에 LOCATION 타입 및 위치 필드 추가 https://git…
SeoHyun1024 Apr 1, 2026
712fd91
채팅 위치 보내기 추가 : feat : WebSocket sendMessage에 LOCATION 타입 지원 추가 https:…
SeoHyun1024 Apr 1, 2026
ffac297
채팅 위치 보내기 추가 : feat : 채팅 위치 선택 화면(ChatLocationPickerScreen) 추가 https:…
SeoHyun1024 Apr 1, 2026
ad89184
채팅 위치 보내기 추가 : refactor : ChatLocationPickerScreen dead code 제거 및 iPa…
SeoHyun1024 Apr 1, 2026
6992151
채팅 위치 보내기 추가 : feat : 채팅 위치 말풍선(ChatLocationBubble) 위젯 추가 https://git…
SeoHyun1024 Apr 1, 2026
f89443d
채팅 위치 보내기 추가 : fix : ChatLocationBubble Static Maps 이미지 요청 크기 수정 http…
SeoHyun1024 Apr 1, 2026
a8ef7f1
채팅 위치 보내기 추가 : feat : ChatInputBar에 위치 보내기 메뉴 항목 추가 https://github.co…
SeoHyun1024 Apr 1, 2026
cc1c15e
채팅 위치 보내기 추가 : feat : ChatMessageItem에 LOCATION 타입 말풍선 렌더링 추가 https:/…
SeoHyun1024 Apr 1, 2026
ae95907
채팅 위치 보내기 추가 : feat : 채팅방 위치 보내기 기능 연결 완료 https://github.com/TEAM-ROM…
SeoHyun1024 Apr 1, 2026
df9a1b9
채팅 화면에서 위치전송 기능 추가 : feat : ChatMessage 에서 address 필드 삭제, 위치 말풍선 주소 반…
SeoHyun1024 Apr 2, 2026
3ca336b
채팅 화면에서 위치전송 기능 추가 : feat : naver static map url 변경 https://github.co…
SeoHyun1024 Apr 2, 2026
5c71f90
채팅 화면에서 위치전송 기능 추가 : feat : 위치 말풍선 UI 명세대로 수정 https://github.com/TEAM…
SeoHyun1024 Apr 2, 2026
2a0accd
채팅 화면에서 위치전송 기능 추가 : feat : 위치 전송 화면 UI 명세대로 수정, 위치 미리보기 text 수정 htt…
SeoHyun1024 Apr 2, 2026
a32140f
채팅 화면에서 위치전송 기능 추가 : feat : chatMessageItem에 ValueKey 추가 https://git…
SeoHyun1024 Apr 2, 2026
297a936
코드 포매팅 처리 히스토리 관리 : refactor : auto format code https://github.com/TE…
github-actions[bot] Apr 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
835 changes: 835 additions & 0 deletions docs/superpowers/plans/2026-04-02-chat-location-share.md

Large diffs are not rendered by default.

131 changes: 131 additions & 0 deletions docs/superpowers/specs/2026-04-02-chat-location-share-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# 채팅 위치 공유 기능 설계

**날짜:** 2026-04-02
**이슈:** 채팅방 위치 보내기 기능

---

## 개요

채팅방에서 본인의 위치를 지도 말풍선 형태로 전송하는 기능.
백엔드와 `MessageType.LOCATION`으로 소통하며, Naver Static Maps API로 지도 썸네일을 렌더링한다.

---

## 유저 플로우

1. 채팅 입력 바 `+` 버튼 탭 → 컨텍스트 메뉴 "위치 보내기" 선택
2. `ChatLocationPickerScreen` 진입 — 현재 위치로 초기화된 전체 화면 네이버 지도
3. 지도를 움직여 원하는 위치 선택 → "보내기" 버튼
4. `Navigator.pop`으로 `LocationAddress` 반환 → `ChatRoomScreen`에서 WebSocket 전송
5. 채팅창에 위치 말풍선 노출
6. 말풍선 "지도에서 보기" 버튼 → 네이버지도 앱 실행 (없으면 웹)

---

## 데이터 변경

### `MessageType` (lib/enums/message_type.dart)
```dart
@JsonValue('LOCATION')
location,
```

### `ChatMessage` (lib/models/apis/objects/chat_message.dart)
필드 3개 추가:
```dart
final double? latitude;
final double? longitude;
final String? address;
```
`build_runner`로 `.g.dart` 재생성 필요.

### `ChatWebSocketService.sendMessage`
LOCATION 타입일 때 payload에 포함:
```json
{
"chatRoomId": "...",
"type": "LOCATION",
"latitude": 37.5665,
"longitude": 126.9780,
"address": "서울특별시 광진구 능동로 209"
}
```

---

## 새 파일

### 1. `lib/screens/chat_location_picker_screen.dart`

`ItemRegisterLocationScreen`과 동일한 패턴:
- 초기 위치: 현재 위치 (권한 없으면 서울시청 폴백)
- 중앙 핀 SVG + 카메라 idle 시 역지오코딩
- 하단 "보내기" 버튼: `Navigator.pop(context, selectedAddress)`
- 커스텀 `CurrentLocationButton`

### 2. `lib/widgets/chat_location_bubble.dart`

말풍선 구조 (264.w 고정폭):
```
┌────────────────────────┐
│ Naver Static Map │ Image.network (264w × 160h)
│ 이미지 (마커 포함) │
├────────────────────────┤
│ 주소 텍스트 │ padding 12w/10h
├────────────────────────┤
│ 지도에서 보기 │ GestureDetector 버튼
└────────────────────────┘
```

**Static Maps API:**
```
URL: https://naveropenapi.apigw.ntruss.com/map-static/v2/raster
?w=264&h=160
&center={lng},{lat}
&level=15
&markers=type:d|size:mid|pos:{lng}%20{lat}
Headers:
X-NCP-APIGW-API-KEY-ID: {NMF_CLIENT_ID}
X-NCP-APIGW-API-KEY: {NMF_CLIENT_SECRET}
```
`Image.network`의 `headers` 파라미터로 인증.
로딩 중: shimmer 또는 Container(color: AppColors.secondaryBlack1)
에러 시: 주소 텍스트만 표시

**"지도에서 보기" 딥링크:**
```dart
// 앱
nmap://map?lat={lat}&lng={lng}&zoom=15&appname=com.alom.romrom
// 웹 폴백
https://map.naver.com/v5/?c={lng},{lat},15,0,0,0,dh
```

---

## 수정 파일

### `lib/widgets/chat_input_bar.dart`
- `onSendLocation: VoidCallback` 파라미터 추가
- `ContextMenuItem` 리스트에 위치 항목 추가

### `lib/widgets/chat_message_item.dart`
- `_buildBubble`에 LOCATION 타입 분기:
```dart
if (message.type == MessageType.location) {
return ChatLocationBubble(message: message);
}
```

### `lib/screens/chat_room_screen.dart`
- `_onSendLocation()` 핸들러 추가
- `ChatLocationPickerScreen` push 후 결과 받아 WebSocket 전송

---

## 고려사항

- **Static Maps API**: NCP 콘솔에서 "Maps Static API" 서비스 활성화 필요
- **iPad 대응**: 말풍선 고정 픽셀값 사용 (`.w` 최소화)
- **에러 처리**: 위치 권한 거부 시 서울시청 폴백 (기존 패턴 동일)
- **중복 전송 방지**: `_pendingRequests` 패턴 적용
44 changes: 22 additions & 22 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,11 @@ SPEC CHECKSUMS:
AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d
firebase_auth: e9031a1dbe04a90d98e8d11ff2302352a1c6d9e8
firebase_core: ee30637e6744af8e0c12a6a1e8a9718506ec2398
firebase_messaging: 343de01a8d3e18b60df0c6d37f7174c44ae38e02
firebase_auth: e7aec07fcada64e296cf237a61df9660e52842c2
firebase_core: 8d5e24676350f15dd111aa59a88a1ae26605f9ba
firebase_messaging: 834cfc0887393d3108cdb19da8e57655c54fd0e4
FirebaseAppCheckInterop: ba3dc604a89815379e61ec2365101608d365cf7d
FirebaseAuth: 4c289b1a43f5955283244a55cf6bd616de344be5
FirebaseAuthInterop: 95363fe96493cb4f106656666a0768b420cba090
Expand All @@ -280,34 +280,34 @@ SPEC CHECKSUMS:
FirebaseInstallations: 6a14ab3d694ebd9f839c48d330da5547e9ca9dc0
FirebaseMessaging: 7f42cfd10ec64181db4e01b305a613791c8e782c
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
flutter_naver_map: d9f447f1271059759d4d5e38c3b347d7ab2ba835
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
geocoding_ios: 33776c9ebb98d037b5e025bb0e7537f6dd19646e
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
google_sign_in_ios: 000870aa06da9b28d1d0bf7ef70ff0213059dd28
flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
flutter_naver_map: a1f6df83f663d9770e01624ba97a2d34c4fe349b
flutter_secure_storage_darwin: 557817588b80e60213cbecb573c45c76b788018d
geocoding_ios: eafacae6ad11a1eb56681f7d11df602a5fd49416
geolocator_apple: 66b711889fd333205763b83c9dcf0a57a28c7afd
google_sign_in_ios: 7336a3372ea93ea56a21e126a0055ffca3723601
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleSignIn: fcee2257188d5eda57a5e2b6a715550ffff9206d
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
kakao_flutter_sdk_common: 682b3606698f87467788598dc2dc09d4e6867fbd
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
kakao_flutter_sdk_common: a21740b9dd4900f96161f365a2b6ece7a97cbf4f
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
NMapsGeometry: 4e02554fa9880ef02ed96b075dc84355d6352479
NMapsMap: 1964e6f9073301ad3cbe3a12235ba36f6f6cd905
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
patrol: cea8074f183a2a4232d0ebd10569ae05149ada42
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
patrol: d49fcd015892f19189a4cec572f21f3c3358e761
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4

PODFILE CHECKSUM: ce2a4dd764e1c7aeed6a7cdc5e61d092b6dc6d32

Expand Down
2 changes: 2 additions & 0 deletions lib/enums/message_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ enum MessageType {
image,
@JsonValue('SYSTEM')
system,
@JsonValue('LOCATION')
location,
}
16 changes: 12 additions & 4 deletions lib/models/apis/objects/chat_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ part 'chat_message.g.dart';
/// 채팅 메시지 모델 (MongoDB)
@JsonSerializable()
class ChatMessage extends BaseEntity {
final String? chatMessageId; // MongoDB ObjectId
final String? chatRoomId; // UUID
final String? senderId; // UUID
final String? recipientId; // UUID
final String? chatMessageId;
final String? chatRoomId;
final String? senderId;
final String? recipientId;
final String? content;
final List<String>? imageUrls;
final MessageType? type;
final bool? isProfanityDetected;
final double? latitude;
final double? longitude;

ChatMessage({
super.createdDate,
Expand All @@ -28,6 +30,8 @@ class ChatMessage extends BaseEntity {
this.imageUrls,
this.type,
this.isProfanityDetected,
this.latitude,
this.longitude,
});

factory ChatMessage.fromJson(Map<String, dynamic> json) => _$ChatMessageFromJson(json);
Expand All @@ -48,6 +52,8 @@ extension ChatMessageCopy on ChatMessage {
bool? isProfanityDetected,
DateTime? createdDate,
DateTime? updatedDate,
double? latitude,
double? longitude,
}) => ChatMessage(
chatMessageId: chatMessageId ?? this.chatMessageId,
chatRoomId: chatRoomId ?? this.chatRoomId,
Expand All @@ -59,5 +65,7 @@ extension ChatMessageCopy on ChatMessage {
isProfanityDetected: isProfanityDetected ?? this.isProfanityDetected,
createdDate: createdDate ?? this.createdDate,
updatedDate: updatedDate ?? this.updatedDate,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
);
}
11 changes: 10 additions & 1 deletion lib/models/apis/objects/chat_message.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/models/app_urls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class AppUrls {
static const String imageBaseUrl = "https://suh-project.synology.me"; // 이미지 서버 주소
static const String naverReverseGeoCodeApiUrl =
"https://maps.apigw.ntruss.com/map-reversegeocode/v2/gc"; // 네이버 주소 API
static const String naverStaticMapApiUrl = 'https://maps.apigw.ntruss.com/map-static/v2/raster';

// 스토어 URL
static const String androidStoreUrl = "https://play.google.com/store/apps/details?id=com.alom.romrom&hl=ko";
Expand Down
Loading