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 @@ -59,7 +59,7 @@ API 설계 결정과 트레이드오프는 `../decisions/README.md`를 함께
| API-008 | POST | `/cover-letters` | Implemented | REQ-003 | 자기소개서 초안 생성 |
| API-009 | PUT | `/cover-letters/{coverLetterId}/basic-info` | Implemented | REQ-004 | 등록 step1 저장 |
| API-010 | PUT | `/cover-letters/{coverLetterId}/preferences` | Implemented | REQ-004 | 등록 step2 저장 |
| API-011 | PUT | `/cover-letters/{coverLetterId}/questions` | Planned | REQ-004 | 등록 step3 저장 |
| API-011 | PUT | `/cover-letters/{coverLetterId}/questions` | Implemented | REQ-004 | 등록 step3 저장 |
| API-012 | GET | `/cover-letters/{coverLetterId}` | Planned | REQ-003 | 자기소개서 상세 |
| API-013 | DELETE | `/cover-letters/{coverLetterId}` | Implemented | REQ-003 | 자기소개서 soft delete |
| API-014 | POST | `/cover-letters/{coverLetterId}/submit` | Planned | REQ-005 | 첨삭 Job 생성 |
Expand Down
37 changes: 37 additions & 0 deletions docs/api/cover-letters.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,43 @@ Response:
}
```

Validation Error Response:

```json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "요청 값이 올바르지 않습니다.",
"details": [
Comment thread
yong203 marked this conversation as resolved.
{
"field": "questions[0].question",
"reason": "질문을 입력해야 합니다."
},
{
"field": "questions[0].maxAnswerLength",
"reason": "최대 답변 글자 수는 100자 이상 5000자 이하여야 합니다."
},
{
"field": "questions[0].originalAnswer",
"reason": "답변을 입력해야 합니다."
}
]
}
}
```

Conflict Response:

```json
{
"error": {
"code": "COVER_LETTER_NOT_DRAFT",
"message": "제출 이후에는 원본 자기소개서를 수정할 수 없습니다.",
"details": []
}
}
```

### 자기소개서 상세 조회

등록 step4 확인 화면과 AI 첨삭 페이지 상단 정보에서 사용한다.
Expand Down
4 changes: 2 additions & 2 deletions docs/status.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ API 계약과 API별 상태는 `docs/api/README.md`와 `docs/api/` 하위 도메
| REQ-001 | 공통 예외 응답 기반 | Verified | High | 공통 에러 응답 | [#2](https://github.com/Rewrite-Team/Rewrite-BE/issues/2), [PR #3](https://github.com/Rewrite-Team/Rewrite-BE/pull/3) | `GlobalExceptionHandlerTest`, `./gradlew test` | `BusinessException`, `ErrorCode`, `GlobalExceptionHandler`, `ErrorResponse` 구현됨 |
| REQ-002 | 개발용 현재 사용자 Provider | Verified | High | 인증 필요 API 공통 | [#4](https://github.com/Rewrite-Team/Rewrite-BE/issues/4), [PR #6](https://github.com/Rewrite-Team/Rewrite-BE/pull/6) | `DevCurrentUserProviderTest`, `./gradlew test` | `CurrentUserProvider`, `DevCurrentUserProvider` 구현됨 |
| 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 저장 | In Progress | 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) | `CoverLetterServiceTest`, `CoverLetterControllerTest`, `GlobalExceptionHandlerTest`, `./gradlew test`, `./gradlew check` | API-009 등록 step1 기본 정보 저장, API-010 등록 step2 채용 우대사항 저장 계약 구현됨. API-011은 후속 slice 후보 기준으로 분리 진행 |
| 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 생성 | Planned | High | API-014, API-015, API-016 | - | - | LLM provider 호출 전 skeleton 우선 |
| REQ-006 | 첨삭 버전 조회와 최종 작성본 저장 | Planned | High | API-017, API-018, API-019, API-024 | - | - | 제출/Job skeleton 이후 진행 권장 |
| 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는 별도 이슈로 분리 |
Expand Down Expand Up @@ -59,7 +59,7 @@ API 계약과 API별 상태는 `docs/api/README.md`와 `docs/api/` 하위 도메
| Candidate | Related REQ | Related APIs | Suggested Scope |
|---|---|---|---|
| 자기소개서 상세 조회 | REQ-003 | API-012 | DB/JPA repository 기반 owner 검증, deletedAt 제외, 질문/최신 Job 요약 포함 skeleton, service/web/repository test |
| 등록 step3 저장 | REQ-004 | API-011 | questions replace, order 재부여, 글자 수 검증, service/web test |
| 자기소개서 제출과 LLM Job skeleton | REQ-005 | API-014, API-015 | 필수 step 데이터 검증, cover letter 상태 전환, LLM job persistence skeleton, service/web/repository test |

## Update Rules

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
import com.daon.rewrite.coverletter.dto.SaveBasicInfoResponse;
import com.daon.rewrite.coverletter.dto.SavePreferencesRequest;
import com.daon.rewrite.coverletter.dto.SavePreferencesResponse;
import com.daon.rewrite.coverletter.dto.SaveQuestionsRequest;
import com.daon.rewrite.coverletter.dto.SaveQuestionsResponse;
import com.daon.rewrite.coverletter.entity.CoverLetter;
import com.daon.rewrite.coverletter.entity.CoverLetterStatus;
import com.daon.rewrite.coverletter.service.CoverLetterService;
import com.daon.rewrite.coverletter.service.SaveQuestionsResult;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -76,4 +79,16 @@ public SavePreferencesResponse savePreferences(
);
return SavePreferencesResponse.from(result);
}

@PutMapping("/cover-letters/{coverLetterId}/questions")
public SaveQuestionsResponse saveQuestions(
@PathVariable String coverLetterId,
@RequestBody SaveQuestionsRequest request
) {
SaveQuestionsResult result = coverLetterService.saveQuestions(
coverLetterId,
request.toInputs()
);
return SaveQuestionsResponse.from(result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.daon.rewrite.coverletter.dto;

import com.daon.rewrite.coverletter.service.SaveQuestionInput;

import java.util.List;

public record SaveQuestionsRequest(
List<QuestionRequest> questions
) {

public List<SaveQuestionInput> toInputs() {
if (questions == null) {
return null;
}
return questions.stream()
.map(question -> question == null
? null
: new SaveQuestionInput(
question.question(),
question.maxAnswerLength(),
question.originalAnswer()
))
.toList();
}

public record QuestionRequest(
String question,
Integer maxAnswerLength,
String originalAnswer
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.daon.rewrite.coverletter.dto;

import com.daon.rewrite.coverletter.entity.CoverLetterQuestion;
import com.daon.rewrite.coverletter.service.SaveQuestionsResult;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;

public record SaveQuestionsResponse(
String coverLetterId,
List<QuestionResponse> questions,
LocalDateTime updatedAt
) {
private static final ZoneId API_ZONE = ZoneId.of("Asia/Seoul");

public static SaveQuestionsResponse from(SaveQuestionsResult result) {
return new SaveQuestionsResponse(
result.coverLetter().getId(),
result.questions().stream()
.map(QuestionResponse::from)
.toList(),
LocalDateTime.ofInstant(result.coverLetter().getUpdatedAt(), API_ZONE)
);
}

public record QuestionResponse(
String id,
int order,
String question,
int maxAnswerLength,
String originalAnswer
) {
private static QuestionResponse from(CoverLetterQuestion question) {
return new QuestionResponse(
question.getId(),
question.getQuestionOrder(),
question.getQuestion(),
question.getMaxAnswerLength(),
question.getOriginalAnswer()
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ public void fillPreferences(String preferences, Instant now) {
this.updatedAt = now;
}

public void touch(Instant now) {
this.updatedAt = now;
}

public void markDeleted(Instant now) {
this.deletedAt = now;
this.updatedAt = now;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.daon.rewrite.coverletter.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "cover_letter_questions")
public class CoverLetterQuestion {

@Id
@Column(name = "id", nullable = false, length = 64)
private String id;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "cover_letter_id", nullable = false)
private CoverLetter coverLetter;

@Column(name = "question_order", nullable = false)
private int questionOrder;

@Column(name = "question", nullable = false, length = 300)
private String question;

@Column(name = "max_answer_length", nullable = false)
private int maxAnswerLength;

@Column(name = "original_answer", nullable = false, columnDefinition = "text")
private String originalAnswer;

private CoverLetterQuestion(
String id,
CoverLetter coverLetter,
int questionOrder,
String question,
int maxAnswerLength,
String originalAnswer
) {
this.id = id;
this.coverLetter = coverLetter;
this.questionOrder = questionOrder;
this.question = question;
this.maxAnswerLength = maxAnswerLength;
this.originalAnswer = originalAnswer;
}

public static CoverLetterQuestion create(
String id,
CoverLetter coverLetter,
int questionOrder,
String question,
int maxAnswerLength,
String originalAnswer
) {
return new CoverLetterQuestion(
id,
coverLetter,
questionOrder,
question,
maxAnswerLength,
originalAnswer
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.daon.rewrite.coverletter.repository;

import com.daon.rewrite.coverletter.entity.CoverLetter;
import com.daon.rewrite.coverletter.entity.CoverLetterQuestion;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface CoverLetterQuestionRepository extends JpaRepository<CoverLetterQuestion, String> {

List<CoverLetterQuestion> findByCoverLetterIdOrderByQuestionOrderAsc(String coverLetterId);

void deleteByCoverLetter(CoverLetter coverLetter);
}
Loading
Loading