cd /projects
$ cat ddok/README.md
project : 똑DDOK - 지도 기반 프로젝트·스터디 매칭 플랫폼
period : 2025.08 - 2025.09
role : Team Lead, Fullstack Developer, Infra
stack :
ReactTypeScriptViteSpring BootJava 17PostgreSQLRedisWebSocketKakao Maps APIDockerAWSElasticsearchSentryCoolSMS
// key metrics
  • 7명 팀 프로젝트 팀장 (풀스택 + 인프라 담당)
  • 2개월 기획 및 개발 완료
  • 실시간 채팅 및 알림 시스템 구현
$ open demo.mp4
$ open demo.mp4 // 똑DDOK - 지도 기반 프로젝트·스터디 매칭 플랫폼 데모 영상

문제 정의

개발자들이 스터디나 사이드 프로젝트를 찾을 때 겪는 어려움:

  • 오픈톡/커뮤니티에서 지역/역할 매칭이 어려움
  • 참여 의사 표현 후 연락 두절, 책임감 부재
  • 팀 구성 후 협업 도구(채팅, 일정 관리)가 분산됨

목표: 지도 기반으로 주변 스터디/프로젝트를 찾고, 원클릭 참여부터 팀 협업까지 한 곳에서 해결하는 플랫폼

역할과 범위

역할: 팀장 / 풀스택 개발자 / 인프라 담당
범위:

  • 프론트엔드: React UI/UX, 카카오맵 연동, 실시간 채팅 클라이언트
  • 백엔드: Spring Boot API, 인증, 채팅, 신뢰도 시스템
  • 인프라: AWS 배포 (EC2, S3, CloudFront, RDS), Docker, CI/CD

아키텍처

┌─────────────────────────────────────────────────────────────┐
│                    Frontend (React + Vite)                    │
│    React + TypeScript + TanStack Query + Zustand + Kakao Map  │
└─────────────────────────────────────────────────────────────┘

              ┌───────────────┴───────────────┐
              │                               │
              ▼                               ▼
┌─────────────────────────┐       ┌─────────────────────────┐
│   Backend (Spring Boot)  │       │   AWS Infrastructure    │
│                         │       │                         │
│ - REST API              │       │ - EC2 (Backend)         │
│ - WebSocket/STOMP Chat  │       │ - S3 + CloudFront (FE)  │
│ - Spring Security + JWT │       │ - RDS (PostgreSQL)      │
│ - Spring Batch          │       │ - ElastiCache (Redis)   │
└───────────┬─────────────┘       └─────────────────────────┘

    ┌───────┴───────┐
    ▼               ▼
┌────────┐   ┌────────────┐
│ Redis  │   │ PostgreSQL │
│ (Cache)│   │  (Main DB) │
└────────┘   └────────────┘

레포지토리 구성

서비스기술 스택역할
ddok-feReact, TypeScript, Vite프론트엔드 SPA
ddok-beSpring Boot, Java 17API 서버, 채팅, 인증

핵심 구현

1. 카카오맵 기반 지역 탐색

카카오맵 SDK와 클러스터링을 활용한 스터디/프로젝트 위치 시각화:

// 카카오맵 마커 클러스터링
const MapContainer = () => {
  const { data: projects } = useQuery({
    queryKey: ['projects', bounds],
    queryFn: () => fetchProjectsInBounds(bounds)
  });

  return (
    <Map center={center} level={5} onBoundsChanged={handleBoundsChange}>
      <MarkerClusterer
        averageCenter={true}
        minLevel={4}
        calculator={[10, 30, 50]}
      >
        {projects?.map((project) => (
          <MapMarker
            key={project.id}
            position={{ lat: project.lat, lng: project.lng }}
            onClick={() => openProjectDetail(project)}
          >
            <ProjectInfoWindow project={project} />
          </MapMarker>
        ))}
      </MarkerClusterer>
    </Map>
  );
};

2. WebSocket STOMP 실시간 채팅

팀 생성 시 자동으로 채팅방 생성, 실시간 메시지 전송:

