Skip to content

Latest commit

ย 

History

History
848 lines (740 loc) ยท 35.3 KB

File metadata and controls

848 lines (740 loc) ยท 35.3 KB

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 โ†’ main ๋ฐ develop ์–‘์ชฝ ๋ฐ˜์˜.
  • 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 ํ๋ฆ„

CallWaiting โ†’ CallActive โ†’ CallEnd (์ข…๋ฃŒ ํ›„ ์š”์•ฝ/์ €์žฅ)

  • 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~)๊ณผ ์‚ฌ์œ  ์ €์žฅ, ์šด์˜ ๊ฐœ์„ ์— ํ™œ์šฉ

๋งจ ์œ„๋กœ โคด