pathname : “/chat-room”

InboxCard, LinboxCard, MessageCard에서 router.push로 접근이 가능하다

type === “TICKET”인 경우 신고가되서나온 고객센터 채팅방이다

{
"court_id": 31, 
"created_at": "2025-11-26T13:26:41.296Z", 
"messages": {"1110": [Object]}, 
"participants": {"center_id": 67, "user_id": 60}, 
"slot_id": 4155, 
"status": "ACTIVE", 
"type": "NORMAL"
}

type === “NORMAL”인 데이터이며 일반 채팅방으로 보인다

 {
 "court_id": 31, 
 "created_at": "2025-11-26T13:26:41.296Z", 
 "messages": [Object], 
 "participants": [Object], 
 "slot_id": 4155, 
 "status": "ACTIVE", 
 "type": "NORMAL"
 }
# ChatRoom 컴포넌트 코드 분석

## 📋 개요
`chat-room.tsx`는 사용자와 코트 관리자 간의 실시간 채팅 기능을 제공하는 화면입니다. Firebase Realtime Database를 사용하여 실시간 메시지 동기화를 구현하고 있습니다.

---

## 🎯 주요 기능

### 1. **실시간 채팅**
- Firebase Realtime Database의 `onValue` 리스너를 통해 실시간 메시지 수신
- 메시지 자동 스크롤 및 읽음 처리

### 2. **예약 정보 표시**
- `BOOKING` 상태: 일반 예약 정보 표시 (`ScheduleCard`)
- `LOCK` 상태: 잠금된 스케줄 정보 표시 (`LScheduleCard`)

### 3. **신고 기능**
- 채팅방 신고 기능 (`handleReportChatRoom`)
- 만료 시간 기준으로 신고 가능 여부 판단

### 4. **티켓 시스템**
- `TICKET` 타입일 경우 이슈 신고 화면으로 전환
- 티켓 열기/닫기 상태 관리

### 5. **키보드 처리**
- iOS/Android 플랫폼별 키보드 애니메이션 처리
- 키보드 높이에 따른 레이아웃 자동 조정

---

## 📦 의존성 및 Import

### 외부 라이브러리
- `react-native`: 기본 컴포넌트 및 API
- `swr`: 서버 데이터 페칭 (예약 정보)
- `firebase/database`: 실시간 데이터베이스
- `expo-router`: 네비게이션
- `lodash`: 유틸리티 함수

### 내부 컴포넌트
- `InboxItem`: 개별 메시지 아이템
- `InboxInput`: 메시지 입력 컴포넌트
- `ScheduleCard` / `LScheduleCard`: 예약 정보 카드
- `Header`: 상단 헤더

---

## 🔑 핵심 State 관리