@Controller
public class ChatController {
    
    @MessageMapping("/chat/{teamId}")
    @SendTo("/topic/team/{teamId}")
    public ChatMessageDto sendMessage(
        @DestinationVariable Long teamId,
        @Payload ChatMessageDto message,
        @AuthenticationPrincipal UserDetails user
    ) {
        message.setSender(user.getUsername());
        message.setTimestamp(LocalDateTime.now());
        
        // 메시지 저장 및 알림 발송
        chatService.saveAndNotify(teamId, message);
        
        return message;
    }
}
// 프론트엔드 STOMP 클라이언트
const useChatConnection = (teamId: number) => {
  const stompClient = useRef<Client | null>(null);
  const { addMessage } = useChatStore();

  useEffect(() => {
    const client = new Client({
      brokerURL: `${WS_URL}/ws`,
      onConnect: () => {
        client.subscribe(`/topic/team/${teamId}`, (message) => {
          const chatMessage = JSON.parse(message.body);
          addMessage(chatMessage);
        });
      },
    });
    
    client.activate();
    stompClient.current = client;
    
    return () => client.deactivate();
  }, [teamId]);

  const sendMessage = (content: string) => {
    stompClient.current?.publish({
      destination: `/app/chat/${teamId}`,
      body: JSON.stringify({ content }),
    });
  };

  return { sendMessage };
};

3. 신뢰도(온도) 시스템

프로젝트 완주율, 팀원 평가를 기반으로 한 사용자 신뢰도 계산:

@Service
public class ReputationService {
    
    private static final double BASE_TEMPERATURE = 36.5;
    
    public double calculateTemperature(Member member) {
        // 프로젝트 완주율
        double completionRate = calculateCompletionRate(member);
        
        // 팀원 평가 점수
        double evaluationScore = getAverageEvaluation(member);
        
        // 활동 기간 보정
        double activityBonus = calculateActivityBonus(member);
        
        return BASE_TEMPERATURE 
            + (completionRate * 5)      // 완주율 최대 +5도
            + (evaluationScore * 3)     // 평가 최대 +3도
            + activityBonus;            // 활동 보너스 최대 +1도
    }
}

// Spring Batch로 일간 랭킹 갱신
@Configuration
public class RankingBatchConfig {
    
    @Bean
    public Job rankingJob() {
        return jobBuilderFactory.get("rankingJob")
            .start(calculateRankingStep())
            .next(updateRedisCacheStep())
            .build();
    }
}

4. Kakao OAuth2 소셜 로그인

@Service
public class KakaoOAuthService {
    
    public LoginResponse loginWithKakao(String authCode) {
        // 1. 인가 코드로 액세스 토큰 발급
        KakaoTokenResponse tokenResponse = requestToken(authCode);
        
        // 2. 사용자 정보 조회
        KakaoUserInfo userInfo = getUserInfo(tokenResponse.getAccessToken());
        
        // 3. 회원 조회 또는 생성
        Member member = memberRepository.findByKakaoId(userInfo.getId())
            .orElseGet(() -> createMember(userInfo));
        
        // 4. JWT 토큰 발급
        String jwt = jwtProvider.createToken(member);
        String refreshToken = jwtProvider.createRefreshToken(member);
        
        // 5. Redis에 리프레시 토큰 저장
        redisTemplate.opsForValue().set(
            "refresh:" + member.getId(),
            refreshToken,
            Duration.ofDays(14)
        );
        
        return new LoginResponse(jwt, refreshToken, member);
    }
}

트러블슈팅

이슈 1: 카카오맵 마커 렌더링 성능 저하

현상: 수백 개의 스터디/프로젝트 마커 동시 렌더링 시 지도 조작이 버벅거림

원인: 모든 마커를 개별 DOM 요소로 렌더링하여 리플로우 발생

해결:

  • 마커 클러스터링으로 화면 내 마커 수 제한
  • 지도 bounds 변경 시 debounce 적용
  • 뷰포트 외 마커는 렌더링 제외
