Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ API 설계 결정과 트레이드오프는 `../decisions/README.md`를 함께
| API-017 | GET | `/cover-letters/{coverLetterId}/review-versions` | Implemented | REQ-006 | 첨삭 버전 목록 |
| API-018 | GET | `/cover-letters/{coverLetterId}/review-versions/{versionId}` | Implemented | REQ-006 | 첨삭 버전 상세 |
| API-019 | PUT | `/cover-letters/{coverLetterId}/review-versions/{versionId}/final-answers` | Implemented | REQ-006 | 최종 작성본 일괄 저장 |
| API-020 | POST | `/cover-letters/{coverLetterId}/keyword-analysis` | Planned | REQ-009 | 키워드 분석 시작/재분석 |
| API-020 | POST | `/cover-letters/{coverLetterId}/keyword-analysis` | Implemented | REQ-009 | 키워드 분석 시작/재분석 |
| API-021 | GET | `/cover-letters/{coverLetterId}/keyword-analysis/latest` | Planned | REQ-009 | 최신 키워드 분석 조회 |
| API-022 | POST | `/cover-letters/{coverLetterId}/interviews` | Planned | REQ-010 | 면접 세션/질문 생성 |
| API-023 | POST | `/interview-threads/{threadId}/messages` | Planned | REQ-010 | 면접 답변 전송 |
Expand Down
4 changes: 1 addition & 3 deletions docs/api/common.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,9 +413,7 @@ MVP에서는 partial result를 서버 메모리에 저장한다. 서버 재시

`keywords`는 중요도 기준 상위 20개를 제공한다. `importance`는 1~100 범위의 정수다.

재첨삭 완료 시 기존 키워드 분석 결과는 삭제된다. 재첨삭 후에는 사용자가 다시 키워드 분석을 실행해야 최신 첨삭 기준 결과가 생성된다.

키워드 분석 결과가 있는 상태에서 사용자가 `AI 키워드 재분석`을 실행하면 같은 `KeywordAnalysis` 리소스를 `PROCESSING`으로 전환하고, 가장 최근 `ReviewVersion`을 기준으로 다시 분석한다. 성공 시 기존 키워드 결과와 `sourceReviewVersionId`를 최신 분석 결과로 덮어쓴다.
재첨삭 완료만으로 기존 키워드 분석 결과를 삭제하지 않는다. 키워드 분석 결과가 있는 상태에서 사용자가 `AI 키워드 재분석`을 실행하면 같은 `KeywordAnalysis` 리소스를 `PROCESSING`으로 전환하고, 가장 최근 `ReviewVersion`을 기준으로 다시 분석한다. 성공 시 기존 키워드 결과와 `sourceReviewVersionId`를 최신 분석 결과로 덮어쓴다.

상태:

Expand Down
4 changes: 3 additions & 1 deletion docs/api/keyword-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Request:

재분석 요청에서는 `sourceReviewVersionId`를 보내지 않는다. 서버는 항상 `CoverLetter.latestReviewVersionId`를 기준으로 다시 분석한다.

현재 구현 범위에서는 `KeywordAnalysis` persistence와 `KEYWORD_ANALYSIS` LLM Job 생성 계약을 제공한다. 실제 LLM 키워드 추출 worker와 완료 결과 저장은 후속 이슈에서 구현한다.

Response:

```json
Expand Down Expand Up @@ -74,7 +76,7 @@ Conflict Response:
GET /cover-letters/{coverLetterId}/keyword-analysis/latest
```

키워드 분석 결과가 없거나 재첨삭 후 삭제된 경우:
키워드 분석 결과가 없는 경우:

