๋ณดํธ์โํ์ ์ฐ๊ฒฐ, ํตํ ๊ธฐ๋ก ๋ถ์, ๊ฐ์ ๋ฆฌํฌํธ, ๋ฆฌ๋ง์ธ๋์ ์๋ฆผ์ ์ ๊ณตํ๋ ์น๋งค ํ์ ์ผ์ด ์๋น์ค
- GitHub : https://github.com/Alzheimer-dinger
- API ๋ฌธ์(Swagger) : https://api.alzheimerdinger.com/swagger-ui/index.html#/
ํ๋ก์ ํธ ์๊ฐ | ํ์ ๊ตฌ์ฑ | ๊ธฐ์ ์คํ | ์ ์ฅ์ยท๋ธ๋์น ์ ๋ตยท๊ตฌ์กฐ | ๊ฐ๋ฐ ๊ธฐ๊ฐยท์์ ๊ด๋ฆฌ | ์ ๊ฒฝ ์ด ๋ถ๋ถ | ํ์ด์ง๋ณ ๊ธฐ๋ฅ | ์ฃผ์ API
๋ณธ ํ๋ก์ ํธ๋ ์น๋งค ํ์์ ๋ณดํธ์๋ฅผ ์ํ AI ๋๋ฐ ์ผ์ด ์น์ฑ์ ๋๋ค. ํ์๋ ์ฑ์์ ์ธ๊ณต์ง๋ฅ๊ณผ ์ค์๊ฐ ๋ํ(์์ฑ/์๋ง)๋ก ์ผ์์ ๊ณต์ ํ๊ณ , ๋ณดํธ์๋ ์ฐ๊ฒฐ ๊ณ์ ์ ํตํด ์ฌ๋ฆฌ ์ํ์ ์ด์ ์งํ๋ฅผ ๋ชจ๋ํฐ๋งํฉ๋๋ค. ํ๋ฃจํ๋ฃจ ์ถ์ ๋๋ ๋ํยทํ๋ ๋ฐ์ดํฐ๋ฅผ ๋ถ์ํด ์ผยท์ฃผยท์ ๋จ์ ์ข ํฉ ๋ฆฌํฌํธ (๊ฐ์ ํ์๋ผ์ธ, ์ฐธ์ฌ๋, ํ๊ท ํตํ์๊ฐ, ์ํ ์งํ)๋ฅผ ์ ๊ณตํ์ฌ ์ธ์ฌํ ๋๋ด ๊ณํ ์๋ฆฝ์ ๋์ต๋๋ค.
- ์ํด๋ฆญ ํตํ(๋๊ธฐ โ ์งํ โ ์ข ๋ฃ), ์ค์๊ฐ ์๋ง/์๋ต
- RAG ๋ฉ๋ชจ๋ฆฌ๋ก ๊ฐ์ธ ๋งฅ๋ฝ ์ ์ง, ํ ํฐ ํจ์จ ์ต์ ํ
- ๋ณดํธ์โํ์ ๊ด๊ณ ๊ด๋ฆฌ(์์ฒญ/์น์ธ/ํด์ ) ๋ฐ ๋ฆฌ๋ง์ธ๋/์๋ฆผ
- PWA/FCM ๊ธฐ๋ฐ ํธ์ ์๋ฆผ, ์น ๋์๋ณด๋๋ก ๋ฆฌํฌํธ ์ด๋
- ์ด์/๋ชจ๋ํฐ๋ง: Micrometer + Prometheus + Grafana
|
์ ์ฅ์ฐ ํ ๋ฆฌ๋ ยท ๋ฐฑ์๋ ์ฃผ์ ๋๋ฉ์ธ ยท ์ธํ๋ผ ๊ตฌ์ถ |
๊น๊ฒฝ๊ท ๋ฐฑ์๋ ๋๋ฉ์ธ ยท ์ธ์ฆ/์ธ๊ฐ ์์คํ /์ธํ๋ผ ์ค๊ณ |
๋ฐ์๋ ๋ฐฑ์๋ ๋๋ฉ์ธ ยท ์ธํ๋ผ ๊ตฌ์ถ ยท CI/CD ยท ๋ชจ๋ํฐ๋ง |
๋ ธ์์ ํ๋ก ํธ UI/UX ยท ํตํ WebSocket ยท CD ยท FCM |
|
๊นํจ์ ํ๋ก ํธ UI/UX ยท API ์ฐ๋ ยท ์ํ๊ด๋ฆฌ |
์ํ๊ต AI ์์ด๋์ด ยท RAG ๋ฉ๋ชจ๋ฆฌ ยท ๋ถ์ ๋ฆฌํฌํธ |
๊ฐ๋ฏผ์ฌ AI ์ค์๊ฐ ํตํ ยท ๊ฐ์ ๋ถ์ยท์์ฝ |
์ ์ฒด ์คํ์ 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-url | GCS 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
mainโ ๋ฐฐํฌ์ฉ ์์ ๋ธ๋์น. ํ๊น (vX.Y.Z) ํ ๋ฐฐํฌ.developโ ํตํฉ ๊ฐ๋ฐ ๋ธ๋์น. ๊ธฐ๋ฅ/๋ฒ๊ทธ ํฝ์ค ๋จธ์ง ๋์.feature/<scope>-<short-desc>โ ๊ธฐ๋ฅ ๋จ์ ์์ . ์๋ฃ ์ PR โdevelop.hotfix/<issue>โ ๊ธด๊ธ ์์ . PR โmain๋ฐdevelop์์ชฝ ๋ฐ์.release/<version>โ ๋ฆด๋ฆฌ์ฆ ์ค๋น(๋ฒ์ , ๋ฌธ์, ๋ง์ด๊ทธ๋ ์ด์ ) ํmain๋ณํฉ.
- PR ํ ํ๋ฆฟ ์ฌ์ฉ: ๋ฐฐ๊ฒฝ/๋ณ๊ฒฝ์ /ํ ์คํธ/์คํฌ๋ฆฐ์ท/์ฒดํฌ๋ฆฌ์คํธ ํฌํจ
- ๋ฆฌ๋ทฐ 1๋ช ์ด์ ์น์ธ(๐ฆ ์ต์ 1 Approve), CI ํต๊ณผ ํ์
- ๋ผ๋ฒจ:
feature,fix,refactor๋ฑ
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 ๋๋ฉ์ธ์ ๋ํ ๊ตฌ์ฑ์์๋ฅผ ๊ฐ๋จํ ์์ฝํ ์์์
๋๋ค.
์ ์ฒด ์ฝ๋๋ ๋ ํฌ์งํ ๋ฆฌ์์ ํ์ธํ์ธ์.
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 -> ProfileResponse.create(u, imageService.getProfileImageUrl(u)))
.orElseThrow(() -> 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<Void> signUp(@Valid @RequestBody SignUpRequest req) {
userAuthUseCase.signUp(req);
return BaseResponse.onSuccess();
}
@PostMapping("/login")
public BaseResponse<LoginResponse> login(@Valid @RequestBody LoginRequest req) {
return BaseResponse.onSuccess(userAuthUseCase.login(req));
}
@DeleteMapping("/logout")
public BaseResponse<Void> 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 ๋น๋/ํ ์คํธ/๋ฆฌํฌํธ, ๋ฆฐํธยทํฌ๋งทยทํ์ ์ฒดํฌ
ํ์์ AI๊ฐ ์์ฑ์ผ๋ก ๋ํํ๊ณ , ์ค์๊ฐ ์๋ง์ ์ ๊ณตํ๋ ํตํ ๊ธฐ๋ฅ์ ๊ตฌํํ์ต๋๋ค. ํตํ ์ /์ค/ํ ์ํ๋ฅผ ๋ช ํํ ๋ถ๋ฆฌํ๊ณ , ์ค๋์ค ์คํธ๋ฆผ ์ฒ๋ฆฌ์ ์คํธ๋ฆฌ๋ฐ ์๋ต์ ์์ ์ ์ผ๋ก ์ฐ๊ฒฐํฉ๋๋ค.
CallWaiting โ CallActive โ CallEnd (์ข
๋ฃ ํ ์์ฝ/์ ์ฅ)
- CallWaiting: ์ฅ์น/๊ถํ ์ฒดํฌ(๋ง์ดํฌ), ์๋ฒ ์ฐ๊ฒฐ ์ค๋น, ์ํ ํ์
- CallActive: ์ค์๊ฐ ์๋ง(๋ถ๋ถ/์ต์ข ), ๋ฐํ/์๋ต ํ์๋ผ์ธ, ์์๊ฑฐ/์ข ๋ฃ ๋ฒํผ
- CallEnd: ํตํ ์์ฝ ๋ ธ์ถ, ์ ์ฅ/์ดํ ๋์ ๋ถ๊ธฐ
useAudioStreamํ ์ผ๋ก ๋ฐํ ๊ฐ์ง(VAD) ๋ฐ ๋ง์ดํฌ ์คํธ๋ฆผ ์์งWebAudio/MediaDevicesAPI ์ฌ์ฉ, ์ ๋ ฅ ๋ ๋ฒจ ๋ชจ๋ํฐ๋ง ๋ฐ ์ผ์์ ์ง/์ฌ๊ฐ- ์ํ๋ ์ดํธ/์ฑ๋ ์ ๊ทํ โ ๋คํธ์ํฌ ์ ์ก ํฌ๋งท์ผ๋ก ์ธ์ฝ๋ฉ(์คํธ๋ฆฌ๋ฐ)
- WebSocket ๊ธฐ๋ฐ ์๋ฐฉํฅ ์คํธ๋ฆฌ๋ฐ: ์ค๋์ค ์ ์คํธ๋ฆผ, ์๋ง/์ค๋์ค ๋ค์ด์คํธ๋ฆผ
- ๋ถ๋ถ/์ต์ข ์๋ง ๊ตฌ๋ถ ๋ ๋๋ง(๋ถ๋ถ ๊ฐฑ์ โ ์ต์ข ํ์ )
- ์ฐ๊ฒฐ ์ ๋ขฐ์ฑ: ํ/ํ ํฌ์ค์ฒดํฌ, ์ง์์ ์ฌ์๋, ์ผ์ ๋คํธ์ํฌ ๋จ์ ๋ณต๊ตฌ
- ์๋ฌ/์์ธ ์ฒ๋ฆฌ: ์ธ์ฆ ์ค๋ฅ, ์ฅ์น ์ ๊ทผ ์คํจ, ๋ชจ๋ธ ๊ณผ๋ถํ ์ ์ฌ์ฉ์ ๊ฐ์ด๋
- ๋ฆฌ์์ค ์ ๋ฆฌ: ํธ๋ stop, ์์ผ close, ๋ฉ๋ชจ๋ฆฌ ํด์ (์ข ๋ฃ/์ดํ ์)
์ผ๊ฐ/๊ธฐ๊ฐ ์ข ํฉ ๊ด์ ์์ ๊ฐ์ ๋ฐ ์ด์ฉ ์งํ๋ฅผ ์๊ฐํํฉ๋๋ค. ๋ ์ง/๊ธฐ๊ฐ ์ ํ์ ๋ฐ๋ผ API ํ๋ผ๋ฏธํฐ๋ฅผ ๊ตฌ์ฑํ๊ณ , ์ ์ฒ๋ฆฌ๋ ๋ฐ์ดํฐ๋ก ๊ทธ๋ํ/์งํ ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํฉ๋๋ค.
- ๋ ์ง ์ ํ + ์๊ฐ ์ด๋ชจ์ง ์บ๋ฆฐ๋๋ก ํ๋ฃจ ํ๋ฆ ๋น ๋ฅธ ํ์
- ๊ฐ์ ๊ณ์ฐ ๋ก์ง: ๋ํ ๋ก๊ทธ ๊ธฐ๋ฐ ์ ์ ์ฐ์ถ(ํ๋ณต/์ฌํ/๋ถ๋ ธ/๋๋/๊ถํ ๋ฑ)
- ์ํ ์ค์ฝ์ด ๊ฒ์ด์ง๋ก ๋น์ผ ์ํ๋ฅผ ์ง๊ด์ ์ผ๋ก ํํ
- ๊ธฐ๊ฐ ์ ํ: 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~)๊ณผ ์ฌ์ ์ ์ฅ, ์ด์ ๊ฐ์ ์ ํ์ฉ