diff --git a/README.md b/README.md new file mode 100644 index 00000000..ce13fabf --- /dev/null +++ b/README.md @@ -0,0 +1,255 @@ +# 프로젝트에 대한 간단한 설명 +## 📌Project Overview + +- HaRu는 소규모 팀을 위한 All-In-One 운영 관리 플랫폼입니다., +- 보다 효율적인 팀 운영을 위한 고민에서 시작된 HaRu는, 각자의 자리에서 치열하게 움직이는 소규모 팀들의 하루를 돕고자 합니다., +- 회의 진행 보조, SNS 이벤트 진행 관리, 팀 분위기 체크까지.HaRu는 모든 팀이 더 가치 있는 순간에 집중할 수 있도록 돕습니다., + +## 🚀HaRu Key Features, + +- **AI 회의 진행 매니저**, + - 실시간 STT 변환, + - HaRu AI 회의 진행 질문 추천, + - 회의록 자동 생성, +- **SNS 이벤트 어시스턴트**, + - Instagram 계정 연동, + - 이벤트 URL 등록, + - 이벤트 참여자 및 당첨자 리스트 추출, +- **팀 분위기 트래커**, + - 설문지 작성 및 배포, + - 팀 분위기 리포트 자동 생성, + - 운영자 맞춤 HaRu 인사이트 제공, + +## ⚒️Technical Overview, + +- **FrontEnd**: Next.js · React · TypeScript · Tailwind CSS · Storybook · Vercel, +- **BackEnd**: Spring · FastAPI · Docker · MySQL · AWS(EC2, S3, RDS) · Redis +--- + +--- + +--- + +# 사용한 브랜치 전략 및 기술 스택, 프로젝트 구조 등 + +## 브랜치 전략 + +Untitled + +- 위 git flow를 따름 +- release브랜치는 제외 +- dev 브랜치 왼편에 feature브랜치말고 refactor, fix브랜치도 존재 + Untitled (1) + + +- 브랜치 + - `main` - 배포용 브랜치 + - `dev` - 개발용 브랜치 + - `feat/*` - 개발 피쳐별 브랜치 (새 기능) + - `fix/*` - 버그 수정 피쳐별 브랜치 (버그 수정) + - `refactor/*` - 리팩토링 브랜치 +- 이슈마다 브랜치 생성하고 커밋 ex) feat/#10-login +- 이슈 해결되면 이슈 close하고 해당 브랜치 삭제 +- **main, dev로 나누고, 개발 된 것은 dev에 merge. main에 merge되면 CI/CD** + +### 브랜치명 컨벤션 + +- {태그}/#{이슈번호}-{작업내용} + - 작업내용 : `kebab-case` , 띄워쓰기는 "-"로 구분 + - kebab-case: 모두 소문자로 표현하며, 단어와 단어 사이에는 하이픈(-)를 사용합니다. + + ex) feat/#0-project-init + + +--- + +## 서버 아키텍처 +HaRu drawio_1 + + +## 기술 스택 +제목 없는 다이어그램 drawio (1) (1) + + +| Programming Languages | Java (17) / Python | +| --- | --- | +| Frameworks | SpringBoot / FastAPI | +| Version Control | Git | +| Cloud Services | AWS Route53 / EC2 / RDS(MySQL) / S3 / Docker | +| Database & Caches | MySQL / Redis | +| Deployment Tools | Nginx (프록시 서버) | +| Extra Library | Swagger / WebSockets / elevenlabs | + +--- + +## 프로젝트 구조 + +### DDD + +### 계층 구조 +image (1) + + + + + + + + +--- + + + +--- + +### 패키지 구조 예시 + +```markdown +└── src + ├── main + │ ├── java + │ │ └── com + │ │ └── haru + │ │ └── server + │ │ ├── ServerApplication.java + │ │ ├── domain + │ │ │ ├── user + │ │ │ │ ├── controller + │ │ │ │ ├── service + │ │ │ │ ├── repository + │ │ │ │ ├── converter + │ │ │ │ ├── dto + │ │ │ │ ├── exception + │ │ │ │ │ ├── handler + │ │ │ │ │ ├── validator + │ │ │ │ │ └── annotation + │ │ │ │ └── entity + │ │ │ ├── ... + │ │ │ ├── moodTracker + │ │ │ │ ├── controller + │ │ │ │ ├── service + │ │ │ │ ├── repository + │ │ │ │ ├── converter + │ │ │ │ ├── dto + │ │ │ │ **├──** exception + │ │ │ │ │ ├── handler + │ │ │ │ │ ├── validator + │ │ │ │ │ └── annotation + │ │ │ │ └── entity + │ │ ├── global + │ │ │ ├── common + │ │ │ │ └── entity + │ │ │ │ └── BaseEntity.java + │ │ │ ├── config + │ │ │ │ ├── SwaggerConfig.java + │ │ │ │ ├── properties + │ │ │ │ └── security + │ │ │ │ └── SecurityConfig.java + │ │ │ ├── apiPayload + │ │ │ │ ├── code + │ │ │ │ │ ├── Status + │ │ │ │ │ │ ├── ErrorStatus + │ │ │ │ │ │ └── SuccessStatus + │ │ │ │ │ ├── BaseCode.java + │ │ │ │ │ ├── BaseErrorCode.java + │ │ │ │ │ ├── ErrorReasonDTO.java + │ │ │ │ │ └── ReasonDTO.java + │ │ │ │ ├── exception + │ │ │ │ │ ├── ExceptionAdvice.java + │ │ │ │ │ └── GeneralException.java + │ │ │ │ └── ApiResponse.java + │ │ │ └── util + │ │ └── infra + │ └── resources + │ └── application.yml + │ └── application-secret.yml +``` + + + +--- + +--- + +--- + +# 팀원 정보 + +| 이름 | 닉네임 | 파트 | 소속 | 전화번호 | GitHub | +| --- | --- | --- | --- | --- | --- | +| 황지원 | 벨라 | PM | 중앙대학교 경영학부 | 010-4139-4130 | hwangjeewon | +| 이수호 | 쏘 | Designer | 한양대학교 ERICA ICT학부 디자인테크놀로지 | 010-9455-9509 | Leesuho9509 | +| 김여진 | 조이 | Frontend Developer | 숭실대학교 IT대학 글로벌미디어학부 | 010-5001-9456 | duwlsssss | +| 박수현 | 노코 | Frontend Developer | 명지대학교 컴퓨터공학과 | 010-6631-1760 | strfunctionk | +| 손기훈 | 제트 | Frontend Developer | 숭실대학교 IT대학 글로벌미디어학부 | 010-3947-5847 | S-Gihun | +| 박경운 | 하늘 | Frontend Developer | 중앙대학교 소프트웨어대학 소프트웨어학부 | 010-9344-8561 | kyeoungwoon | +| 임동재 | 포츠 | Backend Developer | 중앙대학교 소프트웨어대학 소프트웨어학부 | 010-8986-2425 | djlim2425 | +| 김진호 | 루피 | Backend Developer | 중앙대학교 소프트웨어대학 소프트웨어학부 | 010-8828-5091 | Jinho622 | +| 이호근 | 우디 | Backend Developer | 숭실대학교 컴퓨터학부 | 010-9842-4789 | 2ghrms | +| 이석주 | 닉 | Backend Developer | 중앙대학교 소프트웨어대학 소프트웨어학부 | 010-4067-2687 | hknhj | diff --git a/src/main/java/com/haru/api/domain/meeting/controller/MeetingController.java b/src/main/java/com/haru/api/domain/meeting/controller/MeetingController.java index dc14e518..4c4b63c4 100644 --- a/src/main/java/com/haru/api/domain/meeting/controller/MeetingController.java +++ b/src/main/java/com/haru/api/domain/meeting/controller/MeetingController.java @@ -178,4 +178,19 @@ public ApiResponse getMeetingTranscript( } + + @Operation(summary = "회의록 음성파일 조회API", description = + "# [v1.0 (2025-08-14)](https://www.notion.so/AI-24f5da7802c580bc882fe01607e01bbc)" + + "회의록을 다운로드하는 API입니다. URL을 반환합니다." + ) + @GetMapping("{meetingId}/ai-proceeding/voice") + public ApiResponse MeetingvoiceFile( + @PathVariable("meetingId") String meetingId + ){ + Long userId = SecurityUtil.getCurrentUserId(); + + MeetingResponseDTO.proceedingVoiceLinkResponse response = meetingQueryService.MeetingVoiceFile(userId, Long.parseLong(meetingId)); + + return ApiResponse.onSuccess(response); + } } diff --git a/src/main/java/com/haru/api/domain/meeting/dto/MeetingResponseDTO.java b/src/main/java/com/haru/api/domain/meeting/dto/MeetingResponseDTO.java index 92b467d3..128db837 100644 --- a/src/main/java/com/haru/api/domain/meeting/dto/MeetingResponseDTO.java +++ b/src/main/java/com/haru/api/domain/meeting/dto/MeetingResponseDTO.java @@ -56,6 +56,14 @@ public static class proceedingDownLoadLinkResponse { private String downloadLink; } + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class proceedingVoiceLinkResponse { + private String voiceLink; + } + @Getter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/src/main/java/com/haru/api/domain/meeting/service/MeetingQueryService.java b/src/main/java/com/haru/api/domain/meeting/service/MeetingQueryService.java index d3f22508..15c0e0f2 100644 --- a/src/main/java/com/haru/api/domain/meeting/service/MeetingQueryService.java +++ b/src/main/java/com/haru/api/domain/meeting/service/MeetingQueryService.java @@ -13,4 +13,6 @@ public interface MeetingQueryService { MeetingResponseDTO.TranscriptResponse getTranscript(Long userId, Long meetingId); MeetingResponseDTO.proceedingDownLoadLinkResponse downloadMeeting(Long userId, Long meetingId); + + MeetingResponseDTO.proceedingVoiceLinkResponse MeetingVoiceFile(Long userId, Long meetingId); } diff --git a/src/main/java/com/haru/api/domain/meeting/service/MeetingQueryServiceImpl.java b/src/main/java/com/haru/api/domain/meeting/service/MeetingQueryServiceImpl.java index db26825d..9ff4ba09 100644 --- a/src/main/java/com/haru/api/domain/meeting/service/MeetingQueryServiceImpl.java +++ b/src/main/java/com/haru/api/domain/meeting/service/MeetingQueryServiceImpl.java @@ -128,4 +128,31 @@ public MeetingResponseDTO.proceedingDownLoadLinkResponse downloadMeeting(Long us .build(); } + @Override + public MeetingResponseDTO.proceedingVoiceLinkResponse MeetingVoiceFile(Long userId, Long meetingId){ + User foundUser = userRepository.findById(userId) + .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + Meeting foundMeeting = meetingRepository.findById(meetingId) + .orElseThrow(() -> new MeetingHandler(ErrorStatus.MEETING_NOT_FOUND)); + + Workspace foundWorkspace = meetingRepository.findWorkspaceByMeetingId(meetingId) + .orElseThrow(() -> new WorkspaceHandler(ErrorStatus.WORKSPACE_NOT_FOUND)); + + UserWorkspace foundUserWorkspace = userWorkspaceRepository.findByUserIdAndWorkspaceId(userId, foundWorkspace.getId()) + .orElseThrow(() -> new UserWorkspaceHandler(ErrorStatus.USER_WORKSPACE_NOT_FOUND)); + + String audioFileKeyName = foundMeeting.getAudioFileKey(); + + if (audioFileKeyName == null || audioFileKeyName.isBlank()) { + throw new MeetingHandler(ErrorStatus.MEETING_PROCEEDING_NOT_FOUND); + } + + String presignedUrl = amazonS3Manager.generatePresignedUrl(audioFileKeyName); + + return MeetingResponseDTO.proceedingVoiceLinkResponse.builder() + .voiceLink(presignedUrl) + .build(); + } + } diff --git a/src/main/java/com/haru/api/domain/user/security/googleOauth2/CustomOAuth2FailureHandler.java b/src/main/java/com/haru/api/domain/user/security/googleOauth2/CustomOAuth2FailureHandler.java index d07fe0d6..0d4077c8 100644 --- a/src/main/java/com/haru/api/domain/user/security/googleOauth2/CustomOAuth2FailureHandler.java +++ b/src/main/java/com/haru/api/domain/user/security/googleOauth2/CustomOAuth2FailureHandler.java @@ -2,6 +2,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; @@ -10,14 +11,17 @@ @Component public class CustomOAuth2FailureHandler implements AuthenticationFailureHandler { + + @Value("${google-login-frontend-url}") + String baseUrl; + @Override public void onAuthenticationFailure( HttpServletRequest request, HttpServletResponse response, AuthenticationException exception ) throws IOException { - String failureGoogleLoginUrl = "/auth/login/google/callback"; // 프론트엔드 URL로 리다이렉트 (query param 전달) - response.sendRedirect("http://localhost:3000" + failureGoogleLoginUrl + "?status=fail"); + response.sendRedirect(baseUrl + "?status=fail"); } } diff --git a/src/main/java/com/haru/api/domain/user/security/googleOauth2/CustomOAuth2SuccessHandler.java b/src/main/java/com/haru/api/domain/user/security/googleOauth2/CustomOAuth2SuccessHandler.java index b4010bbb..3635b5c0 100644 --- a/src/main/java/com/haru/api/domain/user/security/googleOauth2/CustomOAuth2SuccessHandler.java +++ b/src/main/java/com/haru/api/domain/user/security/googleOauth2/CustomOAuth2SuccessHandler.java @@ -19,6 +19,8 @@ public class CustomOAuth2SuccessHandler implements AuthenticationSuccessHandler private int accessExpTime; @Value("${jwt.refresh-expiration}") private int refreshExpTime; + @Value("${google-login-frontend-url}") + String baseUrl; private final UserCommandService userCommandService; @Override @@ -28,8 +30,6 @@ public void onAuthenticationSuccess( Authentication authentication ) throws IOException { StringBuilder redirectUrl = new StringBuilder(); - String baseUrl = "http://localhost:3000"; // 프론트엔드 URL - String successGoogleLoginUrl = "/auth/login/google/callback"; CustomOauth2UserDetails userDetails = (CustomOauth2UserDetails) authentication.getPrincipal(); // 회원가입이든 로그인이든 똑같이 프론트엔드로 리다이렉트 Long userId = userDetails.getUser().getId(); @@ -38,7 +38,6 @@ public void onAuthenticationSuccess( String refreshToken = userCommandService.generateAndSaveRefreshToken(key, refreshExpTime); // 프론트엔드 URL로 리다이렉트 (query param 전달) redirectUrl.append(baseUrl) - .append(successGoogleLoginUrl) .append("?status=success") .append("&userId=").append(userId) .append("&profileImage=").append(userDetails.getUser().getProfileImage()) diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index f5706bab..bd318086 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -48,6 +48,8 @@ invite-url: invite-url survey-url: survey-url +google-login-frontend-url: google-login-frontend-url + instagram: client: id: "dummy-client-id"