Thanks to visit codestin.com
Credit goes to github.com

Skip to content
@Alzheimer-dinger

Alzheimer-dinger

A-dinger (알츠하이머딩거) — 치매 환자 케어 웹앱

Swagger API Docs Badge MIT License Badge

보호자–환자 연결, 통화 기록 분석, 감정 리포트, 리마인더와 알림을 제공하는 치매 환자 케어 서비스


📒 목차

프로젝트 소개 | 팀원 구성 | 기술 스택 | 저장소·브랜치 전략·구조 | 개발 기간·작업 관리 | 신경 쓴 부분 | 페이지별 기능 | 주요 API

맨 위로 ⤴


📖 프로젝트 소개

본 프로젝트는 치매 환자와 보호자를 위한 AI 동반 케어 웹앱입니다. 환자는 앱에서 인공지능과 실시간 대화(음성/자막)로 일상을 공유하고, 보호자는 연결 계정을 통해 심리 상태와 이상 징후를 모니터링합니다. 하루하루 축적되는 대화·활동 데이터를 분석해 일·주·월 단위 종합 리포트 (감정 타임라인, 참여도, 평균 통화시간, 위험 지표)를 제공하여 세심한 돌봄 계획 수립을 돕습니다.

  • 원클릭 통화(대기 → 진행 → 종료), 실시간 자막/응답
  • RAG 메모리로 개인 맥락 유지, 토큰 효율 최적화
  • 보호자–환자 관계 관리(요청/승인/해제) 및 리마인더/알림
  • PWA/FCM 기반 푸시 알림, 웹 대시보드로 리포트 열람
  • 운영/모니터링: Micrometer + Prometheus + Grafana

👥 팀원 구성

정장우 프로필 이미지
정장우

팀 리더 · 백엔드
주요 도메인 · 인프라 구축
김경규 프로필 이미지
김경규

백엔드
도메인 · 인증/인가
시스템/인프라 설계
박영두 프로필 이미지
박영두

백엔드
도메인 · 인프라 구축 · CI/CD · 모니터링
노예원 프로필 이미지
노예원

프론트
UI/UX · 통화 WebSocket · CD · FCM
김효신 프로필 이미지
김효신

프론트
UI/UX · API 연동 · 상태관리
서현교 프로필 이미지
서현교

AI
아이디어 · RAG 메모리 · 분석 리포트
강민재 프로필 이미지
강민재

AI
실시간 통화 · 감정 분석·요약

🧰 기술 스택

Frontend

React TypeScript Vite React Router styled-components Recharts Axios PWA

Backend

Java Spring Boot Spring Security Spring Data JPA Spring Actuator Spring Batch FastAPI

AI / Data

Vertex AI Gemini Live API Hugging Face Inference Pinecone

Database / Messaging / Caching

MySQL MongoDB Redis Apache Kafka

Infra / DevOps

GCP Compute Engine Google Cloud Storage Artifact Registry Docker Nginx GitHub Actions Cloudflare

Monitoring / Docs / Test

Micrometer Prometheus Grafana Swagger API Docs JUnit 5 Postman

Push / Notification

FCM Firebase Admin SDK


🔑 주요 API (요약)

전체 스펙은 Swagger에서 확인: https://api.alzheimerdinger.com/swagger-ui/index.html#/

Method Endpoint 설명 인증
POST/api/users/sign-up회원가입(Guardian/Patient, 선택: 환자코드)
POST/api/users/login로그인(JWT Access/Refresh 발급, FCM 토큰 접수)
DELETE/api/users/logout로그아웃(토큰 무효화)
POST/api/token토큰 재발급(refreshToken 쿼리)
GET/api/users/profile프로필 조회
PATCH/api/users/profile프로필 수정(이름/성별/비밀번호)
GET/api/images/profile/upload-urlGCS Presigned 업로드 URL 발급(extension)
POST/api/images/profile업로드 파일을 프로필 이미지로 적용(fileKey)
POST/api/relations/send관계 요청 전송(patientCode)
POST/api/relations/resend만료 요청 재전송(relationId)
PATCH/api/relations/reply관계 요청 응답(relationId, status)
GET/api/relations관계 목록 조회
DELETE/api/relations관계 해제(relationId)
GET/api/reminder리마인더 조회
POST/api/reminder리마인더 등록(fireTime, status)
GET/api/transcripts통화 기록 목록(요약)
GET/api/transcripts/{sessionId}통화 기록 상세(요약/대화 로그)
GET/api/analysis/report/latest최근 분석 리포트(periodEnd, userId)
GET/api/analysis/period기간별 감정 분석(start, end, userId)
GET/api/analysis/day일별 감정 분석(date, userId)
POST/api/feedback피드백 저장(rating, reason)