```typescript
// UI 상태
const [canReport, setCanReport] = useState(false);           // 신고 가능 여부
const [sendLoading, setSendLoading] = useState(false);       // 메시지 전송 로딩
const [reportLoading, setReportLoading] = useState(false);   // 신고 로딩
const [openingTicket, setOpeningTicket] = useState<boolean>; // 티켓 열림 상태

// 레이아웃 관련
const [scheduleCardHeight, setScheduleCardHeight] = useState(0);  // 스케줄 카드 높이
const [chatInputHeight, setChatInputHeight] = useState<number>(0); // 입력창 높이
const [keyboardHeight, setKeyboardHeight] = useState(0);         // 키보드 높이

// 메시지 데이터
const [fetchedMessages, setfetchedMessages] = useState<Message[]>([]); // Firebase에서 받은 메시지
const [shouldScrollToEnd, setShouldScrollToEnd] = useState(false);      // 자동 스크롤 플래그

🔄 데이터 흐름

1. 예약 정보 로딩 (SWR)

// BOOKING 상태일 때
const { data: schedule, mutate: fetchBookings } = useSWR(
  isFocused && state === "BOOKING" ? `/bookings/by-id/${slot_id}` : null,
  fetcher
);

// LOCK 상태일 때
const { data: lockedSchedule, mutate: fetchLocked } = useSWR(
  isFocused && state === "LOCK" ? `/schedules/locked-schedules-by-id/${lock_id}` : null,
  fetcher
);

2. Firebase 실시간 메시지 리스너

useEffect(() => {
  const setupDatabaseListener = async () => {
    await ensureFirebaseAuth();
    const chatRoomRef = ref(database, `chatrooms/${chatRoom_id}/messages`);
    
    return onValue(chatRoomRef, async (snapshot) => {
      if (snapshot.exists()) {
        const data = snapshot.val();
        // 각 메시지에 isOwn 속성 추가
        const messages = await Promise.all(
          Object.values(data).map(async (message) => ({
            ...message,
            isOwn: await isOwnMessage(message.human_sender_id),
          }))
        );
        setfetchedMessages(messages);
      }
    });
  };
  setupDatabaseListener();
}, []);

주요 포인트:


🎨 UI 구조

레이아웃 계층 구조

View (container)
├── Stack.Screen (Header)
│   ├── 일반 채팅: 신고 버튼 포함
│   └── TICKET: 이슈 신고 타이틀만
│
└── View (container)
    ├── Animated.View (messageList)
    │   ├── ScheduleCard / LScheduleCard (예약 정보)
    │   └── FlatList (메시지 리스트)
    │       ├── ListEmptyComponent (빈 상태)
    │       ├── renderItem (InboxItem)
    │       └── ListFooterComponent
    │           ├── SendingMessageIndicator
    │           ├── ReportedCard
    │           ├── ReportTicketButton
    │           └── 만료/취소 메시지
    │
    └── Animated.View (chatContainer - 고정 위치)
        └── InboxInput (메시지 입력창)

⌨️ 키보드 처리 로직

iOS vs Android 차이

// iOS: keyboardWillShow/Hide (키보드 애니메이션과 동기화)
// Android: keyboardDidShow/Hide (키보드 표시 후 처리)

const keyboardWillShowListener = Keyboard.addListener(
  Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow",
  (e) => {
    const newKeyboardHeight = 
      e.endCoordinates.height - (Platform.OS === "ios" ? insets.bottom : 0);
    
    // Animated API로 부드러운 애니메이션
    Animated.timing(keyboardAnimValue, {
      toValue: newKeyboardHeight,
      duration: Platform.OS === "ios" ? e.duration || 250 : 250,
    }).start();
  }
);

적용 위치:


📝 주요 함수 분석

1. handleReportChatRoom - 채팅방 신고

const handleReportChatRoom = () => {
  if (!canReport) {  // 만료 시간이 지나지 않았을 때만 신고 가능
    // API 호출
    // 성공 시: type을 "TICKET"으로 변경하여 화면 전환
    // 실패 시: 이미 신고되었다고 처리 (에러 핸들링 특이사항)
  }
};

특이사항:

2. isOwnMessage - 메시지 소유자 판단

const isOwnMessage = async (sender_id: number) => {
  const manager_id = await AsyncStorage.getItem(StorageKeys.UserId);
  return String(sender_id) === String(manager_id);
};

용도:

3. getDescription - 취소 메시지 포맷팅

const getDescription = () => {
  switch (schedule?.data?.Status?.code_name) {
    case "cancelled":
      return `${i18n.t("booking.description.canceled_by_me")} ${formatedCancelDate}`;
    case "cancelled_by_manager":
      return `${i18n.t("booking.description.canceled_by_court")} ${formatedCancelDate}`;
  }
};

🔄 useEffect 훅 분석

1. 키보드 리스너 설정 (108-141줄)

2. 예약 정보 갱신 (143-151줄)

3. 신고 가능 여부 업데이트 (157-159줄)

4. 메시지 읽음 처리 (231-241줄)

5. Firebase 리스너 설정 (243-274줄)


⚠️ 개선이 필요한 부분

1. Firebase 리스너 Cleanup 누락

// 현재 코드 (문제)
useEffect(() => {
  const setupDatabaseListener = async () => {
    const chatRoomRef = ref(database, `chatrooms/${chatRoom_id}/messages`);
    return onValue(chatRoomRef, ...);  // 반환하지만 사용하지 않음
  };
  setupDatabaseListener();
}, []);  // cleanup 함수 없음

개선안:

useEffect(() => {
  let unsubscribe: (() => void) | undefined;
  
  const setupDatabaseListener = async () => {
    await ensureFirebaseAuth();
    const chatRoomRef = ref(database, `chatrooms/${chatRoom_id}/messages`);
    unsubscribe = onValue(chatRoomRef, ...);
  };
  
  setupDatabaseListener();
  
  return () => {
    if (unsubscribe) unsubscribe();
  };
}, [chatRoom_id]);

2. 타입 안정성

3. 에러 핸들링


🎯 비즈니스 로직

채팅방 타입

신고 가능 조건

// 만료 시간이 지나지 않았을 때만 신고 가능
setCanReport(currentTime.isAfter(expired_at));
// canReport가 false일 때 신고 가능 (역논리)

메시지 입력 비활성화 조건

disabled={
  type === "TICKET" 
    ? !openingTicket  // 티켓이 닫혀있으면 비활성화
    : canReport       // 만료 시간이 지나면 비활성화
}

📊 성능 최적화

1. 메시지 정렬

data={_.sortBy(fetchedMessages, ["sent_at"])}

2. 자동 스크롤

onContentSizeChange={() => {
  if (shouldScrollToEnd) {
    setTimeout(() => {
      scrollViewRef.current?.scrollToEnd({ animated: true });
      setShouldScrollToEnd(false);
    }, 100);
  }
}}

3. 레이아웃 측정

onLayout={(event) => {
  setScheduleCardHeight(event.nativeEvent.layout.height);
}}

🔗 연관 컴포넌트

InboxInput

InboxItem

ScheduleCard / LScheduleCard


📌 주요 라우팅 파라미터

{
  type: "TICKET" | 일반 채팅
  state: "BOOKING" | "LOCK"
  status: "OPEN" | "CLOSED"
  slot_id: number (BOOKING일 때)
  lock_id: number (LOCK일 때)
  datetime: string
  center_id: number
  created_at: string
  expired_at: string
  chatRoom_id: string
}

🎨 스타일링 특징

고정 위치 입력창

chatContainer: {
  position: "absolute",
  bottom: keyboardAnimValue,  // 키보드 높이에 따라 동적 조정
  borderTopWidth: 1,
  shadow: {...}  // 상단 그림자 효과
}

동적 패딩

paddingBottom: Animated.add(keyboardAnimValue, chatInputHeight)

💡 개선 제안

  1. React Query 통합: Firebase 데이터를 React Query로 관리하여 캐싱 및 동기화 개선
  2. 에러 바운더리: Firebase 연결 실패 시 사용자 피드백
  3. 메시지 페이지네이션: 메시지가 많을 때 성능 최적화
  4. 타입 안정성 강화: useLocalSearchParams 타입 명시
  5. 메모리 누수 방지: Firebase 리스너 cleanup 함수 추가

📚 참고 자료