const debouncedFetch = useMemo(
  () => debounce((bounds: LatLngBounds) => {
    queryClient.prefetchQuery({
      queryKey: ['projects', bounds],
      queryFn: () => fetchProjectsInBounds(bounds)
    });
  }, 300),
  []
);

결과: 지도 조작 시 프레임 드롭 해소, 부드러운 UX 제공


이슈 2: WebSocket 연결 유지 문제

현상: 모바일 환경에서 채팅 연결이 자주 끊기고 메시지 유실

원인:

  • 모바일 백그라운드 전환 시 WebSocket 연결 해제
  • 네트워크 불안정 시 재연결 로직 부재

해결:

const client = new Client({
  brokerURL: `${WS_URL}/ws`,
  reconnectDelay: 5000,        // 재연결 딜레이
  heartbeatIncoming: 10000,    // 하트비트 설정
  heartbeatOutgoing: 10000,
  onDisconnect: () => {
    // 연결 끊김 시 로컬 상태 유지
    setConnectionStatus('disconnected');
  },
  onStompError: (frame) => {
    console.error('STOMP error:', frame);
    // 에러 발생 시 자동 재연결
  }
});

// 페이지 visibility 변경 시 재연결
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible' && !client.connected) {
    client.activate();
  }
});

재발 방지:

  • 미전송 메시지 로컬 큐에 저장 후 재연결 시 재전송
  • 연결 상태 UI 표시로 사용자 피드백 제공

이슈 3: N+1 쿼리 문제

현상: 프로젝트 목록 조회 시 응답 시간 2초 이상

원인: 프로젝트별 팀원, 기술스택, 위치 정보를 개별 쿼리로 조회

해결: Fetch Join과 EntityGraph 활용

@EntityGraph(attributePaths = {"members", "techStacks", "location"})
@Query("SELECT p FROM Project p WHERE ST_DWithin(p.location, :point, :radius)")
List<Project> findProjectsWithinRadius(
    @Param("point") Point point, 
    @Param("radius") double radius
);

결과: 조회 성능 2초 → 200ms로 개선 (10배 향상)


성과

지표결과
개발 기간2개월 (기획 ~ 배포)
팀 구성7명 (FE 4, BE 2, 풀스택 1)
담당 역할팀장, 풀스택, 인프라
주요 기여채팅 시스템, 인증, 인프라 구축
서비스 배포AWS 기반 프로덕션 배포 완료

주요 기능

  • 지도 기반 탐색: 카카오맵에서 주변 스터디/프로젝트/플레이어를 한눈에 확인
  • 포지션 매칭: 역할/경험/시간대 기반 맞춤 필터와 추천
  • 원클릭 참여: 오픈톡/댓글 없이 클릭 한 번으로 신청/취소
  • 팀 협업: 팀 생성 시 자동 채팅방, 일정 조율(캘린더), 팀 ReadMe
  • 신뢰도 시스템: 온도(완주율/기여도), 배지/랭킹으로 책임감과 지속 참여 유도

프론트엔드 기여

  • 카카오맵 통합: 클러스터링, 커스텀 오버레이, 위치 기반 필터링
  • 실시간 채팅 UI: STOMP 클라이언트, 메시지 상태 관리, 읽음 표시
  • 폼 검증: React Hook Form + Zod 스키마 기반 유효성 검사
  • 상태 관리: Zustand (전역) + TanStack Query (서버 상태)

회고 및 다음 개선

잘한 점

  • 팀장으로서 7명 팀원 역할 분배 및 일정 관리 성공
  • 실시간 채팅 및 알림 시스템 완성
  • 카카오맵 기반 차별화된 UX 제공

개선할 점

  • 테스트 코드 커버리지 부족
  • 모니터링/로깅 체계 강화 필요
  • 성능 최적화 여지 존재 (이미지 최적화 등)

다음 개선 계획

  • Kubernetes 기반 컨테이너 오케스트레이션
  • 프론트엔드 E2E 테스트 (Playwright) 도입
  • 실시간 알림 고도화 (FCM 푸시 알림)