참고: 실시간 통화(음성/자막)은 클라이언트 ↔ AI 서버(WebSocket/Streaming) 연결을 통해 처리되며, 백엔드는 세션/기록/리포트 API를 제공합니다.

맨 위로 ⤴

📦 저장소  ·  브랜치 전략 · 프로젝트 구조

GitHub : https://github.com/Alzheimer-dinger

브랜치 전략 (Git-flow 기반)

  • main — 배포용 안정 브랜치. 태깅(vX.Y.Z) 후 배포.
  • develop — 통합 개발 브랜치. 기능/버그 픽스 머지 대상.
  • feature/<scope>-<short-desc> — 기능 단위 작업. 완료 시 PR → develop.
  • hotfix/<issue> — 긴급 수정. PR → maindevelop 양쪽 반영.
  • release/<version> — 릴리즈 준비(버전, 문서, 마이그레이션) 후 main 병합.

PR 규칙

  • PR 템플릿 사용: 배경/변경점/테스트/스크린샷/체크리스트 포함
  • 리뷰 1명 이상 승인(🚦 최소 1 Approve), CI 통과 필수
  • 라벨: feature, fix, refactor

커밋 컨벤션 (Conventional Commits)

feat(auth): add refresh token rotation
fix(api): handle null imageUrl in profile response
refactor(ui): split ReportChart into small components
docs(readme): add tech stack badges
chore(ci): bump node to 20.x in workflow

프로젝트 구조

/
├─ BE/
│  ├─ build.gradle
│  ├─ src/main/java/opensource/alzheimerdinger/core
│  │  ├─ global/
│  │  └─ domain/
│  │     ├─ user/
│  │     ├─ image/
│  │     ├─ relation/
│  │     ├─ reminder/
│  │     ├─ transcript/
│  │     ├─ analysis/
│  │     └─ feedback/
│  └─ src/main/resources/
│
├─ FE/
│  ├─ package.json
│  └─ src/
│
└─infra/
   ├─ docker-compose.yml
   ├─ nginx/
   ├─ prometheus/
   └─ grafana/

🧩 도메인 예시: user

아래는 user 도메인의 대표 구성요소를 간단히 요약한 예시입니다. 전체 코드는 레포지토리에서 확인하세요.

1) DTO · Request
// LoginRequest.java
public record LoginRequest(
    @Email @NotBlank String email,
    @NotBlank String password,
    @NotNull String fcmToken
) {}

// SignUpRequest.java
public record SignUpRequest(
    @NotBlank String name,
    @Email @NotBlank String email,
    @NotBlank String password,
    @NotNull Gender gender,
    String patientCode
) {}

// UpdateProfileRequest.java
public record UpdateProfileRequest(
    @NotBlank String name,
    @NotNull Gender gender,
    String currentPassword,
    String newPassword
    ) {
        @AssertTrue(message = "currentPassword is required when newPassword is provided")
        public boolean isPasswordChangeValid() {
            if (newPassword == null || newPassword.isBlank()) return true;
            return currentPassword != null && !currentPassword.isBlank();
        }
    }
}
2) DTO · Response
// LoginResponse.java
public record LoginResponse(String accessToken, String refreshToken) {}

// ProfileResponse.java
public record ProfileResponse(
    String userId,
    String name,
    String email,
    Gender gender,
    String imageUrl,
    String patientCode
    ) {
        public static ProfileResponse create(User user, String imageUrl) {
        return new ProfileResponse(
        user.getUserId(),
        user.getName(),
        user.getEmail(),
        user.getGender(),
        imageUrl,
        user.getPatientCode()
        );
    }
}
3) Entity
// Gender.java
public enum Gender { MALE, FEMALE }