```json
{
Expand Down
2 changes: 1 addition & 1 deletion docs/api/review-versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ Response:

재첨삭 Job은 최신 `ReviewVersion`의 문항별 `finalAnswer`를 새 첨삭 입력 원본으로 사용한다. 새 버전의 `requestInstruction`에는 Job 생성 시 저장한 재첨삭 요구사항을 기록한다.

재첨삭 Job이 완료되어 새 `ReviewVersion`이 생성되면 기존 키워드 분석 결과는 삭제된다. 현재 구현 범위에서는 키워드 분석 persistence가 아직 없으므로 삭제 연결은 키워드 분석 구현 이슈에서 적용한다.
재첨삭 Job이 완료되어 새 `ReviewVersion`이 생성되어도 기존 키워드 분석 결과는 삭제하지 않는다. 최신 첨삭 버전 기준 키워드 분석이 필요하면 사용자가 `AI 키워드 재분석`을 실행해 기존 `KeywordAnalysis`를 갱신한다.

재첨삭 Job 시작 시점에는 `ReviewVersion`을 만들지 않는다. Job이 성공적으로 완료된 경우에만 새 `ReviewVersion`과 문항별 첨삭 결과를 생성한다. Job이 실패하면 새 `ReviewVersion`은 생성하지 않고, 기존 최신 버전은 그대로 유지한다.

Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ API 계약: `../api/README.md`
| Decision 025 | 삭제된 자기소개서 복구 API는 제공하지 않는다 | `cover-letters.md` | REQ-003, REQ-004, REQ-005 | API-007 - API-014 | Active |
| Decision 026 | 자기소개서당 AI 면접 세션은 하나만 유지한다 | `interviews.md` | REQ-010 | API-022 - API-023, API-025 - API-029 | Active |
| Decision 027 | 재첨삭 후에도 기존 AI 면접 세션은 유지한다 | `interviews.md` | REQ-010 | API-022 - API-023, API-025 - API-029 | Active |
| Decision 028 | 재첨삭 완료 시 기존 키워드 분석 결과를 삭제한다 | `keyword-analysis.md` | REQ-009 | API-020 - API-021 | Active |
| Decision 028 | 재첨삭 완료 시 기존 키워드 분석 결과를 유지한다 | `keyword-analysis.md` | REQ-009 | API-020 - API-021 | Active |
| Decision 029 | 키워드 분석 결과 화면에서 재분석을 제공한다 | `keyword-analysis.md` | REQ-009 | API-020 - API-021 | Active |
| Decision 030 | 면접 답변은 최대 2000자로 제한한다 | `interviews.md` | REQ-010 | API-022 - API-023, API-025 - API-029 | Active |
| Decision 031 | 면접 꼬리질문은 피드백과 같은 assistant 메시지에 저장한다 | `interviews.md` | REQ-010 | API-022 - API-023, API-025 - API-029 | Active |
Expand Down
27 changes: 13 additions & 14 deletions docs/decisions/keyword-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,13 @@ PRD는 키워드 분석을 히스토리 기능이 아니라 현재 자기소개



## Decision 028: 재첨삭 완료 시 기존 키워드 분석 결과를 삭제한다
## Decision 028: 재첨삭 완료 시 기존 키워드 분석 결과를 유지한다

### 결정

자기소개서 재첨삭이 완료되어 새 `ReviewVersion`이 생성되면, 기존 키워드 분석 결과를 삭제한다.
자기소개서 재첨삭이 완료되어 새 `ReviewVersion`이 생성되어도 기존 키워드 분석 결과는 삭제하지 않는다.

재첨삭 후 키워드 분석 화면은 다시 `키워드 분석하기` 버튼이 보이는 초기 상태가 된다. 사용자가 다시 분석을 실행하면 최신 첨삭 버전을 기준으로 새 키워드 분석 결과를 생성한다.
사용자가 `AI 키워드 재분석`을 실행하면 같은 `KeywordAnalysis` 리소스를 `PROCESSING`으로 전환하고, 가장 최근 첨삭 버전을 기준으로 다시 분석한다. 성공 시 기존 결과를 최신 분석 결과로 덮어쓴다.

### PRD 근거

Expand All @@ -128,9 +128,10 @@ PRD는 키워드 분석을 히스토리 기능이 아니라 현재 자기소개

### 고려한 대안

1. 기존 키워드 분석 결과 유지
1. 기존 키워드 분석 결과 유지 후 사용자 재분석 시 갱신
- 분석 결과는 생성 당시 `sourceReviewVersionId` 기준으로 유지된다.
- 사용자가 다시 분석하기 전까지 기존 결과를 볼 수 있지만, 최신 첨삭 기준으로 오해될 수 있다.
- 사용자가 `AI 키워드 재분석`을 실행할 때만 최신 첨삭 버전 기준으로 결과를 갱신한다.
- 사용자가 요청하지 않은 LLM 비용이 발생하지 않고, 기존 결과도 갑자기 사라지지 않는다.

2. 재첨삭 완료 시 키워드 분석 결과 삭제
- 최신 첨삭 기준이 아닌 분석 결과를 화면에서 제거한다.
Expand All @@ -142,18 +143,18 @@ PRD는 키워드 분석을 히스토리 기능이 아니라 현재 자기소개

### 선택 이유

키워드 분석은 자기소개서별 최신 결과만 유지하기로 결정했다. 재첨삭 후 이전 첨삭 버전 기준의 분석 결과를 계속 보여주면 사용자가 최신 첨삭 기준 결과로 오해할 수 있다. 따라서 재첨삭 완료 시 기존 분석 결과를 삭제하고, 사용자가 명시적으로 다시 분석하도록 한다.
키워드 분석은 사용자가 명시적으로 실행하는 LLM 작업이다. 재첨삭 완료만으로 기존 분석 결과를 삭제하면 사용자가 요청하지 않았는데 화면 상태가 사라진다. 따라서 기존 결과는 유지하고, 사용자가 `AI 키워드 재분석`을 실행할 때만 최신 첨삭 버전 기준으로 갱신한다.

### 트레이드오프

- 장점
- 최신 첨삭 버전과 맞지 않는 분석 결과를 보여주지 않는다.
- 사용자가 요청하지 않은 상태 변경이 발생하지 않는다.
- 기존 키워드 분석 결과가 갑자기 사라지지 않는다.
- 자동 LLM 비용이 발생하지 않는다.
- 화면 상태가 명확하다.

- 단점
- 재첨삭 후 사용자가 키워드 분석을 다시 실행해야 한다.
- 이전 분석 결과는 보존되지 않는다.
- 재첨삭 후 기존 분석 결과가 최신 첨삭 버전 기준이 아닐 수 있다.
- 화면에서 `sourceReviewVersionId` 또는 재분석 유도를 명확히 표현해야 할 수 있다.



Expand Down Expand Up @@ -187,7 +188,7 @@ POST /cover-letters/{coverLetterId}/keyword-analysis

2. 결과가 있으면 재분석 불가
- 결과 화면만 보여준다.
- 재첨삭으로 기존 결과가 삭제된 뒤에만 다시 분석할 수 있다.
- 기존 결과를 최신 첨삭 버전 기준으로 갱신할 수 없다.

### 선택 이유

Expand All @@ -198,11 +199,9 @@ POST /cover-letters/{coverLetterId}/keyword-analysis
- 장점
- 사용자가 명시적으로 키워드 분석을 다시 실행할 수 있다.
- API와 화면은 계속 자기소개서별 최신 결과 1개만 다룬다.
- 재첨삭 결과가 삭제된 경우에는 초기 분석 흐름을 그대로 사용할 수 있다.
- 재첨삭 완료만으로 기존 결과가 사라지지 않는다.

- 단점
- 반복 LLM 비용이 발생할 수 있다.
- 같은 첨삭 버전에서 재분석해도 결과가 달라질 수 있다.
- 이전 분석 결과는 보존되지 않는다.


3 changes: 2 additions & 1 deletion docs/erd.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,8 @@ Policy:

- 키워드 분석 결과는 자기소개서별 최신 결과만 유지한다.
- 재분석은 같은 row를 갱신한다.
- 재첨삭 완료 시 기존 키워드 분석 결과는 삭제한다.
- 재첨삭 완료만으로 기존 키워드 분석 결과를 삭제하지 않는다.
- 사용자가 재분석을 실행하면 같은 row를 최신 첨삭 버전 기준으로 갱신한다.

### keyword_analysis_keywords

Expand Down
3 changes: 2 additions & 1 deletion docs/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,8 @@ API-008 자기소개서 초안 생성의 공개 API 계약은 유지하되, 현
- 키워드 중요도는 1-100 범위의 정수다.
- 키워드 분석 결과는 자기소개서별 최신 결과만 유지한다.
- 재분석은 새 리소스를 만들지 않고 기존 최신 결과를 갱신한다.
- 재첨삭 완료 시 기존 키워드 분석 결과는 삭제한다.
- 재첨삭 완료만으로 기존 키워드 분석 결과를 삭제하지 않는다.
- 사용자가 `AI 키워드 재분석`을 실행하면 최신 첨삭 버전을 기준으로 기존 결과를 갱신한다.
- Job 실패 시 키워드 분석 리소스는 `FAILED` 상태로 저장한다.

### Acceptance Criteria
Expand Down
5 changes: 3 additions & 2 deletions docs/status.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ API 계약과 API별 상태는 `docs/api/README.md`와 `docs/api/` 하위 도메
| REQ-003 | 자기소개서 기본 CRUD | In Progress | High | API-007, API-008, API-012, API-013 | [#5](https://github.com/Rewrite-Team/Rewrite-BE/issues/5), [#18](https://github.com/Rewrite-Team/Rewrite-BE/issues/18), [#28](https://github.com/Rewrite-Team/Rewrite-BE/issues/28), [#30](https://github.com/Rewrite-Team/Rewrite-BE/issues/30), [PR #15](https://github.com/Rewrite-Team/Rewrite-BE/pull/15) | `CoverLetterServiceTest`, `CoverLetterControllerTest`, `CoverLetterRepositoryTest`, `./gradlew test`, `./gradlew check` | API-007 현재 사용자 자기소개서 목록 조회, API-008 자기소개서 초안 생성, API-013 soft delete 계약 구현됨. 진행 중 Job cancel은 `llm_jobs` 구현 이후 연결 |
| REQ-004 | 자기소개서 등록 step 저장 | Implemented | High | API-009, API-010, API-011 | [#32](https://github.com/Rewrite-Team/Rewrite-BE/issues/32), [#34](https://github.com/Rewrite-Team/Rewrite-BE/issues/34), [#36](https://github.com/Rewrite-Team/Rewrite-BE/issues/36) | `CoverLetterServiceTest`, `CoverLetterControllerTest`, `CoverLetterQuestionRepositoryTest`, `GlobalExceptionHandlerTest`, `./gradlew test`, `./gradlew check` | API-009 등록 step1 기본 정보 저장, API-010 등록 step2 채용 우대사항 저장, API-011 등록 step3 질문과 답변 저장 계약 구현됨 |
| REQ-005 | 자기소개서 제출과 LLM Job 생성 | In Progress | High | API-014, API-015, API-016 | [#38](https://github.com/Rewrite-Team/Rewrite-BE/issues/38), [#40](https://github.com/Rewrite-Team/Rewrite-BE/issues/40), [#42](https://github.com/Rewrite-Team/Rewrite-BE/issues/42), [#44](https://github.com/Rewrite-Team/Rewrite-BE/issues/44), [#48](https://github.com/Rewrite-Team/Rewrite-BE/issues/48) | `CoverLetterRepositoryTest`, `CoverLetterServiceTest`, `CoverLetterControllerTest`, `LlmJobRepositoryTest`, `LlmJobServiceTest`, `LlmJobControllerTest`, `ReviewVersionServiceTest`, `OpenAiFirstReviewClientTest`, `FirstReviewJobWorkerTest`, `FirstReviewJobEventIntegrationTest`, `./gradlew test`, `./gradlew check` | API-014 제출과 PENDING 최초 첨삭 Job 생성, API-015 Job 상태 조회, 최초 첨삭 성공 결과 완료 경계, OpenAI 최초 첨삭 client, after-commit 비동기 worker 연결 구현됨. API-016 SSE, partial result, 자동 재시도는 후속 이슈로 분리 |
| REQ-006 | 첨삭 버전 조회와 최종 작성본 저장 | In Progress | High | API-017, API-018, API-019, API-024 | [#42](https://github.com/Rewrite-Team/Rewrite-BE/issues/42), [#44](https://github.com/Rewrite-Team/Rewrite-BE/issues/44), [#48](https://github.com/Rewrite-Team/Rewrite-BE/issues/48), [#50](https://github.com/Rewrite-Team/Rewrite-BE/issues/50), [#52](https://github.com/Rewrite-Team/Rewrite-BE/issues/52), [#54](https://github.com/Rewrite-Team/Rewrite-BE/issues/54) | `ReviewVersionRepositoryTest`, `ReviewVersionServiceTest`, `ReviewVersionQueryServiceTest`, `ReviewVersionCommandServiceTest`, `ReviewVersionControllerTest`, `OpenAiFirstReviewClientTest`, `FirstReviewJobWorkerTest`, `ReReviewJobWorkerTest`, `FirstReviewJobEventIntegrationTest`, `./gradlew test`, `./gradlew check` | `ReviewVersion`, 문항별 결과 persistence, 최초 첨삭 성공 저장 경계와 OpenAI 문항별 첨삭 결과 client 및 worker 연결, API-017/018 첨삭 버전 목록/상세 조회, API-019 최종 작성본 일괄 저장, API-024 AI 첨삭 다시받기 구현됨. 키워드 분석 결과 삭제 연결은 키워드 분석 persistence 구현 이후 적용 |
| REQ-006 | 첨삭 버전 조회와 최종 작성본 저장 | In Progress | High | API-017, API-018, API-019, API-024 | [#42](https://github.com/Rewrite-Team/Rewrite-BE/issues/42), [#44](https://github.com/Rewrite-Team/Rewrite-BE/issues/44), [#48](https://github.com/Rewrite-Team/Rewrite-BE/issues/48), [#50](https://github.com/Rewrite-Team/Rewrite-BE/issues/50), [#52](https://github.com/Rewrite-Team/Rewrite-BE/issues/52), [#54](https://github.com/Rewrite-Team/Rewrite-BE/issues/54) | `ReviewVersionRepositoryTest`, `ReviewVersionServiceTest`, `ReviewVersionQueryServiceTest`, `ReviewVersionCommandServiceTest`, `ReviewVersionControllerTest`, `OpenAiFirstReviewClientTest`, `FirstReviewJobWorkerTest`, `ReReviewJobWorkerTest`, `FirstReviewJobEventIntegrationTest`, `./gradlew test`, `./gradlew check` | `ReviewVersion`, 문항별 결과 persistence, 최초 첨삭 성공 저장 경계와 OpenAI 문항별 첨삭 결과 client 및 worker 연결, API-017/018 첨삭 버전 목록/상세 조회, API-019 최종 작성본 일괄 저장, API-024 AI 첨삭 다시받기 구현됨 |
| REQ-007 | DB/JPA 전환 | Implemented | High | API-008 내부 persistence, persistence 내부 변경 | [#22](https://github.com/Rewrite-Team/Rewrite-BE/issues/22), [#24](https://github.com/Rewrite-Team/Rewrite-BE/issues/24), [#25](https://github.com/Rewrite-Team/Rewrite-BE/issues/25), [PR #26](https://github.com/Rewrite-Team/Rewrite-BE/pull/26) | `CoverLetterRepositoryTest`, `CoverLetterServiceTest`, `CoverLetterControllerTest`, `./gradlew test`, `./gradlew check` | API-008 공개 계약은 유지하고 `cover_letters` persistence를 DB/JPA로 전환. H2 file 로컬 DB, H2 in-memory 테스트 DB 사용. Flyway는 별도 이슈로 분리 |
| REQ-008 | 실제 인증 경계 | Planned | Medium | API-001 - API-006 | - | - | 카카오 OAuth, cookie, CSRF |
| REQ-009 | 키워드 분석 | Planned | Medium | API-020, API-021 | - | - | LLM Job 기반 |
| REQ-009 | 키워드 분석 | In Progress | Medium | API-020, API-021 | [#56](https://github.com/Rewrite-Team/Rewrite-BE/issues/56) | `KeywordAnalysisRepositoryTest`, `KeywordAnalysisServiceTest`, `KeywordAnalysisControllerTest`, `LlmJobRepositoryTest`, `./gradlew test`, `./gradlew check` | API-020 키워드 분석 시작/재분석 Job 생성과 `KeywordAnalysis` persistence 구현됨. API-021 최신 결과 조회, 실제 LLM 키워드 분석 worker, keyword result 저장은 후속 이슈로 분리 |
| REQ-010 | AI 면접 | Planned | Medium | API-022, API-023, API-025 - API-029 | - | - | LLM Job/SSE 기반 |

## Open Maintenance Work
Expand Down Expand Up @@ -59,6 +59,7 @@ API 계약과 API별 상태는 `docs/api/README.md`와 `docs/api/` 하위 도메

| Candidate | Related REQ | Related APIs | Suggested Scope |
|---|---|---|---|
| 최신 키워드 분석 조회 API | REQ-009 | API-021 | `KeywordAnalysis` 최신 상태와 완료 결과 조회 계약 구현 |
| LLM Job 자동 재시도 | REQ-005, REQ-006 | API-014 이후 내부 실행 | provider/output 실패 시 Decision 017 기준 1회 자동 재시도 처리 |

## Update Rules
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.daon.rewrite.keywordanalysis.controller;

import com.daon.rewrite.keywordanalysis.dto.StartKeywordAnalysisRequest;
import com.daon.rewrite.keywordanalysis.dto.StartKeywordAnalysisResponse;
import com.daon.rewrite.keywordanalysis.service.KeywordAnalysisService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class KeywordAnalysisController {

private final KeywordAnalysisService keywordAnalysisService;

@PostMapping("/cover-letters/{coverLetterId}/keyword-analysis")
public StartKeywordAnalysisResponse startKeywordAnalysis(
@PathVariable String coverLetterId,
@RequestBody(required = false) StartKeywordAnalysisRequest request
) {
return StartKeywordAnalysisResponse.from(
keywordAnalysisService.startMyKeywordAnalysis(
coverLetterId,
request == null ? null : request.sourceReviewVersionId()
)
);
}
}
Comment thread
yong203 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.daon.rewrite.keywordanalysis.dto;

public record StartKeywordAnalysisRequest(
String sourceReviewVersionId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.daon.rewrite.keywordanalysis.dto;

import com.daon.rewrite.keywordanalysis.entity.KeywordAnalysisStatus;
import com.daon.rewrite.keywordanalysis.service.StartKeywordAnalysisResult;

public record StartKeywordAnalysisResponse(
String jobId,
String keywordAnalysisId,
String coverLetterId,
KeywordAnalysisStatus status
) {

public static StartKeywordAnalysisResponse from(StartKeywordAnalysisResult result) {
return new StartKeywordAnalysisResponse(
result.job().getId(),
result.keywordAnalysis().getId(),
result.keywordAnalysis().getCoverLetter().getId(),
result.keywordAnalysis().getStatus()
);
}
}
Loading
Loading