// Role.java
@Getter
public enum Role {
    GUARDIAN("ROLE_GUARDIAN"),
    PATIENT("ROLE_PATIENT");
    private final String name;
    Role(String name) { this.name = name; }
}

// User.java
@Entity
@Table(name = "users")
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User extends BaseEntity {
    @Id @Tsid
    private String userId;
    private String name;
    @Column(nullable = false)
    private String email;
    @Column(nullable = false)
    private String password;
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;
    private String patientCode;
    @Enumerated(EnumType.STRING)
    private Gender gender;
    
    public void updateRole(Role role) {
        this.role = role;
    }
    public void updateProfile(String name, Gender gender, String encodedNewPassword) {
        this.name = name;
        this.gender = gender;
        if (encodedNewPassword != null && !encodedNewPassword.isBlank()) {
            this.password = encodedNewPassword;
        }
    }
}
4) Repository
// UserRepository.java
public interface UserRepository extends JpaRepository<User, String> {

    @Query("select count(u) > 0 from User u where u.email = :email")
    Boolean existsByEmail(@Param("email") String email);

    @Query("select u from User u where u.email = :email")
    Optional<User> findByEmail(@Param("email") String email);

    @Query("select u from User u where u.patientCode = :patientCode")
    Optional<User> findByPatientCode(@Param("patientCode") String patientCode);
}
5) Service (요약)
// UserService.java (발췌)
@Service
@RequiredArgsConstructor
public class UserService {
    private static final Logger log = LoggerFactory.getLogger(UserService.class);
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final ImageService imageService;

    public boolean isAlreadyRegistered(String email) {
        return userRepository.existsByEmail(email);
    }
    
    public User save(SignUpRequest req, String code) {
        return userRepository.save(
        User.builder()
            .email(req.email())
            .password(passwordEncoder.encode(req.password()))
            .role(req.patientCode() == null ? Role.PATIENT : Role.GUARDIAN)
            .patientCode(code)
            .gender(req.gender())
            .name(req.name())
            .build()
        );
    }
    
    public ProfileResponse findProfile(String userId) {
        return userRepository.findById(userId)
            .map(u -&gt; ProfileResponse.create(u, imageService.getProfileImageUrl(u)))
            .orElseThrow(() -&gt; new RestApiException(_NOT_FOUND));
    }
}
6) UseCase
// UpdateProfileUseCase.java (발췌)
@Service
@Transactional
@RequiredArgsConstructor
public class UpdateProfileUseCase {
    private final UserService userService;
    private final PasswordEncoder passwordEncoder;
    private final ImageService imageService;

    @UseCaseMetric(domain = "user-profile", value = "update-profile", type = "command")
    public ProfileResponse update(String userId, UpdateProfileRequest req) {
        User user = userService.findUser(userId);
        String encodedNewPassword = null;
        
        if (req.newPassword() != null && !req.newPassword().isBlank()) {
            boolean matches = passwordEncoder.matches(req.currentPassword(), user.getPassword());
            if (!matches) {
                log.warn("[UpdateProfile] password mismatch: userId={}", userId);
                throw new RestApiException(_UNAUTHORIZED);
            }
            encodedNewPassword = passwordEncoder.encode(req.newPassword());
        }
    
        user.updateProfile(req.name(), req.gender(), encodedNewPassword);
        return ProfileResponse.create(user, imageService.getProfileImageUrl(user));
    }
}
    
// UserAuthUseCase.login(...) (발췌)
public LoginResponse login(LoginRequest req) {
    User user = userService.findByEmail(req.email());
    if (!passwordEncoder.matches(req.password(), user.getPassword())) {
        throw new RestApiException(LOGIN_ERROR);
    }
    String at = tokenProvider.createAccessToken(user.getUserId(), user.getRole());
    String rt = tokenProvider.createRefreshToken(user.getUserId(), user.getRole());
    Duration exp = tokenProvider.getRemainingDuration(rt)
        .orElseThrow(() -> new RestApiException(EXPIRED_MEMBER_JWT));
    refreshTokenService.saveRefreshToken(user.getUserId(), rt, exp);
    fcmTokenService.upsert(user, req.fcmToken());
    return new LoginResponse(at, rt);
}
7) Controller
// AuthController.java (발췌)
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class AuthController {

    private final UserAuthUseCase userAuthUseCase;

    @PostMapping("/sign-up")
    public BaseResponse&lt;Void&gt; signUp(@Valid @RequestBody SignUpRequest req) {
        userAuthUseCase.signUp(req);
        return BaseResponse.onSuccess();
    }
    
    @PostMapping("/login")
    public BaseResponse&lt;LoginResponse&gt; login(@Valid @RequestBody LoginRequest req) {
        return BaseResponse.onSuccess(userAuthUseCase.login(req));
    }
    
    @DeleteMapping("/logout")
    public BaseResponse&lt;Void&gt; logout(HttpServletRequest request) {
        userAuthUseCase.logout(request);
        return BaseResponse.onSuccess();
    }
}

// UserController.java (발췌)
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
@SecurityRequirement(name = "Bearer Authentication")
public class UserController {

    private final UserProfileUseCase userProfileUseCase;
    private final UpdateProfileUseCase updateProfileUseCase;
    
    @GetMapping("/profile")
    public BaseResponse<ProfileResponse> getProfile(@CurrentUser String userId) {
        return BaseResponse.onSuccess(userProfileUseCase.findProfile(userId));
    }
    
    @PatchMapping("/profile")
    public BaseResponse<ProfileResponse> updateProfile(
        @CurrentUser String userId,
        @Valid @RequestBody UpdateProfileRequest req
    ) {
        return BaseResponse.onSuccess(updateProfileUseCase.update(userId, req));
    }
}

// TokenController.java (발췌)
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/token")
@SecurityRequirement(name = "Bearer Authentication")
public class TokenController {

    private final TokenReissueService tokenReissueService;
    
    @PostMapping
    public BaseResponse<TokenReissueResponse> reissue(
        @RefreshToken String refreshToken,
        @CurrentUser String userId
    ) {
        return BaseResponse.onSuccess(tokenReissueService.reissue(refreshToken, userId));
    }
}

↑ 프로젝트 구조로 돌아가기


🗓️ 개발 기간  ·  작업 관리

기간 스프린트 목표 주요 산출물
2025-06-20 ~ 2025-07-03 (1~2주차) 요구사항 정의 · API 명세 · DB 설계 요구사항 정의서, ERD, Swagger 초안
2025-07-04 ~ 2025-07-31 (3~6주차) 핵심 기능·UI/UX 개발, RAG 구현, 프롬프트 엔지니어링 FE 페이지/컴포넌트, BE 도메인/인증, RAG 서비스
2025-08-01 ~ 2025-08-14 (7~8주차) 기능 통합·안정화 테스트 E2E/통합 테스트, 버그픽스, 성능/보안 점검
2025-08-15 ~ 2025-08-21 (9주차) 배포·모니터링·운영 릴리즈 노트, 대시보드, 알림 룰

작업 관리 방식

  • 이슈 추적: GitHub Issues (템플릿: bug/feature/chore)
  • 칸반: GitHub Projects — Backlog → In Progress → In Review → Done
  • WIP 제한: 인당 2개(리뷰 포함), 급한 이슈는 라벨 priority:high
  • 릴리즈: 주 1회 태깅(세맨틱 버저닝), 체인지로그 자동화
  • 품질 게이트: CI 빌드/테스트/리포트, 린트·포맷·타입체크

🧠 핵심 기능 구현 내용

1) 실시간 AI 기반 통화 제공

환자와 AI가 음성으로 대화하고, 실시간 자막을 제공하는 통화 기능을 구현했습니다. 통화 전/중/후 상태를 명확히 분리하고, 오디오 스트림 처리와 스트리밍 응답을 안정적으로 연결합니다.

① UI 흐름

CallWaitingCallActiveCallEnd (종료 후 요약/저장)

  • CallWaiting: 장치/권한 체크(마이크), 서버 연결 준비, 상태 표시
  • CallActive: 실시간 자막(부분/최종), 발화/응답 타임라인, 음소거/종료 버튼
  • CallEnd: 통화 요약 노출, 저장/이탈 동작 분기

② 오디오 처리

  • useAudioStream 훅으로 발화 감지(VAD) 및 마이크 스트림 수집
  • WebAudio / MediaDevices API 사용, 입력 레벨 모니터링 및 일시정지/재개
  • 샘플레이트/채널 정규화 → 네트워크 전송 포맷으로 인코딩(스트리밍)

③ 실시간 연결

  • WebSocket 기반 양방향 스트리밍: 오디오 업스트림, 자막/오디오 다운스트림
  • 부분/최종 자막 구분 렌더링(부분 갱신 → 최종 확정)
  • 연결 신뢰성: 핑/퐁 헬스체크, 지수적 재시도, 일시 네트워크 단절 복구
  • 에러/예외 처리: 인증 오류, 장치 접근 실패, 모델 과부하 시 사용자 가이드
  • 리소스 정리: 트랙 stop, 소켓 close, 메모리 해제(종료/이탈 시)

2) 사용자 맞춤형 통합 보고서

일간/기간 종합 관점에서 감정 및 이용 지표를 시각화합니다. 날짜/기간 선택에 따라 API 파라미터를 구성하고, 전처리된 데이터로 그래프/지표 컴포넌트를 렌더링합니다.

① 일간(DailySection)

  • 날짜 선택 + 월간 이모지 캘린더로 하루 흐름 빠른 탐색
  • 감정 계산 로직: 대화 로그 기반 점수 산출(행복/슬픔/분노/놀람/권태 등)
  • 원형 스코어 게이지로 당일 상태를 직관적으로 표현

② 종합(TotalSection)

  • 기간 선택: 1주 / 1달 / 사용자 지정 범위
  • 감정 타임라인: 날짜별 점수 추세(Recharts 라인/에어리어 차트)
  • 참여도/평균 통화시간/위험도 계산 및 카드 지표로 요약
  • EndDate 기준 종합 보고서: 선택 범위의 말일을 기준으로 요약 문구/지표 확정

③ 데이터 흐름(요약)

  • 통화 중: 마이크 권한 → 오디오 스트림(WebSocket) 전송 → AI 응답(오디오/자막) 수신
  • 통화 후: 세션 요약/대화 로그 서버 기록 → 분석 API가 집계/리포트 생성
  • 리포트 조회: 사용자/연결 대상 식별 → 쿼리 파라미터 구성 → 일간/종합 API 호출 → 시각화

🧭 페이지별 기능

Splash · 온보딩
  • 앱 로드시 스플래시 → 로그인 상태에 따라 라우팅
  • 간단 소개/권한 안내(마이크, 푸시)
로그인/회원가입
  • 이메일·비밀번호 유효성 검사, 오류 메시지 인라인 표시
  • 회원가입 후 프로필 초기 설정(이름/성별/환자코드 옵션)
  • JWT 발급(Access/Refresh), FCM 토큰 등록
프로필
  • 내 프로필: 이미지/이름/성별/비밀번호 수정, 판매 영역은 미사용
  • 관계(보호자-환자) 상태 표시
관계 관리
  • 환자코드로 요청, 만료 시 재전송, 승인/거절
  • 관계 목록/해제, 상태(REQUESTED/APPROVED 등) 표시
통화(실시간 AI)
  • 흐름: CallWaiting → CallActive → End
  • 마이크 권한, 발화 감지(useAudioStream), WebSocket/Streaming
  • 실시간 자막/응답, 종료 후 기록 저장
통화 기록(Transcripts)
  • 목록: 세션ID/제목/일시/지속시간 요약
  • 상세: 요약/대화 로그, 페이징/검색
분석 리포트
  • 일간: 날짜 선택, 월간 이모지 캘린더, 감정 점수, 원형 스코어
  • 종합: 기간(1주/1달/사용자 지정) 선택, 감정 타임라인, 참여도/평균 통화시간/위험도
리마인더
  • 알림 시각·상태 등록/조회(ACTIVE/INACTIVE)
  • PWA/FCM 기반 푸시
설정/로그아웃
  • 세션 종료(토큰 무효화), 보안/알림 설정
피드백
  • 평점(예: VERY_LOW~)과 사유 저장, 운영 개선에 활용

맨 위로 ⤴

Popular repositories Loading

  1. BE BE Public

    Java 1

  2. AI-Call AI-Call Public

    Python

  3. FE FE Public

    TypeScript 1

  4. AI-Analysis AI-Analysis Public

    Python

  5. .github .github Public

Repositories

Showing 5 of 5 repositories

Top languages

Loading…

Most used topics

Loading…